如何使用 Identity 和 Clean 架构在 EF Core 中定义一对零或一的关系?

Yet*_*uff 5 asp.net entity-framework-core clean-architecture asp.net-core-identity

我正在 .NET 5.0 中使用 EF Core 5 和 Identity 进行身份验证/授权来开发 ASP.NET Core Razor Pages 应用程序,并且我尝试使用“干净的架构”来实现此目的。

该应用程序实现了 3 种“类型”的用户:主管、所有者和员工。每个都是单独的实体,因此保留在自己的数据库表中。每个用户类型都有进一步的相关表,此处未显示。

ApplicationUser(主要实体,存储在AspNetUsers表中)需要与每个用户“类型”(依赖实体)建立一对零或一的关系:

//
// 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
    ...
  }
}
Run Code Online (Sandbox Code Playgroud)

ApplicationUser位于Identity基础设施层内的项目中,因为它是特定于实现的(因为它继承自IdentityUser)。

ApplicationUser为了避免违背干净的架构原则,我不应该允许 EF Core 按惯例自动设置关系,因为我认为这需要向实体添加导航属性Staff,而这又需要对基础设施层的引用?

因此,看来我需要使用 Fluent API,但是在没有导航属性的情况下如何定义这些一对零或一的关系呢?

Lut*_*lho 4

这里有两个问题需要解决。

第一个是如何在没有导航属性的情况下将数据库中的关系映射到应用程序模型。

其次是如何映射 IdentityUser 和模型中其他特定用户之间的关系,并MyApp.Identity与模型保持隔离。

1 - 要在 EF 中映射层次结构关系以及每个对象及其自己的表,您应该使用每个类型表 (TPT)配置。我不会在这里解释它,但我会在下面留下一个示例代码。如果需要有关 TPT 的更多信息,请查看我的答案末尾的链接。请注意,TPT 在许多情况下表现出较差的性能
1.1 您必须创建一个基类BaseUser,并且所有其他用户类型都应从中继承。
1.1.1BaseUser也实现了,IUser这将在第 2 项中解释。
1.2Id的每个孩子的属性BaseUser将是 PK 和 FK。

2 - 您想要的这种隔离可以通过 IoC(控制反转)和 DI(依赖注入)轻松实现。

2.1 开始创建一个界面,其中包含有关用户的基本信息,并且是所有类型的用户都需要的。你至少应该把 Id财产放在他们的身上。
2.2 在应用程序层创建一个接口来解析IUser,我将其称为IUserResolver
2.2.1 对于示例案例IUserResolver将仅包含一个方法,但您可以拥有任意多个方法。
2.2.2 请记住不要从 中返回任何对象 Identity您应该有自己的类型来表示您想要公开的信息。 2.3. 你ApplicationUser应该继承IdentityUser<Guid>并实现IUser
2.4。IUserResolver在 2.4.1上创建具体实现MyApp.Identity
而不是 return IdentityUserreturn IUser
2.5 在你的 IoC 层上映IUserResolver射到UserResolver

现在,只要您的应用程序需要 currentUser,您就不需要从 获取它IUserResolver。使用DI。最好的部分是,当AspNet.Identity发布下一版本并且我们进行一些重大更改时,您的所有Identity 逻辑都被隔离在一个项目中。相信我,将会发生重大变化。

我在生产中有一些像这样的解决方案,它的作用就像一个魅力。

//
// 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();
    }
}
Run Code Online (Sandbox Code Playgroud)

有用的链接:

TPT

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