天天看点

ASP.NET Core API标准项目开发流程一、基本准则二、添加EF Core (DB First)三、添加EF Core (Code First)四、实现仓储模式五、添加日志六、添加文档七、Controller增删改查范例

一、基本准则

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文档文件,如下所示:

ASP.NET Core API标准项目开发流程一、基本准则二、添加EF Core (DB First)三、添加EF Core (Code First)四、实现仓储模式五、添加日志六、添加文档七、Controller增删改查范例

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)
           

其显示效果将如下图所示:

ASP.NET Core API标准项目开发流程一、基本准则二、添加EF Core (DB First)三、添加EF Core (Code First)四、实现仓储模式五、添加日志六、添加文档七、Controller增删改查范例
ASP.NET Core API标准项目开发流程一、基本准则二、添加EF Core (DB First)三、添加EF Core (Code First)四、实现仓储模式五、添加日志六、添加文档七、Controller增删改查范例

七、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);
        }
    }
}