一、基本准则
1.1 HTTP方法
使用以下HTTP方法:
方法名称 | 主作用 | 次作用 |
GET | 获取资源 | 增删改查以外的动作,内容在URL中 |
POST | 创建资源 | 增删改查以外的动作,内容在BODY中 |
PUT | 更新资源 | |
DELETE | 删除资源 |
1.2 REST URI设计准则
1、使用名词的复数表示一个资源集合,如:www.example.com/users
2、使用斜线“/”表示资源之间的层次关系,如www.example.com/users/13/books
3、增删改查操作使用HTTP方法,URI中无动词。
4、如果操作非增删改查,可加入动词,但仍以资源为主,如www.example.com/users/tom/login
5、查询字符串可以用来对资源进行筛选、搜索或分页查询。
6、URI使用小写字母。
7、单词分隔使用中划线“-”,不使用下划线“_”。
8、URI不以斜线“/”结尾。
1.3 响应代码
一般情况下,我们使用HTTP定义的响应代码。如果这些代码无法满足我们的需要,我们再增加自定义的代码。
通用的代码:
400 | 参数错误 |
500 | 内部异常 |
添加实体的代码:
201 | 添加成功 |
409 | 已存在 |
更新实体的代码:
204 | 更新成功 |
404 | 不存在 |
删除实体的代码:
200 | 删除成功 |
404 | 不存在 |
获取实体列表的代码:
200 | 获取成功 |
获取单个实体的代码:
200 | 获取成功 |
404 | 未找到 |
二、添加EF Core (DB First)
2.1 添加NuGet包
根据使用的数据库,选择相应的NuGet包:
SQL Server | Microsoft.EntityFrameworkCore.SqlServer Microsoft.EntityFrameworkCore.Tools |
MySQL | MySql.Data.EntityFrameworkCore Microsoft.EntityFrameworkCore.Tools |
SQLite | Microsoft.EntityFrameworkCore.Sqlite Microsoft.EntityFrameworkCore.Tools |
2.2 自动生成实体类
1、在Visual Studio菜单中选择:工具 > NuGet包管理器 > 程序包管理器控制台。
2、输入以下命令行:
//SQL Server
Scaffold-DbContext "连接字符串" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models -Force
//MySQL
Scaffold-DbContext "连接字符串" MySql.Data.EntityFrameworkCore -OutputDir Models -Force
//Sqlite
Scaffold-DbContext "连接字符串" Microsoft.EntityFrameworkCore.Sqlite -OutputDir Models -Force
2.3 处理自增字段
如果数据库中存在自增字段,但上下文类中没有正常处理,如:
entity.Property(e => e.UserId)
.HasColumnName("UserID")
.ValueGeneratedNever();
需要修改为:
entity.Property(e => e.UserId)
.HasColumnName("UserID")
.ValueGeneratedOnAdd();
2.4 增加添加实体
一般情况下,实体都存在主键,而这个主键在实体添加时,客户端是不确定的。其中一种处理的方法是像2.3节那样,注明自增字段,而且在创建时该值必须为0。在实体创建之后,自增字段会变成实际值。但自增字段不一定是数字(可能是Guid),而且,本身创建时就没有主键,客户端上传主键是一个浪费。所以,除了数据库实体,我们还需要增加一个专门用于添加的实体。该实体跟数据库实体的唯一区别是没有主键。两者的转化可以通过AutoMapper处理(添加AutoMapper.Extensions.Microsoft.DependencyInjection包)。
2.5 转移连接字符串
1、把连接字符串写到appsettings.json这个文件中,如下所示:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"DataConnection": "server=localhost;uid=root;pwd=password;port=3306;database=db_name;"
}
}
2、在Startup类的ConfigureServices函数中加入上下文注入(以MySQL为例):
services.AddDbContext<dataContext>(options =>options.UseMySQL(Configuration.GetConnectionString("DatabaseConnection")));
3、删除上下文类中的配置信息,清空OnConfiguring函数:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
}
2.6 使用范例
一个在Controller中读取表数据的范例为:
[ApiController]
[Route("[controller]")]
public class TestController : ControllerBase
{
private readonly ILogger<TestController> _logger;
private readonly dataContext _dataContext;
public TestController(ILogger<TestController> logger, dataContext db)
{
_logger = logger;
_dataContext = db;
}
[HttpGet]
public IEnumerable<Student> Get()
{
return _dataContext.Set<Student>().ToList();
}
}
三、添加EF Core (Code First)
3.1 添加NuGet包
根据使用的数据库,选择相应的NuGet包:
SQL Server | Microsoft.EntityFrameworkCore.SqlServer |
MySQL | MySql.Data.EntityFrameworkCore |
SQLite | Microsoft.EntityFrameworkCore.Sqlite |
3.2 编写实体类和上下文类
实体类如下所示:
/// <summary>
/// 学校
/// </summary>
public class School
{
/// <summary>
/// 学校ID
/// </summary>
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; }
/// <summary>
/// 学校名称
/// </summary>
[Required]
[MinLength(1)]
[MaxLength(20)]
public string Name { get; set; }
}
/// <summary>
/// 学生
/// </summary>
public class Student
{
/// <summary>
/// 学生ID
/// </summary>
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; }
/// <summary>
/// 姓名
/// </summary>
[Required]
[MaxLength(10)]
public string Name { get; set; }
/// <summary>
/// 性别
/// </summary>
[Required]
[RegularExpression("男|女")]
public string Gender { get; set; }
/// <summary>
/// 邮箱
/// </summary>
[EmailAddress]
public string Email { get; set; }
/// <summary>
/// 学校ID
/// </summary>
[Required]
public Guid SchoolId { get; set; }
}
上下文类如下所示:
/// <summary>
/// 学校数据上下文
/// </summary>
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
{
}
/// <summary>
/// 学校实体
/// </summary>
public DbSet<School> Schools { get; set; }
/// <summary>
/// 学生实体
/// </summary>
public DbSet<Student> Students { get; set; }
}
3.3 自动生成数据表
1、在Visual Studio菜单中选择:工具 > NuGet包管理器 > 程序包管理器控制台。
2、运行:Add-Migration InitialCreation
3、运行:Update-Database
4、可删除文件夹Migrations。
3.4 其他操作
其他操作跟DB First相同。
四、实现仓储模式
仓储模式主要用于解除业务逻辑层与数据访问层之间的耦合,使业务逻辑层在存储、访问数据库时无须关心数据的来源及存储方式。
4.1 编写仓储基类
添加仓储基类接口及实现基类:
/// <summary>
/// 仓储基类接口
/// </summary>
public interface IRepositoryBase<T, TId>
{
/// <summary>
/// 获取所有实体
/// </summary>
Task<IEnumerable<T>> GetAllAsync();
/// <summary>
/// 根据条件获取实体
/// </summary>
Task<IEnumerable<T>> GetByConditionAsync(Expression<Func<T, bool>> expression);
/// <summary>
/// 根据ID获取实体
/// </summary>
Task<T> GetByIdAsync(TId id);
/// <summary>
/// 满足某个条件的实体是否存在
/// </summary>
Task<bool> IsExistAsync(Expression<Func<T, bool>> expression);
/// <summary>
/// 创建实体
/// </summary>
void Create(T entity);
/// <summary>
/// 更新实体
/// </summary>
void Update(T entity);
/// <summary>
/// 删除实体
/// </summary>
void Delete(T entity);
/// <summary>
/// 保存修改
/// </summary>
Task SaveAsync();
}
/// <summary>
/// 仓储基类
/// </summary>
public class RepositoryBase<T, TId> : IRepositoryBase<T, TId> where T : class
{
/// <summary>
/// 数据上下文
/// </summary>
public DbContext DbContext { get; set; }
public RepositoryBase(DbContext dbContext)
{
DbContext = dbContext;
}
/// <summary>
/// 获取所有实体
/// </summary>
public Task<IEnumerable<T>> GetAllAsync()
{
return Task.FromResult(DbContext.Set<T>().AsEnumerable());
}
/// <summary>
/// 根据条件获取实体
/// </summary>
public Task<IEnumerable<T>> GetByConditionAsync(Expression<Func<T, bool>> expression)
{
return Task.FromResult(DbContext.Set<T>().Where(expression).AsEnumerable());
}
/// <summary>
/// 根据ID获取实体
/// </summary>
public async Task<T> GetByIdAsync(TId id)
{
return await DbContext.Set<T>().FindAsync(id);
}
/// <summary>
/// 满足某个条件的实体是否存在
/// </summary>
public async Task<bool> IsExistAsync(Expression<Func<T, bool>> expression)
{
return await DbContext.Set<T>().AnyAsync(expression);
}
/// <summary>
/// 创建实体
/// </summary>
public void Create(T entity)
{
DbContext.Set<T>().Add(entity);
}
/// <summary>
/// 更新实体
/// </summary>
public void Update(T entity)
{
DbContext.Set<T>().Update(entity);
}
/// <summary>
/// 删除实体
/// </summary>
public void Delete(T entity)
{
DbContext.Set<T>().Remove(entity);
}
/// <summary>
/// 保存修改
/// </summary>
public async Task SaveAsync()
{
var result = await DbContext.SaveChangesAsync() > 0;
if (!result)
{
throw new Exception("Commit fail.");
}
}
}
4.2 编写针对实体的仓储类
对于每个实体,都需要编写一个仓储类。一方面实例化仓储类中的泛型,另一方面可以添加实体额外需要的一些操作。
下面是一个实体示例:
public interface ISchoolRepository : IRepositoryBase<School, Guid>
{
}
public class SchoolRepository : RepositoryBase<School, Guid>, ISchoolRepository
{
public SchoolRepository(DbContext dbContext) : base(dbContext)
{
}
}
4.3 增加仓储封装类
仓储封装类能够方便我们对仓储类的引用。如下所示:
public interface IRepositoryWrapper
{
ISchoolRepository School { get; }
IStudentRepository Student { get; }
}
public class RepositoryWrapper : IRepositoryWrapper
{
public SchoolContext SchoolContext { get; }
public RepositoryWrapper(SchoolContext schoolContext)
{
SchoolContext = schoolContext;
}
private ISchoolRepository _school = null;
public ISchoolRepository School => _school ?? new SchoolRepository(SchoolContext);
private IStudentRepository _student = null;
public IStudentRepository Student => _student ?? new StudentRepository(SchoolContext);
}
4.4 注入仓储类
在Startup类的ConfigureServices函数中,增加以下语句:
services.AddScoped<IRepositoryWrapper, RepositoryWrapper>();
五、添加日志
5.1 添加日志NuGet包
我们使用NLog输出日志,所以添加NLog.Web.AspNetCore包。
5.2 增加日志配置
在项目中增加一个nlog.config的配置文件,其内容如下:
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
autoReload="true"
internalLogLevel="Info"
internalLogFile="D:\工作\APITemplate\WebData\log\internal-log.txt">
<extensions>
<add assembly="NLog.Web.AspNetCore"/>
</extensions>
<targets>
<target xsi:type="File" name="allfile" fileName="D:\工作\APITemplate\WebData\log\${shortdate}.log"
layout="[${longdate} | ${uppercase:${level}}] [${aspnet-request-url} | ${aspnet-mvc-action}] ${message}" />
</targets>
<rules>
<logger name="*" minlevel="Trace" writeTo="allfile" />
</rules>
</nlog>
5.3 注入日志管理器
把Program的Main修改成如下所示:
public static void Main(string[] args)
{
NLog.Logger logger = NLogBuilder.ConfigureNLog("nlog.config").GetCurrentClassLogger();
try
{
logger.Debug("服务启动");
CreateHostBuilder(args).Build().Run();
}
catch (Exception ex)
{
logger.Error(ex, "服务异常退出");
throw;
}
finally
{
NLog.LogManager.Shutdown();
}
}
另外,CreateHostBuilder函数需添加以下内容:
.ConfigureLogging(logging =>
{
logging.SetMinimumLevel(LogLevel.Trace);
})
.UseNLog();
5.4 使用范例
在Controller的构造函数中注入:
private readonly ILogger<SchoolsController> _logger;
public SchoolsController(ILogger<SchoolsController> logger)
{
_logger = logger;
}
然后就可以在代码中写日志了:
_logger.LogError(ex.Message);
六、添加文档
我们使用Swagger制作API文档,需要引入Swashbuckle.AspNetCore包。
6.1 基于项目原来的注释
另外制作一个说明文档是很麻烦的,我们可以基于代码中的注释生成API文档。那首先,我们需要生成项目注释文档。
打开项目属性,在生成配置,勾选XML文档文件,如下所示:
6.2 配置Swagger
在Startup的ConfigureServices函数中,增加以下代码:
services.AddSwaggerGen(c =>
{
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
c.IncludeXmlComments(xmlPath);
});
在Configure函数中,增加以下代码:
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "APITemplate API");
});
6.3 编写范例
以下是一个文档范例:
/// <summary>
/// 添加学校
/// </summary>
/// <param name="school">学校信息</param>
/// <response code="201">添加成功</response>
/// <response code="409">学校已存在</response>
/// <response code="400">参数错误</response>
/// <response code="500">内部异常</response>
[HttpPost]
[ProducesResponseType(typeof(School), 201)]
public async Task<ActionResult<School>> AddSchool(SchoolToAdd school)
其显示效果将如下图所示:
七、Controller增删改查范例
完成上面的代码编写之后,一般的Controller增删改查操作都是基本相似的。很多时候,在此之上,修改类名即可。
[Route("api/[controller]")]
[ApiController]
public class SchoolsController : ControllerBase
{
private readonly ILogger<SchoolsController> _logger;
private readonly IRepositoryWrapper _repository;
private readonly IMapper _mapper;
public SchoolsController(ILogger<SchoolsController> logger, IRepositoryWrapper repository, IMapper mapper)
{
_logger = logger;
_repository = repository;
_mapper = mapper;
}
/// <summary>
/// 获取学校列表
/// </summary>
/// <response code="200">获取成功</response>
/// <response code="400">参数错误</response>
/// <response code="500">内部异常</response>
[HttpGet]
public async Task<ActionResult<IEnumerable<School>>> GetSchools()
{
try
{
var list = await _repository.School.GetAllAsync();
return Ok(list);
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
/// <summary>
/// 获取指定学校信息
/// </summary>
/// <param name="id">学校ID</param>
/// <response code="200">获取成功</response>
/// <response code="404">未找到学校</response>
/// <response code="400">参数错误</response>
/// <response code="500">内部异常</response>
[HttpGet("{id}")]
public async Task<ActionResult<School>> GetSchool(Guid id)
{
try
{
var school = await _repository.School.GetByIdAsync(id);
if (school == null)
{
return NotFound();
}
return school;
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
/// <summary>
/// 添加学校
/// </summary>
/// <param name="school">学校信息</param>
/// <response code="201">添加成功</response>
/// <response code="409">学校已存在</response>
/// <response code="400">参数错误</response>
/// <response code="500">内部异常</response>
[HttpPost]
[ProducesResponseType(typeof(School), 201)]
public async Task<ActionResult<School>> AddSchool(SchoolToAdd school)
{
try
{
if (await _repository.School.IsExistAsync(p => p.Name == school.Name))
{
return Conflict();
}
var _school = _mapper.Map<School>(school);
_repository.School.Create(_school);
await _repository.School.SaveAsync();
return CreatedAtAction("GetSchool", new { id = _school.Id }, _school);
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
/// <summary>
/// 更新学校
/// </summary>
/// <param name="school">学校信息</param>
/// <response code="204">更新成功</response>
/// <response code="404">学校不存在</response>
/// <response code="400">参数错误</response>
/// <response code="500">内部异常</response>
[HttpPut]
[ProducesResponseType(204)]
public async Task<IActionResult> UpdateSchool(School school)
{
try
{
if (!await _repository.School.IsExistAsync(p => p.Id == school.Id))
{
return NotFound();
}
_repository.School.Update(school);
await _repository.School.SaveAsync();
return NoContent();
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
/// <summary>
/// 删除学校
/// </summary>
/// <param name="id">学校ID</param>
/// <response code="200">删除成功</response>
/// <response code="404">学校不存在</response>
/// <response code="400">参数错误</response>
/// <response code="500">内部异常</response>
[HttpDelete("{id}")]
public async Task<ActionResult<School>> DeleteSchool(Guid id)
{
try
{
var school = await _repository.School.GetByIdAsync(id);
if (school == null)
{
return NotFound();
}
_repository.School.Delete(school);
await _repository.School.SaveAsync();
return school;
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
/// <summary>
/// 获取指定学校的学生信息
/// </summary>
/// <param name="id">学校ID</param>
/// <param name="pageNumber">页码</param>
/// <param name="pageSize">每页项数</param>
/// <response code="200">获取成功</response>
/// <response code="404">未找到学校</response>
/// <response code="400">参数错误</response>
/// <response code="500">内部异常</response>
[HttpGet("{id}/Students")]
public async Task<ActionResult<IEnumerable<Student>>> GetSchoolStudents(Guid id, [FromQuery] int pageNumber, [FromQuery] int pageSize)
{
try
{
var school = await _repository.School.GetByIdAsync(id);
if (school == null)
{
return NotFound();
}
var list = await _repository.Student.GetByConditionAsync(p => p.SchoolId == id);
var pageList = await PageList<Student>.CreateAsync(list, pageNumber, pageSize);
var paginationMetadata = new
{
totalCount = pageList.TotalCount,
pageSize = pageList.PageSize,
currentPage = pageList.CurrentPage,
totalPages = pageList.TotalPages
};
Response.Headers.Add("X-Pagination", JsonConvert.SerializeObject(paginationMetadata));
return Ok(pageList);
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
}