How to access the database in Middleware using Entity Framework 6 - c#

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

Related

Passing arguments to Middleware class

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

Cannot access a disposed context instance with N-layer architecture

I'm trying to make a N-layer architecture for my Telegram Bot. I created DAL, BLL and PL. I would like to add entity News to my DB. But I have some issue with my context.
My DB Context:
public class ApplicationContext : DbContext
{
public DbSet<News> News { get; set; }
public DbSet<User> Users { get; set; }
public ApplicationContext(DbContextOptions<ApplicationContext> options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<News>().Property(tn => tn.Id).ValueGeneratedOnAdd();
modelBuilder.Entity<User>().Property(tn => tn.Id).ValueGeneratedOnAdd();
modelBuilder.Entity<News>().Property(tn => tn.Title).IsRequired();
modelBuilder.Entity<News>().Property(tn => tn.Href).IsRequired();
modelBuilder.Entity<News>().Property(tn => tn.Image).IsRequired();
modelBuilder.Entity<News>().Property(tn => tn.Date).IsRequired();
modelBuilder.Entity<User>().Property(tn => tn.UserId).IsRequired();
modelBuilder.Entity<User>().Property(tn => tn.UserName).IsRequired();
modelBuilder.Entity<User>().Property(tn => tn.DateOfStartSubscription).IsRequired();
base.OnModelCreating(modelBuilder);
}
}
Interface UoW:
public interface IUnitOfWork : IDisposable
{
INewsRepository News { get; }
IUserRepository Users { get; }
int Complete();
}
Class UoW:
public class UnitOfWork : IUnitOfWork
{
public IUserRepository Users { get; }
public INewsRepository News { get; }
private readonly ApplicationContext _context;
public UnitOfWork(ApplicationContext context)
{
_context = context;
Users = new UserRepository.UserRepository(_context);
News = new NewsRepository.NewsRepository(_context);
}
public int Complete() => _context.SaveChanges();
public void Dispose() => _context.Dispose();
}
My DAL Generic Repository:
async Task IGenericRepository<T>.AddAsync(T entity) => await _context.Set<T>().AddAsync(entity);
DAL Injection:
public static class DALInjection
{
public static void Injection(IServiceCollection services)
{
services.AddTransient(typeof(IGenericRepository<>), typeof(GenericRepository<>));
services.AddTransient<IUserRepository, UserRepository.UserRepository>();
services.AddTransient<INewsRepository, NewsRepository.NewsRepository>();
services.AddTransient<IUnitOfWork, UnitOfWork.UnitOfWork>();
}
}
My BLL Service class:
public class ParserService : IParser
{
private IUnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public ParserService(IUnitOfWork unitOfWork, IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
private async Task SaveArticles(IEnumerable<NewsDTO> articlesDTO)
{
var articles = _mapper.Map<IEnumerable<NewsDTO>, IEnumerable<News>>(articlesDTO);
await _unitOfWork.News.AddAsync(articles.First());
_unitOfWork.Complete();
}
BLL Injection:
public static class BLLInjection
{
public static void Injection(IServiceCollection services)
{
DALInjection.Injection(services);
services.AddTransient<IParser, ParserService>();
services.AddTransient<IArticleService, ArticleService>();
services.AddAutoMapper(typeof(CommonMappingProfile));
}
}
My PL:
private static async Task SendArticleAsync(long chatId, int offset, int count)
{
var articles = await _parser.MakeHtmlRequest(offset, count);
foreach (var article in articles)
{
var linkButton = KeyboardGoOver("Перейти", article.Href);
await _client.SendPhotoAsync(chatId: chatId, photo: article.Image,
caption: $"*{article.Title}*", parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown, replyMarkup: linkButton);
}
await OnLoadMoreNewsAsync(chatId, offset + count, count);
}
PL Startup class:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddDbContext<ApplicationContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection"),
b => b.MigrationsAssembly(typeof(ApplicationContext).Assembly.FullName)));
BLLInjection.Injection(services);
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "TelegramBot.WebApi", Version = "v1" });
});
}
When I tried to debug, I had this error but I could not resolve this issue.
_context = Database = {"Cannot access a disposed context instance. A common cause of this error is disposing a context instance that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may o...
Could someone help me with this issue?
There are few problems in your code.
Controllers are scoped entities, their instances created per http request and disposed after request is finished. It means controller is not good place to subscribe to events. When you call /start endpoint you create an instance of TelegramController and TelegramBotClient, but once the request is finished, the controller and all its non-singleton dependencies (IParser in your case) are disposed. But you subscribed for TelegramBotClient events that captured reference to IParser. It means all events that will arrive after request is finished will try to access disposed IParser instance and this is the reason for your exception.
For event based messages it's better to use IHostedService. You will need to use IServiceScopeFactory to create a scope for each message and resolve your dependencies from this scope.
public class TelegramHostedService : IHostedService
{
private IServiceScopeFactory _scopeFactory;
public TimedHostedService(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
public Task StartAsync(CancellationToken stoppingToken)
{
_client = new TelegramBotClient(_token);
_client.OnMessage += OnMessageHandlerAsync;
_client.OnCallbackQuery += OnLoadCallBackAsync;
_client.StartReceiving();
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken stoppingToken)
{
// TODO: Unsubscribe from events
return Task.CompletedTask;
}
public static async void OnMessageHandlerAsync(object sender, MessageEventArgs e)
{
using var scope = _scopeFactory.CreateScope();
var handler = scope.ServiceProvider.GetRequiredService<MessageHandler>();
await handler.Handle(TODO: pass required args); // Move the logic to separate handler class to keep hosted service clean
}
...
}
I moved call to _client.StartReceiving(); after event subscription otherwise there is a chance for race condition when you receive event but you don't yet have subscribers and this event will be lost.
The second issue is as #PanagiotisKanavos said: async void can't be awaited, hence once your code hit first true async method (like DB access, http request, file read or any other I/O operation) the control is returned to the point where async void method was called and continues execution without waiting for operation completion. The whole app can even crash if you throw unhandled exception from such method, hence async void should be avoided. To prevent these problems wrap your async event handlers with sync methods that will block the execution with Wait() method:
public class TelegramHostedService : IHostedService
{
private IServiceScopeFactory _scopeFactory;
public TimedHostedService(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
public Task StartAsync(CancellationToken stoppingToken)
{
_client = new TelegramBotClient(_token);
_client.OnMessage += OnMessageHandler;
_client.OnCallbackQuery += OnLoadCallBack;
_client.StartReceiving();
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken stoppingToken)
{
// TODO: Unsubscribe from events
return Task.CompletedTask;
}
public static void OnMessageHandler(object sender, MessageEventArgs e)
{
OnMessageHandlerAsync(sender, e).Wait();
}
public static async Task OnMessageHandlerAsync(object sender, MessageEventArgs e)
{
using var scope = _scopeFactory.CreateScope();
var handler = scope.ServiceProvider.GetRequiredService<MessageHandler>();
await handler.Handle(TODO: pass required args); // Move the logic to separate handler class to keep hosted service clean
}
...
}

aspnet core change database connection string via middleware

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

Clean database on application startup

I've adapted an approach of mapping signalr connections to users to my aspnet core 3.0 app.
The approach I'm referring to is outlined in Mapping SignalR Users to Connections, section Permanent, external storage. I know that this article was written for a different version of Asp.Net but it came in handy.
This is the code of the Hub:
public class SomeHub : Hub
{
private readonly UserManager _userManager;
private readonly AppDbContext _dbContext;
protected BaseHub(UserManager userManager, AppDbContext dbContext)
{
_userManager = userManager;
_dbContext = dbContext;
}
public override async Task OnConnectedAsync()
{
var user = await _userManager.GetMe();
user.Connections.Add(new Connection { ConnectionId = Context.ConnectionId });
await _dbContext.SaveChangesAsync();
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception ex)
{
var user = await _userManager.GetMe();
if (await _dbContext.Connections.FindAsync(Context.ConnectionId) is {} connection)
{
user.Connections.Remove(connection);
await _dbContext.SaveChangesAsync();
}
await base.OnDisconnectedAsync(ex);
}
}
Question
If I teardown my application, Connection database entries will remain in my database, because the OnDisconnectedAsync method was not called.
Is there a possibility to remove those entries once the application starts?
I needed to add the following code inside the Configure method of the Startup class after calling AddDbContext:
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<AppDbContext>([...]);
using (var serviceProvider = services.BuildServiceProvider())
using (var serviceScope = serviceProvider.CreateScope())
using (var context = scope.ServiceProvider.GetService<AppDbContext>())
{
context.Connections.RemoveRange(context.Connections);
context.SaveChanges();
}
[...]
}
On .net 6
program.cs
builder.Services.AddSingleton<ClassName>();
...
var app = builder.Build();
var cleanconnections = app.Services.GetRequiredService<ClassName>();
cleanconnections.DoStuff();

C# Dependency Injection : Injecting multiple interfaces into other services

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

Categories