在ABP中AppUser表的数据字段是有限的,现在有个场景是和小程序对接,需要在AppUser表中添加一个OpenId字段。今天有个小伙伴在群中遇到的问题是基于ABP的AppUser对象扩展后,用户查询是没有问题的,但是增加和更新就会报"XXX field is required"的问题。本文以AppUser表扩展OpenId字段为例进行介绍。
一.AppUser实体表
AppUser.cs位于BaseService.Domain项目中,如下:
public class AppUser : FullAuditedAggregateRoot<Guid>, IUser { public virtual Guid? TenantId { get; private set; } public virtual string UserName { get; private set; } public virtual string Name { get; private set; } public virtual string Surname { get; private set; } public virtual string Email { get; private set; } public virtual bool EmailConfirmed { get; private set; } public virtual string PhoneNumber { get; private set; } public virtual bool PhoneNumberConfirmed { get; private set; } // 微信应用唯一标识 public string OpenId { get; set; } private AppUser() { } }
因为AppUser继承自聚合根,而聚合根默认都实现了IHasExtraProperties接口,否则如果想对实体进行扩展,那么需要实体实现IHasExtraProperties接口才行。
二.实体扩展管理
BaseEfCoreEntityExtensionMappings.cs位于BaseService.EntityFrameworkCore项目中,如下:
public class BaseEfCoreEntityExtensionMappings { private static readonly OneTimeRunner OneTimeRunner = new OneTimeRunner(); public static void Configure() { BaseServiceModuleExtensionConfigurator.Configure(); OneTimeRunner.Run(() => { ObjectExtensionManager.Instance .MapEfCoreProperty<IdentityUser, string>(nameof(AppUser.OpenId), (entityBuilder, propertyBuilder) => { propertyBuilder.HasMaxLength(128); propertyBuilder.HasDefaultValue(""); propertyBuilder.IsRequired(); } ); }); } }
三.数据库上下文
BaseServiceDbContext.cs位于BaseService.EntityFrameworkCore项目中,如下:
[ConnectionStringName("Default")] public class BaseServiceDbContext : AbpDbContext<BaseServiceDbContext> { ...... public BaseServiceDbContext(DbContextOptions<BaseServiceDbContext> options): base(options) { } protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); builder.Entity<AppUser>(b => { // AbpUsers和IdentityUser共享相同的表 b.ToTable(AbpIdentityDbProperties.DbTablePrefix + "Users"); b.ConfigureByConvention(); b.ConfigureAbpUser(); b.Property(x => x.OpenId).HasMaxLength(128).HasDefaultValue("").IsRequired().HasColumnName(nameof(AppUser.OpenId)); }); builder.ConfigureBaseService(); } }
四.数据库迁移和更新
1.数据库迁移
dotnet ef migrations add add_appuser_openid
2.数据库更新
dotnet ef database update
3.对额外属性操作
数据库迁移和更新后,在AbpUsers数据库中就会多出来一个OpenId字段,然后在后端中就可以通过SetProperty或者GetProperty来操作额外属性了:
// 设置额外属性 var user = await _identityUserRepository.GetAsync(userId); user.SetProperty("Title", "My custom title value!"); await _identityUserRepository.UpdateAsync(user); // 获取额外属性 var user = await _identityUserRepository.GetAsync(userId); return user.GetProperty<string>("Title");
但是在前端呢,主要是通过ExtraProperties字段这个json类型来操作额外属性的。
五.应用层增改操作
UserAppService.cs位于BaseService.Application项目中,如下:
1.增加操作
[Authorize(IdentityPermissions.Users.Create)] public async Task<IdentityUserDto> Create(BaseIdentityUserCreateDto input) { var user = new IdentityUser( GuidGenerator.Create(), input.UserName, input.Email, CurrentTenant.Id ); input.MapExtraPropertiesTo(user); (await UserManager.CreateAsync(user, input.Password)).CheckErrors(); await UpdateUserByInput(user, input); var dto = ObjectMapper.Map<IdentityUser, IdentityUserDto>(user); foreach (var id in input.JobIds) { await _userJobsRepository.InsertAsync(new UserJob(CurrentTenant.Id, user.Id, id)); } foreach (var id in input.OrganizationIds) { await _userOrgsRepository.InsertAsync(new UserOrganization(CurrentTenant.Id, user.Id, id)); } await CurrentUnitOfWork.SaveChangesAsync(); return dto; }
2.更新操作
[Authorize(IdentityPermissions.Users.Update)] public async Task<IdentityUserDto> UpdateAsync(Guid id, BaseIdentityUserUpdateDto input) { UserManager.UserValidators.Clear(); var user = await UserManager.GetByIdAsync(id); user.ConcurrencyStamp = input.ConcurrencyStamp; (await UserManager.SetUserNameAsync(user, input.UserName)).CheckErrors(); await UpdateUserByInput(user, input); input.MapExtraPropertiesTo(user); (await UserManager.UpdateAsync(user)).CheckErrors(); if (!input.Password.IsNullOrEmpty()) { (await UserManager.RemovePasswordAsync(user)).CheckErrors(); (await UserManager.AddPasswordAsync(user, input.Password)).CheckErrors(); } var dto = ObjectMapper.Map<IdentityUser, IdentityUserDto>(user); dto.SetProperty("OpenId", input.ExtraProperties["OpenId"]); await _userJobsRepository.DeleteAsync(_ => _.UserId == id); if (input.JobIds != null) { foreach (var jid in input.JobIds) { await _userJobsRepository.InsertAsync(new UserJob(CurrentTenant.Id, id, jid)); } } await _userOrgsRepository.DeleteAsync(_ => _.UserId == id); if (input.OrganizationIds != null) { foreach (var oid in input.OrganizationIds) { await _userOrgsRepository.InsertAsync(new UserOrganization(CurrentTenant.Id, id, oid)); } } await CurrentUnitOfWork.SaveChangesAsync(); return dto; }
3.UpdateUserByInput()函数
上述增加和更新操作代码中用到的UpdateUserByInput()函数如下:
protected virtual async Task UpdateUserByInput(IdentityUser user, IdentityUserCreateOrUpdateDtoBase input) { if (!string.Equals(user.Email, input.Email, StringComparison.InvariantCultureIgnoreCase)) { (await UserManager.SetEmailAsync(user, input.Email)).CheckErrors(); } if (!string.Equals(user.PhoneNumber, input.PhoneNumber, StringComparison.InvariantCultureIgnoreCase)) { (await UserManager.SetPhoneNumberAsync(user, input.PhoneNumber)).CheckErrors(); } (await UserManager.SetLockoutEnabledAsync(user, input.LockoutEnabled)).CheckErrors(); user.Name = input.Name; user.Surname = input.Surname; user.SetProperty("OpenId", input.ExtraProperties["OpenId"]); if (input.RoleNames != null) { (await UserManager.SetRolesAsync(user, input.RoleNames)).CheckErrors(); } }
实体扩展的好处是不用继承实体,或者修改实体就可以对实体进行扩展,可以说是非常的灵活,但是实体扩展并不适用于复杂的场景,比如使用额外属性创建索引和外键、使用额外属性编写SQL或LINQ等。遇到这种情况该怎么办呢?有种方法是直接引用源码和添加字段。
参考文献:
[1]自定义应用模块:https://docs.abp.io/zh-Hans/abp/6.0/Customizing-Application-Modules-Guide
[2]自定义应用模块-扩展实体:https://docs.abp.io/zh-Hans/abp/6.0/Customizing-Application-Modules-Extending-Entities
[3]自定义应用模块-重写服务:https://docs.abp.io/zh-Hans/abp/6.0/Customizing-Application-Modules-Overriding-Services
[4]ABP-MicroService:https://github.com/WilliamXu96/ABP-MicroService