Search code examples
c#.netasp.net-corebackground-process

.NET Core BackgroudService DI Scope null


I have a .NET backgroud service

public class Worker : BackgroundService
    {
        private readonly ILogger<Worker> _logger;
        private readonly ConvertService _convert;

        public Worker(ILogger<Worker> logger, ConvertService convert)
        {
            _logger = logger;
            _convert = convert;
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                await _convert.GetXML();
                await Task.Delay(TimeSpan.FromHours(12), stoppingToken);
            }
        }
    }

Program.cs

using Microsoft.EntityFrameworkCore;
using P1_APT_SUB;
using P1_APT_SUB.BusService;
using P1_APT_SUB.Persistence;


IHost host = Host.CreateDefaultBuilder(args)
    
    .UseWindowsService(options =>
    {
        options.ServiceName = "APT";
    })
    .ConfigureServices((context,services) =>
    {
        services.AddHttpClient("auth", httpClient =>
        {
          
          
        });
        services.AddHttpClient("ODM", httpClient =>
        {
            
        });
        services.AddTransient<ConvertService>();
        services.AddTransient<BusServiceClass>();
        services.AddHostedService<Worker>();
        services.AddDbContext<DataContext>(
                 options =>
                 {
                     options.UseSqlServer();
                 });
    })
    .Build();

await host.RunAsync();

ConvertService.cs

Here is create scope and pass that to method like GetST() and httpCLient is ok

using Newtonsoft.Json.Linq;
using System.Xml.Linq;
using P1_APT_SUB.BusService;
using P1_APT_SUB.Persistence;
using P1_APT_SUB.Entities;

namespace P1_APT_SUB
{


    public class ConvertService
    {
        private readonly IHttpClientFactory? _httpClientFactory;
        private readonly IConfiguration? _config;
        private readonly DataContext _dataContext;
    
        private readonly BusServiceClass _busService;
        static public string? sites;
        

        public ConvertService(IConfiguration config, IServiceScopeFactory factory)
        {

            _httpClientFactory = factory.CreateScope().ServiceProvider.GetRequiredService<IHttpClientFactory>();
            _config = config;
            _dataContext = factory.CreateScope().ServiceProvider.GetRequiredService<DataContext>();
            _busService = factory.CreateScope().ServiceProvider.GetRequiredService<BusServiceClass>();
        }
        public ConvertService()
        {

        }
    
        public async Task GetXML()
        {
            var data = await new Domains().GetST(_httpClientFactory, _config);
            sites = await new Domains().GetSD(_httpClientFactory, _config);
       
        }

    }}
}

Domains.cs

using Microsoft.Extensions.Configuration;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;

namespace P1_APT_SUB
{
    public class Domains
    {
        public string subjects;
        public string sites;
        private readonly IHttpClientFactory _httpClientFactory;

      

        public async Task<string> GetST(IHttpClientFactory _httpClientFactory, IConfiguration _config)
        {
            
                var accessToken = await Authorization.GetAccessToken(_httpClientFactory);

                var httpClient = _httpClientFactory.CreateClient("ODM");
                httpClient.DefaultRequestHeaders.Add("X-ACCESS-TOKEN", accessToken);

                var httpResponseMessage = await httpClient.GetAsync();
                if (httpResponseMessage.IsSuccessStatusCode)
                {
                    var contentStream =
                       await httpResponseMessage.Content.ReadAsStringAsync();
                subjects = contentStream;
                 
                }
                return subjects;
            
           
           
        }
        public async Task<string> GetSD(IHttpClientFactory _httpClientFactory, IConfiguration _config)
        {
            
            
                var accessToken = await Authorization.GetAccessToken(_httpClientFactory);

                var httpClient = _httpClientFactory.CreateClient("ODM");
                httpClient.DefaultRequestHeaders.Add("X-ACCESS-TOKEN", accessToken);

                var httpResponseMessage = await httpClient.GetAsync();
                if (httpResponseMessage.IsSuccessStatusCode)
                {
                    var contentssStream =
                       await httpResponseMessage.Content.ReadAsStringAsync();

                sites = contentssStream;

                }
                return sites;
           
           
        }
       

    }
}

But when i want to DI/create scope directly in Domains like below it httpCLient is null. It is related that BackgroundService is singleton i think so DI?

using Microsoft.Extensions.Configuration;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;

namespace P1_APT_SUB
{
    public class Domains
    {
        public string subjects;
    
        private readonly IHttpClientFactory _httpClientFactory;

        public Domains(IServiceScopeFactory factory)
        {
            this.subjects = subjects;
            this.sites = sites;
           _httpClientFactory = factory.CreateScope().ServiceProvider.GetRequiredService<IHttpClientFactory>();
        }

        public Domains()
        {
        }

        public async Task<string> GetSubjects(IConfiguration _config)
        {
            
                var accessToken = await Authorization.GetAccessToken(**_httpClientFactory**);

                var httpClient = _httpClientFactory.CreateClient("ODM");
                httpClient.DefaultRequestHeaders.Add("X-ACCESS-TOKEN", accessToken);

                var httpResponseMessage = await httpClient.GetAsync();
                if (httpResponseMessage.IsSuccessStatusCode)
                {
                    var contentStream =
                       await httpResponseMessage.Content.ReadAsStringAsync();
                subjects = contentStream;
                 
                }
                return subjects;
            
           
           
        }
       
       

    }
}

EDIT

About DataContext being singleton i think this will do the job? as using will call Dispose()? sometimes it does not

await using var scope = _factory.CreateAsyncScope(); var context = scope.ServiceProvider.GetRequiredService<DataContext>(); List<Subject>? currentData = context.Subjects.ToList();


Added Domains as service and DI it into ConvertService

 services.AddTransient<Domains>();

 public ConvertService(IConfiguration config, IServiceScopeFactory factory, Domains domains)
        {

            _httpClientFactory = factory.CreateScope().ServiceProvider.GetRequiredService<IHttpClientFactory>();
            _config = config;
            _domains = domains;
            _factory = factory;
         
            _busService = factory.CreateScope().ServiceProvider.GetRequiredService<BusServiceClass>();
        }

now i can DI IServiceScopeFactory in Domains without passing it from Convert


Solution

  • The issue is n your constructor: I assumed the Domains object will created by DI, but you create it manually by using the default constructor:

    public Domains(IServiceScopeFactory factory)
    {
        this.subjects = subjects;
        this.sites = sites;
        _httpClientFactory = factory.CreateScope().ServiceProvider.GetRequiredService<IHttpClientFactory>();
    }
    
    public Domains()
    {
    }
    

    When you call create the Domains instance with new Domains() all members will be null since the parameterless constructor does nothing.

    To fix this you have to call the Domains(IServiceScopeFactory) constructor.

    The easiest way would be to store the IServiceScopeFactory instance in ConverService in a field and pass that to the constructor:

    var data = await new Domains(_factory).GetST(_httpClientFactory, _config);
    sites = await new Domains(_factory).GetSD(_httpClientFactory, _config);
    

    However, please take a look at the scopes as well, as your code currently will make your DataContext basically a singelton because of the way you create the instance.

    EDIT: Original, but wrong answer: You should not create the scopes in the constructor of a singleton.

    Think of the scope as the owner of its created services. Once the scope gets disposed all disposable services created by the scope get disposed as well.

    In your constructors you create temporary scopes that might be disposed by GC before you can use your services.

    What you should do instead is to inject IServiceScopeFactory in your constructor and create and dispose your service scope as needed:

    // Create new scope and dispose after use
    using var scope = _serviceScopeFactory.CreateScope();
    
    // Resolve instances
    var service = scope.ServiceProvider.GetRequiredService<TService>();
    

    For more guidelines take look at https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection-guidelines