[开源]OSharpNS 步步为营系列 - 2. 添加业务数据层

释放双眼,带上耳机,听听看~!

什么是OSharp

OSharpNS全称OSharp Framework with .NetStandard2.0,是一个基于.NetStandard2.0开发的一个.NetCore快速开发框架。这个框架使用最新稳定版的.NetCore SDK(当前是.NET Core 2.2),对 AspNetCore 的配置、依赖注入、日志、缓存、实体框架、Mvc(WebApi)、身份认证、权限授权等模块进行更高一级的自动化封装,并规范了一套业务实现的代码结构与操作流程,使 .Net Core 框架更易于应用到实际项目开发中。

概述

一个模块的数据层,主要包括如下几个方面:

  • 数据实体:数据实体是服务层向数据库进行数据持久化的数据载体,也是数据层进行业务处理的数据载体,数据层与数据层的数据交换都应通过实体类来完成。
  • 输入DTO:InputDto用于前端向API层输入数据,API层向服务层输入数据
  • 输出DTO:OutputDto主要用于数据查询服务,是API层向前端输出数据的载体
    [开源]OSharpNS 步步为营系列 - 2. 添加业务数据层

  • 数据实体映射配置:CodeFirst的开发模式,数据库的设计细节完全靠代码来完成,数据实体映射配置正是负责这项工作的,针对一个实体,可以在这里配置其在数据库中的数据表关系、数据约束及各个数据字段的每一个细节配置。

数据层文件夹布局

src                                     # 源代码文件夹
├─Liuliu.Blogs.Core                     # 项目核心工程
│  └─Blogs                              # 博客模块文件夹
│      ├─Dtos                           # 博客模块DTO文件夹
│      │    ├─BlogInputDto.cs           # 博客输入DTO
│      │    ├─BlogOutputDto.cs          # 博客输出DTO
│      │    ├─PostInputDto.cs           # 文章输入DTO
│      │    └─PostOutputDto.cs          # 文章输出DTO
│      └─Entities                       # 博客模块实体类文件夹
│           ├─Blog.cs                   # 博客实体类
│           └─Post.cs                   # 文章实体类
└─Liuliu.Blogs.EntityConfiguration      # 实体映射配置工程
   └─Blogs                              # 博客模块文件夹
       ├─BlogConfiguration.cs           # 博客实体映射配置类
       └─PostConfiguration.cs           # 文章实体映射配置类

实体类

必选接口

IEntity

数据实体接口:所有的实体类都必须实现 IEntity 接口,此接口给实体类添加一个 名为 Id 的实体唯一编号属性,这个属性是实体类的 主键 属性

/// <summary>
/// 数据模型接口
/// </summary>
/// <typeparam name=\"TKey\">实体主键类型</typeparam>
public interface IEntity<out TKey>
{
    /// <summary>
    /// 获取 实体唯一标识,主键
    /// </summary>
    TKey Id { get; }
}

EntityBase

实体类基类:为了方便实体类快速实现 IEntity 接口,定义了一个通用基类 EntityBase,所有实体都可以通过继承此基类方便实现 IEntity 接口

/// <summary>
/// 实体类基类
/// </summary>
public abstract class EntityBase<TKey> : IEntity<TKey> where TKey : IEquatable<TKey>
{
    /// <summary>
    /// 初始化一个<see cref=\"EntityBase{TKey}\"/>类型的新实例
    /// </summary>
    protected EntityBase()
    {
        if (typeof(TKey) == typeof(Guid))
        {
            Id = CombGuid.NewGuid().CastTo<TKey>();
        }
    }

    /// <summary>
    /// 获取或设置 编号
    /// </summary>
    [DisplayName(\"编号\")]
    public TKey Id { get; set; }

    // ...
}

可选的装配功能接口与特性

OSharp框架给实体类设计了一些可装配的功能,通过特定的接口来提供,实体类实现了特定的接口,则实体类在系统中将自动拥有一些额外的功能。

ICreatedTime

定义创建时间:ICreatedTime 接口给实体类添加一个 创建时间CreatedTime 的属性,该属性将在数据层执行 创建Insert 操作时,自动赋予系统当前时间。

/// <summary>
/// 定义创建时间
/// </summary>
public interface ICreatedTime
{
    /// <summary>
    /// 获取或设置 创建时间
    /// </summary>
    DateTime CreatedTime { get; set; }
}

ILockable

定义可锁定功能:ILockable 接口给实体类添加一个 是否锁定IsLocked 的属性

/// <summary>
/// 定义可锁定功能
/// </summary>
public interface ILockable
{
    /// <summary>
    /// 获取或设置 是否锁定当前信息
    /// </summary>
    bool IsLocked { get; set; }
}

IEntityHash

定义实体Hash功能:给实体添加 GetHash 扩展方法,用于对实体的属性值进行Hash,确定实体是否存在变化,这些变化可用于系统初始化时确定是否需要进行数据同步。
当前系统中 实体信息EntityInfo, 功能点Function, API模块Module 在系统启动时与数据库中的相应数据进行对比同步时,使用的就是这个接口的特性

/// <summary>
/// 定义实体Hash功能,对实体的属性值进行Hash,确定实体是否存在变化,
/// 这些变化可用于系统初始化时确定是否需要进行数据同步
/// </summary>
public interface IEntityHash
{ }

IExpirable

定义可过期性,包含生效时间和过期时间:给实体添加 生效时间BeginTime,过期时间EndTime 的属性

/// <summary>
/// 定义可过期性,包含生效时间和过期时间
/// </summary>
public interface IExpirable
{
    /// <summary>
    /// 获取或设置 生效时间
    /// </summary>
    DateTime? BeginTime { get; set; }

    /// <summary>
    /// 获取或设置 过期时间
    /// </summary>
    DateTime? EndTime { get; set; }
}

ICreationAudited

定义创建审计信息:给实体添加创建时的 创建人CreatorId,创建时间CreatedTime 的审计信息,这些值将在数据层执行 创建Insert 时自动赋值。

/// <summary>
/// 定义创建审计信息
/// </summary>
public interface ICreationAudited<TUserKey> : ICreatedTime
    where TUserKey : struct
{
    /// <summary>
    /// 获取或设置 创建者编号
    /// </summary>
    TUserKey? CreatorId { get; set; }
}

ICreationAudited 已继承了 ICreatedTime 接口,不会重复添加 CreatedTime 属性

IUpdateAudited

定义更新审计的信息:给实体添加更新时的 最后更新人LastUpdaterId,最后更新时间LastUpdatedTime 的审计信息,这些值将在数据层执行 更新Update 操作时自动赋值。

/// <summary>
/// 定义更新审计的信息
/// </summary>
public interface IUpdateAudited<TUserKey> where TUserKey : struct
{
    /// <summary>
    /// 获取或设置 更新者编号
    /// </summary>
    TUserKey? LastUpdaterId { get; set; }

    /// <summary>
    /// 获取或设置 最后更新时间
    /// </summary>
    DateTime? LastUpdatedTime { get; set; }
}

ISoftDeletable

定义逻辑删除功能:给实体添加一个用于逻辑删除的 删除时间DeletedTime 属性,当实体实现了 ISoftDeletable 接口之后,数据层执行 删除 Delete 操作时,将 DeletedTime 赋予当前时间,即表示数据已被删除。数据查询的时候通过全局过滤器自动过滤掉 DeletedTime 不为 null 的数据,从而查询出正常数据。

/// <summary>
/// 定义逻辑删除功能
/// </summary>
public interface ISoftDeletable
{
    /// <summary>
    /// 获取或设置 数据逻辑删除时间,为null表示正常数据,有值表示已逻辑删除,同时删除时间每次不同也能保证索引唯一性
    /// </summary>
    DateTime? DeletedTime { get; set; }
}

UserFlagAttribute

用户标记特性:此特性与数据权限有关,用于实体类的 用户编号UserId 属性,用于标记实体类中与用户私有数据相关联的用户编号,以在数据权限判断时,将实体的用户编号与当前在线用户的用户编号进行对比,找出用户的私有数据。

    /// <summary>
    /// 用户标记,用于标示用户属性/字段
    /// </summary>
    [AttributeUsage(AttributeTargets.Property)]
    public class UserFlagAttribute : Attribute
    { }

博客模块的实体类定义

回到我们的 Liuliu.Blogs 项目,根据 业务模块设计#模块文件夹结构布局给出的结构,在 Liuliu.Blogs.Core 项目中创建 Blogs/Entities 的文件夹存放实体类,添加如下实体类文件。

博客实体 - Blog

namespace Liuliu.Blogs.Blogs.Entities
{
    /// <summary>
    /// 实体类:博客信息
    /// </summary>
    public class Blog : EntityBase<int>, ICreatedTime, ISoftDeletable
    {
        /// <summary>
        /// 获取或设置 博客地址
        /// </summary>
        [Required]
        public string Url { get; set; }

        /// <summary>
        /// 获取或设置 显示名称
        /// </summary>
        [Required]
        public string Display { get; set; }

        /// <summary>
        /// 获取或设置 已开通
        /// </summary>
        public bool IsEnabled { get; set; }

        /// <summary>
        /// 获取或设置 创建时间
        /// </summary>
        public DateTime CreatedTime { get; set; }

        /// <summary>
        /// 获取或设置 数据逻辑删除时间,为null表示正常数据,有值表示已逻辑删除
        /// </summary>
        public DateTime? DeletedTime { get; set; }

        /// <summary>
        /// 获取或设置 作者编号
        /// </summary>
        [UserFlag]
        public int UserId { get; set; }

        /// <summary>
        /// 获取或设置 作者
        /// </summary>
        public virtual User User { get; set; }
    }
}

文章实体 - Post

namespace Liuliu.Blogs.Blogs.Entities
{
    /// <summary>
    /// 实体类:文章信息
    /// </summary>
    public class Post:EntityBase<int>,ICreatedTime,ISoftDeletable
    {
        /// <summary>
        /// 获取或设置 文章标题
        /// </summary>
        [Required]
        public string Title { get; set; }

        /// <summary>
        /// 获取或设置 文章内容
        /// </summary>
        [Required]
        public string Content { get; set; }

        /// <summary>
        /// 获取或设置 创建时间
        /// </summary>
        public DateTime CreatedTime { get; set; }

        /// <summary>
        /// 获取或设置 数据逻辑删除时间,为null表示正常数据,有值表示已逻辑删除
        /// </summary>
        public DateTime? DeletedTime { get; set; }

        /// <summary>
        /// 获取或设置 所属博客编号
        /// </summary>
        public int BlogId { get; set; }

        /// <summary>
        /// 获取或设置 所属博客
        /// </summary>
        public virtual Blog Blog { get; set; }

        /// <summary>
        /// 获取或设置 作者编号
        /// </summary>
        [UserFlag]
        public int UserId { get; set; }

        /// <summary>
        /// 获取或设置 作者
        /// </summary>
        public virtual User User { get; set; }
    }
}

输入输出DTO

输入输出DTO:输入输出DTO主要负责各层次之间的数据传输工作,避免在外层暴露实体类。

输入输出DTO相关接口

InputDto

定义输入DTO:输入DTO 需实现此接口,作为 业务层参数类型、DTO属性合法性验证数据类型 的约束。

/// <summary>
/// 定义输入DTO
/// </summary>
/// <typeparam name=\"TKey\"></typeparam>
public interface IInputDto<TKey>
{
    /// <summary>
    /// 获取或设置 主键,唯一标识
    /// </summary>
    TKey Id { get; set; }
}

IOutputDto

定义输出DTO:输出DTO 需实现此接口,此接口将作为 分页查询 返回类型的约束。

    /// <summary>
    /// 定义输出DTO
    /// </summary>
    public interface IOutputDto
    { }

数据实体映射

定义数据权限的更新,删除状态:用于 输入DTOIOutputDto,此接口为输出DTO提供 是否允许更新Updatable、是否允许删除Deletable 两个数据权限属性,输出DTO实现了此接口,在进行数据分页查询时,将使用 全实体 查询模式并进行 数据权限校验,在输出DTO中自动附加数据权限(是否可更新、是否可删除)信息。

    /// <summary>
    /// 定义数据权限的更新,删除状态
    /// </summary>
    public interface IDataAuthEnabled
    {
        /// <summary>
        /// 获取或设置 是否可更新的数据权限状态
        /// </summary>
        bool Updatable { get; set; }

        /// <summary>
        /// 获取或设置 是否可删除的数据权限状态
        /// </summary>
        bool Deletable { get; set; }
    }

博客模块的输入输出DTO类定义

回到我们的 Liuliu.Blogs 项目,根据 业务模块设计#模块文件夹结构布局给出的结构,在 Liuliu.Blogs.Core 项目中创建 Blogs/Dtos 的文件夹存放实体类,添加如下输入输出DTO类文件。

博客 - Blog

博客 InputDto

namespace Liuliu.Blogs.Blogs.Dtos
{
    /// <summary>
    /// 输入DTO:博客信息
    /// </summary>
    public class BlogInputDto : IInputDto<int>
    {
        /// <summary>
        /// 获取或设置 博客编号
        /// </summary>
        public int Id { get; set; }

        /// <summary>
        /// 获取或设置 博客地址
        /// </summary>
        [Required]
        public string Url { get; set; }

        /// <summary>
        /// 获取或设置 显示名称
        /// </summary>
        [Required]
        public string Display { get; set; }
    }
}

博客 OutputDto

namespace Liuliu.Blogs.Blogs.Dtos
{
    /// <summary>
    /// 输出DTO:博客信息
    /// </summary>
    public class BlogOutputDto : IOutputDto, IDataAuthEnabled
    {
        /// <summary>
        /// 获取或设置 博客编号
        /// </summary>
        public int Id { get; set; }

        /// <summary>
        /// 获取或设置 博客地址
        /// </summary>
        public string Url { get; set; }

        /// <summary>
        /// 获取或设置 显示名称
        /// </summary>
        public string Display { get; set; }

        /// <summary>
        /// 获取或设置 已开通
        /// </summary>
        public bool IsEnabled { get; set; }

        /// <summary>
        /// 获取或设置 创建时间
        /// </summary>
        public DateTime CreatedTime { get; set; }

        /// <summary>
        /// 获取或设置 作者编号
        /// </summary>
        public int UserId { get; set; }

        /// <summary>
        /// 获取或设置 是否可更新的数据权限状态
        /// </summary>
        public bool Updatable { get; set; }

        /// <summary>
        /// 获取或设置 是否可删除的数据权限状态
        /// </summary>
        public bool Deletable { get; set; }
    }
}

文章 - Post

文章 InputDto

namespace Liuliu.Blogs.Blogs.Dtos
{
    /// <summary>
    /// 输入DTO:文章信息
    /// </summary>
    public class PostInputDto : IInputDto<int>
    {
        /// <summary>
        /// 获取或设置 文章编号
        /// </summary>
        public int Id { get; set; }

        /// <summary>
        /// 获取或设置 文章标题
        /// </summary>
        [Required]
        public string Title { get; set; }

        /// <summary>
        /// 获取或设置 文章内容
        /// </summary>
        [Required]
        public string Content { get; set; }
    }
}

文章 OutputDto

namespace Liuliu.Blogs.Blogs.Dtos
{
    /// <summary>
    /// 输出DTO:文章信息
    /// </summary>
    public class PostOutputDto : IOutputDto, IDataAuthEnabled
    {
        /// <summary>
        /// 获取或设置 文章编号
        /// </summary>
        public int Id { get; set; }

        /// <summary>
        /// 获取或设置 文章标题
        /// </summary>
        public string Title { get; set; }

        /// <summary>
        /// 获取或设置 文章内容
        /// </summary>
        public string Content { get; set; }

        /// <summary>
        /// 获取或设置 创建时间
        /// </summary>
        public DateTime CreatedTime { get; set; }

        /// <summary>
        /// 获取或设置 所属博客编号
        /// </summary>
        public int BlogId { get; set; }

        /// <summary>
        /// 获取或设置 作者编号
        /// </summary>
        [UserFlag]
        public int UserId { get; set; }

        /// <summary>
        /// 获取或设置 是否可更新的数据权限状态
        /// </summary>
        public bool Updatable { get; set; }

        /// <summary>
        /// 获取或设置 是否可删除的数据权限状态
        /// </summary>
        public bool Deletable { get; set; }
    }
}

数据实体映射配置

OSharp框架中,实体映射配置类主要有两个作用:

  1. 在系统初始化时将实体类加载到上下文中
  2. 实现实体类到数据库的映射细节配置

相关接口与基类

IEntityRegister

将实体配置类注册到上下文中:IEntityRegister 接口定义了一个DbContextType属性,可将一个实体类注册到指定的上下文中,这在自定义多个数据上下文的时候会用到。
在实体映射类中实现此接口,数据上下文初始化的时候,在OnModelCreating方法中通过调用RegisterTo(ModelBuilder modelBuilder)方法,将实体映射类的实例注册到ModelBuilder中。

/// <summary>
/// 定义将实体配置类注册到上下文中
/// </summary>
public interface IEntityRegister
{
    /// <summary>
    /// 获取所属的上下文类型,如为null,将使用默认上下文,否则使用指定类型的上下文类型
    /// </summary>
    Type DbContextType { get; }

    /// <summary>
    /// 获取 相应的实体类型
    /// </summary>
    Type EntityType { get; }

    /// <summary>
    /// 将当前实体类映射对象注册到数据上下文模型构建器中
    /// </summary>
    /// <param name=\"modelBuilder\">上下文模型构建器</param>
    void RegisterTo(ModelBuilder modelBuilder);
}

EntityTypeConfigurationBase

数据实体映射配置基类:为方便实现实体映射配置类,OSharp定义了此基类,实现了 IEntityTypeConfiguration<TEntity>IEntityRegister 两个接口,在实现实体映射配置类时,只需继承此类,实现Configure(EntityTypeBuilder<TEntity> builder)抽象方法即可。

/// <summary>
/// 数据实体映射配置基类
/// </summary>
/// <typeparam name=\"TEntity\">实体类型</typeparam>
/// <typeparam name=\"TKey\">主键类型</typeparam>
public abstract class EntityTypeConfigurationBase<TEntity, TKey> : IEntityTypeConfiguration<TEntity>, IEntityRegister
    where TEntity : class, IEntity<TKey>
    where TKey : IEquatable<TKey>
{
    /// <summary>
    /// 获取 所属的上下文类型,如为null,将使用默认上下文, 否则使用指定类型的上下文类型
    /// </summary>
    public virtual Type DbContextType => null;

    /// <summary>
    /// 获取 相应的实体类型
    /// </summary>
    public Type EntityType => typeof(TEntity);

    /// <summary>
    /// 将当前实体类映射对象注册到数据上下文模型构建器中
    /// </summary>
    /// <param name=\"modelBuilder\">上下文模型构建器</param>
    public void RegisterTo(ModelBuilder modelBuilder)
    {
        modelBuilder.ApplyConfiguration(this);
        // 给软删除的实体添加全局过滤器
        if (typeof(ISoftDeletable).IsAssignableFrom(typeof(TEntity)))
        {
            modelBuilder.Entity<TEntity>().HasQueryFilter(m => ((ISoftDeletable)m).DeletedTime == null);
        }
    }

    /// <summary>
    /// 重写以实现实体类型各个属性的数据库配置
    /// </summary>
    /// <param name=\"builder\">实体类型创建器</param>
    public abstract void Configure(EntityTypeBuilder<TEntity> builder);
}

博客模块的实体映射配置类实现

回到我们的 Liuliu.Blogs 项目,根据 业务模块设计#模块文件夹结构布局给出的结构,在 Liuliu.Blogs.EntityConfiguration 项目中创建 Blogs 的文件夹存放实体类,添加如下实体映射配置类文件。

博客 - Blog

根据 业务模块设计#数据层的定义,博客实体的设计要求Url属性 唯一索引,并且 博客与博主 之间的关系是 一对一,因此做如下约束:

  1. 创建Url属性的唯一索引,形式本应是builder.HasIndex(m => m.Url),但博客实体引入了逻辑删除,因此唯一索引应加入逻辑删除属性DeletedTime
  2. 博客实体与用户实体之间以UserId为外键,建立一对一关系,并且对于一个博客来说,其拥有者是必须的,因此需要加上IsRequired约束,并禁止级联删除。
/// <summary>
/// 实体映射配置类:博客信息
/// </summary>
public class BlogConfiguration : EntityTypeConfigurationBase<Blog, int>
{
    /// <summary>
    /// 重写以实现实体类型各个属性的数据库配置
    /// </summary>
    /// <param name=\"builder\">实体类型创建器</param>
    public override void Configure(EntityTypeBuilder<Blog> builder)
    {
        builder.HasIndex(m => new {m.Url, m.DeletedTime}).HasName(\"BlogUrlIndex\").IsUnique();
        builder.HasOne(m => m.User).WithOne().HasForeignKey<Blog>(m => m.UserId).OnDelete(DeleteBehavior.Restrict).IsRequired();
    }
}

文章 - Post

根据 业务模块设计#数据层的定义,文章实体的设计要求 文章与博客 之间的关系是 多对一文章与作者 之间的关系是 多对一,因此做如下约束:

  1. 文章实体与博客实体之间以BlogId为外键,建立多对一关系,并且对于一篇文章来说,其所在博客是必须的,因此需要加上IsRequired约束,并禁止级联删除。
  2. 文章实体与用户实体之间以UserId为外键,建立多对一关系,并且对于一篇文章来说,其作者是必须的,因此需要加上IsRequired约束,并禁止级联删除。
    /// <summary>
    /// 实体映射配置类:文章信息
    /// </summary>
    public class PostConfiguration : EntityTypeConfigurationBase<Post, int>
    {
        /// <summary>
        /// 重写以实现实体类型各个属性的数据库配置
        /// </summary>
        /// <param name=\"builder\">实体类型创建器</param>
        public override void Configure(EntityTypeBuilder<Post> builder)
        {
            builder.HasOne(m => m.Blog).WithMany().HasForeignKey(m => m.BlogId).OnDelete(DeleteBehavior.Restrict).IsRequired();
            builder.HasOne(m => m.User).WithMany().HasForeignKey(m => m.UserId).OnDelete(DeleteBehavior.Restrict).IsRequired();
        }
    }

至此,博客模块的数据层代码实现完毕。下面可进行数据迁移将数据结构更新到现有数据库中。

数据迁移

打开 VS2019 的 程序包管理器控制台,将默认项目设置为 src\\Liuliu.Blogs.Web,执行如下命令添加名称为AddBlogsEntities的新的迁移记录:

add-migration AddBlogsEntities

再执行数据库更新命令,执行迁移

update-database

数据迁移完毕,整个过程输出如下:

PM> add-migration AddBlogsEntities
entryAssemblyName: Liuliu.Blogs.Web
To undo this action, use Remove-Migration.
PM> update-database
entryAssemblyName: Liuliu.Blogs.Web
Applying migration \'20190504130412_AddBlogsEntities\'.
Done.
PM> 

打开数据库,我们将看到新的数据表dbo.Blogdbo.Post已经成功创建:
[开源]OSharpNS 步步为营系列 - 2. 添加业务数据层

给TA打赏
共{{data.count}}人
人已打赏
随笔日记

全球最便宜的抗投诉机器:VPS年付6欧元,双路E5独立服务器37欧元

2020-11-9 4:47:27

随笔日记

Golang : pflag 包简介

2020-11-9 4:47:29

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索