使用以下HTTP方法:
方法名称 | 主作用 | 次作用 |
GET | 获取资源 | 增删改查以外的动作,内容在URL中 |
POST | 创建资源 | 增删改查以外的动作,内容在BODY中 |
PUT | 更新资源 | |
DELETE | 删除资源 |
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不以斜线“/”结尾。
一般情况下,我们使用HTTP定义的响应代码。如果这些代码无法满足我们的需要,我们再增加自定义的代码。
通用的代码:
400 | 参数错误 |
500 | 内部异常 |
添加实体的代码:
201 | 添加成功 |
409 | 已存在 |
更新实体的代码:
204 | 更新成功 |
404 | 不存在 |
删除实体的代码:
200 | 删除成功 |
404 | 不存在 |
获取实体列表的代码:
200 | 获取成功 |
获取单个实体的代码:
200 | 获取成功 |
404 | 未找到 |
根据使用的数据库,选择相应的NuGet包:
SQL Server | Microsoft.EntityFrameworkCore.SqlServer Microsoft.EntityFrameworkCore.Tools |
MySQL | MySql.Data.EntityFrameworkCore Microsoft.EntityFrameworkCore.Tools |
SQLite | Microsoft.EntityFrameworkCore.Sqlite Microsoft.EntityFrameworkCore.Tools |
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
如果数据库中存在自增字段,但上下文类中没有正常处理,如:
entity.Property(e => e.UserId)
.HasColumnName("UserID")
.ValueGeneratedNever();
需要修改为:
entity.Property(e => e.UserId)
.HasColumnName("UserID")
.ValueGeneratedOnAdd();
一般情况下,实体都存在主键,而这个主键在实体添加时,客户端是不确定的。其中一种处理的方法是像2.3节那样,注明自增字段,而且在创建时该值必须为0。在实体创建之后,自增字段会变成实际值。但自增字段不一定是数字(可能是Guid),而且,本身创建时就没有主键,客户端上传主键是一个浪费。所以,除了数据库实体,我们还需要增加一个专门用于添加的实体。该实体跟数据库实体的唯一区别是没有主键。两者的转化可以通过AutoMapper处理(添加AutoMapper.Extensions.Microsoft.DependencyInjection包)。
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(options =>options.UseMySQL(Configuration.GetConnectionString("DatabaseConnection")));
3、删除上下文类中的配置信息,清空OnConfiguring函数:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
}
一个在Controller中读取表数据的范例为:
[ApiController]
[Route("[controller]")]
public class TestController : ControllerBase
{
private readonly ILogger _logger;
private readonly dataContext _dataContext;
public TestController(ILogger logger, dataContext db)
{
_logger = logger;
_dataContext = db;
}
[HttpGet]
public IEnumerable Get()
{
return _dataContext.Set().ToList();
}
}
根据使用的数据库,选择相应的NuGet包:
SQL Server | Microsoft.EntityFrameworkCore.SqlServer |
MySQL | MySql.Data.EntityFrameworkCore |
SQLite | Microsoft.EntityFrameworkCore.Sqlite |
实体类如下所示:
///
/// 学校
///
public class School
{
///
/// 学校ID
///
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; }
///
/// 学校名称
///
[Required]
[MinLength(1)]
[MaxLength(20)]
public string Name { get; set; }
}
///
/// 学生
///
public class Student
{
///
/// 学生ID
///
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; }
///
/// 姓名
///
[Required]
[MaxLength(10)]
public string Name { get; set; }
///
/// 性别
///
[Required]
[RegularExpression("男|女")]
public string Gender { get; set; }
///
/// 邮箱
///
[EmailAddress]
public string Email { get; set; }
///
/// 学校ID
///
[Required]
public Guid SchoolId { get; set; }
}
上下文类如下所示:
///
/// 学校数据上下文
///
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions options) : base(options)
{
}
///
/// 学校实体
///
public DbSet Schools { get; set; }
///
/// 学生实体
///
public DbSet Students { get; set; }
}
1、在Visual Studio菜单中选择:工具 > NuGet包管理器 > 程序包管理器控制台。
2、运行:Add-Migration InitialCreation
3、运行:Update-Database
4、可删除文件夹Migrations。
其他操作跟DB First相同。
仓储模式主要用于解除业务逻辑层与数据访问层之间的耦合,使业务逻辑层在存储、访问数据库时无须关心数据的来源及存储方式。
添加仓储基类接口及实现基类:
///
/// 仓储基类接口
///
public interface IRepositoryBase
{
///
/// 获取所有实体
///
Task> GetAllAsync();
///
/// 根据条件获取实体
///
Task> GetByConditionAsync(Expression> expression);
///
/// 根据ID获取实体
///
Task GetByIdAsync(TId id);
///
/// 满足某个条件的实体是否存在
///
Task IsExistAsync(Expression> expression);
///
/// 创建实体
///
void Create(T entity);
///
/// 更新实体
///
void Update(T entity);
///
/// 删除实体
///
void Delete(T entity);
///
/// 保存修改
///
Task SaveAsync();
}
///
/// 仓储基类
///
public class RepositoryBase : IRepositoryBase where T : class
{
///
/// 数据上下文
///
public DbContext DbContext { get; set; }
public RepositoryBase(DbContext dbContext)
{
DbContext = dbContext;
}
///
/// 获取所有实体
///
public Task> GetAllAsync()
{
return Task.FromResult(DbContext.Set().AsEnumerable());
}
///
/// 根据条件获取实体
///
public Task> GetByConditionAsync(Expression> expression)
{
return Task.FromResult(DbContext.Set().Where(expression).AsEnumerable());
}
///
/// 根据ID获取实体
///
public async Task GetByIdAsync(TId id)
{
return await DbContext.Set().FindAsync(id);
}
///
/// 满足某个条件的实体是否存在
///
public async Task IsExistAsync(Expression> expression)
{
return await DbContext.Set().AnyAsync(expression);
}
///
/// 创建实体
///
public void Create(T entity)
{
DbContext.Set().Add(entity);
}
///
/// 更新实体
///
public void Update(T entity)
{
DbContext.Set().Update(entity);
}
///
/// 删除实体
///
public void Delete(T entity)
{
DbContext.Set().Remove(entity);
}
///
/// 保存修改
///
public async Task SaveAsync()
{
var result = await DbContext.SaveChangesAsync() > 0;
if (!result)
{
throw new Exception("Commit fail.");
}
}
}
对于每个实体,都需要编写一个仓储类。一方面实例化仓储类中的泛型,另一方面可以添加实体额外需要的一些操作。
下面是一个实体示例:
public interface ISchoolRepository : IRepositoryBase
{
}
public class SchoolRepository : RepositoryBase, ISchoolRepository
{
public SchoolRepository(DbContext dbContext) : base(dbContext)
{
}
}
仓储封装类能够方便我们对仓储类的引用。如下所示:
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);
}
在Startup类的ConfigureServices函数中,增加以下语句:
services.AddScoped();
我们使用NLog输出日志,所以添加NLog.Web.AspNetCore包。
在项目中增加一个nlog.config的配置文件,其内容如下:
把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();
在Controller的构造函数中注入:
private readonly ILogger _logger;
public SchoolsController(ILogger logger)
{
_logger = logger;
}
然后就可以在代码中写日志了:
_logger.LogError(ex.Message);
我们使用Swagger制作API文档,需要引入Swashbuckle.AspNetCore包。
另外制作一个说明文档是很麻烦的,我们可以基于代码中的注释生成API文档。那首先,我们需要生成项目注释文档。
打开项目属性,在生成配置,勾选XML文档文件,如下所示:
在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");
});
以下是一个文档范例:
///
/// 添加学校
///
/// 学校信息
/// 添加成功
/// 学校已存在
/// 参数错误
/// 内部异常
[HttpPost]
[ProducesResponseType(typeof(School), 201)]
public async Task> AddSchool(SchoolToAdd school)
其显示效果将如下图所示:
完成上面的代码编写之后,一般的Controller增删改查操作都是基本相似的。很多时候,在此之上,修改类名即可。
[Route("api/[controller]")]
[ApiController]
public class SchoolsController : ControllerBase
{
private readonly ILogger _logger;
private readonly IRepositoryWrapper _repository;
private readonly IMapper _mapper;
public SchoolsController(ILogger logger, IRepositoryWrapper repository, IMapper mapper)
{
_logger = logger;
_repository = repository;
_mapper = mapper;
}
///
/// 获取学校列表
///
/// 获取成功
/// 参数错误
/// 内部异常
[HttpGet]
public async Task>> GetSchools()
{
try
{
var list = await _repository.School.GetAllAsync();
return Ok(list);
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
///
/// 获取指定学校信息
///
/// 学校ID
/// 获取成功
/// 未找到学校
/// 参数错误
/// 内部异常
[HttpGet("{id}")]
public async Task> 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);
}
}
///
/// 添加学校
///
/// 学校信息
/// 添加成功
/// 学校已存在
/// 参数错误
/// 内部异常
[HttpPost]
[ProducesResponseType(typeof(School), 201)]
public async Task> AddSchool(SchoolToAdd school)
{
try
{
if (await _repository.School.IsExistAsync(p => p.Name == school.Name))
{
return Conflict();
}
var _school = _mapper.Map(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);
}
}
///
/// 更新学校
///
/// 学校信息
/// 更新成功
/// 学校不存在
/// 参数错误
/// 内部异常
[HttpPut]
[ProducesResponseType(204)]
public async Task 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);
}
}
///
/// 删除学校
///
/// 学校ID
/// 删除成功
/// 学校不存在
/// 参数错误
/// 内部异常
[HttpDelete("{id}")]
public async Task> 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);
}
}
///
/// 获取指定学校的学生信息
///
/// 学校ID
/// 页码
/// 每页项数
/// 获取成功
/// 未找到学校
/// 参数错误
/// 内部异常
[HttpGet("{id}/Students")]
public async Task>> 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.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);
}
}
}