My web api follows this structure
/countrycode/mycontroller e.g. /gbr/mycontroller
I am using middleware to get the country code so I can switch connection string:
public class ConnectionMiddleware
{
public ConnectionMiddleware(RequestDelegate next)
{
this.next = next;
}
private readonly RequestDelegate next;
public async Task InvokeAsync(HttpContext httpContext)
{
var values = httpContext.GetRouteData().Values;
await next(httpContext);
}
}
So now I have my country code I want to set the connection string property in my db service.
My db service is already registered in Startup.cs:
services.AddTransient<INpgSqlDataAccess, NpgSqlDataAccess>();
Can the connection string be set via my middleware now and be seen globally (after already being registered) or should I register the service in my middleware?
First you should create a class for manage connection string.
public interface IDatabaseConnectionManager
{
void SetConnectionString(string connectionString);
string GetConnectionString();
}
public class DatabaseConnectionManager
{
private string _connectionString;
public DatabaseConnectionManager()
{
// _connectionString = DefaultConnectionString;
}
public void SetConnectionString(string connectionString)
{
this._connectionString = connectionString;
}
public void GetConnectionString() {
return _connectionString;
}
}
And then if you want to use middleware you can try this:
public class ConnectionMiddleware
{
private readonly RequestDelegate _next;
public async Task InvokeAsync(HttpContext httpContext,IDatabaaseConnectionManager connectionManager)
{
var connectionString = "some-connection-string"; // Create Your Connection String Here
connectionManager.SetConnectionString(connectionString);
await _next.InvokeAsync(httpContext);
}
And in your Startup:
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IDatabaseConnectionManager,DatabaseConnectionManager>();
services.AddScoped<INgpSqlDataAccess>(options=>
{
var connectionManager = options.GetService<IDatabaseConnectionManager>();
var connectionString = connectionManager.GetConnectionString();
// Create Your SqlDataAccess With ConnectionString
return new NgpSqlDataAccess(connectionString);
});
}
Every time you resolve INgpSqlDataAccess you get a new instance with your desired connectionstring. Be carefull that you must register your dependencies with per http request life time (Scoped).
If your DbContext is called AppDbContext, you can modify the connection string in middleware like this:
public async Task Invoke(
HttpContext httpContext,
IConfiguration configuration,
AppDbContext context)
{
context.Database.SetConnectionString(configuration.GetConnectionString("Other"));
await _next(httpContext);
}
In Startup.ConfigureServices:
services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(Configuration.GetConnectionString("Default")));
Related
I have a piece of middleware I'm trying to build that will check if a user has a particular key and if they can receive a authentication token and if so impersonate a windows user. That particular portion we were able to get to run. However I'm having trouble passing IConfiguration to said middleware. Whenever I run the application I get the following error:
A suitable constructor for type LocalDevelopmentImpersonation.LocalDevelopmentImpersonation' could not be located. Ensure the type is concrete and services are registered for all parameters of a public constructor.
namespace LocalDevelopmentImpersonation
{
public sealed class LocalDevelopmentImpersonation : IMiddleware
{
private readonly RequestDelegate _Next;
private readonly SafeAccessTokenHandle _SafeAccessTokenHandle;
private readonly bool _IsLocal;
public LocalDevelopmentImpersonation(RequestDelegate next, IConfiguration configuration)
{
this._SafeAccessTokenHandle = LocalDevelopmentImpersonationSetup.GetSafeAccessTokenHandle(out var receivedSafeAccessTokenSuccessfully);
this._Next = next;
this._IsLocal = LocalDevelopmentImpersonationSetup.IsLocal(receivedSafeAccessTokenSuccessfully, configuration);
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
if (this._IsLocal)
{
await WindowsIdentity.RunImpersonated(this._SafeAccessTokenHandle, async () =>
{
await this._Next.Invoke(context);
});
}
await this._Next.Invoke(context);
}
}
Methods to use in Program.cs on the instance we created of WebApplication.
namespace LocalDevelopmentImpersonation.ExtensionMethods
{
public static class LocalDevelopmentImpersonationExtensions
{
public static IApplicationBuilder UseLocalDevelopmentImpersonation(this IApplicationBuilder app)
{
return app.UseMiddleware<LocalDevelopmentImpersonation>();
}
public static IApplicationBuilder UseLocalDevelopmentImpersonation(this IApplicationBuilder app, IConfiguration configuration)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
return app.UseMiddleware<LocalDevelopmentImpersonation>(Options.Create(configuration));
}
}
}
Calling instance
var app = builder.Build();
app.UseLocalDevelopmentImpersonation(configuration); //Instance of ConfigurationManager that implements IConfiguration
It isn't possible to pass objects to the factory-activated middleware with UseMiddleware,If you do want to pass argumentss to your middleware,try with Middleware activated by convention.
You could check the doc for more details
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
I created a fresh ASP.NET Core Web API project. Here is ConfigureServices in Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddMemoryCache();
var serviceProvider = services.BuildServiceProvider();
var cache = serviceProvider.GetService<IMemoryCache>();
cache.Set("key1", "value1");
//_cahce.Count is 1
}
As you see I add an item to IMemoryCache. Here is my controller:
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
private readonly IMemoryCache _cache;
public ValuesController(IMemoryCache cache)
{
_cache = cache;
}
[HttpGet("{key}")]
public ActionResult<string> Get(string key)
{
//_cahce.Count is 0
if(!_cache.TryGetValue(key, out var value))
{
return NotFound($"The value with the {key} is not found");
}
return value + "";
}
}
When I request https://localhost:5001/api/values/key1, the cache is empty and I receive a not found response.
In short, the cache instance you're setting the value in is not the same as the one that is later being retrieved. You cannot do stuff like while the web host is being built (i.e. in ConfigureServices/Configure. If you need to do something on startup, you need to do it after the web host is built, in Program.cs:
public class Program
{
public static void Main(string[] args)
{
var host = CreateWebHostBuilder(args).Build();
var cache = host.Services.GetRequiredService<IMemoryCache>();
cache.Set("key1", "value1");
host.Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
}
As #selloape saids, if you manullay call BuildServicesProvider, you are creating a new provider, that will be not used in your controllers.
You can use a hosted service to intialize your cache
public class InitializeCacheService : IHostedService
{
private readonly IServiceProvider _serviceProvider;
public InitializeCacheService (IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public Task StartAsync(CancellationToken cancellationToken)
{
using (var scope = _serviceProvider.CreateScope())
{
var cache = _serviceProvider.GetService<IMemoryCache>();
cache.Set("key1", "value1");
}
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}
Add it in your ConfigureServices
services.AddHostedService<InitializeCacheService>();
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-2.2
I wrote some middleware to log the request path and query in the database. I have two seperate models. One for logging and one business model. After trying a few things I came up with this:
public class LogMiddleware
{
private readonly RequestDelegate _next;
private readonly DbConnectionInfo _dbConnectionInfo;
public LogMiddleware(RequestDelegate next, DbConnectionInfo dbConnectionInfo)
{
_next = next;
_dbConnectionInfo = dbConnectionInfo;
}
public async Task Invoke(HttpContext httpContext)
{
httpContext.Response.OnStarting( async () =>
{
await WriteRequestToLog(httpContext);
});
await _next.Invoke(httpContext);
}
private async Task WriteRequestToLog(HttpContext httpContext)
{
using (var context = new MyLoggingModel(_dbConnectionInfo))
{
context.Log.Add(new Log
{
Path = request.Path,
Query = request.QueryString.Value
});
await context.SaveChangesAsync();
}
}
}
public static class LogExtensions
{
public static IApplicationBuilder UseLog(this IApplicationBuilder builder)
{
return builder.UseMiddleware<LogMiddleware>();
}
}
The Model:
public class MyLoggingModel : DbContext
{
public MyLoggingModel(DbConnectionInfo connection)
: base(connection.ConnectionString)
{
}
public virtual DbSet<Log> Log { get; set; }
}
As you can see nothing special. It works, but not quite the way I would have wanted it to. The problem lies probably in EF6, not being threadsafe.
I started with this in Startup:
public class Startup
{
private IConfigurationRoot _configuration { get; }
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: false, reloadOnChange: true)
.AddEnvironmentVariables();
_configuration = builder.Build();
}
public void ConfigureServices(IServiceCollection services)
{
services.AddOptions();
services.Configure<ApplicationSettings>(_configuration.GetSection("ApplicationSettings"));
services.AddSingleton<ApplicationSettings>();
services.AddSingleton(provider => new DbConnectionInfo { ConnectionString = provider.GetRequiredService<ApplicationSettings>().ConnectionString });
services.AddTransient<MyLoggingModel>();
services.AddScoped<MyModel>();
}
public void Configure(IApplicationBuilder app)
{
app.UseLog();
app.UseStaticFiles();
app.UseMvc();
}
}
MyLoggingModel needs to be transient in order to let it work for the middleware. But this method immediately causes problems:
System.NotSupportedException: A second operation started on this
context before a previous asynchronous operation completed. Use
'await' to ensure that any asynchronous operations have completed
before calling another method on this context. Any instance members
are not guaranteed to be thread safe.
I can assure you that I did add await everywhere. But that did not resolve this. If I remove the async part then I get this error:
System.InvalidOperationException: The changes to the database were
committed successfully, but an error occurred while updating the
object context. The ObjectContext might be in an inconsistent state.
Inner exception message: Saving or accepting changes failed because
more than one entity of type 'MyLoggingModel.Log' have the same
primary key value. Ensure that explicitly set primary key values are
unique. Ensure that database-generated primary keys are configured
correctly in the database and in the Entity Framework model. Use the
Entity Designer for Database First/Model First configuration. Use the
'HasDatabaseGeneratedOption" fluent API or DatabaseGeneratedAttribute'
for Code First configuration.
That's why I came up with above code. I would have wanted to use dependency injection for the model. But I cannot make this to work. I also cannot find examples on accessing the database from middleware. So I get the feeling that I may be doing this in the wrong place.
My question: is there a way to make this work using dependency injection or am I not supposed to access the database in the middleware? And I wonder, would using EFCore make a difference?
-- update --
I tried moving the code to a seperate class and inject that:
public class RequestLog
{
private readonly MyLoggingModel _context;
public RequestLog(MyLoggingModel context)
{
_context = context;
}
public async Task WriteRequestToLog(HttpContext httpContext)
{
_context.EventRequest.Add(new EventRequest
{
Path = request.Path,
Query = request.QueryString.Value
});
await _context.SaveChangesAsync();
}
}
And in Startup:
services.AddTransient<RequestLog>();
And in the middelware:
public LogMiddleware(RequestDelegate next, RequestLog requestLog)
But this doesn't make a difference with the original approach, same errors. The only thing that seems to work (besides the non-DI solution) is:
private async Task WriteRequestToLog(HttpContext httpContext)
{
var context = (MyLoggingModel)httpContext.RequestServices.GetService(typeof(MyLoggingModel));
But I do not understand why this would be different.
Consider abstracting the db context behind a service or create one for the db context itself and used by the middleware.
public interface IMyLoggingModel : IDisposable {
DbSet<Log> Log { get; set; }
Task<int> SaveChangesAsync();
//...other needed members.
}
and have the implementation derived from the abstraction.
public class MyLoggingModel : DbContext, IMyLoggingModel {
public MyLoggingModel(DbConnectionInfo connection)
: base(connection.ConnectionString) {
}
public virtual DbSet<Log> Log { get; set; }
//...
}
The service configurations appear to be done correctly. With my above suggestion it would need to update how the db context is registered.
services.AddTransient<IMyLoggingModel, MyLoggingModel>();
the middleware can either have the abstraction injected via constructor or directly injected into the Invoke method.
public class LogMiddleware {
private readonly RequestDelegate _next;
public LogMiddleware(RequestDelegate next) {
_next = next;
}
public async Task Invoke(HttpContext context, IMyLoggingModel db) {
await WriteRequestToLog(context.Request, db);
await _next.Invoke(context);
}
private async Task WriteRequestToLog(HttpRequest request, IMyLoggingModel db) {
using (db) {
db.Log.Add(new Log {
Path = request.Path,
Query = request.QueryString.Value
});
await db.SaveChangesAsync();
}
}
}
If all else fails consider getting the context from the request's services, using it as a service locator.
public class LogMiddleware {
private readonly RequestDelegate _next;
public LogMiddleware(RequestDelegate next) {
_next = next;
}
public async Task Invoke(HttpContext context) {
await WriteRequestToLog(context);
await _next.Invoke(context);
}
private async Task WriteRequestToLog(HttpContext context) {
var request = context.Request;
using (var db = context.RequestServices.GetService<IMyLoggingModel>()) {
db.Log.Add(new Log {
Path = request.Path,
Query = request.QueryString.Value
});
await db.SaveChangesAsync();
}
}
}
How can I configure SimpleInjector to resolve LogMiddleware's Invoke method dependency, like IMessageService ?
As I know, Asp.net core uses HttpContext.RequestServices (IServiceProvider) to resolve dependencies, I set SimpleInjector container to HttpContext.RequestServices property but didn't work. I want to change ServiceProvider dynamically because each tenant should have a container.
public class LogMiddleware
{
RequestDelegate next;
private readonly ILogger log;
public LogMiddleware(RequestDelegate next, ILoggerFactory loggerFactory)
{
this.next = next;
this.log = loggerFactory.CreateLogger<LogMiddleware>();
}
public async Task Invoke(HttpContext context, IMessageService messageService)
{
await context.Response.WriteAsync(
messageService.Format(context.Request.Host.Value));
}
}
public interface IMessageService
{
string Format(string message);
}
public class DefaultMessageService : IMessageService
{
public string Format(string message)
{
return "Path:" + message;
}
}
You can use your LogMiddleware class as follows:
applicationBuilder.Use(async (context, next) => {
var middleware = new LogMiddleware(
request => next(),
applicationBuilder.ApplicationServices.GetService<ILoggerFactory>());
await middleware.Invoke(context, container.GetInstance<IMessageService>());
});
I however advise you to change your middleware class a little bit. Move the runtime data (the next() delegate) out of the constructor (since components should not require runtime data during construction), and move the IMessageService dependency into the constructor. And replace the ILoggerFactory with a ILogger dependency, since an injection constructor should do no more than store its incoming dependencies (or replace ILogger with your own application-specific logger abstraction instead).
Your middleware class will then look as follows:
public sealed class LogMiddleware
{
private readonly IMessageService messageService;
private readonly ILogger log;
public LogMiddleware(IMessageService messageService, ILogger log) {
this.messageService = messageService;
this.log = log;
}
public async Task Invoke(HttpContext context, Func<Task> next) {
await context.Response.WriteAsync(
messageService.Format(context.Request.Host.Value));
await next();
}
}
This allows you to use your middleware as follows:
var factory = applicationBuilder.ApplicationServices.GetService<ILoggerFactory>();
applicationBuilder.Use(async (context, next) => {
var middleware = new LogMiddleware(
container.GetInstance<IMessageService>(),
factory.CreateLogger<LogMiddleware>());
await middleware.Invoke(context, next);
});
Or in case you registered the ILogger (or your custom logging abstraction) in Simple Injector, you can do the following:
applicationBuilder.Use(async (context, next) => {
var middleware = container.GetInstance<LogMiddleware>();
await middleware.Invoke(context, next);
});
There is two problem with your code.
Your "DefaultMessageService" does not implement interface "IMessageService".
It should be like this.
public class DefaultMessageService : IMessageService
{
public string Format(string message)
{
return "Path:" + message;
}
}
You have to register DefaultMessageService in ConfigureService in Startup.cs.
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddMvc();
services.AddSingleton<IMessageService>(new DefaultMessageService());
}