Clean database on application startup - c#

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

Related

A Command Is Already In Progress

I am attempting to run a background worker for a web app that I am developing. I am using Npgsql as my EF Core provider.
For clarification, I have injected my DbContext with a Transient lifetime, and have allowed Pooling in my connection string, however, whenever I try to test it I get the following error:
Npgsql.NpgsqlOperationInProgressException: A command is already in progress: [My Query Here]
I have my Program Class Set up as such:
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
// Get the configuration
IConfiguration config = hostContext.Configuration;
// DbContext
services.AddDbContext<MyDbContext>(options => options.UseNpgsql(config.GetConnectionString("PostgreSQLString")), ServiceLifetime.Transient);
services.AddHostedService<Worker>();
services.AddScoped<IDTOService, BackgroundDTOService>();
});
}
Which then leads to my Worker class
public class Worker : BackgroundService
{
private Logger logger;
public Worker(IServiceProvider services, IConfiguration configuration)
{
this.Services = services;
var optionsBuilder = new DbContextOptionsBuilder<MyDbContext>();
optionsBuilder.UseNpgsql(configuration.GetConnectionString("PostgreSQLString"));
var context = new StatPeekContext(optionsBuilder.Options);
this.logger = new Logger(new LogWriter(context));
}
public IServiceProvider Services { get; }
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
this.logger.LogInformation("ExecuteAsync in Worker Service is running.");
await this.DoWork(stoppingToken);
}
private async Task DoWork(CancellationToken stoppingToken)
{
using (var scope = Services.CreateScope())
{
var context = scope.ServiceProvider.GetRequiredService<MyDbContext>();
var dtoService = scope.ServiceProvider.GetRequiredService<IDTOService>();
await dtoService.ProcessJSON(stoppingToken);
}
}
public override async Task StopAsync(CancellationToken cancellationToken)
{
this.logger.LogInformation("Worker service is stopping.");
await Task.CompletedTask;
}
}
which leads to my BackGroundDTOService
public class BackgroundDTOService : IDTOService
{
private int executionCount = 0;
private Logger logger;
private MyDbContext context;
private DbContextOptionsBuilder<MyDbContext> optionsBuilder;
public BackgroundDTOService(IConfiguration configuration, MyDbContext context)
{
this.optionsBuilder = new DbContextOptionsBuilder<MyDbContext>();
this.optionsBuilder.UseNpgsql(configuration.GetConnectionString("PostgreSQLString"));
this.logger = new Logger(new LogWriter(new MyDbContext(this.optionsBuilder.Options)));
this.context = context;
}
public async Task ProcessJSON(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
this.executionCount++;
this.logger.LogInformation($"DTO Service is working. Count: {this.executionCount}");
this.ProcessTeams();
await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken);
}
}
public void ProcessTeams()
{
// Add Any Franchises that don't exist
var franchiseDumps = this.context.RequestDumps.Where(rd => rd.Processed == false && rd.DumpType == "leagueteams");
foreach (RequestDump teamDump in franchiseDumps)
{
var league = this.context.Leagues.Include(l => l.Franchises).FirstOrDefault(l => l.Id == teamDump.LeagueID);
var teams = Jeeves.GetJSONFromKey<List<DTOTeam>>(teamDump.JsonDump, "leagueTeamInfoList");
foreach (DTOTeam team in teams)
{
this.UpdateFranchise(team, league);
}
this.logger.LogInformation($"DTO Service Processed League Teams on count {this.executionCount}");
}
this.context.SaveChanges();
}
The error appears to occur immediately after snagging franchiseDumps when it tries to get league
Could you try materialising the query:
var franchiseDumps = this.context.RequestDumps.Where(rd => rd.Processed == false && rd.DumpType == "leagueteams").ToList();
I had the same issue and I realized that PostgreSQL doesn't support Multiple Active Result Sets (MARS) compare to MSSQL.
An alternative way that worked for me to fetch objects with foreign key:
IEnumerable<Expense> objList = _db.Expenses;
foreach (var obj in objList.ToList())
{
obj.ExpenseCategory = _db.ExpenseCategories.FirstOrDefault(u => u.Id == obj.ExpenseCategoryId);
}

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

Overriding database provider in integration test with WebApplicationFactory

I am following the official MS documentation for integration testing .Net Core (https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-2.1).
I was able to get the first part of the integration test done where I was not overriding the startup class of the application I am testing (i.e. I was using a web application factorythat did not override any services).
I want to override the database setup to use an in-memory database for the integration test. The problem I am running into is that the configuration continues to try and use the sql server for services.AddHangfire().
How do I override only above specific item in my integration test? I only want to override the AddHangfire setup and not services.AddScoped<ISendEmail, SendEmail>(). Any help would be appreciated.
Test Class with the custom web application factory
public class HomeControllerShouldCustomFactory : IClassFixture<CustomWebApplicationFactory<Startup>>
{
private readonly HttpClient _client;
private readonly CustomWebApplicationFactory<Startup> _factory;
public HomeControllerShouldCustomFactory(CustomWebApplicationFactory<Startup> factory)
{
_factory = factory;
_client = factory.CreateClient();
}
[Fact]
public async Task IndexRendersCorrectTitle()
{
var response = await _client.GetAsync("/Home/Index");
response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync();
Assert.Contains("Send Email", responseString);
}
}
Custom Web Application Factory
public class CustomWebApplicationFactory<TStartup>: WebApplicationFactory<SendGridExample.Startup>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
// Create a new service provider.
var serviceProvider = new ServiceCollection()
.AddEntityFrameworkInMemoryDatabase()
.BuildServiceProvider();
var inMemory = GlobalConfiguration.Configuration.UseMemoryStorage();
services.AddHangfire(x => x.UseStorage(inMemory));
// Build the service provider.
var sp = services.BuildServiceProvider();
});
}
}
My startup.cs in my application that I am testing
public IConfiguration Configuration { get; }
public IHostingEnvironment Environment { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddHangfire(x => x.UseSqlServerStorage(Configuration.GetConnectionString("ASP_NetPractice")));
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddScoped<ISendEmail, SendEmail>();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseHangfireServer();
app.UseHangfireDashboard();
RecurringJob.AddOrUpdate<ISendEmail>((email) => email.SendReminder(), Cron.Daily);
app.UseMvc();
Update
I don't see this issue in my other example project where I am using only entity framework. I have a simple application with an application db context which uses SQL server. In my test class, I override it with an in-memory database and everything works. I am at a loss at to why it will work in my example application but not work in my main application. Is this something to do with how HangFire works?
In my test application (example code below), I can delete my sql database, run my test, and the test passes because the application DB context does not go looking for the sql server instance but uses the in-memory database. In my application, the HangFire service keeps trying to use the sql server database (if I delete the database and try to use an in-memory database for the test - it fails because it can't find the instance its trying to connect to). How come there is such a drastic difference in how the two projects work when a similar path is used for both?
I ran through the debugger for my integration test which calls the index method on the home controller above (using the CustomWebApplicationFactory). As I am initializing a test server, it goes through my startup class which calls below in ConfigureServices:
services.AddHangfire(x => x.UseSqlServerStorage(Configuration.GetConnectionString("ASP_NetPractice")));
After that, the Configure method tries to call below statement:
app.UseHangfireServer();
At this point the test fails as It cannot find the DB. The DB is hosted on Azure so I am trying to replace it with an in-memory server for some of the integration test. Is the approach I am taking incorrect?
My example application where its working
Application DB Context in my example application
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public virtual DbSet<Message> Messages { get; set; }
public async Task<List<Message>> GetMessagesAsync()
{
return await Messages
.OrderBy(message => message.Text)
.AsNoTracking()
.ToListAsync();
}
public void Initialize()
{
Messages.AddRange(GetSeedingMessages());
SaveChanges();
}
public static List<Message> GetSeedingMessages()
{
return new List<Message>()
{
new Message(){ Text = "You're standing on my scarf." },
new Message(){ Text = "Would you like a jelly baby?" },
new Message(){ Text = "To the rational mind, nothing is inexplicable; only unexplained." }
};
}
}
Startup.cs in my example application
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
CustomWebApplicationFactory - in my unit test project
public class CustomWebApplicationFactory<TStartup>
: WebApplicationFactory<Startup>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
// Create a new service provider.
var serviceProvider = new ServiceCollection()
.AddEntityFrameworkInMemoryDatabase()
.BuildServiceProvider();
// Add a database context (ApplicationDbContext) using an in-memory
// database for testing.
services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseInMemoryDatabase("InMemoryDbForTesting");
options.UseInternalServiceProvider(serviceProvider);
});
// Build the service provider.
var sp = services.BuildServiceProvider();
});
}
}
My unit test in my unit test project
public class UnitTest1 : IClassFixture<CustomWebApplicationFactory<Startup>>
{
private readonly HttpClient _client;
private readonly CustomWebApplicationFactory<Startup> _factory;
public UnitTest1(CustomWebApplicationFactory<Startup> factory)
{
_factory = factory;
_client = factory.CreateClient();
}
[Fact]
public async System.Threading.Tasks.Task Test1Async()
{
var response = await _client.GetAsync("/");
//response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync();
Assert.Contains("Home", responseString);
}
Update 2
I think I found an alternate to trying to override all my configuration in my integration test class. Since it's a lot more complicated to override HangFire as opposed to an ApplicationDBContext, I came up with below approach:
Startup.cs
if (Environment.IsDevelopment())
{
var inMemory = GlobalConfiguration.Configuration.UseMemoryStorage();
services.AddHangfire(x => x.UseStorage(inMemory));
}
else
{
services.AddHangfire(x => x.UseSqlServerStorage(Configuration["DBConnection"]));
}
Then in my CustomWebApplicationBuilder, I override the environment type for testing:
public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<SendGridExample.Startup>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseEnvironment("Development"); //change to Production for alternate test
builder.ConfigureServices(services =>
{
// Create a new service provider.
var serviceProvider = new ServiceCollection()
.AddEntityFrameworkInMemoryDatabase()
.BuildServiceProvider();
});
}
}
With that approach, I don't need to worry about having to do extra logic to satisfy hangfire's check for an active DB. It works but I am not 100% convinced its the best approach as I'm introducing branching in my production startup class.
There are two different scenarios you need to check.
Create a job by class BackgroundJob
Create a job by interface IBackgroundJobClient
For the first option, you could not replace the SqlServerStorage with MemoryStorage.
For UseSqlServerStorage, it will reset JobStorage by SqlServerStorage.
public static IGlobalConfiguration<SqlServerStorage> UseSqlServerStorage(
[NotNull] this IGlobalConfiguration configuration,
[NotNull] string nameOrConnectionString)
{
if (configuration == null) throw new ArgumentNullException(nameof(configuration));
if (nameOrConnectionString == null) throw new ArgumentNullException(nameof(nameOrConnectionString));
var storage = new SqlServerStorage(nameOrConnectionString);
return configuration.UseStorage(storage);
}
UseStorage
public static class GlobalConfigurationExtensions
{
public static IGlobalConfiguration<TStorage> UseStorage<TStorage>(
[NotNull] this IGlobalConfiguration configuration,
[NotNull] TStorage storage)
where TStorage : JobStorage
{
if (configuration == null) throw new ArgumentNullException(nameof(configuration));
if (storage == null) throw new ArgumentNullException(nameof(storage));
return configuration.Use(storage, x => JobStorage.Current = x);
}
Which means, no matter what you set in CustomWebApplicationFactory, UseSqlServerStorage will reset BackgroundJob with SqlServerStorage.
For second option, it could replace IBackgroundJobClient with MemoryStorage by
public class CustomWebApplicationFactory<TEntryPoint> : WebApplicationFactory<Startup>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
services.AddSingleton<JobStorage>(x =>
{
return GlobalConfiguration.Configuration.UseMemoryStorage();
});
});
}
}
In conclusion, I suggest you register IBackgroundJobClient and try the second option to achieve your requirement.
Update1
For DB is not available, it could not be resolved by configuring the Dependency Injection. This error is caused by calling services.AddHangfire(x => x.UseSqlServerStorage(Configuration.GetConnectionString("ASP_NetPractice")));.
For resolving this error, you need to overriding this code in Startup.cs.
Try steps below:
Change Startup to below:
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)
{
//Rest Code
ConfigureHangfire(services);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
//Rest Code
app.UseHangfireServer();
RecurringJob.AddOrUpdate(() => Console.WriteLine("RecurringJob!"), Cron.Minutely);
}
protected virtual void ConfigureHangfire(IServiceCollection services)
{
services.AddHangfire(config =>
config.UseSqlServerStorage(Configuration.GetConnectionString("HangfireConnection"))
);
}
}
Create StartupTest in test project.
public class StartupTest : Startup
{
public StartupTest(IConfiguration configuration) :base(configuration)
{
}
protected override void ConfigureHangfire(IServiceCollection services)
{
services.AddHangfire(x => x.UseMemoryStorage());
}
}
CustomWebApplicationFactory
public class CustomWebApplicationFactory<TEntryPoint> : WebApplicationFactory<TEntryPoint> where TEntryPoint: class
{
protected override IWebHostBuilder CreateWebHostBuilder()
{
return WebHost.CreateDefaultBuilder(null)
.UseStartup<TEntryPoint>();
}
}
Test
public class HangfireStorageStartupTest : IClassFixture<CustomWebApplicationFactory<StartupTest>>
{
private readonly HttpClient _client;
private readonly CustomWebApplicationFactory<StartupTest> _factory;
public HangfireStorageStartupTest(CustomWebApplicationFactory<StartupTest> factory)
{
_factory = factory;
_client = factory.CreateClient();
}
}

Using a service class with asp.net core

I'm writing a web app (asp.net core, mvc), and at the moment all of my code is in controllers. I want to move some of it into service classes, to ease with re-use of code, and tidy up my controller classes a little.
The problem is, when I try do this, I keep getting the error 'Cannot access a disposed object.'
It seems that the second time I try to use a database access class (DBcontext or userManager), the class (or something else) is disposed.
A sample of my code is below. I have removed some bits for brevity sake (the removed bits are mostly irrelevant).
Firstly, the controller:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using MyProject.Data;
using MyProject.Models;
using Microsoft.AspNetCore.Identity;
using MyProject.Models.Api;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Configuration;
using MyProject.Services;
namespace MyProject.ApiControllers
{
[Produces("application/json")]
[Route("api/Migration")]
public class MigrationController : Controller
{
private readonly ApplicationDbContext _context;
private UserManager<ApplicationUser> _userManager;
private RoleManager<IdentityRole> _roleManager;
private IConfiguration _configuration;
private InitialMigrationService _initialMigrationService;
public MigrationController(ApplicationDbContext context, UserManager<ApplicationUser> userManager, RoleManager<IdentityRole> roleManager, IConfiguration configuration)
{
_context = context;
_userManager = userManager;
_roleManager = roleManager;
_configuration = configuration;
_initialMigrationService = new InitialMigrationService(userManager, roleManager, context, configuration);
}
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[HttpPost]
[Route("GetDumpData")]
public async Task<bool> GetDumpData([FromBody] ApiDataDumpInfo apiDataDumpInfo)
{
// I have removed some code here to download a file into dumpBytes (byte array). This works fine
Models.InitialMigration.DataDump dataDump = Models.InitialMigration.DataDump.DeserialiseFromByteArray(dumpBytes);
_initialMigrationService.MigrateDataDump(dataDump);
return true;
}
}
}
And the service class (and interface):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using MyProject.Data;
using MyProject.Models;
using MyProject.Models.InitialMigration;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration;
namespace MyProject.Services
{
public class InitialMigrationService : IInitialMigrationService
{
private UserManager<ApplicationUser> _userManager;
private RoleManager<IdentityRole> _roleManager;
private ApplicationDbContext _context;
private IConfiguration _configuration;
public InitialMigrationService(UserManager<ApplicationUser> userManager, RoleManager<IdentityRole> roleManager, ApplicationDbContext context, IConfiguration configuration)
{
_context = context;
_userManager = userManager;
_roleManager = roleManager;
_configuration = configuration;
}
public bool MigrateDataDump(DataDump dump)
{
MigrateUserSetup(dump);
return true;
}
private void MigrateUserSetup(DataDump dump)
{
dump.UserSetupList.ForEach(u => u.Accounts = true);
dump.UserSetupList.ForEach(async delegate (DDUserSetup u)
{
if (string.IsNullOrEmpty(u.Email))
return;
var swUser = _context.SoftwareUser
.SingleOrDefault(du => du.OldID == u.ID);
if (swUser == null)
{
_context.SoftwareUser.Add(new Models.SoftwareUser
{
Name = u.Name
// Have left out lots of other fields being copied over
});
_context.SaveChanges();
swUser = _context.SoftwareUser
.SingleOrDefault(du => du.OldID == u.ID);
string userID = await EnsureUser(u.Password, u.Email, swUser.ID);
await EnsureRole(userID, ConstantData.ConstUserRole);
}
});
}
private async Task<string> EnsureUser(string testUserPw, string userName, int? SoftwareUserId)
{
var user = await _userManager.FindByNameAsync(userName);
IdentityResult result = null;
if (user == null)
{
user = new ApplicationUser { UserName = userName, SoftwareUserID = SoftwareUserId, Email = userName };
if (string.IsNullOrEmpty(testUserPw))
result = await _userManager.CreateAsync(user);
else
result = await _userManager.CreateAsync(user, testUserPw); // This is the line I get the error on.
}
return user.Id;
}
private async Task<IdentityResult> EnsureRole(string uid, string role)
{
try
{
IdentityResult IR = null;
if (!await _roleManager.RoleExistsAsync(role))
{
IR = await _roleManager.CreateAsync(new IdentityRole(role));
}
var user = await _userManager.FindByIdAsync(uid);
IR = await _userManager.AddToRoleAsync(user, role);
return IR;
}
catch (Exception exc)
{
throw;
}
}
}
public interface IInitialMigrationService
{
bool MigrateDataDump(DataDump dump);
}
}
Can someone tell me what I'm doing wrong? I've searched for a specific example of how this sort of thing is meant to be structured, but couldnt find much beyond using an interface (which doesnt seem to help).
Thanks.
--- EDIT ---
As per Camilos suggestion, I have made the following changes:
MigrationController now starts like this:
public class MigrationController : Controller
{
private readonly ApplicationDbContext _context;
private UserManager<ApplicationUser> _userManager;
private RoleManager<IdentityRole> _roleManager;
private IConfiguration _configuration;
private IInitialMigrationService _initialMigrationService;
public MigrationController(ApplicationDbContext context, UserManager<ApplicationUser> userManager, RoleManager<IdentityRole> roleManager, IConfiguration configuration, IInitialMigrationService initialMigrationService)
{
_context = context;
_userManager = userManager;
_roleManager = roleManager;
_configuration = configuration;
_initialMigrationService = initialMigrationService;
}
And made this addition to the Startup.ConfigureServices method:
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IInitialMigrationService, InitialMigrationService>();
But I still get the same error.
----- EDIT 2 ----
I now suspect the issue is with running Async methods. It seems that running an Asynch method makes the object disposed the next time I try to use it.
For example, in the block below (for my "MigrateUserSetup" method), I changed "SaveChanges" to "SaveChangesAsync", and the next time the dbcontext is used, I get the disposed error:
private void MigrateUserSetup(DataDump dump)
{
dump.UserSetupList.ForEach(async delegate (DDUserSetup u)
{
if (string.IsNullOrEmpty(u.Email))
return;
var swUser = _context.SoftwareUser
.SingleOrDefault(du => du.OldID == u.ID);
if (swUser == null)
{
_context.SoftwareUser.Add(new Models.SoftwareUser
{
Name = u.Name,
// Migrate a bunch of fields
});
await _context.SaveChangesAsync(); // This was previously just "SaveChanges()", not Async
swUser = _context.SoftwareUser
.SingleOrDefault(du => du.OldID == u.ID); // Now I get the disposed object error here
string userID = await EnsureUserAsync(u.Password, u.Email, swUser.ID);
await EnsureRole(userID, ConstantData.ConstUserRole);
}
});
}
---- EDIT 3 ----
I've finally got it working. I ended up replacing the user setup loop from a "ForEach" to a "for" loop, as below:
Original "ForEach" loop:
dump.UserSetupList.ForEach(async delegate (DDUserSetup u)
{
New "For" loop:
for (int i = 0; i < dump.UserSetupList.Count; i++)
{
var u = dump.UserSetupList[i];
I'm not sure how this makes such a big difference, or if this is really a desirable solution, but might give a bit more of a clue as to the underlying problem.
This line:
_initialMigrationService = new InitialMigrationService(userManager, roleManager, context, configuration);
is not correct. If you look at all of the previous lines in that constructor, there's not another new there, and there's a reason for that, called Dependency Injection.
When you want to create your own services, you register them up on the DI container that ASP.NET Core provides:
public
{
...
services.AddScoped<IInitialMigrationService, InitialMigrationService>();
...
}
And then you ask for a new instance of that service to be created for you:
public MigrationController(ApplicationDbContext context, UserManager<ApplicationUser> userManager, RoleManager<IdentityRole> roleManager, IConfiguration configuration, IInitialMigrationService initialMigrationService)
{
_context = context;
_userManager = userManager;
_roleManager = roleManager;
_configuration = configuration;
_initialMigrationService = initialMigrationService;
}
In a not-so-unrelated note, beware that injecting IConfiguration can lead to huge wastes of RAM. You should follow the Configuration/Options approaches instead.

How to access the database in Middleware using Entity Framework 6

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

Categories