Factories Reference¶
Merchello uses the Factory pattern to centralize all domain object creation. Instead of scattering new Entity { ... } calls throughout your services and controllers, every entity is created through a dedicated factory class. This keeps construction logic in one place, makes it easier to refactor, and ensures that required defaults (like sequential GUIDs and UTC timestamps) are always applied.
Why Factories?¶
If you have ever tracked down a bug where a DateCreated was set to DateTime.Now instead of DateTime.UtcNow, or an Id was accidentally left as Guid.Empty, you already know why factories matter.
By routing every creation through a factory, you get:
- Consistent defaults -- sequential GUIDs, UTC timestamps, and currency rounding are applied automatically.
- One place to change -- when a new required field is added to a model, you update one factory method, not twenty call sites.
- Testability -- factories can be injected and mocked in unit tests.
Architecture Rule¶
Warning: Never create domain entities with
new Entity { ... }directly in controllers or services. Always use the appropriate factory.
Factories live alongside their feature in the Factories/ folder:
Merchello.Core/
Accounting/Factories/
InvoiceFactory.cs
LineItemFactory.cs
OrderFactory.cs
TaxGroupFactory.cs
Checkout/Factories/
BasketFactory.cs
Customers/Factories/
CustomerFactory.cs
CustomerSegmentFactory.cs
DigitalProducts/Factories/
DownloadLinkFactory.cs
Discounts/Factories/
DiscountFactory.cs
Locality/Factories/
AddressFactory.cs
Payments/Factories/
PaymentFactory.cs
SavedPaymentMethodFactory.cs
Products/Factories/
ProductFactory.cs
ProductRootFactory.cs
ProductOptionFactory.cs
ProductCollectionFactory.cs
ProductFilterFactory.cs
ProductFilterGroupFactory.cs
ProductTypeFactory.cs
ProductFeeds/Factories/
ProductFeedFactory.cs
ProductSync/Factories/
ProductSyncIssueFactory.cs
ProductSyncRunFactory.cs
Shipping/Factories/
ShipmentFactory.cs
ShippingOptionFactory.cs
Suppliers/Factories/
SupplierFactory.cs
Upsells/Factories/
UpsellFactory.cs
Warehouses/Factories/
WarehouseFactory.cs
Factories are registered in the DI container and injected into services via constructor injection. Some factories accept their own dependencies (e.g., ICurrencyService for rounding).
Accounting Factories¶
InvoiceFactory¶
Creates Invoice instances. Requires ICurrencyService for looking up currency symbols.
Key methods:
| Method | What it does |
|---|---|
CreateFromBasket(basket, invoiceNumber, billingAddress, shippingAddress, presentmentCurrency, storeCurrency, customerId, source?, hasAccountTerms, paymentTermsDays?, purchaseOrder?) |
Creates an invoice from a checkout basket. Captures billing/shipping addresses, currency codes, payment terms, and purchase order numbers. Default source is InvoiceSources.Web. |
CreateManual(invoiceNumber, customerId, billingAddress, shippingAddress, currencyCode, subTotal, tax, total, authorName?, authorId?, hasAccountTerms, paymentTermsDays?) |
Creates a manual invoice for admin-created orders. Rounds amounts using the currency service and adds a system note. Source is InvoiceSources.Draft. |
// Creating an invoice from a basket during checkout
var invoice = invoiceFactory.CreateFromBasket(
basket: basket,
invoiceNumber: "INV-1001",
billingAddress: billingAddress,
shippingAddress: shippingAddress,
presentmentCurrency: "GBP",
storeCurrency: "USD",
customerId: customerId);
Note: The factory automatically generates a sequential GUID for the
Id, setsDateCreatedandDateUpdatedtoDateTime.UtcNow, and looks up the currency symbol from the currency service.
LineItemFactory¶
The most feature-rich factory in Merchello. Creates different types of LineItem for every stage of the order lifecycle.
Key methods:
| Method | Purpose |
|---|---|
CreateFromProduct(product, quantity) |
Product line item for a basket. Pulls tax rate and tax group from the product root. |
CreateAutoAddProductLineItem(...) |
Product line item for auto-add upsell scenarios. |
CreateAddonForBasket(...) |
Add-on line item (e.g., gift wrapping) linked to a parent product via DependantLineItemSku. |
CreateShippingLineItem(name, amount) |
Shipping cost line item. |
CreateForOrder(basketLineItem, quantity, amount, cost) |
Copies a basket line item to an order, with allocated quantity for multi-warehouse splits. |
CreateAddonForOrder(addonItem, quantity, amount) |
Copies an add-on to an order. Extracts CostAdjustment from extended data. |
CreateDiscountForOrder(...) |
Scales a discount proportionally when items are split across multiple orders. |
CreateForShipment(source, quantity) |
Copies an order line item to a shipment (partial shipment support). |
CreateDiscountLineItem(...) |
General-purpose discount line item. |
CreateCustomLineItem(...) |
Custom line item for manual orders. |
CreateProductForOrderEdit(...) |
Product line item for order edit operations. |
CreateAddonForOrderEdit(...) |
Add-on line item for order edit operations. |
// Creating a product line item from a product
var lineItem = lineItemFactory.CreateFromProduct(product, quantity: 2);
// Creating a shipping line item
var shippingLine = lineItemFactory.CreateShippingLineItem("Standard Delivery", 5.99m);
Tip: The factory handles
JsonElementunwrapping internally when reading extended data values. You do not need to callUnwrapJsonElement()when using factory methods.
OrderFactory¶
Creates Order instances linked to an invoice and warehouse.
var order = orderFactory.Create(
invoiceId: invoice.Id,
warehouseId: warehouseId,
shippingOptionId: shippingOptionId,
shippingCost: 5.99m);
TaxGroupFactory¶
Creates TaxGroup instances with a name and tax percentage.
Checkout Factories¶
BasketFactory¶
Creates Basket instances for the checkout flow.
var basket = basketFactory.Create(
customerId: customerId, // null for guest checkout
currencyCode: "GBP",
currencySymbol: "\u00a3");
Product Factories¶
ProductRootFactory¶
Creates ProductRoot instances -- the parent entity that holds shared configuration like tax group, product type, and options.
// Simple creation with options
var root = productRootFactory.Create(
name: "T-Shirt",
taxGroup: standardRate,
productType: clothingType,
productOptions: sizeAndColorOptions);
// Full creation with URL, collections, and images
var root = productRootFactory.Create(
name: "T-Shirt",
rootUrl: "t-shirt",
taxGroup: standardRate,
productType: clothingType,
collections: [casualCollection],
isDigitalProduct: false,
rootImages: ["img/tshirt-hero.jpg"]);
ProductFactory¶
Creates Product instances (variants) linked to a product root. Requires SlugHelper for URL generation.
var product = productFactory.Create(
productRoot: root,
name: "T-Shirt - Blue / Large",
price: 24.99m,
costOfGoods: 8.50m,
gtin: "1234567890123",
sku: "TSH-BLU-LG",
isDefault: true,
variantOptionsKey: "blue-large");
ProductOptionFactory¶
Creates empty ProductOption and ProductOptionValue instances for update scenarios where properties are set later.
var option = productOptionFactory.CreateEmpty();
var value = productOptionFactory.CreateEmptyValue();
ProductCollectionFactory¶
Creates ProductCollection instances for grouping products.
ProductFilterFactory and ProductFilterGroupFactory¶
Create filter entities for product browsing.
var group = productFilterGroupFactory.Create("Colour", sortOrder: 1);
var filter = productFilterFactory.Create(
name: "Red",
filterGroupId: group.Id,
hexColour: "#FF0000");
ProductTypeFactory¶
Creates ProductType instances.
Customer Factories¶
CustomerFactory¶
Creates Customer instances from email (minimal checkout creation) or from explicit parameters.
// Minimal creation during checkout
var customer = customerFactory.CreateFromEmail(
email: "jane@example.com",
billingAddress: billingAddress,
acceptsMarketing: true);
// Explicit creation with all fields
var customer = customerFactory.Create(new CreateCustomerParameters
{
Email = "jane@example.com",
FirstName = "Jane",
LastName = "Doe",
MemberKey = memberKey
});
Tip:
CreateFromEmailautomatically extracts first and last names from the billing addressNamefield by splitting on the first space.
CustomerSegmentFactory¶
Creates CustomerSegment and CustomerSegmentMember instances.
var segment = customerSegmentFactory.Create(new CreateSegmentParameters
{
Name = "VIP Customers",
SegmentType = SegmentType.Manual,
CreatedBy = adminUserId
});
var member = customerSegmentFactory.CreateMember(
segmentId: segment.Id,
customerId: customerId,
addedBy: adminUserId,
notes: "Top spender last quarter");
Discount Factories¶
DiscountFactory¶
Creates Discount instances and all their related configuration objects (target rules, eligibility rules, buy-X-get-Y configs, and free shipping configs).
var discount = discountFactory.Create(new CreateDiscountParameters
{
Name = "Summer Sale 20%",
Category = DiscountCategory.Product,
Method = DiscountMethod.Automatic,
ValueType = DiscountValueType.Percentage,
Value = 20m,
StartsAt = DateTime.UtcNow,
EndsAt = DateTime.UtcNow.AddDays(30)
});
// Create a target rule
var targetRule = discountFactory.CreateTargetRule(
targetType: DiscountTargetType.Collection,
targetIds: [summerCollectionId]);
// Create an eligibility rule
var eligibilityRule = discountFactory.CreateEligibilityRule(
eligibilityType: DiscountEligibilityType.CustomerSegment,
eligibilityIds: [vipSegmentId]);
Note: The factory automatically determines the initial status based on scheduling. If
StartsAtis in the future, the discount is created withDiscountStatus.Scheduled. Otherwise, it isDiscountStatus.Active.
Creating Your Own Factories¶
If you are building a Merchello extension that introduces new domain entities, follow the same pattern:
- Create a factory class in your feature's
Factories/folder. - Accept any required services (like
ICurrencyService) via a primary constructor. - Use
GuidExtensions.NewSequentialGuidfor entity IDs. - Always set
DateCreatedandDateUpdatedtoDateTime.UtcNow. - Register the factory in DI and inject it where needed.
public class MyWidgetFactory(ICurrencyService currencyService)
{
public Widget Create(string name, decimal price, string currencyCode)
{
return new Widget
{
Id = GuidExtensions.NewSequentialGuid,
Name = name,
Price = currencyService.Round(price, currencyCode),
DateCreated = DateTime.UtcNow,
DateUpdated = DateTime.UtcNow
};
}
}