实现整洁架构项目分层 这个案例分为Users.Domain、Users.Infrastructure、Users.WebAPI这三个项目。
Users.Domain是领域层项目,主要包含实体类、值对象、领域事件数据类、领域服务、仓储接口、防腐层接口等;
Users.Infrastructure是基础设施项目,主要包含实体类的配置、上下文类的定义、仓储服务、防腐层接口的实现、基础工具类等;
Users.WebAPI是ASP.NET Web API项目,主要包含应用服务、Controller类、领域事件处理者、数据校验、权限校验、工作单元、事务处理等代码。
Users.Domain项目对应的是整洁架构中的领域模型和领域服务,Users.Infrastructure项目对应的是整洁架构中的基础设施、数据库、外部服务,Users.WebAPI项目对应的是整洁架构中的用户界面和应用服务。
Users.Domain不依赖任何项目,Users.Infrastructure依赖于Users.Domain,而Users.WebAPI同时依赖于Users.Domain和Users.Infrastructure。因此Users.WebAPI既可以调用Users.Domain中的领域服务,也可以直接调用Users.Infrastucture。因此Users.WebAPI既可以调用Users.Domain中的领域服务,也可以直接调用Users.Infrastructure中上下文的代码,而Users.Domain则不可以直接调用Users.Infrastructure中上下文的代码,而Users.Domain则不可以直接访问Users.Infrastructure,但是Users.Domain可以通过访问IUsersDomainRepository接口来间接调用Users.Infrastructure中的服务。
领域模型的实现 我们将实现实体类、值对象等基础的领域模型,并且识别和定义出聚合及聚合根,这是DDD的战术起点。这些代码都位于Users.Domain项目中。
作为一个用户管理系统,“用户(User)”是我们识别出的第一个实体类;“用户登录失败次数过多则锁定”这个需求并不属于“用户”这个实体类中的一个常用的特征,我们应当把它拆分到一个单独的实体类中,因此我们识别出一个单独的“用户登录失败”(UserAccessFail)实体类:“用户登陆记录”(UserLoginHistory)也应该识别为一个单独的实体类。User和UserAccessFail的关系是非常紧密的,UserAccessFail,因此该User和UserAccessFail不会独立于User存在,而且我们只有访问到User的时候才会访问UserAccessFail,因此该User和UserAccessFail设计为同一个聚合,并且把User设置为聚合根;由于我没有可能单独查询一段时间内的登录记录等独立与某个用户的需求,因此我们把UserLoginHistory设计为一个单独的聚合。
PhoneNumber 我们的系统中需要保存手机号,由于该系统可能被海外用户访问,而海外用户的手机号还需要包含“国家/地区码”,因此我们设计了用来表示手机号的值对象PhoneNumber。
1 2 3 4 { public record PhoneNumber (int RegionCode, int Number ) ; }
Zack.Commons
为了区别聚合根实体类和普通实体类,我们定义了不包含任何成员的标识接口IAggregateRoot,并且让所有的聚合根实体类实现这个接口。
1 2 3 public interface IAggregateRoot { }
编写UserAccessFail类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 namespace Users.Domain.Entities { internal class UserAccessFail { public Guid Id { get ; init ; } public Guid UserId { get ; init ; } public User User { get ; init ; } public bool lockOut; public DateTime? LockoutEnd { get ; private set ; } public int AccessFailCount { get ; private set ; } private UserAccessFail () { } public UserAccessFail (User user ) { Id = Guid.NewGuid(); UserId = user.Id; } public void Reset () { lockOut = false ; LockoutEnd = null ; AccessFailCount = 0 ; } public bool IsLockOut () { if (lockOut) { if (LockoutEnd >= DateTime.Now) { return true ; } else { AccessFailCount = 0 ; LockoutEnd = null ; return false ; } } else { return false ; } } public void Fail () { AccessFailCount++; if (AccessFailCount >= 5 ) { lockOut = true ; LockoutEnd = DateTime.Now.AddMinutes(5 ); } } } }
UserAccessFail类同样是充血模型,当用户登陆失败一次时,我们就调用Fail方法来记录因此登录失败,如果发现登陆失败超过3次,我们就锁定这个用户5min;我们通过IsLockOut方法判断这个账户是否已经被锁定;一旦登陆成功一次,我们就调用Reset方法来重置登录失败信息。由于实体类中进行的都是抽象操作,并不会直接进行数据库操作,因此我们编写的实体类中的Fail、Reset等方法都只是修改实体类的属性,并没有写入数据库的操作。
User 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 internal class User : IAggregateRoot { public Guid Id { get ; set ; } public PhoneNumber PhoneNumber { get ; private set ; } private string ? passwordHash; public UserAccessFail AccessFail { get ; private set ; } public User () { } public User (PhoneNumber phoneNumber ) { Id = Guid.NewGuid(); PhoneNumber = phoneNumber; AccessFail = new UserAccessFail(this ); } public bool HasPassword () { return !string .IsNullOrEmpty(passwordHash); } public void ChangePassword (string value ) { if (value .Length <= 3 ) { throw new ArgumentException("密码长度不能小于等于3" ); } passwordHash = HashHelper.ComputeMd5Hash(value ); } public bool CheckPassword (string password ) { return passwordHash == HashHelper.ComputeMd5Hash(password); } public void ChangePhoneNumber (PhoneNumber phoneNumber ) { PhoneNumber = phoneNumber; } }
可以看到,作为聚合根,User类实现了IAggregateRoot接口,而且它是一个充血模型。密码的哈希值不应该被外界访问到,因此passwordHash被定义为私有的成员变量。由于用户可以不设置密码,而使用手机号加短信验证码登录,因此passwordHash被定义为可空的string类型,并且提供了HasPassword方法用于判断用户是否设置了密码。ChangePassword、CheckPassword两个方法分别用于修改密码和检查用户输入的密码是否正确,由于“密码采用哈希值保存”属于User类的内部实现细节,因此计算明文密码的哈希值的操作在ChangePassword、CheckPasswrod两个方法中完成。
UserAccessFail 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 internal class UserLoginHistory : IAggregateRoot { public long Id { get ; init ; } public Guid? UserId { get ; init ; } public PhoneNumber PhoneNumber { get ; init ; } public DateTime LoginTime { get ; init ; } public string Messsage { get ; init ; } private UserLoginHistory () { } public UserLoginHistory (Guid? userId, PhoneNumber phoneNumber, DateTime loginTime, string message ) { UserId = userId; PhoneNumber = phoneNumber; LoginTime = loginTime; Messsage = message; } }
如果用户输入一个系统中不存在的手机号,我们也要把他记录到日志中,因此UserId属性为可空的Guid类型。由于UserLoginHistory类是独立的聚合,而在DDD中,聚合之间只通过聚合根实体类的标识来引用,因此UserLoginHistory类中只定义UserId属性,而不是定义User属性,这样我们就把聚合之间的耦合度降低了。从逻辑上来讲,UserLoginHistory类的UserId属性是一个指向User实体类的外键,但是在物理上,我们并没有创建它们的外键关系,这不符合经典的数据库范式理论,但是这样做有利于分库分表、数据库迁移并且性能更好,因此这种不键物理外键的做法越来越常见。
领域服务的实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public enum UserAccessResult{ OK, PhoneNumberNotFound, Lockout, NoPassword, PasswordError }
1 2 3 4 5 6 7 public record class UserAccessResultEvent (PhoneNumber PhoneNumber, UserAccessResult Result ) : INotification ;
IUserDomainRepository 领域服务需要使用仓储接口来通过持久层读写数据,因此我们需要在Users.Domain项目中编写仓储接口IUserDomainRepository,这些代码都位于Users.Domain项目中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 namespace Users.Domain { public interface IUserDomainRepository { Task<User?> FindOneAsync(PhoneNumber phoneNumber); Task<User?> FindOneAsync(Guid userId); Task AddNewLoginHistoryAsync (PhoneNumber phoneNumber, string msg ) ; Task PublishEventAsync (UserAccessResultEvent eventData ) ; Task SavePhoneCodeAsync (PhoneNumber phoneNumber, string code ) ; Task<string > RetrievePhoneCodeAsync (PhoneNumber phoneNumber ) ; } }
两个FindOneAsync方法分别用于根据手机号和用户ID查找用户;AddNewLoginHistoryAsync方法用于记录一次登录操作;PublishEventAsync方法用于发布领域事件;SavePhoneCodeAsync方法用于保存短信验证码,而RetrievePhoneCodeAsync方法用于获取保存的短信验证码。
ISmsCodeSender ISmsCodeSender是用于发送短信验证码的防腐层接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 public interface ISmsCodeSender { Task SendCodeAsync (PhoneNumber phoneNumber, string code ) ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public enum CheckCodeResult{ OK, PhoneNumberNotFound, Lockout, CodeError }
UserDomainService 实体类中定义的方法只是和特定实体类相关的业务逻辑代码,而跨实体类、跨聚合的代码需要定义在领域服务或者应用服务中。因此我们编写领域服务UserDomainService。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 namespace Users.Domain { public class UserDomainService { private readonly IUserDomainRepository repository; private readonly ISmsCodeSender smsSender; public UserDomainService (IUserDomainRepository repository, ISmsCodeSender smsSender ) { this .repository = repository; this .smsSender = smsSender; } public void ResetAccessFail (User user ) { user.AccessFail.Reset(); } public bool IsLockOut (User user ) { return user.AccessFail.IsLockOut(); } public void AccessFail (User user ) { user.AccessFail.Fail(); } public async Task<UserAccessResult> CheckLoginAsync (PhoneNumber phoneNum, string password ) { User? user = await repository.FindOneAsync(phoneNum); UserAccessResult result; if (user == null ) { result = UserAccessResult.PhoneNumberNotFound; } else if (IsLockOut(user)) { result = UserAccessResult.Lockout; } else if (user.HasPassword() == false ) { result = UserAccessResult.NoPassword; } else if (user.CheckPassword(password)) { result = UserAccessResult.OK; } else { result = UserAccessResult.PasswordError; } if (user != null ) { if (result == UserAccessResult.OK) { ResetAccessFail(user); } else { AccessFail(user); } } UserAccessResultEvent eventItem = new (phoneNum, result); await repository.PublishEventAsync(eventItem); return result; } public async Task<UserAccessResult> SendCodeAsync (PhoneNumber phoneNum ) { var user = await repository.FindOneAsync(phoneNum); if (user == null ) { return UserAccessResult.PhoneNumberNotFound; } if (IsLockOut(user)) { return UserAccessResult.Lockout; } string code = Random.Shared.Next(1000 , 9999 ).ToString(); await repository.SavePhoneCodeAsync(phoneNum, code); await smsSender.SendCodeAsync(phoneNum, code); return UserAccessResult.OK; } public async Task<CheckCodeResult> CheckCodeAsync (PhoneNumber phoneNum, string code ) { var user = await repository.FindOneAsync(phoneNum); if (user == null ) { return CheckCodeResult.PhoneNumberNotFound; } if (IsLockOut(user)) { return CheckCodeResult.Lockout; } string ? codeInServer = await repository.RetrievePhoneCodeAsync(phoneNum); if (string .IsNullOrEmpty(codeInServer)) { return CheckCodeResult.CodeError; } if (code == codeInServer) { return CheckCodeResult.OK; } else { AccessFail(user); return CheckCodeResult.CodeError; } } } }
因为在实现领域服务的时候,我们需要调用仓储服务和短信发送服务,所以我们通过构造方法注入IUserDomainRepository和ISmsCodeSender,我们再调用这两个服务的方法的时候,并不需要关心它们是由哪个类实现的,以及是如何实现的,这就是“依赖于接口,而非依赖于现实”的依赖反转带来给系统架构的好处。
由于User是聚合根,所以对UserAccessFail的操作都通过User进行,因此我们在UserDomainService中定义的AccessFail等方法都是通过User进行的。 User等实体类中都是和实体类相关的代码,而UserDomainService领域服务中都是跨实体类操作的代码。
基础设施的实现 领域模型、领域服务中只定义了抽象的实体类、防腐层和仓储,我们需要在基础设施中对它们进行落地和实现。这些代码都位于Users.Infrastructure项目中。
UserConfig\UserAccessFailConfig\UserLoginHistoryConfig 实体类、值对象的定义是和持久机制无关的,它们需要通过EF Core的配置、上下文等建立和数据库的关系,User的配置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 namespace Users.Infrastructure.Configs { internal class UserConfig : IEntityTypeConfiguration <User > { public void Configure (EntityTypeBuilder<User> builder ) { builder.ToTable("T_Users" ); builder.OwnsOne(x => x.PhoneNumber, nb => { nb.Property(x => x.RegionCode).HasMaxLength(5 ).IsUnicode(false ); nb.Property(x => x.Number).HasMaxLength(20 ).IsUnicode(false ); }); builder.Property("passwordHash" ).HasMaxLength(100 ).IsUnicode(false ); builder.HasOne(x => x.AccessFail).WithOne(x => x.User) .HasForeignKey<UserAccessFail>(x => x.UserId); } } }
我们对值对象PhoneNumber进行了配置;由于passwordHash是一个私有成员变量,因此我们对他进行特殊的配置。
1 2 3 4 5 6 7 8 9 10 11 namespace Users.Infrastructure.Configs { internal class UserAccessFailConfig : IEntityTypeConfiguration <UserAccessFail > { public void Configure (Microsoft.EntityFrameworkCore.Metadata.Builders.EntityTypeBuilder<UserAccessFail> builder ) { builder.ToTable("T_UserAccessFails" ); builder.Property("lockOut" ); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 namespace Users.Infrastructure.Configs { internal class UserLoginHistoryConfig : IEntityTypeConfiguration <UserLoginHistory > { public void Configure (EntityTypeBuilder<UserLoginHistory> builder ) { builder.ToTable("T_UserLoginHistories" ); builder.OwnsOne(x=>x.PhoneNumber, nb => { nb.Property(x => x.RegionCode).HasMaxLength(5 ).IsUnicode(false ); nb.Property(x => x.Number).HasMaxLength(20 ).IsUnicode(false ); }); } } }
UserDbContext UserDbContext定义在Users.Infrastructure项目中,并且只为User、UserLoginHistory两个聚合根实体类声明DbSet属性,而不为User聚合中的UserAccessFail实体类定义DbSet属性,这样就约束开发人员尽量通过聚合根来操作聚合内的实体类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 namespace Users.Infrastructure { internal class UserDbContext : DbContext { public DbSet<User> Users { get ; private set ; } public DbSet<UserLoginHistory> LoginHistories { get ; private set ; } public UserDbContext (DbContextOptions<UserDbContext> options ) : base (options ) { } protected override void OnModelCreating (ModelBuilder modelBuilder ) { base .OnModelCreating(modelBuilder); modelBuilder.ApplyConfigurationsFromAssembly(this .GetType().Assembly); } } }
UserDomainRepository/ExpressionHelper 仓储接口IUserDomainRepository的实体类UserDomainRepository
我们把创建的UserLoginHistory对象添加到上下文中以后,并没有立即把数据保存到数据库中,因为到底什么时候保存工作单元中的修改是由应用服务层来决定的,仓储和领域层中都不能执行SaveChangesAsync操作。 文买把短信验证码保存在分布式缓存中,当然我们可以把短信验证码保存到数据库、Redis等地方。验证码保存到什么地方是由UserDomainRepository类来决定的,IUserDomainRepository服务的使用者并不需要知道这些细节。这就是整洁架构的内层定义和使用接口,以及外层实现接口所带来的好处。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 namespace Users.Infrastructure { public class UserDomainRepository : IUserDomainRepository { private readonly UserDbContext dbCtx; private readonly IDistributedCache distCache; private readonly IMediator mediator; public UserDomainRepository (UserDbContext dbCtx, IDistributedCache distCache, IMediator mediator ) { this .dbCtx = dbCtx; this .distCache = distCache; this .mediator = mediator; } public async Task AddNewLoginHistoryAsync (PhoneNumber phoneNumber, string msg ) { var user = await FindOneAsync(phoneNumber); UserLoginHistory history = new UserLoginHistory(user?.Id, phoneNumber, msg); dbCtx.LoginHistories.Add(history); } public Task<User?> FindOneAsync(PhoneNumber phoneNumber) { return dbCtx.Users.Include(u => u.AccessFail).SingleOrDefaultAsync(MakeEqual((User u) => u.PhoneNumber, phoneNumber)); } public Task<User?> FindOneAsync(Guid userId) { return dbCtx.Users.Include(u => u.AccessFail) .SingleOrDefaultAsync(u => u.Id == userId); } public Task PublishEventAsync (UserAccessResultEvent eventData ) { return mediator.Publish(eventData); } public Task<string > RetrievePhoneCodeAsync (PhoneNumber phoneNumber ) { string fullNumber = phoneNumber.RegionCode + phoneNumber.Number; string cacheKey = $"LoginByPhoneCode_Code_{fullNumber} " ; string ? code = distCache.GetString(cacheKey); distCache.Remove(cacheKey); return Task.FromResult(code); } public Task SavePhoneCodeAsync (PhoneNumber phoneNumber, string code ) { string fullNumber = phoneNumber.RegionCode + phoneNumber.Number; var options = new DistributedCacheEntryOptions(); options.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(300 ); distCache.SetString($"LoginByPhoneCode_Code_{fullNumber} " , code, options); return Task.CompletedTask; } } }
封装MakeEqual方法进行手机号的相等性比较。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 namespace Users.Infrastructure { public class ExpressionHelper { public static Expression <Func <TItem , bool >> MakeEqual <TItem , TProp >(Expression<Func<TItem, TProp>> propAccessor, TProp? other ) where TItem : class where TProp : class { var e1 = propAccessor.Parameters.Single(); BinaryExpression? conditionalExpr = null ; foreach (var prop in typeof (TProp ).GetProperties ()) { BinaryExpression equalExpr; object ? otherValue = null ; if (other != null ) { otherValue = prop.GetValue(other); } Type propType = prop.PropertyType; var leftExpr = MakeMemberAccess( propAccessor.Body, prop ); Expression rightExpr = Convert(Constant(otherValue), propType); if (propType.IsPrimitive) { equalExpr = Expression.Equal(leftExpr, rightExpr); } else { equalExpr = MakeBinary(ExpressionType.Equal, leftExpr, rightExpr, false , prop.PropertyType.GetMethod("op_Equality" ) ); } if (conditionalExpr == null ) { conditionalExpr = equalExpr; } else { conditionalExpr = Expression.AndAlso(conditionalExpr, equalExpr); } } if (conditionalExpr == null ) { throw new ArgumentException("propAccessor must have at least one property" ); } return Lambda<Func<TItem, bool >>( conditionalExpr, e1 ); } } }
MockSmsCodeSender 模拟短信验证码发送器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 namespace Users.Infrastructure { internal class MockSmsCodeSender : ISmsCodeSender { private readonly ILogger<MockSmsCodeSender> logger; public MockSmsCodeSender (ILogger<MockSmsCodeSender> logger ) { this .logger = logger; } public Task SendCodeAsync (PhoneNumber phoneNumber, string code ) { logger.LogInformation($"向{phoneNumber} 发送验证码:{code} " ); return Task.CompletedTask; } } }
工作单元的实现 工作单元由应用服务层来确定,其他层不应该调用SaveChangesAsync方法保存对数据的修改。我们把Web API的控制器当成应用服务,而且对于大部分应用场景来讲,一次对控制器中方法的调用就对应一个工作单元,因此我们可以开发一个在控制器的方法调用结束后自动调用SaveChangesAsync的机制。这样就能大大简化应用服务层代码的编写,从而避免对SaveChangesAsync方法的重复调用。当然,对于特殊的应用服务层代码,我们可能仍然需要手动决定调用SaveChangesAsync方法的时机。
UnitOfWorkAttribute 我们定义一个Attribute,将其添加到需要重启自动提交工作单元的操作方法上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 namespace Users.WebAPI { [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true) ] public class UnitOfWorkAttribute : Attribute { public Type[] DbContextTypes { get ; init ; } public UnitOfWorkAttribute (params Type[] dbContextTypes ) { this .DbContextTypes = dbContextTypes; foreach (var type in dbContextTypes) { if (!typeof (DbContext).IsAssignableFrom(type)) { throw new ArgumentException($"{type} 必须继承自 DbContext" ); } } } } }
如果一个控制器上标注了UnitOfWorkAttibute,那么这个控制器中所有的方法都会在执行结束后自动提交工作单元,我们也可以把UnitOfWorkAttribute添加到控制器的方法上。因为一个微服务中可能有多个上下文,所以我们通过DbContextTypes来指定工作单元结束后程序自动调用哪些上下文的SaveChangesAsync方法,DbContextTypes属性用来指定上下文的类型。
UnitOfWorkFillter 我们实现一个全局的ActionFillter,来实现在控制器的操作方法执行结束后自动提交工作单元的功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 namespace Users.WebAPI { public class UnitOfWorkFilter : IAsyncActionFilter { private static UnitOfWorkAttribute? GetUoWAttr(ActionDescriptor actionDesc) { var caDesc = actionDesc as ControllerActionDescriptor; if (caDesc == null ) { return null ; } var uowAttr = caDesc.ControllerTypeInfo.GetCustomAttribute<UnitOfWorkAttribute>(); if (uowAttr != null ) { return uowAttr; } else { return caDesc.MethodInfo.GetCustomAttribute<UnitOfWorkAttribute>(); } } public async Task OnActionExecutionAsync (ActionExecutingContext context, ActionExecutionDelegate next ) { var uowAttr = GetUoWAttr(context.ActionDescriptor); if (uowAttr == null ) { await next(); return ; } List<DbContext> dbCtxs = new List<DbContext>(); foreach (var dbCtxType in uowAttr.DbContextTypes) { var sp = context.HttpContext.RequestServices; DbContext dbCtx = (DbContext)sp.GetRequiredService(dbCtxType); dbCtxs.Add(dbCtx); } var result = await next(); if (result.Exception == null ) { foreach (var dbCtx in dbCtxs) { await dbCtx.SaveChangesAsync(); } } } } }
最后只要把UnitOfWorkFilter注册为ASP.NET Core的全局筛选器,所有添加了[UnitOfWork]的控制器或者操作方法就都能自动进行工作单元的提交了。
应用服务层的实现 ASP.NET Core Web API中的控制器就是应用服务层,因此我们在ASP.NET Core的项目Users.WebAPI中编写的代码就是应用服务层代码。
1 2 3 4 5 6 7 8 9 10 11 12 { "Logging" : { "LogLevel" : { "Default" : "Information" , "Microsoft.AspNetCore" : "Warning" } } , "AllowedHosts" : "*" , "ConnectionStrings" : { "Default" : "Server=localhost;Database=YOUXIANYU;Trusted_Connection=True;MultipleActiveResultSets=true;TrustServerCertificate=True;Encrypt=True;" } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 using Microsoft.AspNetCore.Mvc;using Microsoft.EntityFrameworkCore;using Users.Domain;using Users.Infrastructure;using Users.WebAPI;using MediatR;var builder = WebApplication.CreateBuilder(args);builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddDbContext<UserDbContext>(b => { string connStr = builder.Configuration.GetConnectionString("Default" ); b.UseSqlServer(connStr); }); builder.Services.AddStackExchangeRedisCache(options => { options.Configuration = "localhost" ; options.InstanceName = "UsersCache" ; }); builder.Services.Configure<MvcOptions>(opt => { opt.Filters.Add<UnitOfWorkFilter>(); }); builder.Services.AddScoped<UserDomainService>(); builder.Services.AddScoped<ISmsCodeSender, MockSmsCodeSender>(); builder.Services.AddScoped<IUserDomainRepository, UserDomainRepository>(); builder.Services.AddMediatR(cfg => { cfg.RegisterServicesFromAssembly(typeof (Program).Assembly); }); var app = builder.Build();if (app.Environment.IsDevelopment()){ app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run();