Separating ASP.NET IdentityUser from the other entities

asp.net-mvcentity-frameworkidentity

I have a ProjectName.Core library containing all my business logic and my entities and their behaviour. There's currently no relation whatsoever to Entity Framework or any other DAL because I like to keep those things seperated. The Entity Framework configurations (using the Fluent API) reside in a ProjectName.Infrastructure project so that takes care of pushing my entities into EF. Basically I'm going in the direction of an Onion-like architecture.

However, when adding the ASP.NET Identity framework into the mix, I have to make my ApplicationUser entity inherit from the IdentityUser class but my ApplicationUser class has relations to other entities. In inheriting from IdentityUser I'm introducing a reference to Entity Framework in my entities project, the one place where I wouldn't want to do so. Pulling the ApplicationUser class out of the entities project and into the Infrastructure project (since it is using an Entity Framework based identity system) will result in circular references, so that isn't the way to go either.

Is there any way around this so I can keep the clean seperation between the two layers besides not using ASP.NET Identity?

Best Answer

You can create a User class that has nothing to do with ASP.NET Identity in your core library.

public class User {
    public Guid UserId { get; set; }
    public string UserName { get; set; }
    public string EmailAddress { get; set; }
    public string EmailAddressConfirmed { get; set; }
    public string PhoneNumber { get; set; }
    public string PhoneNumberConfirmed { get; set; }
    public string PasswordHash { get; set; }
    public string SecurityStamp { get; set; }

    ...

    public virtual ICollection<Role> Roles { get; set; }
    public virtual ICollection<UserClaim> UserClaims { get; set; }
    public virtual ICollection<UserLogin> UserLogins { get; set; }
}

If you're using Entity Framework, create a configuration class for your entities (optional).

internal class UserConfiguration : EntityTypeConfiguration<User>
{
    internal UserConfiguration()
    {
        ToTable("User");

        HasKey(x => x.UserId)
            .Property(x => x.UserId)
            .HasColumnName("UserId")
            .HasColumnType("uniqueidentifier")
            .IsRequired();

        Property(x => x.PasswordHash)
            .HasColumnName("PasswordHash")
            .HasColumnType("nvarchar")
            .IsMaxLength()
            .IsOptional();

        Property(x => x.SecurityStamp)
            .HasColumnName("SecurityStamp")
            .HasColumnType("nvarchar")
            .IsMaxLength()
            .IsOptional();

        Property(x => x.UserName)
            .HasColumnName("UserName")
            .HasColumnType("nvarchar")
            .HasMaxLength(256)
            .IsRequired();

        // EmailAddress, PhoneNumber, ...

        HasMany(x => x.Roles)
            .WithMany(x => x.Users)
            .Map(x =>
            {
                x.ToTable("UserRole");
                x.MapLeftKey("UserId");
                x.MapRightKey("RoleId");
            });

        HasMany(x => x.UserClaims)
            .WithRequired(x => x.User)
            .HasForeignKey(x => x.UserId);

        HasMany(x => x.UserLogins)
            .WithRequired(x => x.User)
            .HasForeignKey(x => x.UserId);
    }
}

You'd have to also create classes for Role, UserClaim, and UserLogin. You can name them whatever you choose if you don't like the above names.

In the web layer, create a class called AppUser (Or another name if you choose). This class should implement the ASP.NET Identity IUser<TKey> interface, where TKey is the data type for the primary key (Guid in the above example).

public class AppUser : IUser<Guid>
{
    public AppUser()
    {
        this.Id = Guid.NewGuid();
    }

    public AppUser(string userName)
        : this()
    {
        this.UserName = userName;
    }

    public Guid Id { get; set; }
    public string UserName { get; set; }
    public string EmailAddress { get; set; }
    public string EmailAddressConfirmed { get; set; }
    public string PhoneNumber { get; set; }
    public string PhoneNumberConfirmed { get; set; }
    public string PasswordHash { get; set; }
    public string SecurityStamp { get; set; }
}

Change all references to UserManager in the web project to UserManager<AppUser, Guid>.

Finally, create your own UserStore. Essentially, the custom UserStore will take in the AppUser object, convert it to an User entity object, then persist it. An example of one of these methods is shown below:

public class UserStore : 
    IUserLoginStore<AppUser, Guid>, 
    IUserClaimStore<AppUser, Guid>, 
    IUserRoleStore<AppUser, Guid>, 
    IUserPasswordStore<AppUser, Guid>, 
    IUserSecurityStampStore<AppUser, Guid>, 
    IUserStore<AppUser, Guid>, 
    IDisposable
{
    private User MapFromAppUser(AppUser appUser)
    {
        if (appUser == null)
            return null;

        var userEntity = new User();

        PopulateUser(userEntity, appUser);

        return userEntity;
    }

    private void PopulateUser(User user, AppUser appUser)
    {
        user.UserId = appUser.Id;
        user.UserName = appUser.UserName;
        user.EmailAddress = appUser.EmailAddress;
        user.EmailAddressConfirmed = appUser.EmailAddressConfirmed;
        user.PhoneNumber = appUser.PhoneNumber;
        user.PhoneNumberConfirmed = appUser.PhoneNumberConfirmed;
        user.PasswordHash = appUser.PasswordHash;
        user.SecurityStamp = appUser.SecurityStamp;

        // First name, last name, ... 
    }

    #region IUserStore<AppUser, Guid> Members

    public Task CreateAsync(AppUser appUser)
    {
        if (appUser == null)
            throw new ArgumentNullException("appUser");

        var userEntity = MapFromAppUser(appUser);

        // Persist the user entity to database using a data repository.
        // I'll leave this to you.
    }

    ...

    #endregion
}

To get a full description of a possible implementation, click here.

In the end, it's your choice. Measure the amount of effort it would take for you to maintain this implementation versus just referencing the Identity framework in your Core library. Personally, I thought of doing it the way I described above, however, I didn't because I'd potentially have to change my code every time the ASP.NET Identity framework is updated.

Hopefully this helps and answers your question!