1

the past days I have been struggling with injecting a DbContext in MY background worker. On the one hand, I want to inject the dbContext in my backgorund worker, but on the other hand I also want to use it in my API.

The injecting in my API seems to work fine, but since my worker is a singleton, I can not follow the standard lifetime of scoped for my dbcontext, and I have to add it as transient.

I have already tried to create a unit of work, in which I can refresh the context myself in my worker, effectively creating some kind of scoped service. I would refresh the context every time the worker went through his loop once again. This worked, and the application was working as I wanted, but I was no longer able to properly test, since I would create a new DbContext myself in the code. I feel like there must be a better way for to handle this.

My project structure looks like the following:

API => contains controlers + models I use for post requests. The API project needs to use my database, to get and post data. It uses the repositories for this

Core (class library) => contains some core models

Domain(class library) => Contains my domain models + repositories. All database work goes through here

Worker => Contains some logic. The worker needs to use my database, to get and post data. It uses the repositories for this

Services (class library) => Some services that contain some logic. The worker uses my repositories to get to the database

Tests => Tests for all code. I want to be able to to integrationTesting as well here.

I currently inject all repositories and services in both my API and worker:

Worker configureservices:

  public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureServices((hostContext, services) =>
            {
                services.AddDbContext<CatAPIDbContext>(ServiceLifetime.Transient);
                services.AddTransient(typeof(IFeedingProfileRepository), typeof(FeedingProfileRepository));
                services.AddTransient(typeof(IFeedingTimesRepository), typeof(FeedingTimesRepository));
                services.AddTransient(typeof(IFeedHistoryRepository), typeof(FeedHistoryRepository));
                services.AddTransient(typeof(IMotorController), typeof(MotorController));
                services.AddTransient(typeof(IFoodDispenser), typeof(FoodDispenser));
                services.AddTransient(typeof(IGenericRepository<>), typeof(GenericRepository<>));
                services.AddTransient(typeof(IFeedingTimeChecker), typeof(FeedingTimeChecker));
                services.AddHostedService<Worker>();
            });

(EDIT)Worker code:

public class Worker : BackgroundService
{
    private readonly ILogger<Worker> _logger;
    public IFeedingTimeChecker _feedingTimeChecker { get; }
    public Worker(ILogger<Worker> logger, IFeedingTimeChecker feedingTimeChecker)
    {
        _logger = logger;
        _feedingTimeChecker = feedingTimeChecker;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {

        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
                _feedingTimeChecker.ResetFeedingTimesGivenIfNeeded();
                _feedingTimeChecker.FeedIfNeeded();
            }
            catch(Exception ex)
            {
                _logger.LogError(ex.ToString());
            }
            await Task.Delay(10000, stoppingToken);
        }
    }
}

(EDIT)FeedingTimeChecker (called by worker)

private FeedingProfile _currentProfile { get; set; }
    public DateTime lastResetDataFeedingTimes;
    public DateTime lastProfileRefresh;
    private readonly ILogger<FeedingTimeChecker> _logger;
    private IFeedingProfileRepository _feedingProfileRepository { get; set; }
    private IFeedingTimesRepository _feedingTimesRepository { get; set; }
    private IFoodDispenser _foodDispenser { get; }


    public FeedingTimeChecker(IFeedingProfileRepository feedingProfileRepository, IFeedingTimesRepository feedingTimesRepository,IFoodDispenser foodDispenser, ILogger<FeedingTimeChecker> logger)
    {
        lastResetDataFeedingTimes = DateTime.MinValue.Date;
        lastProfileRefresh = DateTime.MinValue.Date;
        _foodDispenser = foodDispenser;
        _logger = logger;
        _feedingTimesRepository = feedingTimesRepository;
        _feedingProfileRepository = feedingProfileRepository;
    }        
    public void UpdateCurrentProfile()
    {
        if(Time.GetDateTimeNow - TimeSpan.FromSeconds(5) > lastProfileRefresh)
        {
            _logger.LogInformation("Refreshing current profile");
            _currentProfile = _feedingProfileRepository.GetCurrentFeedingProfile();
            lastProfileRefresh = Time.GetDateTimeNow;
        }
    }

API configureServices:

 public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvcCore().SetCompatibilityVersion(CompatibilityVersion.Version_3_0);
        services.AddDbContext<CatAPIDbContext>();
        services.AddTransient(typeof(IFeedingProfileRepository), typeof(FeedingProfileRepository));
        services.AddTransient(typeof(IFeedingTimesRepository), typeof(FeedingTimesRepository));
        services.AddTransient(typeof(IFeedHistoryRepository), typeof(FeedHistoryRepository));
        services.AddTransient(typeof(IMotorController), typeof(MotorController));
        services.AddTransient(typeof(IFoodDispenser), typeof(FoodDispenser));
        services.AddTransient(typeof(IGenericRepository<>), typeof(GenericRepository<>));
    }

in my repositories I use the dbContext like the following:

public class GenericRepository<T> : IGenericRepository<T> where T : class
{
    public CatAPIDbContext _dbContext { get; set; }
    public GenericRepository(CatAPIDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public T GetById(object id)
    {
        return _dbContext.Set<T>().Find(id);
    }
}

The result I would expect, is for my worker and API to behave correctly, always getting the lastest data and disposing of the dbContext on every single request, since I use a transient lifetime for my dbContext.

However, in my worker, I always get the following error: The instance of entity type 'FeedingTime' cannot be tracked because another instance another instance of this type with the same key is already being tracked.

This error occurs when I try to set a column in the FeedingTime table. A feedingProfile has 0-many feedingTimes, and the feedingProfile constantly retrieved.

Any solution where I can keep a testable clean codebase, but yet not run into this problem would be very welcome.

Thanks in advance

Runey
  • 179
  • 1
  • 10
  • 1
    Rather than re-instantiating your context in the worker, could you not just refresh the entity(s) that you need? Something like this: https://stackoverflow.com/a/51290890/3922214 – Jonathan Carroll Oct 29 '19 at 16:44
  • I have just tried that: while I no longer get the error, the data I get now does not match my database. In my worker I set the field 'Given' from the 'FeedingTimes' to true. I see this change reflected in my database. But my API retrieves my profile, containing the feedingTimes and there the feedingTime has given = false. Before, I had set theQueryTrackingBehavior to NoTracking, because it seemed to cause this problem. I have now removed that, in order to make the refreshing a single entity work. – Runey Oct 29 '19 at 16:58
  • It seems like the error is caused by the ChangeTracking. If I add the ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking on the entire context, I get the error. If I remove it, I do not get the error, but I get outdated/incorrect data from the dbcontext. – Runey Oct 29 '19 at 17:08
  • can your put the code of your worker? you need to create a new scope and instantiate a new dbcontext, each time your worker "loops" with a new action. btw, dbcontext is scoped, then your repositories should be scoped also (not transient). – hugo Oct 29 '19 at 21:57
  • I have added some code for the worke and the code the worker calls. I had indeed tried to make my own scope before, which worked. I did not do it with a service resolver or anything though. If all my repositories should be scoped as well, would I then have to create a scope for every single repository and dbcontext for every loop of my singleton myself? If I understand correctly, that would mean I can't really use DI, or would I use DI to create the scope? Last time when I tried to make my own scope, I ended up just creating a new CatAPIDbContext() – Runey Oct 30 '19 at 09:24

0 Answers0