基础功能模块,用户类别管理——锁EF并发处理领域服务应用服务的划分

Posted brucelee

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基础功能模块,用户类别管理——锁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>
        1622100965
        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());
        }
    }
}

 

 

章节过长,未完待续。另征求意见:如此叙述,是否过于繁琐?如大家普遍认为啰嗦,后续我将省略大部分代码解释及配图说明。只保留关键代码说明及设计思路说明。

以上是关于基础功能模块,用户类别管理——锁EF并发处理领域服务应用服务的划分的主要内容,如果未能解决你的问题,请参考以下文章

数据库的锁机制

高并发场景下锁

MySQL基础篇(06):事务管理,锁机制案例详解

EF Code 如何应对高并发

数据库的锁机制

EF+Redis(StackExchange.Redis)实现分布式锁,自测可行