Abp vNext 番外篇-疑難雜症丨淺談擴展屬性與多用戶設計


說明

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對象方式進行存儲

extrapropeites

但如果你想用字段的方式進行存儲的話,可以在你的.EntityFrameworkCore項目的類中寫下這個。然后您需要使用標准Add-Migration和Update-Database命令來創建新的數據庫遷移並將更改應用到您的數據庫。

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

extrapropeites

多用戶設計

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

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

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

  • 然后學生和老師的參數不一樣,怎么辦,老師要填寫工號、系部、教學科目、工齡,學生要填寫年度、班級、學號?,看到過比較粗暴的方案就是直接在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大佬的一些教誨:

設計思想要結合自己的業務場景,再考慮如何更契合、優雅、易用、可擴展

DDD的哲學之一,你不需要,僅僅是你不需要,但是Abp的IdentityManagement模塊是需要的

不能只看表象不看里層。說過不能模塊化還想着模塊化,說過看代碼就去看數據表,這種先入為主的思維方式才是真正的問題,根本不是技術的原因…

不同的領域觀察User,它們關心的數據是不一樣的,你不關心email,identity模塊會關心,所以你冗余出來的MyUser,可以不存儲用戶email

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

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

聯系作者:加群:867095512 @MrChuJiu


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM