Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions docs/IncludeTypesForGeneration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# IncludeTypesForGeneration - Modular Monolith Support

The `IncludeTypesForGeneration` option enables filtering of request types during source generation based on marker interfaces. This is particularly useful for modular monolith architectures where multiple APIs/modules share the same codebase but should only generate mediator code for their respective requests.

## How It Works

When you specify one or more marker interface types in `IncludeTypesForGeneration`, only request types that implement at least one of those marker interfaces will be included in code generation. If the collection is empty or not configured, **all discovered request types will be included** (no filtering applied).

## Usage Example

### 1. Define Marker Interfaces

```csharp
// Marker interface for API1 requests
public interface IApi1Request { }

// Marker interface for API2 requests
public interface IApi2Request { }
```

### 2. Implement Request Types with Marker Interfaces

```csharp
// Shared between both APIs
public record SharedRequest(Guid Id)
: IRequest<SharedResponse>, IApi1Request, IApi2Request;

// API1 only
public record Api1OnlyRequest(Guid Id)
: IRequest<Api1Response>, IApi1Request;

// API2 only
public record Api2OnlyRequest(Guid Id)
: IRequest<Api2Response>, IApi2Request;

// Not included in either API (no marker interface)
public record InternalRequest(Guid Id)
: IRequest<InternalResponse>;
```

### 3. Configure Each API's Mediator

```csharp
// In Api1's startup/configuration
builder.Services.AddMediator(options =>
{
options.Namespace = "Api1";
options.ServiceLifetime = ServiceLifetime.Transient;
options.GenerateTypesAsInternal = true;
options.IncludeTypesForGeneration = [typeof(IApi1Request)];
});

// In Api2's startup/configuration
builder.Services.AddMediator(options =>
{
options.Namespace = "Api2";
options.ServiceLifetime = ServiceLifetime.Transient;
options.GenerateTypesAsInternal = true;
options.IncludeTypesForGeneration = [typeof(IApi2Request)];
});
```

### Result

- **Api1** will generate code for: `SharedRequest`, `Api1OnlyRequest`
- **Api2** will generate code for: `SharedRequest`, `Api2OnlyRequest`
- Neither will generate code for: `InternalRequest` (no marker interface)

## Benefits

1. **No Service Validation Issues**: Only handlers for included request types are registered in DI, so you won't need to disable validation or deal with missing dependencies for handlers you don't use.

2. **Clean Separation**: Each API/module only contains mediator code for requests it actually handles.

3. **Shared Requests**: Requests can be easily shared across multiple APIs by implementing multiple marker interfaces.

4. **Type Safety**: Compiler ensures requests implement the correct marker interfaces at build time.

## Aspire Support

This feature works seamlessly with .NET Aspire applications where you have multiple API projects in the same solution:

```csharp
// AppHost project
var builder = DistributedApplication.CreateBuilder(args);

var api1 = builder.AddProject<Projects.Api1>("api1");
var api2 = builder.AddProject<Projects.Api2>("api2");
var worker = builder.AddProject<Projects.Worker>("worker");

builder.Build().Run();
```

Each project can configure its own mediator with `IncludeTypesForGeneration` to only generate code for its relevant requests.

## Alternative: Exact Type Filtering

While the primary use case is marker interfaces, you can also specify exact request types:

```csharp
options.IncludeTypesForGeneration = [
typeof(Request1),
typeof(Request2)
];
```

However, this requires listing every request type explicitly and doesn't scale well for modular monoliths. The marker interface approach is recommended.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"profiles": {
"MassTransitIntegration": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:55405;http://localhost:55406"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ internal sealed class CompilationAnalyzer
private readonly HashSet<NotificationMessageHandler> _notificationMessageHandlers;
private readonly List<PipelineBehaviorType> _pipelineBehaviors;
private Queue<INamespaceOrTypeSymbol>? _configuredAssemblies;
private HashSet<INamedTypeSymbol>? _includeTypesForGeneration;

public ImmutableArray<RequestMessageHandlerWrapperModel> RequestMessageHandlerWrappers;

Expand Down Expand Up @@ -760,6 +761,34 @@ bool ProcessInterface(
}
else
{
// Filter based on IncludeTypesForGeneration if configured
// Only filter if the collection is not null AND has items
// Empty collection or null means no filtering (include all)
if (_includeTypesForGeneration is not null && _includeTypesForGeneration.Count > 0)
{
var implementsMarkerInterface = false;
foreach (var markerInterface in _includeTypesForGeneration)
{
// Check if typeSymbol implements the marker interface
if (
typeSymbol.AllInterfaces.Any(i =>
_symbolComparer.Equals(i.OriginalDefinition, markerInterface)
)
)
{
implementsMarkerInterface = true;
break;
}
}

if (!implementsMarkerInterface)
{
// Remove the message if it doesn't implement any marker interface
_requestMessages.Remove(message);
return true; // Continue processing, just skip this message
}
}

if (mapping.TryGetValue(typeSymbol, out var requestMessageHandlerObj))
{
mapping[typeSymbol] = null;
Expand Down Expand Up @@ -1428,6 +1457,40 @@ CancellationToken cancellationToken
_configuredAssemblies.Enqueue(assemblySymbol.GlobalNamespace);
}
}
else if (opt == "IncludeTypesForGeneration")
{
if (_includeTypesForGeneration is not null)
{
ReportDiagnostic(
assignment.Left.GetLocation(),
(in CompilationAnalyzerContext c, Location l) =>
c.ReportInvalidCodeBasedConfiguration(
l,
"IncludeTypesForGeneration can only be configured once"
)
);
return false;
}

_includeTypesForGeneration = new(_symbolComparer);
var typeOfExpressions = assignment.Right.DescendantNodes().OfType<TypeOfExpressionSyntax>().ToArray();

foreach (var typeOfExpression in typeOfExpressions)
{
var typeInfo = semanticModel.GetTypeInfo(typeOfExpression.Type, cancellationToken);
if (typeInfo.Type is not INamedTypeSymbol typeSymbol)
{
ReportDiagnostic(
typeOfExpression.Type.GetLocation(),
(in CompilationAnalyzerContext c, Location l) =>
c.ReportInvalidCodeBasedConfiguration(l, $"Could not resolve type: {typeOfExpression.Type}")
);
continue;
}

_includeTypesForGeneration.Add(typeSymbol.OriginalDefinition);
}
}
else if (opt == "PipelineBehaviors" || opt == "StreamPipelineBehaviors")
{
var interfaceName = opt == "PipelineBehaviors" ? "IPipelineBehavior" : "IStreamPipelineBehavior";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,15 @@ namespace Mediator
/// </summary>
public global::System.Collections.Generic.IReadOnlyList<global::Mediator.AssemblyReference> Assemblies { get; set; } = new global::Mediator.AssemblyReference[0];

/// <summary>
/// The collection of marker interface types to filter which request types are included for generation.
/// When specified with one or more types, only request types that implement IRequest AND at least one of the specified marker interfaces will be included.
/// When empty or not configured, all discovered request types will be included (no filtering).
/// This is useful for modular monolith scenarios where you want to generate code only for requests belonging to specific modules.
/// Example: options.IncludeTypesForGeneration = [typeof(IApi1Request)] will only generate code for requests that implement IApi1Request.
/// </summary>
public global::System.Collections.Generic.IReadOnlyList<global::System.Type> IncludeTypesForGeneration { get; set; } = new global::System.Type[0];

/// <summary>
/// The collection of types of pipeline behaviors to register in DI.
/// When the type is an unconstructed generic type, the source generator will register all the constructed types of the generic type (open generics that is supported during AoT).
Expand Down
Loading