├── .gitignore
├── README.md
├── Routine
├── Routine.APi
│ ├── ActionConstraints
│ │ └── RequestHeaderMatchesMediaTypeAttribute.cs
│ ├── Controllers
│ │ ├── CompaniesController.cs
│ │ ├── CompanyCollectionsController.cs
│ │ ├── EmployeesController.cs
│ │ └── RootController.cs
│ ├── Data
│ │ └── RoutineDbContext.cs
│ ├── DtoParameters
│ │ ├── CompanyDtoParameters.cs
│ │ └── EmployeeDtoParameters.cs
│ ├── Entities
│ │ ├── Company.cs
│ │ ├── Employee.cs
│ │ └── Gender.cs
│ ├── Helpers
│ │ ├── ArrayModelBinder.cs
│ │ ├── IEnumerableExtensions.cs
│ │ ├── IQueryableExtensions.cs
│ │ ├── ObjectExtensions.cs
│ │ ├── PagedList.cs
│ │ └── ResourceUriType.cs
│ ├── Migrations
│ │ ├── 20200206121508_AfterP38.Designer.cs
│ │ ├── 20200206121508_AfterP38.cs
│ │ ├── 20200222031609_AddBankruptTime.Designer.cs
│ │ ├── 20200222031609_AddBankruptTime.cs
│ │ └── RoutineDbContextModelSnapshot.cs
│ ├── Models
│ │ ├── CompanyAddDto.cs
│ │ ├── CompanyAddWithBankruptTimeDto.cs
│ │ ├── CompanyFriendlyDto.cs
│ │ ├── CompanyFullDto.cs
│ │ ├── EmployeeAddDto.cs
│ │ ├── EmployeeAddOrUpdateDto.cs
│ │ ├── EmployeeDto.cs
│ │ ├── EmployeeUpdateDto.cs
│ │ └── LinkDto.cs
│ ├── Profiles
│ │ ├── CompanyProfile.cs
│ │ └── EmployeeProfile.cs
│ ├── Program.cs
│ ├── Properties
│ │ └── launchSettings.json
│ ├── Routine.APi.csproj
│ ├── Services
│ │ ├── CompanyRepository.cs
│ │ ├── ICompanyRepository.cs
│ │ ├── IPropertyCheckerService.cs
│ │ ├── IPropertyMapping.cs
│ │ ├── IPropertyMappingService.cs
│ │ ├── PropertyCheckerService.cs
│ │ ├── PropertyMapping.cs
│ │ ├── PropertyMappingService.cs
│ │ └── PropertyMappingValue.cs
│ ├── Startup.cs
│ ├── ValidationAttributes
│ │ └── EmployeeNoMustDifferentFromFirstNameAttribute.cs
│ ├── appsettings.Development.json
│ └── appsettings.json
└── Routine.sln
└── cover.jpg
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Aa][Rr][Mm]/
27 | [Aa][Rr][Mm]64/
28 | bld/
29 | [Bb]in/
30 | [Oo]bj/
31 | [Ll]og/
32 | [Ll]ogs/
33 |
34 | # Visual Studio 2015/2017 cache/options directory
35 | .vs/
36 |
37 | # .NET Core
38 | project.lock.json
39 | project.fragment.lock.json
40 | artifacts/
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ASP.NET-Core-RESTful-Note
2 |
3 | [](https://www.bilibili.com/video/av77957694)
4 |
5 | 本仓库是[杨旭](https://www.cnblogs.com/cgzl/)(solenovex)《[使用 ASP.NET Core 3.x 构建 RESTful Web API](https://www.bilibili.com/video/av77957694)》课程的学习笔记。
6 |
7 | 包含课程的项目代码,并注释随堂笔记与资料。
8 |
9 | 与原项目略有不同,本项目使用 SQL Server 数据库。
10 |
11 | ## 版本
12 | `master` 分支是最新的,涵盖所有课程内容。
13 |
14 | 在寻找更早的版本?欢迎查看本仓库的 [Releases](https://github.com/Surbowl/ASP.NET-Core-RESTful-Note/releases),在课程的每个阶段都有 Release;
15 |
16 | 例如:[截至视频 P8 的代码](https://github.com/Surbowl/ASP.NET-Core-RESTful-Note/releases/tag/P8)、 [截至视频 P19 的代码](https://github.com/Surbowl/ASP.NET-Core-RESTful-Note/releases/tag/P19) 等。
17 |
18 | 杨老师已在 GitHub 发布课程原版代码,[点此访问](https://github.com/solenovex/ASP.NET-Core-3.x-REST-API-Tutorial-Code)。
19 |
20 | ## 小地图
21 | - [课程视频](https://www.bilibili.com/video/av77957694)
22 | - [课程博文](https://www.cnblogs.com/cgzl/p/11814971.html)
23 | - [课程 PPT](https://github.com/solenovex/ASP.NET-Core-3.x-REST-API-Tutorial-Code/tree/master/PPT)
24 | - [ASP.NET Core 3.x 入门课程](https://www.bilibili.com/video/av65313713)
25 | - [How to unapply a migration](https://stackoverflow.com/questions/38192450/how-to-unapply-a-migration-in-asp-net-core-with-ef-core)
26 | - [码云仓库(强制同步)](https://gitee.com/surbowl/ASP.NET-Core-RESTful-Note)
27 |
28 | ## PATH
29 | [Routine.APi](https://github.com/Surbowl/ASP.NET-Core-RESTful-Note/tree/master/Routine/Routine.APi)
30 | ```
31 | │ appsettings.Development.json
32 | │ appsettings.json
33 | │ Program.cs
34 | │ Routine.APi.csproj
35 | │ Startup.cs
36 | │
37 | ├─Controllers
38 | │ RootController.cs // api 根目录
39 | │ CompaniesController.cs // api/companies 公司(单个/集合)
40 | │ CompanyCollectionsController.cs // api/companycollections 指定公司集合
41 | │ EmployeesController.cs // api/companies/{companyId}/employees 员工(单个/集合)
42 | │
43 | ├─Data
44 | │ RoutineDbContext.cs
45 | │
46 | ├─DtoParameters // Uri query parameters
47 | │ CompanyDtoParameters.cs // -GET api/companies
48 | │ EmployeeDtoParameters.cs // -GET api/companies/{companyId}/employees
49 | │
50 | ├─Entities
51 | │ Company.cs
52 | │ Employee.cs
53 | │ Gender.cs
54 | │
55 | ├─Helpers
56 | │ ArrayModelBinder.cs // 自定义 ModelBinder,将 ids 字符串转为 IEnumerable
57 | │ IEnumerableExtensions.cs // IEnumerable 拓展,对集合资源进行数据塑形
58 | │ IQueryableExtensions.cs // IQueryable 拓展,对集合资源进行排序
59 | │ ObjectExtensions.cs // Object 拓展,对单个资源进行数据塑形
60 | │ PagedList.cs // 继承 List,对集合资源进行翻页处理
61 | │ ResourceUriType.cs // 枚举,指明 Uri 前往上一页、下一页或本页
62 | │
63 | ├─Migrations
64 | │ ...
65 | │
66 | ├─Models
67 | │ CompanyFriendlyDto.cs // 公司简略信息 Dto
68 | │ CompanyFullDto.cs // 公司完整信息 Dto
69 | │ CompanyAddDto.cs // 添加公司时使用的 Dto
70 | │ CompanyAddWithBankruptTimeDto.cs // 添加已破产的公司时使用的 Dto
71 | │ EmployeeDto.cs // 员工信息 Dto
72 | │ EmployeeAddOrUpdateDto.cs // 添加或更新员工信息时使用的 Dto 的父类
73 | │ EmployeeAddDto.cs // 添加员工时使用的 Dto,继承 EmployeeAddOrUpdateDto
74 | │ EmployeeUpdateDto.cs // 更新员工信息时使用的 Dto,继承 EmployeeAddOrUpdateDto
75 | │ LinkDto.cs // HATEOAS 的 links 使用的 Dto
76 | │
77 | ├─Profiles // AutoMapper 映射关系
78 | │ CompanyProfile.cs
79 | │ EmployeeProfile.cs
80 | │
81 | ├─Properties
82 | │ launchSettings.json
83 | │
84 | ├─Services
85 | │ ICompanyRepository.cs
86 | │ IPropertyCheckerService.cs
87 | │ IPropertyMapping.cs
88 | │ IPropertyMappingService.cs
89 | │ CompanyRepository.cs
90 | │ PropertyCheckerService.cs // 判断 Uri query parameters 中的 fields 是否合法
91 | │ PropertyMappingValue.cs // 属性映射关系,用于集合资源的排序
92 | │ PropertyMapping.cs // 属性映射关系字典,声明源类型与目标类型,用于集合资源的排序
93 | │ PropertyMappingService.cs // 属性映射关系服务,用于集合资源的排序
94 | │
95 | └─ValidationAttributes // 自定义 Model 验证 Attribute
96 | EmployeeNoMustDifferentFromFirstNameAttribute.cs
97 |
98 | ```
99 |
100 |
101 | 欢迎大家对内容进行补充,只要是合理内容都可以 [pull](https://github.com/Surbowl/ASP.NET-Core-RESTful-Note/pulls)。
102 |
103 | 非常感谢杨老师 🤗
104 |
--------------------------------------------------------------------------------
/Routine/Routine.APi/ActionConstraints/RequestHeaderMatchesMediaTypeAttribute.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc.ActionConstraints;
2 | using Microsoft.AspNetCore.Mvc.Formatters;
3 | using Microsoft.Net.Http.Headers;
4 | using System;
5 |
6 | namespace Routine.APi.ActionConstraints
7 | {
8 | ///
9 | /// 自定义 Attribute,根据 Header 信息判断方法是否能处理请求(视频P44)
10 | ///
11 | [AttributeUsage(AttributeTargets.All, Inherited = true, AllowMultiple = true)]
12 | public class RequestHeaderMatchesMediaTypeAttribute : Attribute, IActionConstraint
13 | {
14 | private readonly string _requestHeaderToMatch;
15 | //支持的媒体类型集合
16 | private readonly MediaTypeCollection _mediaTypes = new MediaTypeCollection();
17 |
18 | public RequestHeaderMatchesMediaTypeAttribute(string requestHeaderToMatch, //指明要匹配 Header 中的哪一项(key)
19 | string mediaType, //key 对应的 value
20 | params string[] otherMediaTypes) //params 关键字的作用 https://www.cnblogs.com/GreenLeaves/p/6756637.html
21 | {
22 | _requestHeaderToMatch = requestHeaderToMatch ?? throw new ArgumentNullException(nameof(requestHeaderToMatch));
23 |
24 | //解析 mediaType 的 MediaTypeHeaderValue
25 | if (MediaTypeHeaderValue.TryParse(mediaType,out MediaTypeHeaderValue parsedMediaType))
26 | {
27 | //解析成功,添加到集合中
28 | _mediaTypes.Add(parsedMediaType);
29 | }
30 | else
31 | {
32 | throw new ArgumentException(nameof(mediaType));
33 | }
34 |
35 | //如果还有 otherMediaTypes,依次进行解析并添加到集合中
36 | foreach (var otherMediaType in otherMediaTypes)
37 | {
38 | if (MediaTypeHeaderValue.TryParse(otherMediaType, out MediaTypeHeaderValue parsedOtherMediaType))
39 | {
40 | _mediaTypes.Add(parsedOtherMediaType);
41 | }
42 | else
43 | {
44 | throw new ArgumentException(nameof(otherMediaTypes));
45 | }
46 | }
47 | }
48 |
49 | ///
50 | /// 判断该方法是否能处理这个请求
51 | ///
52 | /// 上下文
53 | /// 是否能处理这个请求
54 | public bool Accept(ActionConstraintContext context)
55 | {
56 | //判断 Header 中是否含有 _requestHeaderToMatch(key)
57 | var requestHeaders = context.RouteContext.HttpContext.Request.Headers;
58 | if (! requestHeaders.ContainsKey(_requestHeaderToMatch))
59 | {
60 | return false;
61 | }
62 |
63 | //如果含有该 key,判断 value 是否包含在支持的媒体类型集合中
64 | var parsedRequestMediaType = new MediaType(requestHeaders[_requestHeaderToMatch]);
65 | foreach (var mediaType in _mediaTypes)
66 | {
67 | var parsedMediaType = new MediaType(mediaType);
68 | if (parsedRequestMediaType.Equals(parsedMediaType))
69 | {
70 | return true;
71 | }
72 | }
73 |
74 | return false;
75 | }
76 |
77 | public int Order => 0;
78 |
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/Routine/Routine.APi/Controllers/CompaniesController.cs:
--------------------------------------------------------------------------------
1 | using AutoMapper;
2 | using Microsoft.AspNetCore.Mvc;
3 | using Microsoft.Net.Http.Headers;
4 | using Routine.APi.ActionConstraints;
5 | using Routine.APi.DtoParameters;
6 | using Routine.APi.Entities;
7 | using Routine.APi.Helpers;
8 | using Routine.APi.Models;
9 | using Routine.APi.Services;
10 | using System;
11 | using System.Collections.Generic;
12 | using System.Linq;
13 | using System.Text.Encodings.Web;
14 | using System.Text.Json;
15 | using System.Threading.Tasks;
16 |
17 | /*
18 | * HTTP方法: | 安全 幂等
19 | * |
20 | * GET - 查询 | Y Y
21 | * |
22 | * POST - 创建/添加 | N N
23 | * 服务器端负责 URI 的生成 |
24 | * |
25 | * PATCH - 局部修改/更新 | N N
26 | * 请求的 MediaType 是 |
27 | * application/json-patch+json |
28 | * |
29 | * PUT - 如果存在就替换,不存在则创建 | N Y
30 | * 如果 URI 存在,就更新资源 |
31 | * 如果 URI 不存在,就创建资源或 |
32 | * 返回404(可选) |
33 | * |
34 | * DELETE - 移除/删除 | N Y
35 | * |
36 | * OPTIONS - 获取 Web API 的通信选项的信息 | Y Y
37 | * |
38 | * HEAD - 只请求页面的首部 | Y Y
39 | *
40 | * 安全性是指方法执行后并不会改变资源的表述
41 | * 幂等性是指方法无论执行多少次都会得到同样的结果
42 | */
43 |
44 | /*
45 | * 返回状态码:
46 | *
47 | * 2xx 成功
48 | * 200 - OK 请求成功
49 | * 201 - Created 请求成功并创建了资源
50 | * 204 - No Content 请求成功,但是不应该返回任何东西,例如删除操作
51 | *
52 | * 4xx 客户端错误
53 | * 400 - Bad Request API消费者发送到服务器的请求是有错误的
54 | * 401 - Unauthorized 没有提供授权信息或者提供的授权信息不正确
55 | * 403 - Forbidden 身份认证已经成功,但是已认证的用户却无法访问请求的资源
56 | * 404 - Not Found 请求的资源不存在
57 | * 405 - Method Not Allowed 尝试发送请求到资源的时候,使用了不被支持的HTTP方法
58 | * 406 - Not Acceptable API消费者请求的表述格式并不被Web API所支持,并且API不
59 | * 会提供默认的表述格式
60 | * 409 - Conflict 请求与服务器当前状态冲突(通常指更新资源时发生的冲突)
61 | * 415 - Unsupported Media Type 与406正好相反,有一些请求必须带着数据发往服务器,
62 | * 这些数据都属于特定的媒体格式,如果API不支持该媒体类型格式,415就会被返回
63 | * 422 - Unprocessable Entity 它是HTTP拓展协议的一部分,它说明服务器已经懂得了
64 | * Content Type,实体的语法也没有问题,但是服务器仍然无法处理这个实体数据
65 | *
66 | * 5xx 服务器错误
67 | * 500 - Internal Server Error 服务器出现错误
68 | */
69 |
70 | /*
71 | * 绑定数据源:
72 | * [FromBody]
73 | * [FromForm]
74 | * [FromHeader](略)
75 | * [FromQuery]
76 | * [FromRoute]
77 | * [FromService](略)
78 | */
79 |
80 | namespace Routine.APi.Controllers
81 | {
82 | /*[ApiController] 属性并不是强制要求的,但是它会使开发体验更好
83 | * 它会启用以下行为:
84 | * 1.要求使用属性路由(Attribute Routing)
85 | * 2.自动HTTP 400响应
86 | * 3.推断参数的绑定源
87 | * 4.Multipart/form-data请求推断
88 | * 5.错误状态代码的问题详细信息
89 | */
90 | [ApiController]
91 | [Route("api/companies")] //还可用 [Route("api/[controller]")]
92 | public class CompaniesController : ControllerBase
93 | {
94 | private readonly ICompanyRepository _companyRepository;
95 | private readonly IMapper _mapper;
96 | private readonly IPropertyMappingService _propertyMappingService;
97 | private readonly IPropertyCheckerService _propertyCheckerService;
98 |
99 | public CompaniesController(ICompanyRepository companyRepository,
100 | IMapper mapper,
101 | IPropertyMappingService propertyMappingService,
102 | IPropertyCheckerService propertyCheckerService)
103 | {
104 | _companyRepository = companyRepository ?? throw new ArgumentNullException(nameof(companyRepository));
105 | _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
106 | _propertyMappingService = propertyMappingService ?? throw new ArgumentNullException(nameof(propertyMappingService));
107 | _propertyCheckerService = propertyCheckerService ?? throw new ArgumentNullException(nameof(propertyCheckerService));
108 | }
109 |
110 | #region Controllers
111 | #region HttpGet
112 |
113 | [HttpGet(Name = nameof(GetCompanies))]
114 | [HttpHead] //添加对 Http Head 请求的支持,Http Head 请求只获取 Header 信息,没有 Body(视频P16)
115 | public async Task GetCompanies([FromQuery]CompanyDtoParameters parameters,
116 | [FromHeader(Name="Accept")]
117 | string acceptMediaType)
118 | {
119 | //尝试解析 MediaTypeHeaderValue(视频P43)
120 | //关于 MediaTypeHeaderValue https://docs.microsoft.com/zh-cn/dotnet/api/system.net.http.headers.mediatypeheadervalue
121 | if (!MediaTypeHeaderValue.TryParse(acceptMediaType, out MediaTypeHeaderValue parsedAcceptMediaType))
122 | {
123 | return BadRequest(); //返回状态码400
124 | }
125 |
126 | //判断 Uri Query 中的 orderby 字符串是否合法(视频P38)
127 | //无论请求的是 Full Dto 还是 Friendly Dto,都允许按照 Full Dto 中的属性进行排序
128 | if (!_propertyMappingService.ValidMappingExistsFor(parameters.OrderBy))
129 | {
130 | return BadRequest();
131 | }
132 |
133 | //判断 Uri Query 中的 fields 字符串是否合法(视频P39)
134 | if (!_propertyCheckerService.TypeHasProperties(parameters.Fields))
135 | {
136 | return BadRequest();
137 | }
138 |
139 | //GetCompaniesAsync(parameters) 返回的是经过翻页处理的 PagedList(视频P35)
140 | var companies = await _companyRepository.GetCompaniesAsync(parameters);
141 |
142 | var paginationMetdata = new //向 Headers 中添加的翻页信息
143 | {
144 | totalCount = companies.TotalCount,
145 | pageSize = companies.PageSize,
146 | currentPage = companies.CurrentPage,
147 | totalPages = companies.TotalPages
148 | };
149 | Response.Headers.Add("X-Pagination", JsonSerializer.Serialize(paginationMetdata,
150 | new JsonSerializerOptions
151 | { //为了防止 URI 中的‘&’、‘?’符号被转义,使用“不安全”的 Encoder
152 | Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
153 | }));
154 | //是否需要 Full Dto(视频P43)
155 | bool isFull = parsedAcceptMediaType.SubTypeWithoutSuffix
156 | .ToString()
157 | .Contains("full", StringComparison.InvariantCultureIgnoreCase);
158 | //是否需要 links(HATEOAS)(视频P41-43)
159 | bool includeLinks = parsedAcceptMediaType.SubTypeWithoutSuffix
160 | .EndsWith("hateoas", StringComparison.InvariantCultureIgnoreCase); //大小写不敏感
161 |
162 | var shapedData = isFull ?
163 | _mapper.Map>(companies).ShapeData(parameters.Fields)
164 | :
165 | _mapper.Map>(companies).ShapeData(parameters.Fields);
166 |
167 | //使用 HATEOAS,返回 value 与 links (视频P43)
168 | if (includeLinks)
169 | {
170 | //首先创建 Companies 集合中每个 Company 自己的 Links
171 | var shapedCompaniesWithLinks = shapedData.Select(c =>
172 | {
173 | var companyDict = c as IDictionary;
174 | var links = CreateLinksForCompany((Guid)companyDict["Id"], null);
175 | companyDict.Add("links", links);
176 | return companyDict;
177 | });
178 | //然后创建整个 Companies 集合的 Links
179 | var linkedCollectionResource = new
180 | {
181 | value = shapedCompaniesWithLinks,
182 | links = CreateLinksForCompany(parameters, companies.HasPrevious, companies.HasNext)
183 | };
184 | return Ok(linkedCollectionResource); //返回状态码200
185 | }
186 |
187 | //不使用 HATEOAS,直接返回 shapedData
188 | return Ok(shapedData);
189 | }
190 |
191 | [HttpGet("{companyId}", Name = nameof(GetCompany))] //可省略 [Route("{companyId}")]
192 |
193 | //已在 Startup.cs 中对输出格式化器进行全局设置,因此不再使用 Produces 属性进行局部设置
194 | //以下代码启用后将忽视 Startup.cs 中对输出格式化器的全局设置,导致 GetCompany 不支持 xml
195 | //[Produces("application/json",//当收到以下 Accept 值时,实际返回 application/json(视频P43)
196 | // "application/vnd.company.hateoas+json",
197 | // "application/vnd.company.friendly+json",
198 | // "application/vnd.company.friendly.hateoas+json",
199 | // "application/vnd.company.full+json",
200 | // "application/vnd.company.full.hateoas+json")]
201 | public async Task GetCompany(Guid companyId,
202 | string fields,
203 | [FromHeader(Name="Accept")]
204 | string acceptMediaType)
205 | {
206 | //判断 Uri Query 中的 fields 字符串是否合法(视频P39)
207 | if (!_propertyCheckerService.TypeHasProperties(fields))
208 | {
209 | return BadRequest(); //返回状态码400
210 | }
211 |
212 | //尝试解析 MediaTypeHeaderValue(视频P43)
213 | if (!MediaTypeHeaderValue.TryParse(acceptMediaType, out MediaTypeHeaderValue parsedAcceptMediaType))
214 | {
215 | return BadRequest();
216 | }
217 |
218 | var company = await _companyRepository.GetCompanyAsync(companyId);
219 | if (company == null)
220 | {
221 | return NotFound(); //返回状态码404
222 | }
223 |
224 | //是否需要 links(HATEOAS)(视频P41-43)
225 | bool includeLinks = parsedAcceptMediaType.SubTypeWithoutSuffix
226 | .EndsWith("hateoas", StringComparison.InvariantCultureIgnoreCase); //大小写不敏感
227 | //是否需要 Full Dto
228 | bool isFull = parsedAcceptMediaType.SubTypeWithoutSuffix
229 | .ToString()
230 | .Contains("full", StringComparison.InvariantCultureIgnoreCase);
231 |
232 | var shapedData = isFull ?
233 | _mapper.Map(company).ShapeData(fields)
234 | :
235 | _mapper.Map(company).ShapeData(fields);
236 |
237 | if (includeLinks)
238 | {
239 | var companyDict = shapedData as IDictionary;
240 | var links = CreateLinksForCompany(companyId, fields);
241 | companyDict.Add("links", links);
242 | return Ok(companyDict);
243 | }
244 |
245 | return Ok(shapedData);
246 | }
247 |
248 | #endregion HttpGet
249 |
250 | #region HttpPost
251 |
252 | [HttpPost(Name = nameof(CreateCompany))]
253 | [RequestHeaderMatchesMediaType("Content-Type", //当 Content-Type 是以下 value 时,使用该方法(相当于路由)(视频P44)
254 | "application/json",
255 | "application/vnd.company.companyforcreation+json")]
256 | //指明该方法可以消费哪些格式的 Content-Type(视频P44)
257 | [Consumes("application/json", "application/vnd.company.companyforcreation+json")]
258 | public async Task CreateCompany([FromBody]CompanyAddDto company,
259 | [FromHeader(Name="Accept")]
260 | string acceptMediaType)
261 | {
262 | //使用 [ApiController] 属性后,会自动返回400错误,无需再使用以下代码:
263 | //if (!ModelState.IsValid) { return UnprocessableEntity(ModelState); }
264 |
265 | //Core 3.x 使用 [ApiController] 属性后,无需再使用以下代码:
266 | //if (company == null) { return BadRequest(); }
267 |
268 | //尝试解析 MediaTypeHeaderValue(视频P43)
269 | if (!MediaTypeHeaderValue.TryParse(acceptMediaType, out MediaTypeHeaderValue parsedAcceptMediaType))
270 | {
271 | return BadRequest();
272 | }
273 |
274 | //是否需要 links(HATEOAS)(视频P41-43)
275 | bool includeLinks = parsedAcceptMediaType.SubTypeWithoutSuffix
276 | .EndsWith("hateoas", StringComparison.InvariantCultureIgnoreCase); //大小写不敏感
277 | //是否需要 Full Dto
278 | bool isFull = parsedAcceptMediaType.SubTypeWithoutSuffix
279 | .ToString()
280 | .Contains("full", StringComparison.InvariantCultureIgnoreCase);
281 |
282 | var entity = _mapper.Map(company);
283 | _companyRepository.AddCompany(entity);
284 | await _companyRepository.SaveAsync();
285 |
286 | var shapedData = isFull ?
287 | _mapper.Map(entity).ShapeData(null)
288 | :
289 | _mapper.Map(entity).ShapeData(null);
290 |
291 | if (includeLinks)
292 | {
293 | var companyDict = shapedData as IDictionary;
294 | var links = CreateLinksForCompany(entity.Id, null);
295 | companyDict.Add("links", links);
296 | //返回状态码201
297 | //通过使用 CreatedAtRoute 返回时可以在 Header 中添加一个地址(Loaction)
298 | return CreatedAtRoute(nameof(GetCompany), new { companyId = entity.Id }, companyDict);
299 | }
300 |
301 | return CreatedAtRoute(nameof(GetCompany), new { companyId = entity.Id }, shapedData);
302 | }
303 |
304 | //含 KankruptTime 的 Create Company,使用 CompanyAddWithBankruptTimeDto(视频P44)
305 | [HttpPost(Name = nameof(CreateCompanyWithBankruptTime))]
306 | [RequestHeaderMatchesMediaType("Content-Type", //当 Content-Type 是以下 value 时,使用该方法(相当于路由)(视频P44)
307 | "application/vnd.company.companyforcreationwithbankrupttime+json")]
308 | //指明该方法可以消费哪些格式的 Content-Type(视频P44)
309 | [Consumes("application/vnd.company.companyforcreationwithbankrupttime+json")]
310 | public async Task CreateCompanyWithBankruptTime([FromBody]CompanyAddWithBankruptTimeDto companyWithBankruptTime,
311 | [FromHeader(Name="Accept")]
312 | string acceptMediaType)
313 | {
314 | //尝试解析 MediaTypeHeaderValue(视频P43)
315 | if (!MediaTypeHeaderValue.TryParse(acceptMediaType, out MediaTypeHeaderValue parsedAcceptMediaType))
316 | {
317 | return BadRequest();
318 | }
319 |
320 | //是否需要 links(HATEOAS)(视频P41-43)
321 | bool includeLinks = parsedAcceptMediaType.SubTypeWithoutSuffix
322 | .EndsWith("hateoas", StringComparison.InvariantCultureIgnoreCase); //大小写不敏感
323 | //是否需要 Full Dto
324 | bool isFull = parsedAcceptMediaType.SubTypeWithoutSuffix
325 | .ToString()
326 | .Contains("full", StringComparison.InvariantCultureIgnoreCase);
327 |
328 | var entity = _mapper.Map(companyWithBankruptTime);
329 | _companyRepository.AddCompany(entity);
330 | await _companyRepository.SaveAsync();
331 |
332 | var shapedData = isFull ?
333 | _mapper.Map(entity).ShapeData(null)
334 | :
335 | _mapper.Map(entity).ShapeData(null);
336 |
337 | if (includeLinks)
338 | {
339 | var companyDict = shapedData as IDictionary;
340 | var links = CreateLinksForCompany(entity.Id, null);
341 | companyDict.Add("links", links);
342 | return CreatedAtRoute(nameof(GetCompany), new { companyId = entity.Id }, companyDict);
343 | }
344 |
345 | return CreatedAtRoute(nameof(GetCompany), new { companyId = entity.Id }, shapedData);
346 | }
347 |
348 | #endregion HttpPost
349 |
350 | #region HttpDelete
351 |
352 | [HttpDelete("{companyId}", Name = nameof(DeleteCompany))]
353 | public async Task DeleteCompany(Guid companyId)
354 | {
355 | var companyEntity = await _companyRepository.GetCompanyAsync(companyId);
356 | if (companyEntity == null)
357 | {
358 | return NotFound();
359 | }
360 | //删除 Company 时将属于它的 Employees 一并删除
361 | //该代码把属于 Company 的 Employees 加载到内存中,使删除时可以追踪(视频P33)
362 | await _companyRepository.GetEmployeesAsync(companyId, new EmployeeDtoParameters());
363 |
364 | _companyRepository.DeleteCompany(companyEntity);
365 | await _companyRepository.SaveAsync();
366 | return NoContent();
367 | }
368 |
369 | #endregion HttpDelete
370 |
371 | #region HttpOptions
372 |
373 | [HttpOptions]
374 | public IActionResult GetCompaniesOptions()
375 | {
376 | Response.Headers.Add("Allow", "DELETE,GET,PATCH,PUT,OPTIONS");
377 | return Ok();
378 | }
379 |
380 | #endregion HttpOptions
381 | #endregion Controllers
382 |
383 | #region Functions
384 |
385 | ///
386 | /// 生成上一页、下一页或当前页的 URI(视频P35)
387 | ///
388 | /// CompanyDtoParameters
389 | /// ResourceUriType
390 | /// 跳转到目标页的 Uri
391 | private string CreateCompaniesResourceUri(CompanyDtoParameters parameters,
392 | ResourceUriType type)
393 | {
394 | switch (type)
395 | {
396 | case ResourceUriType.PreviousPage: //上一页
397 | return Url.Link(
398 | nameof(GetCompanies), //方法名
399 | new //Uri Query 字符串参数
400 | {
401 | pageNumber = parameters.PageNumber - 1,
402 | pageSize = parameters.PageSize,
403 | companyName = parameters.companyName,
404 | searchTerm = parameters.SearchTerm,
405 | orderBy = parameters.OrderBy, //排序(视频P38)
406 | fields = parameters.Fields //数据塑形(视频P39)
407 | }); ;
408 |
409 | case ResourceUriType.NextPage: //下一页
410 | return Url.Link(
411 | nameof(GetCompanies),
412 | new
413 | {
414 | pageNumber = parameters.PageNumber + 1,
415 | pageSize = parameters.PageSize,
416 | companyName = parameters.companyName,
417 | searchTerm = parameters.SearchTerm,
418 | orderBy = parameters.OrderBy,
419 | fields = parameters.Fields
420 | });
421 |
422 | //case ResourceUriType.CurrentPage: //当前页
423 | default:
424 | return Url.Link(
425 | nameof(GetCompanies),
426 | new
427 | {
428 | pageNumber = parameters.PageNumber,
429 | pageSize = parameters.PageSize,
430 | companyName = parameters.companyName,
431 | searchTerm = parameters.SearchTerm,
432 | orderBy = parameters.OrderBy,
433 | fields = parameters.Fields
434 | });
435 | }
436 | }
437 |
438 | ///
439 | /// 为 Company 单个资源创建 HATEOAS 的 links(视频P41)
440 | ///
441 | /// Company Id
442 | /// fields 字符串
443 | /// Company 单个资源的 links
444 | private IEnumerable CreateLinksForCompany(Guid companyId, string fields)
445 | {
446 | var links = new List();
447 |
448 | //GetCompany 的 link
449 | if (string.IsNullOrWhiteSpace(fields))
450 | {
451 | links.Add(new LinkDto(Url.Link(nameof(GetCompany), new { companyId }), //href - 超链接
452 | "self", //rel - 与当前资源的关系或描述
453 | "GET")); //method - 方法
454 | }
455 | else
456 | {
457 | links.Add(new LinkDto(Url.Link(nameof(GetCompany), new { companyId, fields }),
458 | "self",
459 | "GET"));
460 | }
461 |
462 | //DeleteCompany 的 link
463 | links.Add(new LinkDto(Url.Link(nameof(DeleteCompany), new { companyId, fields }),
464 | "delete_company",
465 | "DELETE"));
466 |
467 | //CreateEmployeeForCompany 的 link
468 | links.Add(new LinkDto(Url.Link(nameof(EmployeesController.CreateEmployeeForCompany), new { companyId }),
469 | "create_employee_for_company",
470 | "POST"));
471 |
472 | //GetEmployeesForCompany 的 link
473 | links.Add(new LinkDto(Url.Link(nameof(EmployeesController.GetEmployeesForCompany), new { companyId }),
474 | "employees",
475 | "GET"));
476 |
477 | return links;
478 | }
479 |
480 | ///
481 | /// 为 Companies 集合资源创建 HATEOAS 的 links(视频P42)
482 | ///
483 | /// CompanyDtoParameters
484 | /// 是否有上一页
485 | /// 是否有下一页
486 | /// GetCompanies 集合资源的 links
487 | private IEnumerable CreateLinksForCompany(CompanyDtoParameters parameters, bool hasPrevious, bool hasNext)
488 | {
489 | var links = new List();
490 |
491 | //CurrentPage 当前页链接
492 | links.Add(new LinkDto(CreateCompaniesResourceUri(parameters, ResourceUriType.CurrentPage),
493 | "self",
494 | "GET"));
495 |
496 | if (hasPrevious)
497 | {
498 | //PreviousPage 上一页链接
499 | links.Add(new LinkDto(CreateCompaniesResourceUri(parameters, ResourceUriType.PreviousPage),
500 | "previous_page",
501 | "GET"));
502 | }
503 |
504 | if (hasNext)
505 | {
506 | //NextPage 下一页链接
507 | links.Add(new LinkDto(CreateCompaniesResourceUri(parameters, ResourceUriType.NextPage),
508 | "next_page",
509 | "GET"));
510 | }
511 |
512 | return links;
513 | }
514 |
515 | #endregion Functions
516 | }
517 | }
518 |
--------------------------------------------------------------------------------
/Routine/Routine.APi/Controllers/CompanyCollectionsController.cs:
--------------------------------------------------------------------------------
1 | using AutoMapper;
2 | using Microsoft.AspNetCore.Mvc;
3 | using Routine.APi.Entities;
4 | using Routine.APi.Helpers;
5 | using Routine.APi.Models;
6 | using Routine.APi.Services;
7 | using System;
8 | using System.Collections.Generic;
9 | using System.Linq;
10 | using System.Threading.Tasks;
11 |
12 | namespace Routine.APi.Controllers
13 | {
14 | [ApiController]
15 | [Route("api/companycollections")]
16 | public class CompanyCollectionsController : ControllerBase
17 | {
18 | private readonly IMapper _mapper;
19 | private readonly ICompanyRepository _companyRepository;
20 |
21 | public CompanyCollectionsController(IMapper mapper, ICompanyRepository companyRepository)
22 | {
23 | _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
24 | _companyRepository = companyRepository ??
25 | throw new ArgumentNullException(nameof(companyRepository));
26 | }
27 |
28 | #region Controllers
29 | #region HttpGet
30 |
31 | //自定义 Model 绑定器,获取集合资源(视频P24)
32 | [HttpGet("({ids})", Name = nameof(GetCompanyCollection))]
33 | public async Task GetCompanyCollection([FromRoute]
34 | [ModelBinder(BinderType = typeof(ArrayModelBinder))]
35 | IEnumerable ids)
36 | {
37 | if (ids == null)
38 | {
39 | return BadRequest();
40 | }
41 | var entities = await _companyRepository.GetCompaniesAsync(ids);
42 |
43 | if (ids.Count() != entities.Count())
44 | {
45 | return NotFound();
46 | }
47 |
48 | //使用 Company Full Dto
49 | var dtosToReturn = _mapper.Map>(entities);
50 | return Ok(dtosToReturn);
51 | }
52 |
53 | #endregion HttpGet
54 |
55 | #region HttpPost
56 |
57 | //同时创建父子关系的资源(视频P23)
58 | [HttpPost]
59 | public async Task CreateCompanyCollection(IEnumerable companyCollection) //Task = Task>>
60 | {
61 | //.Net Core 会自动对所有 Company 与 Employee 资源进行模型验证
62 |
63 | var companyEntities = _mapper.Map>(companyCollection);
64 | foreach (var company in companyEntities)
65 | {
66 | _companyRepository.AddCompany(company);
67 | }
68 | await _companyRepository.SaveAsync();
69 | var dtosToReturn = _mapper.Map>(companyEntities);
70 | var idsString = string.Join(",", dtosToReturn.Select(x => x.Id));
71 | return CreatedAtRoute(nameof(GetCompanyCollection), new { ids = idsString }, dtosToReturn);
72 | }
73 |
74 | #endregion HttpPost
75 | #endregion Controllers
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/Routine/Routine.APi/Controllers/EmployeesController.cs:
--------------------------------------------------------------------------------
1 | using AutoMapper;
2 | using Marvin.Cache.Headers;
3 | using Microsoft.AspNetCore.JsonPatch;
4 | using Microsoft.AspNetCore.Mvc;
5 | using Microsoft.AspNetCore.Mvc.ModelBinding;
6 | using Microsoft.Extensions.DependencyInjection;
7 | using Microsoft.Extensions.Options;
8 | using Routine.APi.DtoParameters;
9 | using Routine.APi.Entities;
10 | using Routine.APi.Models;
11 | using Routine.APi.Services;
12 | using System;
13 | using System.Collections.Generic;
14 | using System.Threading.Tasks;
15 |
16 | namespace Routine.APi.Controllers
17 | {
18 | [ApiController]
19 | [Route("api/companies/{companyId}/employees")]
20 | //[ResponseCache(CacheProfileName = "120sCacheProfile")] //允许被缓存120秒(视频P46)
21 | public class EmployeesController : ControllerBase
22 | {
23 | private readonly ICompanyRepository _companyRepository;
24 | private readonly IMapper _mapper;
25 | private readonly IPropertyMappingService _propertyMappingService;
26 |
27 | public EmployeesController(ICompanyRepository companyRepository, IMapper mapper, IPropertyMappingService propertyMappingService)
28 | {
29 | _companyRepository = companyRepository ?? throw new ArgumentNullException(nameof(companyRepository));
30 | _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
31 | _propertyMappingService = propertyMappingService ?? throw new ArgumentNullException(nameof(propertyMappingService));
32 | }
33 |
34 | #region Controllers
35 | #region HttpGet
36 |
37 | [HttpGet(Name = nameof(GetEmployeesForCompany))]
38 | //单独指定这个方法的缓存策略
39 | //[ResponseCache(Duration = 60)] //(视频P46)
40 | [HttpCacheExpiration(CacheLocation = CacheLocation.Public, MaxAge = 1800)] //(视频P48)
41 | [HttpCacheValidation(MustRevalidate = false)] //(视频P48)
42 | public async Task GetEmployeesForCompany(Guid companyId,
43 | [FromQuery]EmployeeDtoParameters parameters)
44 | {
45 | //判断 Uri Query 中的 orderBy 字符串是否合法(视频P38)
46 | if (! _propertyMappingService.ValidMappingExistsFor(parameters.OrderBy))
47 | {
48 | return BadRequest(); //返回状态码400
49 | }
50 |
51 | if (await _companyRepository.CompanyExistsAsync(companyId))
52 | {
53 | var employees = await _companyRepository.GetEmployeesAsync(companyId, parameters);
54 | var employeeDtos = _mapper.Map>(employees);
55 | return Ok(employeeDtos);
56 | }
57 | else
58 | {
59 | return NotFound();
60 | }
61 | }
62 |
63 | [HttpGet("{employeeId}", Name = nameof(GetEmployeeForCompany))]
64 | public async Task GetEmployeeForCompany(Guid companyId, Guid employeeId)
65 | {
66 | if (await _companyRepository.CompanyExistsAsync(companyId))
67 | {
68 | var employee = await _companyRepository.GetEmployeeAsync(companyId, employeeId);
69 | if (employee == null)
70 | {
71 | return NotFound();
72 | }
73 | var employeeDto = _mapper.Map(employee);
74 | return Ok(employeeDto);
75 | }
76 | else
77 | {
78 | return NotFound();
79 | }
80 | }
81 |
82 | #endregion HttpGet
83 |
84 | #region HttpPost
85 |
86 | [HttpPost(Name = nameof(CreateEmployeeForCompany))]
87 | public async Task CreateEmployeeForCompany([FromRoute]Guid companyId,
88 | [FromBody]EmployeeAddDto employee)
89 | //此处的 [FromRoute] 与 [FromBody] 其实不指定也可以,会自动匹配
90 | {
91 | if (!await _companyRepository.CompanyExistsAsync(companyId))
92 | {
93 | return NotFound();
94 | }
95 | var entity = _mapper.Map(employee);
96 | _companyRepository.AddEmployee(companyId, entity);
97 | await _companyRepository.SaveAsync();
98 | var returnDto = _mapper.Map(entity);
99 | return CreatedAtAction(nameof(GetEmployeeForCompany),
100 | new { companyId = returnDto.CompanyId, employeeId = returnDto.Id },
101 | returnDto);
102 | }
103 |
104 | #endregion HttpPost
105 |
106 | #region HttpPut
107 |
108 | //整体更新/替换,PUT不是安全的,但是幂等
109 | [HttpPut("{employeeId}")]
110 | public async Task UpdateEmployeeForCompany(Guid companyId,
111 | Guid employeeId,
112 | EmployeeUpdateDto employeeUpdateDto)
113 | {
114 | if (!await _companyRepository.CompanyExistsAsync(companyId))
115 | {
116 | return NotFound();
117 | }
118 |
119 | var employeeEntity = await _companyRepository.GetEmployeeAsync(companyId, employeeId);
120 | if (employeeEntity == null)
121 | {
122 | //不允许客户端生成 Guid
123 | //return NotFound();
124 |
125 | //允许客户端生成 Guid
126 | var employeeToAddEntity = _mapper.Map(employeeUpdateDto);
127 | employeeToAddEntity.Id = employeeId;
128 | _companyRepository.AddEmployee(companyId, employeeToAddEntity);
129 | await _companyRepository.SaveAsync();
130 | var returnDto = _mapper.Map(employeeToAddEntity);
131 | return CreatedAtAction(nameof(GetEmployeeForCompany),
132 | new { companyId = companyId, employeeId = employeeId },
133 | returnDto);
134 | }
135 |
136 | //把 updateDto 映射到 entity
137 | _mapper.Map(employeeUpdateDto, employeeEntity);
138 | _companyRepository.UpdateEmployee(employeeEntity);
139 | await _companyRepository.SaveAsync();
140 | return NoContent(); //返回状态码204
141 | }
142 |
143 | #endregion HttpPut
144 |
145 | #region HttpPatch
146 |
147 | /*
148 | * HTTP PATCH 举例(视频P32)
149 | * 原资源:
150 | * {
151 | * "baz":"qux",
152 | * "foo":"bar"
153 | * }
154 | *
155 | * 请求的 Body:
156 | * [
157 | * {"op":"replace","path":"/baz","value":"boo"},
158 | * {"op":"add","path":"/hello","value":["world"]},
159 | * {"op":"remove","path":"/foo"}
160 | * ]
161 | *
162 | * 修改后的资源:
163 | * {
164 | * "baz":"boo",
165 | * "hello":["world"]
166 | * }
167 | *
168 | * JSON PATCH Operations:
169 | * Add:
170 | * {"op":"add","path":"/biscuits/1","value":{"name","Ginger Nut"}}
171 | * Replace:
172 | * {"op":"replace","path":"/biscuits/0/name","value":"Chocolate Digestive"}
173 | * Remove:
174 | * {"op":"remove","path":"/biscuits"}
175 | * {"op":"remove","path":"/biscuits/0"}
176 | * Copy:
177 | * {"op":"copy","from":"/biscuits/0","path":"/best_biscuit"}
178 | * Move:
179 | * {"op":"move","from":"/biscuits","path":"/cookies"}
180 | * Test:
181 | * {"op":"test","path":"/best_biscuit","value":"Choco Leibniz}
182 | */
183 | [HttpPatch("{employeeId}")]
184 | public async Task PartiallyUpdateEmployeeForCompany(Guid companyId,
185 | Guid employeeId,
186 | JsonPatchDocument patchDocument)
187 | {
188 | if (!await _companyRepository.CompanyExistsAsync(companyId))
189 | {
190 | return NotFound();
191 | }
192 |
193 | var employeeEntity = await _companyRepository.GetEmployeeAsync(companyId, employeeId);
194 | if (employeeEntity == null)
195 | {
196 | //不允许客户端生成 Guid
197 | //return NotFound();
198 |
199 | //允许客户端生成 Guid
200 | var employeeDto = new EmployeeUpdateDto();
201 | //传入 ModelState 进行验证
202 | patchDocument.ApplyTo(employeeDto, ModelState);
203 | if (!TryValidateModel(employeeDto))
204 | {
205 | return ValidationProblem(ModelState);
206 | }
207 |
208 | var employeeToAdd = _mapper.Map(employeeDto);
209 | employeeToAdd.Id = employeeId;
210 | _companyRepository.AddEmployee(companyId, employeeToAdd);
211 | await _companyRepository.SaveAsync();
212 | var dtoToReturn = _mapper.Map(employeeToAdd);
213 |
214 | return CreatedAtAction(nameof(GetEmployeeForCompany),
215 | new { companyId = companyId, employeeId = employeeId },
216 | dtoToReturn);
217 | }
218 |
219 | var dtoToPatch = _mapper.Map(employeeEntity);
220 | //将 Patch 应用到 dtoToPatch(EmployeeUpdateDto)
221 | patchDocument.ApplyTo(dtoToPatch);
222 | //验证模型
223 | if (!TryValidateModel(dtoToPatch))
224 | {
225 | return ValidationProblem(ModelState); //返回状态码与错误信息
226 | }
227 | _mapper.Map(dtoToPatch, employeeEntity);
228 | _companyRepository.UpdateEmployee(employeeEntity);
229 | await _companyRepository.SaveAsync();
230 | return NoContent(); //返回状态码204
231 | }
232 |
233 | #endregion HttpPatch
234 |
235 | #region HttpDelete
236 |
237 | [HttpDelete("{employeeId}")]
238 | public async Task DeleteEmployeeForCompany(Guid companyId, Guid employeeId)
239 | {
240 | if (!await _companyRepository.CompanyExistsAsync(companyId))
241 | {
242 | return NotFound();
243 | }
244 |
245 | var employeeEntity = await _companyRepository.GetEmployeeAsync(companyId, employeeId);
246 | if (employeeEntity == null)
247 | {
248 | return NotFound();
249 | }
250 |
251 | _companyRepository.DeleteEmployee(employeeEntity);
252 | await _companyRepository.SaveAsync();
253 | return NoContent();
254 | }
255 |
256 | #endregion HttpDelete
257 |
258 | #region HttpOptions
259 |
260 | [HttpOptions]
261 | public IActionResult GetCompaniesOptions()
262 | {
263 | Response.Headers.Add("Allowss", "DELETE,GET,PATCH,PUT,OPTIONS");
264 | return Ok();
265 | }
266 |
267 | #endregion HttpOptions
268 | #endregion Controllers
269 |
270 | #region Functions
271 |
272 | ///
273 | /// 重写 ValidationProblem(视频P32)
274 | /// 使 PartiallyUpdateEmployeeForCompany 中的 ValidationProblem() 返回状态码422而不是400
275 | ///
276 | ///
277 | ///
278 | public override ActionResult ValidationProblem(ModelStateDictionary modelStateDictionary)
279 | {
280 | var options = HttpContext.RequestServices
281 | .GetRequiredService>();
282 | return (ActionResult)options.Value.InvalidModelStateResponseFactory(ControllerContext);
283 | }
284 |
285 | #endregion Functions
286 | }
287 | }
288 |
--------------------------------------------------------------------------------
/Routine/Routine.APi/Controllers/RootController.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 | using Routine.APi.Models;
3 | using System.Collections.Generic;
4 |
5 | namespace Routine.APi.Controllers
6 | {
7 | //根目录(视频P42)
8 | [Route("api")]
9 | [ApiController]
10 | public class RootController : ControllerBase
11 | {
12 | #region Controllers
13 | #region HttpGet
14 |
15 | [HttpGet(Name =nameof(GetRoot))]
16 | public IActionResult GetRoot()
17 | {
18 | var links = new List();
19 | links.Add(new LinkDto(Url.Link(nameof(GetRoot), new { }),
20 | "self",
21 | "GET"));
22 | links.Add(new LinkDto(Url.Link(nameof(CompaniesController.GetCompanies), new { }),
23 | "companies",
24 | "GET"));
25 | links.Add(new LinkDto(Url.Link(nameof(CompaniesController.CreateCompany), new { }),
26 | "create_company",
27 | "POST"));
28 | var resource = new
29 | {
30 | links = links
31 | };
32 | return Ok(resource);
33 | }
34 |
35 | #endregion HttpGet
36 | #endregion Controllers
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Routine/Routine.APi/Data/RoutineDbContext.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using Routine.APi.Entities;
3 | using System;
4 |
5 | namespace Routine.APi.Data
6 | {
7 | public class RoutineDbContext : DbContext
8 | {
9 | //调用并获取父类的options
10 | public RoutineDbContext(DbContextOptions options) : base(options)
11 | {
12 |
13 | }
14 |
15 | public DbSet Companies { get; set; }
16 | public DbSet Employees { get; set; }
17 |
18 | protected override void OnModelCreating(ModelBuilder modelBuilder)
19 | {
20 | modelBuilder.Entity().Property(x => x.Name).IsRequired().HasMaxLength(100);
21 | modelBuilder.Entity().Property(x => x.Introduction).HasMaxLength(500);
22 | modelBuilder.Entity().Property(x => x.EmployeeNo).IsRequired().HasMaxLength(10);
23 | modelBuilder.Entity().Property(x => x.FirstName).IsRequired().HasMaxLength(50);
24 | modelBuilder.Entity().Property(x => x.LastName).IsRequired().HasMaxLength(50);
25 | modelBuilder.Entity()
26 | //指明一对多关系(可省略)
27 | .HasOne(x => x.Company)
28 | .WithMany(x => x.Employees)
29 | //外键
30 | .HasForeignKey(x => x.CompanyId)
31 | //禁止级联删除:删除 Company 时如果有 Employee,则无法删除
32 | //.OnDelete(DeleteBehavior.Restrict);
33 | //允许级联删除:删除 Company 时自动删除拥有的 Employee
34 | .OnDelete(DeleteBehavior.Cascade);
35 | //种子数据
36 | modelBuilder.Entity().HasData(
37 | new Company //1
38 | {
39 | Id = Guid.Parse("bbdee09c-089b-4d30-bece-44df5923716c"),
40 | Name = "Microsoft",
41 | Country = "USA",
42 | Industry= "Internet",
43 | Product ="Software",
44 | Introduction = "Great Company"
45 | },
46 | new Company //2
47 | {
48 | Id = Guid.Parse("6fb600c1-9011-4fd7-9234-881379716440"),
49 | Name = "Google",
50 | Country = "USA",
51 | Industry = "Internet",
52 | Product = "Software",
53 | Introduction = "Don't be evil"
54 | },
55 | new Company //3
56 | {
57 | Id = Guid.Parse("5efc910b-2f45-43df-afee-620d40542853"),
58 | Name = "Alipapa",
59 | Country = "CN",
60 | Industry = "Internet",
61 | Product = "Software",
62 | Introduction = "Fubao Company"
63 | },
64 | new Company //4
65 | {
66 | Id = Guid.Parse("8cc04f96-2c42-4f76-832e-1903835b0190"),
67 | Name = "Huawei",
68 | Country = "CN",
69 | Industry = "Communication",
70 | Product = "Hardware",
71 | Introduction = "Building a Smart World of Everything"
72 | },
73 | new Company //5
74 | {
75 | Id = Guid.Parse("d1f1f410-f563-4355-aa91-4774d693363f"),
76 | Name = "Xiaomi",
77 | Country = "CN",
78 | Industry = "Communication",
79 | Product = "Hardware",
80 | Introduction = "Born for a fever"
81 | },
82 | new Company //6
83 | {
84 | Id = Guid.Parse("19b8d0f9-4fdf-41ab-b172-f2d5d725b6d9"),
85 | Name = "Wuliangye",
86 | Country = "CN",
87 | Industry = "Wine",
88 | Product = "Wine",
89 | Introduction = "Great Wine"
90 | },
91 | new Company //7
92 | {
93 | Id = Guid.Parse("6c28b511-34f6-43b2-89f6-fa3dab77bcf9"),
94 | Name = "UNIQLO",
95 | Country = "JP",
96 | Industry = "Textile",
97 | Product = "Costume",
98 | Introduction = "Good clothes"
99 | },
100 | new Company //8
101 | {
102 | Id = Guid.Parse("4ab2b4af-45ce-41b3-8aed-5447c3140330"),
103 | Name = "ZARA",
104 | Country = "ESP",
105 | Industry = "Textile",
106 | Product = "Costume",
107 | Introduction = "Stylish clothes"
108 | },
109 | new Company //9
110 | {
111 | Id = Guid.Parse("cd11c117-551c-409f-80e9-c15d89fd7ca8"),
112 | Name = "Mercedes-Benz",
113 | Country = "GER",
114 | Industry = "Auto",
115 | Product = "Car",
116 | Introduction = "The best car"
117 | },
118 | new Company //10
119 | {
120 | Id = Guid.Parse("a39f7877-3849-48a1-b6af-e35b90c73e6a"),
121 | Name = "BMW",
122 | Country = "GER",
123 | Industry = "Auto",
124 | Product = "Car",
125 | Introduction = "Good car"
126 | },
127 | new Company //11
128 | {
129 | Id = Guid.Parse("eb8fc677-2600-4fdb-a8ef-51c006e7fc20"),
130 | Name = "Yahoo!",
131 | Country = "USA",
132 | Industry = "Internet",
133 | Product = "Software",
134 | Introduction = "An American web services provider headquartered in Sunnyvale"
135 | }
136 | );
137 | modelBuilder.Entity().HasData(
138 | //Microsoft employees
139 | new Employee
140 | {
141 | Id = Guid.Parse("ca268a19-0f39-4d8b-b8d6-5bace54f8027"),
142 | CompanyId = Guid.Parse("bbdee09c-089b-4d30-bece-44df5923716c"),
143 | DateOfBirth = new DateTime(1955, 10, 28),
144 | EmployeeNo = "M001",
145 | FirstName = "William",
146 | LastName = "Gates",
147 | Gender = Gender.男
148 | },
149 | new Employee
150 | {
151 | Id = Guid.Parse("265348d2-1276-4ada-ae33-4c1b8348edce"),
152 | CompanyId = Guid.Parse("bbdee09c-089b-4d30-bece-44df5923716c"),
153 | DateOfBirth = new DateTime(1998, 1, 14),
154 | EmployeeNo = "M002",
155 | FirstName = "Kent",
156 | LastName = "Back",
157 | Gender = Gender.男
158 | },
159 | //Google employees
160 | new Employee
161 | {
162 | Id = Guid.Parse("47b70abc-98b8-4fdc-b9fa-5dd6716f6e6b"),
163 | CompanyId = Guid.Parse("6fb600c1-9011-4fd7-9234-881379716440"),
164 | DateOfBirth = new DateTime(1986, 11, 4),
165 | EmployeeNo = "G001",
166 | FirstName = "Mary",
167 | LastName = "King",
168 | Gender = Gender.女
169 | },
170 | new Employee
171 | {
172 | Id = Guid.Parse("059e2fcb-e5a4-4188-9b46-06184bcb111b"),
173 | CompanyId = Guid.Parse("6fb600c1-9011-4fd7-9234-881379716440"),
174 | DateOfBirth = new DateTime(1977, 4, 6),
175 | EmployeeNo = "G002",
176 | FirstName = "Kevin",
177 | LastName = "Richardson",
178 | Gender = Gender.男
179 | },
180 | new Employee
181 | {
182 | Id = Guid.Parse("910e7452-c05f-4bf1-b084-6367873664a1"),
183 | CompanyId = Guid.Parse("6fb600c1-9011-4fd7-9234-881379716440"),
184 | DateOfBirth = new DateTime(1982, 3, 1),
185 | EmployeeNo = "G003",
186 | FirstName = "Frederic",
187 | LastName = "Pullan",
188 | Gender = Gender.男
189 | },
190 | //Alipapa employees
191 | new Employee
192 | {
193 | Id = Guid.Parse("a868ff18-3398-4598-b420-4878974a517a"),
194 | CompanyId = Guid.Parse("5efc910b-2f45-43df-afee-620d40542853"),
195 | DateOfBirth = new DateTime(1964, 9, 10),
196 | EmployeeNo = "A001",
197 | FirstName = "Jack",
198 | LastName = "Ma",
199 | Gender = Gender.男
200 | },
201 | new Employee
202 | {
203 | Id = Guid.Parse("2c3bb40c-5907-4eb7-bb2c-7d62edb430c9"),
204 | CompanyId = Guid.Parse("5efc910b-2f45-43df-afee-620d40542853"),
205 | DateOfBirth = new DateTime(1997, 2, 6),
206 | EmployeeNo = "A002",
207 | FirstName = "Lorraine",
208 | LastName = "Shaw",
209 | Gender = Gender.女
210 | },
211 | new Employee
212 | {
213 | Id = Guid.Parse("e32c33a7-df20-4b9a-a540-414192362d52"),
214 | CompanyId = Guid.Parse("5efc910b-2f45-43df-afee-620d40542853"),
215 | DateOfBirth = new DateTime(2000, 1, 24),
216 | EmployeeNo = "A003",
217 | FirstName = "Abel",
218 | LastName = "Obadiah",
219 | Gender = Gender.女
220 | },
221 | //Huawei employees
222 | new Employee
223 | {
224 | Id = Guid.Parse("3fae0ed7-5391-460a-8320-6b0255b62b72"),
225 | CompanyId = Guid.Parse("8cc04f96-2c42-4f76-832e-1903835b0190"),
226 | DateOfBirth = new DateTime(1972, 1, 12),
227 | EmployeeNo = "H001",
228 | FirstName = "Alexia",
229 | LastName = "More",
230 | Gender = Gender.女
231 | },
232 | new Employee
233 | {
234 | Id = Guid.Parse("1b863e75-8bd8-4876-8292-e99998bfa4b1"),
235 | CompanyId = Guid.Parse("8cc04f96-2c42-4f76-832e-1903835b0190"),
236 | DateOfBirth = new DateTime(1999, 12, 6),
237 | EmployeeNo = "H002",
238 | FirstName = "Barton",
239 | LastName = "Robin",
240 | Gender = Gender.女
241 | },
242 | new Employee
243 | {
244 | Id = Guid.Parse("c8353598-5b34-4529-a02b-dc7e9f93e59b"),
245 | CompanyId = Guid.Parse("8cc04f96-2c42-4f76-832e-1903835b0190"),
246 | DateOfBirth = new DateTime(1990, 6, 26),
247 | EmployeeNo = "H003",
248 | FirstName = "Ted",
249 | LastName = "Howard",
250 | Gender = Gender.男
251 | },
252 | new Employee
253 | {
254 | Id = Guid.Parse("ca86eded-a704-4fbc-8d5e-979761a2e0b8"),
255 | CompanyId = Guid.Parse("8cc04f96-2c42-4f76-832e-1903835b0190"),
256 | DateOfBirth = new DateTime(2000, 2, 2),
257 | EmployeeNo = "M003",
258 | FirstName = "Victor",
259 | LastName = "Burns",
260 | Gender = Gender.男
261 | }
262 | );
263 | }
264 | }
265 | }
266 |
--------------------------------------------------------------------------------
/Routine/Routine.APi/DtoParameters/CompanyDtoParameters.cs:
--------------------------------------------------------------------------------
1 | namespace Routine.APi.DtoParameters
2 | {
3 | public class CompanyDtoParameters
4 | {
5 | private const int MaxPageSize = 20; //翻页(视频P34-35)
6 | public string companyName { get; set; }
7 | public string SearchTerm { get; set; }
8 |
9 | public int PageNumber { get; set; } = 1; //默认值为1
10 | public string OrderBy { get; set; } = "Name"; //默认用公司名字排序 //排序(视频P36-38)
11 | public string Fields { get; set; } //数据塑形(视频P39)
12 | private int _pageSize = 5;
13 |
14 | public int PageSize
15 | {
16 | get => _pageSize;
17 | set => _pageSize = (value > MaxPageSize ? MaxPageSize : value);
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Routine/Routine.APi/DtoParameters/EmployeeDtoParameters.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 |
6 | namespace Routine.APi.DtoParameters
7 | {
8 | //(视频P36)
9 | public class EmployeeDtoParameters
10 | {
11 | private const int MaxPageSize = 20;
12 | public string GenderDisplay { get; set; }
13 | public string Q { get; set; }
14 | public int PageNumber { get; set; }
15 | private int _pageSize = 5;
16 | public int PageSize
17 | {
18 | get => _pageSize;
19 | set => _pageSize = (value > MaxPageSize) ? MaxPageSize : value;
20 | }
21 |
22 | //排序依据(默认用 Name,应该使用 Dto 中的命名方式)
23 | public string OrderBy { get; set; } = "Name";
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Routine/Routine.APi/Entities/Company.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace Routine.APi.Entities
5 | {
6 | public class Company
7 | {
8 | public Guid Id { get; set; }
9 | public string Name { get; set; }
10 | public string Country { get; set; }
11 | public string Industry { get; set; }
12 | public string Product { get; set; }
13 | public string Introduction { get; set; }
14 | public DateTime? BankruptTime { get; set; }
15 | public ICollection Employees { get; set; }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Routine/Routine.APi/Entities/Employee.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Routine.APi.Entities
4 | {
5 | public class Employee
6 | {
7 | public Guid Id { get; set; }
8 | public Guid CompanyId { get; set; }
9 | public string EmployeeNo { get; set; }
10 | public string FirstName { get; set; }
11 | public string LastName { get; set; }
12 | public Gender Gender { get; set; }
13 |
14 | public DateTime DateOfBirth { get; set; }
15 |
16 | public Company Company { get; set; }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Routine/Routine.APi/Entities/Gender.cs:
--------------------------------------------------------------------------------
1 | namespace Routine.APi.Entities
2 | {
3 | public enum Gender
4 | {
5 | 男 = 1,
6 | 女 = 0
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/Routine/Routine.APi/Helpers/ArrayModelBinder.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc.ModelBinding;
2 | using System;
3 | using System.ComponentModel;
4 | using System.Linq;
5 | using System.Reflection;
6 | using System.Threading.Tasks;
7 |
8 | namespace Routine.APi.Helpers
9 | {
10 | ///
11 | /// 自定义 Model 绑定器,将 Uri Query 中的 Company ids 字符串处理为 IEnumerable(视频P24)
12 | ///
13 | public class ArrayModelBinder : IModelBinder
14 | {
15 | public Task BindModelAsync(ModelBindingContext bindingContext)
16 | {
17 | if (!bindingContext.ModelMetadata.IsEnumerableType)
18 | {
19 | bindingContext.Result = ModelBindingResult.Failed();
20 | return Task.CompletedTask;
21 | }
22 |
23 | var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName).ToString();
24 |
25 | if (string.IsNullOrWhiteSpace(value))
26 | {
27 | bindingContext.Result = ModelBindingResult.Success(null);
28 | return Task.CompletedTask;
29 | }
30 |
31 | var elementType = bindingContext.ModelType.GetTypeInfo().GenericTypeArguments[0];
32 | //创建一个转换器
33 | var converter = TypeDescriptor.GetConverter(elementType);
34 | var values = value.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries)
35 | .Select(x => converter.ConvertFromString(x.Trim())).ToArray();
36 | var typedValues = Array.CreateInstance(elementType, values.Length);
37 | values.CopyTo(typedValues, 0);
38 | bindingContext.Model = typedValues;
39 | bindingContext.Result = ModelBindingResult.Success(bindingContext.Model);
40 | return Task.CompletedTask;
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Routine/Routine.APi/Helpers/IEnumerableExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Dynamic;
4 | using System.Linq;
5 | using System.Reflection;
6 |
7 | namespace Routine.APi.Helpers
8 | {
9 | public static class IEnumerableExtensions
10 | {
11 | //IEnumerable 的拓展方法
12 | //用于查询集合时的数据塑形(视频P39)
13 | //分开两个拓展方法是出于性能的考虑
14 | //数据塑形可以考虑使用微软的 OData 规范及其相关框架 https://www.odata.org/
15 |
16 | ///
17 | /// 对集合资源进行数据塑形(视频P39)
18 | ///
19 | /// 资源类型
20 | /// 资源集合
21 | /// Uri Query 中的 fields 字符串,大小写不敏感,用于指明需要的属性/字段;如果需要所有属性传入 null 即可
22 | /// 塑形后的集合资源
23 | public static IEnumerable ShapeData(this IEnumerable source,
24 | string fields)
25 | {
26 | if (source == null)
27 | {
28 | throw new ArgumentNullException(nameof(source));
29 | }
30 |
31 | var expandoObjectList = new List(source.Count());
32 | var propertyInfoList = new List();
33 |
34 | if (string.IsNullOrWhiteSpace(fields))
35 | {
36 | var propertyInfos = typeof(TSource)
37 | .GetProperties(BindingFlags.Public
38 | | BindingFlags.Instance);
39 | propertyInfoList.AddRange(propertyInfos);
40 | }
41 | else
42 | {
43 | var fieldsAfterSplit = fields.Split(",");
44 | foreach (var field in fieldsAfterSplit)
45 | {
46 | var propertyName = field.Trim();
47 | var propertyInfo = typeof(TSource)
48 | .GetProperty(propertyName,
49 | BindingFlags.IgnoreCase //IgnoreCase 忽略大小写
50 | | BindingFlags.Public
51 | | BindingFlags.Instance);
52 | if (propertyInfo == null)
53 | {
54 | throw new Exception($"Property:{propertyName} 没有找到:{typeof(TSource)}");
55 | }
56 |
57 | propertyInfoList.Add(propertyInfo);
58 | }
59 | }
60 |
61 | foreach (TSource obj in source)
62 | {
63 | var shapedObj = new ExpandoObject();
64 |
65 | foreach (var propertyInfo in propertyInfoList)
66 | {
67 | var propertyValue = propertyInfo.GetValue(obj);
68 | ((IDictionary)shapedObj).Add(propertyInfo.Name, propertyValue);
69 | }
70 |
71 | expandoObjectList.Add(shapedObj);
72 | }
73 |
74 | return expandoObjectList;
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/Routine/Routine.APi/Helpers/IQueryableExtensions.cs:
--------------------------------------------------------------------------------
1 | using Routine.APi.Services;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Linq.Dynamic.Core;
6 |
7 | namespace Routine.APi.Helpers
8 | {
9 |
10 | public static class IQueryableExtensions //这并不是一个接口,这种命名方式有点问题
11 | {
12 | //这是一个 IQueryable 的拓展方法,接收排序字符串与属性映射字典,返回排序后的 IQueryable
13 | //关于拓展方法,可以参考杨老师的另一个视频课程 https://www.bilibili.com/video/av62661924?p=5
14 |
15 | ///
16 | /// 对集合资源进行排序(视频P37)
17 | ///
18 | /// 资源类型
19 | /// IQueryable 资源集合
20 | /// Uri Query 中的 orderBy 字符串,用于声明资源集合的排序规则;如果无需排序传入 null 即可
21 | /// 资源类型的映射关系字典
22 | /// 排好序的 IQueryable 资源集合
23 | public static IQueryable ApplySort(this IQueryable source,
24 | string orderBy,
25 | Dictionary mappingDictionary)
26 | {
27 | if (source == null)
28 | {
29 | throw new ArgumentNullException(nameof(source));
30 | }
31 | if (mappingDictionary == null)
32 | {
33 | throw new ArgumentNullException(nameof(mappingDictionary));
34 | }
35 | if (string.IsNullOrWhiteSpace(orderBy))
36 | {
37 | return source;
38 | }
39 |
40 | //空字符串用来储存 OrderBy T-SQL 命令
41 | string ordering = "";
42 |
43 | //将 orderBy 字符串按 ',' 划分为数组
44 | var orderByAfterSplit = orderBy.Split(",");
45 | //依次处理数组中的每个排序依据
46 | foreach (var orderByClause in orderByAfterSplit)
47 | {
48 | var trimmedOrderByClause = orderByClause.Trim();
49 | //是否 DESC
50 | var orderDescending = trimmedOrderByClause.EndsWith(" desc");
51 | //第一个空格的 index
52 | var indexOfFirstSpace = trimmedOrderByClause.IndexOf(" ");
53 | //属性名
54 | //如果存在空格,移除空格后面的内容(用来移除" desc")
55 | var propertyName = indexOfFirstSpace == -1 ? trimmedOrderByClause : trimmedOrderByClause.Remove(indexOfFirstSpace);
56 |
57 | //在属性映射字典中查找
58 | //属性映射字典的 Key 大小写不敏感,不用担心大小写问题
59 | if (!mappingDictionary.ContainsKey(propertyName))
60 | {
61 | throw new ArgumentNullException($"没有找到Key为{propertyName}的映射");
62 | }
63 |
64 | var propertyMappingValue = mappingDictionary[propertyName];
65 | if (propertyMappingValue == null)
66 | {
67 | throw new ArgumentNullException(nameof(propertyMappingValue));
68 | }
69 |
70 | foreach(var destinationProperty in propertyMappingValue.DestinationProperties)
71 | {
72 | if (propertyMappingValue.Revert)
73 | {
74 | orderDescending = !orderDescending;
75 | }
76 |
77 | //构造 order by T-SQL 命令
78 | //与视频中的方法略有不同,这种方法不用 Revert() 两次,性能更好
79 | if (ordering.Length > 0)
80 | {
81 | ordering += ",";
82 | }
83 | ordering += destinationProperty + (orderDescending ? " descending" : " ascending");
84 | }
85 | }
86 |
87 | //执行 order by T-SQL 命令
88 | //需要安装 System.Linq.Dynamic.Core 包,才能使用以下代码
89 | source = source.OrderBy(ordering);
90 | return source;
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/Routine/Routine.APi/Helpers/ObjectExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Dynamic;
4 | using System.Reflection;
5 |
6 | namespace Routine.APi.Helpers
7 | {
8 | //Object 的拓展方法
9 | public static class ObjectExtensions
10 | {
11 | ///
12 | /// 对单个资源进行数据塑形(视频P39)
13 | ///
14 | /// 资源类型
15 | /// 需要塑形的资源
16 | /// Uri Query 中的 fields 字符串,用于指明需要的属性/字段;如果需要所有属性传入 null 即可
17 | /// 塑形后的资源
18 | public static ExpandoObject ShapeData(this TSource source, string fields)
19 | {
20 | if (source == null)
21 | {
22 | throw new ArgumentNullException(nameof(source));
23 | }
24 |
25 | //理解 ExpandoObject 的使用 https://blog.csdn.net/u010178308/article/details/79773704
26 | var expandoObj = new ExpandoObject();
27 |
28 | //如果没有 fields 字符串指定属性,返回所有属性
29 | if (string.IsNullOrWhiteSpace(fields))
30 | {
31 | //获得 TSource 的属性
32 | var propertyInfos = typeof(TSource).GetProperties(BindingFlags.IgnoreCase //忽略属性名大小写
33 | | BindingFlags.Public //搜索公共成员
34 | | BindingFlags.Instance);
35 | foreach (var propertyInfo in propertyInfos)
36 | {
37 | var propertyValue = propertyInfo.GetValue(source);
38 | ((IDictionary)expandoObj).Add(propertyInfo.Name, propertyValue);
39 | }
40 | }
41 | //如果有 fields 字符串指定属性,返回指定的属性
42 | else
43 | {
44 | var fieldsAfterSpilt = fields.Split(",");
45 | foreach (var field in fieldsAfterSpilt)
46 | {
47 | var propertyName = field.Trim();
48 | //获得 fields 字符串指定的 TSource 中的各属性
49 | var propertyInfo = typeof(TSource).GetProperty(propertyName,
50 | BindingFlags.IgnoreCase //忽略属性名大小写
51 | | BindingFlags.Public //搜索公共成员
52 | | BindingFlags.Instance);
53 | if (propertyInfo == null)
54 | {
55 | throw new Exception($"在{typeof(TSource)}上没有找到{propertyName}这个属性");
56 | }
57 |
58 | var propertyValue = propertyInfo.GetValue(source);
59 | ((IDictionary)expandoObj).Add(propertyInfo.Name, propertyValue);
60 | }
61 | }
62 |
63 | return expandoObj;
64 | }
65 | }
66 | }
--------------------------------------------------------------------------------
/Routine/Routine.APi/Helpers/PagedList.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Threading.Tasks;
6 |
7 | namespace Routine.APi.Helpers
8 | {
9 | ///
10 | /// 翻页类,进行分页操作并保存分页信息(视频P35)
11 | ///
12 | ///
13 | public class PagedList : List
14 | {
15 | ///
16 | /// 当前页码
17 | ///
18 | public int CurrentPage { get; private set; }
19 |
20 | ///
21 | /// 总页数
22 | ///
23 | public int TotalPages { get; private set; }
24 |
25 | ///
26 | /// 单页条目数
27 | ///
28 | public int PageSize { get; private set; }
29 |
30 | ///
31 | /// 总条目数
32 | ///
33 | public int TotalCount { get; private set; }
34 |
35 | ///
36 | /// 是否有上一页
37 | ///
38 | public bool HasPrevious => CurrentPage > 1;
39 |
40 | ///
41 | /// 是否有下一页
42 | ///
43 | public bool HasNext => CurrentPage < TotalPages;
44 |
45 | public PagedList(List items,int count,int pageNumber,int pageSize)
46 | {
47 | TotalCount = count;
48 | PageSize = pageSize;
49 | CurrentPage = pageNumber;
50 | TotalPages = (int)Math.Ceiling(count / (double)pageSize);
51 | AddRange(items);
52 | }
53 |
54 | ///
55 | /// 对资源集合进行翻页处理(视频P35)
56 | ///
57 | /// 待翻页的资源集合
58 | /// 指定页码(当前页)
59 | /// 每页的资源数量
60 | /// 翻页后,指定页码的资源集合
61 | public static async Task> CreateAsync(IQueryable source,int pageNumber,int pageSize)
62 | {
63 | //下面这种写法是错误的,会抛出异常。不能在前一个操作完成之前,在该上下文中启动第二个操作。
64 | //var countTask = source.CountAsync();
65 | //var itemsTask = source.Skip((pageNumber - 1) * pageSize).Take(pageSize).ToListAsync();
66 | //var count = await countTask;
67 | //var items = await itemsTask;
68 | //
69 | //正确写法
70 | var count = await source.CountAsync();
71 | var items = await source.Skip((pageNumber - 1) * pageSize).Take(pageSize).ToListAsync();
72 |
73 | return new PagedList(items, count, pageNumber, pageSize);
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/Routine/Routine.APi/Helpers/ResourceUriType.cs:
--------------------------------------------------------------------------------
1 | namespace Routine.APi.Helpers
2 | {
3 | ///
4 | /// 指明 Uri 是前往上一页、下一页还是本页的(视频P35)
5 | ///
6 | public enum ResourceUriType
7 | {
8 | PreviousPage,
9 | NextPage,
10 | CurrentPage
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Routine/Routine.APi/Migrations/20200206121508_AfterP38.Designer.cs:
--------------------------------------------------------------------------------
1 | //
2 | using System;
3 | using Microsoft.EntityFrameworkCore;
4 | using Microsoft.EntityFrameworkCore.Infrastructure;
5 | using Microsoft.EntityFrameworkCore.Metadata;
6 | using Microsoft.EntityFrameworkCore.Migrations;
7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
8 | using Routine.APi.Data;
9 |
10 | namespace Routine.APi.Migrations
11 | {
12 | [DbContext(typeof(RoutineDbContext))]
13 | [Migration("20200206121508_AfterP38")]
14 | partial class AfterP38
15 | {
16 | protected override void BuildTargetModel(ModelBuilder modelBuilder)
17 | {
18 | #pragma warning disable 612, 618
19 | modelBuilder
20 | .HasAnnotation("ProductVersion", "3.1.0")
21 | .HasAnnotation("Relational:MaxIdentifierLength", 128)
22 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
23 |
24 | modelBuilder.Entity("Routine.APi.Entities.Company", b =>
25 | {
26 | b.Property("Id")
27 | .ValueGeneratedOnAdd()
28 | .HasColumnType("uniqueidentifier");
29 |
30 | b.Property("Country")
31 | .HasColumnType("nvarchar(max)");
32 |
33 | b.Property("Industry")
34 | .HasColumnType("nvarchar(max)");
35 |
36 | b.Property("Introduction")
37 | .HasColumnType("nvarchar(500)")
38 | .HasMaxLength(500);
39 |
40 | b.Property("Name")
41 | .IsRequired()
42 | .HasColumnType("nvarchar(100)")
43 | .HasMaxLength(100);
44 |
45 | b.Property("Product")
46 | .HasColumnType("nvarchar(max)");
47 |
48 | b.HasKey("Id");
49 |
50 | b.ToTable("Companies");
51 |
52 | b.HasData(
53 | new
54 | {
55 | Id = new Guid("bbdee09c-089b-4d30-bece-44df5923716c"),
56 | Country = "USA",
57 | Industry = "Internet",
58 | Introduction = "Great Company",
59 | Name = "Microsoft",
60 | Product = "Software"
61 | },
62 | new
63 | {
64 | Id = new Guid("6fb600c1-9011-4fd7-9234-881379716440"),
65 | Country = "USA",
66 | Industry = "Internet",
67 | Introduction = "Don't be evil",
68 | Name = "Google",
69 | Product = "Software"
70 | },
71 | new
72 | {
73 | Id = new Guid("5efc910b-2f45-43df-afee-620d40542853"),
74 | Country = "CN",
75 | Industry = "Internet",
76 | Introduction = "Fubao Company",
77 | Name = "Alipapa",
78 | Product = "Software"
79 | },
80 | new
81 | {
82 | Id = new Guid("8cc04f96-2c42-4f76-832e-1903835b0190"),
83 | Country = "CN",
84 | Industry = "Communication",
85 | Introduction = "Building a Smart World of Everything",
86 | Name = "Huawei",
87 | Product = "Hardware"
88 | },
89 | new
90 | {
91 | Id = new Guid("d1f1f410-f563-4355-aa91-4774d693363f"),
92 | Country = "CN",
93 | Industry = "Communication",
94 | Introduction = "Born for a fever",
95 | Name = "Xiaomi",
96 | Product = "Hardware"
97 | },
98 | new
99 | {
100 | Id = new Guid("19b8d0f9-4fdf-41ab-b172-f2d5d725b6d9"),
101 | Country = "CN",
102 | Industry = "Wine",
103 | Introduction = "Great Wine",
104 | Name = "Wuliangye",
105 | Product = "Wine"
106 | },
107 | new
108 | {
109 | Id = new Guid("6c28b511-34f6-43b2-89f6-fa3dab77bcf9"),
110 | Country = "JP",
111 | Industry = "Textile",
112 | Introduction = "Good clothes",
113 | Name = "UNIQLO",
114 | Product = "Costume"
115 | },
116 | new
117 | {
118 | Id = new Guid("4ab2b4af-45ce-41b3-8aed-5447c3140330"),
119 | Country = "ESP",
120 | Industry = "Textile",
121 | Introduction = "Stylish clothes",
122 | Name = "ZARA",
123 | Product = "Costume"
124 | },
125 | new
126 | {
127 | Id = new Guid("cd11c117-551c-409f-80e9-c15d89fd7ca8"),
128 | Country = "GER",
129 | Industry = "Auto",
130 | Introduction = "The best car",
131 | Name = "Mercedes-Benz",
132 | Product = "Car"
133 | },
134 | new
135 | {
136 | Id = new Guid("a39f7877-3849-48a1-b6af-e35b90c73e6a"),
137 | Country = "GER",
138 | Industry = "Auto",
139 | Introduction = "Good car",
140 | Name = "BMW",
141 | Product = "Car"
142 | },
143 | new
144 | {
145 | Id = new Guid("eb8fc677-2600-4fdb-a8ef-51c006e7fc20"),
146 | Country = "USA",
147 | Industry = "Internet",
148 | Introduction = "An American web services provider headquartered in Sunnyvale",
149 | Name = "Yahoo!",
150 | Product = "Software"
151 | });
152 | });
153 |
154 | modelBuilder.Entity("Routine.APi.Entities.Employee", b =>
155 | {
156 | b.Property("Id")
157 | .ValueGeneratedOnAdd()
158 | .HasColumnType("uniqueidentifier");
159 |
160 | b.Property("CompanyId")
161 | .HasColumnType("uniqueidentifier");
162 |
163 | b.Property("DateOfBirth")
164 | .HasColumnType("datetime2");
165 |
166 | b.Property("EmployeeNo")
167 | .IsRequired()
168 | .HasColumnType("nvarchar(10)")
169 | .HasMaxLength(10);
170 |
171 | b.Property("FirstName")
172 | .IsRequired()
173 | .HasColumnType("nvarchar(50)")
174 | .HasMaxLength(50);
175 |
176 | b.Property("Gender")
177 | .HasColumnType("int");
178 |
179 | b.Property("LastName")
180 | .IsRequired()
181 | .HasColumnType("nvarchar(50)")
182 | .HasMaxLength(50);
183 |
184 | b.HasKey("Id");
185 |
186 | b.HasIndex("CompanyId");
187 |
188 | b.ToTable("Employees");
189 |
190 | b.HasData(
191 | new
192 | {
193 | Id = new Guid("ca268a19-0f39-4d8b-b8d6-5bace54f8027"),
194 | CompanyId = new Guid("bbdee09c-089b-4d30-bece-44df5923716c"),
195 | DateOfBirth = new DateTime(1955, 10, 28, 0, 0, 0, 0, DateTimeKind.Unspecified),
196 | EmployeeNo = "M001",
197 | FirstName = "William",
198 | Gender = 1,
199 | LastName = "Gates"
200 | },
201 | new
202 | {
203 | Id = new Guid("265348d2-1276-4ada-ae33-4c1b8348edce"),
204 | CompanyId = new Guid("bbdee09c-089b-4d30-bece-44df5923716c"),
205 | DateOfBirth = new DateTime(1998, 1, 14, 0, 0, 0, 0, DateTimeKind.Unspecified),
206 | EmployeeNo = "M002",
207 | FirstName = "Kent",
208 | Gender = 1,
209 | LastName = "Back"
210 | },
211 | new
212 | {
213 | Id = new Guid("47b70abc-98b8-4fdc-b9fa-5dd6716f6e6b"),
214 | CompanyId = new Guid("6fb600c1-9011-4fd7-9234-881379716440"),
215 | DateOfBirth = new DateTime(1986, 11, 4, 0, 0, 0, 0, DateTimeKind.Unspecified),
216 | EmployeeNo = "G001",
217 | FirstName = "Mary",
218 | Gender = 0,
219 | LastName = "King"
220 | },
221 | new
222 | {
223 | Id = new Guid("059e2fcb-e5a4-4188-9b46-06184bcb111b"),
224 | CompanyId = new Guid("6fb600c1-9011-4fd7-9234-881379716440"),
225 | DateOfBirth = new DateTime(1977, 4, 6, 0, 0, 0, 0, DateTimeKind.Unspecified),
226 | EmployeeNo = "G002",
227 | FirstName = "Kevin",
228 | Gender = 1,
229 | LastName = "Richardson"
230 | },
231 | new
232 | {
233 | Id = new Guid("910e7452-c05f-4bf1-b084-6367873664a1"),
234 | CompanyId = new Guid("6fb600c1-9011-4fd7-9234-881379716440"),
235 | DateOfBirth = new DateTime(1982, 3, 1, 0, 0, 0, 0, DateTimeKind.Unspecified),
236 | EmployeeNo = "G003",
237 | FirstName = "Frederic",
238 | Gender = 1,
239 | LastName = "Pullan"
240 | },
241 | new
242 | {
243 | Id = new Guid("a868ff18-3398-4598-b420-4878974a517a"),
244 | CompanyId = new Guid("5efc910b-2f45-43df-afee-620d40542853"),
245 | DateOfBirth = new DateTime(1964, 9, 10, 0, 0, 0, 0, DateTimeKind.Unspecified),
246 | EmployeeNo = "A001",
247 | FirstName = "Jack",
248 | Gender = 1,
249 | LastName = "Ma"
250 | },
251 | new
252 | {
253 | Id = new Guid("2c3bb40c-5907-4eb7-bb2c-7d62edb430c9"),
254 | CompanyId = new Guid("5efc910b-2f45-43df-afee-620d40542853"),
255 | DateOfBirth = new DateTime(1997, 2, 6, 0, 0, 0, 0, DateTimeKind.Unspecified),
256 | EmployeeNo = "A002",
257 | FirstName = "Lorraine",
258 | Gender = 0,
259 | LastName = "Shaw"
260 | },
261 | new
262 | {
263 | Id = new Guid("e32c33a7-df20-4b9a-a540-414192362d52"),
264 | CompanyId = new Guid("5efc910b-2f45-43df-afee-620d40542853"),
265 | DateOfBirth = new DateTime(2000, 1, 24, 0, 0, 0, 0, DateTimeKind.Unspecified),
266 | EmployeeNo = "A003",
267 | FirstName = "Abel",
268 | Gender = 0,
269 | LastName = "Obadiah"
270 | },
271 | new
272 | {
273 | Id = new Guid("3fae0ed7-5391-460a-8320-6b0255b62b72"),
274 | CompanyId = new Guid("8cc04f96-2c42-4f76-832e-1903835b0190"),
275 | DateOfBirth = new DateTime(1972, 1, 12, 0, 0, 0, 0, DateTimeKind.Unspecified),
276 | EmployeeNo = "H001",
277 | FirstName = "Alexia",
278 | Gender = 0,
279 | LastName = "More"
280 | },
281 | new
282 | {
283 | Id = new Guid("1b863e75-8bd8-4876-8292-e99998bfa4b1"),
284 | CompanyId = new Guid("8cc04f96-2c42-4f76-832e-1903835b0190"),
285 | DateOfBirth = new DateTime(1999, 12, 6, 0, 0, 0, 0, DateTimeKind.Unspecified),
286 | EmployeeNo = "H002",
287 | FirstName = "Barton",
288 | Gender = 0,
289 | LastName = "Robin"
290 | },
291 | new
292 | {
293 | Id = new Guid("c8353598-5b34-4529-a02b-dc7e9f93e59b"),
294 | CompanyId = new Guid("8cc04f96-2c42-4f76-832e-1903835b0190"),
295 | DateOfBirth = new DateTime(1990, 6, 26, 0, 0, 0, 0, DateTimeKind.Unspecified),
296 | EmployeeNo = "H003",
297 | FirstName = "Ted",
298 | Gender = 1,
299 | LastName = "Howard"
300 | },
301 | new
302 | {
303 | Id = new Guid("ca86eded-a704-4fbc-8d5e-979761a2e0b8"),
304 | CompanyId = new Guid("8cc04f96-2c42-4f76-832e-1903835b0190"),
305 | DateOfBirth = new DateTime(2000, 2, 2, 0, 0, 0, 0, DateTimeKind.Unspecified),
306 | EmployeeNo = "M003",
307 | FirstName = "Victor",
308 | Gender = 1,
309 | LastName = "Burns"
310 | });
311 | });
312 |
313 | modelBuilder.Entity("Routine.APi.Entities.Employee", b =>
314 | {
315 | b.HasOne("Routine.APi.Entities.Company", "Company")
316 | .WithMany("Employees")
317 | .HasForeignKey("CompanyId")
318 | .OnDelete(DeleteBehavior.Cascade)
319 | .IsRequired();
320 | });
321 | #pragma warning restore 612, 618
322 | }
323 | }
324 | }
325 |
--------------------------------------------------------------------------------
/Routine/Routine.APi/Migrations/20200206121508_AfterP38.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.EntityFrameworkCore.Migrations;
3 |
4 | namespace Routine.APi.Migrations
5 | {
6 | public partial class AfterP38 : Migration
7 | {
8 | protected override void Up(MigrationBuilder migrationBuilder)
9 | {
10 | migrationBuilder.CreateTable(
11 | name: "Companies",
12 | columns: table => new
13 | {
14 | Id = table.Column(nullable: false),
15 | Name = table.Column(maxLength: 100, nullable: false),
16 | Country = table.Column(nullable: true),
17 | Industry = table.Column(nullable: true),
18 | Product = table.Column(nullable: true),
19 | Introduction = table.Column(maxLength: 500, nullable: true)
20 | },
21 | constraints: table =>
22 | {
23 | table.PrimaryKey("PK_Companies", x => x.Id);
24 | });
25 |
26 | migrationBuilder.CreateTable(
27 | name: "Employees",
28 | columns: table => new
29 | {
30 | Id = table.Column(nullable: false),
31 | CompanyId = table.Column(nullable: false),
32 | EmployeeNo = table.Column(maxLength: 10, nullable: false),
33 | FirstName = table.Column(maxLength: 50, nullable: false),
34 | LastName = table.Column(maxLength: 50, nullable: false),
35 | Gender = table.Column(nullable: false),
36 | DateOfBirth = table.Column(nullable: false)
37 | },
38 | constraints: table =>
39 | {
40 | table.PrimaryKey("PK_Employees", x => x.Id);
41 | table.ForeignKey(
42 | name: "FK_Employees_Companies_CompanyId",
43 | column: x => x.CompanyId,
44 | principalTable: "Companies",
45 | principalColumn: "Id",
46 | onDelete: ReferentialAction.Cascade);
47 | });
48 |
49 | migrationBuilder.InsertData(
50 | table: "Companies",
51 | columns: new[] { "Id", "Country", "Industry", "Introduction", "Name", "Product" },
52 | values: new object[,]
53 | {
54 | { new Guid("bbdee09c-089b-4d30-bece-44df5923716c"), "USA", "Internet", "Great Company", "Microsoft", "Software" },
55 | { new Guid("6fb600c1-9011-4fd7-9234-881379716440"), "USA", "Internet", "Don't be evil", "Google", "Software" },
56 | { new Guid("5efc910b-2f45-43df-afee-620d40542853"), "CN", "Internet", "Fubao Company", "Alipapa", "Software" },
57 | { new Guid("8cc04f96-2c42-4f76-832e-1903835b0190"), "CN", "Communication", "Building a Smart World of Everything", "Huawei", "Hardware" },
58 | { new Guid("d1f1f410-f563-4355-aa91-4774d693363f"), "CN", "Communication", "Born for a fever", "Xiaomi", "Hardware" },
59 | { new Guid("19b8d0f9-4fdf-41ab-b172-f2d5d725b6d9"), "CN", "Wine", "Great Wine", "Wuliangye", "Wine" },
60 | { new Guid("6c28b511-34f6-43b2-89f6-fa3dab77bcf9"), "JP", "Textile", "Good clothes", "UNIQLO", "Costume" },
61 | { new Guid("4ab2b4af-45ce-41b3-8aed-5447c3140330"), "ESP", "Textile", "Stylish clothes", "ZARA", "Costume" },
62 | { new Guid("cd11c117-551c-409f-80e9-c15d89fd7ca8"), "GER", "Auto", "The best car", "Mercedes-Benz", "Car" },
63 | { new Guid("a39f7877-3849-48a1-b6af-e35b90c73e6a"), "GER", "Auto", "Good car", "BMW", "Car" },
64 | { new Guid("eb8fc677-2600-4fdb-a8ef-51c006e7fc20"), "USA", "Internet", "An American web services provider headquartered in Sunnyvale", "Yahoo!", "Software" }
65 | });
66 |
67 | migrationBuilder.InsertData(
68 | table: "Employees",
69 | columns: new[] { "Id", "CompanyId", "DateOfBirth", "EmployeeNo", "FirstName", "Gender", "LastName" },
70 | values: new object[,]
71 | {
72 | { new Guid("ca268a19-0f39-4d8b-b8d6-5bace54f8027"), new Guid("bbdee09c-089b-4d30-bece-44df5923716c"), new DateTime(1955, 10, 28, 0, 0, 0, 0, DateTimeKind.Unspecified), "M001", "William", 1, "Gates" },
73 | { new Guid("265348d2-1276-4ada-ae33-4c1b8348edce"), new Guid("bbdee09c-089b-4d30-bece-44df5923716c"), new DateTime(1998, 1, 14, 0, 0, 0, 0, DateTimeKind.Unspecified), "M002", "Kent", 1, "Back" },
74 | { new Guid("47b70abc-98b8-4fdc-b9fa-5dd6716f6e6b"), new Guid("6fb600c1-9011-4fd7-9234-881379716440"), new DateTime(1986, 11, 4, 0, 0, 0, 0, DateTimeKind.Unspecified), "G001", "Mary", 0, "King" },
75 | { new Guid("059e2fcb-e5a4-4188-9b46-06184bcb111b"), new Guid("6fb600c1-9011-4fd7-9234-881379716440"), new DateTime(1977, 4, 6, 0, 0, 0, 0, DateTimeKind.Unspecified), "G002", "Kevin", 1, "Richardson" },
76 | { new Guid("910e7452-c05f-4bf1-b084-6367873664a1"), new Guid("6fb600c1-9011-4fd7-9234-881379716440"), new DateTime(1982, 3, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), "G003", "Frederic", 1, "Pullan" },
77 | { new Guid("a868ff18-3398-4598-b420-4878974a517a"), new Guid("5efc910b-2f45-43df-afee-620d40542853"), new DateTime(1964, 9, 10, 0, 0, 0, 0, DateTimeKind.Unspecified), "A001", "Jack", 1, "Ma" },
78 | { new Guid("2c3bb40c-5907-4eb7-bb2c-7d62edb430c9"), new Guid("5efc910b-2f45-43df-afee-620d40542853"), new DateTime(1997, 2, 6, 0, 0, 0, 0, DateTimeKind.Unspecified), "A002", "Lorraine", 0, "Shaw" },
79 | { new Guid("e32c33a7-df20-4b9a-a540-414192362d52"), new Guid("5efc910b-2f45-43df-afee-620d40542853"), new DateTime(2000, 1, 24, 0, 0, 0, 0, DateTimeKind.Unspecified), "A003", "Abel", 0, "Obadiah" },
80 | { new Guid("3fae0ed7-5391-460a-8320-6b0255b62b72"), new Guid("8cc04f96-2c42-4f76-832e-1903835b0190"), new DateTime(1972, 1, 12, 0, 0, 0, 0, DateTimeKind.Unspecified), "H001", "Alexia", 0, "More" },
81 | { new Guid("1b863e75-8bd8-4876-8292-e99998bfa4b1"), new Guid("8cc04f96-2c42-4f76-832e-1903835b0190"), new DateTime(1999, 12, 6, 0, 0, 0, 0, DateTimeKind.Unspecified), "H002", "Barton", 0, "Robin" },
82 | { new Guid("c8353598-5b34-4529-a02b-dc7e9f93e59b"), new Guid("8cc04f96-2c42-4f76-832e-1903835b0190"), new DateTime(1990, 6, 26, 0, 0, 0, 0, DateTimeKind.Unspecified), "H003", "Ted", 1, "Howard" },
83 | { new Guid("ca86eded-a704-4fbc-8d5e-979761a2e0b8"), new Guid("8cc04f96-2c42-4f76-832e-1903835b0190"), new DateTime(2000, 2, 2, 0, 0, 0, 0, DateTimeKind.Unspecified), "M003", "Victor", 1, "Burns" }
84 | });
85 |
86 | migrationBuilder.CreateIndex(
87 | name: "IX_Employees_CompanyId",
88 | table: "Employees",
89 | column: "CompanyId");
90 | }
91 |
92 | protected override void Down(MigrationBuilder migrationBuilder)
93 | {
94 | migrationBuilder.DropTable(
95 | name: "Employees");
96 |
97 | migrationBuilder.DropTable(
98 | name: "Companies");
99 | }
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/Routine/Routine.APi/Migrations/20200222031609_AddBankruptTime.Designer.cs:
--------------------------------------------------------------------------------
1 | //
2 | using System;
3 | using Microsoft.EntityFrameworkCore;
4 | using Microsoft.EntityFrameworkCore.Infrastructure;
5 | using Microsoft.EntityFrameworkCore.Metadata;
6 | using Microsoft.EntityFrameworkCore.Migrations;
7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
8 | using Routine.APi.Data;
9 |
10 | namespace Routine.APi.Migrations
11 | {
12 | [DbContext(typeof(RoutineDbContext))]
13 | [Migration("20200222031609_AddBankruptTime")]
14 | partial class AddBankruptTime
15 | {
16 | protected override void BuildTargetModel(ModelBuilder modelBuilder)
17 | {
18 | #pragma warning disable 612, 618
19 | modelBuilder
20 | .HasAnnotation("ProductVersion", "3.1.0")
21 | .HasAnnotation("Relational:MaxIdentifierLength", 128)
22 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
23 |
24 | modelBuilder.Entity("Routine.APi.Entities.Company", b =>
25 | {
26 | b.Property("Id")
27 | .ValueGeneratedOnAdd()
28 | .HasColumnType("uniqueidentifier");
29 |
30 | b.Property("BankruptTime")
31 | .HasColumnType("datetime2");
32 |
33 | b.Property("Country")
34 | .HasColumnType("nvarchar(max)");
35 |
36 | b.Property("Industry")
37 | .HasColumnType("nvarchar(max)");
38 |
39 | b.Property("Introduction")
40 | .HasColumnType("nvarchar(500)")
41 | .HasMaxLength(500);
42 |
43 | b.Property("Name")
44 | .IsRequired()
45 | .HasColumnType("nvarchar(100)")
46 | .HasMaxLength(100);
47 |
48 | b.Property("Product")
49 | .HasColumnType("nvarchar(max)");
50 |
51 | b.HasKey("Id");
52 |
53 | b.ToTable("Companies");
54 |
55 | b.HasData(
56 | new
57 | {
58 | Id = new Guid("bbdee09c-089b-4d30-bece-44df5923716c"),
59 | Country = "USA",
60 | Industry = "Internet",
61 | Introduction = "Great Company",
62 | Name = "Microsoft",
63 | Product = "Software"
64 | },
65 | new
66 | {
67 | Id = new Guid("6fb600c1-9011-4fd7-9234-881379716440"),
68 | Country = "USA",
69 | Industry = "Internet",
70 | Introduction = "Don't be evil",
71 | Name = "Google",
72 | Product = "Software"
73 | },
74 | new
75 | {
76 | Id = new Guid("5efc910b-2f45-43df-afee-620d40542853"),
77 | Country = "CN",
78 | Industry = "Internet",
79 | Introduction = "Fubao Company",
80 | Name = "Alipapa",
81 | Product = "Software"
82 | },
83 | new
84 | {
85 | Id = new Guid("8cc04f96-2c42-4f76-832e-1903835b0190"),
86 | Country = "CN",
87 | Industry = "Communication",
88 | Introduction = "Building a Smart World of Everything",
89 | Name = "Huawei",
90 | Product = "Hardware"
91 | },
92 | new
93 | {
94 | Id = new Guid("d1f1f410-f563-4355-aa91-4774d693363f"),
95 | Country = "CN",
96 | Industry = "Communication",
97 | Introduction = "Born for a fever",
98 | Name = "Xiaomi",
99 | Product = "Hardware"
100 | },
101 | new
102 | {
103 | Id = new Guid("19b8d0f9-4fdf-41ab-b172-f2d5d725b6d9"),
104 | Country = "CN",
105 | Industry = "Wine",
106 | Introduction = "Great Wine",
107 | Name = "Wuliangye",
108 | Product = "Wine"
109 | },
110 | new
111 | {
112 | Id = new Guid("6c28b511-34f6-43b2-89f6-fa3dab77bcf9"),
113 | Country = "JP",
114 | Industry = "Textile",
115 | Introduction = "Good clothes",
116 | Name = "UNIQLO",
117 | Product = "Costume"
118 | },
119 | new
120 | {
121 | Id = new Guid("4ab2b4af-45ce-41b3-8aed-5447c3140330"),
122 | Country = "ESP",
123 | Industry = "Textile",
124 | Introduction = "Stylish clothes",
125 | Name = "ZARA",
126 | Product = "Costume"
127 | },
128 | new
129 | {
130 | Id = new Guid("cd11c117-551c-409f-80e9-c15d89fd7ca8"),
131 | Country = "GER",
132 | Industry = "Auto",
133 | Introduction = "The best car",
134 | Name = "Mercedes-Benz",
135 | Product = "Car"
136 | },
137 | new
138 | {
139 | Id = new Guid("a39f7877-3849-48a1-b6af-e35b90c73e6a"),
140 | Country = "GER",
141 | Industry = "Auto",
142 | Introduction = "Good car",
143 | Name = "BMW",
144 | Product = "Car"
145 | },
146 | new
147 | {
148 | Id = new Guid("eb8fc677-2600-4fdb-a8ef-51c006e7fc20"),
149 | Country = "USA",
150 | Industry = "Internet",
151 | Introduction = "An American web services provider headquartered in Sunnyvale",
152 | Name = "Yahoo!",
153 | Product = "Software"
154 | });
155 | });
156 |
157 | modelBuilder.Entity("Routine.APi.Entities.Employee", b =>
158 | {
159 | b.Property("Id")
160 | .ValueGeneratedOnAdd()
161 | .HasColumnType("uniqueidentifier");
162 |
163 | b.Property("CompanyId")
164 | .HasColumnType("uniqueidentifier");
165 |
166 | b.Property("DateOfBirth")
167 | .HasColumnType("datetime2");
168 |
169 | b.Property("EmployeeNo")
170 | .IsRequired()
171 | .HasColumnType("nvarchar(10)")
172 | .HasMaxLength(10);
173 |
174 | b.Property("FirstName")
175 | .IsRequired()
176 | .HasColumnType("nvarchar(50)")
177 | .HasMaxLength(50);
178 |
179 | b.Property("Gender")
180 | .HasColumnType("int");
181 |
182 | b.Property("LastName")
183 | .IsRequired()
184 | .HasColumnType("nvarchar(50)")
185 | .HasMaxLength(50);
186 |
187 | b.HasKey("Id");
188 |
189 | b.HasIndex("CompanyId");
190 |
191 | b.ToTable("Employees");
192 |
193 | b.HasData(
194 | new
195 | {
196 | Id = new Guid("ca268a19-0f39-4d8b-b8d6-5bace54f8027"),
197 | CompanyId = new Guid("bbdee09c-089b-4d30-bece-44df5923716c"),
198 | DateOfBirth = new DateTime(1955, 10, 28, 0, 0, 0, 0, DateTimeKind.Unspecified),
199 | EmployeeNo = "M001",
200 | FirstName = "William",
201 | Gender = 1,
202 | LastName = "Gates"
203 | },
204 | new
205 | {
206 | Id = new Guid("265348d2-1276-4ada-ae33-4c1b8348edce"),
207 | CompanyId = new Guid("bbdee09c-089b-4d30-bece-44df5923716c"),
208 | DateOfBirth = new DateTime(1998, 1, 14, 0, 0, 0, 0, DateTimeKind.Unspecified),
209 | EmployeeNo = "M002",
210 | FirstName = "Kent",
211 | Gender = 1,
212 | LastName = "Back"
213 | },
214 | new
215 | {
216 | Id = new Guid("47b70abc-98b8-4fdc-b9fa-5dd6716f6e6b"),
217 | CompanyId = new Guid("6fb600c1-9011-4fd7-9234-881379716440"),
218 | DateOfBirth = new DateTime(1986, 11, 4, 0, 0, 0, 0, DateTimeKind.Unspecified),
219 | EmployeeNo = "G001",
220 | FirstName = "Mary",
221 | Gender = 0,
222 | LastName = "King"
223 | },
224 | new
225 | {
226 | Id = new Guid("059e2fcb-e5a4-4188-9b46-06184bcb111b"),
227 | CompanyId = new Guid("6fb600c1-9011-4fd7-9234-881379716440"),
228 | DateOfBirth = new DateTime(1977, 4, 6, 0, 0, 0, 0, DateTimeKind.Unspecified),
229 | EmployeeNo = "G002",
230 | FirstName = "Kevin",
231 | Gender = 1,
232 | LastName = "Richardson"
233 | },
234 | new
235 | {
236 | Id = new Guid("910e7452-c05f-4bf1-b084-6367873664a1"),
237 | CompanyId = new Guid("6fb600c1-9011-4fd7-9234-881379716440"),
238 | DateOfBirth = new DateTime(1982, 3, 1, 0, 0, 0, 0, DateTimeKind.Unspecified),
239 | EmployeeNo = "G003",
240 | FirstName = "Frederic",
241 | Gender = 1,
242 | LastName = "Pullan"
243 | },
244 | new
245 | {
246 | Id = new Guid("a868ff18-3398-4598-b420-4878974a517a"),
247 | CompanyId = new Guid("5efc910b-2f45-43df-afee-620d40542853"),
248 | DateOfBirth = new DateTime(1964, 9, 10, 0, 0, 0, 0, DateTimeKind.Unspecified),
249 | EmployeeNo = "A001",
250 | FirstName = "Jack",
251 | Gender = 1,
252 | LastName = "Ma"
253 | },
254 | new
255 | {
256 | Id = new Guid("2c3bb40c-5907-4eb7-bb2c-7d62edb430c9"),
257 | CompanyId = new Guid("5efc910b-2f45-43df-afee-620d40542853"),
258 | DateOfBirth = new DateTime(1997, 2, 6, 0, 0, 0, 0, DateTimeKind.Unspecified),
259 | EmployeeNo = "A002",
260 | FirstName = "Lorraine",
261 | Gender = 0,
262 | LastName = "Shaw"
263 | },
264 | new
265 | {
266 | Id = new Guid("e32c33a7-df20-4b9a-a540-414192362d52"),
267 | CompanyId = new Guid("5efc910b-2f45-43df-afee-620d40542853"),
268 | DateOfBirth = new DateTime(2000, 1, 24, 0, 0, 0, 0, DateTimeKind.Unspecified),
269 | EmployeeNo = "A003",
270 | FirstName = "Abel",
271 | Gender = 0,
272 | LastName = "Obadiah"
273 | },
274 | new
275 | {
276 | Id = new Guid("3fae0ed7-5391-460a-8320-6b0255b62b72"),
277 | CompanyId = new Guid("8cc04f96-2c42-4f76-832e-1903835b0190"),
278 | DateOfBirth = new DateTime(1972, 1, 12, 0, 0, 0, 0, DateTimeKind.Unspecified),
279 | EmployeeNo = "H001",
280 | FirstName = "Alexia",
281 | Gender = 0,
282 | LastName = "More"
283 | },
284 | new
285 | {
286 | Id = new Guid("1b863e75-8bd8-4876-8292-e99998bfa4b1"),
287 | CompanyId = new Guid("8cc04f96-2c42-4f76-832e-1903835b0190"),
288 | DateOfBirth = new DateTime(1999, 12, 6, 0, 0, 0, 0, DateTimeKind.Unspecified),
289 | EmployeeNo = "H002",
290 | FirstName = "Barton",
291 | Gender = 0,
292 | LastName = "Robin"
293 | },
294 | new
295 | {
296 | Id = new Guid("c8353598-5b34-4529-a02b-dc7e9f93e59b"),
297 | CompanyId = new Guid("8cc04f96-2c42-4f76-832e-1903835b0190"),
298 | DateOfBirth = new DateTime(1990, 6, 26, 0, 0, 0, 0, DateTimeKind.Unspecified),
299 | EmployeeNo = "H003",
300 | FirstName = "Ted",
301 | Gender = 1,
302 | LastName = "Howard"
303 | },
304 | new
305 | {
306 | Id = new Guid("ca86eded-a704-4fbc-8d5e-979761a2e0b8"),
307 | CompanyId = new Guid("8cc04f96-2c42-4f76-832e-1903835b0190"),
308 | DateOfBirth = new DateTime(2000, 2, 2, 0, 0, 0, 0, DateTimeKind.Unspecified),
309 | EmployeeNo = "M003",
310 | FirstName = "Victor",
311 | Gender = 1,
312 | LastName = "Burns"
313 | });
314 | });
315 |
316 | modelBuilder.Entity("Routine.APi.Entities.Employee", b =>
317 | {
318 | b.HasOne("Routine.APi.Entities.Company", "Company")
319 | .WithMany("Employees")
320 | .HasForeignKey("CompanyId")
321 | .OnDelete(DeleteBehavior.Cascade)
322 | .IsRequired();
323 | });
324 | #pragma warning restore 612, 618
325 | }
326 | }
327 | }
328 |
--------------------------------------------------------------------------------
/Routine/Routine.APi/Migrations/20200222031609_AddBankruptTime.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.EntityFrameworkCore.Migrations;
3 |
4 | namespace Routine.APi.Migrations
5 | {
6 | public partial class AddBankruptTime : Migration
7 | {
8 | protected override void Up(MigrationBuilder migrationBuilder)
9 | {
10 | migrationBuilder.AddColumn(
11 | name: "BankruptTime",
12 | table: "Companies",
13 | nullable: true);
14 | }
15 |
16 | protected override void Down(MigrationBuilder migrationBuilder)
17 | {
18 | migrationBuilder.DropColumn(
19 | name: "BankruptTime",
20 | table: "Companies");
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Routine/Routine.APi/Migrations/RoutineDbContextModelSnapshot.cs:
--------------------------------------------------------------------------------
1 | //
2 | using System;
3 | using Microsoft.EntityFrameworkCore;
4 | using Microsoft.EntityFrameworkCore.Infrastructure;
5 | using Microsoft.EntityFrameworkCore.Metadata;
6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
7 | using Routine.APi.Data;
8 |
9 | namespace Routine.APi.Migrations
10 | {
11 | [DbContext(typeof(RoutineDbContext))]
12 | partial class RoutineDbContextModelSnapshot : ModelSnapshot
13 | {
14 | protected override void BuildModel(ModelBuilder modelBuilder)
15 | {
16 | #pragma warning disable 612, 618
17 | modelBuilder
18 | .HasAnnotation("ProductVersion", "3.1.0")
19 | .HasAnnotation("Relational:MaxIdentifierLength", 128)
20 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
21 |
22 | modelBuilder.Entity("Routine.APi.Entities.Company", b =>
23 | {
24 | b.Property("Id")
25 | .ValueGeneratedOnAdd()
26 | .HasColumnType("uniqueidentifier");
27 |
28 | b.Property("BankruptTime")
29 | .HasColumnType("datetime2");
30 |
31 | b.Property("Country")
32 | .HasColumnType("nvarchar(max)");
33 |
34 | b.Property("Industry")
35 | .HasColumnType("nvarchar(max)");
36 |
37 | b.Property("Introduction")
38 | .HasColumnType("nvarchar(500)")
39 | .HasMaxLength(500);
40 |
41 | b.Property("Name")
42 | .IsRequired()
43 | .HasColumnType("nvarchar(100)")
44 | .HasMaxLength(100);
45 |
46 | b.Property("Product")
47 | .HasColumnType("nvarchar(max)");
48 |
49 | b.HasKey("Id");
50 |
51 | b.ToTable("Companies");
52 |
53 | b.HasData(
54 | new
55 | {
56 | Id = new Guid("bbdee09c-089b-4d30-bece-44df5923716c"),
57 | Country = "USA",
58 | Industry = "Internet",
59 | Introduction = "Great Company",
60 | Name = "Microsoft",
61 | Product = "Software"
62 | },
63 | new
64 | {
65 | Id = new Guid("6fb600c1-9011-4fd7-9234-881379716440"),
66 | Country = "USA",
67 | Industry = "Internet",
68 | Introduction = "Don't be evil",
69 | Name = "Google",
70 | Product = "Software"
71 | },
72 | new
73 | {
74 | Id = new Guid("5efc910b-2f45-43df-afee-620d40542853"),
75 | Country = "CN",
76 | Industry = "Internet",
77 | Introduction = "Fubao Company",
78 | Name = "Alipapa",
79 | Product = "Software"
80 | },
81 | new
82 | {
83 | Id = new Guid("8cc04f96-2c42-4f76-832e-1903835b0190"),
84 | Country = "CN",
85 | Industry = "Communication",
86 | Introduction = "Building a Smart World of Everything",
87 | Name = "Huawei",
88 | Product = "Hardware"
89 | },
90 | new
91 | {
92 | Id = new Guid("d1f1f410-f563-4355-aa91-4774d693363f"),
93 | Country = "CN",
94 | Industry = "Communication",
95 | Introduction = "Born for a fever",
96 | Name = "Xiaomi",
97 | Product = "Hardware"
98 | },
99 | new
100 | {
101 | Id = new Guid("19b8d0f9-4fdf-41ab-b172-f2d5d725b6d9"),
102 | Country = "CN",
103 | Industry = "Wine",
104 | Introduction = "Great Wine",
105 | Name = "Wuliangye",
106 | Product = "Wine"
107 | },
108 | new
109 | {
110 | Id = new Guid("6c28b511-34f6-43b2-89f6-fa3dab77bcf9"),
111 | Country = "JP",
112 | Industry = "Textile",
113 | Introduction = "Good clothes",
114 | Name = "UNIQLO",
115 | Product = "Costume"
116 | },
117 | new
118 | {
119 | Id = new Guid("4ab2b4af-45ce-41b3-8aed-5447c3140330"),
120 | Country = "ESP",
121 | Industry = "Textile",
122 | Introduction = "Stylish clothes",
123 | Name = "ZARA",
124 | Product = "Costume"
125 | },
126 | new
127 | {
128 | Id = new Guid("cd11c117-551c-409f-80e9-c15d89fd7ca8"),
129 | Country = "GER",
130 | Industry = "Auto",
131 | Introduction = "The best car",
132 | Name = "Mercedes-Benz",
133 | Product = "Car"
134 | },
135 | new
136 | {
137 | Id = new Guid("a39f7877-3849-48a1-b6af-e35b90c73e6a"),
138 | Country = "GER",
139 | Industry = "Auto",
140 | Introduction = "Good car",
141 | Name = "BMW",
142 | Product = "Car"
143 | },
144 | new
145 | {
146 | Id = new Guid("eb8fc677-2600-4fdb-a8ef-51c006e7fc20"),
147 | Country = "USA",
148 | Industry = "Internet",
149 | Introduction = "An American web services provider headquartered in Sunnyvale",
150 | Name = "Yahoo!",
151 | Product = "Software"
152 | });
153 | });
154 |
155 | modelBuilder.Entity("Routine.APi.Entities.Employee", b =>
156 | {
157 | b.Property("Id")
158 | .ValueGeneratedOnAdd()
159 | .HasColumnType("uniqueidentifier");
160 |
161 | b.Property("CompanyId")
162 | .HasColumnType("uniqueidentifier");
163 |
164 | b.Property("DateOfBirth")
165 | .HasColumnType("datetime2");
166 |
167 | b.Property("EmployeeNo")
168 | .IsRequired()
169 | .HasColumnType("nvarchar(10)")
170 | .HasMaxLength(10);
171 |
172 | b.Property("FirstName")
173 | .IsRequired()
174 | .HasColumnType("nvarchar(50)")
175 | .HasMaxLength(50);
176 |
177 | b.Property("Gender")
178 | .HasColumnType("int");
179 |
180 | b.Property("LastName")
181 | .IsRequired()
182 | .HasColumnType("nvarchar(50)")
183 | .HasMaxLength(50);
184 |
185 | b.HasKey("Id");
186 |
187 | b.HasIndex("CompanyId");
188 |
189 | b.ToTable("Employees");
190 |
191 | b.HasData(
192 | new
193 | {
194 | Id = new Guid("ca268a19-0f39-4d8b-b8d6-5bace54f8027"),
195 | CompanyId = new Guid("bbdee09c-089b-4d30-bece-44df5923716c"),
196 | DateOfBirth = new DateTime(1955, 10, 28, 0, 0, 0, 0, DateTimeKind.Unspecified),
197 | EmployeeNo = "M001",
198 | FirstName = "William",
199 | Gender = 1,
200 | LastName = "Gates"
201 | },
202 | new
203 | {
204 | Id = new Guid("265348d2-1276-4ada-ae33-4c1b8348edce"),
205 | CompanyId = new Guid("bbdee09c-089b-4d30-bece-44df5923716c"),
206 | DateOfBirth = new DateTime(1998, 1, 14, 0, 0, 0, 0, DateTimeKind.Unspecified),
207 | EmployeeNo = "M002",
208 | FirstName = "Kent",
209 | Gender = 1,
210 | LastName = "Back"
211 | },
212 | new
213 | {
214 | Id = new Guid("47b70abc-98b8-4fdc-b9fa-5dd6716f6e6b"),
215 | CompanyId = new Guid("6fb600c1-9011-4fd7-9234-881379716440"),
216 | DateOfBirth = new DateTime(1986, 11, 4, 0, 0, 0, 0, DateTimeKind.Unspecified),
217 | EmployeeNo = "G001",
218 | FirstName = "Mary",
219 | Gender = 0,
220 | LastName = "King"
221 | },
222 | new
223 | {
224 | Id = new Guid("059e2fcb-e5a4-4188-9b46-06184bcb111b"),
225 | CompanyId = new Guid("6fb600c1-9011-4fd7-9234-881379716440"),
226 | DateOfBirth = new DateTime(1977, 4, 6, 0, 0, 0, 0, DateTimeKind.Unspecified),
227 | EmployeeNo = "G002",
228 | FirstName = "Kevin",
229 | Gender = 1,
230 | LastName = "Richardson"
231 | },
232 | new
233 | {
234 | Id = new Guid("910e7452-c05f-4bf1-b084-6367873664a1"),
235 | CompanyId = new Guid("6fb600c1-9011-4fd7-9234-881379716440"),
236 | DateOfBirth = new DateTime(1982, 3, 1, 0, 0, 0, 0, DateTimeKind.Unspecified),
237 | EmployeeNo = "G003",
238 | FirstName = "Frederic",
239 | Gender = 1,
240 | LastName = "Pullan"
241 | },
242 | new
243 | {
244 | Id = new Guid("a868ff18-3398-4598-b420-4878974a517a"),
245 | CompanyId = new Guid("5efc910b-2f45-43df-afee-620d40542853"),
246 | DateOfBirth = new DateTime(1964, 9, 10, 0, 0, 0, 0, DateTimeKind.Unspecified),
247 | EmployeeNo = "A001",
248 | FirstName = "Jack",
249 | Gender = 1,
250 | LastName = "Ma"
251 | },
252 | new
253 | {
254 | Id = new Guid("2c3bb40c-5907-4eb7-bb2c-7d62edb430c9"),
255 | CompanyId = new Guid("5efc910b-2f45-43df-afee-620d40542853"),
256 | DateOfBirth = new DateTime(1997, 2, 6, 0, 0, 0, 0, DateTimeKind.Unspecified),
257 | EmployeeNo = "A002",
258 | FirstName = "Lorraine",
259 | Gender = 0,
260 | LastName = "Shaw"
261 | },
262 | new
263 | {
264 | Id = new Guid("e32c33a7-df20-4b9a-a540-414192362d52"),
265 | CompanyId = new Guid("5efc910b-2f45-43df-afee-620d40542853"),
266 | DateOfBirth = new DateTime(2000, 1, 24, 0, 0, 0, 0, DateTimeKind.Unspecified),
267 | EmployeeNo = "A003",
268 | FirstName = "Abel",
269 | Gender = 0,
270 | LastName = "Obadiah"
271 | },
272 | new
273 | {
274 | Id = new Guid("3fae0ed7-5391-460a-8320-6b0255b62b72"),
275 | CompanyId = new Guid("8cc04f96-2c42-4f76-832e-1903835b0190"),
276 | DateOfBirth = new DateTime(1972, 1, 12, 0, 0, 0, 0, DateTimeKind.Unspecified),
277 | EmployeeNo = "H001",
278 | FirstName = "Alexia",
279 | Gender = 0,
280 | LastName = "More"
281 | },
282 | new
283 | {
284 | Id = new Guid("1b863e75-8bd8-4876-8292-e99998bfa4b1"),
285 | CompanyId = new Guid("8cc04f96-2c42-4f76-832e-1903835b0190"),
286 | DateOfBirth = new DateTime(1999, 12, 6, 0, 0, 0, 0, DateTimeKind.Unspecified),
287 | EmployeeNo = "H002",
288 | FirstName = "Barton",
289 | Gender = 0,
290 | LastName = "Robin"
291 | },
292 | new
293 | {
294 | Id = new Guid("c8353598-5b34-4529-a02b-dc7e9f93e59b"),
295 | CompanyId = new Guid("8cc04f96-2c42-4f76-832e-1903835b0190"),
296 | DateOfBirth = new DateTime(1990, 6, 26, 0, 0, 0, 0, DateTimeKind.Unspecified),
297 | EmployeeNo = "H003",
298 | FirstName = "Ted",
299 | Gender = 1,
300 | LastName = "Howard"
301 | },
302 | new
303 | {
304 | Id = new Guid("ca86eded-a704-4fbc-8d5e-979761a2e0b8"),
305 | CompanyId = new Guid("8cc04f96-2c42-4f76-832e-1903835b0190"),
306 | DateOfBirth = new DateTime(2000, 2, 2, 0, 0, 0, 0, DateTimeKind.Unspecified),
307 | EmployeeNo = "M003",
308 | FirstName = "Victor",
309 | Gender = 1,
310 | LastName = "Burns"
311 | });
312 | });
313 |
314 | modelBuilder.Entity("Routine.APi.Entities.Employee", b =>
315 | {
316 | b.HasOne("Routine.APi.Entities.Company", "Company")
317 | .WithMany("Employees")
318 | .HasForeignKey("CompanyId")
319 | .OnDelete(DeleteBehavior.Cascade)
320 | .IsRequired();
321 | });
322 | #pragma warning restore 612, 618
323 | }
324 | }
325 | }
326 |
--------------------------------------------------------------------------------
/Routine/Routine.APi/Models/CompanyAddDto.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.ComponentModel.DataAnnotations;
3 |
4 | namespace Routine.APi.Models
5 | {
6 | //查询、插入、更新应该使用不同的Dto,便于业务升级与重构
7 |
8 | ///
9 | /// Create Company 时使用的 Dto,不含 BankruptTime 属性
10 | ///
11 | public class CompanyAddDto
12 | {
13 | [Display(Name = "公司名")]
14 | [Required(ErrorMessage = "{0}这个字段是必填的")]
15 | [MaxLength(100, ErrorMessage = "{0}的最大长度不可以超过{1}")]
16 | public string Name { get; set; } //请注意,此处的属性名为 Name ,与视频中的 CompanyName 不同
17 |
18 | [Display(Name = "国家")]
19 | [Required(ErrorMessage = "{0}这个字段是必填的")]
20 | [MaxLength(100, ErrorMessage = "{0}的最大长度不可以超过{1}")]
21 | public string Country { get; set; }
22 |
23 | [Display(Name = "行业")]
24 | [MaxLength(100, ErrorMessage = "{0}的最大长度不可以超过{1}")]
25 | public string Industry { get; set; }
26 |
27 | [Display(Name = "产品")]
28 | [MaxLength(100, ErrorMessage = "{0}的最大长度不可以超过{1}")]
29 | public string Product { get; set; }
30 |
31 | [Display(Name = "简介")]
32 | [StringLength(500,MinimumLength =10,ErrorMessage = "{0}的长度范围从{2}到{1}")]
33 | //[MaxLength(500, ErrorMessage = "{0}的最大长度不可以超过{1}")]
34 | //[MinLength(10, ErrorMessage = "{0}的长度至少{1}位")]
35 | public string Introduction { get; set; }
36 |
37 | public ICollection Employees { get; set; } = new List(); //这种写法可以避免空引用异常
38 | }
39 | }
40 |
41 | /*
42 | * 推荐使用第三方库 FluentValidation
43 | * - 容易创建复杂的验证规则
44 | * - 验证规则与 Model 分离
45 | * - 容易进行单元测试
46 | */
47 |
--------------------------------------------------------------------------------
/Routine/Routine.APi/Models/CompanyAddWithBankruptTimeDto.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.ComponentModel.DataAnnotations;
3 |
4 | namespace Routine.APi.Models
5 | {
6 | //继承自 CompanyAddDto,增加 BankruptTime 列
7 |
8 | ///
9 | /// Create Company 时使用的 Dto,增加了 BankruptTime 属性(视频P44)
10 | ///
11 | public class CompanyAddWithBankruptTimeDto : CompanyAddDto
12 | {
13 | [Display(Name = "破产时间")]
14 | public DateTime? BankruptTime { get; set; }
15 | }
16 | }
--------------------------------------------------------------------------------
/Routine/Routine.APi/Models/CompanyFriendlyDto.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Routine.APi.Models
4 | {
5 | //从视频P43 Vendor-specific Media Types 开始,CompanyDto 分为 CompanyFriendlyDto 与 CompanyFullDto
6 |
7 | ///
8 | /// 输出 Company 使用的 Friendly Dto,仅含 Company 的部分属性/字段(视频P43)
9 | ///
10 | public class CompanyFriendlyDto
11 | {
12 | public Guid Id { get; set; }
13 | public string Name { get; set; } //请注意,此处的属性名为 Name ,与视频中的 CompanyName 不同
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Routine/Routine.APi/Models/CompanyFullDto.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Routine.APi.Models
4 | {
5 | //从视频P43 Vendor-specific Media Types 开始,CompanyDto 分为 CompanyFriendlyDto 与 CompanyFullDto
6 |
7 | ///
8 | /// 输出 Company 使用的 Full Dto,包含 Company 的所有属性/字段(视频P43)
9 | ///
10 | public class CompanyFullDto
11 | {
12 | public Guid Id { get; set; }
13 | public string Name { get; set; } //请注意,此处的属性名为 Name ,与视频中的 CompanyName 不同
14 | public string Country { get; set; }
15 | public string Industry { get; set; }
16 | public string Product { get; set; }
17 | public string Introduction { get; set; }
18 | public DateTime? BankruptTime { get; set; }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Routine/Routine.APi/Models/EmployeeAddDto.cs:
--------------------------------------------------------------------------------
1 | namespace Routine.APi.Models
2 | {
3 | ///
4 | /// Create Employee 时使用的 Dto
5 | ///
6 | public class EmployeeAddDto : EmployeeAddOrUpdateDto
7 | {
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/Routine/Routine.APi/Models/EmployeeAddOrUpdateDto.cs:
--------------------------------------------------------------------------------
1 | using Routine.APi.Entities;
2 | using Routine.APi.ValidationAttributes;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.ComponentModel.DataAnnotations;
6 |
7 | namespace Routine.APi.Models
8 | {
9 | //因为该项目的 EmployeeAddDto 与 EmployeeUpdateDto 高度一致
10 | //因此使用这个抽象类,可以减少重复代码
11 |
12 | ///
13 | /// Create 或 Update Employee 时的 Dto 抽象类,不能直接使用
14 | ///
15 | [EmployeeNoMustDifferentFromFirstNameAttribute(ErrorMessage = "员工号必须与名不同")] //作用于类
16 | public abstract class EmployeeAddOrUpdateDto : IValidatableObject
17 | {
18 | [Display(Name = "员工号")]
19 | [Required(ErrorMessage = "{0}是必填项")]
20 | [StringLength(4, MinimumLength = 4, ErrorMessage = "{0}的长度是{1}")]
21 | public string EmployeeNo { get; set; }
22 |
23 | [Display(Name = "名")]
24 | [Required(ErrorMessage = "{0}是必填项")]
25 | [MaxLength(50, ErrorMessage = "{0}的长度不能超过{1}")]
26 | public string FirstName { get; set; }
27 |
28 | [Display(Name = "姓"), Required(ErrorMessage = "{0}是必填项"), MaxLength(50, ErrorMessage = "{0}的长度不能超过{1}")]
29 | public string LastName { get; set; }
30 |
31 | [Display(Name = "性别")]
32 | public Gender Gender { get; set; }
33 |
34 | [Display(Name = "出生日期")]
35 | public DateTime DateOfBirth { get; set; }
36 |
37 | ///
38 | /// 通过实现 IValidatableObject 接口,自定义验证规则(视频P27)
39 | ///
40 | ///
41 | ///
42 | public IEnumerable Validate(ValidationContext validationContext)
43 | {
44 | //FirstName 与 FirstName 不能相同
45 | if (FirstName == LastName)
46 | {
47 | //错误信息与引起错误的位置
48 | yield return new ValidationResult("姓和名不能一样", new[] { nameof(LastName), nameof(FirstName) });
49 | }
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Routine/Routine.APi/Models/EmployeeDto.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Routine.APi.Models
4 | {
5 | ///
6 | /// 输出 Employee 使用的 Dto,包含 Employee 的所有属性/字段,不区分 Full 与 Friendly
7 | ///
8 | public class EmployeeDto
9 | {
10 | public Guid Id { get; set; }
11 | public Guid CompanyId { get; set; }
12 | public string EmployeeNo { get; set; }
13 | public string Name { get; set; }
14 | public string GenderDisplay { get; set; }
15 |
16 | public int Age { get; set; }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Routine/Routine.APi/Models/EmployeeUpdateDto.cs:
--------------------------------------------------------------------------------
1 | namespace Routine.APi.Models
2 | {
3 | ///
4 | /// Update Employee 时使用的 Dto
5 | ///
6 | public class EmployeeUpdateDto : EmployeeAddOrUpdateDto
7 | {
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/Routine/Routine.APi/Models/LinkDto.cs:
--------------------------------------------------------------------------------
1 | namespace Routine.APi.Models
2 | {
3 | ///
4 | /// HATEOAS 的 links Dto(视频P41)
5 | ///
6 | public class LinkDto
7 | {
8 | public string Href { get; }
9 | public string Rel { get; }
10 | public string Method { get; }
11 | public LinkDto(string href,string rel,string method)
12 | {
13 | Href = href;
14 | Rel = rel;
15 | Method = method;
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Routine/Routine.APi/Profiles/CompanyProfile.cs:
--------------------------------------------------------------------------------
1 | using AutoMapper;
2 | using Routine.APi.Entities;
3 | using Routine.APi.Models;
4 |
5 | ///
6 | /// AutoMapper 针对 Company 的映射关系配置文件(视频P12)
7 | ///
8 | namespace Routine.APi.Profiles
9 | {
10 | public class CompanyProfile : Profile
11 | {
12 | public CompanyProfile()
13 | {
14 | //原类型Company -> 目标类型CompanyDto
15 | //AutoMapper 基于约定
16 | //属性名称一致时自动赋值
17 | //自动忽略空引用
18 | CreateMap();
19 | CreateMap();
20 | CreateMap();
21 | CreateMap();
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Routine/Routine.APi/Profiles/EmployeeProfile.cs:
--------------------------------------------------------------------------------
1 | using AutoMapper;
2 | using Routine.APi.Entities;
3 | using Routine.APi.Models;
4 | using System;
5 |
6 | ///
7 | /// AutoMapper 针对 Employee 的映射关系配置文件(视频P12)
8 | ///
9 | namespace Routine.APi.Profiles
10 | {
11 | public class EmployeeProfile : Profile
12 | {
13 | public EmployeeProfile()
14 | {
15 | CreateMap()
16 | .ForMember(dest => dest.Name, opt => opt.MapFrom(src => $"{src.FirstName} {src.LastName}"))
17 | .ForMember(dest => dest.GenderDisplay, opt => opt.MapFrom(src => src.Gender.ToString()))
18 | .ForMember(dest => dest.Age, opt => opt.MapFrom(src => GetAge(src.DateOfBirth)));
19 |
20 | CreateMap();
21 | CreateMap();
22 | CreateMap();
23 | }
24 |
25 | ///
26 | /// 获得年龄
27 | ///
28 | /// 出生时间
29 | /// 年龄
30 | private int GetAge(DateTime dateOfBirth)
31 | {
32 | DateTime dateOfNow = DateTime.Now;
33 | if (dateOfBirth > dateOfNow)
34 | {
35 | throw new ArgumentOutOfRangeException(nameof(dateOfBirth));
36 | }
37 | int age = dateOfNow.Year - dateOfBirth.Year;
38 | if (dateOfNow.Month < dateOfBirth.Month)
39 | {
40 | age--;
41 | }
42 | else if (dateOfNow.Month == dateOfBirth.Month && dateOfNow.Day < dateOfBirth.Day)
43 | {
44 | age--;
45 | }
46 | return age;
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Routine/Routine.APi/Program.cs:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Surbowl/ASP.NET-Core-RESTful-Note/e9a986ffbb98e45cea70e51fea42fa7a21736243/Routine/Routine.APi/Program.cs
--------------------------------------------------------------------------------
/Routine/Routine.APi/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/launchsettings.json",
3 | "profiles": {
4 | "Routine.APi": {
5 | "commandName": "Project",
6 | "launchBrowser": true,
7 | "launchUrl": "api",
8 | "environmentVariables": {
9 | "ASPNETCORE_ENVIRONMENT": "Development"
10 | },
11 | "applicationUrl": "http://localhost:5000"
12 | }
13 | }
14 | }
--------------------------------------------------------------------------------
/Routine/Routine.APi/Routine.APi.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp3.1
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | all
23 | runtime; build; native; contentfiles; analyzers; buildtransitive
24 |
25 |
26 |
27 | all
28 | runtime; build; native; contentfiles; analyzers; buildtransitive
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/Routine/Routine.APi/Services/CompanyRepository.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using Routine.APi.Data;
3 | using Routine.APi.DtoParameters;
4 | using Routine.APi.Entities;
5 | using Routine.APi.Helpers;
6 | using Routine.APi.Models;
7 | using System;
8 | using System.Collections.Generic;
9 | using System.Linq;
10 | using System.Threading.Tasks;
11 |
12 | namespace Routine.APi.Services
13 | {
14 | ///
15 | /// Company Repository
16 | ///
17 | public class CompanyRepository : ICompanyRepository
18 | {
19 | private readonly RoutineDbContext _context;
20 | private readonly IPropertyMappingService _propertyMappingService;
21 |
22 | public CompanyRepository(RoutineDbContext context,IPropertyMappingService propertyMappingService)
23 | {
24 | _context = context ?? throw new ArgumentNullException(nameof(context));
25 | _propertyMappingService = propertyMappingService
26 | ?? throw new ArgumentNullException(nameof(propertyMappingService));
27 | }
28 |
29 | public void AddCompany(Company company)
30 | {
31 | if (company == null)
32 | {
33 | throw new ArgumentNullException(nameof(company));
34 | }
35 | company.Id = Guid.NewGuid();
36 | if (company.Employees != null)
37 | {
38 | foreach (var employee in company.Employees)
39 | {
40 | employee.Id = Guid.NewGuid();
41 | }
42 | }
43 | _context.Companies.Add(company);
44 | }
45 |
46 | public void AddEmployee(Guid companyId, Employee employee)
47 | {
48 | if (companyId == Guid.Empty)
49 | {
50 | throw new ArgumentNullException(nameof(companyId));
51 | }
52 | if (employee == null)
53 | {
54 | throw new ArgumentNullException(nameof(employee));
55 | }
56 |
57 | employee.CompanyId = companyId;
58 | _context.Employees.Add(employee);
59 | }
60 |
61 | public async Task CompanyExistsAsync(Guid companyId)
62 | {
63 | if (companyId == Guid.Empty)
64 | {
65 | throw new ArgumentNullException(nameof(companyId));
66 | }
67 |
68 | return await _context.Companies.AnyAsync(x => x.Id == companyId);
69 | }
70 |
71 | public void DeleteCompany(Company company)
72 | {
73 | if (company == null)
74 | {
75 | throw new ArgumentNullException(nameof(company));
76 | }
77 |
78 | _context.Companies.Remove(company);
79 | }
80 |
81 | public void DeleteEmployee(Employee employee)
82 | {
83 | if (employee == null)
84 | {
85 | throw new ArgumentNullException(nameof(employee));
86 | }
87 |
88 | _context.Employees.Remove(employee);
89 | }
90 |
91 | public async Task GetCompanyAsync(Guid companyId)
92 | {
93 | if (companyId == Guid.Empty)
94 | {
95 | throw new ArgumentNullException(nameof(companyId));
96 | }
97 |
98 | return await _context.Companies.FirstOrDefaultAsync(x => x.Id == companyId);
99 | }
100 |
101 | public async Task> GetCompaniesAsync(CompanyDtoParameters parameters)
102 | {
103 | if (parameters == null)
104 | {
105 | throw new ArgumentNullException(nameof(parameters));
106 | }
107 |
108 | var queryExpression = _context.Companies as IQueryable;
109 | //查找指定公司
110 | if (! string.IsNullOrWhiteSpace(parameters.companyName))
111 | {
112 | parameters.companyName = parameters.companyName.Trim();
113 | queryExpression = queryExpression.Where(x => x.Name == parameters.companyName);
114 | }
115 | //模糊搜索
116 | if (! string.IsNullOrWhiteSpace(parameters.SearchTerm))
117 | {
118 | parameters.SearchTerm = parameters.SearchTerm.Trim();
119 | queryExpression = queryExpression.Where(x => x.Name.Contains(parameters.SearchTerm)
120 | || x.Introduction.Contains(parameters.SearchTerm));
121 | }
122 | //排序(视频P38)
123 | if (! string.IsNullOrWhiteSpace(parameters.OrderBy))
124 | {
125 | //取得属性映射关系字典
126 | var mappingDictionary = _propertyMappingService.GetPropertyMapping();
127 | //ApplySort 是一个自己定义的拓展方法
128 | //传入 orderBy 字符串与属性映射关系字典
129 | //返回排序好的 IQueryable 资源集合
130 | queryExpression = queryExpression.ApplySort(parameters.OrderBy, mappingDictionary);
131 | }
132 |
133 | //return await queryExpression.Skip((parameters.PageNumber - 1) * parameters.PageSize)
134 | // .Take(parameters.PageSize)
135 | // .ToListAsync();
136 |
137 | //返回经过翻页处理的 PagedList,PagedList 会执行翻页操作,并保存页码等信息
138 | return await PagedList.CreateAsync(queryExpression, parameters.PageNumber, parameters.PageSize);
139 | }
140 |
141 | public async Task> GetCompaniesAsync(IEnumerable companyIds)
142 | {
143 | if (companyIds == null)
144 | {
145 | throw new ArgumentNullException(nameof(companyIds));
146 | }
147 |
148 | return await _context.Companies
149 | .Where(x => companyIds.Contains(x.Id))
150 | .OrderBy(x => x.Name)
151 | .ToListAsync();
152 | }
153 |
154 | public async Task GetEmployeeAsync(Guid companyId, Guid employeeId)
155 | {
156 | if (companyId == Guid.Empty)
157 | {
158 | throw new ArgumentNullException(nameof(companyId));
159 | }
160 | if (employeeId == Guid.Empty)
161 | {
162 | throw new ArgumentNullException(nameof(employeeId));
163 | }
164 |
165 | return await _context.Employees
166 | .Where(x => x.Id == employeeId && x.CompanyId == companyId)
167 | .FirstOrDefaultAsync();
168 | }
169 |
170 | public async Task> GetEmployeesAsync(Guid companyId, EmployeeDtoParameters parameters)
171 | {
172 | if (companyId == Guid.Empty)
173 | {
174 | throw new ArgumentNullException(nameof(companyId));
175 | }
176 |
177 | var queryExpression = _context.Employees.Where(x => x.CompanyId == companyId);
178 |
179 | //性别筛选
180 | if (! string.IsNullOrWhiteSpace(parameters.GenderDisplay))
181 | {
182 | parameters.GenderDisplay = parameters.GenderDisplay.Trim();
183 | var gender = Enum.Parse(parameters.GenderDisplay);
184 | queryExpression = queryExpression.Where(x => x.Gender == gender);
185 | }
186 | //查询
187 | if (! string.IsNullOrWhiteSpace(parameters.Q))
188 | {
189 | parameters.Q = parameters.Q.Trim();
190 | queryExpression = queryExpression.Where(x => x.EmployeeNo.Contains(parameters.Q)
191 | || x.FirstName.Contains(parameters.Q)
192 | || x.LastName.Contains(parameters.Q));
193 | }
194 | //排序(视频P36-P37)
195 | if (!string.IsNullOrWhiteSpace(parameters.OrderBy))
196 | {
197 | //取得映射关系字典
198 | var mappingDictionary = _propertyMappingService.GetPropertyMapping();
199 | //ApplySort 是一个自己定义的拓展方法
200 | //传入 FormQuery 中的 OrderBy 字符串与映射关系字典
201 | //返回排序好的字符串
202 | queryExpression = queryExpression.ApplySort(parameters.OrderBy, mappingDictionary);
203 | }
204 |
205 | return await queryExpression.ToListAsync();
206 | }
207 |
208 | public void UpdateCompany(Company company)
209 | {
210 | //使用 EF,无需显式地声明
211 | //_context.Entry(company).State = EntityState.Modified;
212 | }
213 |
214 | public void UpdateEmployee(Employee employee)
215 | {
216 | //使用 EF,无需显式地声明
217 | //_context.Entry(employee).State = EntityState.Modified;
218 | }
219 |
220 | public async Task SaveAsync()
221 | {
222 | return await _context.SaveChangesAsync() >= 0;
223 | }
224 | }
225 | }
226 |
--------------------------------------------------------------------------------
/Routine/Routine.APi/Services/ICompanyRepository.cs:
--------------------------------------------------------------------------------
1 | using Routine.APi.DtoParameters;
2 | using Routine.APi.Entities;
3 | using Routine.APi.Helpers;
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Threading.Tasks;
7 |
8 | namespace Routine.APi.Services
9 | {
10 | ///
11 | /// Company Repository 的接口
12 | ///
13 | public interface ICompanyRepository
14 | {
15 | Task> GetCompaniesAsync(CompanyDtoParameters parameters);
16 | Task GetCompanyAsync(Guid companyId);
17 | Task> GetCompaniesAsync(IEnumerable companyIds);
18 | void AddCompany(Company company);
19 | void UpdateCompany(Company company);
20 | void DeleteCompany(Company company);
21 | Task CompanyExistsAsync(Guid companyId);
22 | Task> GetEmployeesAsync(Guid companyId, EmployeeDtoParameters parameters);
23 | Task GetEmployeeAsync(Guid companyId, Guid employeeId);
24 | void AddEmployee(Guid companyId, Employee employee);
25 | void UpdateEmployee(Employee employee);
26 | void DeleteEmployee(Employee employee);
27 | Task SaveAsync();
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Routine/Routine.APi/Services/IPropertyCheckerService.cs:
--------------------------------------------------------------------------------
1 | namespace Routine.APi.Services
2 | {
3 | ///
4 | /// PropertyCheckerService 的接口,用于判断 Uri Query 中的 fields 字符串是否合法(视频P39)
5 | ///
6 | public interface IPropertyCheckerService
7 | {
8 | bool TypeHasProperties(string fields);
9 | }
10 | }
--------------------------------------------------------------------------------
/Routine/Routine.APi/Services/IPropertyMapping.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 |
6 | namespace Routine.APi.Services
7 | {
8 | ///
9 | /// PropertyMappin 的接口(视频P37)
10 | ///
11 | public interface IPropertyMapping
12 | {
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Routine/Routine.APi/Services/IPropertyMappingService.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace Routine.APi.Services
4 | {
5 | ///
6 | /// PropertyMappingService 的接口
7 | ///
8 | public interface IPropertyMappingService
9 | {
10 | Dictionary GetPropertyMapping();
11 | bool ValidMappingExistsFor(string fields);
12 | }
13 | }
--------------------------------------------------------------------------------
/Routine/Routine.APi/Services/PropertyCheckerService.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 |
3 | namespace Routine.APi.Services
4 | {
5 | public class PropertyCheckerService : IPropertyCheckerService
6 | {
7 | ///
8 | /// 判断 Uri query parameters 中的 fields 是否合法(视频P39)
9 | ///
10 | /// 待返回的资源类型
11 | /// Uri Query 中的 fields 字符串,大小写不敏感,允许为 null
12 | /// fields 字符串是否合法
13 | public bool TypeHasProperties(string fields)
14 | {
15 | if (string.IsNullOrWhiteSpace(fields))
16 | {
17 | return true;
18 | }
19 |
20 | var fieldsAfterSplit = fields.Split(",");
21 | foreach (var field in fieldsAfterSplit)
22 | {
23 | var propertyName = field.Trim();
24 | var propertyInfo = typeof(T).GetProperty(propertyName,
25 | BindingFlags.IgnoreCase //大小写不敏感
26 | | BindingFlags.Public
27 | | BindingFlags.Instance);
28 | if (propertyInfo == null)
29 | {
30 | return false;
31 | }
32 | }
33 |
34 | return true;
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Routine/Routine.APi/Services/PropertyMapping.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace Routine.APi.Services
5 | {
6 | ///
7 | /// 指明 TSource 与 TDestination 的属性映射关系字典,用于集合资源的排序(视频P37)
8 | ///
9 | /// 源类型
10 | /// 目标类型
11 | public class PropertyMapping : IPropertyMapping
12 | {
13 | ///
14 | /// 属性映射关系字典
15 | ///
16 | public Dictionary MappingDictionary { get; private set; }
17 | public PropertyMapping(Dictionary mappingDictionary)
18 | {
19 | MappingDictionary = mappingDictionary ?? throw new ArgumentNullException(nameof(mappingDictionary));
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Routine/Routine.APi/Services/PropertyMappingService.cs:
--------------------------------------------------------------------------------
1 | using Routine.APi.Entities;
2 | using Routine.APi.Models;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using System.Threading.Tasks;
7 |
8 | namespace Routine.APi.Services
9 | {
10 | ///
11 | /// 对集合资源进行排序时的属性映射服务(视频P37)
12 | ///
13 | public class PropertyMappingService : IPropertyMappingService
14 | {
15 | //定义属性映射关系字典
16 |
17 | //Company Friendly Dto 的属性映射关系字典
18 | //GetCompanies 时,无论请求的是 Full Dto 还是 Friendly Dto,都允许按照 Full Dto 中的属性进行排序,所以这个字典用不上了
19 | //private readonly Dictionary _companyPropertyMapping
20 | // = new Dictionary(StringComparer.OrdinalIgnoreCase) //Key 大小写不敏感
21 | // {
22 | // {"Id",new PropertyMappingValue(new List{"Id"}) },
23 | // {"Name",new PropertyMappingValue(new List{"Name"}) }
24 | // };
25 |
26 | //Company Full Dto 的属性映射关系字典
27 | private readonly Dictionary _companyFullPropertyMapping
28 | = new Dictionary(StringComparer.OrdinalIgnoreCase) //Key 大小写不敏感
29 | {
30 | {"Id",new PropertyMappingValue(new List{"Id"}) },
31 | {"Name",new PropertyMappingValue(new List{"Name"}) },
32 | {"Country",new PropertyMappingValue(new List{"Country"}) },
33 | {"Industry",new PropertyMappingValue(new List{"Industry"}) },
34 | {"Product",new PropertyMappingValue(new List{"Product"}) },
35 | {"Introduction",new PropertyMappingValue(new List{"Introduction"}) },
36 | {"BankruptTime",new PropertyMappingValue(new List{"BankruptTime"}) },
37 | };
38 |
39 | //Employee Dto 的属性映射关系字典
40 | private readonly Dictionary _employeeDtoPropertyMapping
41 | = new Dictionary(StringComparer.OrdinalIgnoreCase) //Key 大小写不敏感
42 | {
43 | {"Id",new PropertyMappingValue(new List{"Id"}) },
44 | {"CompanyId",new PropertyMappingValue(new List{"CompanyId"}) },
45 | {"EmployeeNo",new PropertyMappingValue(new List{"EmployeeNo"}) },
46 | {"Name",new PropertyMappingValue(new List{"FirstName","LastName"}) },//"Name" 对应 FirstName 与 LastName 两个属性
47 | {"GenderDisplay",new PropertyMappingValue(new List{"Gender"}) },
48 | {"Age",new PropertyMappingValue(new List{"DateOfBirth"}, true) } //"Age" 对应 DateOfBirth 属性,并且要翻转顺序
49 | };
50 |
51 |
52 | //因为不能在 IList 中直接使用泛型了,无法解析 IList>
53 | //所有需要定义一个接口 IPropertyMapping,让 PropertyMapping 实现这个接口
54 | //然后使用 IList 来实现
55 | ///
56 | /// “指明 TSource 与 TDestination 的属性映射关系字典”的列表
57 | ///
58 | private IList _propertyMappings = new List();
59 |
60 | public PropertyMappingService()
61 | {
62 | //向列表中添加属性映射关系字典,同时指明该字典对应的源类型与目标类型
63 | //即向列表中添加“指明 TSource 与 TDestination 的属性映射关系字典”
64 | //_propertyMappings.Add(new PropertyMapping(_companyPropertyMapping));
65 | _propertyMappings.Add(new PropertyMapping(_companyFullPropertyMapping));
66 | _propertyMappings.Add(new PropertyMapping(_employeeDtoPropertyMapping));
67 | }
68 |
69 | ///
70 | /// 如果 TSource 与 TDestination 存在映射关系,返回属性映射关系字典
71 | ///
72 | /// 源类型
73 | /// 目标类型
74 | /// 从源类型到目标类型的属性映射关系
75 | public Dictionary GetPropertyMapping()
76 | {
77 | var matchingMapping = _propertyMappings.OfType>();
78 | var propertyMapping = matchingMapping.ToList();
79 | if (propertyMapping.Count == 1)
80 | {
81 | //如果 TSource 与 TDestination 存在映射关系,返回对应的属性映射关系字典
82 | return propertyMapping.First().MappingDictionary;
83 | }
84 | throw new Exception($"无法找到唯一的映射关系:{typeof(TSource)},{typeof(TDestination)}");
85 | }
86 |
87 | ///
88 | /// 判断 Uri query parameters 中的 orderBy 是否合法(视频P38)
89 | ///
90 | /// 源类型
91 | /// 目标类型
92 | /// Uri Query 中的 orderBy 字符串,大小写不敏感
93 | /// orderBy 字符串是否合法
94 | public bool ValidMappingExistsFor(string orderBy)
95 | {
96 | var propertyMapping = GetPropertyMapping();
97 | if (string.IsNullOrWhiteSpace(orderBy))
98 | {
99 | return true;
100 | }
101 | var fieldAfterSplit = orderBy.Split(",");
102 | foreach(var field in fieldAfterSplit)
103 | {
104 | var trimedField = field.Trim();
105 | var indexOfFirstSpace = trimedField.IndexOf(" ");
106 | var propertyName = indexOfFirstSpace == -1 ? trimedField : trimedField.Remove(indexOfFirstSpace);
107 | if (!propertyMapping.ContainsKey(propertyName))
108 | {
109 | return false;
110 | }
111 | }
112 | return true;
113 | }
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/Routine/Routine.APi/Services/PropertyMappingValue.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace Routine.APi.Services
5 | {
6 | ///
7 | /// 单条属性映射关系,用于集合资源的排序(视频P37)
8 | ///
9 | public class PropertyMappingValue
10 | {
11 | public IEnumerable DestinationProperties { get; set; }
12 |
13 | ///
14 | /// 顺序是否需要翻转
15 | ///
16 | public bool Revert { get; set; }
17 |
18 | public PropertyMappingValue(IEnumerable destinaryProperties, bool revert = false)
19 | {
20 | DestinationProperties = destinaryProperties ?? throw new ArgumentNullException(nameof(destinaryProperties));
21 | Revert = revert;
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Routine/Routine.APi/Startup.cs:
--------------------------------------------------------------------------------
1 | using AutoMapper;
2 | using Microsoft.AspNetCore.Builder;
3 | using Microsoft.AspNetCore.Hosting;
4 | using Microsoft.AspNetCore.Http;
5 | using Microsoft.AspNetCore.Mvc;
6 | using Microsoft.AspNetCore.Mvc.Formatters;
7 | using Microsoft.EntityFrameworkCore;
8 | using Microsoft.Extensions.Configuration;
9 | using Microsoft.Extensions.DependencyInjection;
10 | using Microsoft.Extensions.Hosting;
11 | using Newtonsoft.Json.Serialization;
12 | using Routine.APi.Data;
13 | using Routine.APi.Services;
14 | using System;
15 | using System.Linq;
16 |
17 | namespace Routine.APi
18 | {
19 | public class Startup
20 | {
21 | public Startup(IConfiguration configuration)
22 | {
23 | Configuration = configuration;
24 | }
25 |
26 | public IConfiguration Configuration { get; }
27 |
28 | // 注册服务 This method gets called by the runtime. Use this method to add services to the container.
29 | public void ConfigureServices(IServiceCollection services)
30 | {
31 | //添加缓存服务(视频P46)
32 | services.AddResponseCaching();
33 |
34 | //支持高级 CacheHeaders,并进行全局配置(视频P48)
35 | services.AddHttpCacheHeaders(expires =>
36 | {
37 | expires.MaxAge = 60;
38 | expires.CacheLocation = Marvin.Cache.Headers.CacheLocation.Private;
39 | }, validation =>
40 | {
41 | //如果响应过期,必须重新验证
42 | validation.MustRevalidate = true;
43 | });
44 |
45 | /*
46 | * 内容协商:
47 | * 针对一个响应,当有多种表述格式的时候,选取最佳的一个表述,例如 application/json、application/xml
48 | *
49 | * Accept Header 指明服务器输出格式,对应 ASP.NET Core 里的 Output Formatters
50 | * 如果服务器不支持客户端请求的媒体类型(Media Type),返回状态码406
51 | *
52 | * Content-Type Header 指明服务器输入格式,对应 ASP.NET Core 里的 Input Formatters
53 | */
54 |
55 | /*
56 | * .Net Core 默认使用 Problem details for HTTP APIs RFC (7807) 标准
57 | * - 为所需错误信息的应用,定义了通用的错误格式
58 | * - 可以识别问题属于哪个 API
59 | */
60 |
61 | //以下是一种添加序列化工具的写法,在本项目中未采用(视频P8)
62 | //services.AddControllers(options =>
63 | //{
64 | // //OutputFormatters 默认有且只有 Json 格式
65 | // //添加对输出 XML 格式的支持
66 | // //此时默认输出格式依然是 Json ,因为 Json 格式位于第一位置
67 | // options.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter());
68 | // //如果在 Index 0 位置插入对 XML 格式的支持,那么默认输出格式是 XML
69 | // //options.OutputFormatters.Insert(0, new XmlDataContractSerializerOutputFormatter());
70 | //});
71 |
72 | services.AddControllers(options =>
73 | {
74 | //启用406状态码(视频P7)
75 | options.ReturnHttpNotAcceptable = true;
76 |
77 | //配置缓存字典(视频P46)
78 | //options.CacheProfiles.Add("120sCacheProfile", new CacheProfile
79 | //{
80 | // Duration = 120
81 | //});
82 | })
83 | //默认格式取决于序列化工具的添加顺序
84 | .AddNewtonsoftJson(options => //第三方 JSON 序列化和反序列化工具(会替换掉原本默认的 JSON 序列化工具)(视频P32)
85 | {
86 | options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
87 | })
88 | .AddXmlDataContractSerializerFormatters() //XML 序列化和反序列化工具(视频P8)
89 | .ConfigureApiBehaviorOptions(options => //自定义错误报告(视频P29)
90 | {
91 | //创建一个委托 context,在 IsValid == false 时执行
92 | options.InvalidModelStateResponseFactory = context =>
93 | {
94 | var problemDetails = new ValidationProblemDetails(context.ModelState)
95 | {
96 | Type = "https://www.bilibili.com/video/av77957694",
97 | Title = "出现错误",
98 | Status = StatusCodes.Status422UnprocessableEntity,
99 | Detail = "请看详细信息",
100 | Instance = context.HttpContext.Request.Path
101 | };
102 | problemDetails.Extensions.Add("traceId", context.HttpContext.TraceIdentifier);
103 | return new UnprocessableEntityObjectResult(problemDetails)
104 | {
105 | ContentTypes = { "application/problem+json" }
106 | };
107 | };
108 | });
109 |
110 | services.Configure(options =>
111 | {
112 | //全局设置 NewtonsoftJsonOutputFormatter(视频P43)
113 | var newtonSoftJsonOutputFormatter = options.OutputFormatters
114 | .OfType()
115 | ?.FirstOrDefault();
116 | if (newtonSoftJsonOutputFormatter != null)
117 | {
118 | //将 NewtonsoftJsonOutputFormatter 设为 "application/vnd.company.hateoas+json" 等 Media type 的输出格式化器
119 | newtonSoftJsonOutputFormatter.SupportedMediaTypes.Add("application/vnd.company.hateoas+json");
120 | newtonSoftJsonOutputFormatter.SupportedMediaTypes.Add("application/vnd.company.friendly+json");
121 | newtonSoftJsonOutputFormatter.SupportedMediaTypes.Add("application/vnd.company.friendly.hateoas+json");
122 | newtonSoftJsonOutputFormatter.SupportedMediaTypes.Add("application/vnd.company.full+json");
123 | newtonSoftJsonOutputFormatter.SupportedMediaTypes.Add("application/vnd.company.full.hateoas+json");
124 | }
125 | });
126 |
127 | //使用 AutoMapper,扫描当前应用域的所有 Assemblies 寻找 AutoMapper 的配置文件(视频P12)
128 | services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
129 |
130 | //AddScoped 针对每一次 HTTP 请求都会建立一个新的实例(视频P1)
131 | services.AddScoped();
132 |
133 | services.AddDbContext(options =>
134 | {
135 | options.UseSqlServer("Data Source=localhost;DataBase=routine;Integrated Security=SSPI");
136 | });
137 |
138 | //轻量服务可以使用 Transient,每次从容器(IServiceProvider)中获取的都是一个新的实例
139 | //深入理解依赖注入、Singleton、Scoped、Transient 参见 https://www.cnblogs.com/gdsblog/p/8465101.html
140 | //排序使用的属性映射服务(视频P37)
141 | services.AddTransient();
142 | //判断 Uri query 字符串中的 fields 是否合法(视频P39)
143 | services.AddTransient();
144 | }
145 |
146 | //路由中间件 This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
147 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
148 | {
149 | //添加中间件的顺序非常重要,如果你把授权中间件放在了Controller的后边,
150 | //那么即使需要授权,那么请求也会先到达Controller并执行里面的代码,这样的话授权就没有意义了。(视频P1)
151 |
152 | if (env.IsDevelopment())
153 | {
154 | app.UseDeveloperExceptionPage();
155 | }
156 | else
157 | {
158 | //500 错误信息
159 | app.UseExceptionHandler(appBuilder =>
160 | {
161 | appBuilder.Run(async context =>
162 | {
163 | context.Response.StatusCode = 500;
164 | await context.Response.WriteAsync("Unexpected Error!");
165 | });
166 | });
167 | }
168 |
169 | //缓存中间件
170 | //app.UseResponseCaching(); //(视频P46)
171 | app.UseHttpCacheHeaders(); //(视频P48)
172 |
173 | app.UseRouting();
174 |
175 | app.UseAuthorization();
176 |
177 | app.UseEndpoints(endpoints =>
178 | {
179 | endpoints.MapControllers();
180 | });
181 | }
182 | }
183 | }
184 |
--------------------------------------------------------------------------------
/Routine/Routine.APi/ValidationAttributes/EmployeeNoMustDifferentFromFirstNameAttribute.cs:
--------------------------------------------------------------------------------
1 | using Routine.APi.Models;
2 | using System.ComponentModel.DataAnnotations;
3 |
4 | namespace Routine.APi.ValidationAttributes
5 | {
6 | ///
7 | /// 自定义验证 Attribute(视频P28)
8 | ///
9 | public class EmployeeNoMustDifferentFromFirstNameAttribute : ValidationAttribute
10 | {
11 | protected override ValidationResult IsValid(object value, ValidationContext validationContext)
12 | {
13 | //当自定义验证 Attribute 作用于属性时 value 是属性值,作用于类时 value 是类对象
14 | //而 validationContext 始终是类对象
15 | //var employeeAddDto = (EmployeeAddDto)value;
16 | var employeeAddDto = (EmployeeAddOrUpdateDto)validationContext.ObjectInstance;
17 |
18 | //EmployeeNo 不能与 FirstName 相等
19 | if (employeeAddDto.EmployeeNo == employeeAddDto.FirstName)
20 | {
21 | return new ValidationResult(ErrorMessage, new[] { nameof(EmployeeAddOrUpdateDto) });
22 | }
23 |
24 | return ValidationResult.Success;
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Routine/Routine.APi/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/Routine/Routine.APi/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | },
9 | "AllowedHosts": "*"
10 | }
11 |
--------------------------------------------------------------------------------
/Routine/Routine.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.29609.76
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Routine.APi", "Routine.APi\Routine.APi.csproj", "{A2858339-9E52-40F8-89FD-78AB5547C345}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Release|Any CPU = Release|Any CPU
12 | EndGlobalSection
13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
14 | {A2858339-9E52-40F8-89FD-78AB5547C345}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {A2858339-9E52-40F8-89FD-78AB5547C345}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 | {A2858339-9E52-40F8-89FD-78AB5547C345}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 | {A2858339-9E52-40F8-89FD-78AB5547C345}.Release|Any CPU.Build.0 = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(SolutionProperties) = preSolution
20 | HideSolutionNode = FALSE
21 | EndGlobalSection
22 | GlobalSection(ExtensibilityGlobals) = postSolution
23 | SolutionGuid = {4ACAE6E7-88C3-486B-9894-925E4B3B9C12}
24 | EndGlobalSection
25 | EndGlobal
26 |
--------------------------------------------------------------------------------
/cover.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Surbowl/ASP.NET-Core-RESTful-Note/e9a986ffbb98e45cea70e51fea42fa7a21736243/cover.jpg
--------------------------------------------------------------------------------