ASP.NET Core Web API

作者 Zhendong Ho 日期 2020-02-18
ASP.NET Core Web API

架构

  • RESTful Web API
  • Repository
  • Controller

创建项目

打开VS 2019,创建新项目ASP.NET Core Web 应用程序,项目名称为ThreeApi,选择.NET Core 3.1Empty模板。然后把HTTPS配置Docker选项去掉,点击创建按钮。

基本配置

打开Startup.cs文件,修改代码如下。Web API项目只需要用到Controller注入

public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

app.UseRouting();

app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}

Three项目中的Models文件夹以及Services文件夹中的IClock.csChinaClock.csUtcClock.cs复制到本项目中(注意要修改命名空间)。

IClock在Startup.cs中,进行依赖注入。

services.AddSingleton<IClock, UtcClock>();

添加Repository

IDepartmentRepository

项目右键,新建Repositories文件夹。在Repositories文件夹下,添加IDepartmentRepository.cs

public interface IDepartmentRepository
{
Task<Department> Add(Department model);
Task<IEnumerable<Department>> GetAll();
Task<Department> GetById(int id);
}

DepartmentRepository

在Repositories文件夹下,添加DepartmentRepository.cs

public class DepartmentRepository : IDepartmentRepository
{
private readonly List<Department> _departments = new List<Department>();

public DepartmentRepository()
{
_departments.Add(new Department
{
Id = 1,
Name = "HR",
EmployeeCount = 16,
Location = "Beijing"
});
_departments.Add(new Department
{
Id = 2,
Name = "G&D",
EmployeeCount = 52,
Location = "Shanghai"
});
_departments.Add(new Department
{
Id = 3,
Name = "Sales",
EmployeeCount = 200,
Location = "China"
});
}

public Task<IEnumerable<Department>> GetAll()
{
return Task.Run(function: () => _departments.AsEnumerable());
}

public Task<Department> GetById(int id)
{
return Task.Run(function: () => _departments.FirstOrDefault(x => x.Id == id));
}

public Task<Department> Add(Department department)
{
department.Id = _departments.Max(x => x.Id) + 1;
_departments.Add(department);
return Task.Run(function: () => department);
}
}

ISummaryRepository

在Repositories文件夹下,添加ISummaryRepository.cs。这是从DepartmentRepository中分离出来的方法。

public interface ISummaryRepository
{
Task<CompanySummary> GetCompanySummary();
}

SummaryRepository

在Repositories文件夹下,添加SummaryRepository.cs。由于SummaryRepository需要用到DepartmentRepository,因此在构造器中注入

public class SummaryRepository : ISummaryRepository
{
private readonly IDepartmentRepository _departmentRepository;

public SummaryRepository(IDepartmentRepository departmentRepository)
{
_departmentRepository = departmentRepository;
}

public Task<CompanySummary> GetCompanySummary()
{
return Task.Run(function: () =>
{
var all = _departmentRepository.GetAll().Result;
return new CompanySummary
{
EmployeeCount = all.Sum(x => x.EmployeeCount),
AverageDepartmentEmployeeCount = (int)all.Average(x => x.EmployeeCount)
};
});
}
}

IEmployeeRepository

在Repositories文件夹下,添加IEmployeeRepository.cs

public interface IEmployeeRepository
{
Task<Employee> Add(Employee employee);
Task<IEnumerable<Employee>> GetByDepartmentId(int departmentId);
Task<Employee> Fire(int id);
Task<Employee> GetById(int id);
}

EmployeeRepository

在Repositories文件夹下,添加EmployeeRepository.cs

public class EmployeeRepository : IEmployeeRepository
{
private readonly List<Employee> _employees = new List<Employee>();

public EmployeeRepository()
{
_employees.Add(new Employee
{
Id = 1,
DepartmentId = 1,
FirstName = "Nick",
LastName = "Carter",
Gender = Gender.男
});
_employees.Add(new Employee
{
Id = 2,
DepartmentId = 1,
FirstName = "Michael",
LastName = "Jackson",
Gender = Gender.男
});
_employees.Add(new Employee
{
Id = 3,
DepartmentId = 1,
FirstName = "Mariah",
LastName = "Carey",
Gender = Gender.女
});
_employees.Add(new Employee
{
Id = 4,
DepartmentId = 2,
FirstName = "Axl",
LastName = "Rose",
Gender = Gender.男
});
_employees.Add(new Employee
{
Id = 5,
DepartmentId = 2,
FirstName = "Kate",
LastName = "Winslet",
Gender = Gender.女
});
_employees.Add(new Employee
{
Id = 6,
DepartmentId = 3,
FirstName = "Rob",
LastName = "Thomas",
Gender = Gender.男
});
_employees.Add(new Employee
{
Id = 7,
DepartmentId = 3,
FirstName = "Avril",
LastName = "Lavigne",
Gender = Gender.女
});
_employees.Add(new Employee
{
Id = 8,
DepartmentId = 3,
FirstName = "Katy",
LastName = "Perry",
Gender = Gender.女
});
_employees.Add(new Employee
{
Id = 9,
DepartmentId = 3,
FirstName = "Michelle",
LastName = "Monaghan",
Gender = Gender.女
});
}

public Task<Employee> Add(Employee employee)
{
employee.Id = _employees.Max(x => x.Id) + 1;
_employees.Add(employee);
return Task.Run(function: () =>
{
return employee;
});
}

public Task<IEnumerable<Employee>> GetByDepartmentId(int departmentId)
{
return Task.Run(function: () => _employees.Where(x => x.DepartmentId == departmentId));
}

public Task<Employee> Fire(int id)
{
return Task.Run(function: () =>
{
var employee = _employees.FirstOrDefault(e => e.Id == id);
if (employee != null)
{
employee.Fired = true;
return employee;
}
return null;
});
}

public Task<Employee> GetById(int id)
{
return Task.Run(function: () =>
{
var employee = _employees.FirstOrDefault(e => e.Id == id);
if (employee != null)
{
return employee;
}
return null;
});
}
}

注入Repository

在Startup类中,添加repository的注入。

services.AddSingleton<IDepartmentRepository, DepartmentRepository>();
services.AddSingleton<IEmployeeRepository, EmployeeRepository>();
services.AddSingleton<ISummaryRepository, SummaryRepository>();

添加Controller

DepartmentsController

项目右键,新建Controllers文件夹。在Controllers文件夹下,添加DepartmentsController.cs,并通过AttuibuteRouting设置路由。

[Route(template: "v1/[controller]")]
[ApiController]
public class DepartmentsController : ControllerBase
{
private readonly IDepartmentRepository _departmentRepository;

public DepartmentsController(IDepartmentRepository departmentRepository)
{
_departmentRepository = departmentRepository;
}

[HttpGet] // v1/Departments verb: GET
public async Task<ActionResult<IEnumerable<Department>>> GetAll()
{
var departments = await _departmentRepository.GetAll();
if (!departments.Any())
{
return NoContent();
}

return Ok(departments);
//return new ObjectResult(departments);
}

[HttpPost] // v1/Departments verb: POST
public async Task<ActionResult<Department>> Add([FromBody]Department department)
{
var added = await _departmentRepository.Add(department);

return Ok(added);
}
}

代码解析

  • 路由属性中,v1代表版本号,controller就是Controller的名称。这里没有指定Action,因为不需要用到Action,是通过动词来区分请求到了哪个方法,如GETPOST动词。
  • API Controller前面需要添加ApiController特性,然后必须继承于ControllBase类。而MVC Controller则需要继承于Controller类
  • FromBody指定了从Body传过来的参数。

EmployeesController

在Controllers文件夹下,添加EmployeesController.cs,内容与DepartmentsController类似。

[Route(template: "v1/[controller]")]
[ApiController]
public class EmployeesController : ControllerBase
{
private readonly IEmployeeRepository _employeeRepository;

public EmployeesController(IEmployeeRepository employeeRepository)
{
_employeeRepository = employeeRepository;
}

[HttpGet(template: "{departmentId}")]
public async Task<IActionResult> GetByDepartmentId(int departmentId)
{
var employees = await _employeeRepository.GetByDepartmentId(departmentId);

if (!employees.Any())
{
return NoContent();
}
return Ok(employees);
}

[HttpGet(template: "One/{id}", Name = "GetById")]
public async Task<IActionResult> GetById(int id)
{
var result = await _employeeRepository.GetById(id);
if (result == null)
{
return NotFound();
}
return Ok(result);
}

[HttpPost]
public async Task<IActionResult> Add([FromBody] Employee model)
{
var added = await _employeeRepository.Add(model);
return CreatedAtRoute(routeName: "GetById", routeValues: new { id = added.Id }, value: added);
}

[HttpPut(template: "{id}")]
public async Task<IActionResult> Fire(int id)
{
var result = await _employeeRepository.Fire(id);
if (result != null)
{
return NoContent();
}
return NotFound();
}
}

代码解析

  • GetById方法GetByDepartmentId方法,虽然意义不一样,但是路由的路径一样的。因此为了区分,在GetById方法的路由前加上OneName参数指定了Action的名称,在CreatedAtRoute方法这家拍卖行睡觉哦精辟
  • CreatedAtRoute方法是标准写法,它可以指定对于已经创建好的资源,将来可以在哪个Action中获取routeName参数是可以获取的Action,routeValues是传递的参数,value是添加后的资源。CreateAtRoute返回的状态码是201,而Ok返回的状态码是200
  • HttpPut是对资源做整体的更新。

SummaryController

在Controllers文件夹下,添加SummaryController.cs。内容与上面两个Controller类似。

[Route(template: "v1/[controller]")]
[ApiController]
public class SummaryController : ControllerBase
{
private readonly ISummaryRepository _summaryRepository;

public SummaryController(ISummaryRepository summaryRepository)
{
_summaryRepository = summaryRepository;
}

public async Task<IActionResult> Get()
{
var result = await _summaryRepository.GetCompanySummary();
return Ok(result);
}
}

ApiController Attribute

  • Attribute路由
  • 对Model自动验证
  • 推断绑定源
    1. [FromBody],[FromForm],[FromRoute],[FromQuery]
    2. [FromHeader],[FromServices]

FromBody

[HttpPost]
public async Task<ActionResult<Department>> Add([FromBody]Department department) // [FromBody]可以省略
{
var added = _departmentRepository.Add(department);
return Ok(added);
}

FromRoute

[HttpGet(template: "{departmentId}")]
public async Task<IActionResult> GetByDepartmentId([FromRoute]int departmentId) // [FromRoute]可以省略
{
var employees = await _employeeRepository.GetByDepartmentId(departmentId);

if (!employees.Any())
{
return NoContent();
}
return Ok(employees);
}

其他绑定源

  • FromBody:参数将以一个整体的josn对象的形式传递。
  • FromForm:参数将以表单的形式提交。
  • FromQuery:queryString字符串,地址栏参数。
  • FromHeader:HTTP请求中的Header。
  • FromServices:从DI容器中获取依赖参数。

测试接口

DepartmentsController

GET请求

项目名称运行项目,打开Postman,使用GET请求如下地址:localhost:5000/v1/Departments,可以看到返回结果为json格式数据

image-20200221114002768

ASP.NET Core Web API默认情况下只返回json数据。如果想要支持xml格式,则需要

  1. 在Postman的请求部分,设置Accept的值为application/xml
  2. 在Startup类中增加依赖注入AddXmlSerializerFormatters
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers()
.AddXmlSerializerFormatters(); // 支持返回xml格式数据
services.AddSingleton<IClock, UtcClock>();
services.AddSingleton<IDepartmentRepository, DepartmentRepository>();
services.AddSingleton<IEmployeeRepository, EmployeeRepository>();
services.AddSingleton<ISummaryRepository, SummaryRepository>();
}

运行项目,使用Postman重新请求,可以得到xml格式数据

image-20200221114637130

POST请求

请求Body中输入如下json参数,使用POST请求如下地址:localhost:5000/v1/Departments

{
"Name": "Develop",
"Location": "Guangzhou",
"EmployeeCount": 300
}

可以看到返回的状态码为200,并且部门已经添加。

注意,使用POST请求时,要在Postman的Header中添加参数,Content-Type的值为application/json

EmployeesController

获取某个部门所有员工

GET方式请求如下地址:localhost:5000/v1/Employees/1,成功返回部门1的员工数据

image-20200221123055828

获取单个员工

GET方式请求如下地址:localhost:5000/v1/Employees/One/1,成功返回Id为1的员工数据

image-20200221123352758

添加员工

请求Body中输入如下json参数,使用POST请求如下地址:localhost:5000/v1/Employees

{
"departmentId": 1,
"firstName": "Nick",
"lastName": "Torres",
"gender": 1
}

可以看到返回的状态为200,并且员工已经添加。

解雇员工

PUT方式请求如下地址:localhost:5000/v1/Employees/1,解雇Id为1的员工。

返回结果的状态码为204 No Content,代表请求成功,这是正确的。

再使用GET方式获取Id为1的员工,可以看到员工1已被解雇。

image-20200221130247040

SummaryController

GET方式请求如下地址:localhost:5000/v1/Summary,成功返回结果。

image-20200221130526550