dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.19k stars 4.72k forks source link

Complex architecture using DI scoped service #70415

Closed daniel-p-tech closed 2 years ago

daniel-p-tech commented 2 years ago

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;
...
ghost commented 2 years ago

Tagging subscribers to this area: @dotnet/area-extensions-dependencyinjection See info in area-owners.md if you want to be subscribed.

Issue Details
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 ```csharp public static void AddServices(this IServiceCollection services, IConfiguration configuration) { services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); ... } ``` ```csharp public class Process : 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 will be instantiated when accessed for the first time inside this loop await m_workflowFactory.Create(scope.ServiceProvider, notification.FilingTypeGuid).Execute(notification.FilingNotificationGuid); } } } } ... ``` ```csharp public class WorkflowFactory : IWorkflowFactory { public Workflow Create(IServiceProvider serviceProvider, Guid filingTypeGuid) { Workflow workflow = null; if (filingTypeGuid == Constant.FilingTypeGuid.Will) { workflow = ActivatorUtilities.CreateInstance(serviceProvider); } else if (filingTypeGuid == Constant.FilingTypeGuid.MentalHealth) { workflow = ActivatorUtilities.CreateInstance(serviceProvider); } ... ``` ```csharp 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 m_documents; private readonly List m_blobs; private readonly List 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; ... ```
Author: daniel-p-tech
Assignees: -
Labels: `area-Extensions-DependencyInjection`
Milestone: -
daniel-p-tech commented 2 years ago

Answered elsewhere.

https://github.com/dotnet/aspnetcore/issues/42103