BuyEngine is a .NET library designed to provide a simple, but fully-featured, e-commerce capability to new or existing .NET projects using patterns and technologies that are already familiar to .NET developers
Plenty of turnkey e-commerce solutions exist across languages and stacks. But rather than reinventing the wheel building e-commerce into an existing app, or fighting an e-commerce product's plugin/customization model to bend it to your needs, BuyEngine takes a different approach: use your existing toolset and workflow. No custom templating language, no custom execution or hosting model — just a library you add to a normal .NET project.
Version 0.1 - Basic Product Management functionality (Products, Suppliers, Brands)Version 0.2 - Cart ManagementVersion 0.3 - SQL Data Provider, Testing ImprovementsVersion 0.4 - Product Browse By Brand, SupplierVersion 0.5 - Payment Processing - Stripe, Paypal, and SquareVersion 0.6 - Shipping Providers - Multi-carrier seam (Shippo), Store PickupVersion 0.7 - Additional Relational Data Providers - SQLite, PostgreSQL (self-hosted, Amazon Aurora, Azure Database for PostgreSQL)- Version 0.8 - Additional Data Providers (MongoDb, CosomoDb)
- Version 0.9 - API Security and documentation
- L10N
- Tax Calculators
AddBuyEngine is the front door for wiring BuyEngine into a host's IServiceCollection. It registers Catalog + Checkout with their sensible defaults (EF InMemory, store-pickup shipping, PayAtStorePaymentProvider), then hands you a BuyEngineBuilder to opt into providers from add-on packages:
services.AddBuyEngine(cfg =>
{
cfg.UseSqlServer(sql => sql.ConnectionStringName = "BuyEngine"); // BuyEngine.Data.Sql
cfg.UseStripeForPayment(pay => pay.ApiKeyName = "Stripe:ApiKey"); // BuyEngine.Payment.Stripe
cfg.UseShippoForShipping(s => s.ApiKeyName = "Shippo:ApiKey"); // BuyEngine.Shipping.Shippo
cfg.UseStorePickupShipping(p => p.DefaultMethodName = "Curbside"); // stacks alongside any carrier
cfg.UseWebApi(web => web.HostPrefix = "be-api"); // BuyEngine.WebApi
});
// ...
app.MapBuyEngineWebApi(); // reads the HostPrefix configured aboveEach Use* method lives in the package it configures (core has no reference to BuyEngine.Data.Sql, .WebApi, any payment package, or any shipping package) and is an extension on BuyEngineBuilder, so only the packages you reference show up as options. Use*ForPayment and Use*ForShipping methods are each mutually exclusive within their own kind - configuring a second payment provider throws, and so does configuring a second shipping carrier/aggregator (e.g. Shippo and FedEx together), matching the "one provider wins" behavior of the underlying registrations. UseStorePickupShipping is the one exception: it never claims the shipping slot, so it always composes alongside whichever carrier is configured (or stands alone as the zero-config default) - a shopper can be offered both "UPS Ground via Shippo" and "In-Store Pickup" on the same order.
*Name-suffixed options (ConnectionStringName, ApiKeyName, ...) are resolved from IConfiguration at first use, so the host can add configuration sources after calling AddBuyEngine. The plain-value options (ConnectionString, ApiKey, ...) are used verbatim - set one or the other, never both.
The original à-la-carte methods (AddCatalogServices(), AddCheckoutServices(), AddSqlDataServices(...), AddStripePaymentServices(...), AddBuyEngineWebApiServices(), ...) remain available for hosts that don't want the builder.
A store manager curates which products are offered alongside another product - an Upsell (a better version of what the shopper is looking at) or a CrossSell (something that goes with it). These are never returned inside the product payload: a shopper's product page reads them with a second call, so product reads stay cheap and a storefront decides for itself whether to ask.
GET products/product/{productId}/upsells the enabled upsell Products, in sort order
GET products/product/{productId}/cross-sells the enabled cross-sell Products, in sort order
GET products/product/{productId}/recommendations every link, enabled or not (store manager view)
POST products/product/{productId}/recommendations { recommendedProductId, type, sortOrder, enabled }
DELETE products/product/{productId}/recommendations/{recommendationId}
type crosses the wire as "Upsell" / "CrossSell". The same pair of products can be linked once per type, a product cannot recommend itself, and a recommendation whose target product is disabled is dropped from the shopper-facing calls. IProductRecommendationService is the equivalent in-process surface.
Every provider below registers the same ICatalogDbContext/ICheckoutDbContext surface, splitting a single connection string into a .Catalog and a .Checkout database - see each package's Use* extension for the exact options.
BuyEngine.Data.Sql- SQL Server, viacfg.UseSqlServer(...).BuyEngine.Data.Sqlite- SQLite, viacfg.UseSqlite(sqlite => sqlite.DataSource = "buyengine.db"). No server to stand up - good for evaluating BuyEngine, single-node deployments, or tests that need a real relational engine without Docker.BuyEngine.Data.PostgreSql- PostgreSQL, viacfg.UsePostgreSql(pg => pg.ConnectionStringName = "BuyEngine"). Works unchanged against self-hosted PostgreSQL, Amazon Aurora PostgreSQL, and Azure Database for PostgreSQL (Flexible Server) - those are all wire-compatible Postgres, so nothing beyond the connection string differs:- Aurora's writer/reader failover:
Host=writer.cluster-xxx.rds.amazonaws.com,reader.cluster-xxx.rds.amazonaws.com;Target Session Attributes=primary;... - Azure's required TLS:
Host=<server>.postgres.database.azure.com;Ssl Mode=VerifyFull;... PostgreSqlOptions.EnableRetryOnFailure(on by default) andMaxRetryCountabsorb the transient network blips both managed services occasionally produce.- Token-based auth (Azure Entra ID, AWS IAM) isn't a static connection-string option - use the two-delegate
AddPostgreSqlDataServices(configureCatalog, configureCheckout)overload and build theNpgsqlDataSource/connection yourself inside those callbacks.
- Aurora's writer/reader failover:
Each provider owns a full, independent copy of the EF model (DbContexts + IEntityTypeConfigurations) rather than sharing one - see each project's CLAUDE.md notes for why. Product.RowVersion-based optimistic concurrency is implemented per provider's native mechanism: SQL Server's rowversion column, a SaveChanges-time stamp for SQLite, and PostgreSQL's xmin system column.