用NetCore + ReactJS 实现一个前后端分离的网站 (3) 仓储层的实现
1. 前言
这两天在仓储模式上纠结了几回,差点放弃仓储层的实现,因为从网上搜到一些文章,说efcore已经是按照仓储模式设计的了,不需要在用一个仓储层来抽象。不过,在实际使用中,拥有一些通用的方法,还是可以比较轻松地添加新功能,所以这里接着往下写。
2. 数据库接入
实体类当然是放在Model工程中,但是Context文件放哪里呢,一般小项目直接放在API层,Migration也在应用层做。
但是仓储层也要用Context,所以把Context文件也放到了Model工程中。
这个项目的数据库需要实现Code First,所以要先定义实体,并通过EFCore的migration功能自动初始化表结构,同时添加一些种子数据。
- 新建Models文件夹,把原本的Novel.cs移动进去并修改,设置字段属性,给EFCore生成建表语句。
Novel.cs
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace NovelTogether.Core.Model.Models
{
public class Novel
{
[Key]
public int ID { get; set; }
[Required]
[Column(TypeName = "nvarchar")]
[MaxLength(128)]
public string? Name { get; set; }
[Required]
[Column(TypeName = "nvarchar")]
[MaxLength(128)]
public string? Type { get; set; }
[Required]
[Column(TypeName = "nvarchar")]
[MaxLength(128)]
public string? SubType { get; set; }
[Required]
[Column(TypeName = "nvarchar")]
[MaxLength(1024)]
public string? Description { get; set; }
[Required]
public int CreatedBy { get; set; }
[Required]
public DateTime CreatedTime { get; set; }
public int? ModifiedBy { get; set; }
public DateTime? ModifiedTime { get; set; }
[Required]
public bool IsDeleted { get; set; }
}
}
- 新建ORM文件夹,新建类NovelTogetherContext.cs
这里有几点要注意
- 构造函数接受一个参数,这个参数会在Program.cs通过添加服务方式设置(步骤3)。
- 创建数据库的时候提供了一些种子数据。
NovelTogetherContext.cs
using Microsoft.EntityFrameworkCore;
using NovelTogether.Core.Model.Models;
namespace NovelTogether.Core.Model.ORM
{
public class NovelTogetherContext: DbContext
{
public NovelTogetherContext(DbContextOptions<NovelTogetherContext> options):base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// 初始化种子数据
modelBuilder.AddNovelSeed();
}
public DbSet<Novel> Novel { get; set; }
}
}
- 添加种子数据,在ORM文件夹中新建ModelBuilderExtension.cs
ModelBuilderExtension.cs
using Microsoft.EntityFrameworkCore;
using NovelTogether.Core.Model.Models;
namespace NovelTogether.Core.Model.ORM
{
public static class ModelBuilderExtension
{
public static void AddNovelSeed(this ModelBuilder modelBuilder)
{
modelBuilder.Entity<Novel>().HasData(
new Novel { ID = 1, Name = "三国演义", Type = "古典名著", SubType = "", Description = "", CreatedBy = 1, CreatedTime = DateTime.Now, IsDeleted = false },
new Novel { ID = 2, Name = "红楼梦", Type = "古典名著", SubType = "", Description = "", CreatedBy = 1, CreatedTime = DateTime.Now, IsDeleted = false },
new Novel { ID = 3, Name = "水浒传", Type = "古典名著", SubType = "", Description = "", CreatedBy = 1, CreatedTime = DateTime.Now, IsDeleted = false },
new Novel { ID = 4, Name = "西游记", Type = "古典名著", SubType = "", Description = "", CreatedBy = 1, CreatedTime = DateTime.Now, IsDeleted = false }
);
}
}
}
- 往appsettings.json中添加配置项。
appsettings.json
"Config": {
"Database": {
"SqlServer": {
"InUse": true,
"ConnectionString": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=NovelTogether;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False"
}
}
}
- 在API工程中新建Filters文件夹,创建MigrationStartupFilter.cs,提供随程序启动执行Migration的服务。
MigrationStartupFilter.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.OpenApi.Writers;
namespace NovelTogether.Core.API.Filters
{
public class MigrationStartupFilter<TContext> : IStartupFilter where TContext : DbContext
{
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
return app =>
{
using (var scope = app.ApplicationServices.CreateScope())
{
foreach (var context in scope.ServiceProvider.GetServices<TContext>())
{
context.Database.SetCommandTimeout(200);
context.Database.Migrate();
}
}
next(app);
};
}
}
}
- 设置目标数据库的类型和连接字符串,并设置在程序启动的时候执行Migration。
先引用EFCore相关的两个库,前者是用来连接SqlServer数据库的,后者使用管理Migration的。
- Microsoft.EntityFrameworkCore.SqlServer
- Microsoft.EntityFrameworkCore.Design
然后修改入口程序。
Program.cs
// get configuration
var configuration = builder.Configuration;
#region EFCore
builder.Services.AddDbContext<NovelTogetherContext>(option =>
{
// 因为NovelTogetherContext不在API工程中,需要手动指定Migration所在的程序集名称。
option.UseSqlServer(configuration.GetValue<string>("Config:Database:SqlServer:ConnectionString"), b => b.MigrationsAssembly("NovelTogether.Core.API"));
});
// 在程序启动的时候执行数据库的升级
builder.Services.AddTransient<IStartupFilter, MigrationStartupFilter<NovelTogetherContext>>();
#endregion
- 在API工程中创建Migration
Command
dotnet ef migrations add InitialCreate
以上步骤完毕之后,F5启动程序,就可以自动创建数据库。通过客户端工具连接数据库,可以看到初始化的四条数据。
下面我们接着完善仓储和服务层。
3. 仓储和服务层
- 在IRepository工程中新建文件夹Base,并新建接口IBaseRepository。这里面定义了基本的增删改查方法。
IBaseRepository.cs
using System.Linq.Expressions;
namespace NovelTogether.Core.IRepository.Base
{
public interface IBaseRepository<TEntity> where TEntity : class
{
Task<TEntity> SelectAsync(Expression<Func<TEntity, bool>> expression);
Task<List<TEntity>> SelectRangeAsync(Expression<Func<TEntity, bool>> expression);
Task<List<TEntity>> SelectRangeAsync(Expression<Func<TEntity, bool>> expression, Dictionary<string, bool> sortingColumnsWithDirection);
Task<List<TEntity>> SelectRangeAsync(Expression<Func<TEntity, bool>> expression, Dictionary<string, bool> sortingColumnsWithDirection, int pageSize, int pageIndex);
Task<TEntity> AddAsync(TEntity entity);
Task<List<TEntity>> AddRangeAsync(List<TEntity> entities);
Task<TEntity> UpdateAsync(TEntity entity);
Task<List<TEntity>> UpdateRangeAsync(List<TEntity> entities);
Task<bool> DeleteAsync(TEntity entity);
Task<bool> DeleteRangeAsync(List<TEntity> entities);
}
}
- 在IRepository工程根目录创建接口INovelRepository,继承基接口IBaseRepository。
INovelRepository.cs
using NovelTogether.Core.Model.Models;
namespace NovelTogether.Core.IRepository.Base
{
public interface INovelRepository: IBaseRepository<Novel>
{
}
}
- 在Repository工程中新建文件夹Base,并新建类BaseRepository,这个类继承了IBaseRepository,它里面实现了每个方法。
注意:构造函数中的参数NovelTogetherContext是由子类传进来的。
注意:IQueryable没有OrderBy、OrderByDescending,IOrderedQueryable没有ThenBy,ThenByDescending,需要自己扩展,代码见文章结尾。
BaseRepository.cs
using Microsoft.EntityFrameworkCore;
using NovelTogether.Core.Common.ORM;
using NovelTogether.Core.IRepository.Base;
using NovelTogether.Core.Model.ORM;
using System.Linq.Expressions;
using System.Runtime.InteropServices;
namespace NovelTogether.Core.Repository.Base
{
// new() 用来约束TEntity必须要有一个不带参数的构造函数
public class BaseRepository<TEntity> : IBaseRepository<TEntity> where TEntity : class, new()
{
private DbContext dbContext;
#region 构造函数
public BaseRepository(NovelTogetherContext novelTogetherContext)
{
dbContext = novelTogetherContext;
}
#endregion
#region 公共方法
public async Task<TEntity> AddAsync(TEntity entity)
{
await dbContext.AddAsync(entity);
await dbContext.SaveChangesAsync();
return entity;
}
public async Task<List<TEntity>> AddRangeAsync(List<TEntity> entities)
{
await dbContext.AddRangeAsync(entities);
await dbContext.SaveChangesAsync();
return entities;
}
public async Task<bool> DeleteAsync(TEntity entity)
{
dbContext.Remove(entity);
await dbContext.SaveChangesAsync();
return true;
}
public async Task<bool> DeleteRangeAsync(List<TEntity> entities)
{
dbContext.RemoveRange(entities);
await dbContext.SaveChangesAsync();
return true;
}
public async Task<TEntity> SelectAsync(Expression<Func<TEntity, bool>> expression)
{
var entity = await dbContext.Set<TEntity>().Where(expression).FirstOrDefaultAsync();
return entity;
}
public async Task<List<TEntity>> SelectRangeAsync(Expression<Func<TEntity, bool>> expression)
{
var entities = SelectRangeQuerable(expression, new Dictionary<string, bool>());
return await entities.ToListAsync();
}
public async Task<List<TEntity>> SelectRangeAsync(Expression<Func<TEntity, bool>> expression, Dictionary<string, bool> sortingColumnsWithDirection)
{
var entities = SelectRangeQuerable(expression, sortingColumnsWithDirection);
return await entities.ToListAsync();
}
public async Task<List<TEntity>> SelectRangeAsync(Expression<Func<TEntity, bool>> expression, Dictionary<string, bool> sortingColumnsWithDirection, int pageSize, int pageIndex)
{
var entities = SelectRangeQuerable(expression, sortingColumnsWithDirection);
entities = entities.Skip((pageIndex - 1) * pageSize).Take(pageSize);
return await entities.ToListAsync();
}
public async Task<TEntity> UpdateAsync(TEntity entity)
{
dbContext.Update(entity);
await dbContext.SaveChangesAsync();
return entity;
}
public async Task<List<TEntity>> UpdateRangeAsync(List<TEntity> entities)
{
dbContext.UpdateRange(entities);
await dbContext.SaveChangesAsync();
return entities;
}
#endregion
#region 私有方法
private IQueryable<TEntity> SelectRangeQuerable(Expression<Func<TEntity, bool>> expression, Dictionary<string, bool> sortingColumnsWithDirection)
{
var entities = dbContext.Set<TEntity>().Where(expression);
if (sortingColumnsWithDirection.Keys.Count > 0)
{
var first = sortingColumnsWithDirection.First();
IOrderedQueryable<TEntity> orderedEntities;
if (first.Value)
{
orderedEntities = entities.OrderBy(first.Key);
}
else
{
orderedEntities = entities.OrderByDescending(first.Key);
}
// remove first
sortingColumnsWithDirection.Remove(first.Key);
// sort left columns
foreach (KeyValuePair<string, bool> kvp in sortingColumnsWithDirection)
{
if (kvp.Value)
{
orderedEntities = orderedEntities.ThenBy(kvp.Key);
}
else
{
orderedEntities = orderedEntities.ThenByDescending(kvp.Key);
}
}
return orderedEntities;
}
return entities;
}
#endregion
}
}
- 在Repository工程的根目录新建类NovelRepository,继承BaseRepository和INovelRepository,并把注入的NovelTogetherContext传给父类的构造函数。
NovelRepository.cs
using NovelTogether.Core.IRepository.Base;
using NovelTogether.Core.Model.Models;
using NovelTogether.Core.Model.ORM;
using NovelTogether.Core.Repository.Base;
namespace NovelTogether.Core.Repository
{
public class NovelRepository: BaseRepository<Novel>, INovelRepository
{
public NovelRepository(NovelTogetherContext novelTogetherContext):base(novelTogetherContext)
{
}
}
}
- 在IService工程中新建文件Base,并新建接口IBaseService。这里面同样定义了基本的增删改查方法,和IBaseRepository中的一样,当然还可以加上一些别的方法。
IBaseService.cs
using System.Linq.Expressions;
namespace NovelTogether.Core.IService.Base
{
public interface IBaseService<TEntity> where TEntity : class
{
Task<TEntity> SelectAsync(Expression<Func<TEntity, bool>> expression);
Task<List<TEntity>> SelectRangeAsync(Expression<Func<TEntity, bool>> expression);
Task<List<TEntity>> SelectRangeAsync(Expression<Func<TEntity, bool>> expression, Dictionary<string, bool> sortingColumnsWithDirection);
Task<List<TEntity>> SelectRangeAsync(Expression<Func<TEntity, bool>> expression, Dictionary<string, bool> sortingColumnsWithDirection, int pageSize, int pageIndex);
Task<TEntity> AddAsync(TEntity entity);
Task<List<TEntity>> AddRangeAsync(List<TEntity> entities);
Task<TEntity> UpdateAsync(TEntity entity);
Task<List<TEntity>> UpdateRangeAsync(List<TEntity> entities);
Task<bool> DeleteAsync(TEntity entity);
Task<bool> DeleteRangeAsync(List<TEntity> entities);
}
}
- 在IService工程的根目录新建接口INovelService,继承基接口IBaseService。
INovelService.cs
using NovelTogether.Core.IService.Base;
using NovelTogether.Core.Model.Models;
namespace NovelTogether.Core.IService
{
public interface INovelService: IBaseService<Novel>
{
}
}
- 在Service工程中新建文件夹Base,并新建类BaseService,继承基接口IBaseService,子类由依赖注入得到的IBaseRepository通过构造函数传递给BaseService。
BaseService.cs
using NovelTogether.Core.IRepository.Base;
using NovelTogether.Core.IService.Base;
using System.Linq.Expressions;
namespace NovelTogether.Core.Service.Base
{
public class BaseService<TEntity> : IBaseService<TEntity> where TEntity : class, new()
{
private IBaseRepository<TEntity> repository;
public BaseService(IBaseRepository<TEntity> Repository)
{
this.repository = Repository;
}
public async Task<TEntity> AddAsync(TEntity entity)
{
return await repository.AddAsync(entity);
}
public async Task<List<TEntity>> AddRangeAsync(List<TEntity> entities)
{
return await repository.AddRangeAsync(entities);
}
public async Task<bool> DeleteAsync(TEntity entity)
{
return await repository.DeleteAsync(entity);
}
public async Task<bool> DeleteRangeAsync(List<TEntity> entities)
{
return await repository.DeleteRangeAsync(entities);
}
public async Task<TEntity> SelectAsync(Expression<Func<TEntity, bool>> expression)
{
return await repository.SelectAsync(expression);
}
public async Task<List<TEntity>> SelectRangeAsync(Expression<Func<TEntity, bool>> expression)
{
return await repository.SelectRangeAsync(expression);
}
public async Task<List<TEntity>> SelectRangeAsync(Expression<Func<TEntity, bool>> expression, Dictionary<string, bool> sortingColumnsWithDirection)
{
return await repository.SelectRangeAsync(expression, sortingColumnsWithDirection);
}
public async Task<List<TEntity>> SelectRangeAsync(Expression<Func<TEntity, bool>> expression, Dictionary<string, bool> sortingColumnsWithDirection, int pageSize, int pageIndex)
{
return await repository.SelectRangeAsync(expression, sortingColumnsWithDirection, pageSize, pageIndex);
}
public async Task<TEntity> UpdateAsync(TEntity entity)
{
return await repository.UpdateAsync(entity);
}
public async Task<List<TEntity>> UpdateRangeAsync(List<TEntity> entities)
{
return await repository.UpdateRangeAsync(entities);
}
}
}
- 在Service工程的根目录新建类NovelService,继承BaseService。
NovelService.cs
using NovelTogether.Core.IRepository.Base;
using NovelTogether.Core.IService;
using NovelTogether.Core.Model.Models;
using NovelTogether.Core.Service.Base;
namespace NovelTogether.Core.Service
{
public class NovelService : BaseService<Novel>, INovelService
{
public NovelService(INovelRepository repository):base(repository)
{
}
}
}
- 修改NovelController.cs
NovelController.cs
using Microsoft.AspNetCore.Mvc;
using NovelTogether.Core.IService;
using NovelTogether.Core.Model.Models;
namespace NovelTogether.Core.API.Controllers
{
[ApiController]
[Route("[controller]")]
public class NovelController : Controller
{
private readonly INovelService _novelService;
// 通过构造函数注入依赖
public NovelController(INovelService novelService)
{
_novelService = novelService;
}
[HttpGet]
public async Task<Novel> Get(int id)
{
return await _novelService.SelectAsync(x => x.ID == id);
}
}
}
- F5执行
4. 扩展方法
EntityFrameworkExtensions.cs
using System.Linq.Expressions;
using System.Reflection;
namespace NovelTogether.Core.Common.ORM
{
public static class EntityFrameworkExtensions
{
public static IOrderedQueryable<TSource> OrderBy<TSource>(this IQueryable<TSource> query, string propertyName)
{
var entityType = typeof(TSource);
//Create x=>x.PropName
var propertyInfo = entityType.GetProperty(propertyName);
ParameterExpression arg = Expression.Parameter(entityType, "x");
MemberExpression property = Expression.Property(arg, propertyName);
var selector = Expression.Lambda(property, new ParameterExpression[] { arg });
//Get System.Linq.Queryable.OrderBy() method.
var enumarableType = typeof(System.Linq.Queryable);
var method = enumarableType.GetMethods()
.Where(m => m.Name == "OrderBy" && m.IsGenericMethodDefinition)
.Where(m =>
{
var parameters = m.GetParameters().ToList();
return parameters.Count == 2;
}).Single();
//The linq's OrderBy<TSource, TKey> has two generic types, which provided here
MethodInfo genericMethod = method.MakeGenericMethod(entityType, propertyInfo.PropertyType);
// Call query.OrderBy(selector), with query and selector: x=> x.PropName
// Note that we pass the selector as Expression to the method and we don't compile it.
// By doing so EF can extract "order by" columns and generate SQL for it
var newQuery = (IOrderedQueryable<TSource>)genericMethod.Invoke(genericMethod, new object[] { query, selector });
return newQuery;
}
public static IOrderedQueryable<TSource> ThenBy<TSource>(this IOrderedQueryable<TSource> query, string propertyName)
{
var entityType = typeof(TSource);
//Create x=>x.PropName
var propertyInfo = entityType.GetProperty(propertyName);
ParameterExpression arg = Expression.Parameter(entityType, "x");
MemberExpression property = Expression.Property(arg, propertyName);
var selector = Expression.Lambda(property, new ParameterExpression[] { arg });
//Get System.Linq.Queryable.OrderBy() method.
var enumarableType = typeof(System.Linq.Queryable);
var method = enumarableType.GetMethods()
.Where(m => m.Name == "OrderBy" && m.IsGenericMethodDefinition)
.Where(m =>
{
var parameters = m.GetParameters().ToList();
return parameters.Count == 2;
}).Single();
//The linq's OrderBy<TSource, TKey> has two generic types, which provided here
MethodInfo genericMethod = method.MakeGenericMethod(entityType, propertyInfo.PropertyType);
// Call query.OrderBy(selector), with query and selector: x=> x.PropName
// Note that we pass the selector as Expression to the method and we don't compile it.
// By doing so EF can extract "order by" columns and generate SQL for it
var newQuery = (IOrderedQueryable<TSource>)genericMethod.Invoke(genericMethod, new object[] { query, selector });
return newQuery;
}
public static IOrderedQueryable<TSource> OrderByDescending<TSource>(this IQueryable<TSource> query, string propertyName)
{
var entityType = typeof(TSource);
//Create x=>x.PropName
var propertyInfo = entityType.GetProperty(propertyName);
ParameterExpression arg = Expression.Parameter(entityType, "x");
MemberExpression property = Expression.Property(arg, propertyName);
var selector = Expression.Lambda(property, new ParameterExpression[] { arg });
//Get System.Linq.Queryable.OrderBy() method.
var enumarableType = typeof(System.Linq.Queryable);
var method = enumarableType.GetMethods()
.Where(m => m.Name == "OrderByDescending" && m.IsGenericMethodDefinition)
.Where(m =>
{
var parameters = m.GetParameters().ToList();
return parameters.Count == 2;
}).Single();
//The linq's OrderByDescending<TSource, TKey> has two generic types, which provided here
MethodInfo genericMethod = method.MakeGenericMethod(entityType, propertyInfo.PropertyType);
// Call query.OrderByDescending(selector), with query and selector: x=> x.PropName
// Note that we pass the selector as Expression to the method and we don't compile it.
// By doing so EF can extract "order by" columns and generate SQL for it
var newQuery = (IOrderedQueryable<TSource>)genericMethod.Invoke(genericMethod, new object[] { query, selector });
return newQuery;
}
public static IOrderedQueryable<TSource> ThenByDescending<TSource>(this IOrderedQueryable<TSource> query, string propertyName)
{
var entityType = typeof(TSource);
//Create x=>x.PropName
var propertyInfo = entityType.GetProperty(propertyName);
ParameterExpression arg = Expression.Parameter(entityType, "x");
MemberExpression property = Expression.Property(arg, propertyName);
var selector = Expression.Lambda(property, new ParameterExpression[] { arg });
//Get System.Linq.Queryable.OrderBy() method.
var enumarableType = typeof(System.Linq.Queryable);
var method = enumarableType.GetMethods()
.Where(m => m.Name == "OrderByDescending" && m.IsGenericMethodDefinition)
.Where(m =>
{
var parameters = m.GetParameters().ToList();
return parameters.Count == 2;
}).Single();
//The linq's OrderByDescending<TSource, TKey> has two generic types, which provided here
MethodInfo genericMethod = method.MakeGenericMethod(entityType, propertyInfo.PropertyType);
// Call query.OrderByDescending(selector), with query and selector: x=> x.PropName
// Note that we pass the selector as Expression to the method and we don't compile it.
// By doing so EF can extract "order by" columns and generate SQL for it
var newQuery = (IOrderedQueryable<TSource>)genericMethod.Invoke(genericMethod, new object[] { query, selector });
return newQuery;
}
}
}
5.总结
这一节做了以下事情:
- 通过EFCore引入了数据库,实现Code First,并注册为服务。
- 创建仓储层和服务层的基接口和基类,具体的仓储类和服务类只需要简单继承基类就可以向上层提供服务。
- 数据访问层在BaseRepository中实现,这个类中的Context由子类通过构造函数注入。
- 服务层在BaseService中实现,子类把注入的Repository通过构造函数传给BaseService,然后调用仓储层的方法。
下一节引入用户注册、登录。会有两种实现,一种是在本地数据库中管理用户,另一种是通过第三方的授权来访问网站。
标签:Core,Task,NovelTogether,NetCore,实现,ReactJS,var,using,public From: https://www.cnblogs.com/wang-yi-yi/p/16911366.html