smartadmin.core.urf offers a complete, modular and layered software architecture based on Domain Driven Design principles and patterns. It also provides the necessary infrastructure to implement this architecture, focusing on the core of the business Demand, reduce duplication coding, allow junior programmers to develop professional and beautiful Web applications
Domain-driven design (DDD) is the concept that the structure and language of software code (class names, class methods, class variables) should match the business domain. For example, if a software processes loan applications, it might have classes such as LoanApplication and Customer, and methods such as AcceptOffer and Withdraw. DDD connects the implementation to an evolving model. Domain-driven design is predicated on the following goals:
- placing the project's primary focus on the core domain and domain logic;
- basing complex designs on a model of the domain;
- initiating a creative collaboration between technical and domain experts to iteratively refine a conceptual model that addresses particular domain problems.。
Demo Site
UserName:demo Password:123456 Demo (https://admin.i247365.net/)
Please give me Star if you like it. Every Star is a motivation to encourage me to continue updating.
Layers
smartadmin.core.urf follow the DDD design pattern to implement the four-layer model of the application
- Presentation Layer:This layer is the part where interaction with external systems happens. This layer is the gateway to the effects that a human, an application or a message will have on the domain. Requests will be accepted from this layer and the response will be shaped in this layer and displayed to the user.the project use SmartAdmin - Responsive WebApp and Jquery EasyUI
- Application Layer:It is the layer where business process flows are handled. The capabilities of the application can be observed in this layer. Domain entities are created and subject to update here. Depending on the usage scenarios, topics such as transaction management are also resolved here. In this layer, execution of work commands and reactions to domain events are coded. The code snippet for handling the CreateUser work command is given below as an example. In this example, by creating an object of User that comes from the Domain Layer and storing this object in the data storage, request for user creation is resolved. the project:StartAdmin.Service.csproj
- Domain Layer:This will be the core of the application. It is the layer where all business rules related to the problem to be solved are included. In this layer; entities, value objects, aggregates, factories and interfaces will take place. This layer should be kept away from dependencies as much as possible. Third party libraries should not be added as much as possible, as it should not take other layers as a reference.the project use EntityFrmework Core Code-first and Repository Implement. the project:StartAdmin.Domain.csproj
- Infrastructure Layer:This layer will be the layer that accesses external services such as database, messaging systems and email services. The IUserRepository interface designed in the domain layer and used in the application layer will be implemented in this layer and gain an identity.the project use:Nlog,service discovery:Swagger UI,EventBus:dotnetcore/CAP,Authentication and Authorization:Microsoft.AspNetCore.Identity,etc.
Domain Layer
- Entity:(SmartAdmin.Entity.csproj)
- Inherit a base class "Entity",Add necessary audit classes such as: creation time, last modification time, etc.
- There must be a primary key, preferably GRUID (composite primary key is not recommended), but this project uses an incremental int type
- The field should not be too redundant, you can define the association relationship
- Use virtual keywords as much as possible for field properties and methods. Some ORM and dynamic proxy tools require
Application Layer
Infrastructure
- Visual Studio .Net 2019
- .Net 5.0.1
- Sql Server(LocalDb)
- Include="AutoMapper" Version="10.1.1"
- Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="8.1.1"
- Include="DotNetCore.CAP" Version="3.1.2"
- Include="DotNetCore.CAP.Dashboard" Version="3.1.2"
- Include="DotNetCore.CAP.RabbitMQ" Version="3.1.2"
- Include="DotNetCore.CAP.SqlServer" Version="3.1.2"
- Include="DotNetCore.NPOI" Version="1.2.3"
- Include="Mapster" Version="7.1.5"
- Include="MediatR" Version="9.0.0"
- Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="9.0.0"
- Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.3"
- Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="5.0.3"
- Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="5.0.3"
- Include="Microsoft.AspNetCore.Identity.UI" Version="5.0.3"
- Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="5.0.3"
- Include="Microsoft.AspNetCore.Mvc.ViewFeatures" Version="2.2.0"
- Include="Microsoft.AspNetCore.SignalR" Version="1.1.0"
- Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.3"
- Include="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.3"
- Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.3">
- Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="5.0.2"
- Include="NLog" Version="4.7.8"
- Include="NLog.Extensions.Logging" Version="1.7.1"
- Include="NLog.Web.AspNetCore" Version="4.11.0"
- Include="Swashbuckle.AspNetCore" Version="6.1.0"
- Include="System.Data.SqlClient" Version="4.8.2"
- Include="System.Linq.Dynamic.Core" Version="1.2.8"
Start with the simple requirement \ develop CRUD, import and export function with Company object
SmartAdmin.Entity.csproj>Models new Company.cs class
//Note: define the best practices of entity objects, inherit the base class, use the virtual keyword, and define every attribute, name, type, length, verification rule, index, default value, etc. as much as possible 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 Contact { 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<Company> Companies { get; set; }
Add Service Layer
the project SmartAdmin.Service.csproj add ICompanyService.cs,CompanyService.cs implement business requirements and use cases
//ICompany.cs //Create methods based on actual business use cases, the default CRUD, additions, deletions, and changes do not need to be defined namespace SmartAdmin.Service { // Example: extending IService<TEntity> and/or ITrackableRepository<TEntity>, scope: ICustomerService public interface ICompanyService : IService<Company> { // Example: adding synchronous Single method, scope: ICustomerService Company Single(Expression<Func<Company, bool>> predicate); Task ImportDataTableAsync(DataTable datatable); Task<Stream> ExportExcelAsync(string filterRules = "", string sort = "Id", string order = "asc"); } }
// implementation of the interface method namespace SmartAdmin.Service { public class CompanyService : Service<Company>, ICompanyService { private readonly IDataTableImportMappingService mappingservice; private readonly ILogger<CompanyService> logger; public CompanyService( IDataTableImportMappingService mappingservice, ILogger<CompanyService> logger, ITrackableRepository<Company> repository) : base(repository) { this.mappingservice = mappingservice; this.logger = logger; }
public async Task
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<DateTime>)))
{
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<Func<Company, bool>> predicate) {
return this.Repository.Queryable().Single(predicate);
} } }
new Controller
MVC Controller
namespace SmartAdmin.WebUI.Controllers { public class CompaniesController : Controller { private readonly ICompanyService companyService; private readonly IUnitOfWork unitOfWork; private readonly ILogger<CompaniesController> _logger; private readonly IWebHostEnvironment _webHostEnvironment; public CompaniesController(ICompanyService companyService, IUnitOfWork unitOfWork, IWebHostEnvironment webHostEnvironment, ILogger<CompaniesController> logger) { this.companyService = companyService; this.unitOfWork = unitOfWork; this._logger = logger; this._webHostEnvironment = webHostEnvironment; }
// GET: Companies
public IActionResult Index()=> View();
//datagrid 数据源
public async Task
}
//编辑
[HttpPost]
[ValidateAntiForgeryToken]
public async Task
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
//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
catch (Exception e)
{
return Json(new { success = false, err = e.GetBaseException().Message });
}
}
//删除选中的记录
[HttpPost]
public async Task
}
//导出Excel
[HttpPost]
public async Task
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
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"] = "<i class='fal fa-window text-primary'></i> 客户信息"; ViewData["Category1"] = "组织架构"; ViewData["PageDescription"] = ""; } <div class="row"> <div class="col-lg-12 col-xl-12"> <div id="panel-1" class="panel"> <div class="panel-hdr"> <h2> 客户信息 </h2> <div class="panel-toolbar"> <button class="btn btn-panel bg-transparent fs-xl w-auto h-auto rounded-0" data-action="panel-collapse" data-toggle="tooltip" data-offset="0,10" data-original-title="Collapse"><i class="fal fa-window-minimize"></i></button> <button class="btn btn-panel bg-transparent fs-xl w-auto h-auto rounded-0" data-action="panel-fullscreen" data-toggle="tooltip" data-offset="0,10" data-original-title="Fullscreen"><i class="fal fa-expand"></i></button> </div>
<div id="customerdetailwindow" class="easyui-window" title="明细数据" data-options="modal:true, closed:true, minimizable:false, collapsible:false, maximized:false, iconCls:'fal fa-window', onBeforeClose:function(){ var that = $(this); if(customerhasmodified()){ $.messager.confirm('确认','你确定要放弃保存修改的记录?',function(r){ if (r){ var opts = that.panel('options'); var onBeforeClose = opts.onBeforeClose; opts.onBeforeClose = function(){}; that.panel('close'); opts.onBeforeClose = onBeforeClose; hook = false; } }); return false; } }, onOpen:function(){ $(this).window('vcenter'); $(this).window('hcenter'); }, onRestore:function(){ }, onMaximize:function(){ } " style="width:820px;height:420px;display:none">
@await Component.InvokeAsync("ImportExcel", new ImportExcelOptions { entity = "Customer", folder = "Customers", url = "/Customers/ImportExcel", tpl = "/Customers/Download"
})
@section HeadBlock {
} @section ScriptsBlock {
}
> The code of the View layer above is very complicated, but they are all in a fixed format and can be quickly generated with scaffold
+ Configure dependency injection (DI), register services
> Open startup.cs 在 public void ConfigureServices(IServiceCollection services)
> register services
services.AddScoped<IRepositoryX<Customer>, RepositoryX<Customer>>(); \
services.AddScoped<ICustomerService, CustomerService>();
+ migration database
> EF Core Code-First migration \
> in Visual Studio.Net \
> Package Manager Controle run \
>PM>:add-migration create_Company \
>PM>:update-database \
>PM>:migration
+ Debug project
![](https://raw.githubusercontent.com/neozhu/smartadmin.core.urf/master/img/meitu_1.jpg)
## 高级应用
> CAP The solution and application scenario of distributed transaction \
> nuget install \
>PM> Install-Package DotNetCore.CAP \
>PM> Install-Package DotNetCore.CAP.RabbitMQ \
>PM> Install-Package DotNetCore.CAP.SqlServer \
+ config Startup.cs
```javascript
public void ConfigureServices(IServiceCollection services)
{
services.AddCap(x =>
{
x.UseEntityFramework<SmartDbContext>();
x.UseRabbitMQ("127.0.0.1");
x.UseDashboard();
x.FailedRetryCount = 5;
x.FailedThresholdCallback = failed =>
{
var logger = failed.ServiceProvider.GetService<ILogger<Startup>>();
logger.LogError($@"A message of type {failed.MessageType} failed after executing {x.FailedRetryCount} several times,
requiring manual troubleshooting. Message name: {failed.Message.GetName()}");
};
});
}
qq群!