Search code examples
c#dependency-injectionvisual-studio-2022ridermicrosoft-extensions-di

Microsoft.Extensions.DependencyInjection: DI'd code runs fine on dotnet CLI / Rider default .NET profile, but crashes VS2022


A number of users reported a bug running a pretty basic sample project in one of our OSS repositories here: https://github.com/akkadotnet/Akka.Hosting/issues/179

System.TypeLoadException

The sample code looks like the following:

public interface IReplyGenerator
{
    string Reply(object input);
}

public class DefaultReplyGenerator : IReplyGenerator
{
    public string Reply(object input)
    {
        return input.ToString()!;
    }
}

public class EchoActor : ReceiveActor
{
    private readonly string _entityId;
    private readonly IReplyGenerator _replyGenerator;
    public EchoActor(string entityId, IReplyGenerator replyGenerator)
    {
        _entityId = entityId;
        _replyGenerator = replyGenerator;
        ReceiveAny(message => {
            Sender.Tell($"{Self} rcv {_replyGenerator.Reply(message)}");
        });
    }
}

public class Program
{
    private const int NumberOfShards = 5;
    
    private static Option<(string, object)> ExtractEntityId(object message)
        => message switch {
            string id => (id, id),
            _ => Option<(string, object)>.None
        };

    private static string? ExtractShardId(object message)
        => message switch {
            string id => (id.GetHashCode() % NumberOfShards).ToString(),
            _ => null
        };

    public static void Main(params string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);

        builder.Services.AddScoped<IReplyGenerator, DefaultReplyGenerator>();
        builder.Services.AddAkka("MyActorSystem", configurationBuilder =>
        {
            configurationBuilder
                .WithRemoting(hostname: "localhost", port: 8110)
                .WithClustering(new ClusterOptions{SeedNodes = new []{ "akka.tcp://MyActorSystem@localhost:8110", }})
                .WithShardRegion<Echo>(
                    typeName: "myRegion",
                    entityPropsFactory: (_, _, resolver) =>
                    {
                        return s => resolver.Props<EchoActor>(s);
                    },
                    extractEntityId: ExtractEntityId,
                    extractShardId: ExtractShardId,
                    shardOptions: new ShardOptions());
        });

        var app = builder.Build();

        app.MapGet("/", async (HttpContext context, IRequiredActor<Echo> echoActor) =>
        {
            var echo = echoActor.ActorRef;
            var body = await echo.Ask<string>(
                    message: context.TraceIdentifier, 
                    cancellationToken: context.RequestAborted)
                .ConfigureAwait(false);
            await context.Response.WriteAsync(body);
        });

        app.Run();    
    }
}

The key line that is triggering the failure (only sometimes!) is here:

 entityPropsFactory: (_, _, resolver) =>
                    {
                        return s => resolver.Props<EchoActor>(s);
                    },

The resolver.Props<TActor> method is part of Akka.DependencyInjection and under the hood it does the following (so you can see exactly how Microsoft.Extensions.DependencyInjection is being called:)

internal class ServiceProviderActorProducer : IIndirectActorProducer
    {
        private readonly IServiceProvider _provider;
        private readonly object[] _args;

        public ServiceProviderActorProducer(IServiceProvider provider, Type actorType, object[] args)
        {
            _provider = provider;
            _args = args;
            ActorType = actorType;
        }

        public ActorBase Produce()
        {
            return (ActorBase)ActivatorUtilities.CreateInstance(_provider, ActorType, _args);
        }

        public Type ActorType { get; }

        public void Release(ActorBase actor)
        {
            // no-op
        }
    }

You can see the current Akka.NET version of the above code here.

The error message makes it sound as though the ActivatorUtilities class was trying to use a 0-argument constructor, which the defined type does not have - hence the error.

Here is what makes this weird: we don't see this error when launching the sample application via dotnet run or Rider 2022.3's default ".NET profile". The error only occurs when running in VS2022 or when using Rider 2022.3's ".NET Launch profile" which is different somehow. You can see our list of testing results here: https://github.com/akkadotnet/Akka.Hosting/issues/179#issuecomment-1372320831

Why does this code work fine under one configuration but not the other?


Solution

  • So I got an answer to my question on Twitter - apparently the problem was really that we were creating IReplyGenerator as a scoped dependency in an environment where there aren't any scopes to speak of (Akka.NET actors don't have scopes):

    builder.Services.AddScoped<IReplyGenerator, DefaultReplyGenerator>();
    

    As for why this isn't an issue in some environments (ASPNETCORE_ENVIRONMENT="Production") but it is for others (ASPNETCORE_ENVIRONMENT="Development") - that's due to this setting inside the IServiceProvider implementation: https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.serviceprovideroptions.validatescopes

    ServiceProviderOptions.ValidateScopes Property in MSFT documentation

    In development environments this value is set to true but in production it defaults to false - thus in production environments these resources will be created even without scopes, but in a dev environment an error is raised to try to alert the developer that there isn't a IScope for this "scoped" dependency.

    The error message doesn't make that clear at all, but hopefully this answer will help someone else.

    Credit to https://twitter.com/chton/status/1611024413314400260 for figuring this out.