pakrym / jab

C# Source Generator based dependency injection container implementation.
MIT License
1.04k stars 33 forks source link

Use in Web API #40

Open Tandis5 opened 3 years ago

Tandis5 commented 3 years ago

Just saw a demo of this on YouTube (shoutout to Nick Chapsas... love his channel!) and it looks very interesting! However, 95% of what I do is in a web API. Do you have plans to support a clean way of using Jab in a web API? I could use the default DI to inject my Jab "service provider" into controllers/classes and request what I need from there, but that's not a clean solution IMO.

LechuckThePirate commented 3 years ago

+1

pakrym commented 3 years ago

This (Jab fully replacing the default DI) is something I would love to implement. But unfortunately there are two technical issues that stand in the way:

  1. In a typical ASP.NET Core app service configuration happens during runtime via the AddSingleton/Transient calls and might be based on the runtime logic (if (environment.IsDevelopment)). It's very hard to get a correct service graph during compilation time as it requires executing the app.
  2. Source generators are governed by the C# accessibility rules while dynamic methods used in default DI are not. What it means is that even if I was able to get the full and correct service graph I can't generate new ImplementationType() calls because the ImplementationType is, in many cases, internal.
pakrym commented 3 years ago

So while fully replacing the default service provider is hard short term I'll still try to think if there is any way to integrate them in a clean way.

I'm also open to ideas of how you would like to see it.

Tandis5 commented 3 years ago

Hi Pavel... thanks for the reply! I have no doubt that it's well beyond my current knowledge to understand how it all works under the covers. As far as ideas on how we would see it (at least for the environment logic)...

[ServiceProvider]
[Scoped(typeof(IService), typeof(DevelopmentServiceImplementation), Environment = Environments.Development)]
[Scoped(typeof(IService), typeof(IntegrationServiceImplementation), Environment = "Integration")]
[Scoped(typeof(IService), typeof(ServiceImplementation))]
internal partial class MyServiceProvider { }

So Jab would generate the injection code for all 3 scenarios (development, integration, default) and use the appropriate injection based on the current running environment at runtime. A "default" (no environment specified) would be required (build would fail without it) to cover any environments not listed.

Just my thoughts. Thanks again for the response!

LechuckThePirate commented 3 years ago

Maybe you could have a look at https://github.com/YairHalberstadt/stronginject/tree/main/Samples/AspNetCore .. it's a similar solution to yours, and someone managed to workaround ASP.NET controllers dependency injection.

I would like even to have a look myself at it when I'm a little more free, but right now I don't have time... maybe in 15 days I can have a look at it... if I can apply the same solution as theirs I would make a PR for you ;)

pakrym commented 3 years ago

The linked approach has a problem: now you have 2 DI containers managing the same set of objects causing problems like multiple disposal. It also makes it hard to resolve outer DI services from the source generated container.

I have a branch where I try to marry Microsoft.Extensions.DependencyInjection with Jab and make them aware of each other and cooperating. It's very much WIP but might result in something interesting.

dazinator commented 3 years ago

This (Jab fully replacing the default DI) is something I would love to implement. But unfortunately there are two technical issues that stand in the way:

  1. In a typical ASP.NET Core app service configuration happens during runtime via the AddSingleton/Transient calls and might be based on the runtime logic (if (environment.IsDevelopment)). It's very hard to get a correct service graph during compilation time as it requires executing the app.
  2. Source generators are governed by the C# accessibility rules while dynamic methods used in default DI are not. What it means is that even if I was able to get the full and correct service graph I can't generate new ImplementationType() calls because the ImplementationType is, in many cases, internal.

Just chucking out some ideas:

I think if you wanted to instead build a jab container for all of the asp.net core dependencies it would be a lot of work to figure out the various runtime conditions like environment or config checks like you have mentioned.

Another approach:

Forgive me if you've already established a way forward with this!

MisinformedDNA commented 1 year ago

AutoFaq can also consume Microsoft's DI. I agree that not everything needs to be compile-time.

dotnetprofessional commented 1 year ago

Any update on this? Just discovered this project and I like the concept. Though like others backend work is my bread and butter and honestly that's where the perf gains are most useful, don't really care too much about saving a few ns on a client, but on the server that = $$ saved.

R2D221 commented 11 months ago

I made a prototype integration with Microsoft.Extensions.DependencyInjection:

https://github.com/R2D221/Jab.MediIntegration/blob/master/WebApplication1/MyServiceProvider.Medi.cs

It may be rough and not optimized beyond the obvious, but for the moment I think it gets the job done.

Essentialy what I made was the following:

I don't know if this still aligns with the spirit of the project, but I think this could be useful to be able to handle the dynamic nature of Microsoft.Extensions.DependencyInjection while still allowing us to add our compile-time dependencies.

If you'd want to refine this and include it officially in the project, I'd be happy to help. Even if not, I'm happy to share my solution with anyone who needs it.

matthew-a-thomas commented 2 months ago

For anyone who thinks that the answer ought to be simpler than @R2D221's integration above, perhaps even as simple as using IServiceCollection.BuildServiceProvider() to get an IServiceProvider instance that can be composed very easily... the answer is you can't! The complexity is there for a reason. The ServiceProvider that you get from the call to BuildServiceProvider() only knows about itself and its own little world of IServiceProvider implementations and there's no way to educate it about the world of Jab.

For example, you won't be able to constructor-inject any of your controllers with Jab stuff, because the thing that resolves controllers will live within the ServiceProvider while the dependencies will live in Jab. StrongInject gets around this specifically by re-registering IControllerActivator (with the ServiceBasedControllerActivator class), and some workarounds to get that to work as expected. But that approach might not work in the general case, I dunno.

So in effect you have to duplicate the functionality of ServiceProvider, which effectively is what has been done by @R2D221, Autofac, and certain others who use UseServiceProviderFactory.

A very rough and hacky alternative is to just re-register all the Jab services into Microsoft's IServiceCollection. This doesn't consider scopes, lifetimes, or any of that... everything is registered as transient:

using System.Linq.Expressions;
using Jab;
using Microsoft.Extensions.DependencyInjection;

static class ServiceCollectionExtensions
{
    public static void AddJabServices(this IServiceCollection serviceCollection, object services)
    {
        foreach (var iface in services.GetType().GetInterfaces())
        {
            if (!iface.IsGenericType)
                continue;
            var genericType = iface.GetGenericTypeDefinition();
            if (genericType != typeof(IServiceProvider<>))
                continue;
            var getServiceMethod = iface.GetMethod(nameof(IServiceProvider<object>.GetService)) ?? throw new Exception("Cannot find the method");
            var serviceType = iface.GetGenericArguments().Single();
            var servicesParameter = Expression.Parameter(typeof(object));
            var getService = Expression.Lambda<Func<object, object>>(
                Expression.Convert(
                    Expression.Call(
                        Expression.Convert(
                            servicesParameter,
                            iface
                        ),
                        getServiceMethod
                    ),
                    typeof(object)
                ),
                servicesParameter
            ).Compile();
            serviceCollection.AddTransient(
                serviceType,
                _ => getService(services)
            );
        }
    }
}
var builder = WebApplication.CreateBuilder();
builder.Services.AddJabServices(services); // services is an instance of your Jab services class