I've recently started messing with the Clean Architecture concept while using .NET 5. The problem I am running into is that although Identity is well isolated in the Infrastructure layer, if I want to use additional properties on the user without mixing that data with the AspNetUsers table, the problems start.
I will have to create a new entity in the Domain layer, for example UserProfile, and figure out a way to relate that entity to the ApplicationUser entity without
- including an ApplicationUser reference in my new entity
- moving the ApplicationUser entity to the Domain layer and reference Identity namespaces in there.
In all Clean Architecture projects this situation is either not addressed (they simply add new properties into ApplicationUser) or the solution given is to include a reference to the Identity User Id in the new entity.
I don't like the first solution since it forces me to add custom fields to an Identity table. The second solution brings new and complex problems since it will force me to manually synchronize 2 tables in case of creation or deletion of data, also forcing the use of transactions to avoid orphan data in case of synchronization failure. It also brings an abstraction issue since deleting a user using only Identity will not take care automatically of unknown entities (from future plugins) that might depend or relate somehow to AspNetUsers table.
Let me be more concrete. Here's my simplified project structure (using .NET core 5):
Application
- Interface
- IApplicationDbContext.cs
Core
- Domain
- UserProfile.cs (my custom user class with additional properties)
Infrastructure
- Data
- ApplicationDbContext.cs (Interface implementation)
- Identity
- ApplicationRole.cs
- ApplicationUser.cs DependencyInjection.cs
Web
my web application
Here's my code:
IApplicationDbContext.cs
namespace Application.Common.Interfaces
{
public interface IApplicationDbContext
{
Task<int> SaveChangesAsync(CancellationToken cancellationToken);
}
}
UserProfile.cs
namespace Core.Domain
{
public class UserProfile : AuditEntity
{
// Don't want to reference ApplicationUser in domain.
// [ForeignKey("Id")]
//public virtual ApplicationUser User { get; set; }
[StringLength(50)]
public string FirstName { get; set; }
[StringLength(50)]
public string LastName { get; set; }
[StringLength(50)]
public string JobPosition { get; set; }
[StringLength(50)]
public string Photo { get; set; }
[DefaultValue(true)]
public bool IsListed { get; set; }
[DefaultValue(false)]
public bool IsDisabled { get; set; }
[DefaultValue(false)]
public bool IsLocked { get; set; }
public DateTime? LastLoginDate { get; set; }
public DateTime? LastActivityDate { get; set; }
[StringLength(100)]
public string LastSessionId { get; set; }
[StringLength(256)]
public string PrivateFolder { get; set; }
public bool IsOnApproval { get; set; } = true;
public bool IsDeleted { get; set; } = false;
}
}
ApplicationDbContext.cs
namespace Infrastructure.Data
{
public partial class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, int>, IApplicationDbContext
{
public ApplicationDbContext(DbContextOptions options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfiguration(new Configurations.UserConfiguration());
modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
}
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken)
{
var entries = ChangeTracker.Entries().Where(x => x.Entity is AuditEntity && (x.State == EntityState.Added || x.State == EntityState.Modified));
foreach (var entry in entries)
{
if (entry.State == EntityState.Added)
{
((AuditEntity)entry.Entity).CreatedBy = _currentUserService.UserId;
((AuditEntity)entry.Entity).CreatedDate = DateTime.UtcNow;
}
((AuditEntity)entry.Entity).LastModifiedBy = _currentUserService.UserId;
((AuditEntity)entry.Entity).LastModifiedDate = DateTime.UtcNow;
}
return base.SaveChangesAsync(cancellationToken);
}
}
}
ApplicationRole.cs
namespace Infrastructure.Identity
{
public class ApplicationRole : IdentityRole<int> //, ISoftDeletable
{
public ApplicationRole() : base() { }
public ApplicationRole(string name) : base(name) { }
}
}
ApplicationUser.cs
namespace Infrastructure.Identity
{
public class ApplicationUser: IdentityUser<int>
{
}
}
DependencyInjection.cs
namespace Infrastructure
{
public static class DependencyInjection
{
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration config)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
config.GetConnectionString("eCMConnection"),
context => context.MigrationsAssembly(Assembly.GetExecutingAssembly().FullName)));
services.AddIdentity<ApplicationUser, ApplicationRole>(
options =>
{
options.Password.RequireDigit = true;
options.Password.RequiredLength = 8;
options.Password.RequireNonAlphanumeric = true;
options.SignIn.RequireConfirmedAccount = false;
}
)
.AddRoleManager<RoleManager<ApplicationRole>>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders(); // two factor authentication
services.AddScoped<IApplicationDbContext>(provider => provider.GetService<ApplicationDbContext>());
services.AddTransient<IEmailService, EmailService>();
return services;
}
}
}
So, my question is, what is the best approach to be able to have a custom UserProfile entity with data stored in a table related to AspNetUsers table without having to reference ApplicationUser and Identity in UserProfile domain entity. What is the best approach to be able to create and delete Identity users but also related data from my UserProfile table. As anyone seen an example of this implemented?
Thanks! Hope someone can throw some light into this.