Index

Part 0 – Introduction
Part 1 – Persistence
Part 2 - Domain Events

A while back I watched Udi Dahan’s presentation on Making Roles Explicit and realised that it addressed a number of concerns with implementing and working with domain models that I didn’t even know I had. It’s rather awesome so I suggest you go watch it. I’ll wait.

Based on these concepts Avernus seeks to provide:

  • The infrastructure to support a role interface based API for your domain model including:
  • A simple abstraction to find and save role interfaces
  • Default strategies for fetching and saving role interfaces that “just work”
  • The ability to override the default strategies on a case-by-case basis in order to improve performance or adapt to client requirements. The substitution of strategies is transparent to the client API.
  • The ability to raise events within the domain to allow access to additional services without attempting to perform injection or direct instantiation of dependencies inside the domain model.

It should be noted that the Avernus Persistence infrastructure is intended only for executing business login in a domain. It is not intended to provide query access to data which should be handled separately.

Key Abstractions

For general usage of Avernus you only need to know a few key abstractions. These start with IEntity, a marker interface that all role interfaces extend. Extending IEntity implies that an interface will:

  • Will provide one (and only one) business method that performs the action the role implies.
  • May be fetched by the underlying infrastructure
  • May be saved to the underlying infrastructure.

A domain implementation of the role interface is provided. This is often a class mapped to the database by NHibernate but this is not mandatory. By providing appropriate fetching and saving strategies a non-mapped implementation may be used (although this type would be logically part of the domain). The client is not expected to (and is required not to) care about the implementation of the role interface with which it is provided. The client simply invokes the business method in order to perform the necessary business function. How this function is implemented is a concern of the implementer of the role interface not the client.

The second key abstraction is the IPersistenceFacade interface. This is provided to the client as their primary gateway into the system. It has two methods:

  • FindById is a method for locating a role interface based on a key. The nature of the key is defined by the role interface (as part of its implicit contract). The client should not care how the persistence facade obtains the instance it returns.
  • Save supports indicating to the infrastructure that the instance is to be persisted. It is a requirement that the client calls Save after invoking the role interface’s business method.

These two abstractions form the entirety of the API exposed to the client.

Avernus uses strategies in order to fetch and save role interfaces. It provides default implementations that support the general usage case. The default fetching strategy simply requests the relevant mapped object from NHibernate with the supplied id as the primary key. The default saving strategy passes the role interface directly to the NHibernate ISession.Save() method. These implementations work in the general case but are not always ideal. In particular the fetching strategy loads dependent objects as per the NHibernate mapping configuration. This means that it may be loading either too much data (which is directly inefficient) or too little (which is inefficient when it is subsequently lazy loaded). Also you may wish to load a role interface based on something other than the mapped object primary key. To handle these scenarios the IFetchingStrategy<T> is provided.

By implementing IFetchingStrategy<T> and registering it with the IoC container you are telling the infrastructure that you wish to be responsible for how a specific role interface is retrieved from the persistence store. The default implementation simply provides a type conversion so that a mapped type that NHibernate understands may be requested. A custom implementation may provide anything it likes so long as the result implements the requested role interface. In most cases it will execute a query against NHibernate specifying custom loading and/or a query other than by the primary ID. This allows more efficient retrieval of the information necessary to perform the specific business action. When implementing IFetchingStrategy<T> it is the developers responsibility to ensure that the query is efficient.

The fetching strategy is complemented by ISavingStrategy<T> which handles persisting a role interface back into the persistence store. As with the fetching strategy there is a default implementation that handles most cases. However in some scenarios it is necessary to provide more information to NHibernate in order for it to operate efficiently or (in limited scenarios) at all. This may include explicitly calling Save on a child object so that NHibernate is not forced to query for its existence or handle the persistence of a non-mapped object constructed by a custom fetching strategy. Saving strategies are provided to the framework by registering an appropriate implementation of ISavingStrategy<T> with the IoC container.

Domain Concepts

Using the Avernus Persistence infrastructure requires adherence to a few rules:

  • All operations happen against a domain object. This impacts domain design as every business method must have an instance it can be executed against. This requires thinking about the context in which an initial interaction will operate. For instance in the example below adding a new customer occurs in the context of a Store, an existing domain construct that can implement the role interface. This does not necessarily need to be a mapped object if you are willing to write appropriate fetching and saving strategies.
  • A role interface provides a single business method. This method gets passed everything required to perform the business action. The business logic is then executed in the domain.
  • Role business methods do not return values. As they are often invoked in NServiceBus handlers in response to one-way messages they cannot assume that the client can do anything with a return value.
  • The business method is invoked only a single time. If you need to invoke it multiple times then the role is incorrectly specified.
  • All role interfaces must be saved to ensure that the appropriate save logic is executed. The client should not know the cases when automatic NHibernate behaviour means this is not required. Relying on such cases will introduce bugs if this should cease to be true in future.
  • Clients are responsible for handling the NHibernate Session and any transaction to be used. When integrated with NServiceBus Avernus provides infrastructure to handle this automatically. For other uses the StandaloneUnitOfWork class is provided (as seen in the example below)

Sample

The Avernus source code (available at: https://github.com/ColinScott/Avernus) includes a sample executable that demonstrates the usage of the Avernus Persistence infrastructure. Avernus Store is a simple console application that shows how the framework is used with a very simple domain model (in practice such a domain model is rather smaller than would generally warrant using Avernus).

The Domain namespace contains two role interfaces (IAddCustomer and ICreateOrder) that define business functions that the domain supports. It also includes the CustomerDetails DTO that is used in the add customer API. The client uses only types in this namespace, it does not refer to or in any way use the mapped objects themselves.

The DomainObjects namespace is where we find the mapped objects (complete with mapping files, Fluent NHibernate support is pending). Here we see that IAddCustomer is implemented on the Store object. Store is an aggregate root used as a context into which customers may be added. We create an initial store through the use of database scripts when the database is generated, this forms part of the basic system implementation. Additionally we see that Order instances are created through ICreateOrder which is a function of the Customer.

The FetchingStrategies namespace is used to store a custom fetching strategy to be used with the ICreateOrder role interface. This strategy changes the lookup of the mapped instance to be the customer name when using the ICreateOrder role.

The main Program class of the console application demonstrates setting up the Avernus infrastructure and some sample operations. It will also generate the store database (dropping any existing database) using Avernus infrastructure not described here.

After the call to generate the database we start configuring Avernus (in practice you are unlikely to perform this configuration in the same class as you use to execute domain logic). We start with StructureMap configuration as shown:

private static void ConfigureStructureMap()
{
    ObjectFactory.Initialize(initialise =>
        {
            initialise.AddRegistry<CoreRegistry>();
            initialise.AddRegistry<PersistenceDomainRegistry>();

            initialise.For<IPersistenceConfiguration>()
                .Use((IPersistenceConfiguration) ConfigurationManager.GetSection("persistence"));
            initialise.For<IPersistenceConfigurator>()
                .Singleton()
                .Use<PersistenceConfigurator<MsSql2008Dialect, SqlClientDriver>>();
            initialise.For<IContextStrategy<ISession>>()
                .Use<ThreadStaticContextStrategy<ISession>>();
            initialise.For<ISession>().Use(context => context.GetInstance<IContextStrategy<ISession>>().Retrieve());

            initialise.Scan(scan =>
                {
                    scan.TheCallingAssembly();
                    scan.ConnectImplementationsToTypesClosing(typeof(IFetchingStrategy<>));
                });
        });
}

 

To start with we add the relevant Avernus registries. CoreRegistry comes from the base Avernus library and performs registration of some low level components. PersistenceDomainRegistry registers the persistence specific components that are common to all usages.

Once we have done this we instruct StructureMap on how to locate the persistence configuration. We then register the PersistenceConfigurator<TDialect, TDriver> class. By specifying the appropriate type parameters here you can control which NHibernate dialect and driver classes to use. This prevents Avernus from being tied to any specific database. This configuration is specified in code as Avernus is not intended for systems which must select their database at deployment time. You can include conditional logic to register the appropriate classes if you wish but this scenario is not officially supported.

Next we register the context strategy used to hold session information. IContextStrategy<T> is an abstraction that hides the context mechanism (call context, thread local, ASP.NET etc.) from client code. Here we specify that we are using a thread local strategy.

The registration for ISession is necessary when executing outside the NServiceBus support (which has alternate infrastructure for this case). In this case we specify that the current context strategy should be retrieved and the session loaded from it.

Finally we scan the current assembly looking for anything that closes the open IFetchingStrategy<> generic type. This handles detection of any fetching strategies and their automatic registration. An additional call to ConnectImplementationsToTypesClosing can be made to handle ISavingStrategy<T> if it is being used.

After registering the relevant types with StructureMap we configure Avernus. This is done using:

private static void ConfigureInfrastructure()
{
    ObjectFactory.GetInstance<IPersistenceConfigurator>()
        .ConfigurePersistence(new[] {typeof(Store).Assembly});
    ObjectFactory.GetInstance<IStrategyRegistrar>().Register();
}

The first call creates the NHibernate ISessionFactory instance and configures it for use. The argument to this call is a list of assemblies that contain mapped objects. This supports scenarios where you want to pull in business logic from multiple assemblies (which is occasionally very useful). Subsequent to this we get an instance of IStrategyRegistrar and invoke it. This causes the Avernus infrastructure to locate all the objects mapped by the previous call and to configure Avernus to use them (primarily by providing default strategies for them). The Avernus persistence layer is now ready to use. Personally I generally just copy most of this when starting a new project, the primary gotcha is ensuring that all the relevant assemblies are passed to the ConfigurePersistence method.

Now that this is done we can do some work against the domain.

private static void RunExample()
{
    var customerId = Guid.NewGuid();

    var persistenceFacade = ObjectFactory.GetInstance<IPersistenceFacade>();

    using (var unitOfWork = new StandaloneUnitOfWork())
    {
        var addCustomer = persistenceFacade.FindById<IAddCustomer>("Avernus");

        addCustomer.AddCustomer(new CustomerDetails {CustomerId = customerId, Name = "New Customer"});

        persistenceFacade.Save(addCustomer);

        unitOfWork.Commit();
    }

    using (var unitOfWork = new StandaloneUnitOfWork())
    {
        var createOrder = persistenceFacade.FindById<ICreateOrder>("New Customer");

        createOrder.AddOrder("ABC012345");

        persistenceFacade.Save(createOrder);

        unitOfWork.Commit();
    }
}

This demonstrates creating a customer then adding an order to a customer. To start we generate a customer ID. This is necessary in scenarios where the client must know an identifier so that they may perform additional actions. Depending on scenario you may wish to use an alternate reference number with business significance.

Next we obtain an instance of IPersistenceFacade. This interface is stateless and the implementation may be reused. Alternately you may request a new instance if tracking it is undesirable.

Once we have these resources we create an instance of StandaloneUnitOfWork. This type is specific to cases where the code is not running in the context of another framework and is not used in scenarios such as NServiceBus integration. It encapsulates the creation of the ISession instance and its registration in the context as well as the creation of a transaction. The instance is disposable and handles committing the transaction when it is itself disposed.

Next comes the actual usage of Avernus. We request an implementation of IAddCustomer from the persistence facade specifying a store of “Avernus” (often this identifier will be carried on an incoming message). We then invoke the business method to create the actual customer passing in a CustomerDetails instance with the relevant data. This is followed by the Save call to ensure the information is persisted.

Lastly in the unit of work we call its Commit method. This flags that the operation completed successfully so that we know to commit and not roll back the transaction of dispose. If this method is not called we assume that an exception was thrown somewhere in the operation. This prevents us having to do nasty and inefficient detection of whether the current thread is in an exception inside the StandaloneUnitOfWork dispose method.

We then repeat this pattern using the ICreateOrder role interface. In this example the identifier passed to FindById is the customer name. Avernus will automatically used the previously registered fetching strategy in this case in order to obtain the customer by the correct property. This is not visible to the client code which differs only in the interface used and therefore the name and parameters of the business method invoked.

What’s Next

Often in response to business logic we will wish to perform actions against external dependencies. This includes invoking services, sending messages and similar. Injecting dependencies into domain objects is complex and fragile. The next post in this series will describe the domain event capabilities Avernus provides to allow external interaction without polluting the domain model.