z4kn4fein / stashbox-extensions-dependencyinjection

Stashbox Integration for ASP.NET Core, .NET Generic Host and ServiceCollection based applications.
https://z4kn4fein.github.io/stashbox
MIT License
17 stars 2 forks source link

Question: Choose from an implementation at runtime (ASP Core 6 Web API) #9

Closed abgenullt closed 1 year ago

abgenullt commented 1 year ago

Hi,

I have a service interface with different implementations (registered with different names) like this:

builder.Services.AddScoped<IService, ServiceA>("a");
builder.Services.AddScoped<IService, ServiceB>("b");
builder.Services.AddScoped<IService, ServiceC>("c");

I need to resolve this inside a request of a controller. I would normally use the IServiceProvider for that. But how can I resolve named services?

This is my sample controller:

    [ApiController]
    [Route("[controller]")]
    public class GreetingsController : Controller
    {
        private readonly IServiceProvider _serviceProvider;

        public GreetingsController(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider;

        [HttpGet("[action]")]
        public Task<string> SayHello(string serviceName)
        {
            // How can I do this?
            var service = _serviceProvider.GetRequiredService<IService>(serviceName);

            return service.Greetings();
        }
    }

Regards abgenullt

z4kn4fein commented 1 year ago

Hi @abgenullt, thank you for reaching out. In v5.0.0, I've published additional extension methods for IServiceProvider that allows named resolution in the way you posted.

If you don't want to update to v5.0.0, you can use an IDependencyResolver (instead of IServiceProvider) which has the ability to resolve named services with Resolve<IService>(serviceName).

Let me know your thoughts!

abgenullt commented 1 year ago

Awesome, thank you!

I will test your solution next week and give feedback.

Regards abgenullt

abgenullt commented 1 year ago

I try to resolve the service with the new version 5.0 and the IServiceProvider but I get a NotSupportedException ('Only a StashboxServiceProvider can serve named resolution requests.'). Is there something I am missing?

Regards abgenullt

z4kn4fein commented 1 year ago

Hi @abgenullt, the NotSupportedException means that you called the GetService(object name) on a simple ServiceProvider. However, it is only supported on a StashboxServiceProvider because the Microsoft.Extensions.DependencyInjection service provider has no such functionality. The extension replaces the default service provider with StashboxServiceProvider when you call the ServiceCollection.UseStashbox() or IHostBuilder.UseStashbox() extension methods.

May I ask how you integrate Stashbox into your ASP.NET Core application?

abgenullt commented 1 year ago

Hi, sure. I'm using the default template "ASP.NET Core Web API" with .NET 6. My code looks like this:

Program.cs

using Stashbox;
using TestApi.Test;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Host.UseStashbox();

builder.Host.ConfigureContainer<IStashboxContainer>((context, container) =>
{
    // Execute container validation in development mode.
    if (context.HostingEnvironment.IsDevelopment())
    {
        container.Validate();
    }
});

builder.Services.AddControllers()
       .AddControllersAsServices();

// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddScoped<IService, ServiceA>("a");
builder.Services.AddScoped<IService, ServiceB>("b");
builder.Services.AddScoped<IService, ServiceC>("c");

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseAuthorization();

app.MapControllers();

app.Run();

This is my controller:

using Microsoft.AspNetCore.Mvc;
using TestApi.Test;

namespace TestApi.Controllers;

[ApiController]
[Route("[controller]")]
public class GreetingsController : Controller
{
    private readonly IServiceProvider _serviceProvider;

    public GreetingsController(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider;

    [HttpGet("[action]")]
    public Task<string> SayHello()
    {
        var service = _serviceProvider.GetService<IService>("b"); // <- Throws exception

        return service.Greetings();
    }
}
z4kn4fein commented 1 year ago

Hi @abgenullt, thanks for the example! I've found the bug and released the fix in v5.1.0. Let me know if there's any further issues!

abgenullt commented 1 year ago

Thank you very much, but with the new version, I get an AggregateException at "container.Validate()":

Container validation failed. See the inner exceptions for details. (Expression of type 'System.IServiceProvider' cannot be used for constructor parameter of type 'Microsoft.Extensions.DependencyInjection.IServiceProviderIsService' (Parameter 'arguments[2]')) (Expression of type 'System.IServiceProvider' cannot be used for constructor parameter of type 'Microsoft.Extensions.DependencyInjection.IServiceProviderIsService' (Parameter 'arguments[2]')) (Expression of type 'System.IServiceProvider' cannot be used for constructor parameter of type 'Microsoft.Extensions.DependencyInjection.IServiceProviderIsService' (Parameter 'arguments[2]')) (Expression of type 'System.IServiceProvider' cannot be used for constructor parameter of type 'Microsoft.Extensions.DependencyInjection.IServiceProviderIsService' (Parameter 'arguments[2]'))

And this is one of the inner exceptions:

Expression of type 'System.IServiceProvider' cannot be used for constructor parameter of type 'Microsoft.Extensions.DependencyInjection.IServiceProviderIsService' (Parameter 'arguments[2]')

z4kn4fein commented 1 year ago

Thanks for the report! Sorry for missing this. I'm going to work on the fix.

z4kn4fein commented 1 year ago

@abgenullt I've released the fix in v5.1.1, please let me know if you see further issues!

abgenullt commented 1 year ago

Thank you for your effort. The resolving is working now.

But shouldn't the GetService method return "null", if a service could not be resolved, but not throwing an "ResolutionFailedException"? This should only happen with the GetRequiredService, if I understand the purposes correctly.

z4kn4fein commented 1 year ago

Yes, you explained it correctly, that's the difference between GetService() and GetRequiredService(). Have you received a ResolutionFailedException by calling GetService()? If so, then it's a bug.

z4kn4fein commented 1 year ago

I have found the issue, I've put the wrong Stashbox resolution method behind the generic version of GetService<T>()🤦. Really sorry, the fix is on the way!

z4kn4fein commented 1 year ago

I've released a fix in v5.1.2. My apologies for the inconvenience, let me know if you spot further issues!

abgenullt commented 1 year ago

No problem, I'm glad to help.