We have two databases with User tables like below:
We use MasterDb to log-in the users and then we connect them to a customer database, for example Customer1DB (or some other customer db depending on the user). Both user tables have identical schemas based on ASP.NET Identity Framework and we can use IdentityManager to manage users on MasterDb.
Now what I want is any changes performed on a MasterDb user record, mirror that to the users table on the customer's database (with same user Id). I was wondering what is the best way to do this? Do I need to modify all operations on UserStore
, RoleStore
, UserManager
, RoleManager
? I have a function that gets a UserId and adds or updates it from the first db to the second db, but I'm not sure how exactly I should integrate it in the Identity Framework implementations.
Thanks!
I think you have 2 options:
Create adapters for the stores and functions you need. The adapters will call the same method on both databases.
In the Master DbContext, when you SaveChanges, you look at the ChangeTracker and duplicate the changes to the other DbContext.
Each method has is advantages and inconveniences.
Sample adapter:
class MultiDatabaseUserStore<T> : IUserStore<T>
where T : IdentityUser
{
private readonly UserStore<T>[] stores;
public MultiDatabaseUserStore(params IdentityDbContext[] dbContexts)
{
if (dbContexts == null || dbContexts.Length <= 0)
{
throw new ArgumentException("At least one db context is required.", "dbContexts");
}
this.stores = dbContexts.Select(x => new UserStore<T>(x)).ToArray();
}
public void Dispose()
{
foreach (var store in this.stores)
{
store.Dispose();
}
}
public Task CreateAsync(T user)
{
return this.ExecuteOnAll(x => x.CreateAsync(user));
}
public Task UpdateAsync(T user)
{
return this.ExecuteOnAll(x => x.UpdateAsync(user));
}
public Task DeleteAsync(T user)
{
return this.ExecuteOnAll(x => x.DeleteAsync(user));
}
public Task<T> FindByIdAsync(string userId)
{
return this.stores.First().FindByIdAsync(userId);
}
public Task<T> FindByNameAsync(string userName)
{
return this.stores.First().FindByNameAsync(userName);
}
private Task ExecuteOnAll(Func<UserStore<T>, Task> function)
{
return Task.WhenAll(this.stores.Select(function));
}
}
And it integrates like this:
var store = new MultiDatabaseUserStore<IdentityUser>(new MasterDb(), new Customer1DB());
var userManager = new UserManager<IdentityUser>(store);
This works but depending on your needs you will have a lot more interfaces to implement:
Sample Master DbContext:
class MasterDb : IdentityDbContext
{
public MasterDb()
{
}
public override int SaveChanges()
{
var changes = this.GetChangesToReplicate();
var i = base.SaveChanges();
this.SaveReplicatedChanges(changes);
return i;
}
public override Task<int> SaveChangesAsync()
{
return Task.Run(async () =>
{
var changes = this.GetChangesToReplicate();
var i = await base.SaveChangesAsync();
this.SaveReplicatedChanges(changes);
return i;
});
}
private void SaveReplicatedChanges(IEnumerable<Action<IdentityDbContext>> changes)
{
if (changes != null)
{
using (var db = new Customer1DB())
{
foreach (var change in changes)
{
change(db);
}
db.SaveChanges();
}
}
}
private IEnumerable<Action<IdentityDbContext>> GetChangesToReplicate()
{
var actions = new List<Action<IdentityDbContext>>();
var userTye = typeof(IdentityUser);
var changedEntries = this.ChangeTracker.Entries();
var users = changedEntries.Where(x => x.Entity.GetType() == userTye).ToList();
foreach (var u in users)
{
switch (u.State)
{
case EntityState.Added:
var userToAdd = (IdentityUser)u.Entity;
actions.Add(db => db.Users.Add(userToAdd));
break;
case EntityState.Modified:
var userToUpdate = (IdentityUser)u.Entity; ;
actions.Add(db =>
{
db.Users.Attach(userToUpdate);
db.Entry(userToUpdate).State = EntityState.Modified;
});
break;
case EntityState.Deleted:
var userToDelete = (IdentityUser)u.Entity; ;
actions.Add(db =>
{
db.Users.Attach(userToDelete);
db.Entry(userToDelete).State = EntityState.Deleted;
});
break;
}
}
return actions;
}
}
This also works but it's limited to the user entity. You will have to add replication of roles, claims,... depending on your needs.
Thinking about it, you can skip the type check and just replicated all changes to the other DbContext. This simplifies a lot the code and it will work for roles/claims. You would also need to add a bool property IsReplicatingChanges
to enable the replication only when necessary.
You will have to create an instance only for the identity Stores and set the flag to true
:
var dbContext = new MasterDb { IsReplicatingChanges = true };
var userManager = new UserManager<IdentityUser>(new UserStore<IdentityUser>(dbContext));
var roleManager = new RoleManager<IdentityRole>(new RoleStore<IdentityRole>(dbContext));
And GetChangesToReplicate()
becomes:
private IEnumerable<Action<IdentityDbContext>> GetChangesToReplicate()
{
if (!this.IsReplicatingChanges) // Flag
{
// Return null when not activated
return null;
}
var actions = new List<Action<IdentityDbContext>>();
var changedEntries = this.ChangeTracker.Entries().Where(x => x.State != EntityState.Unchanged);
foreach (var u in changedEntries)
{
var entity = u.Entity;
var state = u.State;
actions.Add(db =>
{
db.Set(entity.GetType()).Attach(entity);
db.Entry(entity).State = state;
});
}
return actions;
}
And don't forget to add the null check to SaveReplicatedChanges()
.