Mediator icon indicating copy to clipboard operation
Mediator copied to clipboard

Modular Monolith support

Open R4ND3LL opened this issue 7 months ago • 20 comments

A while back I tried to implement this mediator in a modular system where:

  • Contracts project has mediator request objects
  • Module project has mediator handlers, marked internal
  • we have various front end executables (desktop, web) that dynamically load the modules at startup (like plugins).

This mediator hit a major roadblock with this app configuration, as I recall, stemming from how the EXE that does "AddMediator" contains all the generated source, and therefore, must know about all the modules that will be used up front. I think the handlers also had to be public for that reason.

Is this still the case in v3.0, or is this kind of app configuration supported?

From what I understand, the source generation would have to happen in each module (handler owner), and the mediator services registered on startup would have to support multiple modules each with their own generated source.

R4ND3LL avatar Jul 17 '25 14:07 R4ND3LL

Going from the title, I think it is possible to design a modular monolith with this library (multiple frontends, mixed deployement), but where it might get tricky is if you are talking about dynamically loading assemblies during runtime based on some configuration?

From what I understand, the source generation would have to happen in each module (handler owner), and the mediator services registered on startup would have to support multiple modules each with their own generated source.

It's been discussed a little bit before but it would require some refactoring that is not part of v3. But fundamentally all the registration and a lot of the performance comes from knowing a lot up front about types and compile time configuration (AddMediator is also considered compile time configuration). So making more decisions during runtime is kind of a big shift. This is something I'd need to experiment with along with interceptors I think for v4

Related: https://github.com/martinothamar/Mediator/issues/94#issuecomment-3064472634

martinothamar avatar Aug 05 '25 06:08 martinothamar

How is this supposed to work with a Aspire project where you can have several shared modules that have request / request handlers but you can have multiple Entry applications. For example you could have 2 api projects in the solution. Everything should build and startup together but doesn't work because you get the error "MediatorGenerator found multiple handlers of message type...." If i build each api project in its own sln it works but sometimes you have more than 1 startup project in your solution of which each needs to be able to call "AddMediator". I tried giving each api project a different namespace but didn't seem to help.

dsolteszopyn avatar Nov 07 '25 15:11 dsolteszopyn

@dsolteszopyn, I am not sure I fully understand your problem, but in our app, where we have multiple projects that plug into different deployable units, we use "GenerateTypesAsInternal" type for each "AddMediator" and we call "AddMediator" in the class library and not in the Api proejct.

So, we have two API projects and two different Class Library Projects, where each class library project is independent, so each class library calls it like this:


    public static IServiceCollection AddClassLibraryServices(this IServiceCollection services)
    {
        services.AddMediator(options =>
        {
            options.ServiceLifetime = ServiceLifetime.Transient;
            options.GenerateTypesAsInternal = true;
        });
    }

And the Api project just calls the AddClassLibraryServices. This works because each API project just uses 1 class library. I don't know if this would still work if you use 2 class libraries that call AddMediator() from the same API deployable project

ayuksekkaya avatar Nov 13 '25 20:11 ayuksekkaya

OK, great I think that’s what I need to do and was missing setting that as internal


From: Ali Yuksekkaya @.> Sent: Thursday, November 13, 2025 3:25:21 PM To: martinothamar/Mediator @.> Cc: Dan Soltesz @.>; Mention @.> Subject: Re: [martinothamar/Mediator] Modular Monolith support (Issue #216)

[https://avatars.githubusercontent.com/u/64444373?s=20&v=4]ayuksekkaya left a comment (martinothamar/Mediator#216)https://github.com/martinothamar/Mediator/issues/216#issuecomment-3529580905

@dsolteszopynhttps://github.com/dsolteszopyn, I am not sure I fully understand your problem, but in our app, where we have multiple projects that plug into different deployable units, we use "GenerateTypesAsInternal" type for each "AddMediator" and we call "AddMediator" in the class library and not in the Api proejct.

So, we have two API projects and two different Class Library Projects, where each class library project is independent, so each class library calls it like this:

public static IServiceCollection AddClassLibraryServices(this IServiceCollection services)
{
    services.AddMediator(options =>
    {
        options.ServiceLifetime = ServiceLifetime.Transient;
        options.GenerateTypesAsInternal = true;
    });
}

And the Api project just calls the AddClassLibraryServices. This works because each API project just uses 1 class library. I don't know if this would still work if you use 2 class libraries that call AddMediator() from the same API deployable project

— Reply to this email directly, view it on GitHubhttps://github.com/martinothamar/Mediator/issues/216#issuecomment-3529580905, or unsubscribehttps://github.com/notifications/unsubscribe-auth/A3RGB45U5XHXO3TXBQNDKNT34TSLDAVCNFSM6AAAAACBYC7AXWVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZTKMRZGU4DAOJQGU. You are receiving this because you were mentioned.Message ID: @.***>

dsolteszopyn avatar Nov 13 '25 21:11 dsolteszopyn

I also have an issue where the requests live in a separate shared assembly outside of where the handlers are so then that causes problems as well because it can’t find the requestHandler. Not every class library will implement handlers for all of the shared requests. Is there a way to filter out certain requests to ignore not to code gen or if it doesn't find a handler then it should just continue.


From: Dan Soltesz @.> Sent: Thursday, November 13, 2025 4:09:39 PM To: martinothamar/Mediator @.>; martinothamar/Mediator @.> Cc: Mention @.> Subject: Re: [martinothamar/Mediator] Modular Monolith support (Issue #216)

OK, great I think that’s what I need to do and was missing setting that as internal


From: Ali Yuksekkaya @.> Sent: Thursday, November 13, 2025 3:25:21 PM To: martinothamar/Mediator @.> Cc: Dan Soltesz @.>; Mention @.> Subject: Re: [martinothamar/Mediator] Modular Monolith support (Issue #216)

[https://avatars.githubusercontent.com/u/64444373?s=20&v=4]ayuksekkaya left a comment (martinothamar/Mediator#216)https://github.com/martinothamar/Mediator/issues/216#issuecomment-3529580905

@dsolteszopynhttps://github.com/dsolteszopyn, I am not sure I fully understand your problem, but in our app, where we have multiple projects that plug into different deployable units, we use "GenerateTypesAsInternal" type for each "AddMediator" and we call "AddMediator" in the class library and not in the Api proejct.

So, we have two API projects and two different Class Library Projects, where each class library project is independent, so each class library calls it like this:

public static IServiceCollection AddClassLibraryServices(this IServiceCollection services)
{
    services.AddMediator(options =>
    {
        options.ServiceLifetime = ServiceLifetime.Transient;
        options.GenerateTypesAsInternal = true;
    });
}

And the Api project just calls the AddClassLibraryServices. This works because each API project just uses 1 class library. I don't know if this would still work if you use 2 class libraries that call AddMediator() from the same API deployable project

— Reply to this email directly, view it on GitHubhttps://github.com/martinothamar/Mediator/issues/216#issuecomment-3529580905, or unsubscribehttps://github.com/notifications/unsubscribe-auth/A3RGB45U5XHXO3TXBQNDKNT34TSLDAVCNFSM6AAAAACBYC7AXWVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZTKMRZGU4DAOJQGU. You are receiving this because you were mentioned.Message ID: @.***>

dsolteszopyn avatar Nov 13 '25 21:11 dsolteszopyn

I guess it does give you a warning. I was getting build errors because i treat warnings as errors. I would have to ignore those warnings.

dsolteszopyn avatar Nov 14 '25 15:11 dsolteszopyn

MediatorPOC.zip

put together sample poc to show the issue. i feel like the codegen should just look for handlers in the class library to codegen, it seems to look for all of the "requests" when you call AddMediator when it should look for handlers in the class library to register.

dsolteszopyn avatar Nov 14 '25 15:11 dsolteszopyn

Hey, just had a look at that project

  • One of the requests inherited multiple interfaces, just removed that (there was a warning diagnostic)
  • Api1 is basically fine
  • Api2 is where we hit issues, since it adds both classlibraries which have their own distinct implementations of IMediator, we wouldn't really know which implementation to return when requested through IServiceProvider. There is no clever partioning or routing going on here. I think what is perhaps not immediately obvious is that AddMediator doesn't just add "this projects" code to the DI container, it is actually telling the sourcegenerator to generate an implementation of IMediator based on the options provided. So that is the underlying issue that arises with referencing multiple class libraries which have their own AddMediator calls.

So to get that project working I pulled the AddMediator calls into the API projects instead. They essentially needs to exist where the runtime starts, the outermost layer. You can still have multiple class libraries with their own requests and handlers. The AddClass1LibraryServices etc could still be responsible for registering services required by handlers it knows about. Do you see any issues with that approach?

martinothamar avatar Nov 15 '25 19:11 martinothamar

I have the shared class library for the request because sometimes module A needs to get data from module B, but we don’t want a hard dependency so we use the request objects that are shared across all of them so I can say hey module A give me some data for module B, but in a loosely coupled way


From: Martin Othamar @.> Sent: Saturday, November 15, 2025 2:54:40 PM To: martinothamar/Mediator @.> Cc: Dan Soltesz @.>; Mention @.> Subject: Re: [martinothamar/Mediator] Modular Monolith support (Issue #216)

[https://avatars.githubusercontent.com/u/5425986?s=20&v=4]martinothamar left a comment (martinothamar/Mediator#216)https://github.com/martinothamar/Mediator/issues/216#issuecomment-3536826227

Hey, just had a look at that project

  • One of the requests inherited multiple interfaces, just removed that (there was a warning diagnostic)
  • Api1 is basically fine
  • Api2 is where we hit issues, since it adds both classlibraries which have their own distinct implementations of IMediator, we wouldn't really know which implementation to return when requested through IServiceProvider. There is no clever partioning or routing going on here. I think what is perhaps not immediately obvious is that AddMediator doesn't just add "this projects" code to the DI container, it is actually telling the sourcegenerator to generate an implementation of IMediator based on the options provided. So that is the underlying issue that arises with referencing multiple class libraries which have their own AddMediator calls.

So to get that project working I pulled the AddMediator calls into the API projects instead. They essentially needs to exist where the runtime starts, the outermost layer. You can still have multiple class libraries with their own requests and handlers. The AddClass1LibraryServices etc could still be responsible for registering services required by handlers it knows about

— Reply to this email directly, view it on GitHubhttps://github.com/martinothamar/Mediator/issues/216#issuecomment-3536826227, or unsubscribehttps://github.com/notifications/unsubscribe-auth/A3RGB46PPCJCJDYAFZVK6DD346AIBAVCNFSM6AAAAACBYC7AXWVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZTKMZWHAZDMMRSG4. You are receiving this because you were mentioned.Message ID: @.***>

dsolteszopyn avatar Nov 15 '25 20:11 dsolteszopyn

Yes I think that should still work, if I understand you correctly.. I just tested by having the Request2Handler simply Send(new Request1) to let the Request1Handler do the work (meaning Class2 calls Class1 even through they don't reference eachother directly)

martinothamar avatar Nov 15 '25 21:11 martinothamar

Can you zip your sample and post for me to review?


From: Martin Othamar @.> Sent: Saturday, November 15, 2025 4:22:02 PM To: martinothamar/Mediator @.> Cc: Dan Soltesz @.>; Mention @.> Subject: Re: [martinothamar/Mediator] Modular Monolith support (Issue #216)

[https://avatars.githubusercontent.com/u/5425986?s=20&v=4]martinothamar left a comment (martinothamar/Mediator#216)https://github.com/martinothamar/Mediator/issues/216#issuecomment-3536912203

Yes I think that should still work, if I understand you correctly.. I just tested by having the Request2Handler simply Send(new Request1) to let the Request1Handler do the work (meaning Class2 calls Class1 even through they don't reference eachother directly)

— Reply to this email directly, view it on GitHubhttps://github.com/martinothamar/Mediator/issues/216#issuecomment-3536912203, or unsubscribehttps://github.com/notifications/unsubscribe-auth/A3RGB454ZB5KMCWL3QUBRT3346KPVAVCNFSM6AAAAACBYC7AXWVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZTKMZWHEYTEMRQGM. You are receiving this because you were mentioned.Message ID: @.***>

dsolteszopyn avatar Nov 15 '25 21:11 dsolteszopyn

I think this can work. I'm going to give it a shot in my main project. Thanks for your help.

dsolteszopyn avatar Nov 17 '25 13:11 dsolteszopyn

I have everything working besides 1 issue I found which i think it's a bug in the generator. The core issue is that the source generator is not properly handling the generic type constraints when generating the wrapper code for my domain events.

Error CS0246: The type or namespace name 'TEntity' could not be found (are you missing a using directive or an assembly reference?) (1026, 48)

I've updated the POC to demonstrator this error.

MediatorPOC.zip

dsolteszopyn avatar Nov 18 '25 19:11 dsolteszopyn

I think in order to really support modular monolith that can have multiple "api" or "functions" or entry applications in the same solution, now very common with Aspire. Need a way to tell Api1 to allow you to override types it tries to generate not just the default "IRequest" during code generation. Otherwise all types get registered even though api1 doesn't use them all. If Request2 handler had other dependencies, DI would try to validate even though we are not referencing or using Request2 / Class2. Which leads you to having to do something like this

// Disable service validation for descriptors that can't be constructed // This suppresses validation errors for class2-specific handlers (that depend on ISomeService, etc.) // which are registered but not used in the API project. builder.Host.UseDefaultServiceProvider((context, options) => { options.ValidateScopes = context.HostingEnvironment.IsDevelopment(); options.ValidateOnBuild = false; // Disable validation to suppress errors for job handlers });

Would be nice to just add MediatorOption to IncludeTypesForGeneration

` builder.Services.AddMediator(options => { options.Namespace = "Api1"; options.ServiceLifetime = ServiceLifetime.Transient; options.GenerateTypesAsInternal = true; options.IncludeTypesForGeneration = [typeof(IAp1Request)]
});

builder.Services.AddMediator(options => { options.Namespace = "Api2"; options.ServiceLifetime = ServiceLifetime.Transient; options.GenerateTypesAsInternal = true; options.IncludeTypesForGeneration = [typeof(IAp2Request)]
});

public record Request1() : IRequest<List<WeatherForecast>>, IApi1Request, IApi2Request; public record Request2() : IRequest<List<WeatherForecast>>, IApi2Request;

`

Thoughts?

dsolteszopyn avatar Nov 19 '25 14:11 dsolteszopyn

I've just migrated our modular monolith project from MediatR to Mediator, but it's failing to run properly. The issue arises because our Commands and Handlers are declared as internal within each module, while Mediator requires all message types and handlers to be visible to the host startup project—otherwise, services cannot be resolved at runtime.

In contrast, MediatR relies on reflection and runtime activation, which allows it to work with internal types, so it functioned correctly under our original architecture.

I recommend decoupling the global configuration of IMediator/ISender/IPublisher (i.e., AddMediator()) from the source generator, so that the host project explicitly invokes it. Meanwhile, the source generator should only be responsible for generating the concrete service registration code for each module’s internal IRequest and IRequestHandler implementations (e.g., services.AddTransient<IRequestHandler<..., ...>, ...>()).

ynanech avatar Dec 12 '25 15:12 ynanech

@dsolteszopyn

I have everything working besides 1 issue I found which i think it's a bug in the generator. The core issue is that the source generator is not properly handling the generic type constraints when generating the wrapper code for my domain events.

Is the message/request type generic? Because this library doesn't support generic messages yet

martinothamar avatar Dec 12 '25 19:12 martinothamar

@dsolteszopyn

Would be nice to just add MediatorOption to IncludeTypesForGeneration

I think this is already supported? Through options.Assemblies = [typeof(...)];

martinothamar avatar Dec 12 '25 19:12 martinothamar

@ynanech

I recommend decoupling the global configuration of IMediator/ISender/IPublisher (i.e., AddMediator()) from the source generator, so that the host project explicitly invokes it. Meanwhile, the source generator should only be responsible for generating the concrete service registration code for each module’s internal IRequest and IRequestHandler implementations (e.g., services.AddTransient<IRequestHandler<..., ...>, ...>()).

That is something to consider for the next major version I think. I'm not sure how expensive that will be. My only recurring objection to the coupling argument is that I would say that the coupling isn't really gone when you make a handler internal, it is just less visible. A still calls B, and so they are coupled regardless what we do. So I'm personally not convinced yet that there is much value in enabling the use of internal handlers across project boundaries if it has significant cost either in implementation/complexity or runtime perf. We shall see 😄

martinothamar avatar Dec 12 '25 19:12 martinothamar

I created a pr to do this already, just waiting for you to review 😀


From: Martin Othamar @.> Sent: Friday, December 12, 2025 2:08:17 PM To: martinothamar/Mediator @.> Cc: Dan Soltesz @.>; Mention @.> Subject: Re: [martinothamar/Mediator] Modular Monolith support (Issue #216)

[https://avatars.githubusercontent.com/u/5425986?s=20&v=4]martinothamar left a comment (martinothamar/Mediator#216)https://github.com/martinothamar/Mediator/issues/216#issuecomment-3647783356

@dsolteszopynhttps://github.com/dsolteszopyn

Would be nice to just add MediatorOption to IncludeTypesForGeneration

I think this is already supported? Through options.Assemblies = [typeof(...)];

— Reply to this email directly, view it on GitHubhttps://github.com/martinothamar/Mediator/issues/216#issuecomment-3647783356, or unsubscribehttps://github.com/notifications/unsubscribe-auth/A3RGB4ZJ7GUXLTXEOOC5RTT4BMHCDAVCNFSM6AAAAACBYC7AXWVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZTMNBXG44DGMZVGY. You are receiving this because you were mentioned.Message ID: @.***>

dsolteszopyn avatar Dec 12 '25 19:12 dsolteszopyn