Practical .NET

Creating a Flexible, Extendable Update Process

In any real-world business, updating data isn't simple and changing the rows in the table is just the start of a complex set of processes. Fortunately, you can break those updates down into a bunch of simple processes that can be easily extended.

The Command Query Responsibility Separation (CQRS) pattern suggests that you separate updates from queries because the two processes are very different. And that's true, especially because updates are where your application usually runs into problems. If all you had to do was move data from the UI and into tables in your database, all applications would be easy. Sadly, that's not even remotely true.

Take a sales order, for example. Sure, you have to update the sales order header and sales order detail tables from the user's data. But that's just the start of the process. You also have to arrange to bill the customers, update inventory and arrange to ship the goods to the buyer. Both you and the customer are probably also interested in the order's current status so, as you make changes, you also want to write to some audit log.

Domain-Driven Design (DDD) recommends that you break complex enterprise models like this one down into a series of simpler domains that hand off work to each other. In my sales order I've already defined domains that include inventory management, accounting and shipping. Who knows what goes on in those domains?

In fact, if you're the developer for the sales order application, it's probably safe to say two things: One, you probably don't know of all of the activities that follow on from creating a sales order; two, in the future, your organization will probably add more activities that need to be done (and those "more activities" are impossible to predict).

The Command Stack
Therefore, what you need is a command stack: a set of commands to be executed after the creation of a sales order. Over time, your organization will add and remove commands from this stack, which means that command stack must be built dynamically, based on the organization's current needs.

This idea of a command stack is just an extension of the Command design pattern but, unlike the Command design pattern, a command stack separates the processing from the data that drives the processing. With the command stack, you build an object with all of the necessary data and pass that to some other objects (processors) that carry out the activity. A dispatcher (or command bus/hub) dynamically finds all the related processors and executes them.

If this definition sounds like an event that can have multiple handlers attached to it (including handlers from other domains), then that's probably a pretty good mental model. One of the beauties of this design is that, like an event, the application doesn't have to be aware of all of the relevant processors.

Defining the Command Object
A Command object, in addition to passing the necessary data to the processor, is going to have to support tracking and auditing. Useful properties to support those needs might include CreatedBy and CreationDate properties. Therefore, a base CommandObject class might look like this:

Public MustInherit Class CommandObject
  Property CreationDate As DateTime
  Property CreatedBy As String
  Public Sub New()
    CreationDate = DateTime.Now
  End Sub
End Class

But the primary purpose of the CommandObject is to carry the data that processors will need in order to carry out their tasks. In the case of my sales order, that could be the entire sales order or just the sales order Id. One of the assumptions of DDD is that there are no "enterprise-wide" objects -- instead, each domain can have its own, tailored implementation of the SalesOrder object. So, rather than force another domain to use the order entry application's SalesOrder object, I'll just pass the SalesOrderId. The classes that process the SalesOrder will have to re-fetch the sales order from the database, but this does have the advantage of ensuring that those classes get the latest version of the SalesOrder data.

A command object that inherits from CommandObject to support creating a SalesOrder just needs a property to hold the SalesOrderId. That object might look like this:

Public Class SalesOrderCreatedCommand
  Inherits CommandObject
  Public Property SalesOrderId As String
End Class

Dispatching Commands to Processors
My Sales Order application will now, after performing any immediate updates, create the SalesOrderCreated object and pass it to a dispatcher. The overhead on creating a dispatcher is probably sufficiently high that you won't want to create it more than once (more on this later in this column) so, in the following code, I've assumed it's running in an ASP.NET application and I retrieve my dispatcher from the Cache:

Dim soc As New SalesOrderCreatedCommand
soc.CreatedBy = "SalesOrderApplication"
soc.SalesOrderId = salesOrder.Id
Dim cd As CommandDispatcher 
cd = TryCast(Cache("CommandDispatcher"), CommandDispatcher) 
If cd IsNot Nothing Then
  cd = New CommandDispatcher()
  Cache("CommandDispatcher") = cd
End If
cd.ProcessAllCommands(soc)

Before looking at the CommandDispatcher, let's look at a typical processor. I want all of my processors to look alike so, to achieve that, I define the IProcessor interface (if I thought there was some code that could be shared among all processors, I would've used a base class to achieve the same goal).

Here's a sample interface that includes a single method the dispatcher will call to start processing the command object:

Public Interface IProcessor
  Sub ProcessACommand(co As CommandObject)
End Interface

There's no reason, by the way, why you can't use a command dispatcher to handle immediate updates (though it's probably over-engineering the solution). In that scenario, the ProcessACommand method should return an error message object of some kind that will allow the dispatcher to handle errors or return the errors to the application.

A class that actually handles some of the processing might look like the one in Listing 1, which handles reserving inventory for a sales order.

Listing 1: A Processor Class
Public Class SalesOrderCreatedInventoryManager
  Implements IProcessor

  Public Sub ProcessACommand(CommandObject As CommandObject) Implements IProcessor.Process
    Dim socc As SalesOrderCreatedCommand
    socc = TryCast(CommandObject, SalesOrderCreatedCommand)
    If socc IsNot Nothing Then
      Dim so As SalesOrder
      so = InventoryRepository.GetOrderBySalesOrderId(socc.SalesOrderId)
      If so IsNot Nothing Then
        For Each sod In so.SalesOrderDetails
          ...reserve inventory for products/quantity listed on SalesOrderDetails...
        Next
      End If
    End If
  End Sub

End Class

This processor would be part of the Inventory domain and the only thing that it needs to share with the SalesOrder application's domain is the SalesOrderCreatedCommand object. The SalesOrder object used in the Inventory domain doesn't have to be the same SalesOrder used by the application (in fact, the Inventory domain's SalesOrder object is probably a read-only value object). The InventoryRepository class that retrieves the Inventory domain's sales order object might even be retrieving data from a different database than the SalesOrder application.

Finding Processors
With all of that infrastructure in place, all the CommandDispatcher has to do is find the processors tied to SalesOrderCreatedCommand and call their ProcessACommand method, passing the SalesOrderCommand object. Therefore, the first thing the dispatcher needs is a collection that lets it hold a list of dispatchers associated with the Type of a CommandObject:

Public Class CommandDispatcher
  Private processors As New Dictionary(Of Type, List(Of IProcessor))

The next step is to load that list with the available processors. You could use a dependency injection engine of some kind that would find the related objects and load them (I'd choose MEF, but that's just me). However, you could just as easily have the CommandDispatcher instantiate a set of CommandProcessors when it starts up, as shown in Listing 2 (it's loading all the processors that could make the CommandDispatcher expensive to instantiate). The list associates each processor with the type of a CommandObject.

Listing 2: Loading Processors for Command Objects
Public Sub New()
  AddToProcessorList(GetType(SalesOrderCreatedCommand), New SalesOrderCreatedInventoryManager)
  AddToProcessorList(GetType(SalesOrderCreatedCommand), New SalesOrderCreatedAccountsManager)
  AddToProcessorList(GetType(SalesOrderDetailAddedCommand), New SalesOrderDetailAddedInventoryManager)
  AddToProcessorList(GetType(CustomerCreatedCommand), New CustomerCreatedAccountsManager)
  ...more processors...
End Sub

Public Sub AddToProcessorList(coType As Type, proc As IProcessor)
  Dim processorList As List(Of IProcessor)
  If Not processors.Keys.Contains(coType) Then
    processors.Add(coType, New List(Of IProcessor))
  End If
  processorList = processors(coType)
  processorList.Add(proc)
End Sub

The code to find all the processors for a command object, pass the command object to the processor's ProcessACommand method and start processing is relatively straightforward. When passed a command object, the code just loops through the collection of processors looking for any processor associated with the CommandObject type. When a processor is found, the CommandObject is passed to the processors ProcessACommand method:

Public Sub ProcessAllCommands(co As CommandObject)
  Dim procs As List(Of IProcessor)
  procs = processors(co.GetType)
  For Each proc In procs
    proc.ProcessACommand(co)
  Next
End Sub

The system could be extended in a variety of ways -- there's no reason why some or all of the processors couldn't run in parallel, for example (a SupportsParallel property in the IProcessor interface could be used to signal that). The CommandDispatcher doesn't even have to be called directly from the Sales Order application -- the application might write the CommandObject to a message queue that the CommandDispatcher reads (that would improve the responsiveness of the application). There are all sorts of options to explore.

Regardless, as the business adds new functionality related to creating a sales order, all developers have to do is create new processors and add them to the dispatcher. And you've now taken advantage of the CQRS pattern to build a dedicated command process that you can evolve as you see fit.

About the Author

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube