Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V4 projections and DI containers #1670

Closed
jeremydmiller opened this issue Dec 18, 2020 · 5 comments
Closed

V4 projections and DI containers #1670

jeremydmiller opened this issue Dec 18, 2020 · 5 comments

Comments

@jeremydmiller
Copy link
Member

So, we know that some projections are gonna want to use services registered in the application's DI container. V2/3 has the LazyLoadedProjection to work around this a little bit, but for V4 let's do something more systematic and take advantage of the generic host builder integration.

Also, I'm killing off LazyLoadedProjection as part of the V4 projections anyway:)

If the projection's dependencies are all singletons

Meaning in this case that the projection only needs a single instance of the service dependency that can be used everywhere, we could:

  • Have options in the AddMarten() bootstrapping to just say "use this projection type from the DI container" like options.Events.Projections.Async(Type type). I'd vote for no generic () registrations because you can't overload that w/ generic constraints. At bootstrapping time, the creation of the DocumentStore would build that upfront and stick it in the right places
  • Have an option to register a projection like options.Events.Projections.Inline(Func<IServiceProvider, IInlineProjection>) that does the same. That gives you the option to use IConfiguration and IHostingEnvironment as well. Not that you can't use that other wise in AddMarten() anyway though, so maybe nevermind.

If the projection needs to use a new instance of the service dependency every time...

One, let's try really hard not to need this because the complexity would skyrocket.

Could use method injection ala ASP.Net Core controller actions like: Apply(MyEvent e, MyAggregate a, [FromServices] ISomething). That makes Marten responsible for doing service location, lifecycle management through scoped containers, disposal, and generally slows the projections down quite a bit.

Or force the author of the custom projection to deal with all that themselves and just have them inject IServiceProvider into their own projection class and make them deal with all that crap themselves. Serves them right for doing funky stuff like this anyway.

@jeremydmiller jeremydmiller added this to the 4.0 milestone Dec 18, 2020
@jeremydmiller jeremydmiller modified the milestones: 4.0, 4.1.0 Feb 14, 2021
@oskardudycz oskardudycz modified the milestones: 4.1.0, 4.2.0 Nov 12, 2021
@oskardudycz oskardudycz removed this from the 4.2.0 milestone Nov 21, 2021
@jeremydmiller
Copy link
Member Author

I'm vetoing this for now.

@rosdi
Copy link

rosdi commented Apr 15, 2023

Hi sorry for asking in this long closed issue. Is there any way to perform DI into projections now? I would like to inject asp.net core ILogger into my projections.

@mysticmind
Copy link
Member

Hi sorry for asking in this long closed issue. Is there any way to perform DI into projections now? I would like to inject asp.net core ILogger into my projections.

https://martendb.io/events/projections/custom.html

@rosdi
Copy link

rosdi commented Apr 17, 2023

Hi @mysticmind, I don't see any DI in the custom projection you shared?

@AndyPook
Copy link

AndyPook commented Aug 4, 2023

here's an approach that keeps the "complexity" low, from the dev point of view
The AddSingleton method is just sugar, you could do this now with the IServiceProvider version of AddMarten().
AddScoped just wraps your type, using the serviceprovider as a factory. Obviously there's cost and allocations happening on every invocation. But if you need a new instance each time, then... 🤷


builder.Services.AddMarten((sp, options) =>
{
    ...
    options.Projections.AddScoped<MyProjection>(sp, ProjectionLifecycle.Async);
})
.AddAsyncDaemon(DaemonMode.Solo);

MyProjection is just an impl of IProjection

public static class MartenExtensions
{
    public static MartenServiceCollectionExtensions..MartenConfigurationExpression AddMarten(
        this IServiceCollection services,
        Action<IServiceProvider, StoreOptions> configure)
    {
        return services.AddMarten(sp =>
        {
            var options = new StoreOptions();
            configure(sp, options);
            return options;
        });
    }

    public static void AddScoped<TProjection>(
        this ProjectionOptions options, 
        IServiceProvider sp, 
        ProjectionLifecycle lifecycle, 
        string projectionName = null!, 
        Action<AsyncOptions> asyncConfiguration = null!)
        where TProjection : IProjection
    {
        options.Add(new ScopedProjectionWrapper<TProjection>(sp), lifecycle, projectionName, asyncConfiguration);
    }

    public static void AddSingleton<TProjection>(
        this ProjectionOptions options, 
        IServiceProvider sp, 
        ProjectionLifecycle lifecycle, 
        string projectionName = null!, 
        Action<AsyncOptions> asyncConfiguration = null!)
        where TProjection : IProjection
    {
        var projection = ActivatorUtilities.CreateInstance<TProjection>(sp);
        options.Add(projection, lifecycle, projectionName, asyncConfiguration);
    }

    private class ScopedProjectionWrapper<TProjection> : IProjection
        where TProjection : IProjection
    {
        private readonly IServiceProvider serviceProvider;

        public ScopedProjectionWrapper(IServiceProvider serviceProvider)
        {
            this.serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
        }

        public void Apply(IDocumentOperations operations, IReadOnlyList<StreamAction> streams)
        {
            using var scope = serviceProvider.CreateScope();
            var sp = scope.ServiceProvider;
            var projection = ActivatorUtilities.CreateInstance<TProjection>(sp);
            projection.Apply(operations, streams);
        }

        public async Task ApplyAsync(IDocumentOperations operations, IReadOnlyList<StreamAction> streams, CancellationToken cancellation)
        {
            using var scope = serviceProvider.CreateScope();
            var sp = scope.ServiceProvider;
            var projection = ActivatorUtilities.CreateInstance<TProjection>(sp);
            await projection.ApplyAsync(operations, streams, cancellation);
        }
    }
}

I am sure there are devils in the details, like...
What happens if you use IProjectionSource, ProjectionBase, CustomProjection ... it's a little awkward that some base types implement some of the interface methods explicitly, means the cannot be overridden, so there needs to be some copy&paste.

This works for me and my specific use case. Your experience will be different 😄

Do not use this if one of your dependencies is IDcoumentStore or StoreOptions (the container will just recurse forever)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants