Skip to content

Question about Internal Handler Support in Modular Monolith Architecture #270

@gimmickj

Description

@gimmickj

Question about Internal Handler Support in Modular Monolith Architecture

Background

After reading issue #216 about modular monolith support, I decided to explore implementing a modular monolith architecture with this library. During my exploration, I encountered an interesting challenge with internal handler classes that I'd like to discuss.

Modular Monolith Structure

I'm implementing a modular monolith where each business domain is encapsulated in its own module/project, following these principles:

  • Each module contains its own handlers and business logic
  • Modules should encapsulate their implementation details (using internal classes)
  • The API layer orchestrates between modules through the mediator pattern

Project Structure

Solution/
├── Api/                    # Web API project (contains Mediator.SourceGenerator)
│   └── Program.cs         # IoC registration
├── Module1/               # Business module 1
│   └── Request1Handler.cs # Handler implementation
├── Module2/               # Business module 2
│   └── Request2Handler.cs # Handler implementation
└── Contracts/             # Shared contracts
    └── Request1.cs        # Request/Response definitions
    └── Request2.cs        # Request/Response definitions

IoC Registration in API Project

// Api/Program.cs
using Contracts;
using Mediator;

var builder = WebApplication.CreateBuilder(args);

// Mediator registration - SourceGenerator scans all referenced assemblies
builder.Services.AddMediator(options =>
{
    options.ServiceLifetime = ServiceLifetime.Transient;
    options.GenerateTypesAsInternal = true;
});

var app = builder.Build();

// API endpoints
app.MapGet("/weatherforecast", async (IMediator mediator) =>
{
    var result = await mediator.Send(new Request1());
    return result;
});

app.Run();

Project References

<!-- Api.csproj -->
<ItemGroup>
  <PackageReference Include="Mediator.SourceGenerator" Version="3.1.0-preview.22">
    <PrivateAssets>all</PrivateAssets>
    <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
  </PackageReference>
</ItemGroup>

<ItemGroup>
  <ProjectReference Include="..\Contracts\Contracts.csproj" />
  <ProjectReference Include="..\Module1\Module1.csproj" />
  <ProjectReference Include="..\Module2\Module2.csproj" />
</ItemGroup>

The Issue

When I try to use internal class for handlers in modules:

// In Module1 project
namespace Module1;

internal class Request1Handler : IRequestHandler<Request1, List<WeatherForecast>>
{
    // Implementation...
}

I get a compilation error:

error CS0122: "Request1Handler" is not accessible due to its protection level

Root Cause Analysis

After investigating the source code, I found that:

  1. Mediator.SourceGenerator scans all assemblies for handler types
  2. It generates DI registration code like:
    services.Add(new ServiceDescriptor(
        typeof(global::Mediator.IRequestHandler<Request1, List<WeatherForecast>>), 
        typeof(Module1.Request1Handler), // ❌ Cross-assembly access to internal type
        ServiceLifetime.Transient));
  3. The generated code is in the Api project, but handlers are in Module1 project
  4. Api project cannot access internal types from Module1 project

Current Workaround

I can solve this by adding InternalsVisibleTo in each module:

<!-- Module1.csproj -->
<ItemGroup>
  <ProjectReference Include="..\Contracts\Contracts.csproj" />
</ItemGroup>

<ItemGroup>
  <InternalsVisibleTo Include="Api" />
</ItemGroup>
<!-- Module2.csproj -->
<ItemGroup>
  <ProjectReference Include="..\Contracts\Contracts.csproj" />
</ItemGroup>

<ItemGroup>
  <InternalsVisibleTo Include="Api" />
</ItemGroup>

This allows the generated code in the Api project to access internal handlers from the modules.

Comparison with Clean Architecture

In clean architecture projects (like your samples), this works fine because:

// Application/DependencyInjection.cs
public static class DependencyInjection
{
    public static IServiceCollection AddApplication(this IServiceCollection services)
    {
        services.AddMediator(config =>
        {
            config.NotificationPublisherType = typeof(ForeachAwaitPublisher);
            config.Namespace = "Application";
            config.ServiceLifetime = ServiceLifetime.Scoped;
        });
        return services;
    }
}
// Application/Features/SomeFeature/SomeHandler.cs
namespace Application.Features.SomeFeature;

internal sealed class SomeHandler : IRequestHandler<SomeRequest, SomeResponse>
{
    // This works because handler and generated code are in the same assembly
}

Key differences:

  • Handlers are typically in the Application layer
  • Mediator.SourceGenerator is also in the Application layer
  • Generated code and handlers are in the same assembly
  • No cross-assembly visibility issues

Questions for the Author

  1. Is this behavior intentional for modular monoliths? Should handlers always be public when implementing a modular monolith architecture, or is this a limitation that could be addressed?

  2. Modular monolith best practices: Based on issue Modular Monolith support #216 discussions, what's the recommended approach for modular monoliths with this library? Should we:

    • Always use public handlers (sacrificing encapsulation)?
    • Use InternalsVisibleTo (current workaround)?
    • Structure projects differently?
    • Consider a different registration approach per module?
  3. Future enhancements: Would you consider adding better support for modular monoliths? For example:

    • Generating registration code in each module's assembly
    • Providing a configuration option to handle cross-assembly internal types
    • Module-specific mediator instances that can be composed
  4. Performance considerations: In a modular monolith with many modules, does the current approach of scanning all assemblies have any performance implications during startup?

Related to Issue #216

This question builds upon the modular monolith discussion in #216. While that issue focused on general modular monolith patterns, this specific case highlights a practical challenge when trying to maintain proper encapsulation (using internal classes) in a modular monolith architecture.

Additional Context

  • Using Mediator version: 3.1.0-preview.22
  • Target framework: .NET 10.0
  • The InternalsVisibleTo workaround works but feels like it breaks encapsulation principles

Thank you for this excellent library! Looking forward to your thoughts on this architectural scenario.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions