Friday, 1 August 2025

Dynamics-365 Commerce Implementation

 This project provides comprehensive assets and guidance for extending and integrating Dynamics 365 Commerce. It demonstrates how to build custom user interfaces (like React-based POS UI extensions) and connect external services (such as local AI models or payment gateways). The core Headless Commerce Engine acts as a unified API, supported by extensibility points and SDKs for tailoring business logic and managing standardized data models.

Visual Overview



Chapter 1: Commerce Domain Data Models

Welcome to the first chapter of our Dynamics 365 FastTrack tutorial! If you're new to Dynamics 365 Commerce or just getting started with its technical aspects, you're in the right place. In this chapter, we'll demystify a core concept: Commerce Domain Data Models.

Imagine you're running an online store. You need to keep track of what you sell, what customers put in their shopping baskets, and what purchases they've made. How does Dynamics 365 Commerce, a powerful and complex system, understand all this information? It uses something called Commerce Domain Data Models.

What Problem Do They Solve?

Think of it like this: If you want to order a pizza, you don't just shout random words. You fill out a form or tell the person on the phone specific details: "one large pepperoni pizza," "my address is 123 Main St." Everyone involved (the person taking the order, the chef, the delivery driver) understands these details because they follow a common structure.

Dynamics 365 Commerce works similarly. It needs a "common language" or "standardized forms" to handle all the business information. These "forms" are our Commerce Domain Data Models. They ensure that every part of the system, and any other system you connect to it, understands what a "product" is, what a "shopping cart" contains, or what a "sales order" looks like.

Without these models, chaos would reign! Different parts of the system might describe a product in different ways, leading to confusion, errors, and impossible integrations.

The Core Idea: Standardized Blueprints

At its heart, a Commerce Domain Data Model is a standardized "blueprint" or "form" that defines the structure of key business information. Each blueprint tells you:

  • What properties (fields) it has: For example, a Product blueprint might have fields for "Name," "Price," and "Description."
  • What type of data each property holds: Is "Price" a number? Is "Name" text?
  • How different blueprints relate to each other: A Cart blueprint might "contain" Product blueprints.

Let's look at some of the most important blueprints you'll encounter.

Key Commerce Data Models

Here are some fundamental data models in Dynamics 365 Commerce:

Data ModelAnalogyWhat it representsKey Properties (Examples)
ProductAn item in a store's catalogSomething you sellItemIdNamePriceDescriptionDimensions (like color, size)
CartA shopping basketItems a customer intends to buyIdCartLines (items in the cart), TotalAmountSubtotalAmount
SalesOrderA completed purchase receiptA confirmed transactionSalesIdTotalAmountSalesLines (items purchased), CustomerId
CustomerA shopper's profileInformation about a person who buysEmailFirstNameLastNameAddresses

Let's see them in action with small code examples from the CommerceClient.cs file provided in the project. This client acts as a "helper" to talk to Dynamics 365 Commerce.

The Product Model

The Product model represents an item available for sale. When you search for items in your store, the system returns Product information.

// Inside CommerceClient.cs, simplified snippet public async Task SearchProductsByKeyword(string keyword) { // Get a manager that knows how to work with products IProductManager productManager = factory.GetManager<IProductManager>(); // Search for products based on a keyword var products = await productManager.Search(new ProductSearchCriteria { SearchCondition = keyword }, null); // Loop through the results, each 'product' is a Product data model foreach (Product product in products) { // Access properties of the Product model logger.Log(LogStatus.Info, $"> [{product.ItemId,10}] {product.SearchName,-20} ${product.Price,5}"); } }

What happens here: This code snippet shows how we can search for products. The result of the search is a list of Product objects. Each Product object is an instance of the Product data model, containing properties like ItemId (the product's unique code), SearchName (what it's called), and Price (how much it costs).

You can explore more properties of the Product model, such as Description or Dimensions (for variations like size or color), by looking at the detailed data mapping in the project's documentation: Product Integration.

The Cart Model

The Cart model is your customer's digital shopping basket. It holds all the items they intend to buy before they complete the purchase.

// Inside CommerceClient.cs, simplified snippet public async Task<Cart> CreateCart() { // Get a manager that knows how to work with carts ICartManager cartManager = factory.GetManager<ICartManager>(); // Create a new, empty cart Cart cart = new(); // This is an instance of the Cart data model cart = await cartManager.Create(cart); logger.Log(LogStatus.Info, $"> A cart is created. Cart Id {cart.Id}."); return cart; } public async Task<Cart> AddItemsToCartAsync(string cartId, long productId, decimal quantity = 1) { ICartManager cartManager = factory.GetManager<ICartManager>(); // Create a new CartLine, which is a part of the Cart model var cartLine = new CartLine { ProductId = productId, Quantity = quantity }; // Add this CartLine to the existing cart var updatedCart = await cartManager.AddCartLines(cartId, [cartLine], null); // Access properties of the updated Cart model logger.Log(LogStatus.Info, $"> There are now {updatedCart.CartLines.Count} lines in the cart."); return updatedCart; }

What happens here: First, we create an empty Cart using cartManager.Create(). This gives us a Cart object with a unique Id. Then, to add items, we create a CartLine object for each product, specifying its ProductId and Quantity. This CartLine is a sub-part of the Cart data model. When we AddCartLines, the Cart object is updated to include these new items. The Cart model also keeps track of things like TotalAmount and SubtotalAmount.

The ConsoleExtensions.cs file also contains a DisplayCart method which helps visualize the Cart model's properties in a friendly way.

The SalesOrder Model

Once a customer is ready to buy, their Cart is transformed into a SalesOrder. This model represents the final, confirmed transaction.

// Inside CommerceClient.cs, simplified snippet public async Task<SalesOrder> Checkout(string cartId, decimal? amountDue, string receiptEmail) { ICartManager cartManager = factory.GetManager<ICartManager>(); // Perform the checkout operation on the cart // This transforms the Cart into a SalesOrder var salesOrder = await cartManager.Checkout( cartId, receiptEmail, null, receiptNumberSequence: string.Empty, null, // Simplified: actual payment lines are more complex null ); return salesOrder; // This is an instance of the SalesOrder data model }

What happens here: The Checkout method takes a cartId and other payment details. Its primary job is to convert the temporary shopping Cart into a permanent SalesOrder. The returned salesOrder object is an instance of the SalesOrder data model, containing all the details of the completed purchase. This includes a unique SalesIdTotalAmount, and a list of SalesLines (similar to CartLines, but for a finalized order).

You can find more details about the SalesOrder entity and its SalesLines in the project's order documentation: Order integration.

The Customer Model

The Customer model holds information about the shopper. While not directly shown in the CommerceClient.cs snippets above for creation, it's crucial for linking orders to individuals. A SalesOrder typically has a CustomerId property, connecting it to a Customer record.

You can learn more about how customer data is structured and integrated by exploring the project's customer documentation: Customer Integration.

How These Models Work Together (An Online Shopping Scenario)

Let's put it all together. Imagine a simple online shopping journey:



What this diagram shows: This flow highlights how different data models (ProductCartSalesOrder) are used at various stages of an online transaction. The Commerce Client (like our CommerceClient.cs helper) communicates with the Commerce Scale Unit (CSU), which is the "brain" of Dynamics 365 Commerce. The CSU then interacts with the Commerce Database to store and retrieve the actual data, all based on these structured data models.

Under the Hood: Data Model Structure

Behind the scenes, these "models" are typically defined as programming classes (like C# classes). These classes provide a clear structure, with properties corresponding to the fields described in our blueprints.

For instance, a simplified Cart data model in C# might look something like this:

// Simplified representation of a Cart data model (actual class has more properties) namespace Microsoft.Dynamics.Commerce.RetailProxy { public class Cart { public string Id { get; set; } // Unique identifier for the cart public List<CartLine> CartLines { get; set; } // List of items in the cart public decimal TotalAmount { get; set; } // Total price of items in the cart // ... other properties like SubtotalAmount, DiscountAmount, etc. } public class CartLine { public long ProductId { get; set; } // The ID of the product public decimal Quantity { get; set; } // How many of this product public string ItemId { get; set; } // The human-readable item ID // ... other properties like Price, Description, etc. } }

What this snippet means: This C# code shows how the Cart and CartLine data models are structured. Notice how Cart contains a List of CartLine objects. This demonstrates the relationship between models: a cart is made up of individual cart lines. When the Commerce Client makes an API call, it sends and receives data that conforms to these defined structures, ensuring everything is organized and understandable.

The CommerceClient.cs file uses these exact types (like Microsoft.Dynamics.Commerce.RetailProxy.Cart and Microsoft.Dynamics.Commerce.RetailProxy.Product) when interacting with the Dynamics 365 Commerce system.

Why Understanding Them Matters

Understanding these Commerce Domain Data Models is crucial for several reasons:

  • Customization: If you want to add a new field to a product (e.g., "eco-friendly rating"), you need to understand how the Product model works to extend it.
  • Integrations: When connecting Dynamics 365 Commerce with external systems (like a marketing platform or a warehouse system), these models serve as the common language for data exchange. You'll map fields from your external system to the properties in these models.
  • Troubleshooting: When something goes wrong, knowing the expected structure of a Cart or SalesOrder helps you quickly identify where data might be missing or incorrect.

These models are the backbone of data interaction within Dynamics 365 Commerce. They provide the necessary structure for information to flow smoothly, making development and integration much more manageable.

Conclusion

In this chapter, we've explored Commerce Domain Data Models, understanding them as standardized "blueprints" for key business entities like ProductCartSalesOrder, and Customer. We saw how these models simplify data exchange and serve as a common language for all parts of the Dynamics 365 Commerce system and its integrations. We also walked through a common online shopping scenario, illustrating how these models work together behind the scenes.

In the next chapter, we'll dive into the Headless Commerce Engine (CSU). This is the powerful component that actually uses these data models to process all the commerce operations we discussed today.


Chapter 2: Headless Commerce Engine (CSU)

Welcome back! In Chapter 1: Commerce Domain Data Models, we learned about the essential "blueprints" or "forms" that Dynamics 365 Commerce uses to understand information like products, shopping carts, and customer details. These data models are like the standardized ingredients in a recipe.

Now, let's talk about the "chef" who takes those ingredients and turns them into a delicious meal (or in our case, processes your online store's operations!). This "chef" is the Headless Commerce Engine, which runs on something called the Commerce Scale Unit (CSU).

What Problem Does It Solve?

Imagine you're running a big restaurant with many different ways customers can order:

  • Someone orders at the counter.
  • Someone calls in an order.
  • Someone uses your mobile app.
  • Someone orders from your website.

Each of these ordering methods (or "channels") needs to do the same things:

  • Check if ingredients are available (product inventory).
  • Calculate the total price, including discounts and taxes.
  • Send the order to the kitchen.
  • Process payment.

If each ordering method had to figure out all this complex logic on its own, it would be a mess! Prices might be different, inventory could be wrong, and your chefs would be confused.

The Headless Commerce Engine (CSU) solves this by being the central "brain" for all these operations. It ensures that no matter where an order comes from, the business rules are applied consistently and correctly. It's like having one master chef in the kitchen who knows all the recipes and ensures every dish is perfect, no matter who takes the order.

The Core Idea: The Central Brain

The Headless Commerce Engine, powered by the Commerce Scale Unit (CSU), is the heart of Dynamics 365 Commerce. Think of it as:

  • The "Brain": It's where all the smart business logic happens – calculating prices, managing inventory, processing payments, handling returns, and more.
  • The "API Gateway": It provides a set of powerful APIs (Application Programming Interfaces) that all your storefronts (websites, mobile apps, in-store POS systems) can talk to. These APIs are the "order pads" that different waiters use to send requests to the kitchen (the CSU).

When we talk about Commerce Scale Unit (CSU), we are often referring to the underlying infrastructure that hosts this powerful engine. So, while CSU is the platform, the Headless Commerce Engine is the key software running on it, processing all your commerce operations. They are closely linked and often mentioned interchangeably.

How it Works: Taking Your Orders

Let's revisit our online shopping scenario from Chapter 1. Remember how the CommerceClient.cs (our "helper" application) interacted with the system? All those interactions, like searching for products, adding items to a cart, or checking out, are actually requests sent to the Headless Commerce Engine (CSU).

The CommerceClient.cs doesn't do the heavy lifting of figuring out product prices, calculating tax, or checking inventory. It simply sends a request to the CSU, and the CSU processes it.

Let's look at some examples from CommerceClient.cs to see this in action:

Searching for Products

When your online store needs to show products, it asks the CSU: "Hey, give me products matching 't-shirt'!"

// Inside CommerceClient.cs, simplified snippet public async Task SearchProductsByKeyword(string keyword) { // Get a manager that knows how to talk to the CSU about products IProductManager productManager = factory.GetManager<IProductManager>(); // Send the search request to the CSU var products = await productManager.Search(new ProductSearchCriteria { SearchCondition = keyword }, null); // The CSU returns Product data models (from Chapter 1) foreach (Product product in products) { logger.Log(LogStatus.Info, $"> [{product.ItemId,10}] {product.SearchName,-20} ${product.Price,5}"); } }

What happens here: The productManager.Search() method is essentially sending an API call to the Headless Commerce Engine. The engine then processes the search, fetches the product data (using the Product data model), and sends it back to your application. Your application just displays the results.

Managing Your Shopping Cart

When a customer adds an item to their cart, your app doesn't calculate the new total or handle inventory. It just tells the CSU: "Add this item to this cart!"

// Inside CommerceClient.cs, simplified snippet public async Task<Cart> AddItemsToCartAsync(string cartId, long productId, decimal quantity = 1) { // Get a manager that knows how to talk to the CSU about carts ICartManager cartManager = factory.GetManager<ICartManager>(); // Prepare the item details (CartLine data model from Chapter 1) var cartLine = new CartLine { ProductId = productId, Quantity = quantity }; // Send the "add item" request to the CSU var updatedCart = await cartManager.AddCartLines(cartId, [cartLine], null); // The CSU returns the updated Cart data model logger.Log(LogStatus.Info, $"> There are now {updatedCart.CartLines.Count} lines in the cart."); return updatedCart; }

What happens here: The cartManager.AddCartLines() method sends your request to the Headless Commerce Engine. The engine then updates the Cart (potentially calculating new totals, applying discounts, checking inventory), and sends the fully updated cart data back to your application.

Processing Checkout

When a customer checks out, the biggest tasks are payment processing and creating the final order. Again, your application doesn't do this. It relies on the CSU:

// Inside CommerceClient.cs, simplified snippet public async Task<SalesOrder> Checkout(string cartId, decimal? amountDue, string receiptEmail) { // Get a manager to talk to the CSU about carts ICartManager cartManager = factory.GetManager<ICartManager>(); // Send the "checkout" request to the CSU var salesOrder = await cartManager.Checkout( cartId, receiptEmail, null, receiptNumberSequence: string.Empty, null, // Simplified: actual payment lines are more complex null ); // The CSU processes the payment, converts cart to SalesOrder, and returns it return salesOrder; // This is an instance of the SalesOrder data model }

What happens here: The cartManager.Checkout() method tells the Headless Commerce Engine to finalize the transaction. The engine handles the payment, transforms the Cart into a permanent SalesOrder, and stores it. It then returns the confirmed SalesOrder back to your application.

Under the Hood: The CSU in Action

Let's visualize how the Headless Commerce Engine (CSU) acts as the central brain in our online shopping journey:


What this diagram shows: The diagram illustrates that the Commerce Client (the helper application for your storefront) never directly touches the Commerce Database. Instead, all requests go through the Headless Commerce Engine (CSU). The CSU is responsible for:

  1. Receiving requests: It listens for API calls from different applications.
  2. Applying business logic: This is the core "smartness." It ensures all rules (pricing, inventory, taxes, discounts) are applied correctly. This logic is part of the Commerce Runtime (CRT), which we'll explore in detail in the next chapter.
  3. Interacting with the database: It fetches and updates information in the Commerce Database, always using the structured Commerce Domain Data Models.
  4. Returning results: It sends back the processed data (like an updated cart or a confirmed order) to the requesting application.

Why is it "Headless"?

The term "headless" simply means that the Headless Commerce Engine (CSU) operates without a graphical user interface (GUI) or "head" attached. It's just the brain and the APIs. This is a huge advantage because:

FeatureDescriptionBenefit
Channel AgnosticIt serves any sales channel (website, mobile app, kiosk, in-store POS) with the same consistent logic.Consistency: Ensures customers have the same experience and pricing, regardless of how they shop.
Flexible Front-EndsYou can use any front-end technology (React, Angular, mobile native apps) to build your customer experience.Agility & Innovation: Developers can use their preferred tools, allowing for faster development of unique and engaging customer interfaces without being tied to a specific platform's UI.
Centralized LogicAll complex business rules (pricing, promotions, tax, inventory) are managed in one place.Efficiency & Accuracy: Reduces errors, simplifies updates, and ensures business rules are applied uniformly across all touchpoints.
ScalabilityThe CSU is designed to handle a large volume of requests, scaling up or down as needed, especially during peak shopping seasons.Reliability: Your commerce operations remain smooth and responsive, even with high traffic.
ExtensibilityWhile powerful, you can add your own custom business logic or connect to external services through the CSU, further extending its capabilities. (More on this in later chapters!)Adaptability: The system can evolve with your business needs, integrating with specific third-party systems or adding unique features.

Conclusion

In this chapter, we've explored the Headless Commerce Engine (CSU) as the central "brain" or "API gateway" that orchestrates all Dynamics 365 Commerce operations. We saw how it takes requests from various front-ends, applies complex business logic, interacts with the Commerce Domain Data Models and the database, and returns consistent results. The "headless" nature provides immense flexibility, allowing you to build diverse and engaging customer experiences while maintaining a single, powerful backend.

In the next chapter, we'll dive deeper into the Commerce Runtime (CRT). This is the crucial component inside the CSU that contains all the actual business logic and rules, and it's where much of the system's extensibility comes into play!


Chapter 3: Commerce Runtime (CRT) Extensibility

Welcome back! In Chapter 1: Commerce Domain Data Models, we learned about the essential "blueprints" for commerce data. In Chapter 2: Headless Commerce Engine (CSU), we discovered the Commerce Scale Unit (CSU), the powerful "brain" that processes all commerce operations using these blueprints.

Now, let's zoom in a bit more. Inside that "brain" (the CSU) is something called the Commerce Runtime (CRT). Think of the CRT as the specific "rulebook" or "logic engine" that the CSU uses to perform its tasks. For example, when you add an item to a cart, it's the CRT that figures out the price, applies discounts, or checks inventory.

What Problem Does It Solve?

Imagine you own an online shoe store. By default, Dynamics 365 Commerce handles most things perfectly. But what if your store has a unique rule? For instance:

  • "We never sell more shoes than we actually have in stock at a specific store location." (Preventing "negative inventory")
  • "Customers buying luxury shoes get a special discount only on Tuesdays."
  • "For every 5 pairs of socks bought, add a free shoe cleaning kit to the cart."

The standard Dynamics 365 Commerce system is robust, but it can't anticipate every single unique business rule for every store in the world. If you tried to change the core system every time you had a new rule, it would be difficult to update and maintain.

This is where Commerce Runtime (CRT) Extensibility comes in. It solves the problem of needing to add your own custom business rules and logic without touching or changing the core, standard system. It's like adding a special, personalized chapter to the system's rulebook without rewriting the whole book!

The Core Idea: "Plug-in Points"

The core idea of CRT Extensibility is providing "plug-in points" within the system. These are specific places where you, as a developer, can "plug in" your own custom code. When the CSU processes an operation, it will hit these plug-in points and run your custom logic alongside its standard rules.

This allows businesses to:

  • Tailor processes: Customize how carts work, how orders are fulfilled, or how products are managed.
  • Inject custom rules: Add unique pricing logic, inventory checks, or validation steps.
  • Maintain integrity: Your custom code lives separately, so upgrading the core Dynamics 365 Commerce system is easier and safer.

How CRT Extensibility Works

When the Headless Commerce Engine (CSU) receives a request (like "add item to cart" or "checkout"), it uses the CRT to process it. As the CRT executes its standard business logic, it looks for any custom code "plugged in" at specific points.

You define these plug-in points by adding special entries to a configuration file named CommerceRuntime.Ext.config. This file tells the CRT which custom pieces of code (your extensions) it should load and use.

Let's look at a simplified example from the Commerce/NegativeInventoryCheck/CommerceRuntime.Ext.config file in the project:

<!-- Commerce/NegativeInventoryCheck/CommerceRuntime.Ext.config, simplified --> <commerceRuntimeExtensions> <composition> <!-- This line tells the CRT to load our custom inventory check service --> <add source="type" value="Extensions.Crt.InventoryAvailability.ExtInventoryAvailabilityService, Extensions.Crt.InventoryAvailability" /> <!-- These lines tell the CRT to use our custom rules when saving/validating carts --> <add source="type" value="Extensions.Crt.NegativeInventoryCheck.SaveCartVersionedDataRequestTrigger, Extensions.Crt.NegativeInventoryCheck" /> <add source="type" value="Extensions.Crt.NegativeInventoryCheck.ValidateCartForCheckoutRequestTrigger, Extensions.Crt.NegativeInventoryCheck" /> <!-- ... other triggers ... --> </composition> <!-- ... other settings ... --> </commerceRuntimeExtensions>

What happens here: This CommerceRuntime.Ext.config file acts as a manifest for your custom code. The <composition> section is where you "register" your extensions. Each <add source="type" ... /> line tells the CRT: "Hey, load this specific C# class. It contains some custom logic that you need to consider when running operations." This is how you make your custom code a part of the CRT's rulebook.

Key Types of Extensibility

The two most common types of "plug-in points" in the CRT are:

Extension TypeAnalogyWhat it doesWhen to use it
Request HandlerA specialized chef for a unique dishCompletely handles a specific request or performs a specific calculation.When you need to provide a new service or replace an existing core service's behavior entirely (e.g., a custom inventory calculation).
TriggerA quality inspector checking ingredients (before) or plating (after)Runs custom code before (pre-trigger) or after (post-trigger) a standard CRT operation takes place.When you want to add validation, logging, or modify data before or after a standard operation (e.g., validate inventory before checkout).

Let's use our example of "preventing negative inventory" to see how these types of extensions work together.

Example: Preventing Negative Inventory with CRT Extensions

Our goal: When a customer adds an item to their cart or checks out, we want to ensure there's enough stock at the store location to prevent inventory from going below zero. If not, we'll stop the sale.

Here's how CRT Extensibility helps us achieve this:

Step 1: Reacting to Standard Operations (Using Triggers)

When a customer adds an item to their cart, the standard CRT logic saves the cart. This is a perfect "plug-in point" for us to add our custom inventory check before the save happens. We'll use a Request Trigger.

The SaveCartVersionedDataRequestTrigger.cs file from the project's NegativeInventoryCheck example shows this:

// Commerce/NegativeInventoryCheck/Crt.NegativeInventoryCheck/SaveCartVersionedDataRequestTrigger.cs public class SaveCartVersionedDataRequestTrigger : IRequestTrigger { // Tells the CRT which operation this trigger cares about public IEnumerable<Type> SupportedRequestTypes { get { return new[] { typeof(Microsoft.Dynamics.Commerce.Runtime.DataServices.Messages.SaveCartVersionedDataRequest) }; } } // This method runs BEFORE the standard cart save operation public void OnExecuting(Request request) { // Cast the request to access cart details SaveCartVersionedDataRequest r = (SaveCartVersionedDataRequest)request; // If there are active items in the cart... if (r.SalesTransaction != null && r.SalesTransaction.ActiveSalesLines.Any()) { // Call a helper method to validate inventory for these items var productQuantityPairs = r.SalesTransaction.ActiveSalesLines .Where(asl => !asl.IsVoided) .GroupBy(cl => cl.ProductId) .Select(group => Tuple.Create(group.Key.ProductId, group.Sum(i => i.Quantity))); InventoryRequestHelper.ValidateOnHandQuantities(productQuantityPairs, request.RequestContext); } } public void OnExecuted(Request request, Response response) { /* Runs AFTER operation */ } }

What happens here: This SaveCartVersionedDataRequestTrigger class is a Trigger. It's configured in CommerceRuntime.Ext.config to "listen" for SaveCartVersionedDataRequest (which happens when a cart is updated, like adding an item). The OnExecuting method runs before the cart is officially saved. Inside OnExecuting, we extract the product IDs and quantities from the cart and then call our custom InventoryRequestHelper.ValidateOnHandQuantities method to perform the actual check.

There's also a similar trigger, ValidateCartForCheckoutRequestTrigger, that runs when a customer tries to check out, ensuring inventory is checked one last time.

Step 2: Calculating Custom Inventory (Using a Request Handler)

Our InventoryRequestHelper needs a way to actually calculate the current inventory for an item, taking into account recent sales or returns at the store. This is a perfect job for a custom Request Handler.

The ExtInventoryAvailabilityService.cs file calculates the accurate on-hand inventory:

// Commerce/NegativeInventoryCheck/Crt.InventoryAvailability/ExtInventoryAvailabilityService.cs public class ExtInventoryAvailabilityService : SingleRequestHandler<ExtGetInventoryAvailabilityForFulfillmentLinesServiceRequest, EntityDataServiceResponse<FulfillmentLine>> { // This method processes the custom inventory request protected override EntityDataServiceResponse<FulfillmentLine> Process(ExtGetInventoryAvailabilityForFulfillmentLinesServiceRequest request) { // 1. Get requested products and store location var fulfillmentLines = request.FulfillmentLines; var inventLocationId = request.InventLocationId; // 2. Query product availability synced from HQ var getProductDataRequest = new GetProductDimensionsInventoryAvailabilityDataRequest( fulfillmentLines.Select(f => new ProductWarehouse() { ProductId = f.ProductId, InventLocationId = inventLocationId })); var productAvailability = request.RequestContext.Execute<EntityDataServiceResponse<ProductDimensionInventoryAvailability>>(getProductDataRequest).PagedEntityCollection.Results; // 3. Get unposted quantities (local store transactions not yet synced to HQ) var dataRetriever = new Microsoft.Dynamics.Commerce.Runtime.Services.InventoryAvailabilityServiceDataRetriever(request.RequestContext); var unpostedQuantity = dataRetriever.GetUnpostedQuantities(productAvailability.Select(p => new ProductWarehouse() { ProductId = p.ProductId })); // 4. Calculate final on-hand for each item foreach (var line in fulfillmentLines) { var p = productAvailability.First(x => x.ProductId == line.ProductId); var u = unpostedQuantity.First(x => x.ProductId == line.ProductId); // This is the custom logic: PhysicalInventory - Reserved + Unprocessed (local) + Unposted (local) line.StoreInventoryTotalQuantity = p.PhysicalInventory - p.PhysicalReserved + p.UnprocessedQty + u.UnpostedQuantity; } return new EntityDataServiceResponse<FulfillmentLine>(fulfillmentLines.AsPagedResult()); } }

What happens here: This ExtInventoryAvailabilityService is our custom Request Handler. It receives a special request (defined by ExtGetInventoryAvailabilityForFulfillmentLinesServiceRequest) from InventoryRequestHelper. Its Process method then does the heavy lifting: it queries the store's inventory data (both what's synced from HQ and what's happened locally) and calculates the true current on-hand quantity for each item using a specific formula. It then returns this custom calculated inventory.

Step 3: Preventing the Sale (Throwing an Exception)

Finally, back in our InventoryRequestHelper, after getting the custom inventory calculation, we compare it with the quantity the customer wants to buy. If there isn't enough, we stop the operation!

// Commerce/NegativeInventoryCheck/Crt.NegativeInventoryCheck/InventoryRequestHelper.cs internal sealed class InventoryRequestHelper { public static void ValidateOnHandQuantities(IEnumerable<Tuple<long, decimal>> lineQuantities, RequestContext context) { // ... (code to check if inventory check should be skipped for this request) ... // Execute our custom inventory availability service ExtGetInventoryAvailabilityForFulfillmentLinesServiceRequest request = new ExtGetInventoryAvailabilityForFulfillmentLinesServiceRequest( context.GetChannelConfiguration().InventLocation, fulfillmentLines: lineQuantities.Select(pq => new FulfillmentLine() { ProductId = pq.Item1 }).ToArray()); var response = context.Runtime.Execute<EntityDataServiceResponse<FulfillmentLine>>(request, context); foreach (var fulfillmentLine in response.ToArray()) { var itemInCartQuantity = lineQuantities.First(cl => cl.Item1 == fulfillmentLine.ProductId).Item2; // If the custom calculated total quantity is less than what's in the cart... if (fulfillmentLine.StoreInventoryTotalQuantity < itemInCartQuantity) { // Throw a CommerceException to stop the transaction and show an error message. throw new CommerceException("Microsoft_Dynamics_Commerce_30104", ExceptionSeverity.Warning, null, "Custom error") { LocalizedMessage = string.Format("Not enough inventory for item '{0}'. You have only {1} on hand.", fulfillmentLine.ItemId, fulfillmentLine.StoreInventoryTotalQuantity) }; } } } }

What happens here: The InventoryRequestHelper.ValidateOnHandQuantities method first creates a request for our ExtInventoryAvailabilityService (our custom handler from Step 2). It then executes this request to get the precise on-hand quantity. If the quantity in the cart is more than what's available, it throws a CommerceException. This special type of exception is recognized by Dynamics 365 Commerce and will typically display a friendly error message to the user (e.g., in the Point of Sale system).

Under the Hood: The Flow with CRT Extensibility

Let's visualize how a request (like "add item to cart") flows through the system and hits our CRT extensions:



What this diagram shows: When a user adds an item, the request goes through the Commerce Client to the Headless Commerce Engine (CSU). The CSU then hands it off to the Commerce Runtime (CRT). This is where the magic happens: the CRT, recognizing our registered Trigger, pauses its standard operation to run our SaveCartVersionedDataRequestTrigger. This trigger, in turn, calls our custom Request Handler (ExtInventoryAvailabilityService) to get the real-time inventory. Based on that, the trigger decides whether to allow the operation to continue or throw an error, effectively preventing the sale.

Conclusion

In this chapter, we've explored Commerce Runtime (CRT) Extensibility as the powerful mechanism within Dynamics 365 Commerce that allows businesses to inject custom rules and tailor core commerce processes without modifying the standard platform. We learned about "plug-in points," specifically Request Handlers for custom logic and Triggers for intercepting standard operations. Through the "negative inventory check" example, we saw how these extensions work together to enforce unique business requirements, making Dynamics 365 Commerce incredibly flexible and adaptable.

In the next chapter, we'll shift our focus to External Service Integration Connectors. While CRT extensibility allows us to customize internal logic, these connectors will show us how Dynamics 365 Commerce can seamlessly talk to other systems outside of its own boundaries.


Chapter 4: External Service Integration Connectors

Welcome back! In Chapter 1: Commerce Domain Data Models, we learned about the "blueprints" for all our commerce information. Then, in Chapter 2: Headless Commerce Engine (CSU), we explored the "brain" that processes these blueprints. In Chapter 3: Commerce Runtime (CRT) Extensibility, we saw how to add our own specific business rules inside that brain.

But what if Dynamics 365 Commerce needs to talk to other specialized systems outside of its own world? Imagine your store wants to use a super-smart AI helper or needs to process credit card payments through a bank. Dynamics 365 Commerce is powerful, but it doesn't do everything itself.

This is where External Service Integration Connectors come in.

What Problem Do They Solve?

Think of Dynamics 365 Commerce as a well-organized city. It has its own roads, buildings, and rules. But sometimes, this city needs to interact with other specialized "neighboring cities" or "service providers" that offer unique capabilities:

  • A "smart advice" city (like an AI model): You want to ask it complex questions or get recommendations.
  • A "secure money handling" city (like a payment gateway): You need to securely process credit card transactions.

The challenge is that these "neighboring cities" often speak different languages and have their own specific ways of doing things. If our Commerce city tried to learn every single language and custom, it would become too complicated.

External Service Integration Connectors solve this problem by acting as "specialized plugs" or "translators." They bridge the gap between Dynamics 365 Commerce and these external systems. This allows our Commerce system to send and receive data from third-party services, extending its native capabilities without having to re-invent the wheel for every specialized task.

The Core Idea: Specialized Plugs and Translators

At its heart, an External Service Integration Connector is a piece of software that knows how to:

  1. Speak Commerce's language: Understands requests from Dynamics 365 Commerce.
  2. Speak the external service's language: Knows how to format requests and interpret responses from the third-party system (like an AI model or a payment gateway).
  3. Translate: Converts information back and forth between the two.

This way, Dynamics 365 Commerce doesn't need to worry about the complex details of talking to a specific AI model or a particular payment provider. It just sends a request to the right "connector," and the connector handles all the translation and communication.

How They Work: Two Real-World Examples

Let's look at two important examples of how Dynamics 365 Commerce uses these connectors:

  1. Connecting to a Local AI Model (like Ollama): Imagine a cashier in a store using the Point of Sale (POS) system asking an AI for quick product information.
  2. Connecting to a Payment Gateway (like Adyen): When a customer pays with a credit card, the payment details need to be securely sent to a specialized payment processor.

Use Case 1: Talking to a Local AI Model (Ollama)

Suppose you want to integrate a local AI model (like phi3:mini running via Ollama) to help your store staff. For example, a cashier might ask the AI: "What are the benefits of this new smartphone model?"

Here's how Dynamics 365 Commerce can use a connector to make this happen:

The Path: Store Commerce App -> Hardware Station -> Local AI Connector -> Ollama AI

The Hardware Station acts like a local hub for various devices and services in a store. We can extend it to include our "Local AI Connector."

1. Asking from the Store Commerce App (POS)

The Store Commerce App (the POS system) sends a request to its connected Hardware Station. This is done using a special request type:

// From: Commerce/HWSLocalAIModelConnector/readme.md (simplified) let aiRequest: { Message: string } = { Message: "What are the benefits of this new smartphone model?", }; // Create a request to send to the Hardware Station let localAIModelHWSRequest = new HardwareStationDeviceActionRequest( "LocalAIModel", // The connector name "SendMessage", // The action to perform aiRequest // The message for the AI ); // Execute the request this.context.runtime.executeAsync(localAIModelHWSRequest) .then((airesponse) => { // Handle the AI's response here console.log("AI says:", airesponse.data.response); }) .catch((reason: any) => { console.error("Error talking to AI:", reason); });

What happens here: The Store Commerce App (our POS system) doesn't know how to talk to Ollama directly. Instead, it sends a generic HardwareStationDeviceActionRequest to its Hardware Station, specifying that it wants to talk to a "LocalAIModel" and "SendMessage" a specific message.

2. The Hardware Station Connector in Action (C#)

On the Hardware Station, our "Local AI Model Connector" receives this request. It's built as a C# class that acts as the "translator."

// From: Commerce/HWSLocalAIModelConnector/LocalAIModelController.cs (simplified) public class LocalAIModelController : IController { // This method receives the "SendMessage" request from the POS [HttpPost] public async Task<string> SendMessage(LocalAIModelRequest request) { // Prepare a system message (e.g., "You are a helpful assistant.") var systemMessages = new OllamaChatMessage[] { /* ... */ }; // Prepare the user's message var userMessages = new OllamaChatMessage[] { new OllamaChatMessage { Role = "user", Content = request.Message } }; // Initialize the Ollama client (our direct communicator with the AI) var client = OllamaClient.Initialize("phi3:mini", systemMessages); // Send the messages to Ollama and get the AI's response return await client.ChatSession.SendUserMessagesAsync(userMessages).ConfigureAwait(false); } }

What happens here: The LocalAIModelController is the "connector" here. It takes the LocalAIModelRequest from the Hardware Station, formats it into a request that the Ollama AI understands (OllamaChatMessage), and then uses the OllamaClient to actually send that message to the AI.

3. The Direct AI Communication (C#)

Finally, the OllamaClient handles the low-level communication with the running Ollama AI service, usually via a standard HTTP request.

// From: Commerce/HWSLocalAIModelConnector/OllamaClient.cs (simplified) public class OllamaClient { // ... other code ... public static async Task<string> SendRawChatAsync(OllamaChatPrompt prompt, CancellationToken cancellationToken = default) { var json = JsonSerializer.Serialize(prompt); using (var content = new StringContent(json, Encoding.UTF8, "application/json")) { // This is the actual call to the local Ollama AI service! using (var response = await HttpClient.PostAsync("http://localhost:11434/api/chat", content, cancellationToken).ConfigureAwait(false)) { response.EnsureSuccessStatusCode(); // ... code to read and combine the AI's response ... return /* AI's full response */; } } } }

What happens here: The OllamaClient uses a standard HttpClient to send a JSON message to the Ollama AI service, which is running locally (usually on http://localhost:11434). It then reads the AI's response and sends it back through the chain of calls.


Use Case 2: Secure Payment Processing (Adyen)

When a customer pays for an order, Dynamics 365 Commerce needs to talk to a payment gateway (like Adyen) to authorize and capture funds. This is a critical process involving sensitive financial data.

The Path: Commerce System -> Payment Connector (using Payment SDK) -> Payment Gateway

Dynamics 365 Commerce handles payments using a specialized framework called the Payment SDK. This SDK simplifies the process of building Payment Connectors.

Core Idea: Payment Properties and Blobs

When payments are processed, the payment connector translates the details (like card number, expiry, amount, authorization code) into a standardized structure called PaymentProperty objects. These properties are then bundled together into a special "package" often referred to as a "payment blob" (a compressed XML string).

These "blobs" are crucial because they contain all the necessary information for Dynamics 365 Commerce to understand the payment and, more importantly, to perform future actions like re-authorizations, captures, or refunds, even if the payment initially happened on an external website.

Let's look at a helper class that mimics how a payment connector would talk to Adyen and produce these "blobs."

// From: Commerce/HeadlessCommerceSamples/Assets/HeadlessSampleConsoleApp/SampleConsoleAppAuthorizePayment/AdyenPaymentHelper.cs (simplified) public class AdyenPaymentHelper { public required string PaymentServiceURL { get; set; } public required string Token { get; set; } // Adyen API key public required string MerchantAccount { get; set; } public required Amount Amount { get; set; } public required AdyenPaymentMethod PaymentMethod { get; set; } // This method sends the payment request to Adyen public async Task<string> AuthorizePaymentAsync() { var requestBody = new { // ... Construct the request body using properties like Amount, PaymentMethod ... merchantAccount = MerchantAccount, amount = Amount, paymentMethod = PaymentMethod }; // Send the request to Adyen's API endpoint var response = await ExecuteHttpRequestAsync("payments", requestBody); // ... Process the response from Adyen ... return PaymentResponse.ResultCode; } // This method converts Adyen's response into a CardToken "blob" for D365 Commerce public string GetCardToken() { var paymentProperties = new List<PaymentProperty> { new PaymentProperty(GenericNamespace.PaymentCard, PaymentCardProperties.Last4Digits, PaymentMethod.Number?[^4..]), new PaymentProperty(GenericNamespace.PaymentCard, PaymentCardProperties.CardType, PaymentResponse.AdditionalData.CardPaymentMethod), // ... many other properties describing the card ... }; // Convert the list of properties into the D365 Commerce "blob" format return PaymentProperty.ConvertPropertyArrayToXML(paymentProperties.ToArray()); } // This method converts Adyen's response into an Authorization "blob" for D365 Commerce public string GetAuthorizationToken() { var authorizationProperties = new List<PaymentProperty> { new PaymentProperty(GenericNamespace.AuthorizationResponse, AuthorizationResponseProperties.ApprovedAmount, Amount.Value), new PaymentProperty(GenericNamespace.AuthorizationResponse, AuthorizationResponseProperties.ApprovalCode, PaymentResponse.AdditionalData.AuthCode), // ... many other properties describing the authorization ... }; // Convert the list of properties into the D365 Commerce "blob" format return PaymentProperty.ConvertPropertyArrayToXML(authorizationProperties.ToArray()); } }

What happens here:

  1. The AuthorizePaymentAsync method builds a specific request (using the AmountPaymentMethod, etc.) and sends it to the Adyen payment gateway using a standard HttpClient.
  2. After Adyen processes the payment and sends back a response, the GetCardToken() and GetAuthorizationToken() methods are called. These methods take the raw response from Adyen and translate it into a list of standardized PaymentProperty objects.
  3. Finally, PaymentProperty.ConvertPropertyArrayToXML() bundles these properties into a compact XML string (the "payment blob"). This "blob" is what Dynamics 365 Commerce stores in its TenderLine (the payment line on an order) to represent the payment. This allows D365 Commerce to understand and manage the payment throughout the order's lifecycle.

The payments.overview.md document in this project provides a deeper dive into the Payment SDK and the structure of these payment properties.


Under the Hood: A Typical Connector Flow (AI Example)

Let's visualize the AI example to see how the connector facilitates communication:


What this diagram shows: The Store Commerce App sends a request to the Hardware Station. The Hardware Station then hands it off to our Local AI Connector (which is a specific implementation of a Hardware Station extension). This connector acts as the "translator." It makes the actual call to the Ollama AI Service, handles the specific language and protocol of Ollama, and then translates the AI's response back into a format that the Hardware Station (and ultimately the Store Commerce App) can understand.

Why External Service Integration Connectors Matter

Connectors are a fundamental part of a modern commerce system like Dynamics 365 Commerce.

FeatureWithout ConnectorsWith Connectors
CapabilitiesLimited to Dynamics 365 Commerce's built-in features.Extended capabilities: Integrate with best-of-breed external services (AI, advanced payments, fraud detection, etc.).
Specialized TasksCommerce system would have to build every specialized feature itself (e.g., complex payment security).Focus on core: Commerce system focuses on its main job, offloading specialized tasks to dedicated external services.
Security & ComplianceCommerce system would need to maintain direct compliance for all types of external interactions.Delegated responsibility: Specialized connectors or services handle compliance (e.g., PCI DSS for payments).
Agility & FlexibilityDifficult to switch external providers or add new types of integrations.Pluggable architecture: Easier to swap or add new external services without changing core Commerce code.
MaintenanceHigh, as all integrations are tightly coupled to the core system.Lower: Connectors isolate external system changes, reducing impact on core Commerce.

These connectors allow Dynamics 365 Commerce to be both powerful and flexible, adapting to a wide range of business needs by seamlessly integrating with the external systems that specialize in specific tasks.

Conclusion

In this chapter, we've explored External Service Integration Connectors as the "specialized plugs" or "translators" that enable Dynamics 365 Commerce to connect with external systems. We saw how they extend Commerce's capabilities, allowing for features like local AI-powered interactions via a Hardware Station extension, and secure payment processing through payment gateways like Adyen using the Payment SDK. Understanding these connectors is key to building a comprehensive and adaptable commerce ecosystem.

In the next chapter, we'll shift our focus to the Retail Proxy SDK. This is the toolkit that client applications (like our CommerceClient.cs helper from earlier chapters) use to easily talk to the Dynamics 365 Commerce system, effectively using the services and connectors we've discussed.


Chapter 5: Retail Proxy SDK

Welcome back! In our previous chapters, we've built up our understanding of the Dynamics 365 Commerce ecosystem. In Chapter 1: Commerce Domain Data Models, we learned about the "blueprints" for commerce data. In Chapter 2: Headless Commerce Engine (CSU), we explored the "brain" that processes these blueprints. And in Chapter 3: Commerce Runtime (CRT) Extensibility, we saw how to add our own specific business rules inside that brain. Finally, in Chapter 4: External Service Integration Connectors, we discovered how Commerce talks to specialized systems outside its own boundaries.

Now, imagine you're a developer building a custom website or mobile app for your online store. How does your app actually talk to this powerful Headless Commerce Engine (CSU)? Does it have to write complex code to create web requests, handle security, and understand the intricate data formats? That sounds like a lot of work!

This is where the Retail Proxy SDK comes into play.

What Problem Does It Solve?

Think of it like this: You have a sophisticated smart home system (Headless Commerce Engine (CSU)) that controls all your lights, doors, and music. If you want to turn on the lights from your phone, you don't manually send a complex series of electrical signals or cryptic commands. Instead, you use a remote control or a smart home app. You just press a button like "Turn on Living Room Lights."

The Retail Proxy SDK acts as this specialized "remote control" or "translator" for .NET applications (like your custom website or mobile app) that need to communicate with the Headless Commerce Engine (CSU).

Without the Retail Proxy SDK, developers would face several challenges:

  • Complex Web Requests: Manually building HTTP requests (URLs, headers, body) for every single operation (search products, add to cart, checkout) would be tedious and error-prone.
  • Authentication: Handling secure login and access tokens for every request would add significant complexity.
  • Data Conversion: Converting C# objects to JSON/XML for sending, and then back from JSON/XML to C# objects (Commerce Domain Data Models) for receiving, would require a lot of boilerplate code.

The Retail Proxy SDK solves these problems by simplifying the integration process. It makes calling Commerce APIs feel almost as easy as calling a local function in your C# code.

The Core Idea: Object-Oriented APIs

The core idea of the Retail Proxy SDK is to provide familiar C# objects and methods that represent the operations and data within Dynamics 365 Commerce. Instead of worrying about web protocols, you just work with C# classes like IProductManager or ICartManager.

Here's how it works at a high level:

  1. Managers: The SDK provides "manager" interfaces (like IProductManagerICartManagerISalesOrderManager). These interfaces group related operations (e.g., all product-related actions are under IProductManager).
  2. Methods: Each manager has methods that correspond directly to Commerce API operations (e.g., Search on IProductManagerAddCartLines on ICartManager).
  3. Data Models: These methods use the Commerce Domain Data Models (like ProductCartSalesOrder) as input and output. You pass in C# Product objects, and you get back C# Cart objects.

The SDK handles all the low-level details:

  • Connecting to the right Headless Commerce Engine (CSU) endpoint.
  • Managing authentication (making sure your app has permission).
  • Converting your C# objects into web request formats (like JSON).
  • Sending the request over the network.
  • Receiving the web response.
  • Converting the response back into C# objects.

This makes development quicker, less prone to errors, and allows developers to focus on building the application's unique features, not on the plumbing.

How to Use It: A Walkthrough with CommerceClient.cs

The CommerceClient.cs file (from Commerce/HeadlessCommerceSamples/Assets/HeadlessSampleConsoleApp/SampleConsoleApp.Common/CommerceClient.cs) in our project is a perfect example of how to use the Retail Proxy SDK. It demonstrates common operations like searching products, managing a cart, and checking out.

Let's look at how it initializes and uses the SDK's "managers."

1. Initializing the SDK and Getting a Manager Factory

First, you need to tell the SDK where your Headless Commerce Engine (CSU) is located and how to authenticate. This is typically done once at the start of your application.

// From ConfigHelper.cs (simplified) using Microsoft.Dynamics.Commerce.RetailProxy; // ... other usings ... public class ConfigHelper { public static ManagerFactory InitFactory() { // 1. Get configuration values (like CSU URL, authentication details) string commerceApiEndpoint = configuration["CommerceApiEndpoint"]; // ... (authentication setup) ... // 2. Create the RetailServerContext, which holds connection info var context = RetailServerContext.Create( new Uri(commerceApiEndpoint), "", // Operating unit number (can be set later) AuthenticationHelper.GetAuthenticationResult(...).Result); // 3. Create the ManagerFactory, your entry point to all managers return ManagerFactory.Create(context); } }

What happens here: The ConfigHelper.InitFactory() method sets up the initial connection. It takes the Commerce API Endpoint (the URL of your Headless Commerce Engine (CSU)) and authentication details. It then creates a RetailServerContext, which is like a "connection string" for the SDK. Finally, it uses ManagerFactory.Create(context) to give you a ManagerFactory object. This ManagerFactory is your central hub for getting all other SDK managers.

2. Getting a Specific Manager (e.g., IProductManager)

Once you have the ManagerFactory, you can ask it for specific "managers" based on the type of operation you want to perform.

// From CommerceClient.cs (simplified) public sealed class CommerceClient(ManagerFactory factory, ILogger logger) { private readonly ManagerFactory factory = factory; // ... other fields ... public async Task SearchProductsByKeyword(string keyword) { // Ask the factory for a Product Manager IProductManager productManager = factory.GetManager<IProductManager>(); // ... (define search criteria) ... // Use the Product Manager to search for products var products = await productManager.Search(criteria, settings); // 'products' is a collection of C# Product data models! foreach (Product product in products) { logger.Log(LogStatus.Info, $"> [{product.ItemId,10}] {product.SearchName,-20} ${product.Price,5}"); } } }

What happens here: Inside the CommerceClient, when we want to search for products, we call factory.GetManager<IProductManager>(). This gives us an instance of IProductManager. Now, instead of building an HTTP request, we can just call productManager.Search(), passing in a C# ProductSearchCriteria object. The SDK takes care of sending this request to the CSU and returns a collection of C# Product objects, which are instances of our Commerce Domain Data Models.

3. Performing Other Operations (e.g., Cart Management)

The process is similar for other commerce operations. You get the relevant manager and call its methods.

// From CommerceClient.cs (simplified) public async Task<Cart> AddItemsToCartAsync(string cartId, long productId, decimal quantity = 1, long? cartVersion = null) { // Get the Cart Manager ICartManager cartManager = factory.GetManager<ICartManager>(); // Prepare a C# CartLine object (part of the Cart data model) var cartLine = new CartLine { ProductId = productId, Quantity = quantity }; // Use the Cart Manager to add items to the cart var cart = await cartManager.AddCartLines(cartId, [cartLine], cartVersion); // 'cart' is an updated C# Cart data model! logger.Log(LogStatus.Info, $"> There are now {cart.CartLines.Count} lines in the cart."); return cart; }

What happens here: Here, we get an ICartManager from the factory. To add items, we create a CartLine object (another one of our Commerce Domain Data Models) and pass it to cartManager.AddCartLines(). The SDK handles the rest, and returns an updated C# Cart object.

This is the power of the Retail Proxy SDK: it abstracts away all the complexity of web service communication, letting you interact with Dynamics 365 Commerce using familiar C# programming constructs.

Under the Hood: The "Proxy" in Action

The "proxy" in Retail Proxy SDK means it acts as a stand-in or representative for the actual Headless Commerce Engine (CSU). When you call a method on IProductManager, you're not directly calling the CSU. You're calling the proxy, which then handles the communication.

Let's visualize this process:


What this diagram shows:

  1. Your .NET application calls a method on an SDK manager (e.g., productManager.Search()).
  2. The Retail Proxy SDK intercepts this call. It takes your C# ProductSearchCriteria object and converts it into a standard HTTP request, often using JSON format. It also adds necessary authentication headers.
  3. The SDK then sends this HTTP request over the network to the Headless Commerce Engine (CSU).
  4. The CSU receives the request, processes it (perhaps by querying the Commerce Database for product information, applying CRT Extensibility rules, or using External Service Integration Connectors).
  5. The CSU sends back an HTTP response, typically containing the results in JSON format.
  6. The Retail Proxy SDK receives this HTTP response. It then takes the JSON data and converts it back into familiar C# objects, like a List<Product>.
  7. Finally, the SDK returns these C# objects to your .NET application.

The core components of the Retail Proxy SDK are typically found in NuGet packages like Microsoft.Dynamics.Commerce.Proxy.ScaleUnit. This package contains the compiled C# classes and interfaces that define the managers, data models, and the underlying communication logic. For example, you can see this reference in the SampleCommerceProductPublisher project:

<!-- From: Commerce/HeadlessCommerceSamples/Assets/SampleCommerceProductPublisher/ProductPublisher.Core/ProductPublisher.Core.csproj --> <Project Sdk="Microsoft.NET.Sdk"> <ItemGroup> <PackageReference Include="Microsoft.Dynamics.Commerce.Proxy.ScaleUnit" Version="9.51.24345.13" /> <!-- ... other package references ... --> </ItemGroup> </Project>

What this means: By including the Microsoft.Dynamics.Commerce.Proxy.ScaleUnit NuGet package in your .NET project, you get access to all the necessary classes (like ManagerFactoryIProductManagerProductCart, etc.) that make up the Retail Proxy SDK.

Why the Retail Proxy SDK Matters

The Retail Proxy SDK is a crucial tool for any developer building custom integrations or applications for Dynamics 365 Commerce.

FeatureWithout Retail Proxy SDKWith Retail Proxy SDK
Development SpeedSlow, requires extensive knowledge of REST APIs, JSON schemas, and authentication flows.Fast: Focus on business logic, not communication details. Use familiar C# objects and methods.
Code ComplexityHigh, involves manual HTTP request construction, parsing, and error handling.Low: SDK handles networking, serialization, and deserialization, reducing boilerplate code.
MaintainabilityDifficult to update with API changes; brittle integrations due to tight coupling.Easier: SDK provides a stable abstraction layer. Updates to the underlying API are often managed by SDK versions.
SecurityRequires manual implementation of authentication, potentially leading to vulnerabilities.Built-in Security: SDK handles secure token management and authentication automatically.
Error HandlingRequires parsing raw HTTP error codes and messages.Structured Errors: SDK translates web errors into meaningful C# exceptions.

In essence, the Retail Proxy SDK transforms the complex task of talking to a remote web service into a straightforward, object-oriented programming experience, enabling developers to build powerful and reliable commerce applications more efficiently.

Conclusion

In this chapter, we've demystified the Retail Proxy SDK, understanding it as the essential "remote control" or "translator" for .NET applications communicating with the Headless Commerce Engine (CSU). We saw how it abstracts away complex web requests, authentication, and data conversions, allowing developers to interact with Commerce APIs using familiar C# objects and methods. By leveraging the SDK, we can build custom applications faster and with fewer errors.

In the next chapter, we'll shift our focus to POS UI Extensions (React & Fluent UI). This will show us how to customize the actual user interface of the Point of Sale system, allowing us to build engaging and tailored experiences for store associates, often leveraging the underlying commerce logic we've learned about.


Chapter 6: POS UI Extensions (React & Fluent UI)

Welcome back! In our previous chapters, we've explored the inner workings of Dynamics 365 Commerce. In Chapter 5: Retail Proxy SDK, we learned how client applications can easily talk to the powerful Headless Commerce Engine (CSU) and interact with its Commerce Domain Data Models and Commerce Runtime (CRT) Extensibility using the Retail Proxy SDK.

But what if you need to change what the actual cashier or store associate sees and interacts with on the Point of Sale (POS) screen? The standard Dynamics 365 Store Commerce App is comprehensive, but businesses often have unique needs, custom workflows, or specific information they want to display. How do you add a brand-new button, a custom information panel, or a unique interactive area to the POS without rewriting the entire application?

This is where POS UI Extensions (React & Fluent UI) come into play.

What Problem Do They Solve?

Imagine your cashier is using the Dynamics 365 Store Commerce App at the checkout. It has all the standard buttons for sales, returns, and payments. But your store has a new loyalty program, and you want to display a custom "Customer Loyalty Points" summary right on the customer details screen. Or perhaps you want to add a special "Quick Order" button that simplifies a specific workflow.

The challenge is:

  • Customization: How do you add unique features to the existing POS interface?
  • Modern Look and Feel: How do you ensure any new additions look professional, are easy to use, and match the modern aesthetic of Dynamics 365, especially across different devices (desktop, tablet)?
  • Maintainability: How do you make these changes without breaking the core POS application or making future updates difficult?

POS UI Extensions solve these problems by allowing you to add custom, interactive "widgets" (built using React components) directly into the Dynamics 365 Store Commerce App. These widgets are styled with a consistent design language (Fluent UI), ensuring they look and feel like a natural part of the application. This approach enables businesses to create unique and visually appealing workflows directly within the Point of Sale application, enhancing user experience and accessibility while seamlessly integrating with the existing POS framework.

The Core Idea: Modular Widgets and Consistent Design

The core idea revolves around two powerful technologies:

  1. React (for "Widgets"): Think of React as a very efficient LEGO building system. Instead of building an entire application from scratch, you build small, independent, and reusable "LEGO bricks" called components. Each component is a self-contained piece of the user interface, like a button, a text box, or an entire panel. You can then assemble these components to create complex, interactive screens. This makes building dynamic UIs much faster and easier to manage.

  2. Fluent UI (for "Consistent Look"): Fluent UI is like the official LEGO instruction manual and pre-designed, high-quality LEGO bricks that match the overall Dynamics 365 style. It provides a set of ready-to-use UI components (buttons, input fields, dropdowns, etc.) that automatically look and behave consistently with the rest of Dynamics 365. Critically, Fluent UI also focuses on accessibility, ensuring your custom additions are usable by everyone, including those with disabilities.

Together, React and Fluent UI let you "plug in" your custom UI pieces into the existing POS application, making it feel like a seamless part of the product.

How it Works: Adding a Custom Counter to the Customer Details Page

Let's look at a concrete example from the provided CommerceReactFluentUISample project: adding a simple interactive counter to the Customer Details page within the Dynamics 365 Store Commerce App.

Here's how we achieve this:

1. Defining the React Component (Counter.tsx)

First, we create our "widget" using React. This component handles its own display and interaction logic.

// From: Commerce/CommerceReactFluentUISample/Src/ReactComponents/Counter.tsx (simplified) import React, { useState } from 'react'; import { FluentProvider, Button, makeStyles } from '@fluentui/react-components'; const useStyles = makeStyles({ custombutton: { /* ... styling rules ... */ } }); export const Counter = () => { const [count, setCount] = useState(0); // State for our counter const increment = () => setCount((c) => c + 1); const decrement = () => setCount((c) => c - 1); const reset = () => setCount(0); const styles = useStyles(); return ( <FluentProvider> <div> <h2>Counter: {count}</h2> <Button onClick={increment} className={styles.custombutton}>Increment</Button> <Button onClick={decrement} className={styles.custombutton}>Decrement</Button> <Button onClick={reset} className={styles.custombutton}>🔁 Reset</Button> </div> </FluentProvider> ); }

What happens here: This Counter.tsx file defines our interactive "widget." It uses React's useState to manage a number (count) that goes up or down. The FluentProvider and Button components from Fluent UI ensure our counter's buttons look consistent and are styled correctly, making them blend seamlessly with the rest of the POS application. This is our reusable UI "brick."

2. Rendering the React Component (RenderCounter.tsx)

Next, we need a small helper that takes our React component and actually puts it into a specific placeholder on the web page.

// From: Commerce/CommerceReactFluentUISample/Src/ReactComponents/RenderCounter.tsx (simplified) import React from 'react'; import { createRoot } from "react-dom/client"; import { Counter } from './Counter'; export const renderCounter = (element: HTMLElement): void => { if (!element) { throw new Error("Invalid element. Element cannot be null or undefined."); } const root = createRoot(element); // Create a React "root" root.render(<Counter />); // Render our Counter component into the element };

What happens here: The renderCounter function is a crucial helper. It receives an HTMLElement (which will be a div or similar placeholder in our POS extension) and uses createRoot and root.render from react-dom/client to insert our Counter React component into that specific HTML element. This is the "glue" that mounts our React widget onto the page.

3. Providing an HTML Container (SampleReactPanel.html)

The POS extensibility framework uses HTML files as containers for custom controls. This file provides the "spot" where our React component will be rendered.

<!-- From: Commerce/CommerceReactFluentUISample/ViewExtensions/ReactExtensions/SampleReactPanel.html (simplified) --> <div id="samplereactcomponent" data-bind="with: this"> <!-- Our React component will be rendered here --> </div>

What happens here: This SampleReactPanel.html file is a simple HTML snippet that defines an empty div with the ID samplereactcomponent. This div acts as the blank canvas or "placeholder" where our Counter React component will eventually appear. The data-bind="with: this" is a detail related to the underlying Knockout.js framework used by the POS, but for our purposes, just know this div is our target.

4. Integrating with the POS (SampleReactPanel.ts)

This is the "bridge" between the Dynamics 365 POS framework (which uses TypeScript and Knockout.js) and our React components. It tells the POS where and when to display our React widget.

// From: Commerce/CommerceReactFluentUISample/ViewExtensions/ReactExtensions/SampleReactPanel.ts (simplified) import { CustomerDetailsCustomControlBase } from "PosApi/Extend/Views/CustomerDetailsView"; import { renderCounter } from "reactcomponents"; // Our React rendering helper import ko from "knockout"; // The POS's UI framework export default class SampleReactPanel extends CustomerDetailsCustomControlBase { public onReady(element: HTMLElement): void { // Apply Knockout bindings (standard POS requirement) ko.applyBindingsToNode(element, { template: { name: SampleReactPanel.TEMPLATE_ID, data: this, }, }, this ); // Find our placeholder div let sampleReactComponentElem: HTMLDivElement = document.getElementById( "samplereactcomponent" ) as HTMLDivElement; // Call our React rendering helper to put the Counter inside the div renderCounter(sampleReactComponentElem); } public init(state: any): void { this.title("React panel"); this.isVisible = true; // Make sure our panel is visible } }

What happens here: The SampleReactPanel class extends CustomerDetailsCustomControlBase, which is a special class from the POS SDK telling the system this is a custom control for the Customer Details page.

  • The onReady method is called by the POS when the control is ready to be displayed.
  • Inside onReady, we find our div placeholder (samplereactcomponent) from the HTML.
  • Crucially, we then call our renderCounter function (from RenderCounter.tsx) and pass it this div. This is the moment our React Counter component gets "mounted" and appears on the POS screen.

5. Registering the Extension (manifest.json)

Finally, we need to tell the Dynamics 365 Store Commerce App about our new extension and where to find its files. This is done in the manifest.json file.

// From: Commerce/CommerceReactFluentUISample/manifest.json (simplified) { "$schema": "./devDependencies/schemas/manifestSchema.json", "name": "Contoso.Pos.ReactExtensions.Sample", "publisher": "Contoso", "version": "1.0.0", "minimumPosVersion": "9.29.0.0", "dependencies": [ { "alias": "reactcomponents", "format": "amd", "modulePath": "DistReact/reactcomponents" } // ... other dependencies ... ], "components": { "extend": { "views": { "CustomerDetailsView": { // We're extending the Customer Details page "controlsConfig": { "customControls": [ { "controlName": "sampleReactPanel", "htmlPath": "ViewExtensions/ReactExtensions/SampleReactPanel.html", // Our HTML placeholder "modulePath": "ViewExtensions/ReactExtensions/SampleReactPanel" // Our TypeScript bridge } ] } } } } // ... other configurations ... } }

What happens here: This manifest.json file is like a "map" for the POS.

  • The dependencies section tells the POS where to find our compiled React code. Notice modulePath: "DistReact/reactcomponents", which points to the JavaScript file generated by our React build process.
  • The extend.views.CustomerDetailsView.controlsConfig.customControls section declares our sampleReactPanel. It specifies:
    • The controlName (a unique identifier).
    • The htmlPath (pointing to SampleReactPanel.html, our placeholder).
    • The modulePath (pointing to SampleReactPanel.ts, our integration logic).

This tells the POS: "On the Customer Details screen, add a custom control named sampleReactPanel, use this HTML for its structure, and run this TypeScript code to make it interactive."

Under the Hood: The Build and Runtime Flow

Building POS UI Extensions involves a few steps to prepare your React/TypeScript code, and then the POS loads them at runtime.

The Build Process

Your React and TypeScript code (.tsx.ts files) cannot be directly understood by web browsers or the POS. They need to be converted into standard JavaScript.


What this diagram shows:

  1. Source Code (Src folder): You write your React components in TypeScript (.tsx files).
  2. Webpack & Babel: These are build tools. Webpack bundles all your React files into a single (or a few) optimized JavaScript file(s). Babel (configured in babel.config.json) converts modern TypeScript/JavaScript into older JavaScript that browsers and the POS can understand. This process is triggered by npm install and npx webpack commands in the Src folder, as defined in package.json and webpack.config.js.
  3. Output (DistReact folder): The bundled and converted JavaScript files (like reactcomponents.js) are placed in the DistReact folder.
  4. POS.csproj: This .NET project file is configured (via its PreBuild target) to first run the npm and webpack commands. It then excludes the original Src folder from the .NET build but includes the compiled .js files from the DistReact folder to ensure they are part of the final POS deployment package.
  5. Deployment: When you deploy your POS extension, the compiled JavaScript files are included alongside your manifest.json and HTML files.

The Runtime Flow in POS

Once deployed, here's what happens when a cashier opens the Customer Details page:

SampleReactPanel.htmlSampleReactPanel.tsSrc/ReactComponents/Counter.tsx (Logic)DistReact/reactcomponents.jsViewExtensions/ReactExtensions/SampleReactPanel.tsViewExtensions/ReactExtensions/SampleReactPanel.htmlmanifest.jsonStore Commerce AppSampleReactPanel.htmlSampleReactPanel.tsSrc/ReactComponents/Counter.tsx (Logic)DistReact/reactcomponents.jsViewExtensions/ReactExtensions/SampleReactPanel.tsViewExtensions/ReactExtensions/SampleReactPanel.htmlmanifest.jsonStore Commerce App1. Customer Details View Loads, Checks for Extensions2. Identifies 'sampleReactPanel' for CustomerDetailsView3. Loads HTML Template4. Instantiates SampleReactPanel5. Calls renderCounter function6. Executes React code7. Renders UI into HTML placeholder8. Inserts HTML into placeholder div9. Custom UI now visible on screen

What this diagram shows:

  1. The Store Commerce App starts and loads the Customer Details view.
  2. It consults its manifest.json to find any registered extensions for this view.
  3. The manifest.json points to SampleReactPanel.html (the placeholder) and SampleReactPanel.ts (the integration logic).
  4. The SampleReactPanel.ts class is activated. In its onReady method, it finds the div in SampleReactPanel.html and calls the renderCounter function.
  5. The renderCounter function (from the bundled reactcomponents.js) then takes the core logic of our Counter.tsx component and "renders" it into that div, making our interactive counter visible to the user.

Why POS UI Extensions Matter

POS UI Extensions are crucial for businesses seeking to tailor their Dynamics 365 Store Commerce App experience.

FeatureWithout POS UI ExtensionsWith POS UI Extensions (React & Fluent UI)
User InterfaceLimited to standard Dynamics 365 POS screens and workflows.Customizable: Add unique buttons, panels, and interactive elements for specific business needs.
Modern ExperienceUI might feel constrained by existing framework, less responsive.Modern & Responsive: Leverage React's dynamic capabilities and Fluent UI's responsive design for a modern feel.
Consistency & AccessibilityCustom additions may look out of place or lack accessibility features.Consistent & Accessible: Fluent UI ensures a uniform look and feel, and built-in accessibility.
Developer ProductivityRequires deep knowledge of legacy POS extensibility frameworks, harder to build complex UIs.Faster Development: Use popular, component-based frameworks like React, leading to quicker development and easier maintenance.
ScalabilityCustomizations are tightly coupled, harder to manage across many stores or updates.Modular & Scalable: Component-based approach allows for reusable parts and isolated changes, simplifying deployment and updates.

In essence, POS UI Extensions empower businesses to evolve their in-store experience, making the Store Commerce App truly their own, while benefiting from modern web technologies and Microsoft's design principles.

Conclusion

In this chapter, we've explored POS UI Extensions (React & Fluent UI), understanding how they enable the modernization and customization of the Dynamics 365 Store Commerce App's user interface. We learned that React allows us to build modular, interactive "widgets," while Fluent UI ensures a consistent, accessible, and professional look. Through the example of adding a custom counter, we saw how these extensions are defined, rendered, integrated, and registered within the POS framework, giving businesses the flexibility to create tailored and engaging cashier experiences.