基于领域驱动设计(DDD)超轻量级快速开发架构[GitHub开源代码]

meitu_0.jpg

smartadmin.core.urf 这个项目是基于asp.net core 3.1(最新)基础上参照领域驱动设计(DDD)的理念,并参考目前最为了流行的abp架构开发的一套轻量级的快速开发web application 技术架构,专注业务核心需求,减少重复代码,开始构建和发布,让初级程序员也能开发出专业并且漂亮的Web应用程序

域驱动设计(DDD)是一种通过将实现与不断发展的模型相连接来满足复杂需求的软件开发方法。域驱动设计的前提如下:

  • 将项目的主要重点放在核心领域和领域逻辑上;
  • 将复杂的设计基于领域模型;
  • 启动技术专家和领域专家之间的创造性合作,以迭代方式完善解决特定领域问题的概念模型。

Demo 网站

login.png

演示站点
账号:demo 密码:123456
源代码https://github.com/neozhu/smartadmin.core.urf

喜欢请给个 Star 每一颗Star都是鼓励我继续更新的动力 谢谢
如果你用于自己公司及盈利性的项目,希望给与金钱上的赞助,并且保留原作者的版权

分层

smartadmin.core.urf遵行DDD设计模式来实现应用程序的四层模型

  • 表示层(Presentation Layer):用户操作展示界面,使用SmartAdmin - Responsive WebApp模板+Jquery EasyUI
  • 应用层(Application Layer):在表示层与域层之间,实现具体应用程序逻辑,业务用例,Project:StartAdmin.Service.csproj
  • 域层(Domain Layer):包括业务对象(Entity)和核心(域)业务规则,应用程序的核心,使用EntityFrmework Core Code-first + Repository实现
  • 基础结构层(Infrastructure Layer):提供通用技术功能,这些功能主要有第三方库来支持,比如日志:Nlog,服务发现:Swagger UI,事件总线(EventBus):dotnetcore/CAP,认证与授权:Microsoft.AspNetCore.Identity,后面会具体介绍

内容

image
  • 域层(Domain Layer)

    • 实体(Entity,BaseEntity) 通常实体就是映射到关系数据库中的表,这里说名一下最佳做法和惯例:
    • 在域层定义:本项目就是(SmartAdmin.Entity.csproj)
    • 继承一个基类 Entity,添加必要审计类比如:创建时间,最后修改时间等
    • 必须要有一个主键最好是GRUID(不推荐复合主键),但本项目使用递增的int类型
    • 字段不要过多的冗余,可以通过定义关联关系
    • 字段属性和方法尽量使用virtual关键字。有些ORM和动态代理工具需要
    • 存储库(Repositories) 封装基本数据操作方法(CRUD),本项目应用 URF.Core实现
    • 域服务
    • 技术指标
  • 应用层

    • 应用服务:用于实现应用程序的用例。它们用于将域逻辑公开给表示层,从表示层(可选)使用DTO(数据传输对象)作为参数调用应用程序服务。它使用域对象执行某些特定的业务逻辑,并(可选)将DTO返回到表示层。因此,表示层与域层完全隔离。对应本项目:(SmartAdmin.Service.csproj)
    • 数据传输对象(DTO):用于在应用程序层和表示层或其他类型的客户端之间传输数据,通常,使用DTO作为参数从表示层(可选)调用应用程序服务。它使用域对象执行某些特定的业务逻辑,并(可选)将DTO返回到表示层。因此,表示层与域层完全隔离.对应本项目:(SmartAdmin.Dto.csproj)
    • Unit of work:管理和控制应用程序中操作数据库连接和事务 ,本项目使用 URF.Core实现
  • 基础服务层

    • UI样式定义:根据用户喜好选择多种页面显示模式
    • 租户管理:使用EntityFrmework Core提供的Global Filter实现简单多租户应用
    • 账号管理: 对登录系统账号维护,注册,注销,锁定,解锁,重置密码,导入、导出等功能
    • 角色管理:使用Microsoft身份库管理角色,用户及其权限管理
    • 导航菜单:系统主导航栏配置
    • 角色授权:配置角色显示的菜单
    • 键值对配置:常用的数据字典维护,如何正确使用和想法后面会介绍
    • 导入&导出配置:使用Excel导入导出做一个可配置的功能
    • 系统日志:asp.net core 自带的日志+Nlog把所有日志保存到数据库方便查询和分析
    • 消息订阅:集中订阅CAP分布式事件总线的消息
    • WebApi: Swagger UI Api服务发现和在线调试工具
    • CAP: CAP看板查看发布和订阅的消息

快速上手开发

  • 开发环境
    • Visual Studio .Net 2019
    • .Net Core 3.1
    • Sql Server(LocalDb)
  • 附加数据库

    使用SQL Server Management Studio 附加.\src\SmartAdmin.Data\db\smartadmindb.mdf 数据库(如果是localdb,那么不需要修改数据库连接配置)

  • 打开解决方案

第一个简单的需求开始
新增 Company 企业信息 完成CRUD 导入导出功能

  • 新建实体对象(Entity)

在SmartAdmin.Entity.csproj项目的Models目录下新增一个Company.cs类

//记住:定义实体对象最佳做法,继承基类,使用virtual关键字,尽可能的定义每个属性,名称,类型,长度,校验规则,索引,默认值等
namespace SmartAdmin.Data.Models
{
    public partial class Company : URF.Core.EF.Trackable.Entity
    {
        [Display(Name = "企业名称", Description = "归属企业名称")]
        [MaxLength(50)]
        [Required]
        //[Index(IsUnique = true)]
        public virtual string Name { get; set; }
        [Display(Name = "组织代码", Description = "组织代码")]
        [MaxLength(12)]
        //[Index(IsUnique = true)]
        [Required]
        public virtual string Code { get; set; }
        [Display(Name = "地址", Description = "地址")]
        [MaxLength(128)]
        [DefaultValue("-")]
        public virtual string Address { get; set; }
        [Display(Name = "联系人", Description = "联系人")]
        [MaxLength(12)]
        public virtual string Contect { get; set; }
        [Display(Name = "联系电话", Description = "联系电话")]
        [MaxLength(20)]
        public virtual string PhoneNumber { get; set; }
        [Display(Name = "注册日期", Description = "注册日期")]
        [DefaultValue("now")]
        public virtual  DateTime RegisterDate { get; set; }
    }
}
//在 SmartAdmin.Data.csproj 项目 SmartDbContext.cs 添加
public virtual DbSet Companies { get; set; }
  • 添加服务对象 Service

在项目 SmartAdmin.Service.csproj 中添加ICompanyService.cs,CompanyService.cs 就是用来实现业务需求 用例的地方

//ICompany.cs
//根据实际业务用例来创建方法,默认的CRUD,增删改查不需要再定义
namespace SmartAdmin.Service
{
  // Example: extending IService and/or ITrackableRepository, scope: ICustomerService
  public interface ICompanyService : IService
  {
    // Example: adding synchronous Single method, scope: ICustomerService
    Company Single(Expression> predicate);
    Task ImportDataTableAsync(DataTable datatable);
    Task ExportExcelAsync(string filterRules = "", string sort = "Id", string order = "asc");
  }
}
// 具体实现接口的方法
namespace SmartAdmin.Service
{
  public class CompanyService : Service, ICompanyService
  {
    private readonly IDataTableImportMappingService mappingservice;
    private readonly ILogger logger;
    public CompanyService(
      IDataTableImportMappingService mappingservice,
      ILogger logger,
      ITrackableRepository repository) : base(repository)
    {
      this.mappingservice = mappingservice;
      this.logger = logger;
    }

    public async Task ExportExcelAsync(string filterRules = "", string sort = "Id", string order = "asc")
    {
      var filters = PredicateBuilder.FromFilter(filterRules);
      var expcolopts = await this.mappingservice.Queryable()
             .Where(x => x.EntitySetName == "Company")
             .Select(x => new ExpColumnOpts()
             {
               EntitySetName = x.EntitySetName,
               FieldName = x.FieldName,
               IgnoredColumn = x.IgnoredColumn,
               SourceFieldName = x.SourceFieldName
             }).ToArrayAsync();

      var works = (await this.Query(filters).OrderBy(n => n.OrderBy(sort, order)).SelectAsync()).ToList();
      var datarows = works.Select(n => new
      {
        Id = n.Id,
        Name = n.Name,
        Code = n.Code,
        Address = n.Address,
        Contect = n.Contect,
        PhoneNumber = n.PhoneNumber,
        RegisterDate = n.RegisterDate.ToString("yyyy-MM-dd HH:mm:ss")
      }).ToList();
      return await NPOIHelper.ExportExcelAsync("Company", datarows, expcolopts);
    }

    public async Task ImportDataTableAsync(DataTable datatable)
    {
      var mapping = await this.mappingservice.Queryable()
                        .Where(x => x.EntitySetName == "Company" &&
                           (x.IsEnabled == true || (x.IsEnabled == false && x.DefaultValue != null))
                           ).ToListAsync();
      if (mapping.Count == 0)
      {
        throw new  NullReferenceException("没有找到Work对象的Excel导入配置信息,请执行[系统管理/Excel导入配置]");
      }
      foreach (DataRow row in datatable.Rows)
      {

        var requiredfield = mapping.Where(x => x.IsRequired == true && x.IsEnabled == true && x.DefaultValue == null).FirstOrDefault()?.SourceFieldName;
        if (requiredfield != null || !row.IsNull(requiredfield))
        {
          var item = new Company();
          foreach (var field in mapping)
          {
            var defval = field.DefaultValue;
            var contain = datatable.Columns.Contains(field.SourceFieldName ?? "");
            if (contain && !row.IsNull(field.SourceFieldName))
            {
              var worktype = item.GetType();
              var propertyInfo = worktype.GetProperty(field.FieldName);
              var safetype = Nullable.GetUnderlyingType(propertyInfo.PropertyType) ?? propertyInfo.PropertyType;
              var safeValue = (row[field.SourceFieldName] == null) ? null : Convert.ChangeType(row[field.SourceFieldName], safetype);
              propertyInfo.SetValue(item, safeValue, null);
            }
            else if (!string.IsNullOrEmpty(defval))
            {
              var worktype = item.GetType();
              var propertyInfo = worktype.GetProperty(field.FieldName);
              if (string.Equals(defval, "now", StringComparison.OrdinalIgnoreCase) && (propertyInfo.PropertyType == typeof(DateTime) || propertyInfo.PropertyType == typeof(Nullable)))
              {
                var safetype = Nullable.GetUnderlyingType(propertyInfo.PropertyType) ?? propertyInfo.PropertyType;
                var safeValue = Convert.ChangeType(DateTime.Now, safetype);
                propertyInfo.SetValue(item, safeValue, null);
              }
              else if (string.Equals(defval, "guid", StringComparison.OrdinalIgnoreCase))
              {
                propertyInfo.SetValue(item, Guid.NewGuid().ToString(), null);
              }
              else if (string.Equals(defval, "user", StringComparison.OrdinalIgnoreCase))
              {
                propertyInfo.SetValue(item, "", null);
              }
              else
              {
                var safetype = Nullable.GetUnderlyingType(propertyInfo.PropertyType) ?? propertyInfo.PropertyType;
                var safeValue = Convert.ChangeType(defval, safetype);
                propertyInfo.SetValue(item, safeValue, null);
              }
            }
          }
          this.Insert(item);
        }
      }
    }

    // Example, adding synchronous Single method
    public Company Single(Expression> predicate)
    {
      
      return this.Repository.Queryable().Single(predicate);

    }
  }
}
  • 添加Controller

MVC Controller

namespace SmartAdmin.WebUI.Controllers
{
  public class CompaniesController : Controller
  {
    private  readonly ICompanyService companyService;
    private readonly IUnitOfWork unitOfWork;
    private readonly ILogger _logger;
    private readonly IWebHostEnvironment _webHostEnvironment;
    public CompaniesController(ICompanyService companyService,
          IUnitOfWork unitOfWork,
          IWebHostEnvironment webHostEnvironment,
          ILogger logger)
    {
      this.companyService = companyService;
      this.unitOfWork = unitOfWork;
      this._logger = logger;
      this._webHostEnvironment = webHostEnvironment;
    }

    // GET: Companies
    public IActionResult Index()=> View();
    //datagrid 数据源
    public async Task GetData(int page = 1, int rows = 10, string sort = "Id", string order = "asc", string filterRules = "")
    {
      try
      {
        var filters = PredicateBuilder.FromFilter(filterRules);
        var total = await this.companyService
                             .Query(filters)
                             .AsNoTracking()
                             .CountAsync()
                              ;
        var pagerows = (await this.companyService
                             .Query(filters)
                              .AsNoTracking()
                           .OrderBy(n => n.OrderBy(sort, order))
                           .Skip(page - 1).Take(rows)
                           .SelectAsync())
                           .Select(n => new
                           {
                             Id = n.Id,
                             Name = n.Name,
                             Code = n.Code,
                             Address = n.Address,
                             Contect = n.Contect,
                             PhoneNumber = n.PhoneNumber,
                             RegisterDate = n.RegisterDate.ToString("yyyy-MM-dd HH:mm:ss")
                           }).ToList();
        var pagelist = new { total = total, rows = pagerows };
        return Json(pagelist);
      }
      catch(Exception e) {
        throw e;
        }

    }
    //编辑 
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task Edit(Company company)
    {
      if (ModelState.IsValid)
      {
        try
        {
          this.companyService.Update(company);

          var result = await this.unitOfWork.SaveChangesAsync();
          return Json(new { success = true, result = result });
        }
         catch (Exception e)
        {
          return Json(new { success = false, err = e.GetBaseException().Message });
        }
      }
      else
      {
        var modelStateErrors = string.Join(",", this.ModelState.Keys.SelectMany(key => this.ModelState[key].Errors.Select(n => n.ErrorMessage)));
        return Json(new { success = false, err = modelStateErrors });
        //DisplayErrorMessage(modelStateErrors);
      }
      //return View(work);
    }
    //新建
    [HttpPost]
    [ValidateAntiForgeryToken]
   
    public async Task Create([Bind("Name,Code,Address,Contect,PhoneNumber,RegisterDate")] Company company)
    {
      if (ModelState.IsValid)
      {
        try
        {
          this.companyService.Insert(company);
       await this.unitOfWork.SaveChangesAsync();
          return Json(new { success = true});
        }
        catch (Exception e)
        {
          return Json(new { success = false, err = e.GetBaseException().Message });
        }

        //DisplaySuccessMessage("Has update a Work record");
        //return RedirectToAction("Index");
      }
      else
       {
        var modelStateErrors = string.Join(",", this.ModelState.Keys.SelectMany(key => this.ModelState[key].Errors.Select(n => n.ErrorMessage)));
        return Json(new { success = false, err = modelStateErrors });
        //DisplayErrorMessage(modelStateErrors);
      }
      //return View(work);
    }
    //删除当前记录
    //GET: Companies/Delete/:id
    [HttpGet]
    public async Task Delete(int id)
    {
      try
      {
        await this.companyService.DeleteAsync(id);
        await this.unitOfWork.SaveChangesAsync();
        return Json(new { success = true });
      }
     
      catch (Exception e)
      {
        return Json(new { success = false, err = e.GetBaseException().Message });
      }
    }
    //删除选中的记录
    [HttpPost]
    public async Task DeleteChecked(int[] id)
    {
      try
      {
        foreach (var key in id)
        {
          await this.companyService.DeleteAsync(key);
        }
        await this.unitOfWork.SaveChangesAsync();
        return Json(new { success = true });
      }
      catch (Exception e)
      {
        return Json(new { success = false, err = e.GetBaseException().Message });
      }
    }
    //保存datagrid编辑的数据
    [HttpPost]
    public async Task AcceptChanges(Company[] companies)
    {
      if (ModelState.IsValid)
      {
        try
        {
          foreach (var item in companies)
          {
            this.companyService.ApplyChanges(item);
          }
          var result = await this.unitOfWork.SaveChangesAsync();
          return Json(new { success = true, result });
        }
        catch (Exception e)
        {
          return Json(new { success = false, err = e.GetBaseException().Message });
        }
      }
      else
      {
        var modelStateErrors = string.Join(",", ModelState.Keys.SelectMany(key => ModelState[key].Errors.Select(n => n.ErrorMessage)));
        return Json(new { success = false, err = modelStateErrors });
      }

    }
    //导出Excel
    [HttpPost]
    public async Task ExportExcel(string filterRules = "", string sort = "Id", string order = "asc")
    {
      var fileName = "compnay" + DateTime.Now.ToString("yyyyMMddHHmmss") + ".xlsx";
      var stream = await this.companyService.ExportExcelAsync(filterRules, sort, order);
      return File(stream, "application/vnd.ms-excel", fileName);
    }
    //导入excel
    [HttpPost]
    public async Task ImportExcel()
    {
      try
      {
        var watch = new Stopwatch();
        watch.Start();
        var total = 0;
        if (Request.Form.Files.Count > 0)
        {
          for (var i = 0; i < this.Request.Form.Files.Count; i++)
          {
            var model = Request.Form["model"].FirstOrDefault() ?? "company";
            var folder = Request.Form["folder"].FirstOrDefault() ?? "company";
            var autosave = Convert.ToBoolean(Request.Form["autosave"].FirstOrDefault());
            var properties = (Request.Form["properties"].FirstOrDefault()?.Split(','));
            var file = Request.Form.Files[i];
            var filename = file.FileName;
            var contenttype = file.ContentType;
            var size = file.Length;
            var ext = Path.GetExtension(filename);
            var path = Path.Combine(this._webHostEnvironment.ContentRootPath, "UploadFiles", folder);
            if (!Directory.Exists(path))
            {
              Directory.CreateDirectory(path);
            }
            var datatable = await NPOIHelper.GetDataTableFromExcelAsync(file.OpenReadStream(), ext);
            await this.companyService.ImportDataTableAsync(datatable);
            await this.unitOfWork.SaveChangesAsync();
            total = datatable.Rows.Count;
            if (autosave)
            {
              var filepath = Path.Combine(path, filename);
              file.OpenReadStream().Position = 0;

              using (var stream = System.IO.File.Create(filepath))
              {
                await file.CopyToAsync(stream);
              }
            }

          }
        }
        watch.Stop();
        return Json(new { success = true, total = total, elapsedTime = watch.ElapsedMilliseconds });
      }
      catch (Exception e) {
        this._logger.LogError(e, "Excel导入失败");
        return this.Json(new { success = false,  err = e.GetBaseException().Message });
      }
        }
    //下载模板
    public async Task Download(string file) {
      
      this.Response.Cookies.Append("fileDownload", "true");
      var path = Path.Combine(this._webHostEnvironment.ContentRootPath, file);
      var downloadFile = new FileInfo(path);
      if (downloadFile.Exists)
      {
       var fileName = downloadFile.Name;
       var mimeType = MimeTypeConvert.FromExtension(downloadFile.Extension);
       var fileContent = new byte[Convert.ToInt32(downloadFile.Length)];
        using (var fs = downloadFile.Open(FileMode.Open, FileAccess.Read, FileShare.Read))
        {
          await fs.ReadAsync(fileContent, 0, Convert.ToInt32(downloadFile.Length));
        }
        return this.File(fileContent, mimeType, fileName);
      }
      else
      {
        throw new FileNotFoundException($"文件 {file} 不存在!");
      }
    }

    }
}
  • 新建 View

MVC Views\Companies\Index

@model SmartAdmin.Data.Models.Customer
@{
  ViewData["Title"] = "客户信息";
  ViewData["PageName"] = "customers_index";
  ViewData["Heading"] = " 客户信息";
  ViewData["Category1"] = "组织架构";
  ViewData["PageDescription"] = "";
}

客户信息

@await Component.InvokeAsync("ImportExcel", new ImportExcelOptions { entity = "Customer", folder = "Customers", url = "/Customers/ImportExcel", tpl = "/Customers/Download" }) @section HeadBlock { } @section ScriptsBlock { }

上面View层的代码非常的复杂,但都是固定格式,可以用scaffold快速生成

  • 配置依赖注入(DI),注册服务

打开 startup.cs 在 public void ConfigureServices(IServiceCollection services)
注册服务
services.AddScoped, RepositoryX>();
services.AddScoped();

  • 更新数据库

EF Core Code-First 同步更新数据库
在 Visual Studio.Net
Package Manager Controle 运行
PM>:add-migration create_Company
PM>:update-database
PM>:更新完成

  • Debug 运行项目


    image

高级应用

CAP 分布式事务的解决方案及应用场景
nuget 安装组件
PM> Install-Package DotNetCore.CAP
PM> Install-Package DotNetCore.CAP.RabbitMQ
PM> Install-Package DotNetCore.CAP.SqlServer \

  • 配置Startup.cs
public void ConfigureServices(IServiceCollection services)
    {
      services.AddCap(x =>
      {
        x.UseEntityFramework();
        x.UseRabbitMQ("127.0.0.1");
        x.UseDashboard();
        x.FailedRetryCount = 5;
        x.FailedThresholdCallback = failed =>
        {
          var logger = failed.ServiceProvider.GetService>();
          logger.LogError($@"A message of type {failed.MessageType} failed after executing {x.FailedRetryCount} several times, 
                        requiring manual troubleshooting. Message name: {failed.Message.GetName()}");
        };
      });
    }
  • 发布消息
  • 订阅消息

roadmap

  • 支持My SQL数据库
  • 还会继续重构和完善代码
  • 开发Scaffold MVC模板,生成定制化的Controller 和 View 减少开发人员重复工作
  • 完善授权访问策略(policy-based authorization)
  • 开发Visual Sutdio.net代码生成插件(类似国内做比较好的52abp)

我的联系方式,qq群,赞助二维码

me.png

qqcode.png

wx.jpg

src="https://code.jquery.com/jquery-3.5.1.slim.min.js"
integrity="sha256-4+XzXVhsDmqanXGHaHvgh1gMQKX40OUvDEBTu8JcmNs="
crossorigin="anonymous">

你可能感兴趣的:(基于领域驱动设计(DDD)超轻量级快速开发架构[GitHub开源代码])