开发指南
实体定义
QuickAdmin.EntityBase 类库提供了多组基础接口及默认实现, 包括自增值、审计、软删除、显示顺序、树形结构等等,可利用它们来快速定义各种实体,这些实体的主键属性均为 Id,主键字段名称默认也为 Id。
QuickAdmin.Net 内置了与这些不同类型实体匹配的实体服务,服务内部会自动处理相关属性。
Sortable 打头的基类表示可指定记录显示顺序的实体(含有一个 DisplayOrder 属性),SoftDeletion 打头的基类表示软删除实体, Audit 打头的基类表示包含录入人、更新人信息的审计实体,FullAudit 打头的基类表示包含录入人、更新人、删除人信息的审计实体(即它也是软删除实体)。
TIP
QuickAdmin.Net 内的系统菜单、组织机构、用户等等均是 Sortable 实体以便能够调整显示顺序,可去示例项目或在线演示看看效果。
定义实体时可从这些基类继承,然后书写业务属性即可。以下为实体定义部分示例。
using QuickAdmin.EntityBase;
using QuickAdmin.EntityBase.AuditBaseImplementation;
using FreeSql.DataAnnotations;
// long 类型主键实体
public class MyEntity1 : EntityWithIdKey
{
}
// long 类型自增值主键实体
public class MyEntity2 : EntityWithAutoIdKey
{
}
// int 类型主键实体
public class MyEntity3 : EntityWithIdKey<int>
{
}
// string 类型主键实体
public class MyEntity4 : EntityWithIdKey<string>
{
}
// UUID 类型主键实体(主键数据类型为 string,默认值为 32 位 GUID)
public class MyEntity5 : EntityWithUUIDKey
{
}
// long 类型自增值主键,并可指定显示顺序的实体
public class MyEntity6 : SortableEntityWithAutoIdKey
{
}
// 包含审计字段的 long 类型自增值主键实体
public class MyEntity7 : AuditEntityWithAutoIdKey
{
}
// 可指定显示顺序的,UUID 类型主键的,审计+软删除 实体
public class MyEntity8 : SortableFullAuditEntityWithUUIDKey
{
}
// 与部门相关的业务数据实体
// QuickAdmin.Net 内置了对 与部门相关的业务数据 的关联查询支持,可递归查询某机构下的业务数据,
// 只需实体实现 IDeptRelatedEntity 或 IDeptRelatedEntityWithDeptPath,然后用内置的 DeptRelatedCRUDService 即可进行递归查询
public class MyEntity9 : EntityWithAutoIdKey, IDeptRelatedEntityWithDeptPath
{
[Column(IsNullable = false, StringLength = 36)]
public string DeptId { get; set; }
// 此属性不映射到表字段,通常是在列表里需要显示数据所属部门时使用
[Column(IsIgnore = true)]
public string DeptPath { get; set; }
}
// 树形结构示例,比如行政区域实体
// QuickAdmin.Net 内置了可对树形结构实体进行增、删、改、查,并可递归查询的相关服务
public class Area : EntityWithIdKey<string>, ITreeEntity<Area>, IEntityWithNodeLevel
{
public string Name { get; set; }
public string ParentId { get; set; }
[Navigate(nameof(ParentId))]
[System.Text.Json.Serialization.JsonIgnore]
public Area Parent { get; set; }
[Navigate(nameof(ParentId))]
public List<Area> Childs { get; set; } = new List<Area>();
// 可在树形结构实体内加入一个节点级别字段,以方便某些查询需求。节点级别从 1 开始,1 表示根节点
// QuickAdmin.Net 的内置 TreeEntityCRUDService 服务支持在添加记录时自动处理 NodeLevel
public int NodeLevel { get; set; }
}
当要适配现有数据库去定义实体时,若基类的某些定义与现有数据表不一致,可通过重写去达成适配。
比如该数据库各表的审计字段只有录入人/录入时间,没有修改人/修改时间,且录入人/录入时间字段名称不是 AuditEntity 里的属性名称,此时可自行定义一个基类并重写审计属性,相关实体再从该基类继承:
public abstract class MyAuditEntityBase : AuditEntityWithAutoIdKey
{
[Column(CanUpdate = false, IsNullable = false, Name = "LrrId")] // 字段名称不是 CreatorId,而是 LrrId
public override string CreatorId { get => base.CreatorId; set => base.CreatorId = value; }
[Column(CanUpdate = false, ServerTime = DateTimeKind.Local, Name = "LrShj")] // 字段名称不是 CreatedTime,而是 LrShj
public override DateTime CreatedTime { get => base.CreatedTime; set => base.CreatedTime = value; }
[Column(IsIgnore = true)] // 表中没有 UpdaterId 字段
public override string UpdaterId { get => base.UpdaterId; set => base.UpdaterId = value; }
[Column(IsIgnore = true)] // 表中没有 UpdatedTime 字段
public override DateTime? UpdatedTime { get => base.UpdatedTime; set => base.UpdatedTime = value; }
}
[Table(Name = "some_table")]
public class SomeEntity : MyAuditEntityBase
{
}
实体服务
QuickAdmin.Service 类库提供了通用 CRUD/Paging 服务,可利用它们来快速定义各种实体的 CRUD 和分页查询服务,参照下面的介绍去使用。
使用内置的通用服务不是必须的,你可以完全自行实现自己的实体服务。
设计实体类型
通用 CRUD 需要一个实体设计三个类型:实体类型、输入类型和过滤器类型。
输入类型可以直接就用实体类型或者单独设计,视情况而定。单独设计时,必须用 AutoMapper 建立输入类型与实体类型可互相转换的映射关系。
过滤器类型用来在查询数据时构造查询条件,可设计为从 CommonFilter 继承的一个类,这样能够充分利用本类库内置的各种查询功能,并简化使用者代码。
以 ASP.NET Core 教程里的电影信息数据表为例,其相关类型可设计如下:
// Movie 实体,将其设计为 FullAuditEntity
public class Movie : FullAuditEntityWithAutoIdKey
{
[Required]
public string Title { get; set; }
public DateTime ReleaseDate { get; set; }
public string Genre { get; set; }
public decimal Price { get; set; }
}
// Movie 输入DTO
public class MovieInput
{
public string Title { get; set; }
public DateTime ReleaseDate { get; set; }
public string Genre { get; set; }
public decimal Price { get; set; }
}
// Movie 过滤器DTO,可以自行设计:
public class MovieFilter
{
public string Title { get; set; }
public DateTime? BeginReleaseDate { get; set; }
public DateTime? EndReleaseDate { get; set; }
public decimal? MinPrice { get; set; }
public decimal? MaxPrice { get; set; }
}
// 也可以直接继承自 CommonFilter:
public class MovieFilter : QuickAdmin.Entity.DTO.Common.CommonFilter
{
// 继承后仍然可以定义更多过滤条件
}
需要用 AutoMapper 建立 Movie 和 MovieInput 可互相转换的映射关系:
// 定义 AutoMapper Profile
public class MyAutoMapperProfile : AutoMapper.Profile
{
public MyAutoMapperProfile()
{
CreateMap<Movie, MovieInput>().ReverseMap();
...
}
}
// 然后在启动代码加入:
builder.Services.AddAutoMapper(typeof(MyAutoMapperProfile));
定义实体服务
QuickAdmin.Service 类库内通用 CRUD 的核心类型是 ICRUDService<> 泛型基接口和 CRUDServiceBase<> 泛型抽象基类, CRUDServiceBase 内实现了增删改查以及分页查询服务,然后针对不同的实体类型又派生出若干泛型基类,如对 ISortableEntity 类型实体,实现对记录显示顺序的调整,对树形结构实体,实现各种树形查询等等。 你可以利用这些接口/基类快速实现自己的实体服务。
各个泛型基类对一些操作尽可能的进行了分解,以方便继承类通过重写进行定制,具体请查阅其参考文档。
使用方法很简单:根据自己实体的类型从 QuickAdmin.Service 命名空间下选取合适的接口和泛型基类, 然后定义自己的实体的服务接口和服务实现类,服务接口继承一下选取的接口,服务实现类则直接继承对应的泛型基类并实现你的服务接口即可。
以上侧的 Movie 实体为例,当 MovieFilter 完全自定义时其实体服务可设计如下:
using QuickAdmin.Service;
// Movie CRUD服务接口
public interface IMovieService : ICRUDService<Movie, long, MovieInput, MovieFilter>
{
// 可继续定义对 Movie 的其它操作,并在 MovieService 里去实现
}
// Movie CRUD服务实现类,继承自抽象基类,必须重写应用过滤器的各方法
public class MovieService : CRUDServiceBase<Movie, long, MovieInput, MovieFilter>, IMovieService
{
protected override ISelect<Movie> ApplyFilter(ISelect<Movie> iSel, MovieFilter filter)
{
if (filter == null) return iSel;
if (filter.Title.NotNullNorEmpty())
iSel = iSel.Where(a => a.Title.Contains(filter.Title));
if (filter.BeginReleaseDate.HasValue)
iSel = iSel.Where(a => a.ReleaseDate >= filter.BeginReleaseDate);
if (filter.EndReleaseDate.HasValue)
iSel = iSel.Where(a => a.ReleaseDate <= filter.EndReleaseDate);
if (filter.MinPrice.HasValue)
iSel = iSel.Where(a => a.Price >= filter.MinPrice);
if (filter.MaxPrice.HasValue)
iSel = iSel.Where(a => a.Price <= filter.MaxPrice);
return iSel;
}
protected override IServiceResult<IUpdate<Movie>> GetFilteredIUpdate(MovieFilter filter)
{
if (filter == null)
throw new ArgumentNullException(nameof(filter));
IUpdate<Movie> iUpdate = IUpdateObj;
if (filter.Title.NotNullNorEmpty())
iUpdate = iUpdate.Where(a => a.Title.Contains(filter.Title));
if (filter.BeginReleaseDate.HasValue)
iUpdate = iUpdate.Where(a => a.ReleaseDate >= filter.BeginReleaseDate);
if (filter.EndReleaseDate.HasValue)
iUpdate = iUpdate.Where(a => a.ReleaseDate <= filter.EndReleaseDate);
if (filter.MinPrice.HasValue)
iUpdate = iUpdate.Where(a => a.Price >= filter.MinPrice);
if (filter.MaxPrice.HasValue)
iUpdate = iUpdate.Where(a => a.Price <= filter.MaxPrice);
return ServiceResult.Ok(iUpdate);
}
protected override IServiceResult<IDelete<Movie>> GetFilteredIDelete(MovieFilter filter)
{
if (filter == null)
throw new ArgumentNullException(nameof(filter));
IDelete<Movie> iDel = IDeleteObj;
if (filter.Title.NotNullNorEmpty())
iDel = iDel.Where(a => a.Title.Contains(filter.Title));
if (filter.BeginReleaseDate.HasValue)
iDel = iDel.Where(a => a.ReleaseDate >= filter.BeginReleaseDate);
if (filter.EndReleaseDate.HasValue)
iDel = iDel.Where(a => a.ReleaseDate <= filter.EndReleaseDate);
if (filter.MinPrice.HasValue)
iDel = iDel.Where(a => a.Price >= filter.MinPrice);
if (filter.MaxPrice.HasValue)
iDel = iDel.Where(a => a.Price <= filter.MaxPrice);
return ServiceResult.Ok(iDel);
}
}
当 MovieFilter 继承自 CommonFilter 时其实体服务可设计如下:
using QuickAdmin.Service;
// Movie CRUD服务接口
public interface IMovieService : ICRUDService<Movie, long, MovieInput, MovieFilter>
{
// 可继续定义对 Movie 的其它操作,并在 MovieService 里去实现
}
// Movie CRUD服务实现类,直接使用 CommonCRUDService
public class MovieService : CommonCRUDService<Movie, long, MovieInput, MovieFilter>, IMovieService
{
}
如此定义后,IMovieService 就已经具备了增删改查和分页查询功能。
CRUDServiceBase/CommonCRUDService 泛型基类内对一些操作进行了分解,如果需要,你可在 MovieService 里方便的重写相关方法/属性去定制要执行的逻辑。
Movie 被设计为 FullAuditEntity,CRUDServiceBase 内会自动处理相关审计字段,不需要在继承类 MovieService 里做任何额外实现:添加记录时会自动填充录入人/录入时间, 更新记录时会自动填充修改人/修改时间,删除记录时则是软删除而不会物理删除(填充删除人/删除时间)。
有关内置服务的更多介绍参见通用 CRUD 章节。
注册服务
单个服务注册:
builder.Services.AddQuickAdmin(...);
// 在调用 AddQuickAdmin() 之后去注册
builder.Services.AddSingleton(typeof(IMovieService), typeof(MovieService));
当实体服务位于独立的程序集内或位于某个命名空间下,可利用 QuickAdmin.Net 提供的批量注册方法去注册,只需一行代码,例如:
using QuickAdmin.Common;
...
// 注册类型 IMovieService 所在程序集内的所有满足条件的服务
builder.Services.AddServicesFromAssemblyExceptNotUseDIs(typeof(IMovieService).Assembly);
// 注册类型 SomeType 所在程序集内的,"QHSE.Service" 命名空间下的所有满足条件的服务
builder.Services.AddServicesFromAssemblyExceptNotUseDIs(typeof(SomeType).Assembly, "QHSE.Service.");
参见 AddServicesFromAssemblyExceptNotUseDIs 以及其它批量注册扩展方法文档, 以及 UseDI/NotUseDI 特性说明。
使用服务
用常规做法使用,比如采用构造函数注入的方式,将服务注入到控制器或页面中:
public class MyController : ControllerBase
{
IMovieService movieService;
public MyController(IMovieService movieService)
{
this.movieService = movieService;
}
}
也可利用 QuickAdmin.Net 提供的一组获取服务的方法,如 GetAppService<T>, 随时获取所需的服务:
using QuickAdmin.Common;
public class MyPageModel : PageModel
{
public void OnGet()
{
var service = G.GetAppService<IMovieService>();
var pagingOutput = service.GetPagingOutput();
...
}
public IActionResult OnPostDeleteMovie(long id)
{
...
var service = G.GetAppService<IMovieService>();
var serviceResult = service.Delete(id);
...
}
}
操作其它数据库的数据
要操作与系统表不在同一数据库的其它数据库中的数据,你需要构造相应的 IFreeSql 实例,然后重写 fsql 属性即可:
// 构造其它数据库 IFreeSql 实例的类
public class OthDbs
{
static Lazy<IFreeSql> _sqlServerLazy = new Lazy<IFreeSql>(() => new FreeSql.FreeSqlBuilder()
.UseConnectionString(FreeSql.DataType.SqlServer, "Data Source=(local);User ID=user;Password=pwd;Initial Catalog=db;Pooling=true;Min Pool Size=1;TrustServerCertificate=true")
.Build()
);
public static IFreeSql SqlServer => _sqlServerLazy.Value;
}
public interface IMovieService : ICRUDService<Movie, long, MovieInput, MovieFilter>
{
}
public class MovieService : CommonCRUDService<Movie, long, MovieInput, MovieFilter>, IMovieService
{
// 操作指定 SqlServer 数据库里的 Movie 数据
protected override IFreeSql fsql => OthDbs.SqlServer;
}
Razor 页面
QuickAdmin.Net 内置了布局文件、页面基类以及对 FineUICore 的支持,创建 Razor 页面时你可自行决定是否使用它们。
使用内置的布局文件、页面基类来创建 Razor 页面,你将能得到 QuickAdmin.Net 提供的全部页面功能;只使用二者之一,则得到部分页面功能。
FineUICore 提供了丰富的控件,可快速构建页面,但你也可以选择用 html/css/js 构建页面内容。
页面布局
QuickAdmin.Net 内置了布局文件 "_QAdminLayout.cshtml",修改相应 _ViewStart.cshtml 和 _ViewImports.cshtml 文件来使用该布局。
修改 _ViewStart.cshtml:
@{
Layout = "/Areas/QAdmin/Pages/Shared/_QAdminLayout.cshtml";
}
修改 _ViewImports.cshtml:
@using QuickAdminApp
@namespace QuickAdminApp.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using QuickAdmin.RCL
@using FineUICore
@addTagHelper *, FineUICore
若是用 QuickAdmin.Net.Templates 里的模板创建的项目,_ViewStart.cshtml 和 _ViewImports.cshtml 文件已改好。
页面结构
QAdminBasePageModel 是 QuickAdmin.Net 提供的一个页面基类, 新建 Razor 页面后将其改为从该类继承即可使用 QuickAdmin.Net 的各项内置页面功能:登录判断、授权判断、自动加载 css/js 资源、辅助方法等等,具体参考 QAdminBasePageModel 类文档。
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using QuickAdmin.RCL;
namespace QuickAdminApp.Pages
{
public class SimplePageModel : QAdminBasePageModel
{
public IActionResult OnGet()
{
return Page();
}
}
}
不使用 QAdminBasePageModel 页面基类也是支持的,只不过你需要自行处理相关逻辑。
"_QAdminLayout.cshtml" 布局文件要求页面的典型结构是这样的:
@page
@model QuickAdminApp.Pages.SimplePageModel
@{
ViewData["Title"] = "Simple Page";
var F = @Html.F();
}
@section head {
}
@section body {
}
@section script {
}
即页面内可包含三个 Section:"head"、"body" 和 "script"。
可只包含 "body" Section:
@page
@model QuickAdminApp.Pages.SimplePageModel
@{
ViewData["Title"] = "Simple Page";
var F = @Html.F();
}
@section body {
<div>
this is a simple page
</div>
}
也可一个 Section 也不包含:
@page
@model QuickAdminApp.Pages.SimplePageModel
@{
ViewData["Title"] = "Simple Page";
}
<div>
this is a simple page
</div>
此时所书写的内容会被全部当作 "body" Section。注意若没有 "body" Section,"head" 和 "script" Section 也不能有。
自定义登录页
可自行设计实现自己的登录页,只需要在你的登录验证成功后再去调用一个 IAuthService 服务的执行登录逻辑的方法: Login、DirectlyLogin、DirectlyLoginWithMobile 或 DirectlyLoginWithEmail。
如果仍然是让用户输入账户和密码登录,后端可去调用 Login 方法;如果登录验证完全是在自己的登录逻辑内处理的(比如用手机验证码登录、第三方登录等),那么验证成功后再去调用 DirectlyLogin 或 DirectlyLoginWithMobile 进入登录状态:
...
// 获取 IAuthService
var authService = G.GetAppService<IAuthService>();
// 调用方法
var serviceResult = authService.DirectlyLogin(...);
...
如果你的登录页不是应用根目录下的 Login 页,则还需在配置文件里的 "RCL:LoginPageName" 项去指定,对应属性为 LoginPageName。
自定义默认页
若不想用 QuickAdmin.Net 内置的 Index 作为默认页,只需保留应用 Pages 目录下的 Index 页即可,在其中自行设计布局。
IAuthService 包含获取用户可用的系统菜单、快速链接等的方法,如有需要可去调用获取。
如果默认页不是应用根目录下的 Index 页,则还需在配置文件里的 "RCL:DefaultPageName" 项去指定,对应属性为 DefaultPageName。
自定义内置服务
QuickAdmin.Net 内置的各个服务均可被定制,只需定义继承自内置服务的类,重写需要定制的方法/属性,然后将该类在应用启动时注册到系统服务即可。
例如 ClientInfoParser 用来解析客户端信息,重写 GetDevice() 去完善用户设备信息的识别:
public class MyClientInfoParser : ClientInfoParser
{
protected override string GetDevice(IHeaderDictionary headers)
{
// 解析设备信息
}
}
重写 AuthService 相应方法执行额外操作:
public class MyAuthService : AuthService
{
protected override LoginOutput OnLoginSucceed(SysUser user, int? loginWay, LoginInput loginInput, SysAutologinToken autologinToken, string identifier, LoginClientInfo clientInfo)
{
LoginOutput loginOutput = base.OnLoginSucceed(user, loginWay, loginInput, autologinToken, identifier, clientInfo);
// 成功登录后的额外操作
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, user.AccountId),
new Claim(ClaimTypes.Role, "SomeRole")
};
var claimsIdentity = new ClaimsIdentity(
claims, CookieAuthenticationDefaults.AuthenticationScheme);
G.Context.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity)
).GetAwaiter().GetResult();
return loginOutput;
}
protected override void AfterLogout(LoggedOnUser loggedOnUser)
{
base.AfterLogout(loggedOnUser);
// 注销后的额外操作
G.Context?.SignOutAsync().GetAwaiter().GetResult();
}
}
重写 UserService 以下方法可强制用户在修改密码后重新登录:
public class MyUserService : UserService
{
public override IServiceResult CurrentUserChangePassword(string oldPwd, string newPwd)
{
IServiceResult result = base.CurrentUserChangePassword(oldPwd, newPwd);
if (!result.Success)
return result;
return ServiceResult.Ok<bool>(true);
}
}
在启动时将新的实现类注册到系统服务:
builder.Services.AddQuickAdmin(...);
// 必须在调用 AddQuickAdmin() 之后去注册
builder.Services.AddSingleton(typeof(IClientInfoParser), typeof(MyClientInfoParser));
builder.Services.AddSingleton(typeof(IAuthService), typeof(MyAuthService));
builder.Services.AddSingleton(typeof(IUserService), typeof(MyUserService));
如无特别说明,所有服务均要注册为 Singleton 单例模式。