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.
Related
I'm trying to implement .net core Identity on an existing system with existing dbcontext. Here is my DbContext class which I made to inherit IdentityDbContext. Basically I want the ASPUsers tables to be in the same database as my application entities. Migration was fine and was able to create the tables successfully, however I am having issues with trying to resolve the dbcontext with the userstore.
DAMDbContext.cs
using DAM.Domain.Entities;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using System.Threading.Tasks;
namespace DAM.Persistence
{
public class DAMDBContext : IdentityDbContext, IDbContext
{
public DAMDBContext(DbContextOptions<DAMDBContext> options)
: base(options)
{
}
public DbSet<ApprovalLevel> ApprovalLevels { get; set; }
public DbSet<ApprovalLevelApprover> ApprovalLevelApprovers { get; set; }
public DbSet<Asset> Assets { get; set; }
public DbSet<AssetAudit> AssetAudit { get; set; }
...
Startup.cs
services.AddTransient<ICacheProvider, CacheAsideProvider>();
services.AddTransient<ICacheKeyGenerator, TypePrefixerCacheKeyGenerator>();
services.AddScoped<IAzureStorageService, AzureStorageService>();
services.AddScoped<IConversionService, ConversionService>();
services.AddScoped<IHelperService, HelperService>();
services.AddScoped<IEmailService, EmailService>();
services.AddScoped<ITagService, TagService>();
services.AddMediatR(typeof(GetAssetRequestHandler).Assembly);
services.AddDbContext<IDbContext, DAMDBContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DAMDBConnectionString"),
sqlServerOptionsAction: sqlOptions =>
{
sqlOptions.EnableRetryOnFailure();
}));
services.AddIdentity<IdentityUser, IdentityRole>(options =>
{
options.Password.RequiredLength = 10;
options.Password.RequiredUniqueChars = 3;
options.User.RequireUniqueEmail = true;
options.SignIn.RequireConfirmedEmail = true;
}).AddEntityFrameworkStores<DAMDBContext>()
.AddDefaultTokenProviders();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
AppController.cs
private readonly UserManager<IdentityUser> _userManager;
private readonly SignInManager<IdentityUser> _signInManager;
private IEmailService _emailService;
private IConfiguration _configuration;
public AppController(UserManager<IdentityUser> userManager, SignInManager<IdentityUser> signInManager, IEmailService emailService, IConfiguration configuration)
{
_userManager = userManager ?? throw new ArgumentNullException(nameof(userManager));
_signInManager = signInManager ?? throw new ArgumentNullException(nameof(signInManager));
_emailService = emailService ?? throw new ArgumentNullException(nameof(emailService));
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
}
// ASP .NET CORE Identity Implementation
[HttpGet]
[Route("Users/{id}")]
public async Task<IActionResult> GetAppUsers(string id)
{
var users = new List<IdentityUser>();
if (string.IsNullOrEmpty(id))
{
users = _userManager.Users.ToList();
}
else
{
users.Add(await _userManager.FindByIdAsync(id));
}
return Ok(new
{
Users = users,
});
}
Issue is when I use the UserManager DI in my controllers I get this issue:
System.InvalidOperationException: Unable to resolve service for type 'DAM.Persistence.DAMDBContext' while attempting to activate 'Microsoft.AspNetCore.Identity.EntityFrameworkCore.UserStore`9[Microsoft.AspNetCore.Identity.IdentityUser,Microsoft.AspNetCore.Identity.IdentityRole,DAM.Persistence.DAMDBContext,System.String,Microsoft.AspNetCore.Identity.IdentityUserClaim`1[System.String],Microsoft.AspNetCore.Identity.IdentityUserRole`1[System.String],Microsoft.AspNetCore.Identity.IdentityUserLogin`1[System.String],Microsoft.AspNetCore.Identity.IdentityUserToken`1[System.String],Microsoft.AspNetCore.Identity.IdentityRoleClaim`1[System.String]]'.
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateArgumentCallSites(Type serviceType, Type implementationType, CallSiteChain callSiteChain, ParameterInfo[] parameters, Boolean throwIfCallSiteNotFound)
Any thoughts on what I am lacking/doing wrong? Thanks in advance!
Edit: Nevermind, saw what was causing the issue.
Changed this:
services.AddDbContext<IDbContext, DAMDBContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DAMDBConnectionString"),
sqlServerOptionsAction: sqlOptions =>
{
sqlOptions.EnableRetryOnFailure();
}));
to:
services.AddDbContext<DAMDBContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DAMDBConnectionString"),
sqlServerOptionsAction: sqlOptions =>
{
sqlOptions.EnableRetryOnFailure();
}));
Turns out IDBContext was messing things up.
I'm trying to connect with a database using Razor pages and Entity Framework but I get this error
Error CS0103: The name '_context' does not exist in the current context
Here is the code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using myWebApp.Models;
using Microsoft.EntityFrameworkCore;
namespace myWebApp.Pages
{
public class EmployeeModel : PageModel
{
private readonly ILogger<EmployeeModel> _logger;
public EmployeeModel(ILogger<EmployeeModel> logger)
{
_logger = logger;
}
public void OnGet()
{
}
// Create
public async Task<IActionResult> OnPostAsync()
{
var emptyreservation = new Reservation();
if (await TryUpdateModelAsync<Reservation>(
emptyreservation,
"reservation", // Prefix for form value.
r => r.reservationid, r => r.dayid, r => r.roomid, r => r.employeeid))
{
_context.Reservation.Add(emptyreservation);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
return Page();
}
}
}
Because you are trying to access dbcontext instance without creating it. (missing a Dependency Injection)
add the following to your code and it will work
private readonly ILogger<EmployeeModel> _logger;
private YourDbContextName _context
public EmployeeModel(ILogger<EmployeeModel> logger, YourDbContextName context)
{
_logger = logger;
_context = context
}
also make sure you have your startup properly configured.
You need to define it at the top of the class and inject it in the constructor
like that
private readonly ILogger<EmployeeModel> _logger;
private [YourDbContextName] _context;
public EmployeeModel(ILogger<EmployeeModel> logger,[YourDbContextName] context)
{
_logger = logger;
_context = context;
}
And don't forget to configure Context in the Startup.cs file inside ConfigureServices function
var connectionString = configuration["ConnectionStrings:sqlConnection"];
services.AddEntityFrameworkSqlServer();
services.AddDbContextPool<DatabaseContext>(options =>
options.UseSqlServer(connectionString));
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();
This code uses Authorization to allow only admin to view or manage users.
The problem is that when I uncomment the authorization tag with roles as Constants.Administration it works but when I don't it shows access denied.
#INCLUDED
This is my Constants Class File Too
namespace fptbpharmarcy
{
public static class Constants
{
public const string AdministratorRole = "Administrator";
public const string PatientRole = "Patient";
public const string PharmarcyRole = "Pharmarcy";
public const string EveryoneRole = "Everyone";
public const int AdminType = 1;
public const int PatientType = 2;
public const int PharmarcyType = 3;
}
}
//This is my startup file
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using fptbpharmarcy.Data;
using fptbpharmarcy.Models;
using fptbpharmarcy.Services;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace fptbpharmarcy {
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) {
services.AddDbContext<ApplicationDbContext> (options =>
options.UseSqlite (Configuration.GetConnectionString ("DefaultConnection")));
services.AddIdentity<ApplicationUser, IdentityRole> ()
.AddEntityFrameworkStores<ApplicationDbContext> ()
.AddDefaultTokenProviders ();
// Add application services.
services.AddTransient<IEmailSender, EmailSender> ();
services.AddMvc ();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure (IApplicationBuilder app, IHostingEnvironment env, UserManager<ApplicationUser> userManager,
RoleManager<IdentityRole> roleManager) {
if (env.IsDevelopment ()) {
app.UseDeveloperExceptionPage ();
app.UseDatabaseErrorPage ();
EnsureRolesAsync (roleManager).Wait ();
EnsureTestAdminAsync (userManager).Wait ();
} else {
app.UseExceptionHandler ("/Home/Error");
}
app.UseStaticFiles ();
app.UseAuthentication ();
app.UseMvc (routes => {
routes.MapRoute (
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
private static async Task EnsureRolesAsync (RoleManager<IdentityRole> roleManager) {
var alreadyExists = await roleManager.RoleExistsAsync (Constants.AdministratorRole);
if (alreadyExists) return;
await roleManager.CreateAsync (
new IdentityRole (Constants.AdministratorRole));
}
private static async Task EnsureTestAdminAsync (UserManager<ApplicationUser> userManager) {
var testAdmin = await userManager.Users.Where (x => x.Email == "superadmin#gmail.com").SingleOrDefaultAsync ();
if (testAdmin != null) return;
testAdmin = new ApplicationUser {
UserName = "superadmin", Email = "superadmin#gmail.com"
};
await userManager.CreateAsync (testAdmin, "SECRET#12345as");
await userManager.AddToRoleAsync (testAdmin, Constants.AdministratorRole);
}
}
}
//ManageUsersController
namespace fptbpharmarcy.Controllers
{
[Authorize(Roles = Constants.AdministratorRole)]
[Route("[controller]/[action]")]
public class ManageUsersController : Controller
{
private readonly UserManager<ApplicationUser> _userManager;
public ManageUsersController(
UserManager<ApplicationUser> userManager
)
{
_userManager = userManager;
}
[HttpGet]
public async Task<IActionResult> Index()
{
var admin = await _userManager.GetUsersInRoleAsync(Constants.AdministratorRole);
var pharmacies = await _userManager.GetUsersInRoleAsync(Constants.PharmarcyRole);
var patients = await _userManager.GetUsersInRoleAsync(Constants.PatientRole);
var everyone = await _userManager.Users.ToArrayAsync();
var model = new ManageUsersViewModel
{
Administrators = admin,
Pharmacists = pharmacies,
Patients = patients,
Everyone = everyone
};
return View(model);
}
}
}
SCREENSHOT OF OUTPUT- WHEN THE AUTHORIZATION IS COMMENTED
//[Authorize(Roles = Constants.AdministratorRole)]
[]2
You can see that from the above picture none of the users with the administrative role could access the file. Please what exactly do you think might be causing these. I have spent hours trying to fix this. But to no avail.
I want to create my custom service via using userManager. But I don't know how to access to dbContext.
public static class UserManagerExtensions
{
public static async Task<IdentityResult> AddProfileAsync(this UserManager<ApplicationUser> userManager, UserProfileModel model)
{
// how to access to dbContext here???
if (await dbContext.SaveChangesAsync() > 0)
{
return IdentityResult.Success;
}
return IdentityResult.Failed();
}
}
Usage:
public class UserController : Controller
{
private readonly UserManager<ApplicationUser> _userManager;
public UserController(UserManager<ApplicationUser> userManager)
{
_userManager = userManager;
}
public async Task<IActionResult> Update(UserProfileModel model)
{
await _userManager.AddProfileAsync(model);
return View();
}
}
How to access DbContext class inside a static class?
Thank you!
You should inject it into your controller and then pass it to your method:
public class UserController : Controller
{
private readonly DbContext _dbContext;
private readonly UserManager<ApplicationUser> _userManager;
public UserController(DbContext dbContext, UserManager<ApplicationUser> userManager)
{
_dbContext = dbContext;
_userManager = userManager;
}
public async Task<IActionResult> Update(UserProfileModel model)
{
await _userManager.AddProfileAsync(_dbContext, model);
return View();
}
}
That way, you have full control over where there database context comes from and with what kind of lifetime it exists.
If your goal is to actually create a service as you say, you should do so and not work with extension methods at all. You could do this:
public class UserController : Controller
{
private readonly ProfileService _profileService;
public UserController(ProfileService profileService)
{
_profileService = profileService;
}
public async Task<IActionResult> Update(UserProfileModel model)
{
await _profileService.AddProfileAsync(model);
return View();
}
}
public class ProfileService
{
private readonly DbContext _dbContext;
private readonly UserManager<ApplicationUser> _userManager;
public ProfileService(DbContext dbContext, UserManager<ApplicationUser> userManager)
{
_dbContext = dbContext;
_userManager = userManager;
}
public async Task<IdentityResult> AddProfileAsync(UserProfileModel model)
{
// do stuff
if (await dbContext.SaveChangesAsync() > 0)
{
return IdentityResult.Success;
}
return IdentityResult.Failed();
}
}
And don’t forget to register your service in Startup.ConfigureServices using:
services.AddTransient<ProfileService>();
That way, you now have an actual service which has clear dependencies and which you can test properly, and you are also moving more logic out of your controllers which makes it easier to reason about it.
It's generally best to only create a DbContext as needed and discard after "SaveChanges". To be able to create it you just need a connection string, which is usually something you store in a central configuration store.