首页 > 解决方案 > 如何在具有标识和干净架构的 EF Core 中定义一对零或一关系?

问题描述

我正在使用带有身份验证/授权的 EF Core 5 在 .NET 5.0 中开发 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
    ...
  }
}

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

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

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

标签: asp.netentity-framework-coreasp.net-core-identityclean-architecture

解决方案


你有两个问题要解决。

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

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

1 - 要将 EF 中的分层关系映射到每个对象与自己的表,您应该使用Table Per Type (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应该从 2.4继承IdentityUser<Guid>并实施。IUser
创建IUserResolveron MyApp.Identity
2.4.1 的具体实现而不是 return IdentityUserreturnIUser
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();
    }
}

有用的链接:

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


推荐阅读