三、基礎功能模塊,用戶類別管理——鎖、EF並發處理、領域服務、應用服務的划分


在上一章節中,我們處理了MVC多級目錄問題,參見《二、處理MVC多級目錄問題——以ABP為基礎架構的一個中等規模的OA開發日志》。從這章開始,我們將進入正式的開發過程。首先,我們要完成系統的基礎設置模塊(在后續的功能中,需要大量使用這些基礎設置信息)。和一般的OA系統不同,在律所OA系統中,用戶類別管理是基礎模塊中非常重要、使用頻率非常高的一個基礎模塊。雖然此功能只是很小的一個字典項設置,但是其中涉及了鎖、並發處理、領域服務於應用服務的划分等繁瑣問題。

UI功能頁面介紹(因用戶功能未完成,欠缺刪除頁面)

UI方面,我們使用了Metronic+EasyUI做為主要呈現方式。其中我們對EasyUI做了相應調整,已使其更加適用於Metronic風格。其中,上圖所示的cnblogs.scss為本博客的UI風格(仿小米風格,未完工);color.scss為全局顏色設置;common.scss為全局公用css;easyui.custom.scss為我們對EasyUI的樣式修改;metronic.scss為我們對Metronic的樣式調整; site.scss為程序主css文件。我們在common.scss文件中,導入了color.scss。在site.scss文件中,導入了common.scss、metronic.scss和easyui.custom.scss文件。這樣在布局頁引入css文件時,我們僅需要引入單個site.min.css文件即可,而不用引入一大堆css文件。我們在布局頁中,大量使用了存儲於cdn上的一些css和js文件。這些大多都是一些公用類庫,大家可根據需要自行下載。

ABP對CSRF/XSRF跨站攻擊的處理

abp通過token驗證以解決上述攻擊問題。只要在模板文件(布局頁)中,增加@{SetAntiForgeryCookie();},即可方便在ABP內置的ajax輔助方法中發送生成的token。但在實際使用中,我們需要做一些處理。ABP的view頁面文件繼承自View目錄下EasyFastWebViewPageBase類(該類最終繼承自AbpWebViewPage類),而SetAntiForgeryCookie()方法則是AbpWebViewPage類的內置方法。所以,要使用SetAntiForgeryCookie()方法。我們必須要所有的布局頁全部繼承自EasyFastWebViewPageBase類。

如圖所示,在View根目錄下,有EasyFastWebViewPageBase.cs文件(依據您的項目名,此處會有不同的文件名)。為了保證在Areas中的布局頁也能正常的使用SetAntiForgeryCookie()方法。需要在布局頁繼承該類。(也可以復制一遍,放到所有Areas的View目錄下,但顯然,繼承的方式更合理)感謝ABP架構交流群的朋友們,剛開始時作者也被這里卡了很久,是群里的朋友們最終指出了問題所在。

實體設置

我們在Core層下創建Entities目錄(目錄名可以隨意起,這里采用的常用習慣Entity的復數形式)。然后創建BaseEntity和UserType兩個類。

namespace EasyFast.Core.Entities
{
    public class BaseEntity : FullAuditedEntity<long>
    {
        public BaseEntity()
        {
            OrderId = 999;
            Guid = Guid.NewGuid();
        }

        /// <summary>
        /// model的Guid,用於記錄操作日志
        /// </summary>
        public Guid Guid { get; set; }

        /// <summary>
        /// 排序Id
        /// </summary>
        [Range(1,999)]
        public int OrderId { get; set; }

        /// <summary>
        /// 行號,用於樂觀並發控制
        /// </summary>
        [Timestamp]
        public byte[] RowVersion { get; set; }
    }
}
namespace EasyFast.Core.Entities
{
    public class UserType : BaseEntity
    {
        /// <summary>
        /// 人員類別名稱
        /// </summary>
        [StringLength(50)]
        public string Name { get; set; }

        /// <summary>
        /// 備注信息
        /// </summary>
        public string Remarks { get; set; }
        public ICollection<User> User { get; set; }
    }
}

BaseEntity類將做為我們大多數實體的基類。該基本繼承自FullAuditedEntity<long>。這樣一來,BaseEntity就自動繼承了 public long Id{ get; set; }這個屬性。我們追加的Guid字段用於記錄操作日志。這個在日后使用時再詳細說明。RowVersion字段用於對EF進行並發控制。在SQLServer中,行中的數據每變動一次,RowVersion自動+1(該字段為16進制)。通過對比該字段的變化,我們即可得知在修改或是刪除數據時,是否存在並發沖突。

應用層(EasyFast.Application)主要代碼簡析

特別提醒:本人對應用層、領域層的講解僅僅只是本人的一點淺見,不代表DDD的最佳實踐要求這么干。在本系列文章里,我們更關注解決工程問題,而不是進行理論研究。如您發現我們的設計有不合理之處,或是對ABP的使用或理解有不對之處。歡迎批評指正。

Application層一般稱之為應用服務於層。在DDD設計規范里,此層專門針對頁面進行服務。這個說法可能讓人費解。我們舉個實際的例子做參考:在OA系統中,我們要展示一個律師的信息時,既要展示User表本身的信息,也要同時展示其關聯的Case表、Client表、Finance表等內容。在N層架構的做法中,我們會分別實例化User、Case、Client等對應的業務邏輯類。然后將其查詢的結果存儲成ViewBag發送到View頁面。接着再在View頁面中,將對應的ViewBag轉換成model進行輸出。

        public ActionResult Index()
        {
            long UserID = User.GetUserInfo().LawyerId;
            var user = LawyerService.Find(UserID);
            if (user.Status == JingShOnline.Models.Enum.LawyerStatus.Normal)
            {
                return RedirectToAction("Normal", "Authen");
            }

            ViewBag.CaseList = CaseService.Where(o =>o.LawyerId== UserID)
                .Include(o=>o.CaseReason)
                .Include(o=>o.Practice)
                .Include(o=>o.Court)
                .Include(o=>o.Industry)
                .Take(10).ToList();

            var history = user.LawyerWorkHistory.OrderByDescending(o => o.StartDate).ToList();
            var education = user.LawyerEducation.OrderByDescending(o => o.StartDate).ToList();
            var academic = user.LawyerAcademic.OrderByDescending(o => o.Id).ToList();
            var certificate = user.LawyerCertificate.OrderByDescending(o => o.Id).ToList();
            var socialposition = user.LawyerSocialPosition.OrderByDescending(o => o.Id).ToList();

            //簡歷完整度
            var ResumeCompletion = (history.Count > 0 ? 35 : 0) + (education.Count > 0 ? 35 : 0) + (academic.Count > 0 ? 10 : 0) + (certificate.Count > 0 ? 10 : 0) + (socialposition.Count > 0 ? 10 : 0);

            ViewBag.WorkHistory = history;
            ViewBag.Education = education;
            ViewBag.Academic = academic;
            ViewBag.Certificate = certificate;
            ViewBag.SocialPosition = socialposition;
            ViewBag.ResumeCompletion = ResumeCompletion;

            return View(user);
        }

  參見上述代碼。View頁面需要多個模塊的數據做集中展示。為達到目的,只好在Controller里初始化多個Service,進行多次查詢,然后將查詢的結果存儲到ViewBag中,發送到前台。再在前台進行數據類型轉換並輸出。如此做法主要有兩個個弊端:

  1. Controller過於重型化,不利於代碼質量控制。在MVC中,Controller應只負責基礎效驗和Action的跳轉。
  2. ViewBag是弱類型的,前台使用時,容易出錯。且日后代碼進行擴展或是重構時,將大大增大bug出現幾率。

Application層的出現,其實就是為了解決這兩個問題。在DDD設計規范中,Application針對View進行服務,View需要什么類型的數據,那么Application就返回什么類型的數據。在本例子中,Application會返回一個包含了上述所有ViewBag類型的綜合model。這樣就可以把大量代碼轉移到Application或是Core層去實現,且前台只接受一個含有具體數據的model,model是強類型的,不用考慮數據類型轉換問題。

在ABP里,作者推薦為每一個應用服務單獨建立目錄,且每一個應用服務目錄中都應包含Dto子目錄。該目錄用於存放ViewModel。Application層負責將從Core或是Repository中得到的Entity轉化成ViewModel,發送到前台。參見上圖,我們將UserType這個應用服務單獨創建目錄(單獨將UserType視為一個應用服務其實不太合理,更合理的做法是將和User相關的所有內容統一在User這個應用服務中實現)。Dto目錄里存放着UserTypeAppService中每一個方法所對應的輸入輸出參數。ABP推薦將Application中的服務方法所需的參數全部類型化。並分別以Input、Output結尾以做區分。比如我們的刪除用戶類別方法,需要兩個參數:long oldId, long newId,我們仍舊把這兩個參數組成一個類去傳遞。這樣做的好處是,日后進行重構或是功能調整時,會大幅減少程序中的修改地方。降低出現bug的幾率。

namespace EasyFast.Application.UserType
{
    public interface IUserTypeAppService : IApplicationService
    {
        UserTypeInput Find(long id);
        long Add(UserTypeInput model);
        long Update(UserTypeInput model);
        EasyUIDataGrid<UserTypeDataGridDto> GetDataGrid(UserTypeSearch search);
        /// <summary>
        /// 檢測傳入的全部用戶類別是否含有用戶,用於判斷直接刪除or轉移用戶后再刪除
        /// </summary>
        /// <param name="ids">long[] Model.UserType.Id</param>
        /// <returns>true:含有用戶 false:不含用戶</returns>
        bool CheckIsHaveUser(long[] ids);
        void Delete(DeleteInput model);
    }
}
namespace EasyFast.Application.UserType
{
    public class UserTypeAppService : EasyFastAppServiceBase, IUserTypeAppService
    {
        private readonly IRepository<Core.Entities.UserType, long> _userTypeRepository;
        private readonly IUserTypeService _userTypeService;


        public UserTypeAppService(IRepository<Core.Entities.UserType, long> userTypeRepository, IUserTypeService userTypeService)
        {
            _userTypeRepository = userTypeRepository;
            _userTypeService = userTypeService;
        }

        public UserTypeInput Find(long id)
        {
            var data = _userTypeRepository.FirstOrDefault(id);
            return Mapper.Map<UserTypeInput>(data);
        }

        public long Add(UserTypeInput model)
        {
            var data = Mapper.Map<Core.Entities.UserType>(model);
            return _userTypeService.Add(data);
        }

        public long Update(UserTypeInput model)
        {
            var data = Mapper.Map<Core.Entities.UserType>(model);
            return _userTypeService.Update(data);
        }

        public EasyUIDataGrid<UserTypeDataGridDto> GetDataGrid(UserTypeSearch search)
        {
            var data = _userTypeRepository.GetAll()
                .Where(o => o.Name.Contains(search.Name), !string.IsNullOrEmpty(search.Name));
            var total = data.Count();
            var list = Mapper.Map<List<UserTypeDataGridDto>>(data);
            var rows = list.OrderBy(String.Format("{0} {1}", search.Sort, search.Order))
                .Skip((search.Page - 1) * search.Rows).Take(search.Rows).ToList();
            return new EasyUIDataGrid<UserTypeDataGridDto> { total = total, rows = rows };
        }

        public bool CheckIsHaveUser(long[] ids)
        {
            return _userTypeRepository.GetAllIncluding(o => o.User).Where(o => ids.Contains(o.Id)).Any(o => o.User != null);
        }

        public void Delete(DeleteInput model)
        {
            _userTypeService.Delete(model.OldId, model.NewId);
        }
    }
}

ABP要求給所有應用服務提取接口,並且接口要繼承自IApplicationService。只有繼承了這個接口,ABP才會自動實現依賴注入。在UserTypeAppService類中,我們自動注入了UserTypeService這個領域服務和UserTypeRepository這個倉儲。除了使用構造參數的注入方式外,您也可以使用屬性注入,但構造參數注入顯得更高大上一點。在作者理解,簡單功能,應用服務直接調用倉儲接口實現。復雜功能(尤指業務邏輯代碼)在領域服務中實現(Core中的Service),然后應用服務調用領域服務的處理結果,返回給用戶。其中,部分功能通過系統默認的倉儲接口無法實現的,就自定義倉儲然后根據情況,選擇應用服務或是領域服務調用並返回。

在我們的設計實現中,新增或是修改人員類別時,要保證不重名,我們沒有采用數據庫唯一性約束,而是通過代碼實現的,先重名檢測,再進行增、改這部分代碼屬於業務邏輯。所以我們將這些代碼放在了領域服務層去實現,應用服務本身不處理這些,其通過調用領域服務中對應的方法並返回合適的結果,供前台使用。

因ViewModel(或者稱為Dto,一個意思,兩種常見名稱)只在Web和Application中使用,所以將AutoMapper相關的映射代碼防止在Application層中最合適不過。我們可以新建一個AutoMapperConfig類,並在其中配置好映射關系后,直接在EasyFastApplicationModule.cs文件中調用即可。不用再web項目中的Global.asax中再次調用,ABP會自動在應用程序初始化時加載我們的配置文件。

namespace EasyFast.Application.AutoMapper
{
    public static class AutoMapperConfig
    {
        public static void Bind(IMapperConfigurationExpression opt)
        {
            #region UserType
            opt.CreateMap<Core.Entities.UserType, UserTypeInput>();
            opt.CreateMap<UserTypeInput, Core.Entities.UserType>()
                .ForMember(d => d.User, s => s.Ignore());
            opt.CreateMap<Core.Entities.UserType, UserTypeDataGridDto>()
                .ForMember(d => d.UserCount, s => s.MapFrom(o => o.User.Count));
            #endregion

            //Mapper.AssertConfigurationIsValid();//驗證所有的映射配置是否都正常
        }

        public static void Config()
        {
            Mapper.Initialize(Bind);
        }
    }
}
namespace EasyFast.Application
{
    [DependsOn(typeof(EasyFastCoreModule), typeof(AbpAutoMapperModule))]
    public class EasyFastApplicationModule : AbpModule
    {
        public override void PreInitialize()
        {
            Configuration.Modules.AbpAutoMapper().Configurators.Add(mapper =>
            {
                AutoMapperConfig.Bind(mapper);
                //Add your custom AutoMapper mappings here...
                //mapper.CreateMap<,>()
            });
        }

        public override void Initialize()
        {
            IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly());
        }
    }
}

 

 

章節過長,未完待續。另征求意見:如此敘述,是否過於繁瑣?如大家普遍認為啰嗦,后續我將省略大部分代碼解釋及配圖說明。只保留關鍵代碼說明及設計思路說明。


免責聲明!

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



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