dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.4k stars 10k forks source link

Complex architecture using DI scoped service #42103

Closed daniel-p-tech closed 2 years ago

daniel-p-tech commented 2 years ago

Is there an existing issue for this?

Is your feature request related to a problem? Please describe the problem.

I'm not sure if this is the proper repo, but I did not get any response in dotnet/runtime repo. I'm hoping someone from the team can take a look? Thank you!

Describe the solution you'd like

Hi,

I am architecting a complex background process that will be processing items (called "workflows" in my application) from a queue (an IEnumerable returned from a Web API call). The process consists of numerous services, some of which expose data structures that need to be accessible from other services: Service A and Service B may need to add items to a collection maintained by Service C. Once a "workflow" item is processed, next "workflow" item to be processed from the queue needs to start with a new state (all services need to be disposed of and a new instance of Service C is provided when requested again).

My understanding is that this should be possible to implement using a scoped service. However, I have spent several hours doing research/reading documentation, but I was unable to find a good example on how to architect this. Please take a look at the code snippets below. Based on my preliminary tests, this approach seems to work. Basically, I pass in the scope.ServiceProvider reference to the workflow factory which then uses this IServiceProvider instance to create a new Workflow object. Is my assumption true that all services from this point on will be resolved using this scoped IServiceProvider rather that the root IServiceProvider? For instance, in the constructor of the DocumentBuilder object, I need all services that were registered with scoped lifetime to be resolved using the scoped IServieProvider passed to WorkfloFactory.Create() method.

In other words, I am expecting the same behavior as ASP.NET Core web app except that my request is created inside a custom loop rather that an HTTP request.

Thank you for your help, Daniel

    public static void AddServices(this IServiceCollection services, IConfiguration configuration)
    {
        services.AddScoped<IDocumentBuilderFactory, DocumentBuilderFactory>();
        services.AddScoped<IEmailBuilderFactory, EmailBuilderFactory>();
        services.AddScoped<IDocumentBuilderFactory, DocumentBuilderFactory>();
        services.AddScoped<IEmailClientFactory, SmtpEmailClientFactory>();
        services.AddScoped<IConsolidatedFormFactory, ConsolidatedFormFactory>();
        ...
    }
    public class Process<TFiling, TPosting> : IHostedService
        where TFiling : Filing where TPosting : PostingModel, new()
    {
    ...
        public async Task StartAsync(CancellationToken cancellationToken)
        {
            var notifications = await m_notificationWebApiService.GetUnprocessedFilingNotificationsAsync();
            foreach (var notification in notifications)
            {
                using (var scope = m_serviceProvider.CreateScope())
                {
                    // all scoped services resolved from this point on need to be instantiated when accessed for the first time
                    await m_workflowFactory.Create(scope.ServiceProvider, notification.FilingTypeGuid).Execute(notification.FilingNotificationGuid);
                }
            }
        }
    }
...
    public class WorkflowFactory : IWorkflowFactory<Filing, Posting>
    {
        public Workflow<Filing, Posting> Create(IServiceProvider serviceProvider, Guid filingTypeGuid)
        {
            Workflow<Filing, Posting> workflow = null;

            if (filingTypeGuid == Constant.FilingTypeGuid.Will)
            {
                workflow = ActivatorUtilities.CreateInstance<Will>(serviceProvider);
            }
            else if (filingTypeGuid == Constant.FilingTypeGuid.MentalHealth)
            {
                workflow = ActivatorUtilities.CreateInstance<MentalHealth>(serviceProvider);
            }
...
    public class DocumentBuilder : IDocumentBuilder
    {
        private readonly AppSettings m_appSettings;
        private readonly IElectronicStamp m_electronicStamp;
        private readonly FilingNotificationFormRepository m_filingNotificationFormRepository;
        private readonly IConsolidatedFormRepository m_consolidatedFormRepository;
        private readonly NotificationWebApiService m_notificationWebApiService;
        private readonly Filing m_filing;

        private readonly List<PostDocument> m_documents;
        private readonly List<Blob> m_blobs;
        private readonly List<DocketEntryDocument> m_docketEntryDocuments;

        public DocumentBuilder(AppSettings appSettings, IElectronicStamp electronicStamp,
            FilingNotificationFormRepository filingFormRepository, IConsolidatedFormRepository consolidatedFormRepository,
            NotificationWebApiService notificationWebApiService, Filing filing)
        {
            m_appSettings = appSettings;
            m_electronicStamp = electronicStamp;
            m_filingNotificationFormRepository = filingFormRepository;
            m_consolidatedFormRepository = consolidatedFormRepository;
...

Additional context

No response

davidfowl commented 2 years ago

Yes this looks fine. Beware that ActivatorUtilities.CreateInstance is inefficient and CreateFactory should be used instead.

daniel-p-tech commented 2 years ago

Thanks!