I am having trouble with switching partially to .NET Standard.
I am in the process of migrating a class library to .NET Standard, in this library I have the repositories and the database communication. I already migrated this successfully to use AspNetCore.Identity.EntityFrameworkCore
. What I try to achieve is eventually having 1 .NET Standard project taking care of the database, where 1 MVC .NET Framework, 1 API .NET Framework and 1 new .NET Core application will be using it. Besides that a few other .NET Framework class libraries depend on it. Basically the .NET Core application has been made already but the back-end has not been 'merged' for overlapping functionalities.
Little overview:
Reason for not converting MVC/API to .Core is that too many other libraries currently depend on .NET Framework, some are not yet convertible, but using the same library for the database is a fundamental change which will avoid double implementations of some repositories.
I have also already converted my entities that implement Microsoft.AspNetCore.Identity.IdentityUser
, Microsoft.AspNetCore.Identity.IdentityRole
, etc.
So my DbContext class looks like this:
public class DatabaseContext : IdentityDbContext<ApplicationUser, ApplicationRole, string, ApplicationUserClaim, ApplicationUserRole, ApplicationUserLogin, ApplicationRoleClaim, ApplicationUserToken>
{
private IConfiguration _config;
public DatabaseContext(IConfiguration config) : base()
{
_config = config;
}
//all DbSet, OnModelCreating
}
I have successfully ran the EFCore Code First Migrations.
Now I am trying to configure the Identity in my MVC application (and then in the API project as well).
I just have the standard IdentityConfig.cs
, Startup.Auth.cs
where all the configuration is done. I have tried looking at this documentation (migration identity). All I could do is add this, where AddMvc()
does not exist so that throws an compile error:
Startup.cs
using System;
using System.IO;
using Babywatcher.Core.Data.Database;
using Babywatcher.Core.Data.Entities;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Owin;
using Owin;
[assembly: OwinStartupAttribute(typeof(MyProject.MVC.Startup))]
namespace MyProject.MVC
{
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
var services = new ServiceCollection();
ConfigureAuth(app);
ConfigureServices(services);
}
public void ConfigureServices(IServiceCollection services)
{
// Add EF services to the services container.
services.AddDbContext<DatabaseContext>(options =>
options.UseSqlServer(""));//Configuration trying to refer to above method: Configuration.GetConnectionString("DefaultConnection")
services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddEntityFrameworkStores<DatabaseContext>()
.AddDefaultTokenProviders();
services.AddMvc();
}
}
}
Well I guess this doesn't really do much, Also I am using SimpleInjector throughout both .NET Framework projects which I would prefer to keep using if possible in stead of going to use the default dependency injector.
By adding above I don't really know what to do with the ConfigureAuth
method and where to put all the configuration.
When I try to adjust the IdentityManager, to try and reference the same types within AspNetCore.Identity
I start to get issues when trying to change the ApplicationUserManager
:
Creating the UserStore is not a problem, but trying to create the UserManager
is more difficult, I also have tried to use this in my UserRepository
, like I did before, which I can't use now anymore :(.
Old UserRepository
private readonly UserManager<ApplicationUser, string> _userManager = null;
private readonly RoleManager<ApplicationRole, string> _roleManager = null;
internal static IDataProtectionProvider DataProtectionProvider { get; private set; }
public UserRepository(DatabaseContext dbContext) : base(dbContext, c => c.contactId, m => m.ContactId)
{
var userStore =
new UserStore<ApplicationUser, ApplicationRole, string, ApplicationUserLogin, ApplicationUserRole, ApplicationUserClaim>(dbContext);
var roleStore = new RoleStore<ApplicationRole, string, ApplicationUserRole>(dbContext);
_userManager = new UserManager<ApplicationUser, string>(userStore);
_roleManager = new RoleManager<ApplicationRole, string>(roleStore);
_userManager.UserValidator = new UserValidator<ApplicationUser>(_userManager) { AllowOnlyAlphanumericUserNames = false };
if (DataProtectionProvider == null)
{
DataProtectionProvider = new MachineKeyProtectionProvider();
}
_userManager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser, string>(DataProtectionProvider.Create("Identity"));
}
Attempting to do above again gives issues when creating the UserManager
because of the 9 arguments it asks so I kind-of felt that this shouldn't be done this way... Probably going to do something like this to get them there of course I first want to fix the root problem.
Last but not least: Keep in mind that these applications are all in production already, so especially around the users I need to make sure that the logins all still work. When bringing this version live I will need to migrate the data to a new database because of some other migration issues.
Alright so this is a long answer, be prepared to spent the next day on pulling your hair out of your head :).
First an interface of IApplicationUser
which is implemented by 2 different classes:
public interface IApplicationUser
{
string Id { get; set; }
/// <summary>Gets or sets the user name for this user.</summary>
string UserName { get; set; }
}
Implementation 1, this one is part of my database entities:
public class ApplicationUser : IdentityUser<string>, IApplicationUser
{
public DateTime? LockoutEndDateUtc { get; set; }
public bool RequiresPasswordCreation { get; set; }
public string TemporaryToken { get; set; }
}
Implementation 2, for my .NET Framework projects:
public class ApplicationUserMvc : IdentityUser<string, ApplicationUserLogin, ApplicationUserRole, ApplicationUserClaim>, IApplicationUser
{
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUserMvc, string> manager)
{
// Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
// Add custom user claims here
return userIdentity;
}
...all the other things
}
I then created my own Identity Manager (interface)
public interface IIdentityManager
{
//User manager methods
Task<IIdentityResult> CreateAsync(IApplicationUser user);
//..all methods needed
}
public interface IIdentityResult
{
bool Succeeded { get; set; }
List<string> Errors { get; set; }
}
Then my actual implementations of this, so this one is for my .NET Core projects.
public class IdentityManagerCore : IIdentityManager
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly RoleManager<ApplicationRole> _roleManager;
public IdentityManagerCore(UserManager<ApplicationUser> userManager, RoleManager<ApplicationRole> roleManager)
{
_userManager = userManager;
_roleManager = roleManager;
}
public async Task<IIdentityResult> CreateAsync(IApplicationUser user)
{
ApplicationUser realUser = new ApplicationUser()
{
Id = user.Id,
TemporaryToken = user.TemporaryToken,
AccessFailedCount = user.AccessFailedCount,
ConcurrencyStamp = user.ConcurrencyStamp,
Email = user.Email,
EmailConfirmed = user.EmailConfirmed,
LockoutEnabled = user.LockoutEnabled,
LockoutEnd = user.LockoutEnd,
NormalizedEmail = user.NormalizedEmail,
NormalizedUserName = user.NormalizedUserName,
PasswordHash = user.PasswordHash,
PhoneNumber = user.PhoneNumber,
PhoneNumberConfirmed = user.PhoneNumberConfirmed,
RequiresPasswordCreation = user.RequiresPasswordCreation,
SecurityStamp = user.SecurityStamp,
TwoFactorEnabled = user.TwoFactorEnabled,
UserName = user.UserName
};
var result = await _userManager.CreateAsync(realUser);
return ConvertToInterface(result);
}
private IIdentityResult ConvertToInterface(IdentityResult result)
{
IIdentityResult realResult = new IdentityResultCore();
realResult.Succeeded = result.Succeeded;
realResult.Errors = result.Errors?.Select(x => x.Description).ToList();
return realResult;
}
}
public class IdentityResultCore : IdentityResult, IIdentityResult
{
private IEnumerable<string> _errors;
private bool _succeed;
public new bool Succeeded
{
get => base.Succeeded || _succeed;
set => _succeed = value;
}
public new List<string> Errors
{
get => base.Errors?.Select(x => x.Description).ToList() ?? _errors?.ToList();
set => _errors = value;
}
}
The UserManager
and RoleManager
are injected on startup like so:
services.AddTransient<IIdentityManager, IdentityManagerCore>();
The .NET Framework implementation:
public class IdentityManagerMvc : IIdentityManager
{
private readonly UserManager<ApplicationUserMvc, string> _userManager = null;
private readonly RoleManager<ApplicationRoleMvc, string> _roleManager = null;
internal static IDataProtectionProvider DataProtectionProvider { get; private set; }
public IdentityManagerMvc(DatabaseContextMvc dbContext)
{
var userStore =
new UserStore<ApplicationUserMvc, ApplicationRoleMvc, string, ApplicationUserLogin, ApplicationUserRole, ApplicationUserClaim>(dbContext);
var roleStore = new RoleStore<ApplicationRoleMvc, string, ApplicationUserRole>(dbContext);
_userManager = new UserManager<ApplicationUserMvc, string>(userStore);
_roleManager = new RoleManager<ApplicationRoleMvc, string>(roleStore);
_userManager.UserValidator = new UserValidator<ApplicationUserMvc>(_userManager) { AllowOnlyAlphanumericUserNames = false };
if (DataProtectionProvider == null)
{
DataProtectionProvider = new MachineKeyProtectionProvider();
}
_userManager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUserMvc, string>(DataProtectionProvider.Create("Identity"));
}
public async Task<IIdentityResult> CreateAsync(IApplicationUser user)
{
ApplicationUserMvc realUser = new ApplicationUserMvc()
{
Id = user.Id,
TemporaryToken = user.TemporaryToken,
AccessFailedCount = user.AccessFailedCount,
ConcurrencyStamp = user.ConcurrencyStamp,
Email = user.Email,
EmailConfirmed = user.EmailConfirmed,
LockoutEnabled = user.LockoutEnabled,
LockoutEnd = user.LockoutEnd,
NormalizedEmail = user.NormalizedEmail,
NormalizedUserName = user.NormalizedUserName,
PasswordHash = user.PasswordHash,
PhoneNumber = user.PhoneNumber,
PhoneNumberConfirmed = user.PhoneNumberConfirmed,
RequiresPasswordCreation = user.RequiresPasswordCreation,
SecurityStamp = user.SecurityStamp,
TwoFactorEnabled = user.TwoFactorEnabled,
UserName = user.UserName
};
var result = await _userManager.CreateAsync(realUser);
return ConvertToInterface(result);
}
private IIdentityResult ConvertToInterface(IdentityResult result)
{
IIdentityResult realResult = new IdentityResultMvc();
realResult.Succeeded = result.Succeeded;
realResult.Errors = result.Errors?.ToList();
return realResult;
}
}
public class IdentityResultMvc : IdentityResult, IIdentityResult
{
private IEnumerable<string> _errors;
private bool _succeed;
public new bool Succeeded
{
get => base.Succeeded || _succeed;
set => _succeed = value;
}
public new List<string> Errors
{
get => base.Errors?.ToList() ?? _errors?.ToList();
set => _errors = value;
}
}
Last but not least you would need a seperate DatabaseContext
for your .NET Framework project, this one will only be used for "identity" purposes, so not to actually query any data, just for authentication, authorization.
public class DatabaseContextMvc : IdentityDbContext<ApplicationUserMvc, ApplicationRoleMvc, string, ApplicationUserLogin,
ApplicationUserRole, ApplicationUserClaim>
{
public DatabaseContextMvc() : base("DatabaseContext")
{
Configuration.LazyLoadingEnabled = false;
Configuration.ProxyCreationEnabled = false;
//Database.SetInitializer<DatabaseContextMvc>(null);
}
public void SetTimeout(int minutes)
{
this.Database.CommandTimeout = minutes * 60;
}
public static DatabaseContextMvc Create()
{
return new DatabaseContextMvc();
}
}
At this moment you should have all the classes necessary to use it everywhere.
So for example, in your .NET Framework project, you can have your ApplicationUserManager
like so:
public class ApplicationUserManager : UserManager<ApplicationUserMvc, string>
{
public ApplicationUserManager(IUserStore<ApplicationUserMvc, string> store)
: base(store)
{//look at where i used applicationUserMvc
}
public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context)
{
var userStore =
new UserStore<ApplicationUserMvc, ApplicationRoleMvc, string, ApplicationUserLogin, ApplicationUserRole, ApplicationUserClaim>(context.Get<DatabaseContextMvc>());
//ApplicationUserLogin,UserRole,UserClaim are self created but just override IdentityUserLogin (for example).
var manager = new ApplicationUserManager(userStore);
}
...
}
In whatever Dependency Injection you use in .NET Framework, make sure to Register both your DatabaseContext
and DatabaseContextMvc
.
Here is my DatabaseContext
which is inside the .NET Standard library and used across .NET Core and .NET Framework:
public class DatabaseContext : IdentityDbContext<ApplicationUser, ApplicationRole, string, ApplicationUserClaim, ApplicationUserRole, ApplicationUserLogin, ApplicationRoleClaim, ApplicationUserToken>
{
private IConfiguration _config;
public string ConnectionString { get; }
public DatabaseContext(IConfiguration config) : base()
{
_config = config;
var connectionString = config.GetConnectionString("DatabaseContext");
ConnectionString = connectionString;
}
public void SetTimeout(int minutes)
{
Database.SetCommandTimeout(minutes * 60);
}
public virtual DbSet<Address> Addresses { get; set; }
public static DatabaseContext Create(IConfiguration config)
{
return new DatabaseContext(config);
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
//setup code for the DbContextOptions
optionsBuilder
.UseSqlServer(ConnectionString,
providerOptions => providerOptions.CommandTimeout(60))
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
base.OnConfiguring(optionsBuilder);
}
your focus is probably already gone after this long post or after implementing, but A few more words:
You will have to do it the-netcore-way for settings: so you need appsettings.json in your .NET Framework project as well. Also you still need it in the Web.Config, this is mostly for the small part of authentication.
private IConfiguration GetConfiguartion()
{
var path = Server.MapPath("~/");
var builder = new ConfigurationBuilder()
.SetBasePath(path)
.AddJsonFile("appsettings.json");
return builder.Build();//inject the return IConfiguration in your DI
}
Good luck. If you think this is difficult and causes a lot of trouble: correct, if you have a small application you are better of converting everything to .NET Core / .NET Standard.