Asp.NetCoreWebApi - RESTful Api
1. RESTREST : 具象状态传输(Representational State Transfer,简称REST),是Roy Thomas Fielding博士于2000年在他的博士论文 "Architectural Styles and the Design of Network-based Software Architectures" 中提出来的一种万维网软件架构风格。 符合REST设计风格的Web API称为RESTful API。它从以下三个方面资源进行定义:
PUT和DELETE方法是幂等方法.GET方法是安全方法(不会对服务器端有修改,因此当然也是幂等的). ps 关于幂等方法 : 不像基于SOAP的Web服务,RESTful Web服务并没有“正式”的标准。这是因为REST是一种架构,而SOAP只是一个协议。虽然REST不是一个标准,但大部分RESTful Web服务实现会使用HTTP、URI、JSON和XML等各种标准。 2. 常用http动词括号中是相应的SQL命令.
3. WebApi 在 Asp.NetCore 中的实现这里以用户增删改查为例. 3.1. 创建WebApi项目.参考ASP.NET Core WebAPI 开发-新建WebAPI项目. 注意,本文建立的Asp.NetCore WebApi项目选择.net core版本是2.2,不建议使用其他版本,2.1版本下会遇到依赖文件冲突问题!所以一定要选择2.2版本的.net core. 3.2. 集成Entity Framework Core操作Mysql3.2.1. 安装相关的包(为Xxxx.Infrastructure项目安装)
这里注意一下,Mysql官方的包是 PS: Mysql文档原文:
为Xxxx.Infrastructure项目安装EFCore相关的包: 为Xxxx.Api项目安装 3.2.2. 建立Entity和ContextApiUsernamespace ApiStudy.Core.Entities { using System; public class ApiUser { public Guid Guid { get; set; } public string Name { get; set; } public string Passwd { get; set; } public DateTime RegistrationDate { get; set; } public DateTime Birth { get; set; } public string ProfilePhotoUrl { get; set; } public string PhoneNumber { get; set; } public string Email { get; set; } } } UserContextnamespace ApiStudy.Infrastructure.Database { using ApiStudy.Core.Entities; using Microsoft.EntityFrameworkCore; public class UserContext:DbContext { public UserContext(DbContextOptions<UserContext> options): base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<ApiUser>().HasKey(u => u.Guid); base.OnModelCreating(modelBuilder); } public DbSet<ApiUser> ApiUsers { get; set; } } } 3.2.3. ConfigureService中注入EF服务services.AddDbContext<UserContext>(options => { string connString = "Server=Xxx:xxx:xxx:xxx;Database=Xxxx;Uid=root;Pwd=Xxxxx; "; options.UseMySQL(connString); }); 3.2.4. 迁移数据库
3.2.5. 数据库迁移结果3.2.6. 为数据库创建种子数据
这个时候运行程序会出现异常,打断点看一下异常信息: 可以猜到,Mysql的varbinary(16)放不下C# Guid.NewGuid()方法生成的Guid,所以配置一下数据库Guid字段类型为varchar(256)可以解决问题. 解决方案: protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<ApiUser>().Property(p => p.Guid) .HasColumnType("nvarchar(256)"); modelBuilder.Entity<ApiUser>().HasKey(u => u.Guid); base.OnModelCreating(modelBuilder); } 4. 支持https将所有http请求全部映射到https Startup中: services.AddHttpsRedirection(options => { options.RedirectStatusCode = StatusCodes.Status307TemporaryRedirect; options.HttpsPort = 5001; }); Configure方法使用该中间件: app.UseHttpsRedirection() 5. 支持HSTSConfigureServices方法注册 services.AddHsts(options => { options.Preload = true; options.IncludeSubDomains = true; options.MaxAge = TimeSpan.FromDays(60); options.ExcludedHosts.Add("example.com"); options.ExcludedHosts.Add("www.example.com"); }); Configure方法配置中间件管道 app.UseHsts(); 注意 app.UseHsts() 方法最好放在 app.UseHttps() 方法之后. 6. 使用SerilLog有关日志的微软官方文档 SerilLog github仓库 使用方法: 6.1. 安装nuget包
6.2. 添加代码Program.Main方法中: Log.Logger = new LoggerConfiguration() .MinimumLevel.Debug() .MinimumLevel.Override("Microsoft",LogEventLevel.Information) .Enrich.FromLogContext() .WriteTo.Console() .CreateLogger(); 修改Program.CreateWebHostBuilder(...) public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>() .UseSerilog(); // <-- Add this line; } 6.3. 自行测试7. Asp.NetCore配置文件7.1. 默认配置文件默认 appsettings.json 7.2. 获得配置IConfiguration[“Key:ChildKey”] private static IConfiguration Configuration { get; set; } public StartupDevelopment(IConfiguration config) { Configuration = config; } ... Configuration[“Key:ChildKey”] 8. 自定义一个异常处理,ExceptionHandler8.1. 弄一个类,写一个扩展方法处理异常namespace ApiStudy.Api.Extensions { using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using System; public static class ExceptionHandlingExtensions { public static void UseCustomExceptionHandler(this IApplicationBuilder app,ILoggerFactory loggerFactory) { app.UseExceptionHandler( builder => builder.Run(async context => { context.Response.StatusCode = StatusCodes.Status500InternalServerError; context.Response.ContentType = "application/json"; Exception ex = context.Features.Get<Exception>(); if (!(ex is null)) { ILogger logger = loggerFactory.CreateLogger("ApiStudy.Api.Extensions.ExceptionHandlingExtensions"); logger.LogError(ex,"Error occurred."); } await context.Response.WriteAsync(ex?.Message ?? "Error occurred,but cannot get exception message.For more detail,go to see the log."); })); } } } 8.2. 在Configuration中使用扩展方法// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app,ILoggerFactory loggerFactory) { app.UseCustomExceptionHandler(loggerFactory); //modified code //app.UseDeveloperExceptionPage(); app.UseHsts(); app.UseHttpsRedirection(); app.UseMvc(); //使用默认路由 } 9. 实现数据接口类(Resource),使用AutoMapper在Resource和Entity中映射9.1. 为Entity类创建对应的Resource类ApiUserResourcenamespace ApiStudy.Infrastructure.Resources { using System; public class ApiUserResource { public Guid Guid { get; set; } public string Name { get; set; } //public string Passwd { get; set; } public DateTime RegistrationDate { get; set; } public DateTime Birth { get; set; } public string ProfilePhotoUrl { get; set; } public string PhoneNumber { get; set; } public string Email { get; set; } } } 9.2. 使用 AutoMapper
10. 使用FluentValidationFluentValidation官网 10.1. 安装Nuget包
10.2. 为每一个Resource配置验证器
11. 实现Http Get(翻页,排序)基本的Get实现[HttpGet] public async Task<IActionResult> Get() { IEnumerable<ApiUser> apiUsers = await _apiUserRepository.GetAllApiUsersAsync(); IEnumerable<ApiUserResource> apiUserResources = _mapper.Map<IEnumerable<ApiUser>,IEnumerable<ApiUserResource>>(apiUsers); return Ok(apiUserResources); } [HttpGet("{guid}")] public async Task<IActionResult> Get(string guid) { ApiUser apiUser = await _apiUserRepository.GetApiUserByGuidAsync(Guid.Parse(guid)); if (apiUser is null) return NotFound(); ApiUserResource apiUserResource = _mapper.Map<ApiUser,ApiUserResource>(apiUser); return Ok(apiUserResource); } 11.1. 资源命名11.1.1. 资源应该使用名词,例
11.1.2. 资源命名层次结构
11.2. 内容协商ASP.NET Core支持输出和输入两种格式化器.
12. 翻页12.1. 构造翻页请求参数类QueryParametersnamespace ApiStudy.Core.Entities { using System.Collections.Generic; using System.ComponentModel; using System.Runtime.CompilerServices; public abstract class QueryParameters : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; private const int DefaultPageSize = 10; private const int DefaultMaxPageSize = 100; private int _pageIndex = 1; public virtual int PageIndex { get => _pageIndex; set => SetField(ref _pageIndex,value); } private int _pageSize = DefaultPageSize; public virtual int PageSize { get => _pageSize; set => SetField(ref _pageSize,value); } private int _maxPageSize = DefaultMaxPageSize; public virtual int MaxPageSize { get => _maxPageSize; set => SetField(ref _maxPageSize,value); } public string OrderBy { get; set; } public string Fields { get; set; } protected void SetField<TField>( ref TField field,in TField newValue,[CallerMemberName] string propertyName = null) { if (EqualityComparer<TField>.Default.Equals(field,newValue)) return; field = newValue; if (propertyName == nameof(PageSize) || propertyName == nameof(MaxPageSize)) SetPageSize(); PropertyChanged?.Invoke(this,new PropertyChangedEventArgs(propertyName)); } private void SetPageSize() { if (_maxPageSize <= 0) _maxPageSize = DefaultMaxPageSize; if (_pageSize <= 0) _pageSize = DefaultPageSize; _pageSize = _pageSize > _maxPageSize ? _maxPageSize : _pageSize; } } } ApiUserParametersnamespace ApiStudy.Core.Entities { public class ApiUserParameters:QueryParameters { public string UserName { get; set; } } } 12.2. Repository实现支持翻页请求参数的方法Repository相关代码/*----- ApiUserRepository -----*/ public PaginatedList<ApiUser> GetAllApiUsers(ApiUserParameters parameters) { return new PaginatedList<ApiUser>( parameters.PageIndex,parameters.PageSize,_context.ApiUsers.Count(),_context.ApiUsers.Skip(parameters.PageIndex * parameters.PageSize) .Take(parameters.PageSize)); } public Task<PaginatedList<ApiUser>> GetAllApiUsersAsync(ApiUserParameters parameters) { return Task.Run(() => GetAllApiUsers(parameters)); } /*----- IApiUserRepository -----*/ PaginatedList<ApiUser> GetAllApiUsers(ApiUserParameters parameters); Task<PaginatedList<ApiUser>> GetAllApiUsersAsync(ApiUserParameters parameters); UserController部分代码... [HttpGet(Name = "GetAllApiUsers")] public async Task<IActionResult> GetAllApiUsers(ApiUserParameters parameters) { PaginatedList<ApiUser> apiUsers = await _apiUserRepository.GetAllApiUsersAsync(parameters); IEnumerable<ApiUserResource> apiUserResources = _mapper.Map<IEnumerable<ApiUser>,IEnumerable<ApiUserResource>>(apiUsers); var meta = new { PageIndex = apiUsers.PageIndex,PageSize = apiUsers.PageSize,PageCount = apiUsers.PageCount,TotalItemsCount = apiUsers.TotalItemsCount,NextPageUrl = CreateApiUserUrl(parameters,ResourceUriType.NextPage),PreviousPageUrl = CreateApiUserUrl(parameters,ResourceUriType.PreviousPage) }; Response.Headers.Add( "X-Pagination",JsonConvert.SerializeObject( meta,new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() })); return Ok(apiUserResources); } ... private string CreateApiUserUrl(ApiUserParameters parameters,ResourceUriType uriType) { var param = new ApiUserParameters { PageIndex = parameters.PageIndex,PageSize = parameters.PageSize }; switch (uriType) { case ResourceUriType.PreviousPage: param.PageIndex--; break; case ResourceUriType.NextPage: param.PageIndex++; break; case ResourceUriType.CurrentPage: break; default:break; } return Url.Link("GetAllApiUsers",parameters); } PS注意,为HttpGet方法添加参数的话,在.net core2.2版本下,去掉那个ApiUserController上的 [ApiController());] 特性,否则参数传不进来..net core3.0中据说已经修复这个问题. 12.3. 搜索(过滤)修改Repository代码: public PaginatedList<ApiUser> GetAllApiUsers(ApiUserParameters parameters) { IQueryable<ApiUser> query = _context.ApiUsers.AsQueryable(); query = query.Skip(parameters.PageIndex * parameters.PageSize) .Take(parameters.PageSize); if (!string.IsNullOrEmpty(parameters.UserName)) query = _context.ApiUsers.Where( x => StringComparer.OrdinalIgnoreCase.Compare(x.Name,parameters.UserName) == 0); return new PaginatedList<ApiUser>( parameters.PageIndex,query.Count(),query); } 12.4. 排序12.4.1. 排序思路
思路:
MappedPropertynamespace ApiStudy.Infrastructure.Services { public struct MappedProperty { public MappedProperty(string name,bool revert = false) { Name = name; Revert = revert; } public string Name { get; set; } public bool Revert { get; set; } } } IPropertyMappingnamespace ApiStudy.Infrastructure.Services { using System.Collections.Generic; public interface IPropertyMapping { Dictionary<string,List<MappedProperty>> MappingDictionary { get; } } } PropertyMappingnamespace ApiStudy.Infrastructure.Services { using System.Collections.Generic; public abstract class PropertyMapping<TSource,TDestination> : IPropertyMapping { public Dictionary<string,List<MappedProperty>> MappingDictionary { get; } public PropertyMapping(Dictionary<string,List<MappedProperty>> MappingDict) { MappingDictionary = MappingDict; } } } IPropertyMappingContainernamespace ApiStudy.Infrastructure.Services { public interface IPropertyMappingContainer { void Register<T>() where T : IPropertyMapping,new(); IPropertyMapping Resolve<TSource,TDestination>(); bool ValidateMappingExistsFor<TSource,TDestination>(string fields); } } PropertyMappingContainernamespace ApiStudy.Infrastructure.Services { using System; using System.Linq; using System.Collections.Generic; public class PropertyMappingContainer : IPropertyMappingContainer { protected internal readonly IList<IPropertyMapping> PropertyMappings = new List<IPropertyMapping>(); public void Register<T>() where T : IPropertyMapping,new() { if (PropertyMappings.Any(x => x.GetType() == typeof(T))) return; PropertyMappings.Add(new T()); } public IPropertyMapping Resolve<TSource,TDestination>() { IEnumerable<PropertyMapping<TSource,TDestination>> result = PropertyMappings.OfType<PropertyMapping<TSource,TDestination>>(); if (result.Count() > 0) return result.First(); throw new InvalidCastException( string.Format( "Cannot find property mapping instance for {0},{1}",typeof(TSource),typeof(TDestination))); } public bool ValidateMappingExistsFor<TSource,TDestination>(string fields) { if (string.IsNullOrEmpty(fields)) return true; IPropertyMapping propertyMapping = Resolve<TSource,TDestination>(); string[] splitFields = fields.Split(','); foreach(string property in splitFields) { string trimmedProperty = property.Trim(); int indexOfFirstWhiteSpace = trimmedProperty.IndexOf(' '); string propertyName = indexOfFirstWhiteSpace <= 0 ? trimmedProperty : trimmedProperty.Remove(indexOfFirstWhiteSpace); if (!propertyMapping.MappingDictionary.Keys.Any(x => string.Equals(propertyName,x,StringComparison.OrdinalIgnoreCase))) return false; } return true; } } } QueryExtensionsnamespace ApiStudy.Infrastructure.Extensions { using ApiStudy.Infrastructure.Services; using System; using System.Collections.Generic; using System.Linq; using System.Linq.Dynamic.Core; public static class QueryExtensions { public static IQueryable<T> ApplySort<T>( this IQueryable<T> data,in string orderBy,in IPropertyMapping propertyMapping) { if (data == null) throw new ArgumentNullException(nameof(data)); if (string.IsNullOrEmpty(orderBy)) return data; string[] splitOrderBy = orderBy.Split(','); foreach(string property in splitOrderBy) { string trimmedProperty = property.Trim(); int indexOfFirstSpace = trimmedProperty.IndexOf(' '); bool desc = trimmedProperty.EndsWith(" desc"); string propertyName = indexOfFirstSpace > 0 ? trimmedProperty.Remove(indexOfFirstSpace) : trimmedProperty; propertyName = propertyMapping.MappingDictionary.Keys.FirstOrDefault( x => string.Equals(x,propertyName,StringComparison.OrdinalIgnoreCase)); //ignore case of sort property if (!propertyMapping.MappingDictionary.TryGetValue( propertyName,out List<MappedProperty> mappedProperties)) throw new InvalidCastException($"key mapping for {propertyName} is missing"); mappedProperties.Reverse(); foreach(MappedProperty mappedProperty in mappedProperties) { if (mappedProperty.Revert) desc = !desc; data = data.OrderBy($"{mappedProperty.Name} {(desc ? "descending" : "ascending")} "); } } return data; } } } UserController 部分代码[HttpGet(Name = "GetAllApiUsers")] public async Task<IActionResult> GetAllApiUsers(ApiUserParameters parameters) { if (!_propertyMappingContainer.ValidateMappingExistsFor<ApiUserResource,ApiUser>(parameters.OrderBy)) return BadRequest("can't find fields for sorting."); PaginatedList<ApiUser> apiUsers = await _apiUserRepository.GetAllApiUsersAsync(parameters); IEnumerable<ApiUserResource> apiUserResources = _mapper.Map<IEnumerable<ApiUser>,IEnumerable<ApiUserResource>>(apiUsers); IEnumerable<ApiUserResource> sortedApiUserResources = apiUserResources.AsQueryable().ApplySort( parameters.OrderBy,_propertyMappingContainer.Resolve<ApiUserResource,ApiUser>()); var meta = new { apiUsers.PageIndex,apiUsers.PageSize,apiUsers.PageCount,apiUsers.TotalItemsCount,PreviousPageUrl = apiUsers.HasPreviousPage ? CreateApiUserUrl(parameters,ResourceUriType.PreviousPage) : string.Empty,NextPageUrl = apiUsers.HasNextPage ? CreateApiUserUrl(parameters,ResourceUriType.NextPage) : string.Empty,}; Response.Headers.Add( "X-Pagination",new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() })); return Ok(sortedApiUserResources); } private string CreateApiUserUrl(ApiUserParameters parameters,ResourceUriType uriType) { var param = new { parameters.PageIndex,parameters.PageSize }; switch (uriType) { case ResourceUriType.PreviousPage: param = new { PageIndex = parameters.PageIndex - 1,parameters.PageSize }; break; case ResourceUriType.NextPage: param = new { PageIndex = parameters.PageIndex + 1,parameters.PageSize }; break; case ResourceUriType.CurrentPage: break; default: break; } return Url.Link("GetAllApiUsers",param); } 13. 资源塑形(Resource shaping)返回 资源的指定字段 ApiStudy.Infrastructure.Extensions.TypeExtensionsnamespace ApiStudy.Infrastructure.Extensions { using System; using System.Collections.Generic; using System.Reflection; public static class TypeExtensions { public static IEnumerable<PropertyInfo> GetProeprties(this Type source,string fields = null) { List<PropertyInfo> propertyInfoList = new List<PropertyInfo>(); if (string.IsNullOrEmpty(fields)) { propertyInfoList.AddRange(source.GetProperties(BindingFlags.Public | BindingFlags.Instance)); } else { string[] properties = fields.Trim().Split(','); foreach (string propertyName in properties) { propertyInfoList.Add( source.GetProperty( propertyName.Trim(),BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase)); } } return propertyInfoList; } } } ApiStudy.Infrastructure.Extensions.ObjectExtensionsnamespace ApiStudy.Infrastructure.Extensions { using System.Collections.Generic; using System.Dynamic; using System.Linq; using System.Reflection; public static class ObjectExtensions { public static ExpandoObject ToDynamicObject(this object source,in string fields = null) { List<PropertyInfo> propertyInfoList = source.GetType().GetProeprties(fields).ToList(); ExpandoObject expandoObject = new ExpandoObject(); foreach (PropertyInfo propertyInfo in propertyInfoList) { try { (expandoObject as IDictionary<string,object>).Add( propertyInfo.Name,propertyInfo.GetValue(source)); } catch { continue; } } return expandoObject; } internal static ExpandoObject ToDynamicObject(this object source,in IEnumerable<PropertyInfo> propertyInfos,in string fields = null) { ExpandoObject expandoObject = new ExpandoObject(); foreach (PropertyInfo propertyInfo in propertyInfos) { try { (expandoObject as IDictionary<string,propertyInfo.GetValue(source)); } catch { continue; } } return expandoObject; } } } ApiStudy.Infrastructure.Extensions.IEnumerableExtensionsnamespace ApiStudy.Infrastructure.Extensions { using System; using System.Collections.Generic; using System.Dynamic; using System.Linq; using System.Reflection; public static class IEnumerableExtensions { public static IEnumerable<ExpandoObject> ToDynamicObject<T>( this IEnumerable<T> source,in string fields = null) { if (source == null) throw new ArgumentNullException(nameof(source)); List<ExpandoObject> expandoObejctList = new List<ExpandoObject>(); List<PropertyInfo> propertyInfoList = typeof(T).GetProeprties(fields).ToList(); foreach(T x in source) { expandoObejctList.Add(x.ToDynamicObject(propertyInfoList,fields)); } return expandoObejctList; } } } ApiStudy.Infrastructure.Services.TypeHelperServicesnamespace ApiStudy.Infrastructure.Services { using System.Reflection; public class TypeHelperServices : ITypeHelperServices { public bool HasProperties<T>(string fields) { if (string.IsNullOrEmpty(fields)) return true; string[] splitFields = fields.Split(','); foreach(string splitField in splitFields) { string proeprtyName = splitField.Trim(); PropertyInfo propertyInfo = typeof(T).GetProperty( proeprtyName,BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); if (propertyInfo == null) return false; } return true; } } } UserContext.GetAllApiUsers(),UserContext.Get()[HttpGet(Name = "GetAllApiUsers")] public async Task<IActionResult> GetAllApiUsers(ApiUserParameters parameters) { //added code if (!_typeHelper.HasProperties<ApiUserResource>(parameters.Fields)) return BadRequest("fields not exist."); if (!_propertyMappingContainer.ValidateMappingExistsFor<ApiUserResource,ApiUser>()); //modified code IEnumerable<ExpandoObject> sharpedApiUserResources = sortedApiUserResources.ToDynamicObject(parameters.Fields); var meta = new { apiUsers.PageIndex,new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() })); //modified code return Ok(sharpedApiUserResources); } 配置返回的json名称风格为CamelCase StartupDevelopment.ConfigureServicesservices.AddMvc(options => { options.ReturnHttpNotAcceptable = true; options.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter()); }) .AddJsonOptions(options => { //added code options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); }); 14. HATEOASREST里最复杂的约束,构建成熟RESTAPI的核心
UserContextprivate IEnumerable<LinkResource> CreateLinksForApiUser(string guid,string fields = null) { List<LinkResource> linkResources = new List<LinkResource>(); if (string.IsNullOrEmpty(fields)) { linkResources.Add( new LinkResource(Url.Link("GetApiUser",new { guid }),"self","get")); } else { linkResources.Add( new LinkResource(Url.Link("GetApiUser",new { guid,fields }),"get")); } linkResources.Add( new LinkResource(Url.Link("DeleteApiUser","Get")); return linkResources; } private IEnumerable<LinkResource> CreateLinksForApiUsers(ApiUserParameters parameters,bool hasPrevious,bool hasNext) { List<LinkResource> resources = new List<LinkResource>(); resources.Add( new LinkResource( CreateApiUserUrl(parameters,ResourceUriType.CurrentPage),"current_page","get")); if (hasPrevious) resources.Add( new LinkResource( CreateApiUserUrl(parameters,ResourceUriType.PreviousPage),"previous_page","get")); if (hasNext) resources.Add( new LinkResource( CreateApiUserUrl(parameters,"next_page","get")); return resources; } [HttpGet(Name = "GetAllApiUsers")] public async Task<IActionResult> GetAllApiUsers(ApiUserParameters parameters) { if (!_typeHelper.HasProperties<ApiUserResource>(parameters.Fields)) return BadRequest("fields not exist."); if (!_propertyMappingContainer.ValidateMappingExistsFor<ApiUserResource,ApiUser>()); IEnumerable<ExpandoObject> shapedApiUserResources = sortedApiUserResources.ToDynamicObject(parameters.Fields); IEnumerable<ExpandoObject> shapedApiUserResourcesWithLinks = shapedApiUserResources.Select( x => { IDictionary<string,object> dict = x as IDictionary<string,object>; if(dict.Keys.Contains("guid")) dict.Add("links",CreateLinksForApiUser(dict["guid"] as string)); return dict as ExpandoObject; }); var result = new { value = shapedApiUserResourcesWithLinks,links = CreateLinksForApiUsers(parameters,apiUsers.HasPreviousPage,apiUsers.HasNextPage) }; var meta = new { apiUsers.PageIndex,//PreviousPageUrl = apiUsers.HasPreviousPage ? CreateApiUserUrl(parameters,//NextPageUrl = apiUsers.HasNextPage ? CreateApiUserUrl(parameters,new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() })); return Ok(result); } 14.1. 创建供应商特定媒体类型
14.1.1. 判断Media Type类型
//Startup.ConfigureServices 中注册媒体类型 services.AddMvc(options => { options.ReturnHttpNotAcceptable = true; //options.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter()); JsonOutputFormatter formatter = options.OutputFormatters.OfType<JsonOutputFormatter>().FirstOrDefault(); formatter.SupportedMediaTypes.Add("application/vnd.laggage.hateoas+json"); }) // get方法中判断媒体类型 if (mediaType == "application/json") return Ok(shapedApiUserResources); else if (mediaType == "application/vnd.laggage.hateoas+json") { ... return; } 注意,要是的 Action 认识 application/vnd.laggage.hateoss+json,需要在Startup.ConfigureServices中注册这个媒体类型,上面的代码给出了具体操作. UserContext[HttpGet(Name = "GetAllApiUsers")] public async Task<IActionResult> GetAllApiUsers(ApiUserParameters parameters,[FromHeader(Name = "Accept")] string mediaType) { if (!_typeHelper.HasProperties<ApiUserResource>(parameters.Fields)) return BadRequest("fields not exist."); if (!_propertyMappingContainer.ValidateMappingExistsFor<ApiUserResource,ApiUser>()); IEnumerable<ExpandoObject> shapedApiUserResources = sortedApiUserResources.ToDynamicObject(parameters.Fields); if (mediaType == "application/json") return Ok(shapedApiUserResources); else if (mediaType == "application/vnd.laggage.hateoas+json") { IEnumerable<ExpandoObject> shapedApiUserResourcesWithLinks = shapedApiUserResources.Select( x => { IDictionary<string,object>; if (dict.Keys.Contains("guid")) dict.Add("links",CreateLinksForApiUser(dict["guid"] as string)); return dict as ExpandoObject; }); var result = new { value = shapedApiUserResourcesWithLinks,apiUsers.HasNextPage) }; var meta = new { apiUsers.PageIndex,}; Response.Headers.Add( "X-Pagination",JsonConvert.SerializeObject( meta,new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() })); return Ok(result); } return NotFound($"Can't find resources for the given media type: [{mediaType}]."); } [HttpGet("{guid}",Name = "GetApiUser")] public async Task<IActionResult> Get(string guid,[FromHeader(Name = "Accept")] string mediaType,string fields = null) { if (!_typeHelper.HasProperties<ApiUserResource>(fields)) return BadRequest("fields not exist."); ApiUser apiUser = await _apiUserRepository.GetApiUserByGuidAsync(Guid.Parse(guid)); if (apiUser is null) return NotFound(); ApiUserResource apiUserResource = _mapper.Map<ApiUser,ApiUserResource>(apiUser); ExpandoObject shapedApiUserResource = apiUserResource.ToDynamicObject(fields); if (mediaType == "application/json") return Ok(shapedApiUserResource); else if(mediaType == "application/vnd.laggage.hateoas+json") { IDictionary<string,object> shapedApiUserResourceWithLink = shapedApiUserResource as IDictionary<string,object>; shapedApiUserResourceWithLink.Add("links",CreateLinksForApiUser(guid,fields)); return Ok(shapedApiUserResourceWithLink); } return NotFound(@"Can't find resource for the given media type: [{mediaType}]."); }
RequestHeaderMatchingMediaTypeAttribute[AttributeUsage(AttributeTargets.All,Inherited = true,AllowMultiple = true)] public class RequestHeaderMatchingMediaTypeAttribute : Attribute,IActionConstraint { private readonly string _requestHeaderToMatch; private readonly string[] _mediaTypes; public RequestHeaderMatchingMediaTypeAttribute(string requestHeaderToMatch,string[] mediaTypes) { _requestHeaderToMatch = requestHeaderToMatch; _mediaTypes = mediaTypes; } public bool Accept(ActionConstraintContext context) { var requestHeaders = context.RouteContext.HttpContext.Request.Headers; if (!requestHeaders.ContainsKey(_requestHeaderToMatch)) { return false; } foreach (var mediaType in _mediaTypes) { var mediaTypeMatches = string.Equals(requestHeaders[_requestHeaderToMatch].ToString(),mediaType,StringComparison.OrdinalIgnoreCase); if (mediaTypeMatches) { return true; } } return false; } public int Order { get; } = 0; } UserContext[HttpGet(Name = "GetAllApiUsers")] [RequestHeaderMatchingMediaType("Accept",new string[] { "application/vnd.laggage.hateoas+json" })] public async Task<IActionResult> GetHateoas(ApiUserParameters parameters) { if (!_typeHelper.HasProperties<ApiUserResource>(parameters.Fields)) return BadRequest("fields not exist."); if (!_propertyMappingContainer.ValidateMappingExistsFor<ApiUserResource,ApiUser>()); IEnumerable<ExpandoObject> shapedApiUserResources = sortedApiUserResources.ToDynamicObject(parameters.Fields); IEnumerable<ExpandoObject> shapedApiUserResourcesWithLinks = shapedApiUserResources.Select( x => { IDictionary<string,CreateLinksForApiUser(dict["guid"] as string)); return dict as ExpandoObject; }); var result = new { value = shapedApiUserResourcesWithLinks,new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() })); return Ok(result); } [HttpGet(Name = "GetAllApiUsers")] [RequestHeaderMatchingMediaType("Accept",new string[] { "application/json" })] public async Task<IActionResult> Get(ApiUserParameters parameters) { if (!_typeHelper.HasProperties<ApiUserResource>(parameters.Fields)) return BadRequest("fields not exist."); if (!_propertyMappingContainer.ValidateMappingExistsFor<ApiUserResource,ApiUser>()); IEnumerable<ExpandoObject> shapedApiUserResources = sortedApiUserResources.ToDynamicObject(parameters.Fields); return Ok(shapedApiUserResources); } [HttpGet("{guid}",Name = "GetApiUser")] [RequestHeaderMatchingMediaType("Accept",new string[] { "application/vnd.laggage.hateoas+json" })] public async Task<IActionResult> GetHateoas(string guid,ApiUserResource>(apiUser); ExpandoObject shapedApiUserResource = apiUserResource.ToDynamicObject(fields); IDictionary<string,fields)); return Ok(shapedApiUserResourceWithLink); } [HttpGet("{guid}",new string[] { "application/json" })] public async Task<IActionResult> Get(string guid,ApiUserResource>(apiUser); ExpandoObject shapedApiUserResource = apiUserResource.ToDynamicObject(fields); return Ok(shapedApiUserResource); } (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |
- asp.net – Excel Automation Workbooks.Open失败:“无法获
- asp.net-mvc – ASP.NET MVC – 接口类型上的自定义模型绑定
- asp.net-mvc-3 – MVC3 Webgrid Paging在Jquery对话框中不起
- asp.net – Response.Redirect和线程被中止错误?
- ASP.NET拒绝访问该路径
- asp.net-mvc – 保护抓取工具中的网站内容
- asp.net – Html-Agility-Pack没有加载包含完整内容的页面?
- asp.net – 无法将“ASP._Page__ViewStart_cshtml”类型的对
- asp.net-mvc-4 – GAC问题不能在IIS上托管应用程序
- asp.net-mvc – 如何在.net mvc中使用私有操作方法?
- asp.net-mvc – 如何在ASP.NET MVC中生成完整的t
- asp.net web api定义的c/s调用方法一例
- asp.net-mvc – 为什么MvcApplication.RegisterR
- asp.net-mvc – 在ASP.NET MVC3中有一个无会话控
- asp.net-mvc – ASP.net MVC – FluentValidatio
- asp.net – 无法加载类型’system.data.entity.d
- asp.net – 通过CSS重置HTML元素的高度
- asp.net-mvc-4 – Html.LabelFor始终显示属性名称
- asp.net-mvc – 如何重新安装已经在解决方案的pa
- asp.net-mvc – 从Web窗体转换为MVC