需求

经常写CRUD程序的小伙伴们可能都经历过定义很多Repository接口,分别做对应的实现,依赖注入并使用的场景。有的时候会发现,很多分散的XXXXRepository的逻辑都是基本一致的,于是开始思考是否可以将这些操作抽象出去,当然是可以的,而且被抽象出去的部分是可以不加改变地在今后的任何有此需求的项目中直接引入使用。

那么我们本文的需求就是:如何实现一个可重用的Repository模块。

长文预警,包含大量代码。

目标

实现通用Repository模式并进行验证。

原理和思路

通用的基础在于抽象,抽象的粒度决定了通用的程度,但是同时也决定了使用上的复杂度。对于自己的项目而言,抽象到什么程度最合适,需要自己去权衡,也许后面某个时候我会决定自己去实现一个完善的Repository库提供出来(事实上已经有很多人这样做了,我们甚至可以直接下载Nuget包进行使用,但是自己亲手去实现的过程能让你更好地去理解其中的原理,也理解如何开发一个通用的类库。)

总体思路是:在Application中定义相关的接口,在Infrastructure中实现基类的功能。

实现

通用Repository实现

对于要如何去设计一个通用的Repository库,实际上涉及的面非常多,尤其是在获取数据的时候。而且根据每个人的习惯,实现起来的方式是有比较大的差别的,尤其是关于泛型接口到底需要提供哪些方法,每个人都有自己的理解,这里我只演示基本的思路,而且尽量保持简单,关于更复杂和更全面的实现,GIthub上有很多已经写好的库可以去学习和参考,我会列在下面:

很显然,第一步要去做的是在Application/Common/Interfaces中增加一个IRepository<T>的定义用于适用不同类型的实体,然后在Infrastructure/Persistence/Repositories中创建一个基类RepositoryBase<T>实现这个接口,并有办法能提供一致的对外方法签名。

  • IRepository.cs
namespace TodoList.Application.Common.Interfaces;

public interface IRepository<T> where T : class
{
}
  • RepositoryBase.cs
using Microsoft.EntityFrameworkCore;
using TodoList.Application.Common.Interfaces; namespace TodoList.Infrastructure.Persistence.Repositories; public class RepositoryBase<T> : IRepository<T> where T : class
{
private readonly TodoListDbContext _dbContext; public RepositoryBase(TodoListDbContext dbContext) => _dbContext = dbContext;
}

在动手实际定义IRepository<T>之前,先思考一下:对数据库的操作都会出现哪些情况:

新增实体(Create)

新增实体在Repository层面的逻辑很简单,传入一个实体对象,然后保存到数据库就可以了,没有其他特殊的需求。

  • IRepository.cs
// 省略其他...
// Create相关操作接口
Task<T> AddAsync(T entity, CancellationToken cancellationToken = default);
  • RepositoryBase.cs
// 省略其他...
public async Task<T> AddAsync(T entity, CancellationToken cancellationToken = default)
{
await _dbContext.Set<T>().AddAsync(entity, cancellationToken);
await _dbContext.SaveChangesAsync(cancellationToken); return entity;
}

更新实体(Update)

和新增实体类似,但是更新时一般是单个实体对象去操作。

  • IRepository.cs
// 省略其他...
// Update相关操作接口
Task UpdateAsync(T entity, CancellationToken cancellationToken = default);
  • RepositoryBase.cs
// 省略其他...
public async Task UpdateAsync(T entity, CancellationToken cancellationToken = default)
{
// 对于一般的更新而言,都是Attach到实体上的,只需要设置该实体的State为Modified就可以了
_dbContext.Entry(entity).State = EntityState.Modified;
await _dbContext.SaveChangesAsync(cancellationToken);
}

删除实体(Delete)

对于删除实体,可能会出现两种情况:删除一个实体;或者删除一组实体。

  • IRepository.cs
// 省略其他...
// Delete相关操作接口,这里根据key删除对象的接口需要用到一个获取对象的方法
ValueTask<T?> GetAsync(object key);
Task DeleteAsync(object key);
Task DeleteAsync(T entity, CancellationToken cancellationToken = default);
Task DeleteRangeAsync(IEnumerable<T> entities, CancellationToken cancellationToken = default);
  • RepositoryBase.cs
// 省略其他...
public virtual ValueTask<T?> GetAsync(object key) => _dbContext.Set<T>().FindAsync(key); public async Task DeleteAsync(object key)
{
var entity = await GetAsync(key);
if (entity is not null)
{
await DeleteAsync(entity);
}
} public async Task DeleteAsync(T entity, CancellationToken cancellationToken = default)
{
_dbContext.Set<T>().Remove(entity);
await _dbContext.SaveChangesAsync(cancellationToken);
} public async Task DeleteRangeAsync(IEnumerable<T> entities, CancellationToken cancellationToken = default)
{
_dbContext.Set<T>().RemoveRange(entities);
await _dbContext.SaveChangesAsync(cancellationToken);
}

获取实体(Retrieve)

对于如何获取实体,是最复杂的一部分。我们不仅要考虑通过什么方式获取哪些数据,还需要考虑获取的数据有没有特殊的要求比如排序、分页、数据对象类型的转换之类的问题。

具体来说,比如下面这一个典型的LINQ查询语句:

var results = await _context.A.Join(_context.B, a => a.Id, b => b.aId, (a, b) => new
{
// ...
})
.Where(ab => ab.Name == "name" && ab.Date == DateTime.Now)
.Select(ab => new
{
// ...
})
.OrderBy(o => o.Date)
.Skip(20 * 1)
.Take(20)
.ToListAsync();

可以将整个查询结构分割成以下几个组成部分,而且每个部分基本都是以lambda表达式的方式表示的,这转化成建模的话,可以使用Expression相关的对象来表示:

  1. 查询数据集准备过程,在这个过程中可能会出现Include/Join/GroupJoin/GroupBy等等类似的关键字,它们的作用是构建一个用于接下来将要进行查询的数据集。
  2. Where子句,用于过滤查询集合。
  3. Select子句,用于转换原始数据类型到我们想要的结果类型。
  4. Order子句,用于对结果集进行排序,这里可能会包含类似:OrderBy/OrderByDescending/ThenBy/ThenByDescending等关键字。
  5. Paging子句,用于对结果集进行后端分页返回,一般都是Skip/Take一起使用。
  6. 其他子句,多数是条件控制,比如AsNoTracking/SplitQuery等等。

为了保持我们的演示不会过于复杂,我会做一些取舍。在这里的实现我参考了Edi.WangMoonglade中的相关实现。有兴趣的小伙伴也可以去找一下一个更完整的实现:Ardalis.Specification

首先来定义一个简单的ISpecification来表示查询的各类条件:

using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore.Query; namespace TodoList.Application.Common.Interfaces; public interface ISpecification<T>
{
// 查询条件子句
Expression<Func<T, bool>> Criteria { get; }
// Include子句
Func<IQueryable<T>, IIncludableQueryable<T, object>> Include { get; }
// OrderBy子句
Expression<Func<T, object>> OrderBy { get; }
// OrderByDescending子句
Expression<Func<T, object>> OrderByDescending { get; } // 分页相关属性
int Take { get; }
int Skip { get; }
bool IsPagingEnabled { get; }
}

并实现这个泛型接口,放在Application/Common中:

using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore.Query;
using TodoList.Application.Common.Interfaces; namespace TodoList.Application.Common; public abstract class SpecificationBase<T> : ISpecification<T>
{
protected SpecificationBase() { }
protected SpecificationBase(Expression<Func<T, bool>> criteria) => Criteria = criteria; public Expression<Func<T, bool>> Criteria { get; private set; }
public Func<IQueryable<T>, IIncludableQueryable<T, object>> Include { get; private set; }
public List<string> IncludeStrings { get; } = new();
public Expression<Func<T, object>> OrderBy { get; private set; }
public Expression<Func<T, object>> OrderByDescending { get; private set; } public int Take { get; private set; }
public int Skip { get; private set; }
public bool IsPagingEnabled { get; private set; } public void AddCriteria(Expression<Func<T, bool>> criteria) => Criteria = Criteria is not null ? Criteria.AndAlso(criteria) : criteria; protected virtual void AddInclude(Func<IQueryable<T>, IIncludableQueryable<T, object>> includeExpression) => Include = includeExpression;
protected virtual void AddInclude(string includeString) => IncludeStrings.Add(includeString); protected virtual void ApplyPaging(int skip, int take)
{
Skip = skip;
Take = take;
IsPagingEnabled = true;
} protected virtual void ApplyOrderBy(Expression<Func<T, object>> orderByExpression) => OrderBy = orderByExpression;
protected virtual void ApplyOrderByDescending(Expression<Func<T, object>> orderByDescendingExpression) => OrderByDescending = orderByDescendingExpression;
} // https://stackoverflow.com/questions/457316/combining-two-expressions-expressionfunct-bool
public static class ExpressionExtensions
{
public static Expression<Func<T, bool>> AndAlso<T>(this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2)
{
var parameter = Expression.Parameter(typeof(T)); var leftVisitor = new ReplaceExpressionVisitor(expr1.Parameters[0], parameter);
var left = leftVisitor.Visit(expr1.Body); var rightVisitor = new ReplaceExpressionVisitor(expr2.Parameters[0], parameter);
var right = rightVisitor.Visit(expr2.Body); return Expression.Lambda<Func<T, bool>>(
Expression.AndAlso(left ?? throw new InvalidOperationException(),
right ?? throw new InvalidOperationException()), parameter);
} private class ReplaceExpressionVisitor : ExpressionVisitor
{
private readonly Expression _oldValue;
private readonly Expression _newValue; public ReplaceExpressionVisitor(Expression oldValue, Expression newValue)
{
_oldValue = oldValue;
_newValue = newValue;
} public override Expression Visit(Expression node) => node == _oldValue ? _newValue : base.Visit(node);
}
}

为了在RepositoryBase中能够把所有的Spcification串起来形成查询子句,我们还需要定义一个用于组织Specification的SpecificationEvaluator类:

using TodoList.Application.Common.Interfaces;

namespace TodoList.Application.Common;

public class SpecificationEvaluator<T> where T : class
{
public static IQueryable<T> GetQuery(IQueryable<T> inputQuery, ISpecification<T>? specification)
{
var query = inputQuery; if (specification?.Criteria is not null)
{
query = query.Where(specification.Criteria);
} if (specification?.Include is not null)
{
query = specification.Include(query);
} if (specification?.OrderBy is not null)
{
query = query.OrderBy(specification.OrderBy);
}
else if (specification?.OrderByDescending is not null)
{
query = query.OrderByDescending(specification.OrderByDescending);
} if (specification?.IsPagingEnabled != false)
{
query = query.Skip(specification!.Skip).Take(specification.Take);
} return query;
}
}

IRepository中添加查询相关的接口,大致可以分为以下这几类接口,每类中又可能存在同步接口和异步接口:

  • IRepository.cs
// 省略其他...
// 1. 查询基础操作接口
IQueryable<T> GetAsQueryable(); // 2. 查询数量相关接口
int Count(ISpecification<T>? spec = null);
int Count(Expression<Func<T, bool>> condition);
Task<int> CountAsync(ISpecification<T>? spec); // 3. 查询存在性相关接口
bool Any(ISpecification<T>? spec);
bool Any(Expression<Func<T, bool>>? condition = null); // 4. 根据条件获取原始实体类型数据相关接口
Task<T?> GetAsync(Expression<Func<T, bool>> condition);
Task<IReadOnlyList<T>> GetAsync();
Task<IReadOnlyList<T>> GetAsync(ISpecification<T>? spec); // 5. 根据条件获取映射实体类型数据相关接口,涉及到Group相关操作也在其中,使用selector来传入映射的表达式
TResult? SelectFirstOrDefault<TResult>(ISpecification<T>? spec, Expression<Func<T, TResult>> selector);
Task<TResult?> SelectFirstOrDefaultAsync<TResult>(ISpecification<T>? spec, Expression<Func<T, TResult>> selector); Task<IReadOnlyList<TResult>> SelectAsync<TResult>(Expression<Func<T, TResult>> selector);
Task<IReadOnlyList<TResult>> SelectAsync<TResult>(ISpecification<T>? spec, Expression<Func<T, TResult>> selector);
Task<IReadOnlyList<TResult>> SelectAsync<TGroup, TResult>(Expression<Func<T, TGroup>> groupExpression, Expression<Func<IGrouping<TGroup, T>, TResult>> selector, ISpecification<T>? spec = null);

有了这些基础,我们就可以去Infrastructure/Persistence/Repositories中实现RepositoryBase类剩下的关于查询部分的代码了:

  • RepositoryBase.cs
// 省略其他...
// 1. 查询基础操作接口实现
public IQueryable<T> GetAsQueryable()
=> _dbContext.Set<T>(); // 2. 查询数量相关接口实现
public int Count(Expression<Func<T, bool>> condition)
=> _dbContext.Set<T>().Count(condition); public int Count(ISpecification<T>? spec = null)
=> null != spec ? ApplySpecification(spec).Count() : _dbContext.Set<T>().Count(); public Task<int> CountAsync(ISpecification<T>? spec)
=> ApplySpecification(spec).CountAsync(); // 3. 查询存在性相关接口实现
public bool Any(ISpecification<T>? spec)
=> ApplySpecification(spec).Any(); public bool Any(Expression<Func<T, bool>>? condition = null)
=> null != condition ? _dbContext.Set<T>().Any(condition) : _dbContext.Set<T>().Any(); // 4. 根据条件获取原始实体类型数据相关接口实现
public async Task<T?> GetAsync(Expression<Func<T, bool>> condition)
=> await _dbContext.Set<T>().FirstOrDefaultAsync(condition); public async Task<IReadOnlyList<T>> GetAsync()
=> await _dbContext.Set<T>().AsNoTracking().ToListAsync(); public async Task<IReadOnlyList<T>> GetAsync(ISpecification<T>? spec)
=> await ApplySpecification(spec).AsNoTracking().ToListAsync(); // 5. 根据条件获取映射实体类型数据相关接口实现
public TResult? SelectFirstOrDefault<TResult>(ISpecification<T>? spec, Expression<Func<T, TResult>> selector)
=> ApplySpecification(spec).AsNoTracking().Select(selector).FirstOrDefault(); public Task<TResult?> SelectFirstOrDefaultAsync<TResult>(ISpecification<T>? spec, Expression<Func<T, TResult>> selector)
=> ApplySpecification(spec).AsNoTracking().Select(selector).FirstOrDefaultAsync(); public async Task<IReadOnlyList<TResult>> SelectAsync<TResult>(Expression<Func<T, TResult>> selector)
=> await _dbContext.Set<T>().AsNoTracking().Select(selector).ToListAsync(); public async Task<IReadOnlyList<TResult>> SelectAsync<TResult>(ISpecification<T>? spec, Expression<Func<T, TResult>> selector)
=> await ApplySpecification(spec).AsNoTracking().Select(selector).ToListAsync(); public async Task<IReadOnlyList<TResult>> SelectAsync<TGroup, TResult>(
Expression<Func<T, TGroup>> groupExpression,
Expression<Func<IGrouping<TGroup, T>, TResult>> selector,
ISpecification<T>? spec = null)
=> null != spec ?
await ApplySpecification(spec).AsNoTracking().GroupBy(groupExpression).Select(selector).ToListAsync() :
await _dbContext.Set<T>().AsNoTracking().GroupBy(groupExpression).Select(selector).ToListAsync(); // 用于拼接所有Specification的辅助方法,接收一个`IQuerybale<T>对象(通常是数据集合)
// 和一个当前实体定义的Specification对象,并返回一个`IQueryable<T>`对象为子句执行后的结果。
private IQueryable<T> ApplySpecification(ISpecification<T>? spec)
=> SpecificationEvaluator<T>.GetQuery(_dbContext.Set<T>().AsQueryable(), spec);

引入使用

为了验证通用Repsitory的用法,我们可以先在Infrastructure/DependencyInjection.cs中进行依赖注入:

// in AddInfrastructure, 省略其他
services.AddScoped(typeof(IRepository<>), typeof(RepositoryBase<>));

验证

用于初步验证(主要是查询接口),我们在Application项目里新建文件夹TodoItems/Specs,创建一个TodoItemSpec类:

  • TodoItemSpec.cs
using TodoList.Application.Common;
using TodoList.Domain.Entities;
using TodoList.Domain.Enums; namespace TodoList.Application.TodoItems.Specs; public sealed class TodoItemSpec : SpecificationBase<TodoItem>
{
public TodoItemSpec(bool done, PriorityLevel priority) : base(t => t.Done == done && t.Priority == priority)
{
}
}

然后我们临时使用示例接口WetherForecastController,通过日志来看一下查询的正确性。

private readonly IRepository<TodoItem> _repository;
private readonly ILogger<WeatherForecastController> _logger; // 为了验证,临时在这注入IRepository<TodoItem>对象,验证完后撤销修改
public WeatherForecastController(IRepository<TodoItem> repository, ILogger<WeatherForecastController> logger)
{
_repository = repository;
_logger = logger;
}

Get方法里增加这段逻辑用于观察日志输出:

// 记录日志
_logger.LogInformation($"maybe this log is provided by Serilog..."); var spec = new TodoItemSpec(true, PriorityLevel.High);
var items = _repository.GetAsync(spec).Result; foreach (var item in items)
{
_logger.LogInformation($"item: {item.Id} - {item.Title} - {item.Priority}");
}

启动Api项目然后请求示例接口,观察控制台输出:

# 以上省略,Controller日志开始...
[16:49:59 INF] maybe this log is provided by Serilog...
[16:49:59 INF] Entity Framework Core 6.0.1 initialized 'TodoListDbContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer:6.0.1' with options: MigrationsAssembly=TodoList.Infrastructure, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
[16:49:59 INF] Executed DbCommand (51ms) [Parameters=[@__done_0='?' (DbType = Boolean), @__priority_1='?' (DbType = Int32)], CommandType='Text', CommandTimeout='30']
SELECT [t].[Id], [t].[Created], [t].[CreatedBy], [t].[Done], [t].[LastModified], [t].[LastModifiedBy], [t].[ListId], [t].[Priority], [t].[Title]
FROM [TodoItems] AS [t]
WHERE ([t].[Done] = @__done_0) AND ([t].[Priority] = @__priority_1)
# 下面这句是我们之前初始化数据库的种子数据,可以参考上一篇文章结尾的验证截图。
[16:49:59 INF] item: 87f1ddf1-e6cd-4113-74ed-08d9c5112f6b - Apples - High
[16:49:59 INF] Executing ObjectResult, writing value of type 'TodoList.Api.WeatherForecast[]'.
[16:49:59 INF] Executed action TodoList.Api.Controllers.WeatherForecastController.Get (TodoList.Api) in 160.5517ms

总结

在本文中,我大致演示了实现一个通用Repository基础框架的过程。实际上关于Repository的组织与实现有很多种实现方法,每个人的关注点和思路都会有不同,但是大的方向基本都是这样,无非是抽象的粒度和提供的接口的方便程度不同。有兴趣的像伙伴可以仔细研究一下参考资料里的第2个实现,也可以从Nuget直接下载在项目中引用使用。

感谢大家耐心看完。从下一篇文章开始,我们就进入喜闻乐见的CRUD环节。

参考资料

  1. Moonglade from Edi Wang
  2. Ardalis.Specification

系列导航

  • 使用.NET 6开发TodoList应用(1)——系列背景
  • 使用.NET 6开发TodoList应用(2)——项目结构搭建
  • 使用.NET 6开发TodoList应用(3)——引入第三方日志
  • 使用.NET 6开发TodoList应用(4)——引入数据存储
  • 使用.NET 6开发TodoList应用(5)——领域实体创建
  • 使用.NET 6开发TodoList应用(5.1)——实现Repository模式
  • 使用.NET 6开发TodoList应用(6)——实现POST请求
  • 使用.NET 6开发TodoList应用(6.1)——实现CQRS模式
  • 使用.NET 6开发TodoList应用(6.2)——实现AutoMapper
  • 使用.NET 6开发TodoList应用(7)——实现GET请求
  • 使用.NET 6开发TodoList应用(8)——实现全局异常处理
  • 使用.NET 6开发TodoList应用(9)——实现PUT请求
  • 使用.NET 6开发TodoList应用(10)——实现PATCH请求
  • 使用.NET 6开发TodoList应用(11)——HTTP请求幂等性的考虑
  • 使用.NET 6开发TodoList应用(12)——实现接口请求验证
  • 使用.NET 6开发TodoList应用(13)——实现ActionFilter
  • 使用.NET 6开发TodoList应用(14)——实现查询分页
  • 使用.NET 6开发TodoList应用(15)——实现查询过滤
  • 使用.NET 6开发TodoList应用(16)——实现查询搜索
  • 使用.NET 6开发TodoList应用(17)——实现查询排序
  • 使用.NET 6开发TodoList应用(18)——实现数据塑形
  • 使用.NET 6开发TodoList应用(19)——实现HATEAOS支持
  • 使用.NET 6开发TodoList应用(20)——处理OPTION和HEAD请求
  • 使用.NET 6开发TodoList应用(21)——实现Root Document
  • 使用.NET 6开发TodoList应用(22)——实现API版本控制
  • 使用.NET 6开发TodoList应用(23)——实现缓存
  • 使用.NET 6开发TodoList应用(24)——实现请求限流和阈值控制
  • 使用.NET 6开发TodoList应用(25)——实现基于JWT的Identity功能
  • 使用.NET 6开发TodoList应用(26)——实现RefreshToken
  • 使用.NET 6开发TodoList应用(27)——实现Configuration和Option的强类型绑定
  • 使用.NET 6开发TodoList应用(28)——实现API的Swagger文档化
  • 使用.NET 6开发TodoList应用(29)——实现应用程序健康检查
  • 使用.NET 6开发TodoList应用(30)——实现本地化功能
  • 使用.NET 6开发TodoList应用(31)——实现Docker打包和部署
  • 使用.NET 6开发TodoList应用(32)——实现基于Github Actions和ACI的CI/CD

使用.NET 6开发TodoList应用(5.1)——实现Repository模式的更多相关文章

  1. 使用.NET 6开发TodoList应用(5)——领域实体创建

    需求 上一篇文章中我们完成了数据存储服务的接入,从这一篇开始将正式进入业务逻辑部分的开发. 首先要定义和解决的问题是,根据TodoList项目的需求,我们应该设计怎样的数据实体,如何去进行操作? 长文 ...

  2. 使用.NET 6开发TodoList应用(6)——使用MediatR实现POST请求

    需求 需求很简单:如何创建新的TodoList和TodoItem并持久化. 初学者按照教程去实现的话,应该分成以下几步:创建Controller并实现POST方法:实用传入的请求参数new一个数据库实 ...

  3. 使用.NET 6开发TodoList应用文章索引

    系列导航 使用.NET 6开发TodoList应用(1)--系列背景 使用.NET 6开发TodoList应用(2)--项目结构搭建 使用.NET 6开发TodoList应用(3)--引入第三方日志 ...

  4. 使用.NET 6开发TodoList应用(7)——使用AutoMapper实现GET请求

    系列导航 使用.NET 6开发TodoList应用文章索引 需求 需求很简单:实现GET请求获取业务数据.在这个阶段我们经常使用的类库是AutoMapper. 目标 合理组织并使用AutoMapper ...

  5. 使用.NET 6开发TodoList应用(3)——引入第三方日志库

    需求 在我们项目开发的过程中,使用.NET 6自带的日志系统有时是不能满足实际需求的,比如有的时候我们需要将日志输出到第三方平台上,最典型的应用就是在各种云平台上,为了集中管理日志和查询日志,通常会选 ...

  6. 使用.NET 6开发TodoList应用(1)——系列背景

    前言 想到要写这样一个系列博客,初衷有两个:一是希望通过一个实践项目,将.NET 6 WebAPI开发的基础知识串联起来,帮助那些想要入门.NET 6服务端开发的朋友们快速上手,对使用.NET 6开发 ...

  7. 使用.NET 6开发TodoList应用(2)——项目结构搭建

    为了不影响阅读的体验,我把系列导航放到文章最后了,有需要的小伙伴可以直接通过导航跳转到对应的文章 : P TodoList需求简介 首先明确一下我们即将开发的这个TodoList应用都需要完成什么功能 ...

  8. 使用.NET 6开发TodoList应用(4)——引入数据存储

    需求 作为后端CRUD程序员(bushi,数据存储是开发后端服务一个非常重要的组件.对我们的TodoList项目来说,自然也需要配置数据存储.目前的需求很简单: 需要能持久化TodoList对象并对其 ...

  9. 使用.NET 6开发TodoList应用(8)——实现全局异常处理

    系列导航 使用.NET 6开发TodoList应用文章索引 需求 因为在项目中,会有各种各样的领域异常或系统异常被抛出来,那么在Controller里就需要进行完整的try-catch捕获,并根据是否 ...

随机推荐

  1. DirectX12 3D 游戏开发与实战第十二章内容

    12.几何着色器 如果不启用曲面细分,那么几何着色器这个可选阶段将会在位于顶点着色器和像素着色器之间.顶点着色器以顶点作为输入数据,而几何着色器以完整的图元为输入数据.与顶点着色器不同的是,顶点着色器 ...

  2. python-django-常用models里面的Field

    1.models.AutoField 自增列 = int(11) 如果没有的话,默认会生成一个名称为 id 的列 如果要显式的自定义一个自增列,必须设置primary_key=True. 2.mode ...

  3. R语言与医学统计图形-【19】ggplot2坐标轴调节

    ggplot2绘图系统--坐标轴调节 scale函数:图形遥控器.坐标轴标度函数: scale_x_continous scale_y_continous scale_x_discrete scale ...

  4. 63. Binary Tree Level Order Traversal II

    Binary Tree Level Order Traversal II My Submissions QuestionEditorial Solution Total Accepted: 79742 ...

  5. ping 的原理

    ping 的原理ping 程序是用来探测主机到主机之间是否可通信,如果不能ping到某台主机,表明不能和这台主机建立连接.ping 使用的是ICMP协议,它发送icmp回送请求消息给目的主机.ICMP ...

  6. MySQL-数据库多表关联查询太慢,如何进行SQL语句优化

    工作中我们经常用到多个left join去关联其他表查询结果,但是随着数据量的增加,一个表的数据达到百万级别后,这种普通的left join查询将非常的耗时. 举个例子:  现在porder表有 10 ...

  7. 如何利用nrfjprog.exe读写nrf51的flash

    版权声明:本文为博主原创文章,未经博主允许不得转载.   1.目的 为了方便平时在开发中的调试,验证一些想法是否正确. 2.平台: Jlink version:v5.02c nrf51822硬件板等. ...

  8. day05 django框架之路由层

    day05 django框架之路由层 今日内容概要 简易版django请求声明周期流程图(重要) 路由匹配 无名有名分组 反向解析 无名有名解析 路由分发 名称空间 伪静态 虚拟环境 简易版djang ...

  9. day04 sersync实时同步和ssh服务

    day04 sersync实时同步和ssh服务 sersync实时同步 1.什么是实时同步 实时同步是一种只要当前目录发生变化则会触发一个事件,事件触发后会将变化的目录同步至远程服务器. 2.为什么使 ...

  10. 数据存储SharePreferences详解

    1.SharedPreferences存储 SharedPreferences时使用键值对的方式来存储数据的,也就是在保存一条数据时,需要给这条数据提供一个对应的键,这样在读取的时候就可以通过这个键把 ...