How to inject ILogger into EFCore DbContext - c#

I have .net core app with EFCore db context:
public class MyappDbContext : DbContext
{
private ILogger<MyappDbContext> _logger;
public MyappDbContext(DbContextOptions<MyappDbContext> options)
: base(options)
{
// ???
}
}
In startup class I registered context:
services.AddDbContext<MyappDbContext>(options => options.UseSqlServer(connectionString));
How to inject into MyappDbContext, ILogger or ILoggerFactory (to create logger)?

All you need to do is add a ILoggerFactory or ILogger parameter to the context constructor:
public class MyappDbContext : DbContext
{
private readonly ILogger<MyappDbContext> _logger;
public MyappDbContext(DbContextOptions<MyappDbContext> options,
ILogger<MyappDbContext> logger)
: base(options)
{
_logger = logger;
}
}

In case if you need to instantiate the dbcontext manually:
public class Startup
{
public static readonly ILoggerFactory logFactory = LoggerFactory.Create(builder => builder.AddDebug());
....
public Startup(IWebHostEnvironment env)
{
....
}
public void ConfigureServices(IServiceCollection services)
{
....
}
}
public class MyDbContext : DbContext
{
private readonly ILoggerFactory _loggerFactory;
public MyDbContext(DbContextOptions<MyDbContext> options,
ILoggerFactory loggerFactory)
: base(options)
{
_loggerFactory = loggerFactory;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseLoggerFactory(_loggerFactory);
// optionsBuilder.EnableSensitiveDataLogging();
}
}
// Somewhere else
var db = new MyDbContext(new DbContextOptions<MyDbContext>(), Startup.logFactory);
But I would recommend using DI instead:
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<MyDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("Default")).UseLoggerFactory(logFactory));
}

Related

Unable to resolve a type in Integration Testing using WebApplicationFactory

This is the problem I have: InvalidOperationException: Unable to resolve service for type 'WebApi.Test.Repository.TestRepository' while attempting to activate 'WebApi.Test.Services.TestService'.
Of course, the problem is, It seems I cant construct a TestRepository, when the service needs it.
This is the Repo:
public class TestRepository : GenericRepository<TestEntity>
{
public TestRepository(TestContext dbContext
, ILoggerFactory loggerFactory) : base(dbContext, loggerFactory)
{
}
}
The service basically has a TestRepository as a parameter in its constructor (but the issue seems to be before that).
This is my custom WebApplicationFactory
public class ApiWebApplicationFactory : WebApplicationFactory<Startup>
{
public IConfiguration Configuration { get; private set; }
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureAppConfiguration(config =>
{
Configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.Build();
config.AddConfiguration(Configuration);
});
//Add Services
builder.ConfigureTestServices(services =>
{
// Add framework services.
services.AddMvc();
//Add ApplicationDbContext using an in-memory database for testing.
services.AddDbContext<TestContext>(options =>
options.UseInMemoryDatabase("InMemoryDbForTesting"));
services.AddAutoMapper(typeof(Startup));
// Build the service provider.
var serviceProvider = services.BuildServiceProvider();
// Create a scope to obtain a reference to the database
// context (ApplicationDbContext).
using var scope = serviceProvider.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<TestContext>();
// Ensure the database is created.
db.Database.EnsureCreated();
// Seed the database with test data.
InitializeTestDatabase(db);
services.AddScoped<GenericRepository<TestEntity>, TestRepository>();
services.AddScoped<GenericService<TestEntity, TestRequest, TestResponse>, TestService>();
services.AddScoped<GenericController<TestEntity, TestRequest, TestResponse>, TestController>();
});
}
private void InitializeTestDatabase(TestContext textContext)
{
textContext.TestEntities.Add(new TestEntity { Id = 1 });
}
}
All the Generic classes are abstract classes. This is the GenericRepository:
public abstract class GenericRepository<T> : IGenericRepository<T>
where T : BaseEntity
{
protected DbContext _context;
private DbSet<T> _table;
private ILogger _logger;
public GenericRepository(DbContext context
, ILoggerFactory loggerFactory)
{
_context = context;
_table = context.Set<T>();
_logger = loggerFactory.CreateLogger<GenericRepository<T>>();
}
//CRUD Methods
}
I was thinking if it is because Im not creating the ILoggerFactory on my Custom WebAppFactory, if that is the case, how can someone create an ILoggerFactory?.
What I tried:
I tried to add the following line:
services.AddScoped<DbContext, TestContext>();
but I keep getting the same error.
This is the Service:
public abstract class GenericService<T, Request, Response>
where T : BaseEntity
where Response : class, IResponseDto
where Request : class, IRequestDto
{
protected readonly GenericRepository<T> _genericRepository;
protected readonly IMapper _mapper;
protected readonly ILogger _logger;
public GenericService(GenericRepository<T> genericRepository
, IMapper mapper
, ILoggerFactory loggerFactory)
{
_genericRepository = genericRepository;
_mapper = mapper;
_logger = loggerFactory.CreateLogger<GenericService<T, Request, Response>>();
}
//Methods
}

Inject IWebHostingEnvironment into overridden method

Using ASP.NET Core 5.0, I'm attempting to access IWebHostEnvironment in an EF Core 5.0 model class. How can I get access to the check if IsDevelopment is true from the environment?
I'm calling the below class from my Controllers like so:
private MyContext db = new();
Do I really need to also spin up IWebHostEnvironment in each controller that calls this EF class to target the correct constructor?
public partial class MyClass : DbContext
{
private readonly IWebHostEnvironment env;
public MyContext()
{
}
public MyContext(DbContextOptions<MyContext> options)
: base(options)
{
}
public MyContext(IWebHostEnvironment _env)
{
this.env = _env;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if(this.env.IsDevelopment()) { ... } // env is null
}
}
I've seen a few threats about DI, but to be honest, they have convoluted solutions that I can't quite grok.
EDIT with new code:
public IConfiguration Configuration { get; }
public IWebHostEnvironment WebHostEnvironment { get; }
public Startup(IConfiguration configuration, IWebHostEnvironment webHostEnvironment)
{
WebHostEnvironment = webHostEnvironment;
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
DbContextOptionsBuilder<MyContext> builder = new DbContextOptionsBuilder<MyContext>();
builder.UseSqlServer(#"Server=localhost;[...]");
services.AddScoped<MyContext>(_ => new MyContext(builder.Options, WebHostEnvironment));
services.AddDbContext<MyContext>();
EF context:
public partial class MyContext : DbContext
{
private readonly IWebHostEnvironment Env;
public MyContext()
{
}
public MyContext(DbContextOptions<MyContext> options, IWebHostEnvironment env)
: base(options)
{
Env = env;
}
}
Controller:
public class MyController : Controller
{
private MyContext db;
public ActionResult Index(MyContext db)
{
this.db = db; //throws exception below
return View();
}
}
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.
Microsoft.EntityFrameworkCore.Internal.DbContextServices.Initialize(IServiceProvider scopedProvider, IDbContextOptions contextOptions, DbContext context)
Your DbContext
public class MyContext : DbContext
{
private readonly IWebHostEnvironment WebHostEnv;
public MyContext(
DbContextOptions<MyContext> options,
IWebHostEnvironment webHostEnv) : base(options)
{
WebHostEnv = webHostEnv;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if(this.WebHostEnv.IsDevelopment()) { ... } // env is null
}
}
In StartUp add
public IConfiguration Configuration { get; }
public IWebHostEnvironment WebHostEnvironment { get; }
public StartUp(IConfiguration configuration, IWebHostEnvironment webHostEnvironment)
{
Configuration = configuration;
WebHostEnvironment = webHostEnvironment;
}
public void ConfigureServices(IServiceCollection services)
{
// ... some services
DbContextOptionsBuilder<MyContext> optsBuilder = new DbContextOptionsBuilder<MyContext>();
optsBuilder.UseSqlServer(Configuration.GetConnectionString("connstringname"));
services.AddScoped<MyContext>(_ =>
new MyContext(optsBuilder.Options, WebHostEnvironment));
}
Then your controller
public class MyController
{
// MyContext already loaded with IWebHostEnvironment
private readonly MyContext MyLoadedContext;
public MyController(MyContext myLoadedContext)
{
MyLoadedContext = myLoadedContext;
}
}

Getting Connection String in ASP.Net Core MVC data layer

I am using the code below to retrieve the connection string and it works fine. However, the configuration object has to be passed through the layers. Previous versions of .Net would allow me to get the connection string directly in the data layer. So can I still do that (and how do I do that) or do I need to pass the configuration object through the application as I do now?
In startup.cs
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
...
services.AddSingleton(_ => Configuration);
...
}
MyController.cs
public class MyController : Controller
{
protected readonly IConfiguration Configuration;
public MyController(IConfiguration configuration)
{
Configuration = configuration;
}
public IActionResult ListRecords()
{
DatabaseContext ctx = new DatabaseContext(Configuration);
return View();
}
}
DatabaseContext.cs
public class DatabaseContext : DbContext
{
private readonly IConfiguration config;
public DatabaseContext(IConfiguration config)
{
this.config = config;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(config["ConnectionStrings:Dev"]);
}
}
Having to explicitly inject IConfiguration is usually seen as a code smell and indicates design issues.
Take advantage of dependency injection
public class MyController : Controller {
DatabaseContext context;
public MyController(DatabaseContext context) {
this.context = context;
}
public IActionResult ListRecords() {
//...use context here
return View();
}
}
and inject the database options instead
public class DatabaseContext : DbContext {
public DatabaseContext(DbContextOptions<DatabaseContext> options): base(options) {
//...
}
}
Then it is only a matter of configuring the context at startup
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services) {
// ...
services.AddDbContext<DatabaseContext>(options =>
options.UseSqlServer(Congiguration.GetConnectionString("Dev"));
// ...
}
Typically the pattern I've used for setting up DBContext, is to configure at startup.
So if this is startup.cs:
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
var sqlConnString = Configuration.GetConnectionString(dbConnectionStringSettingsName);
services.AddDbContext<DatabaseContext >(opt => opt.UseSqlServer(sqlConnString));
Also, if you pass your context as a service reference, you shouldn't need to give it IConfiguration.
private readonly DatabaseContext _context;
public MyController(DatabaseContext context)
{
_context = context;
}
public IActionResult ListRecords()
{
var dbresults = _context.Table.ToList();
return View(dbresults );
}
use nugget packaeges
Install-Package Microsoft.Extensions.Configuration.Abstractions
Install-Package Microsoft.Extensions.Configuration
and then Inject IConfigurationSection in the web application.
https://github.com/geeksarray/read-appsettings-json-in-net-core-class-library-using-dependency-injection

Access HttpContextAccessor from startup.cs in .net Core WebApi

I'm logging exceptions to database in asp.net core. MyDbContext take HttpContextAccessor parameter.So, I'm sending HttpContextAccessor to MyDbContext.cs for access my JWT. But, I can't access my HttpContextAccessor from Startup.cs. How can I achieve this?
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpContextAccessor();
services.AddMvc();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddDbContext<MyDbContext>();
services.AddTransient<IUnitOfWork, UnitOfWork>();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseExceptionHandler(builder => builder.Run(async context =>
{
var error = context.Features.Get<IExceptionHandlerFeature>();
context.Response.AddApplicationError(error,???????);//I want access HttpContextAccessor
await context.Response.WriteAsync(error.Error.Message);
}));
app.UseHttpsRedirection();
app.UseMvc();
}
ExceptionHelper.cs
public static class ExceptionHelper
{
public static async Task AddApplicationError(this HttpResponse response, IExceptionHandlerFeature error, IHttpContextAccessor httpContextAccessor)
{
Log log = new Log();
log.Message = error.Error.Message;
MyDbContext context = new MyDbContext(null, httpContextAccessor);
UnitOfWork uow = new UnitOfWork(context);
uow.LogRepo.AddOrUpdate(log);
await uow.CompleteAsync(false);
}
}
MyDbContext
public class MyDbContext : DbContext
{
private readonly IHttpContextAccessor _httpContextAccessor;
public MyDbContext(DbContextOptions<MyDbContext> options, IHttpContextAccessor httpContextAccessor)
: base(GetOptions())
{
_httpContextAccessor = httpContextAccessor;
}
private static DbContextOptions GetOptions()
{
return SqlServerDbContextOptionsExtensions.UseSqlServer(new DbContextOptionsBuilder(), "server=asd; database=; user id=asd; password=1234").Options;
}
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken))
{
var token = _httpContextAccessor.HttpContext.Request.Headers["Authorization"];
var audits = AuditHelper.AddAuditLog(base.ChangeTracker, token);
return (await base.SaveChangesAsync(true, cancellationToken));
}
}
You can inject whatever you need into the Configure method. You have already added it to the service collection with this line:
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
So all you need to do is add it to the list of arguments on the method like this:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IHttpContextAccessor accessor)
{
// make use of it here
}
As an aside: I would also point out that it's a bit of a code smell that you are manually creating an instance of your DbContext inside your static helper class when you are using dependency injection.
Update in response to comment
In order to tidy things up a bit I would start by changing your startup to configure you DbContext something like this:
public class Startup
{
private readonly IConfiguration configuration;
public Startup(IConfiguration configuration)
{
this.configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
// register other things here...
services.AddDbContext<DataContext>(o => o.UseSqlServer(
config.GetConnectionString("MyConnectionString") // from appsettings.json
));
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// set up app here...
}
}
You can then remove the .GetOptions() method from MyDbContext, and change the constructor to:
public MyDbContext(DbContextOptions<MyDbContext> options, IHttpContextAccessor httpContextAccessor)
: base(options)
{
_httpContextAccessor = httpContextAccessor;
}
Then you inject an instance of MyDbContext into whatever class needs access to it. The problem is that (to my knowledge) DI does not work well with static classes/methods, and you are using an extension method on the HttpResponse to log your error.
In my opinion it would be better to create a class that is responsible for logging the error with a dependency on your MyDbContext and have that injected into the Configure method:
public class ErrorLogger
{
private MyDataContext db;
public ErrorLogger(MyDataContext db) => this.db = db;
public void LogError(IExceptionHandlerFeature error)
{
Log log = new Log();
log.Message = error.Error.Message;
UnitOfWork uow = new UnitOfWork(this.db);
uow.LogRepo.AddOrUpdate(log);
await uow.CompleteAsync(false);
}
}
Register it with the DI container as you have with other things, then inject it into Configure instead of the HTTP accessor:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ErrorLogger logger)
{
app.UseExceptionHandler(builder => builder.Run(async context =>
{
var error = context.Features.Get<IExceptionHandlerFeature>();
logger.LogError(error);
await context.Response.WriteAsync(error.Error.Message);
}));
}
I have not tested this, and I am not familiar with .UseExceptionHandler(...) as I use application insights to log exceptions etc (take a look at it if you've not seen it). One thing to be aware of is the scope of your dependencies; your DbContext will be Scoped by default (and I think you should leave it that way), which means you cannot inject it into Singleton objects.

Getting InvalidOperationException when trying to inject db context into custom middleware

I need to inject a db context object into a custom middleware called AuthenticateClient, but I get the exception:
InvalidOperationException: Cannot resolve scoped service
'LC.Tools.API.Data.ApiDbContext' from root provider.
AuthenticateClient.cs:
public class AuthenticateClient
{
private readonly RequestDelegate _next;
private readonly ILogger _logger;
private readonly GenericUnitOfWork _worker;
public AuthenticateClient(RequestDelegate next, ApiDbContext db, IHttpContextAccessor httpContext, IHostingEnvironment env, ILoggerFactory loggerFactory, IOptions<Utility.LCLog.Settings> settings)
{
_next = next;
_logger = loggerFactory.CreateLogger(settings.Value.ApplicationName);
_worker = new GenericUnitOfWork(new AppHelper(httpContext, db, env));
}
public async Task Invoke(HttpContext context)
{
if (!context.Request.Headers.Keys.Contains("key") || !context.Request.Headers.Keys.Contains("pass"))
{
context.Response.StatusCode = 400;
await context.Response.WriteAsync("Key or Pass missing from request header values");
return;
}
else
{
Client client;
string key, pass;
key = context.Request.Headers["key"];
pass = context.Request.Headers["pass"];
client = await _worker.GetRepo<Client>().SingleOrDefault(clnt => clnt.Active && clnt.Key.Equals(key) && clnt.Password.Equals(pass));
if (client == null)
{
_logger.LogWarning("Client authentication failed", new string[] { "Key: " + key, "Password: " + pass, "Host: " + context.Request.Host });
context.Response.StatusCode = 401;
await context.Response.WriteAsync("Authentication failed");
return;
}
}
await _next.Invoke(context);
}
}
AuthenticateClientExtension.cs:
public static class AuthenticateClientExtension
{
public static IApplicationBuilder UseClientAuthentication(this IApplicationBuilder builder)
{
return builder.UseMiddleware<AuthenticateClient>();
}
}
Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApiDbContext>(options => options.UseSqlServer(this.ConnectionString));
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddMvc();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IOptions<Settings> lclog)
{
loggerFactory.AddLCLog(lclog.Value);
app.UseClientAuthentication();
app.UseMvc();
}
ApiDbContext.cs:
public class ApiDbContext : DbContext, IApiDbContext
{
public ApiDbContext(DbContextOptions<ApiDbContext> options) : base(options) { }
protected override void OnModelCreating(ModelBuilder builder)
{
...
}
}
The solution was to use IApplicationBuilder to create scope and init
ApiDbContext and pass it to the middleware object. I also changed how I register the ApiDbContext
AuthenticateClientExtension.cs:
public static class AuthenticateClientExtension
{
public static IApplicationBuilder UseClientAuthentication(this IApplicationBuilder builder)
{
var scope = builder.ApplicationServices.CreateScope();
ApiDbContext db = scope.ServiceProvider.GetRequiredService<ApiDbContext>();
return builder.UseMiddleware<AuthenticateClient>(db);
}
}
Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApiDbContext>(options => options.UseSqlServer(this.ConnectionString));
...
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IOptions<Settings> lclog)
{
...
app.UseClientAuthentication();
app.UseMvc();
}

Categories

Resources