开发指南

实体定义

QuickAdmin.EntityBaseopen in new window 类库提供了多组基础接口及默认实现, 包括自增值、审计、软删除、显示顺序、树形结构等等,可利用它们来快速定义各种实体,这些实体的主键属性均为 Id,主键字段名称默认也为 Id
QuickAdmin.Net 内置了与这些不同类型实体匹配的实体服务,服务内部会自动处理相关属性。

Sortable 打头的基类表示可指定记录显示顺序的实体(含有一个 DisplayOrderopen in new window 属性),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.Serviceopen in new window 类库提供了通用 CRUD/Paging 服务,可利用它们来快速定义各种实体的 CRUD 和分页查询服务,参照下面的介绍去使用。
使用内置的通用服务不是必须的,你可以完全自行实现自己的实体服务。

设计实体类型

通用 CRUD 需要一个实体设计三个类型:实体类型、输入类型和过滤器类型。
输入类型可以直接就用实体类型或者单独设计,视情况而定。单独设计时,必须用 AutoMapper 建立输入类型与实体类型可互相转换的映射关系。
过滤器类型用来在查询数据时构造查询条件,可设计为从 CommonFilteropen in new window 继承的一个类,这样能够充分利用本类库内置的各种查询功能,并简化使用者代码。

以 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 建立 MovieMovieInput 可互相转换的映射关系:

// 定义 AutoMapper Profile
public class MyAutoMapperProfile : AutoMapper.Profile
{
  public MyAutoMapperProfile()
  {
    CreateMap<Movie, MovieInput>().ReverseMap();
    ...
  }
}

// 然后在启动代码加入:
builder.Services.AddAutoMapper(typeof(MyAutoMapperProfile));

定义实体服务

QuickAdmin.Service 类库内通用 CRUD 的核心类型是 ICRUDService<>open in new window 泛型基接口和 CRUDServiceBase<>open in new window 泛型抽象基类, CRUDServiceBase 内实现了增删改查以及分页查询服务,然后针对不同的实体类型又派生出若干泛型基类,如对 ISortableEntity 类型实体,实现对记录显示顺序的调整,对树形结构实体,实现各种树形查询等等。 你可以利用这些接口/基类快速实现自己的实体服务。
各个泛型基类对一些操作尽可能的进行了分解,以方便继承类通过重写进行定制,具体请查阅其参考文档。

使用方法很简单:根据自己实体的类型从 QuickAdmin.Serviceopen in new window 命名空间下选取合适的接口和泛型基类, 然后定义自己的实体的服务接口和服务实现类,服务接口继承一下选取的接口,服务实现类则直接继承对应的泛型基类并实现你的服务接口即可。

以上侧的 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.");

参见 AddServicesFromAssemblyExceptNotUseDIsopen in new window 以及其它批量注册扩展方法文档, 以及 UseDIopen in new window/NotUseDIopen in new window 特性说明。

使用服务

用常规做法使用,比如采用构造函数注入的方式,将服务注入到控制器或页面中:

public class MyController : ControllerBase
{
  IMovieService movieService;
  public MyController(IMovieService movieService)
  {
      this.movieService = movieService;
  }
}

也可利用 QuickAdmin.Net 提供的一组获取服务open in new window的方法,如 GetAppService<T>open in new window, 随时获取所需的服务:

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 实例,然后重写 fsqlopen in new window 属性即可:

// 构造其它数据库 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 文件已改好。

页面结构

QAdminBasePageModelopen in new window 是 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 也不能有。

自定义登录页

可自行设计实现自己的登录页,只需要在你的登录验证成功后再去调用一个 IAuthServiceopen in new window 服务的执行登录逻辑的方法: Login、DirectlyLogin、DirectlyLoginWithMobile 或 DirectlyLoginWithEmail。
如果仍然是让用户输入账户和密码登录,后端可去调用 Login 方法;如果登录验证完全是在自己的登录逻辑内处理的(比如用手机验证码登录、第三方登录等),那么验证成功后再去调用 DirectlyLogin 或 DirectlyLoginWithMobile 进入登录状态:

...
// 获取 IAuthService
var authService = G.GetAppService<IAuthService>();
// 调用方法
var serviceResult = authService.DirectlyLogin(...);
...

如果你的登录页不是应用根目录下的 Login 页,则还需在配置文件里的 "RCL:LoginPageName" 项去指定,对应属性为 LoginPageNameopen in new window

自定义默认页

若不想用 QuickAdmin.Net 内置的 Index 作为默认页,只需保留应用 Pages 目录下的 Index 页即可,在其中自行设计布局。
IAuthService 包含获取用户可用的系统菜单、快速链接等的方法,如有需要可去调用获取。

如果默认页不是应用根目录下的 Index 页,则还需在配置文件里的 "RCL:DefaultPageName" 项去指定,对应属性为 DefaultPageNameopen in new window

自定义内置服务

QuickAdmin.Net 内置的各个服务均可被定制,只需定义继承自内置服务的类,重写需要定制的方法/属性,然后将该类在应用启动时注册到系统服务即可。

例如 ClientInfoParseropen in new window 用来解析客户端信息,重写 GetDevice() 去完善用户设备信息的识别:

public class MyClientInfoParser : ClientInfoParser
{
  protected override string GetDevice(IHeaderDictionary headers)
  {
    // 解析设备信息
  }
}

重写 AuthServiceopen in new window 相应方法执行额外操作:

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();
  }
}

重写 UserServiceopen in new window 以下方法可强制用户在修改密码后重新登录:

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 单例模式。