說明

Abp vNext基礎篇的文章還差一個單元測試模組就基本上完成了我爭取10.1放假之前給大家趕稿出來,後面我們會開始進階篇,開始拆一些東西,具體要做的事我會單獨開一個文章來講

緣起

本篇文章緣起於dyAbp大佬們在給夏琳兒(簡稱:小富婆)講解技術的時候發起,因為多使用者設計和使用者擴充套件屬性設計在社群已經是一個每天都會有人來問一遍的問題,這裡淺談一下我的理解,原始碼是根據EasyAbp作者Super寫的程式碼,根據我自己的理解去分析的想法。

擴充套件屬性

先從我們單使用者系統來講,如果我該如何擴充套件使用者屬性?

在Abp預設解決方案Domain.Shared中更改ConfigureExtraProperties,該操作會向IdentityUser實體新增SocialSecurityNumber屬性

public static void ConfigureExtraProperties()
{
OneTimeRunner.Run(() =>
{
ObjectExtensionManager.Instance.Modules()
.ConfigureIdentity(identity =>
{
identity.ConfigureUser(user =>
{
user.AddOrUpdateProperty<string>( //property type: string
"SocialSecurityNumber", //property name
property =>
{
//validation rules
property.Attributes.Add(new RequiredAttribute());
property.Attributes.Add(
new StringLengthAttribute(64) {
MinimumLength = 4
}
); //...other configurations for this property
}
);
});
});
});
}

EntityExtensions還提供了很多配置操作,這裡就簡單的舉幾個常用的例子更多詳細操作可以在文章下方連線到官方連線。

// 預設值選項
property =>
{
property.DefaultValue = 42;
}
//預設值工廠選項
property =>
{
property.DefaultValueFactory = () => DateTime.Now;
}
// 資料註解屬性
property =>
{
property.Attributes.Add(new RequiredAttribute());
property.Attributes.Add(new StringLengthAttribute(64) {MinimumLength = 4});
}
//驗證操作
property =>
{
property.Attributes.Add(new RequiredAttribute());
property.Attributes.Add(new StringLengthAttribute(64) {MinimumLength = 4}); property.Validators.Add(context =>
{
if (((string) context.Value).StartsWith("B"))
{
context.ValidationErrors.Add(
new ValidationResult(
"Social security number can not start with the letter 'B', sorry!",
new[] {"extraProperties.SocialSecurityNumber"}
)
);
}
}); }

目前這種配置方式如果你的前端是mvc或者razor pages是不需要改動程式碼的,頁面會動態生成欄位,但是如果是angular就需要人工來操作了,除了擴充套件屬性外,你可能還需要部分或完全覆蓋某些服務和頁面元件才行,不過Abp官方文件都有相應的操作指南所以沒有任何問題。

具體更多操作官方地址:https://docs.abp.io/en/abp/latest/Module-Entity-Extensions

另外就是大家最關係的資料儲存問題,預設我們新增的資料都會在ExtraProperties以JSON物件方式進行儲存

但如果你想用欄位的方式進行儲存的話,可以在你的.EntityFrameworkCore專案的類中寫下這個。然後您需要使用標準Add-Migration和Update-Database命令來建立新的資料庫遷移並將更改應用到您的資料庫。

ObjectExtensionManager.Instance
.MapEfCoreProperty<IdentityUser, string>(
"SocialSecurityNumber",
(entityBuilder, propertyBuilder) =>
{
propertyBuilder.HasMaxLength(64);
}
);

多使用者設計

舉例你要開發學生管理系統

  • 老師和學生都會進入系統來做自己對應的操作,我們如何來隔離呢?

首先我們就可以想到通過角色來做許可權分配做能力隔離

  • 然後學生和老師的引數不一樣,怎麼辦,老師要填寫工號、系部、教學科目、工齡,學生要填寫年度、班級、學號?,看到過比較粗暴的方案就是直接在IdentityUser表全給幹上去,但是這種做法相對於某個角色來看是不是太冗餘?

這裡我參考Super的一個做法採用使用自己的資料庫表/集合建立新實體,具體什麼意思呢?

我們建立Teacher實體,該實體通過UserId指定IdentityUser,來儲存作為老師的額外屬性

public class Teacher : AggregateRoot<Guid>, IMultiTenant
{
public virtual Guid? TenantId { get; protected set; } public virtual Guid UserId { get; protected set; } public virtual bool Active { get; protected set; } [NotNull]
public virtual string Name { get; protected set; } public virtual int? Age { get; protected set; } protected Teacher()
{
} public Teacher(Guid id, Guid? tenantId, Guid userId, bool active, [NotNull] string name, int? age) : base(id)
{
TenantId = tenantId;
UserId = userId; Update(active, name, age);
} public void Update(bool active, [NotNull] string name, int? age)
{
Active = active;
Name = name;
Age = age;
}
}

處理方案是通過訂閱UserEto,這是User預定義的專用事件類,當User產生Created、Updated和Deleted操作收會到通知,然後執行我們自己邏輯,

 [UnitOfWork]
public class TeacherUserInfoSynchronizer :
IDistributedEventHandler<EntityCreatedEto<UserEto>>,
IDistributedEventHandler<EntityUpdatedEto<UserEto>>,
IDistributedEventHandler<EntityDeletedEto<UserEto>>,
ITransientDependency
{
private readonly IGuidGenerator _guidGenerator;
private readonly ICurrentTenant _currentTenant;
private readonly IUserRoleFinder _userRoleFinder;
private readonly IRepository<Teacher, Guid> _teacherRepository; public TeacherUserInfoSynchronizer(
IGuidGenerator guidGenerator,
ICurrentTenant currentTenant,
IUserRoleFinder userRoleFinder,
IRepository<Teacher, Guid> teacherRepository)
{
_guidGenerator = guidGenerator;
_currentTenant = currentTenant;
_userRoleFinder = userRoleFinder;
_teacherRepository = teacherRepository;
} public async Task HandleEventAsync(EntityCreatedEto<UserEto> eventData)
{
if (!await HasTeacherRoleAsync(eventData.Entity))
{
return;
} await CreateOrUpdateTeacherAsync(eventData.Entity, true);
} public async Task HandleEventAsync(EntityUpdatedEto<UserEto> eventData)
{
if (await HasTeacherRoleAsync(eventData.Entity))
{
await CreateOrUpdateTeacherAsync(eventData.Entity, true);
}
else
{
await CreateOrUpdateTeacherAsync(eventData.Entity, false);
}
} public async Task HandleEventAsync(EntityDeletedEto<UserEto> eventData)
{
await TryUpdateAndDeactivateTeacherAsync(eventData.Entity);
} protected async Task<bool> HasTeacherRoleAsync(UserEto user)
{
var roles = await _userRoleFinder.GetRolesAsync(user.Id); return roles.Contains(MySchoolConsts.TeacherRoleName);
} protected async Task CreateOrUpdateTeacherAsync(UserEto user, bool active)
{
var teacher = await FindTeacherAsync(user); if (teacher == null)
{
teacher = new Teacher(_guidGenerator.Create(), _currentTenant.Id, user.Id, active, user.Name, null); await _teacherRepository.InsertAsync(teacher, true);
}
else
{
teacher.Update(active, user.Name, teacher.Age); await _teacherRepository.UpdateAsync(teacher, true);
}
} protected async Task TryUpdateAndDeactivateTeacherAsync(UserEto user)
{
var teacher = await FindTeacherAsync(user); if (teacher == null)
{
return;
} teacher.Update(false, user.Name, teacher.Age); await _teacherRepository.UpdateAsync(teacher, true);
} protected async Task<Teacher> FindTeacherAsync(UserEto user)
{
return await _teacherRepository.FindAsync(x => x.UserId == user.Id);
}
}

結語

我也是在閱讀文件和對照Super大佬的程式碼後自己的理解,文中可能某些地方可能與作者設計有差距,還請大家多多理解!

也歡迎大家閱讀我的Abp vNext系列教程

聯絡作者:加群:867095512 @MrChuJiu