Getting InvalidOperationException when trying to inject db context into custom middleware - c#

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();
}

Related

Why is middleware not being registered?

I have this middleware which I have some issues with being registered in my yarp reverse proxy.
according to the documentation it should be added like this?
program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddReverseProxy().LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
var app = builder.Build();
app.MapReverseProxy(proxyPipeline =>
{
proxyPipeline.UseRequestResponseLogging();
});
app.UseHttpsRedirection();
app.Run();
requestresponseloggingmiddleware.cs
public partial class RequestResponseLoggerMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger _logger;
private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager;
public RequestResponseLoggerMiddleware(RequestDelegate next,
ILoggerFactory loggerFactory)
{
_next = next;
_logger = loggerFactory
.CreateLogger<RequestResponseLoggerMiddleware>();
_recyclableMemoryStreamManager = new RecyclableMemoryStreamManager();
}
[LoggerMessage(0, LogLevel.Information, "Received request - requestUrl={requestUrl}")]
partial void LogRequest(string requestUrl);
public async Task Invoke(HttpContext context)
{
LogRequest(context.Request.Path);
//code dealing with the request
await _next(context);
//code dealing with the response
LogRequest(context.Request.Path);
}
private async Task LogResponse(HttpContext context)
{
console.log("i was called");
}
RequestResponseLoggerMiddlewareExtensions.cs
public static class RequestResponseLoggerMiddlewareExtensions
{
public static IApplicationBuilder UseRequestResponseLogging(this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestResponseLoggerMiddleware>();
}
}
but when I run it i see nothing in there?

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;
}
}

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.

Middleware DI Error

I am attempting to implement the API Key Validator mentioned in this post. I am running into an issue where the injected service I am using to do validation in the middleware class is returning:
InvalidOperationException: Cannot resolve 'FoosballKeepr.Services.Interfaces.ILeagueService' from root provider because it requires scoped service 'FoosballKeepr.Data.FoosballKeeprContext'.
I believe I am registering my dbContext, services, and repositories correctly in Startup.cs.
public class Startup
{
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)
{
//MVC
services.AddMvc();
//Database
var connection = #"Server=localhost\SQLEXPRESS;Database=FoosballKeepr;Trusted_Connection=True;";
services.AddDbContext<FoosballKeeprContext>(options => options.UseSqlServer(connection));
//Services
services.AddTransient<IPlayerService, PlayerService>();
services.AddTransient<ILeagueService, LeagueService>();
//Repositories
services.AddTransient<IPlayerRepository, PlayerRepository>();
services.AddTransient<ILeagueRepository, LeagueRepository>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseMiddleware<ApiKeyValidatorMiddleware>();
app.UseMvc();
}
}
Custom middleware validator:
public class ApiKeyValidatorMiddleware
{
private readonly RequestDelegate _next;
private ILeagueService _leagueService;
public ApiKeyValidatorMiddleware(RequestDelegate next, ILeagueService leagueService)
{
_next = next;
_leagueService = leagueService;
}
public async Task Invoke(HttpContext context)
{
if (!context.Request.Headers.Keys.Contains("x-api-key"))
{
context.Response.StatusCode = 400;
await context.Response.WriteAsync("API Key Missing.");
return;
}
else
{
int leagueId = _leagueService.ValidateApiKey(context.Request.Headers["x-api-key"]);
if (leagueId == 0)
{
context.Response.StatusCode = 401;
await context.Response.WriteAsync("Invalid API Key");
return;
}
else
{
context.Items["LeagueId"] = leagueId;
}
}
await _next.Invoke(context);
}
}
Service
public class LeagueService : ILeagueService
{
private readonly ILeagueRepository _leagueRepository;
public LeagueService(ILeagueRepository leagueRepository)
{
_leagueRepository = leagueRepository;
}
public int ValidateApiKey(string apiKey)
{
return _leagueRepository.ValidateApiKey(apiKey);
}
}
Repository
public class LeagueRepository : ILeagueRepository
{
private readonly FoosballKeeprContext _context;
public LeagueRepository(FoosballKeeprContext context)
{
_context = context;
}
public int ValidateApiKey(string apiKey)
{
var query = from l in _context.League
where l.ApiKey == apiKey
select l.LeagueId;
return query.FirstOrDefault();
}
}
This is my first time implementing custom middleware functionality so I feel like my issue is not correctly setting something up in the correct context, but nothing is popping up as obvious. Does this look familiar to anyone??
The problem is that middlewares don't have a scope, given that:
Middleware is constructed once per application lifetime
So, when you need to inject scoped services, you do it at the Invoke operation (what's known as method injection):
public async Task Invoke(HttpContext context, ILeagueService service)
{
//...
}

How to inject ILogger into EFCore DbContext

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));
}

Categories

Resources