ASP.NET: ILoggerProvider Singleton Service in Conflict with DbContext - c#

I have created a custom logger that logs to database. The issue I am facing is that when I run my migration, there is a conflict between the AppDbContext and MyLoggerProvider service. It seems that the issue is caused by the fact that the MyLoggerProvider is a singleton service, while the AppDbContext service is a scoped service.
How can I fix this issue to be able to run my migration successfully?
Program.cs:
builder.Services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlite(builder.Configuration.GetConnectionString("AppDbConnection"));
});
builder.Services.AddLogging();
builder.Services.AddSingleton<ILoggerProvider, MyLoggerProvider>();
MyLoggerProvider.cs:
public class MyLoggerProvider : ILoggerProvider
{
private readonly AppDbContext dbContext;
private readonly LogLevel minLevel = LogLevel.Information;
public MyLoggerProvider(AppDbContext dbContext)
{
this.dbContext = dbContext;
}
public ILogger CreateLogger(string categoryName)
{
return new MyLogger(minLevel, dbContext);
}
// rest of the code
}
MyLogger.cs:
public class MyLogger : ILogger
{
private readonly AppDbContext dbContext;
private readonly LogLevel minLevel;
public MyLogger(LogLevel minLevel, AppDbContext dbContext)
{
this.minLevel = minLevel;
this.dbContext = dbContext;
}
// rest of the code
}
Update:
I used IServiceScopeFactory to access the DbContext service as shown in the updated code below:
public class MyLoggerProvider : ILoggerProvider
{
private readonly LogLevel minLevel = LogLevel.Information;
private readonly IServiceScopeFactory scopeFactory;
public MyLoggerProvider(IServiceScopeFactory scopeFactory)
{
this.scopeFactory = scopeFactory;
}
public ILogger CreateLogger(string categoryName)
{
using (var scope = scopeFactory.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return new MyLogger(minLevel, dbContext);
}
}
public void Dispose(){}
}
I thought this would work, but it times out when creating the migration.
An error occurred while accessing the Microsoft.Extensions.Hosting services. Continuing without the application service provider. Error: Timed out waiting for the entry point to build the IHost after 00:05:00. This timeout can be modified using the 'DOTNET_HOST_FACTORY_RESOLVER_DEFAULT_TIMEOUT_IN_SECONDS' environment variable.
Unable to create an object of type 'AppDbContext'. For the different patterns supported at design time, see https://go.microsoft.com/fwlink/?linkid=851728

Either modify your logger service to be Scoped, or setup your db context to be transient:
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")),
ServiceLifetime.Transient,
ServiceLifetime.Transient);
Or see more solutions here: Use DbContext in ASP .Net Singleton Injected Class

Related

Cannot consume scoped service 'ApplicationDbContext' from singleton 'Microsoft.Extensions.Hosting.IHostedService'

I'm using .Net Core Background Service to Connect to Kafka and save messages to SQL Server. My Project Structure looks like this:
In the Infrastructure Dependency, I have the following code to register Entity Framework using IConfiguration configuration passed from Worker's Program.cs file i.e. services.AddInfrastructure(configuration);
namespace JS.Svf.BackgroundServices.Infrastructure
{
public static class DependencyInjection
{
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
{
// Add all the dependencies required by Azure Functions.
// From Infrastructure
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(configuration.GetConnectionString("DefaultConnection"),
b => b.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName)));
services.AddTransient<IApplicationDbContext>(provider => provider.GetRequiredService<ApplicationDbContext>());
services.AddTransient<IProductRepository, ProductRepository>();
services.AddTransient<ISupplierRepository, SupplierRepository>();
return services;
}
}
}
I'm getting the following error after running the Background Service:
Cannot consume scoped service 'ApplicationDbContext' from singleton 'Microsoft.Extensions.Hosting.IHostedService'
With reference, I came to know that we need to use IServiceScopeFactory but I'm bit clueless about how to use it with the current structure. Please advice.
The repository uses the ApplicationDbContext. How to use IServiceScopeFactory here?
namespace JS.Svf.Functions.Infrastructure.Persistence
{
public class Repository<TEntity> : IRepository<TEntity> where TEntity : class
{
protected readonly ApplicationDbContext _context;
public Repository(ApplicationDbContext context)
{
_context = context;
}
public void Add(TEntity entity)
{
_context.Set<TEntity>().Add(entity);
_context.SaveChanges();
}
}
}
In your singleton service, the IHostedService, inject an IServiceScopeFactory into it and use that to create a scope and get a new DbContext from it. For example:
public class MyHostedService : IHostedService
{
private readonly IServiceScopeFactory _scopeFactory;
public MyHostedService(IServiceScopeFactory scopeFactory)
{
// Inject the scope factory
_scopeFactory = scopeFactory;
}
private async Task SomeMethodThatNeedsTheContext()
{
// Create a new scope (since DbContext is scoped by default)
using var scope = _scopeFactory.CreateScope();
// Get a Dbcontext from the scope
var context = scope.ServiceProvider
.GetRequiredService<ApplicationDbContext>();
// Run a query on your context
var quadrupeds = await context.Animals
.Where(a => a.Legs == 4)
.ToListAsync();
}
}

How do I resolve "Unable to resolve service for type '' while attempting to activate ''"

When I attempt requests to a .net core 3.1 WebAPI from Postman I am getting error
System.InvalidOperationException: Unable to resolve service for type 'PaymentsAPI.Repository.PaymentService' while attempting to activate 'PaymentsAPI.Controllers.PaymentController'
'
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddCors(c =>
{
c.AddPolicy("AllowOrigin", options => options.AllowAnyOrigin());
});
services.AddDbContext<ApplicationDbContext>(o => o.UseSqlServer(Configuration.GetConnectionString("SqlSvrConn")));
services.AddTransient<IAsyncPaymentsService<PaymentDetail>, PaymentService>();
}
IAsyncPaymentsService.cs
public interface IAsyncPaymentsService<TEntity>
{
Task<IEnumerable<TEntity>> GetAllAsync();
}
PaymentService.cs
public class PaymentService : IAsyncPaymentsService<PaymentDetail>
{
private readonly ApplicationDbContext _dbContext;
public async Task<IEnumerable<PaymentDetail>> GetAllAsync()
{
return await _dbContext.PaymentDetails.ToListAsync();
}
}
PaymentController.cs
[ApiController]
[Route("[controller]")]
public class PaymentController : ControllerBase
{
private readonly ApplicationDbContext _context;
private readonly PaymentService _service;
public PaymentController(ApplicationDbContext context, PaymentService service)
{
_context = context;
_service = service;
}
[HttpGet]
public async Task<ActionResult<IEnumerable<PaymentDetail>>> GetAsync()
{
var items = (await _service.GetAllAsync());
return Ok(items);
}
}
I have tried rearranging the order of services in the container but the error still persists. What am I missing ?
The quick fix would be to change the controller constructor to depend on the abstraction instead of the implementation since the abstraction is what was registered with the container.
//...
private readonly ApplicationDbContext _context;
private readonly IAsyncPaymentsService<PaymentDetail> _service;
public PaymentController(ApplicationDbContext context, IAsyncPaymentsService<PaymentDetail> service)
{
_context = context;
_service = service;
}
//...
However, the generic abstraction could derived to a closed type if so desired
public interface IPaymentService : IAsyncPaymentsService<PaymentDetail> {
}
applied to the implementation
public class PaymentService : IPaymentService {
//...omitted for brevity
}
registered with the container
services.AddTransient<IPaymentService, PaymentService>();
and refactored in the controller
//...
private readonly ApplicationDbContext _context;
private readonly IPaymentService _service;
public PaymentController(ApplicationDbContext context, IPaymentService service)
{
_context = context;
_service = service;
}
//...
The only thing you should have to change to make this work is to accept the interface into your controller instead of the concrete service.
public PaymentController(ApplicationDbContext context, IAsyncPaymentsService<PaymentDetail> service)
{...}
This is recommended over taking the concrete type for various reasons such as testing. If you truly need the concrete type, you'd have to instead change your registration to
services.AddTransient<PaymentService>();
and leave your controller's constructor as is.

How do I use .Net Core Dependency Injection from within a custom Class

Bit of a newbie question. I am having trouble getting access to dependency injected services from within my own custom class in ASP.NET Core 3.1
I can access services fine from within a controller or razor page e.g. I can get hold of configuration and data context information:
public class DetailModel : PageModel
{
private readonly MyDataContext _context;
private readonly IConfiguration _config;
public DetailModel(MyDataContext context, IConfiguration config)
{
_context = context;
_config = config;
}
etc......
}
I now wish to access these from the constructor of a custom class that is not a controller or razor page. e.g. I am using:
public class ErrorHandling
{
private readonly MyDataContext _context;
private readonly IConfiguration _config;
public ErrorHandling(MyDataContext context, IConfiguration config)
{
_context = context;
_config = config;
}
}
The problem is that when I instantiate my class it insists on me passing the service values into the constructor:
var myErrorHandler = new ErrorHandling(`<wants me to pass context and config values here>`)
This defeats the whole point of DI. I think I am missing something fundamental here!
What am I missing?
You can register ErrorHandling as a service too, in Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
// other stuff..
services.AddScoped<ErrorHandling>(); // this should work as long as both 'MyDataContext' and 'IConfiguration' are also registered
}
If you need an instance of ErrorHandling in your page model, you can specify it in the constructor and ASP.NET Core will resolve it for you at runtime.
This way you won't have to new it:
public class DetailModel : PageModel
{
private readonly MyDataContext _context;
private readonly IConfiguration _config;
private readonly ErrorHandling _errorHandling;
public DetailModel(ErrorHandling errorHandling, MyDataContext context, IConfiguration config)
{
_context = context;
_config = config;
_errorHandling = errorHandling;
}
}
This article can be useful: Dependency injection in ASP.NET Core
If you don't want register as a service, you can use ActivatorUtilities.CreateInstance to resolve ErrorHandling.
Instantiate a type with constructor arguments provided directly and/or from an IServiceProvider.
e.g.:
// IServiceProvider serviceProvider = ...;
var errorHandling = ActivatorUtilities.CreateInstance<ErrorHandling>(serviceProvider);
BUT you need to be careful about this solution:
ServiceProvider scope should equal with dependency object (MyDataContext, IConfiguration). Otherwise, you will get an exception like:
var errorHandling = ActivatorUtilities.CreateInstance<ErrorHandling>(app.ApplicationServices);
// An exception of type 'System.InvalidOperationException' occurred
// in Microsoft.Extensions.DependencyInjection.dll but was not handled in user cod
// e: 'Cannot resolve scoped service 'three.MyDataContext' from root provider.'
For this, you can create an scope to resolve ErrorHandling:
using (var scope = app.ApplicationServices.CreateScope())
{
var errorHandling = ActivatorUtilities.CreateInstance<ErrorHandling>(scope.ServiceProvider);
}
Dependency injection service would not call Dispose on IDisposable instances even out of scope.
For this, you should call Dispose() by yourself:
using (var scope = app.ApplicationServices.CreateScope())
{
using var disposablClass = ActivatorUtilities.CreateInstance<DisposablClass>(scope.ServiceProvider);
}
ActivatorUtilities.CreateInstance will new an instance even you use the same ServiceProvider:
using (var scope = app.ApplicationServices.CreateScope())
{
var errorHandling1 = ActivatorUtilities.CreateInstance<ErrorHandling>(scope.ServiceProvider);
Console.WriteLine(errorHandling1.GetHashCode());
// 11903911
var errorHandling2 = ActivatorUtilities.CreateInstance<ErrorHandling>(scope.ServiceProvider);
Console.WriteLine(errorHandling2.GetHashCode());
// 40026340
}

Injecting DbContext and ILogger into database DAL constructor, best practice

I am building a Web API in ASP.NET Core 3.1 with Entity Framework.
My database context is registered as a service in the Startup.cs:
services.AddDbContext<LocalDbContext>(options =>
{
options.UseSqlServer(builderLocal.ConnectionString);
});
And I am retrieving the DbContext in my controller using DI, passing it into my database-access-class (DAL) when I instantiate it for each method
private readonly LocalDbContext _context;
public HomeController(LocalDbContext context)
{
_context = context;
}
public IActionResult GetSomeData(int id)
{
var localDb = new LocalDb(_context);
return Ok(localDb.GetSomeDataById(id));
}
And then my Database file:
public class LocalDbContext : DbContext
{
public LocalDbContext(DbContextOptions<LocalDbContext> options)
: base (options) { }
**DbSets for my models**
}
public class LocalDb
{
private readonly LocalDbContext _context;
private readonly ILogger<LocalDb> _logger;
// I would want to avoid having to pass in logger in this contstructor
public LocalDb(LocalDbContext context)
{
_context = context;
// Can I retrieve a logger from somewhere else? From the context?
// _logger = logger;
}
public void AddStudent(Student student)
{
_context.Student.Add(student);
try
{
_context.SaveChanges();
}
catch (Exception ex)
{
_logger.LogError("logMessage about exception: " + ex.StackTrace);
throw;
}
}
}
So I wish to have the _logger available to write to, but in my code I have not set the _logger to anything. I know ILogger exists in the DbContext, and there is lots of guides explaining how to add and configure the logger for the Context and EF. But when I want to use my LocalDb class, do I have to pass in a ILogger instance for every time i call var localDb = new LocalDb(_context, _logger)
I feel like having to pass in a logger instance every time is a bit strange, there must be a better solution to this. Would it be possible to add the LocalDb class as a service, then dependency inject context and logger into that class? Whats the best practice?
Notice that HomeController doesn't use LocalDbContext directly, but only uses it to pass it on to its real dependency, LocalDb. Therefore, you should not inject LocalDbContext into HomeController's constructor, but instead inject LocalDb directly. This solves your problems elegantly, because now LocalDb can be registered in the DI Container, and it can resolve it for you with any dependency it might have.
Here's an example of a HomeController that depends on LocalDb directly.
public class HomeController : Controller
{
private readonly LocalDb _db;
public HomeController(LocalDb db)
{
_db = db;
}
public IActionResult GetSomeData(int id)
{
return Ok(_db.GetSomeDataById(id));
}
}
Because LocalDb is injected into the constructor, it must be registered in the DI Container:
services.AddTransient<LocalDb>();
services.AddDbContext<LocalDbContext>(options =>
{
options.UseSqlServer(builderLocal.ConnectionString);
});
But since LocalDb is composed by the DI Container, it can be extended with any dependencies, such as the ILogger dependency:
public class LocalDb
{
private readonly LocalDbContext _context;
private readonly ILogger<LocalDb> _logger;
public LocalDb(LocalDbContext context, ILogger<LocalDb> _logger)
{
_context = context;
_logger = logger;
}
...
}
TIP: Prevent sprinkeling catch statements across your code base that log and rethrow. Prefer instead to have some global infrastructure that logs any requests that have failed. If I'm not mistaken ASP.NET Core does this for you out of the box. If not, this can be enabled with just a few lines of code. This makes code (such as your LocalDb.AddStudent much simpler, and limits the number of dependencies that a class has.

Error while validating the service descriptor 'ServiceType: INewsRepository Lifetime: Singleton ImplementationType: NewsRepository':

I try get data from my database with repository Pattern
i have 3 project
Bmu.Mode 'this is for model to create database'
Bmu.Repo 'it have 2 folder for repository include contract/InewsRepository.cs' and 'Repository/NewsRepository' for implement Interface
Bmu.Api for invoke data from Repo project
news class in Model Project
namespace bmu.model
{
public class News
{
public int Id { get; set; }
public string SubTitle { get; set; }
public string Title { get; set; }
public string Summery { get; set; }
}
}
context class in model project
namespace bmu.model
{
public class BmuContext : DbContext
{
public BmuContext(DbContextOptions<BmuContext> options): base(options)
{
}
public DbSet<News> News { get; set; }
}
}
My interface in Repo project
namespace bmu.repo.Contracts
{
public interface INewsRepository
{
Task<IEnumerable<News>> GetAllAsync();
Task<IEnumerable<News>> GetAllActiveAsync();
}
}
implement interface in bmu.repo
namespace bmu.repo.IRepository
{
public class NewsRepository : INewsRepository
{
private readonly BmuContext _context;
private readonly MemoryCache _memoryCache;
public NewsRepository(BmuContext context, MemoryCache memoryCache)
{
_context = context;
_memoryCache = memoryCache;
}
public async Task<IEnumerable<News>> GetAllAsync()
{
return await _context.News.ToListAsync();
}
public async Task<IEnumerable<News>> GetAllActiveAsync()
{
return await _context.News.Where(x => x.Active).ToListAsync();
}
}
}
Also add
services.AddControllers();
services.AddSingleton<INewsRepository, NewsRepository>();
in startup of Api project
and this is my controller
namespace bmu.api.Controllers
{
[ApiController]
[Route("[controller]")]
public class NewsController : ControllerBase
{
private readonly ILogger<NewsController> _logger;
private readonly INewsRepository _newsRepository;
public NewsController(ILogger<NewsController> logger,INewsRepository newsRepository)
{
_logger = logger;
_newsRepository = newsRepository;
}
[HttpGet]
public async Task<IEnumerable<News>> Get()
{
return await _newsRepository.GetAllActiveAsync();
}
}
}
but when run project i got this error
AggregateException: Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: bmu.repo.Contracts.INewsRepository Lifetime: Singleton ImplementationType: bmu.repo.IRepository.NewsRepository': Unable to resolve service for type 'bmu.model.BmuContext' while attempting to activate 'bmu.repo.IRepository.NewsRepository'.)
also because of multi project add DbContext with this
UPDATE:
namespace bmu.model
{
public class BmuContextFactory : IDesignTimeDbContextFactory<BmuContext>
{
public BmuContext CreateDbContext(string[] args)
{
var optionsBuilder = new DbContextOptionsBuilder<BmuContext>();
optionsBuilder.UseSqlite("Data Source=bmu.db");
return new BmuContext(optionsBuilder.Options);
}
}
}
Is there any solution for this error ?
Firstly,you need to change:
services.AddSingleton<INewsRepository, NewsRepository>();
To:
services.AddTransient<INewsRepository, NewsRepository>();
Secondly,you need to inject IMemoryCache instead of MemoryCache in NewsRepository.
Here is a simple demo like below:
1.Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddSession();
services.AddTransient<INewsRepository, NewsRepository>();
services.AddDbContext<BmuContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("Connectionstring")));
services.AddMemoryCache();
}
2.appsettings.json:
"ConnectionStrings": {
"Connectionstring": "Server=(localdb)\\mssqllocaldb;Database=Bmu;Trusted_Connection=True;MultipleActiveResultSets=true"
}
3.NewsRepository:
public class NewsRepository : INewsRepository
{
private readonly BmuContext _context;
private readonly IMemoryCache _memoryCache;
public NewsRepository(BmuContext context, IMemoryCache memoryCache)
{
_context = context;
}
//...
}
My Error was that I was injecting the service class instead of the interface
It was
//This is wrong
Private readonly DataSerive _dataService;
public void EmployeeHandler(DataSerive dataService)
{
_dataService = dataService;
}
But it should be
//This is correct
Private readonly IDataSerive _dataService;
public void EmployeeHandler(IDataSerive dataService)
{
_dataService = dataService;
}
Here DataService is the class that handles operation
and IDataService is the interface
There is a lifetime type mismatch in your API. EntityFramework DbContext is a scoped service, and you cannot have a singleton instance of the NewsRepository, as it depends on an instance that is generated for each request.
You either have to use NewsRepository as a scoped service, or restructure your dependency resolution, like shown in this SO answer: Use DbContext in ASP .Net Singleton Injected Class
Like Sotiris Koukios-Panopoulos -san comment
I see you are only setting up the options for design time, not in your Startup.cs. I expect a:
services.AddDbContext<BmuContext>(options => options.UseSqlite("your connection string"));
instead.
In my case, I forgot to set this in my Startup.cs
services.AddDbContext<myDbContext>(o => o.UseSqlServer(myConnectionString));
and I forgot to mention this, because I'm using interface an service
services.AddScoped<IMyTruckService, MyTruckService>();
I was adding singleton service that was injecting DbContext class.
services.AddSingleton<WeatherForecastService>();
I changed above to below (Added a transient service scope) and it worked.
services.AddTransient<FoodItemService>();
This error can be caused by circular dependency.
Because probably, you inject service1 in service2 and also service2 in service1.
You should change it and break circular dependency.
I was having two dbcontext, and forgotten to mention this in startup.cs file
services.AddDbContext<Abc>(option => option.UseSqlServer(Configuration.GetConnectionString("ConStr")));
It was because of
private readonly IMemoryCache _memoryCache;
when i remove it every think work fine

Categories