2

Currently: ASP.NET Core 2.2.

I've been doing quite an extensive research in this topic (Domain Driven Design used together with Clean Architecture):

DDD: Where to place domain event handlers?

And I've seen a couple of DDD and Clean Architecture Repos to better understand:

Some other topics:

(plus many other resources)

And I've even been skimming through Vaughn Vernon's book..

I'm trying to keep things as simple as possible. But there is something I'm not understanding... If the Aggregates themselves are the ones supposed to raise the domain events, how are we supposed to resolve the dependencies that the event handler need in order to execute successfully?

Let me explain (This is my complete understanding are you are more than welcome to correct me):

Aggregates are the ones in charge of raising the event. You would have an Event Bus that will subscribe and publish the Events and Handlers accordingly. So far so good. The problem starts when you're going to resolve the handler's dependencies.

From what I'm understanding, Aggregates should have zero external dependencies (That should be the Application's Layer responsibility), therefore you're forced to use a static method that raises the event inside the Aggregate. Once raised, you dispatch them to the respective handler. But what happens if the handler itself has dependencies such as a Repository/Service/Processor that needs to be instantiated?

Wouldn't I have to create some sort of factory that creates these dependencies? That means that I wouldn't be able to use the IoC library to make the dependencies for me, right?

I thought on using MediatR to handle same-tier, same-domain dispatches because it wires into my Dependency Resolver and automatically injects all the dependencies. For this to be achieved, MediatR would need to be injected in the Aggregate, breaking the principle of having no external dependencies.

From one of the GitHub links above, the Ametista project has the events raised in the Application Layer (Command Stack), and not inside the Aggregates.

While that would violate the Aggregate principle, it would've solved the dependency issue. Yet, it would introduce another problem, which is domain events duplication. You would've to reimplement the same event each time the criteria is met for that event to be raised.

That just put me to think... deep, and made me come up with the following (untested) solution (Which I don't have high hopes that it's going to work):

We implement the Service Locator pattern to wrap up MediatR in a Static Class. We would inject via a static method and property the Startup.cs Service Provider. I don't know if the shared static property across the entire application is a good idea. I also don't know if the Service Provider would get disposed or inaccessible in a certain part of the app. I also don't know if I'm creating a consistent MediatR instance that will be able to publish all the events transaction-wise from the bounded context (That is, all the events that were raised in the domain would work as a same transaction within the same bounded context).

In Startup.cs:

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{

     StaticMediatR.LoadServiceProvider(app.ApplicationServices);
}


    public class StaticMediatR
    {
        private static IServiceProvider _serviceProvider;

        public static void LoadServiceProvider(IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider;
        }

        public static IMediator Mediator()
        {
            var serviceScope = _serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope();

                return serviceScope.ServiceProvider.GetRequiredService<IMediator>();

        }

    }

I haven't tried the code above yet... That's what I have in mind.

Edit: I just tried the code above... and effectively, it's been disposed.

Edit x2: Wait, removing the Using does not dispose the object. I've made the changes above.

1

4 Answers 4

2

Using a static class vs injecting some sort of "event bus" into either an Aggregate's constructor or into one of it's methods is a matter of preference, and each has its trade-offs.

While injecting the eventing infrastructure does yield slightly more flexibility, it also means splattering this concern all over your domain. Is EventBus really something a domain expert talks about? Not usually. Furthermore, in my experience, I have never once found myself wanting for the flexibility pure injection gives. The event paradigm is rather simple and tends to remain constant (for good reason).

On the other hand while a static class may "hide" a dependency (although it should be common knowledge your domain objects will need to raise events), it offers a one-line solution to raising events without mucking up method/constructor signatures. In practice, I find it's worth this trade-off.

All that said, your question seems to be more about dependency injection than anything else. How an event is raised is a different concern than how it is handled. You are correct that your event handlers will have dependencies, and that wiring them up is a pain. Using a DI container is a pretty standard solution here, and can save you quite a bit of headache.

1
  • Yup. You got that right. I guess this is another one of those muddy water and unclear statements in which people who implement DDD start to have diverging opinions
    – Jose A
    Commented Jan 20, 2019 at 22:05
1

Note: the literature sucks. You aren't missing anything easy.

Aggregates should have zero external dependencies

That's right.

therefore you're forced to use a static method that raises the event inside the Aggregate

That's not right. You are allowed to pass in the event raising capability as an argument. Think "domain service" -- we're expressing the ability to share information in the language of the domain.

(Note: "event bus" is probably not in the language of your domain).

As far as the domain logic of the aggregate is concerned, we're just taking a bunch of information and throwing it into an "out box". What that outbox does with the information is not the aggregate's concern.

Your complicated graph of dependencies doesn't get injected into the aggregate, but instead into the handler for the aggregate, which will pass the capability into the aggregate when needed.

1
  • Thank you so much for the explanation 😊. That means that I will pass the Event Bus as part of the constructor. I guess it's going to come through the Application Layer... Or should I wire the aggregates to an interface and Inject them into the Application Layer as well?
    – Jose A
    Commented Jan 19, 2019 at 2:08
1

From what I'm understanding, Aggregates should have zero external dependencies (That should be the Application's Layer responsibility), therefore you're forced to use a static method that raises the event inside the Aggregate.

Oh no I'm not.

Zero external dependencies doesn't mean it takes no parameters. It means it doesn't know or care what it's talking to.

By using static methods you are NOT avoiding a dependency. You are hiding one. And you're doing it in a way that hard codes it. Even using a service locator creates a dependency, on the locator.

What you should study to fix this problem is called pure dependency injection. That's a fancy term new term for old school reference passing.

Using that your handlers wont know what they talk to until they are told what to talk to.

Once raised, you dispatch them to the respective handler. But what happens if the handler itself has dependencies such as a Repository/Service/Processor that needs to be instantiated?

Anything that lives for the life to the program can be created in main and passed to whatever needs to talk to it. However, some things, such as timestamps, need to not exist until their time. For these abstract factory is a wonderful pattern.

You don't have to use a hard coded static method to create things on the fly. You can use polymorphism to make whatever whenever. This lets you decouple what from when. It's very powerful. It just a bit of work.

The idea is, while you might not be able to create the time stamp at the start of construction in main, you can configure what kind of time stamps will be created at the start by passing in an abstract factory that creates that type.

This ensures your handlers have no source code dependencies. Because all they know is the interface they talk to this abstract factory through. They have no idea what they're making. That frees them from caring.

I strongly suspect pure DI is the missing ingredient of your studies. It's not a silver bullet. It's not the only way. It's not the easiest. But it gives you the power you're asking for.

2
  • 👌👌👌 Awesome! That does simplify things a lot! I would just pass the Event Bus as part of the constructor! This makes everything easier. I'll b researching how to. I appreciate all the help, time and effort put into the explanation!
    – Jose A
    Commented Jan 19, 2019 at 2:07
  • BTW, from the post above, I'm wondering. Would it be bad to have the aggregate contracted to an Interface so I can use DI to inject it to the Application Layer (In this case, the Command?)
    – Jose A
    Commented Jan 19, 2019 at 2:09
1

I have worked on a solution that solves the exact problem described in the question with success.

Clean architecture + DDD + CQRS with Mediator.

We opted to go with a static service locator for CQRS and dispatching events in the Domain layer.

We are not using dependency injection to handle messages routed through Mediator in our domain objects. Instead, we are respecting the no-dependency rule in our domain objects for better readability and instantiation. The trade-off is worth it.

I'll explain how.

The domain object's method handles messaging using a static Mediator class that uses a service locator to get the IMediator service from the HttpContext or IServiceCollection. This provides us with dependency resolution.

Example domain object:

    public class Person : Aggregate<PersonId>
    {
        public string Name { get; private set; }
        public Person(PersonDto dto)
        {
            Name = dto.Name;
        }

        public static async Task<Person> GetByIdAsync(Guid id)
        {
            var query = new GetPerson(id);
            Person person = await DomainMediator.Send(query);
            return person;
        }

        public static async Task<Person> CreateAsync(CreatePerson command)
        {
            Person createdPerson = await DomainMediator.Send(command);

            await DomainMediator.Publish(new PersonCreated(createdPerson);
            return createdPerson;
        }
    }

In this solution Commands, Queries, and Event dispatching is core to the Domain so understanding Mediator is fundamental to working out of it. The "Domain Mediator" implementation is less of a dependency on a particular class, but more of a dependency on the Domain.

Using this approach we avoid duplicate Commands, Queries and Events. We also get the benefit of knowing all of the functions of a domain object by quickly looking at its folder. Its self-describing.

enter image description here

Since we're using IMediator, all commands, queries, and events use an interface that is easily testable (IRequest, INotification) and most importantly, abstract. This abstraction allows us to write our business logic abstraction in the domain, while the implementation can be executed in the outer layers (Application/Infrastructure). This follows clean architecture design principles.

enter image description here

Here is how to setup the Domain Mediator static class with Service locator

Domain Mediator static class

    public static class DomainMediator
    {
        public static async Task Publish<T>(T args) where T : INotification
        {
            var mediator = ServiceLocator.ServiceProvider.GetService<IMediator>();
            await mediator.Publish<T>(args);
        }

        public static async Task<TResponse> Send<TResponse>(IRequest<TResponse> request, CancellationToken cancellationToken = default(CancellationToken))
        {
            var mediator = ServiceLocator.ServiceProvider.GetService<IMediator>();
            return await mediator.Send(request, cancellationToken);
        }
    }

Service Locator static class

public static class ServiceLocator
    {
        private static IServiceProviderProxy diProxy;
        public static IServiceProviderProxy ServiceProvider => diProxy
            ?? throw new Exception("You should Initialize the ServiceProvider before using it.");

        public static void Initialize(IServiceProviderProxy proxy)
        {
            diProxy = proxy;
        }
    }

IServiceProviderProxy interface

    public interface IServiceProviderProxy
    {
        T GetService<T>();
        IEnumerable<T> GetServices<T>();
        object GetService(Type type);
        IEnumerable<object> GetServices(Type type);
    }

HttpContextServiceProviderProxy static class

public class HttpContextServiceProviderProxy : IServiceProviderProxy
    {
        private readonly IHttpContextAccessor contextAccessor;
        private readonly IServiceProvider serviceProvider;

        public HttpContextServiceProviderProxy(IHttpContextAccessor contextAccessor,
            IServiceProvider defaultServiceProvider)
        {
            this.contextAccessor = contextAccessor;
            serviceProvider = defaultServiceProvider;
        }

        public T GetService<T>()
        {
            if (contextAccessor?.HttpContext is null)
            {
                var scope = serviceProvider.CreateScope();
                var service = scope.ServiceProvider.GetRequiredService<T>();
                return service;
            }

            return contextAccessor.HttpContext.RequestServices.GetService<T>();
        }

        public IEnumerable<T> GetServices<T>()
        {
            return contextAccessor.HttpContext.RequestServices.GetServices<T>();
        }

        public object GetService(Type type)
        {
            return contextAccessor.HttpContext.RequestServices.GetService(type);
        }

        public IEnumerable<object> GetServices(Type type)
        {
            return contextAccessor.HttpContext.RequestServices.GetServices(type);
        }
    }

For configuring this on startup this is a helper class we use to initialize the service proxy

ServiceLocatorProxyConfig

public static class ServiceLocatorProxyConfigurator
    {
        /// Add this to startup.cs Startup.ConfigureServices()
        public static void ConfigureServiceLocatorProxy(this IServiceCollection services)
        {
            services.AddSingleton<IServiceProviderProxy, HttpContextServiceProviderProxy>();
        }

        /// Add this to startup.cs Startup.Configure()
        public static void AddServiceLocatorProxy(this IServiceProvider serviceProvider)
        {
            ServiceLocator.Initialize(serviceProvider.GetService<IServiceProviderProxy>());
        }
    }

Our WebApi where this pattern is installed uses: Azure, Hosted Services, MassTransit, SignalR, Okta, EFCore, Dapper, and GraphQL (hotchoc).

This API supports Test Driven Development (TDD) using AutMoq, Xunit and AutoFixture.

Gotcha

The challenge we had to overcome was testing. Anytime we use a static class and tests are executed in parallel we have to manually handle dependency resolution by carefully setting up our tests. Tests that uses the same static instance should run under the same scope so that static initialization doesn't override eachother.

XUnit allows us to apply testing scope using "ICollection" and "IClassFixture".

For example, in our "Domain.Tests.csproj" we can have them use the same ServiceLocator instance by using XUnit's "ICollection" fixture. Here's an example test:

[Collection(nameof(ServiceLocatorFixture.Name))]
    public class PersonTests
    {
        private readonly CancellationToken _cancellationToken;
        private readonly Mock<IServiceProviderProxy> _mockedServiceProvider;

        public PersonTests(ServiceLocatorFixture fixture)
        {
            _mockedServiceProvider = fixture.MockedServiceProvider;
        }

        [Theory]
        public async Task Should_Get_Person(Guid fakeId,
            Mock<IMediator> mockedMediator,
          )
        {
            // arrange
            _mockedServiceProvider
                 .Setup(sp => sp.GetService<IMediator>())
                 .Returns(mockedMediator.Object);

            mockedMediator
              .Setup(m => m.Send(It.IsAny<GetPerson>(), It.IsAny<CancellationToken>()))
              .ReturnsAsync(program);

            // Act
            var sut = await Person.GetByIdAsync(fakeId);

            // Assert
            mockedMediator.Verify(
                m => m.Send(It.IsAny<GetPerson>(), It.IsAny<CancellationToken>()),
                Times.Once());

        }


        [AutoData, Theory]
        public async Task Should_Create_Person(
            Mock<IMediator> mockedMediator,
            PersonDto personDto
          )
        {
            // arrange
            var person = new Person(personDto);
            var command = new CreatePerson(personDto);
            
            _mockedServiceProvider
                 .Setup(sp => sp.GetService<IMediator>())
                 .Returns(mockedMediator.Object);

            mockedMediator
              .Setup(m => m.Send(It.IsAny<CreatePerson>(), It.IsAny<CancellationToken>()))
              .ReturnsAsync(person);

            // Act
            var sut = await Person.CreateAsync(command);

            // Assert
            mockedMediator.Verify(
                m => m.Send(It.IsAny<CreatePerson>(), It.IsAny<CancellationToken>()),
                Times.Once());

        }
    }

We work on a medium size team and every developer that has joined the team has had a positive experience working out of this solution. Adoption rate is quick, writing tests become intuitive, and the guardrails for development are built into the design.

1
  • Thanks Eric, for the detailed response! I was wondering how'd you do it with the static method, but this surely gives an answer!
    – Jose A
    Commented Mar 11, 2023 at 5:56

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.