Onion architectures still made simple

Very often, onion architectures are described with a core layer, a domain layer, an API layer, and an infrastructure layer. To keep things as simple as possible, I will only express 2 layers. Infrastructure and/or API, and domain.

In our , we need to retrieve repositories from external sources (REST is fine for that…). We also need to specify how to store internal models from the BC, and to specify other infrastructure purposes, like the ESB. We specify all these elements to the object that is responsible for bootstrapping the business code.

new BootStrapper()
    .SetExternalDataSources()
    .SetInternalStorage()
    .SetServiceBus()
    .Execute(d => d.Process());

The bootstrapper can be written as follows :

public class BootStrapper
{
    private BoundedContext _boundedContext;
    private DatabaseContext<DomainEntity> _entitiesContext;
    private DatabaseContext<DomainEvent> _eventsContext;
    private ServiceBus _serviceBus;

    // Bootstrap
    public BootStrapper()
    {
        // Private constructor + factory
        _boundedContext = BoundedContext.CreateUseCase();
    }

    public BootStrapper SetExternalDataSources()
    {
        _boundedContext = _boundedContext
            .WithInvokerAsync<CurrenciesDto>(new Service().RetrieveCurrenciesAsync)
            .WithInvoker<CurrenciesDto>(new Service().RetrieveCurrencies);

        return this;
    }

    public BootStrapper SetInternalStorage()
    {
        // Loads BC definitions that will be later persisted...
        _eventsContext = new DatabaseContext<DomainEvent>(new SqlServerLogic()).Take(d => d.Events);
        _entitiesContext = new DatabaseContext<DomainEntity>(new MongoDbLogic()).Take(d => d.Entities);

        return this;
    }

    public BootStrapper SetServiceBus()
    {
        _serviceBus = new ServiceBus()
            .Take(d => d.Events.Select(e => e.Uid).ToList().AsReadOnly());

        return this;
    }

    public void Execute(Func<BoundedContext, bool> func)
    {
        // Breaks if a critical error occurs
        try
        {
            // Starts the process
            func.Invoke(_boundedContext);

            // Creates a TransactionScope (if possible, more stuff is needed depending on storage runtimes...)
            // Transactions should be the responsability of each context
            using (var transaction = new TransactionScope())
            {
                if (_entitiesContext.Persist(_boundedContext) && _eventsContext.Persist(_boundedContext))
                {
                    transaction.Complete();
                }
            }

            // If everything is OK, returns to the database and play persisted notifications...
            // TODO : Separate Storage from Events
            _serviceBus
                .WithAcknowledgement()
                .Send(_boundedContext);
        }
        catch (Exception exception)
        {
            Console.WriteLine(exception);
        }
    }
}

The bootstrapper contains an instance of the BC, and mechanisms to persist entities and events, and an instance of the ESB. Respectively _boundedContext, _entitiesContext, _eventsContext, and _serviceBus. The constructor of the bootstrapper instanciates the BC, typed as BoundedContext. This is the class that will support all the business. The BC has a private constructor that can be reached through a method factory, to explicitly express the use case. External sources are set through SetExternalDataSources. These sources aim at providing repositories to the BC. They are set by setting delegates within the BC. These delegates, when invoked, provide DTOs. Delegates can be invoked asynchronously, or not. Then, we need to set internal storage, for entities and events. Basic operations are handled by the class DatabaseContext, that can be instanciated from the type of artifact we want to persist. The logic of binding these artifacts to databases is the responsibility of a dedicated object, that inherits from IPersistenceLogic interface. Classes that implement this interface are meant to know how to map, in both ways, the object model of the BC to the storage model of the database. Here, we want to persist events into a RDBMS system, and entities into a NoSQL store (sounds strange but why not… ?). The DatabaseContext object just exposes CRUD persistence capabilities to the bootstrapper.

public interface IPersistenceLogic
{
    // ReSharper disable once UnusedParameter.Global
    bool Store(ReadOnlyCollection<DomainObjectBase> items);
    List<DomainObjectBase> Retrieve();
}

public class SqlServerLogic : IPersistenceLogic
{
    public bool Store(ReadOnlyCollection<DomainObjectBase> items)
    {
        // Todo...
        return true;
    }

    public List<DomainObjectBase> Retrieve()
    {
        // Todo...
        return new List<DomainObjectBase>();
    }
}

You should be careful when using repositories. In fact, you should never load repositories from external sources after the processing has begun. Lazy loading within the BC is evil. When processing, the internal state of the BC should not change due to external solicitations. This is what happens for example when you retrieve at some stage of the process, and then you try to retrieve it back a further stage ahead, while it has been updated by another process. Imagine a small program that tries to convert amounts in a desired currency, given an incomplete list of exchange rates from external sources. If incoming rates are always partially moving during the processing, in certain parts of the set, resulting will never be consistent. Typically, this is why you need to work with snapshots. Take snapshots of the environment context, make it static in the BC, and then process from a known state ! When dealing with domain events, you may have to differ some events integration into your BC, until the end of the process.

Add a Comment

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

99 − = 93