.NET Core 6 inject databasecontext to Singleton Service - c#

I have a problem when try to use database context in one Singleton Service in my app.
App.cs
...
var service = builder.Services;
service.AddDbContext<MyDBContext>(options => options.UseSqlite(connectionString));
service.AddSingleton<MyMonitorManager>();
...
MyDBContext.cs
public class MyDBContext : ApiAuthorizationDbContext<ApplicationUser>
{
public DroneDBContext(...): base(options, operationalStoreOptions){}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder){}
protected override void OnModelCreating(ModelBuilder modelBuilder){}
}
MyMonitorManager.cs
public class MyMonitorManager {
private readonly MyDBContext _databaseManager;
...
}
There are errors:
Cannot consume scoped service 'MyController.MyDBContext' from singleton 'MyController.Service.MyMonitorManager'.
I try to search here and think about use like this:
MyDBContext dbContext = new MyDBContext();
service.AddSingleton(new MyMonitorManager(dbContext));
But not sure this is how I should do in .NET 6

You can use IServiceScopeFactory in your singleton class to create a new scope, and then get a db context reference from that:
public class MyMonitorManager {
private readonly IServiceScopeFactory _scopeFactory;
public MyMonitorManager(IServiceScopeFactory _scopeFactory)
{
_scopeFactory = scopeFactory;
}
public SomeMethod()
{
using (var scope = _scopeFactory.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<MyDBContext>();
...
}
}
}

You can't inject directly scoped service into singleton, you have follwoing options:
Register database context with different lifetime, though in general case it is not the best option:
service.AddDbContext<MyDBContext>(options => options.UseSqlite(connectionString), ServiceLifetime.Transient);
Inject IServiceScopeFactory to create scope and resolve context when needed the singleton service:
public class MyMonitorManager
{
private readonly IServiceScopeFactory _scopeFactory;
public class MyMonitorManager(IServiceScopeFactory scopeFactory) => _scopeFactory = scopeFactory;
public void SomeMethod()
{
using (var scope = _serviceScopeFactory.CreateScope())
{
var ctx = scope.ServiceProvider.GetService<MyDBContext>();
// use context
}
}
}

Related

XUnit integration Test with IDbContextFactory for a Generic Data Service

tl;dr;
How can I inject the IDbContext factory in a Integration XUnit test?
I´m working on a Blazor Server project and I am creating a service that uses IDbContextFactory instead of the normal DbContext. The service uses EntityFrameworkCore to communicate with the DB. I need to create integration tests for this service that use the real test database, so I won´t Moq the factory.
This is the basic structure of my service.
public class CatalogService<T> : IEntityService<T> where T : CatalogBase
{
private readonly IDbContextFactory<ApplicationDbContext> _contextFactory;
private readonly ILogger<T> _logger;
//The factory is injected via constructor
public CatalogService(IDbContextFactory<ApplicationDbContext> contextFactory, ILogger<T> logger)
{
_contextFactory = contextFactory;
_logger = logger;
}
//... All the functions
}
I also have a fixture where some seed data can be created
public class TestDatabaseFixture
{
private const string ConnectionString = #"my_connection";
private static readonly object _lock = new();
private static bool _databaseInitialized;
public TestDatabaseFixture()
{
lock (_lock)
{
if (!_databaseInitialized)
{
using (var context = CreateContext())
{
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
//Create Seed Data for Brands
for (int i = 0; i < 100; i++) {
context.PcMarcas.Add(new PcBrand { Description = $"Brand {(i + 1).ToString("0000") }" });
}
context.SaveChanges();
}
_databaseInitialized = true;
}
}
}
public ApplicationDbContext CreateContext()
{
return new ApplicationDbContext(
new DbContextOptionsBuilder<ApplicationDbContext>()
.UseSqlServer(ConnectionString)
.Options);
}
public IDbContextFactory<ApplicationDbContext> CreateDbContextFactory() {
//How do I return the context factory? <---
}
}
I know how to moq my logger and create a normal DBContext, but I´m not sure how to inject the factory in the test
[Fact]
public async Task GetAsync_ShouldReturnAnItem()
{
//Setup ------------------------------------------------------
var contextFactory = Fixture.CreateDbContextFactory(); //from the fixture
var logger = Mock.Of<ILogger<PcMarca>>();
var service = new CatalogService<PcMarca>(contextFactory, logger);
//Act -------------------------------------------------------
//Assert ----------------------------------------------------
}
You'll need to implement a IDbContextFactory<ApplicationDbContext> yourself.
To reuse your existing code to create an ApplicationDbContext instance, you can choose to implement that IDbContextFactory<ApplicationDbContext> as an inner class of your TestDatabaseFixture.
For brevity below code doesn't include the constructor.
public class TestDatabaseFixture
{
public class ApplicationDbContextFactory : IDbContextFactory<ApplicationDbContext>
{
public ApplicationDbContext CreateDbContext()
{
return CreateApplicationDbContext();
}
}
private const string ConnectionString = #"my_connection";
public ApplicationDbContext CreateContext()
{
return CreateApplicationDbContext();
}
public IDbContextFactory<ApplicationDbContext> CreateDbContextFactory()
{
return new ApplicationDbContextFactory();
}
private static ApplicationDbContext CreateApplicationDbContext()
{
return new ApplicationDbContext(
new DbContextOptionsBuilder<ApplicationDbContext>()
.UseSqlServer(ConnectionString)
.Options);
}
}

.NET custom DbContextFactory registration to DI

i created my own dbcontextfactory and now i don't know how to correctly register it in di. Can you somebody help me please? IApplicationDbContext is just interfaces with db sets.
I have register ma DbContext as pooled db context factory
builder.Services.AddPooledDbContextFactory<MyContext>(options =>
{
....
});
Interface of my db factory
interface IApplicationDbContextFactory
{
IApplicationDbContext CreateDbContext();
}
Implementation db factory
public class MyContextFactory<TContext> : IApplicationDbContextFactory where TContext : DbContext, IApplicationDbContext
{
private readonly IDbContextFactory<TContext> _dbContextFactory;
public MyContextFactory(IDbContextFactory<TContext> dbContextFactory)
{
_dbContextFactory = dbContextFactory;
}
public IApplicationDbContext CreateDbContext()
{
return _dbContextFactory.CreateDbContext();
}
}
How can i correctly register my factory to di?
Thank you
Lifetime for the factory registered by AddPooledDbContextFactory is Singleton. Just register it with builder.Services.AddSingleton<IApplicationDbContextFactory, MyContextFactory<MyContext>>(); (though Scoped and Transient should also work just as fine):
var serviceCollection = new ServiceCollection();
serviceCollection.AddPooledDbContextFactory<SomeContext>(builder => builder.UseSqlite($"Filename={nameof(SomeContext)}.db"));
serviceCollection.AddSingleton<IApplicationDbContextFactory, MyContextFactory<SomeContext>>();
var serviceProvider = serviceCollection.BuildServiceProvider();
var dbContextFactory = serviceProvider.GetRequiredService<IDbContextFactory<SomeContext>>();
using (var scope = serviceProvider.CreateScope())
{
var applicationDbContextFactory = serviceProvider.GetRequiredService<IApplicationDbContextFactory>();
var applicationDbContext = applicationDbContextFactory.CreateDbContext();
}
public class SomeContext : DbContext, IApplicationDbContext
{
public SomeContext(DbContextOptions<SomeContext> options) : base(options)
{
}
public DbSet<MyEntity> Entities { get; set; }
}
interface IApplicationDbContextFactory
{
IApplicationDbContext CreateDbContext();
}
public interface IApplicationDbContext
{
}
public class MyContextFactory<TContext> : IApplicationDbContextFactory where TContext : DbContext, IApplicationDbContext
{
private readonly IDbContextFactory<TContext> _dbContextFactory;
public MyContextFactory(IDbContextFactory<TContext> dbContextFactory)
{
_dbContextFactory = dbContextFactory;
}
public IApplicationDbContext CreateDbContext()
{
return _dbContextFactory.CreateDbContext();
}
}

Implementing unit of work in Entity Framework Core

I have implemented unit of work in the next way in Entity Framework Core.
Context:
public class DaleContext : DbContext, IDaleContext
{
private readonly IConnectionStringProvider _connectionStringProvider;
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(_connectionStringProvider.ConnectionString);
base.OnConfiguring(optionsBuilder);
}
public DaleContext(IConnectionStringProvider connectionStringProvider)
{
_connectionStringProvider = connectionStringProvider;
}
public DbSet<ProductProducts { get; set; }
public override int SaveChanges()
{
ChangeTracker.DetectChanges();
var modifiedEntries = ChangeTracker.Entries()
.Where(x =x.State == EntityState.Added || x.State == EntityState.Modified).ToList();
return base.SaveChanges();
}
}
Unit of work:
public class UnitOfWork : IUnitOfWork
{
public UnitOfWork(DbContext dbContext)
{
DbContext = dbContext;
}
public DbContext DbContext { get; set; }
public int Commit()
{
return DbContext.SaveChanges();
}
public async Task<intCommitAsync()
{
return await DbContext.SaveChangesAsync();
}
}
Repository:
public class Repository<TEntity: IDisposable, IRepository<TEntity>
where TEntity : class
{
private readonly UnitOfWork _unitOfWork;
public Repository(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork as UnitOfWork;
}
public void Dispose()
{
_unitOfWork.DbContext.Dispose();
}
public void Create(TEntity entity)
{
_unitOfWork.DbContext.Entry(entity).State = EntityState.Added;
}
}
I have injected all with autofac:
public static class Container
{
public static ContainerBuilder RegisterInfraestructure(this ContainerBuilder containerBuilder)
{
containerBuilder.RegisterType<UnitOfWork>().As<IUnitOfWork>();
return containerBuilder;
}
}
public static class Container
{
public static ContainerBuilder RegisterDataResources(this ContainerBuilder containerBuilder)
{
var configurationBuilder = new ConfigurationBuilder()
.AddJsonFile("appsettings.json");
var configuration = configurationBuilder.Build();
containerBuilder.Register(x =new ConnectionStringProvider(configuration.GetConnectionString("Fgcm.Dale"))
).As<IConnectionStringProvider>();
containerBuilder.RegisterType<DaleContext>().As<DbContext>().As<IDaleContext>();
return containerBuilder;
}
}
public static class Container
{
public static ContainerBuilder RegisterRepository(this ContainerBuilder containerBuilder)
{
containerBuilder.RegisterType<CustomerRepository>().As<ICustomerRepository>();
containerBuilder.RegisterType<ProductRepository>().As<IProductRepository>();
containerBuilder.RegisterType<SaleDetailRepository>().As<ISaleDetailRepository>();
containerBuilder.RegisterType<SaleRepository>().As<ISaleRepository>();
containerBuilder.RegisterDataResources();
containerBuilder.RegisterInfraestructure();
return containerBuilder;
}
}
public static class Container
{
public static ContainerBuilder RegisterApplicationServiceResources(this ContainerBuilder
containerBuilder)
{
containerBuilder.RegisterRepository();
containerBuilder.RegisterType<DaleApplicationService>().As<IDaleApplicationService>();
return containerBuilder;
}
}
When I try to save data it doesn't works (doesn't insert data) ... I would like to know why ? Here are when I try to save:
public Product Create(Product product)
{
try
{
_productRepository.Create(product);
_unitOfWork.Commit();
return product;
}
catch (Exception e)
{
Debug.WriteLine(e);
return null;
}
}
And of course all are injected:
private readonly ICustomerRepository _customerRepository;
private readonly IProductRepository _productRepository;
private readonly ISaleDetailRepository _saleDetailRepository;
private readonly ISaleRepository _saleRepository;
private readonly IUnitOfWork _unitOfWork;
public DaleApplicationService(IProductRepository productRepository, ICustomerRepository customerRepository,
ISaleRepository saleRepository, ISaleDetailRepository saleDetailRepository, IUnitOfWork unitOfWork)
{
_productRepository = productRepository;
_customerRepository = customerRepository;
_saleRepository = saleRepository;
_saleDetailRepository = saleDetailRepository;
_unitOfWork = unitOfWork;
}
What am I missing?
PS: all this works with .NET Core Web Api.
As #zolty13 sort of hinted the Instance scope of your DbContext (DaleContext) is probably incorrect. By default Autofac sets the instance scope to Instance per dependency (also known as a "transient" lifetime) which means a new instance of DaleContext is created for every class that depend on it. So your UnitOfWork receives a different instance of DaleContext than IProductRepository. So changes in IProductRepository are not reflected in UnitOfWork.
One way to solve this is to avoid this convoluted wrapping of your DbContext like #Igor suggest. Do you really need this UnitOfWork? Instead, use a repository class that has one instance of DaleContext and make all the DB changes in there and save them.
Alternatively (if you really think you need a UnitOfWork) you can register your DaleContext with an instance per request scope. Do note: Entity Framework's DbContext is not thread safe, so if you need to do concurrent work, this is not a safe approach.
Otherwise, read up on Instance scope.

C# Dependency Injection : Injecting multiple interfaces into other services

I'd like to inject a number of interfaces to another service.
Let's take a look at 2 services that I want to have their dependency injected.
Inside Term.cs
private readonly IWSConfig WSConfig;
private readonly IMemoryCache MemCache;
public Term(IWSConfig wsConfig, IMemoryCache memoryCache)
{
WSConfig = wsConfig;
MemCache = memoryCache;
}
public async Task LoadData()
{
List<ConfigTerm> configTerm = await WSConfig.GetData(); // This is a web service call
...
}
Inside Person.cs
private readonly PersonRepo PersonRepository;
private readonly IMemoryCache MemCache;
private readonly ITerm Term;
private readonly IWSLoadLeave LoadLeave;
private readonly IWSLoadPartics LoadPartics;
public Person(PersonRepo personRepository, IMemoryCache memCache, ITerm term, IWSLoadLeave loadLeave, IWSLoadPartics loadPartics)
{
PersonRepository = personRepository;
MemCache = memCache;
Term = term;
LoadLeave = loadLeave;
LoadPartics = loadPartics;
}
Code in Startup.cs
services.AddDbContext<DBContext>(opts => opts.UseOracle(RegistryReader.GetRegistryValue(RegHive.HKEY_LOCAL_MACHINE, Configuration["AppSettings:RegPath"], "DB.ConnectionString", RegWindowsBit.Win64)));
services.AddTransient<ILogging<ServiceLog>, ServiceLogRepo>();
services.AddSingleton<IMemoryCache, MemoryCache>();
services.AddHttpClient<IWSConfig, WSConfig>();
services.AddHttpClient<IWSLoadLeave, WSLoadLeave>();
services.AddHttpClient<IWSLoadPartics, WSLoadPartics>();
var optionsBuilder = new DbContextOptionsBuilder<DBContext>(); // Can we omit this one and just use the one in AddDbContext?
optionsBuilder.UseOracle(RegistryReader.GetRegistryValue(RegHive.HKEY_LOCAL_MACHINE, Configuration["AppSettings:RegPath"], "DB.ConnectionString", RegWindowsBit.Win64));
services.AddSingleton<ITerm, Term>((ctx) => {
WSConfig wsConfig = new WSConfig(new System.Net.Http.HttpClient(), new ServiceLogRepo(new DBContext(optionsBuilder.Options))); // Can we change this to the IWSConfig and the ILogging<ServiceLog>
IMemoryCache memoryCache = ctx.GetService<IMemoryCache>();
return new Term(wsConfig, memoryCache);
});
services.AddSingleton<IPerson, Person>((ctx) => {
PersonRepo personRepo = new PersonRepo(new DBContext(optionsBuilder.Options)); // Can we change this?
IMemoryCache memoryCache = ctx.GetService<IMemoryCache>();
ITerm term = ctx.GetService<ITerm>();
WSLoadLeave loadLeave = new WSLoadLeave(new System.Net.Http.HttpClient(), new ServiceLogRepo(new DBContext(optionsBuilder.Options))); // Can we change this?
WSLoadPartics loadPartics = new WSLoadPartics(new System.Net.Http.HttpClient(), new ServiceLogRepo(new DBContext(optionsBuilder.Options))); // Can we change this?
return new Person(personRepo, memoryCache, term, loadLeave, loadPartics);
});
But there are some duplication here and there. I've marked as the comments in the code above.
How to correct it ?
[UPDATE 1]:
If I change the declaration from singleton with the following:
services.AddScoped<ITerm, Term>();
services.AddScoped<IPerson, Person>();
I'm getting the following error when trying to insert a record using the DbContext.
{System.ObjectDisposedException: Cannot access a disposed object. A
common cause of this error is disposing a context that was resolved
from dependency injection and then later trying to use the same
context instance elsewhere in your application. This may occur if you
are calling Dispose() on the context, or wrapping the context in a
using statement. If you are using dependency injection, you should let
the dependency injection container take care of disposing context
instances. Object name: 'DBContext'.
In my WSConfig, it will inherit a base class. This base class also have reference to the ServiceLogRepo, which will call the DbContext to insert a record to the database
In WSConfig
public class WSConfig : WSBase, IWSConfig
{
private HttpClient WSHttpClient;
public WSConfig(HttpClient httpClient, ILogging<ServiceLog> serviceLog) : base(serviceLog)
{
WSHttpClient = httpClient;
//...
}
//...
}
The WSBase class:
public class WSBase : WSCall
{
private readonly ILogging<ServiceLog> ServiceLog;
public WSBase(ILogging<ServiceLog> serviceLog) : base(serviceLog)
{
}
...
}
The WSCall class:
public class WSCall
{
private readonly ILogging<ServiceLog> ServiceLog;
public WSCall(ILogging<ServiceLog> serviceLog)
{
ServiceLog = serviceLog;
}
....
}
And the ServiceLogRepo code
public class ServiceLogRepo : ILogging<ServiceLog>
{
private readonly DBContext _context;
public ServiceLogRepo(DBContext context)
{
_context = context;
}
public async Task<bool> LogRequest(ServiceLog apiLogItem)
{
await _context.ServiceLogs.AddAsync(apiLogItem);
int i = await _context.SaveChangesAsync();
return await Task.Run(() => true);
}
}
I also have the following in Startup.cs to do the web service call upon application load.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, ITerm term)
{
....
System.Threading.Tasks.Task.Run(async () => await term.LoadData());
}
It seems when going into term.LoadData(), the DBContext is disposed already.
First properly register all the necessary dependencies in ConfigureServices using the appropriate liftetime scopes
services.AddDbContext<DBContext>(opts => opts.UseOracle(RegistryReader.GetRegistryValue(RegHive.HKEY_LOCAL_MACHINE, Configuration["AppSettings:RegPath"], "DB.ConnectionString", RegWindowsBit.Win64)));
services.AddTransient<ILogging<ServiceLog>, ServiceLogRepo>();
services.AddSingleton<IMemoryCache, MemoryCache>();
services.AddHttpClient<IWSConfig, WSConfig>();
services.AddHttpClient<IWSLoadLeave, WSLoadLeave>();
services.AddHttpClient<IWSLoadPartics, WSLoadPartics>();
services.AddScoped<ITerm, Term>();
services.AddScoped<IPerson, Person>();
Given the async nature of the method being called in Configure the DbContext is being disposed before you are done with it.
Now ideally given what you are trying to achieve you should be using a background service IHostedServive which will be started upon startup of the application.
public class TermHostedService : BackgroundService {
private readonly ILogger<TermHostedService> _logger;
public TermHostedService(IServiceProvider services,
ILogger<ConsumeScopedServiceHostedService> logger) {
Services = services;
_logger = logger;
}
public IServiceProvider Services { get; }
protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
_logger.LogInformation("Term Hosted Service running.");
using (var scope = Services.CreateScope()) {
var term = scope.ServiceProvider.GetRequiredService<ITerm>();
await term.LoadData();
_logger.LogInformation("Data Loaded.");
}
}
public override async Task StopAsync(CancellationToken stoppingToken) {
_logger.LogInformation("Term Hosted Service is stopping.");
await Task.CompletedTask;
}
}
when registered at startup
services.AddHostedService<TermHostedService>();
Reference Background tasks with hosted services in ASP.NET Core

Autofac DI does not work as expected in Console Application

I have a console application that Autofac DI is used to inject data and service layer from web application project.
here is the setup on console application:
public static class ContainerConfig
{
public static IContainer Configure()
{
var builder = new ContainerBuilder();
builder.RegisterType<DbFactory>().As<IDbFactory>();
builder.RegisterType<UnitOfWork>().As<IUnitOfWork>();
builder.RegisterType<Application>().As<IApplication>();
builder.RegisterType<DataRepository>().As<IDataRepository>();
builder.RegisterType<DataService>().As<IDataService>();
return builder.Build();
}
}
public interface IApplication
{
void Run();
}
public class Application : IApplication
{
private readonly IDataService _dataService;
public Application(IDataService dataService)
{
_dataService = dataService;
}
public void Run()
{
var data = _dataService.GetDataById(1);
var task = new TestTask("test");
data.AddTask(task);
_dataService.Update(data);
_dataService.SaveChanges();
}
}
main Program class:
class Program
{
static void Main(string[] args)
{
var container = ContainerConfig.Configure();
using (var scope = container.BeginLifetimeScope())
{
var app = scope.Resolve<IApplication>();
app.Run();
}
}
}
When the application is run loading the data works fine. However, saving a new entry does not seem to do the work.
However, when I remove DI and use simple class initializing in the Run method as below the save works fine:
IDbFactory dbFactory = new DbFactory();
IDataRepository dataRepository = new DataRepository(dbFactory);
var unitOfWork = new UnitOfWork(dbFactory);
IDataService service = new DataService(dataRepository, unitOfWork);
var data = service.GetDataById(1);
var task = new TestTask("test");
data.AddTask(task);
service.Update(data);
service.SaveChanges();
Am I missing something while I setup the autofac? It seems to access the data fine but when it comes to save it does not save the data. I debugged to see any issue but the program runs fine with no error. How can I debug this sort of issues to find more details?
Updated
public interface IDataService
{
void Add(TestTask task);
void SaveChanges();
}
public class DataService : IDataService
{
private readonly IDataRepository _dataRepository;
private readonly IUnitOfWork _unitOfWork;
public DataService(IDataRepository dataRepository, IUnitOfWork unitOfWork)
{
_dataRepository = dataRepository;
_unitOfWork = unitOfWork;
}
public void Add(TestTask task)
{
_dataRepository.Add(task);
}
public void SaveChanges()
{
_unitOfWork.Commit();
}
}
public class UnitOfWork : IUnitOfWork
{
private readonly IDbFactory _dbFactory;
private ApplicationDbContext _dbContext;
public UnitOfWork(IDbFactory dbFactory)
{
this._dbFactory = dbFactory;
}
public ApplicationDbContext DbContext => _dbContext ?? (_dbContext = _dbFactory.Init());
public void Commit()
{
DbContext.Commit();
}
}
After reading autofac scopes here
I found out that default scope is Instance Per Dependency. Which means that a unique instance will be returned from each request for a service. DbFactory should be for InstancePerLifetimeScope.
So changing configuration below fixes the issue:
public static class ContainerConfig
{
public static IContainer Configure()
{
var builder = new ContainerBuilder();
builder.RegisterType<DbFactory>().As<IDbFactory>().InstancePerLifetimeScope();
builder.RegisterType<UnitOfWork>().As<IUnitOfWork>();
builder.RegisterType<Application>().As<IApplication>();
builder.RegisterType<DataRepository>().As<IDataRepository>();
builder.RegisterType<DataService>().As<IDataService>();
return builder.Build();
}
}

Categories