├── .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 | [![image](https://raw.githubusercontent.com/Surbowl/ASP.NET-Core-RESTful-Note/master/cover.jpg)](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 --------------------------------------------------------------------------------