Skip to content

Nondeterministic codegen DI chain #2068

@andreikarkkanen

Description

@andreikarkkanen

When using dotnet run -- codegen write with:

opts.ServiceLocationPolicy = ServiceLocationPolicy.NotAllowed;
opts.CodeGeneration.TypeLoadMode = TypeLoadMode.Static;
opts.CodeGeneration.AlwaysUseServiceLocationFor<IMapper>();

the generated adapters are inconsistent. With a single handler, codegen consistently generates an adapter that uses constructor injection for “normal” dependencies and service-location only for the explicitly marked IMapper.

But when the project contains many (e.g., 100) similar handlers, codegen generates a different DI chain where it resolves the entire repository and DbContext from the service provider (service location), even though service location is configured as NotAllowed and only IMapper is explicitly opted into service location.

Repro project:
CodeGen_issue.zip

Steps to reproduce the behavior:

  1. Extract CodeGen solution,
  2. Open Program.cs
  3. Find opts.Discovery.CustomizeHandlerDiscovery(x =>

Case A (works):

opts.Discovery.CustomizeHandlerDiscovery(x =>
{
    // Only discover 1 handler explicitly marked with [WolverineHandler] (see Handlers/Handler.cs)
    x.Includes.WithAttribute<WolverineHandlerAttribute>();
});

Case B (fails):

opts.Discovery.CustomizeHandlerDiscovery(x =>
{
    // Discover 100 handlers in this namespace
    x.Includes.InNamespace("CodeGen.Handlers");
});

Expected behavior
With ServiceLocationPolicy.NotAllowed and AlwaysUseServiceLocationFor<IMapper>, codegen should prefer explicit constructor injection for dependencies it can construct, and use service location only for the specific types configured via AlwaysUseServiceLocationFor().

Example of the expected adapter:

using var serviceScope = _serviceScopeFactory.CreateScope();
// This service has been marked as requiring service location independent of Wolverine's ability to use constructor injection of everything else
var mapper = Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService<AutoMapper.IMapper>(serviceScope.ServiceProvider);
await using var tenantDbContext = await _dbContextBuilder.BuildAndEnrollAsync(context, cancellation);
var customerRepository = new CodeGen.CustomerRepository(tenantDbContext, mapper);

Actual generated adapter behavior (problem)

using var serviceScope = _serviceScopeFactory.CreateScope();

/*
* Using the scoped provider service location approach
* because at least one dependency is directly using IServiceProvider or has an opaque Lambda registration that has ServiceLifetime of either Scoped or Transient
*/
var customerRepository = Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService<CodeGen.ICustomerRepository>(serviceScope.ServiceProvider);
// The actual message body
var createCustomerRequest = (CodeGen.Handlers.CreateCustomerRequest)context.Envelope.Message;

await using var tenantDbContext = await _dbContextBuilder.BuildAndEnrollAsync(context, cancellation);

This contradicts the intent of ServiceLocationPolicy.NotAllowed + AlwaysUseServiceLocationFor<>.

Versions / environment:

  • WolverineFx: 5.11.0
  • WolverineFx.EntityFrameworkCore: 5.11.0
  • WolverineFx.SqlServer: 5.11.0
  • EF Core: 10.0.2
  • SQL Server
  • Target framework: net10.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions