基於ABP的AppUser物件擴充套件

2022-08-10 06:01:22

  在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