Search code examples
c#asp.net-mvcentity-frameworkoopasp.net-identity

Add relationship to .NET Identity ApplicationUser for entity in Data layer


I'm building an .NET MVC application with Identity for user login. The application has four Layers.

  • Web/UI layer, has reference to my domain and service layers
  • Domain layer, has no references
  • Service layer, has reference to my data and domain layers
  • Data layer, has reference to my domain layer

I want my AppUser class to have a reltionship to one of my entities in my data layer, Company.

The AppUser class is residing in my domain layer and it is inheriting from IdentityUser. The reason i placed my AppUser class in my domain layer is beacuse i need to have access to it from my Web/UI layer where the IdentityConfig needs the class.

But when i now want to add a relationhip to an entity in my data layer things got tricky for me.

AppUser Class as i want it to look:

using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using System.Security.Claims;
using System.Threading.Tasks;

namespace ITKA.Domain.Classes
{
    public class AppUser : IdentityUser
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string ContactNumber { get; set; }
        public int CompanyId { get; set; }
        public Company Company { get; set; }

        public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<AppUser> manager)
        {
            var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
            return userIdentity;
        }

        public void Dispose()
        {

        }
    }
}

Company Class as it looks:

using ITKA.Data.Interfaces;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ITKA.Data.Entities
{
    [Table("CompanyTbl")]
    public class Company : IEntity
    {
        [Key]
        public int Id { get; set; }

        [StringLength(100)]
        public string CompanyName { get; set; }

        public virtual ICollection<InventoryItem> InventoryItems { get; set; }
    }
}

IdentityConfig Class as it looks

using System;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin;
using Microsoft.Owin.Security;
using ITKA.Domain.Classes;
using ITKA.Domain.Interfaces;

namespace ITKA.Web
{
    public class AppUserManager : UserManager<AppUser>
    {
        public AppUserManager(IUserStore<AppUser> userStore)
            : base(userStore)
        {
        }

        public static AppUserManager Create(IdentityFactoryOptions<AppUserManager> options, IOwinContext context) 
        {
            var userStore = new UserStore<AppUser>((IdentityDbContext<AppUser>)context.Get<IITKAContext>());
            var manager = new AppUserManager(userStore);

            manager.UserValidator = new UserValidator<AppUser>(manager)
            {
                AllowOnlyAlphanumericUserNames = false,
                RequireUniqueEmail = true
            };

            manager.PasswordValidator = new PasswordValidator
            {
                RequiredLength = 8,
                RequireNonLetterOrDigit = false,
                RequireDigit = true,
                RequireLowercase = true,
                RequireUppercase = true
            };

            manager.UserLockoutEnabledByDefault = false;
            manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5);
            manager.MaxFailedAccessAttemptsBeforeLockout = 5;

            return manager;
        }
    }

    public class AppSignInManager : SignInManager<AppUser, string>
    {
        public AppSignInManager(AppUserManager userManager,
            IAuthenticationManager authManager)
            : base(userManager, authManager) { }

        public static AppSignInManager Create
            (IdentityFactoryOptions<AppSignInManager> options, IOwinContext context)
        {
            return new AppSignInManager(context.Get<AppUserManager>(),
                context.Authentication);
        }
    }

    public class AppRoleManager : RoleManager<IdentityRole>
    {
        public AppRoleManager(RoleStore<IdentityRole> roleStore)
            : base(roleStore) { }

        public static AppRoleManager Create(IOwinContext context)
        {
            var roleStore = new RoleStore<IdentityRole>
                ((IdentityDbContext<AppUser>)context.Get<IITKAContext>());

            return new AppRoleManager(roleStore);
        }
    }
}

I dont know how to be able to place the AppUser class in my data layer so that IdentityConfig can be able to "reach" it since those layers has no reference to each other.

I tried to move my AppUser class to my data layer and made it use an interface i added to my domain layer, IAppUser. I changed IdentityConfig to use my interface instead of my class but it gave me alot of squiggliez.

First issue i ran into was that there where no implicit conversion between IAppUser and IUser, so i made my IAppUser interface inherit from IUser and it solved some problems in the IdentityConfig class.

But in some places it still complained that there is no implicit conversion between IAppUser and the class IdentityUser. I cant make my interface inherit from a class so there that experiment ended.

I dont know if there is a way to add a relationship to my AppUser class to an entity that is in my data layer through an interface in my domain layer maybe? This feels like it wouldn't work to me because i think that EF needs to know implicitly what class/entity it makes a relationship to. And since many diffrent classes kan inherit from an interface this feels impossible to me. Am i wrong?

Or could i have in my AppUser class a property with an interface and EF would create an relationship? If my Company class would use an interface named ICompany and my AppUser class have a property like:

public ICompany Company { get; set; }

would it create a relationship to Company?

I had trouble when IdentityConfig needed my DbContext class that also resides in my data layer but that problem i was able to solve by using an interface in my domain layer.

Im really stuck here and i dont know how to solve this problem. I am thankful for any help.


Solution

  • I see you have a problem with circular references at first if you add data layer to your domain layer references. You don't want Domain and Data to reference eachother.

    Remove this reference:

    • Data layer, has reference to my domain layer

    And reference the Data Layer in your Domain Layer:

    • Domain layer, has no references

    It should now look something like this.

    Web -> Domain  -> Data
    
        -> Service -> Data
                   -> Domain
    
    1. Web layer references Domain and Service layers.
    2. Domain references the Data layer.
    3. Service layer references the Data and Domain layers

    Please note Data Layer is the lowest reference, so the Data Layer will not have any references.

    Keep your AppUser class in the Domain Layer.

    Because of point #2, Domain layer references the Data layer, you can now use the Company entity.

    AppUser.cs

    using ITKA.Data.Entities;
    //or just use ITKA.Data.Entities.Company
    
    namespace ITKA.Domain.Classes 
    {
        public class AppUser : IdentityUser
        {
            ....
            public virtual Company Company { get; set; }
            ....
        }
    }
    

    Now for the Web layer - because of point #1, Web references the Domain layer, allowing it to access the AppUser class

    IdentityConfig.cs

    using ITKA.Domain.Classes;
    //or just use ITKA.Domain.Classes.AppUser;
    
    namespace ITKA.Web 
    {
        public class AppUserManager : UserManager<AppUser>
        {
            ....
            public virtual Company Company { get; set; }
            ....
        }
    }
    

    You most like also have to keep your IdentityDbContext in the domain layer as well, and keep entity DbContext in data layer. Both can still point to the same connection string though.

    I hope this helps.