Skip to content

Obelisk Structure

PeterBeyond edited this page Nov 24, 2023 · 5 revisions

OneBeyond.Studio.Domain

The project is intended to contain your system's domain logic. The responsibility of the domain logic is to maintain system's data consistency from business point of view. Due to that fact, the domain project usually contains only code involved in command execution as this is the only mean to break the consistency. Each feature folder under the domain project generally contains classes for the domain entities, aggregate roots, value objects, commands and domain events. In the ideal world (as there are always exceptions in practice), every command affects only a single aggregate root, and this finds reflection on the methods of the latter:

public sealed record DoSomething { /* snip */ }

public sealed class MyAggregateRoot : Entity<Guid>
{
    public void Apply(DoSomething command/*, SomeParams params, SomeValueObject valueObject*/)
    {
        // Modify AR state based on the command logic

        RaiseDomainEvent(new SomethingHappened()); // optional
    }
}

Based on the Onion Architecture, the domain code does not have any dependencies on external systems. This is why it is easy to write unit tests for each class you have in this project.

OneBeyond.Studio.Application

The main responsibility of this project is connecting the domain layer with system's external dependencies via a set of interfaces. It is expected the connection is implemented by means of command handlers. Each command handler dependencies outline which services are required for implementing a piece of functionality. Quite often, one of such services is a repository which is used for loading an aggregate root from storage (external dependency) and placing its updated state back.

internal sealed class DoSomethingHandler : IRequestHandler<DoSomething, Unit>
{
    public DoSomethingHandler(
        IMyAggregateRootRepository repository, /* This is the connection to external world */
        ISomeService service                   /* Yet another connection to external world */
        )
    {}

    public async Task<Unit> Handle(DoSomething command, CancellationToken cancellationToken)
    {
        var aggregateRoot = await this.repository.GetAsync(command.Id, cancellationToken); // Load an AR
        var someData = await this.service.GetSomeDataAsync(cancellationToken);             // Get some other data

        aggregateRoot.Apply(command, someData);                                            // Modify the AR

        await this.repository.UpdateAsync(aggregateRoot, cancellationToken);               // Put the AR back

        return Unit.Value; 
    }
}

Another possible responsibility of this layer is implementing data fetching aspect of the system via a set of queries and query handlers. By its structure they resemble commands and command handlers. Due to the fact they just fetch consistent data, they do not have any relation to the domain. Sometimes it is more efficient and convenient to implement queries straight on the infrastructure layer closer to the storage and eliminating abstractions of the application layer.

Yet another possible responsibility of this layer is implementing side effects in the form of domain/integration event handlers. The idea is that one handler implements one side effect. For example, you might have one handler sending out some notifications via SignalR, another one via emails, etc. Translating domain events into integration ones and broadcasting the latter might be another example of such handler. Sometimes it is more convenient to initiate event handling on the presentation layer, especially in cases when the handling generates some command.

Usually you want to see the entire functionality provided by the system on this layer without actual connections to the external services.

OneBeyond.Studio.Infrastructure

This project is supposed to have implementations for the interfaces introduced by the application layer. These implementations connect the system to the external services. The most widely used external service is persistent storage where the system keeps its data. It is possible to have this project to be broken down in a more granular way based on a specific external service. For example, XXX.Infrastructure.MsSql, XXX.Infrastructure.AzureBlobs, XXX.Infrastructure.SalesForce. The template just includes a connection to the storage via making use of EF Core with MsSql - you can find the system's DbContext, some entity configurations and initial migration there.

OneBeyond.Studio.Obelisk.WebApi & OneBeyond.Studio.Obelisk.Workers

   These two projects serve two main things:

  • connecting application layer with implementations of external services in infrastructure layer via a mean of dependency injection. This is what is called composition root. The template configures the system for wiring it up with the storage infrastructure mentioned in the previous paragraph as well as some authentication/authorization capabilities.
  • exposing the system's functionality to consumers like SPA, mobile app, messaging. For this, the presentation layer translates incoming data whether it is from HTTP request or some message broker into commands and queries and submits them to the application layer. The template includes some base classes for ASP.Net controllers exposing functionality for HTTP requests as well as implementation of Azure Function receiving domain events and dispatching them to the registered handlers.
  • NB: The WebApi still contains legacy support for a hybrid razor page/SPA front end approach. This is being deprecated in favour of the separate .WebUI project

OneBeyond.Studio.Obelisk.XXX.Tests

   The template containes a single project OneBeyond.Studio.Obelisk.Domain.Tests intended for testing code from the domain layer, i.e., all its SUTs should be located in the OneBeyond.Studio.Obelisk.Domain project. All the other test projects are supposed to be named accordingly.

Obelisk NuGets

Some of the functionality shared by solutions based on the template comes from the Obelisk Core NuGet packages. See Obelisk Core for the contents of these packages.