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.
Related
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
I'm new with .Net Core (using 3.1) and using Dependency injection. I was able to setup NLog in the Web API Controller but now I'm trying to get NLog to work in a separate business class following the basics of what I did in the API Controller. I keep get errors about the logger being NULL and when I put a break point on the _logger and _config, sure enough they are NULL. I'm not sure what I'm missing here.
This is my Business class and I thought I had it setup correctly but obviously not.
public class ShiftBLL
{
private static IConfiguration _config;
private static ILogger<ShiftBLL> _logger;
public ShiftBLL(ILogger<ShiftBLL> logger, IConfiguration config)
{
_config = config;
_logger = logger;
}
public static List<AppsShift> GetShifts(string station, string shiftDate)
{
_logger.LogInformation("Staion: {0} | ShiftDate: {1}", station, shiftDate);
*code removed for clarity. The app breaks on the initial call of _logger.
}
}
FIX
I removed the "static" from the ShiftBLL class as well as from the local parameters. Then I had to create an object of ShiftBLL in my Controller passing in the logger and config from the controller where I have DI working into the ShiftBLL. I ended up with this in my Controller:
ShiftBLL BLL = new ShiftBLL(_logger, _config);
listShifts = BLL.GetShifts(station, shiftDate);
Here is my updated ShiftBLL:
public class ShiftBLL
{
private IConfiguration _config;
private readonly ILogger _logger;
public ShiftBLL(ILogger logger, IConfiguration config)
{
_config = config;
_logger = logger;
}
public List<AppsShift> GetShifts(string station, string shiftDate)
{
_logger.LogInformation("Staion: {0} | ShiftDate: {1}", station, shiftDate);
}
Still getting my head wrapped around Dependency Injection.
When using DI, the DI container will insert the parameters for all dependencies when needed.
I will try to explain with a simplified example:
public class MyClass
{
public MyClass(ShiftBLL shiftBll)
{ .. }
}
When resolving MyClass, the DI container will do:
Find all needed dependencies for creating an instance of MyClass - in this case: ShiftBLL
Find all needed dependencies for creating ShiftBLL, in this ILogger and IConfiguration
Find the registrations for ILogger and IConfiguration (as those are interfaces), and dependencies etc.
Create a instance of ILogger and IConfiguration (via the registrations), create instance of ShiftBLL by injecting the instances into the constructor
Create a instance of MyClass and inject in instance of ShiftBLL.
So with this chain, all your constructor dependencies are created.
But if you're are using static methods in ShiftBLL, you are not sure if the constructor of ShiftBLL (with the ILogger) is ever invoked - so the ILogger field could be null. .
So to fix this case you could do:
Make the method non-static, or
Send all dependencies (ILogger) to the static method as parameter
You mixed up static methods calls and Dependency Injection. I would say to avoid the use of static methods whenever possible, anyway when you are using static methods you cannot access dependencies that are injected through dependency injection. For instance you cannot access _logger from GetShifts because you don't have an instance of ShiftBll and the fact that you call it statically means that its constructor is never called.
This is a working example of how to design classes and their dependencies and how to register them.
public class AController : Controller {
private readonly ILogger<AController> _logger;
private readonly ShiftBll _shiftBll;
public AController(ILogger<AController> logger, ShiftBll shiftBll) {
_logger = logger;
_shiftBll = shiftBll;
//I'm pretty sure you didn't inject the dependency here
//you preferred to use a static reference, but it is not correct in this case
}
public ActionResult AnAction() {
var shifts = _shiftBll.GetShifts("aStation", "aShiftDate");
}
}
public class ShiftBLL
{
private readonly IConfiguration _config; //not static
private readonly ILogger<ShiftBLL> _logger; //not static
public ShiftBLL(ILogger<ShiftBLL> logger, IConfiguration config)
{
_config = config;
_logger = logger;
}
public List<AppsShift> GetShifts(string station, string shiftDate) //not static
{
_logger.LogInformation("Staion: {0} | ShiftDate: {1}", station, shiftDate);
*code removed for clarity. The app breaks on the initial call of _logger.
}
}
.NET Core DI automatically registers controllers, but you have to manually register your own services. To do this go in the ConfigureServices method of your Startup.cs and add
services.AddScoped<ShiftBll>();
now you can use ShiftBll from AController.
I have this piece of code in the startup.cs file of my ASP.Net Core Web API project:
public void ConfigureServices(IServiceCollection services)
{
// Retrieve the database connection string
string connectionString = "Do something to retrieve a connection string";
services.AddDbContext<MyContext>(options => options.UseSqlServer(connectionString));
}
I have reasons to do this here and not in MyContext's OnConfiguring() method, but now I am getting this error:
System.InvalidOperationException: 'No database provider has been
configured for this DbContext. A provider can be configured by
overriding the DbContext.OnConfiguring method or by using AddDbContext
on the application service provider. If AddDbContext is used, then
also ensure that your DbContext type accepts a
DbContextOptions object in its constructor and passes it to
the base constructor for DbContext.'
This is my MyContext class's:
public partial class MyContext : DbContext
{
public MyContext()
{
}
public MyContext(DbContextOptions<MyContext> options)
: base(options)
{
}
...
}
I found somewhere that I also need to do this:
public class MyController : Controller
{
private MyContext _context;
public MyController(MyContext context)
{
_context = context;
}
}
but this is very inconvenient since I am currently not instantiating MyContext in the controllers, but in a different layer, for example:
public ActionResult MyMethod(...)
{
MyManager.DoSomething(); // MyManager instantiates the context
return Ok();
}
This is how I'm currently instantiating the context:
private static readonly MyContext myContext = new MyContext();
I'm guessing I need to somehow inject the options into the context, but I don't know how.
Dependency injection works that way, by injecting via constructor (recommended approach which leads to easier to test code and ensure invariants). You should just use that.
but this is very inconvenient since I am currently not instantiating MyContext in the controllers, but in a different layer, for example:
This seems to be a wrong assumption on your side that injection only works in constructor.
Any service registered with the DI can have stuff injected into it, when resolved. So if you use your MyContext in a service class, inject it there and inject the service into your controller.
Mind the lifetimes though. AddDbContext adds the context with scoped life time, means it will get disposed at the end of the request. This is by design (can be override with one of the AddDbContext overloads), since EF Core is tracking changes in memory and unless disposed can lead to memory leakage and high memory consumption.
It seems that you are registering your context to services collection and it should work if your get your context from there.
But you're simply creating a new unconfigured DbContext instance and so you get an error that it's not configued:
private static readonly MyContext myContext = new MyContext();
Solution: Let the context be injected via DI: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-3.1
public class MyServiceThatNeedsDbContext {
private readonly MyContext _myContext;
MyServiceThatNeedsDbContext(MyContext myContext) {
_myContext = myContext;
}
}
You'll have to register your service class to services collection and get instances from there to make it work.
A simple registration could look like this (Startup.cs):
services.AddTransient<IMyServiceThatNeedsDbContext, MyServiceThatNeedsDbContext>();
.. and in your controller
public class MyController : Controller
{
private IMyServiceThatNeedsDbContext _myService;
public MyController(IMyServiceThatNeedsDbContext myService)
{
_myService = myService;
}
}
I would suggest removing parametherless constructor because there is known issue where that's called instead of one with options in it.
Add in yourDbContext
// overload constructer
public MyContext(DbConfig dbconfig)
{
_dbconfig = dbconfig
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
switch (_dbConfig.DbType)
{
case DbTypes.MsSql:
optionsBuilder.UseSqlServer(_dbConfig.ConnectionString);
break;
case DbTypes.PostgreSql:
optionsBuilder.UseNpgsql(_dbConfig.ConnectionString);
break;
case DbTypes....
.....
}
optionsBuilder.EnableSensitiveDataLogging();
}
}
and Create
public class DbConfig
{
public DbTypes DbType { get; set; }
public string ConnectionString { get; set; }
}
public enum DbTypes
{
MsSql,
PostgreSql,
.....,
....
}
then create global _dbConfig in MyManager and configure dbtype and constring
and then
private yourDbContext GetNewDbContext()
{
return new yourDbContext(_dbConfig);
}
inject in MyManager
Try Unity containers to access your instance when your controller is called.
https://www.tutorialsteacher.com/ioc/unity-container
The context will be provided automatically through dependency injection after registering from your UnityConfig. (The nuget puts in the code.)
public static void Register(HttpConfiguration httpConfiguration)
{
httpConfiguration.DependencyResolver = new UnityDependencyResolver(Container);
}
Using Asp.Net Core we can make use of Dependency Injection in controllers/repositories.
However, I wish do do some logging in my Entity Class.
class Person
{
private ILogger<Person> _logger;
private List<Pets> pets;
public Person(ILogger<Person> logger)
{
_logger = logger;
}
public bool HasCat()
{
_logger.LogTrace("Checking to see if person has a cat.");
// logic to determine cat ownership
hasCat = true;
return hasCat;
}
}
When the Person class is instantiated by EntityFramework it does not attempt to inject any dependencies.
Can I force this? Am i going about it in completely the wrong way?
Ultimatley I just want to be able to use logging consistently throughout the application.
Thanks,
It is possible but I don't recommend it because I agree with commenters that logging belongs in your services and controllers.
EF Core 2.1 allows injecting the DbContext into a private constructor that EF will invoke. See the official docs.
First you need to expose a LoggerFactory property in your DbContext class.
public class MyDbContext : DbContext
{
public MyDbContext(DbContextOptions<MyDbContext> options, ILoggerFactory loggerFactory = null)
{
LoggerFactory = loggerFactory;
}
public ILoggerFactory LoggerFactory { get; }
}
Then you can inject the DbContext into a private constructor in your entity class.
public class Person
{
private readonly ILogger _logger;
public Person() { } // normal public constructor
private Person(MyDbContext db) // private constructor that EF will invoke
{
_logger = db.LoggerFactory?.CreateLogger<Person>();
}
public bool HasCat()
{
_logger?.LogTrace("Check has cat");
return true;
}
}
Something that used to be relatively easy, isn't so anymore. Of the dozens of times I've searched, I rarely find an answer to this situation, which I cannot believe is anything other than prevalent in most project structures.
I have the standard Core 2.0 Web app, and for now, for simplicity sake, an Infrastructure project and a unit test project. I have a good idea how to accomplish the test scenario since the test project doesn't run asp.net and I have a great video tutorial on how to accomplish it.
The problem lies with getting access to the DbContext in my infrastructure project. (.Net Core class library)
The DbContext sets up perfectly fine in Startup
var connString = Configuration.GetSection("ApplicationConfiguration:ConnectionStrings:DefaultConnection").Value;
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connString));
And in a controller I can access it
private ApplicationDbContext _context;
private IConfiguration Configuration { get; set; }
public HomeController(IConfiguration configuration, ApplicationDbContext context)
{
_context = context;
Configuration = configuration;
}
public IActionResult Index()
{
// gets users from the DI injected context in the controller
var users = _context.AppUsers.ToList();
// if GetUsers is defined statically, this doesn't work because the injected context is always null
//var diUsers = DatabaseService.GetUsers():
// making it non-static and newing it up works, but defeats the purpose because you are passing the context, asp.net is not injecting it
var ds = new DatabaseService(_context);
var diUsers = ds.GetUsers();
var svm = SettingsViewModel();
return View(svm);
}
DatabaseService
private ApplicationDbContext _context;
//this is the constructor for DatabaseService class
public DatabaseService(ApplicationDbContext context)
{
_context = context;
}
public List<ApplicationUser> GetUsers()
{
var users = _context.AppUsers.ToList();
return users;
}
Yes, I know I should be using a repository and I will once I get this figured out correctly. How do I set up my classes in the Infrastructure project so I have the injected DbContext created at Startup and not have to pass it as a parameter.
Addendum:
Using the answer provided by Nkosi, I can inject the data service in the controller and use it.
But if I have a separate Infrastructure project (Asp.net core 2 class library), which implements my repository and UoW
public class GenericRepository<T> : IRepository<T> where T : class
{
public GenericRepository()
{
}
//rest of code removed
}
How can I get the DbContext injected there? Do I need to create an Interface, IDbContext, to wrap DbContext, and register that in startup?
Assuming you have the following for your service
public interface IDatabaseService {
List<ApplicationUser> GetUsers();
//...
}
public class DatabaseService : IDatabaseService {
public DatabaseService(ApplicationDbContext context) {
//...code removed for brevity
}
//...code removed for brevity
}
The service explicitly depends on the ApplicationDbContext which will be injected when the implementation is being resolved from the service container.
Register the service in Startup.ConfigureServices
var connString = Configuration.GetSection("ApplicationConfiguration:ConnectionStrings:DefaultConnection").Value;
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connString));
services.AddScoped<IDatabaseService, DatabaseService>();
And refactor the controller to explicitly depend on the service
private IDatabaseService ds;
private IConfiguration Configuration { get; set; }
public HomeController(IConfiguration configuration, IDatabaseService ds) {
this.ds = ds;
Configuration = configuration;
}
public IActionResult Index() {
var diUsers = ds.GetUsers();
var svm = SettingsViewModel();
return View(svm);
}