simpleinjector / SimpleInjector

An easy, flexible, and fast Dependency Injection library that promotes best practice to steer developers towards the pit of success.
https://simpleinjector.org
MIT License
1.22k stars 152 forks source link

Using SimpleInjector in a Modular Monolith: How to Create Independent and Shared Containers #888

Closed oluatte closed 3 years ago

oluatte commented 3 years ago

Thanks for all your great work on Simple Injector.

I am building a ddd style modular monolith with several independent modules/bounded contexts used by a single api project.. For the most part, modules are completely independent and should not share instances.

The exceptions are of course when we need cross module communication e.g. while orchestrating a cross module business process.

So we would use the private container for internal (to the module) registrations ( e.g. domain events and handlers) and the shared container for cross module uses (e.g. integration events).

Questions:

Thanks!

dotnetjunkie commented 3 years ago

There are probably more ways to skin a cat, but you could consider the following:

Given there's a way to have independent containers, what is a good way to still share one container?

I'm afraid I don't understand your question.

oluatte commented 3 years ago

@dotnetjunkie I really appreciate your response (fast and detailed), so thank you.

These are fantastic ideas, and I think I understood most of them. A few follow questions/comments.

Thanks again!

EDIT: Thought it might be helpful to illustrate to show an example of cross module communication that i'm struggling with. I first ran into this while trying to implement an Orchestration Saga between modules. This is usually something seen in microservices, but i'm trying to do it locally and in memory, hopefully without the network related downsides of microservices.

dotnetjunkie commented 3 years ago

If you have in-process, inter-module communication, where each module has its own Container, there must be some shared state. This typically means there is some Mediator class that contains the list of all Module Containers and dispatches to them. Say, for instance, you have an IRelatedItemsProvider service with an IEnumerable<RelatedItem> GetRelatedItemsFor(string entityName, Guid entityId) method.

The provider implementation should forward the request to all modules, thus meaning it should have access to those modules:

public class InterModuleRelatedItemsProvider : IRelatedItemsProvider
{
    public List<Container> ModuleContainers { get; } = new List<Container>();

    // As an example, this method executes the calls to the containers in parallel, which might not
    // be required, but as you can see, easily implemented.
    public IEnumerable<RelatedItem> GetRelatedItemsFor(string entityName, Guid entityId) => (
        from container in this.ModuleContainers.AsParallel()
        from item in this.GetRelatedItemsForContainer(container, entityName, entityId)
        select item)
        .ToArray();

    private RelatedItem[] GetRelatedItemsForContainer(
        Container container, string entityName, Guid entityId)
    {
        // Wrap the resolve in a Scope, which is likely required
        using (AsyncScopedLifestyle.BeginScope(container))
        {
            return (
                from provider in container.GetAllInstances<IRelatedItemsProvider>()
                from item in provider.GetRelatedItemsFor(entityName, entityId)
                select item)
                .ToArray();
        }
    }
}

Example registration (per module)

public static Container BuildContainer(
    InterModuleRelatedItemsProvider provider // global single instance)
{
    var container = new Container();

    // Register the global mediator for usage  
    container.RegisterInstance<IRelatedItemsProvider>(provider);

    // Adds itself to the provider  
    provider.ModuleContainers.Add(container);

    Assembly[] moduleAssemblies = // fill with module's assemblies

    // Register all module-specific providers as collection.
    container.Collection.Register<IRelatedItemsProvider>(moduleAssemblies);
}

Such module-specific provider could be implemented as follows:

public RelatedOrdersForCustomerProvider : IRelatedItemsProvider
{
    public IEnumerable<RelatedItem> GetRelatedItemsFor(string entityName, Guid entityId)
    {
        if (entityName != "customer") return Array.Empty<RelatedItem>();

        return
            from order in this.context.Orders
            where order.CustomerId == entityId
            select new RelatedItem
            {
                Type = "order",
                Description = order.Number,
                Link = "/orders/" + order.Id
            };
    }
}

I'll get back to you on your different questions.

oluatte commented 3 years ago

This is super helpful!

So the single instance of the mediator that knows about all the containers is passed into the composition root for each module to be registered there as instance. That feels like a really great way to set things up.

Thank you.

dotnetjunkie commented 3 years ago

I was planning to use MVC, but in a completely separate project that communicates with the API over http (could be hosted on the same machine or different ones). I was still planning to have the MVC app fan out to all the necessary modules (regardless of if they were separate instances or just separate endpoints inside a single API). Does that bring up any red flags for you?

No, that certainly doesn't raise any red flags with me, although I would say that the HTTP-hosted communication between MVC and your API should be optional. If you pick your design right, whether or not you use in-process communication between MVC and the 'API' layer, or do a full-fledged HTTP web service calls to a different machine should be an implementation detail. You already seem to be using a design similar to what I proposed above, since you are using a completely message-based approach (using MediatR in this case). Although the default IMediator implementation of MediatR uses in-process messaging, replacing that with an implementation that sends the messages over HTTP to a web API is not difficult. For ideas I would, again, like to refer to this.

oluatte commented 3 years ago

Thanks a ton! I think I'm all set now. Really appreciate all the help.

PS.

I spent some time going through the Solid Services repo at your suggestion and it is very interesting (and new) to me. Will definitely be integrating some of those ideas as move forward.