How Do I Define One-to-Zero-Or-One Relationships in EF Core with Identity and Clean Architecture?

Viewed 289

I'm developing an ASP.NET Core Razor Pages application in .NET 5.0 using EF Core 5 with Identity for authentication/authorisation and I'm attempting to do this with a 'Clean Architecture'.

The application implements 3 'types' of user: Supervisors, Owners and Staff. Each are separate entities and so are persisted in their own db tables. Each user type has further related tables not shown here.

ApplicationUser (the principle entity, stored in the AspNetUsers table) needs a one-to-zero-or-one relationship to each of these user 'types' (the dependent entities):

//
// Infrastructure
//
namespace MyApp.Identity
{
  public class ApplicationUser : IdentityUser<Guid>
  {
    ...
  }
}

//
// Core
//
namespace MyApp.Domain.Entities
{
  public class Supervisor
  {
    public int Id { get; set; } // PK
    public string Department { get; set; }
    
    public Guid UserId { get; set; } // FK
    ...
  }

  public class Owner
  {
    public int Id { get; set; } // PK
    public string Company { get; set; }

    public Guid UserId { get; set; } // FK
    ...
  }

  public class Staff
  {
    public int Id { get; set; } // PK
    public string Phone { get; set; }
    public string PostCode { get; set; }
    public string Notes { get; set; }

    public Guid UserId { get; set; } // FK
    ...
  }
}

ApplicationUser is in the Identity project within the Infrastructure layer as it is implementation specific (as it inherits from IdentityUser).

To avoid going against the clean architecture principles, I shouldn't allow EF Core to automatically setup the relationships by convention because I believe that would require the addition of an ApplicationUser navigation property to the Staff entity, which in turn would require a reference to the infrastructure layer?

Hence, it appears I need to use Fluent API but how do I define these one-to-zero-or-one relationships without having the navigation properties please?

1 Answers

You have two probelms to sove here.

First one is how to map the relationships in your database to your application model without navigation properties.

Second is how to map the relationship between IdentityUser and other specific users in your model, keeping MyApp.Identity isolated from your model.

1 - To map a hieraquial relationship in EF and each object with their own table, yout should use Table Per Type (TPT) configuration. I'll not explain it here, but I will leave an example code bellow. If need more information about TPT look the links at the end of my answer. And be aware that TPT in many cases shows inferior performance
1.1 You will have to create a base class BaseUser and all other user types should inhiret from it.
1.1.1 BaseUser also implements IUser it will be explained on item 2.
1.2 The Id property of each child of BaseUser will be PK and FK.

2 - This kind of isolation you want is easly acomplished with IoC (Inversion of Control) and DI (Dependency Injection).

2.1 Begin creating a interface that contains base information about your user, and is needed for all kinds of user. You should put at least the Id property their.
2.2 Create an interface in your application layer to resolve IUser , I'll call it IUserResolver
2.2.1 For the example case IUserResolver will only contains one method, but you can have as many you want.
2.2.2 Just remember to do not return any object from Identity, you should have your own types to represent the information you want to expose. 2.3. Your ApplicationUser should inhiret from IdentityUser<Guid> and implements IUser
2.4. Create the concrete implemantation of IUserResolver on MyApp.Identity
2.4.1 Instead of return IdentityUser return IUser
2.5 On your IoC layer map the IUserResolver to UserResolver

Now anytime you need the currentUser on your application you junt need to get it from IUserResolver. using DI. The best part of it, is when next version of AspNet.Identity be released and we get some breaking changes, all your Identity logic is isolated in just one project. Believe me, will have breking changes.

I have a few solutions like this one in production, and it works like a charm.

//
// Infrastructure
//
namespace MyApp.Identity
{
  public class ApplicationUser : IdentityUser<Guid>, IUser
  {
    ...
  }
  
  public class UserResolver : IUserResolver
  {
    private readonly UserManager<ApplicationUser> _userManager;
        private readonly ClaimsPrincipal _user;


        public UserResolver(UserManager<ApplicationUser> userManager, IHttpContextAccessor accessor)
        {
            _user = accessor.HttpContext.User;
            _userManager = userManager;
        }

        public async Task<IUser> GetCurrentUserAsync()
        {
            if (!_user.Identity.IsAuthenticated)
                return null;
            
            var userId = // get id from _user;
            return await _userManager.FindByIdAsync(userId);
        }
        ...
  }
}

///
/// IoC
///
   services.AddScoped<IUserResolver, UserResolver>();

//
// Core
//
namespace MyApp.Domain.Contracts
{
  public interface IUser
  {
     public Guid Id {get; set;}
     // ... Other commom properties between IdentityUser & BaseUser.
  }
  
  public interface IUserResolver
  {
      Task<IUser> GetCurrentUserAsync();
  }
}

namespace MyApp.Domain.Entities
{

  public class BaseUser : IUser
  {
     public Guid Id {get; set;} // Required
     public string UserName {get; set;} // Optional
     // ... Other optional properties. 
     // Avoid map specifc Identity properties
  }

  public class Supervisor : BaseUser
  {
    public Guid Id { get; set; } // PK & FK
    public string Department { get; set; }
    
    ...
  }

  public class Owner : BaseUser
  {
    public Guid Id { get; set; } // PK & FK
    public string Company { get; set; }

    ...
  }

  public class Staff : BaseUser
  {
    public Guid Id { get; set; } // PK & FK
    public string Phone { get; set; }
    public string PostCode { get; set; }
    public string Notes { get; set; }
    ...
  }
}

///
/// EF Mapping
///
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.Entity<BaseUser>().ToTable("AspNetUsers");
    modelBuilder.Entity<Supervisor>().ToTable("Supervisor");
    modelBuilder.Entity<Owner>().ToTable("Owner");
    modelBuilder.Entity<Staff>().ToTable("Staff");
}

///
/// Handle this data
///
public SetUserAsSupervisor(IUser baseUser, string department)
{
    using (var context = new EntityContext())
  {
    Supervisor supervisor = (Supervisor)baseUser;
    supervisor.Department = "Marketing";
    
        context.BaseUser.Update(supervisor);
        context.SaveChanges();
    }
}

public SetUserAsOwner(IUser baseUser, string company)
{
    using (var context = new EntityContext())
  {
    Owner onwer = (Owner)baseUser;
    onwer.Company = "Microsoft";
    
        context.BaseUser.Update(onwer);
        context.SaveChanges();
    }
}

Usefull Links:

TPT

https://docs.microsoft.com/en-us/ef/core/modeling/inheritance#table-per-type-configuration
https://entityframework.net/tpt
https://www.entityframeworktutorial.net/code-first/inheritance-strategy-in-code-first.aspx
https://www.learnentityframeworkcore5.com/whats-new-in-ef-core-5/table-per-type-tpt-mapping

Related