參考 :html
REST : 具象狀態傳輸(Representational State Transfer,簡稱REST),是Roy Thomas Fielding博士於2000年在他的博士論文 "Architectural Styles and the Design of Network-based Software Architectures" 中提出來的一種萬維網軟件架構風格。
目前在三種主流的Web服務實現方案中,由於REST模式與複雜的SOAP和XML-RPC相比更加簡潔,愈來愈多的web服務開始採用REST風格設計和實現。例如,Amazon.com提供接近REST風格的Web服務執行圖書查詢;mysql
符合REST設計風格的Web API稱爲RESTful API。它從如下三個方面資源進行定義:git
PUT和DELETE方法是冪等方法.GET方法是安全方法(不會對服務器端有修改,所以固然也是冪等的).github
ps 關於冪等方法 :
看這篇 理解HTTP冪等性.
簡單說,客戶端屢次請求服務端返回的結果都相同,那麼就說這個操做是冪等的.(我的理解,詳細的看上面給的文章)web
不像基於SOAP的Web服務,RESTful Web服務並無「正式」的標準。這是由於REST是一種架構,而SOAP只是一個協議。雖然REST不是一個標準,但大部分RESTful Web服務實現會使用HTTP、URI、JSON和XML等各類標準。sql
括號中是相應的SQL命令.數據庫
這裏以用戶增刪改查爲例.json
參考ASP.NET Core WebAPI 開發-新建WebAPI項目.api
注意,本文創建的Asp.NetCore WebApi項目選擇.net core版本是2.2,不建議使用其餘版本,2.1版本下會遇到依賴文件衝突問題!因此必定要選擇2.2版本的.net core.安全
這裏注意一下,Mysql官方的包是 MySql.Data.EntityFrameworkCore
,可是這個包有bug,我在github上看到有人說有替代方案 - Pomelo.EntityFrameworkCore.MySql
,通過嘗試,後者比前者好用.全部這裏就選擇後者了.使用前者的話可能會致使數據庫遷移失敗(Update的時候).
PS: Mysql文檔原文:
Install the MySql.Data.EntityFrameworkCore NuGet package.
For EF Core 1.1 only: If you plan to scaffold a database, install the MySql.Data.EntityFrameworkCore.Design NuGet package as well.EFCore - MySql文檔
Mysql版本要求:
Mysql版本要高於5.7
使用最新版本的Mysql Connector(2019 6/27 目前是8.x).
爲Xxxx.Infrastructure項目安裝EFCore相關的包:
爲Xxxx.Api項目安裝 Pomelo.EntityFrameworkCore.MySql
namespace 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; } } }
namespace 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; } } }
services.AddDbContext<UserContext>(options => { string connString = "Server=Xxx:xxx:xxx:xxx;Database=Xxxx;Uid=root;Pwd=Xxxxx; "; options.UseMySQL(connString); });
寫一個建立種子數據的類
namespace ApiStudy.Infrastructure.Database { using ApiStudy.Core.Entities; using Microsoft.Extensions.Logging; using System; using System.Linq; using System.Threading.Tasks; public class UserContextSeed { public static async Task SeedAsync(UserContext context,ILoggerFactory loggerFactory) { try { if (!context.ApiUsers.Any()) { context.ApiUsers.AddRange( new ApiUser { Guid = Guid.NewGuid(), Name = "la", Birth = new DateTime(1998, 11, 29), RegistrationDate = new DateTime(2019, 6, 28), Passwd = "123587", ProfilePhotoUrl = "https://www.laggage.top/", PhoneNumber = "10086", Email = "yu@outlook.com" }, new ApiUser { Guid = Guid.NewGuid(), Name = "David", Birth = new DateTime(1995, 8, 29), RegistrationDate = new DateTime(2019, 3, 28), Passwd = "awt87495987", ProfilePhotoUrl = "https://www.laggage.top/", PhoneNumber = "1008611", Email = "David@outlook.com" }, new ApiUser { Guid = Guid.NewGuid(), Name = "David", Birth = new DateTime(2001, 8, 19), RegistrationDate = new DateTime(2019, 4, 25), Passwd = "awt87495987", ProfilePhotoUrl = "https://www.laggage.top/", PhoneNumber = "1008611", Email = "David@outlook.com" }, new ApiUser { Guid = Guid.NewGuid(), Name = "Linus", Birth = new DateTime(1999, 10, 26), RegistrationDate = new DateTime(2018, 2, 8), Passwd = "awt87495987", ProfilePhotoUrl = "https://www.laggage.top/", PhoneNumber = "17084759987", Email = "Linus@outlook.com" }, new ApiUser { Guid = Guid.NewGuid(), Name = "YouYou", Birth = new DateTime(1992, 1, 26), RegistrationDate = new DateTime(2015, 7, 8), Passwd = "grwe874864987", ProfilePhotoUrl = "https://www.laggage.top/", PhoneNumber = "17084759987", Email = "YouYou@outlook.com" }, new ApiUser { Guid = Guid.NewGuid(), Name = "小白", Birth = new DateTime(1997, 9, 30), RegistrationDate = new DateTime(2018, 11, 28), Passwd = "gewa749864", ProfilePhotoUrl = "https://www.laggage.top/", PhoneNumber = "17084759987", Email = "BaiBai@outlook.com" }); await context.SaveChangesAsync(); } } catch(Exception ex) { ILogger logger = loggerFactory.CreateLogger<UserContextSeed>(); logger.LogError(ex, "Error occurred while seeding database"); } } } }
修改Program.Main方法
IWebHost host = CreateWebHostBuilder(args).Build(); using (IServiceScope scope = host.Services.CreateScope()) { IServiceProvider provider = scope.ServiceProvider; UserContext userContext = provider.GetService<UserContext>(); ILoggerFactory loggerFactory = provider.GetService<ILoggerFactory>(); UserContextSeed.SeedAsync(userContext, loggerFactory).Wait(); } host.Run();
這個時候運行程序會出現異常,打斷點看一下異常信息:Data too long for column 'Guid' at row 1
能夠猜到,Mysql的varbinary(16)放不下C# Guid.NewGuid()方法生成的Guid,因此配置一下數據庫Guid字段類型爲varchar(256)能夠解決問題.
解決方案:
修改 UserContext.OnModelCreating 方法
配置一下 ApiUser.Guid 屬性到Mysql數據庫的映射:
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); }
將全部http請求所有映射到https
Startup中:
ConfigureServices方法註冊,並配置端口和狀態碼等:
services.AddHttpsRedirection(…)
services.AddHttpsRedirection(options => { options.RedirectStatusCode = StatusCodes.Status307TemporaryRedirect; options.HttpsPort = 5001; });
Configure方法使用該中間件:
app.UseHttpsRedirection()
ConfigureServices方法註冊
看官方文檔
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() 方法以後.
有關日誌的微軟官方文檔
SerilLog github倉庫
該github倉庫上有詳細的使用說明.
使用方法:
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; }
默認 appsettings.json
ConfigurationBuilder().AddJsonFile("appsettings.json").Build()-->IConfigurationRoot(IConfiguration)
IConfiguration[「Key:ChildKey」]
針對」ConnectionStrings:xxx」,可使用IConfiguration.GetConnectionString(「xxx」)
private static IConfiguration Configuration { get; set; } public StartupDevelopment(IConfiguration config) { Configuration = config; } ... Configuration[「Key:ChildKey」]
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."); })); } } }
// 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(); //使用默認路由 }
namespace 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; } } }
添加nuget包
AutoMapper
AutoMapper.Extensions.Microsoft.DependencyInjection
配置映射
能夠建立Profile
CreateMap<TSource,TDestination>()
namespace ApiStudy.Api.Extensions { using ApiStudy.Core.Entities; using ApiStudy.Infrastructure.Resources; using AutoMapper; using System; using System.Text; public class MappingProfile : Profile { public MappingProfile() { CreateMap<ApiUser, ApiUserResource>() .ForMember( d => d.Passwd, opt => opt.AddTransform(s => Convert.ToBase64String(Encoding.Default.GetBytes(s)))); CreateMap<ApiUserResource, ApiUser>() .ForMember( d => d.Passwd, opt => opt.AddTransform(s => Encoding.Default.GetString(Convert.FromBase64String(s)))); } } }
注入服務
services.AddAutoMapper()
繼承於AbstractValidator
namespace ApiStudy.Infrastructure.Resources { using FluentValidation; public class ApiUserResourceValidator : AbstractValidator<ApiUserResource> { public ApiUserResourceValidator() { RuleFor(s => s.Name) .MaximumLength(80) .WithName("用戶名") .WithMessage("{PropertyName}的最大長度爲80") .NotEmpty() .WithMessage("{PropertyName}不能爲空!"); } } }
註冊到容器:services.AddTransient<>()
services.AddTransient<IValidator<ApiUserResource>, ApiUserResourceValidator>();
[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); }
api/department/{departmentId}/emoloyees
, 這就表示了 department
(部門)和員工api/department/{departmentId}/emoloyees/{employeeId}
,就表示了該部門下的某個員ASP.NET Core支持輸出和輸入兩種格式化器.
services.AddMvc(options => { options.ReturnHttpNotAcceptable = true; });
options.OutputFormatters.Add(newXmlDataContractSerializerOutputFormatter());
namespace 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; } } }
namespace ApiStudy.Core.Entities { public class ApiUserParameters:QueryParameters { public string UserName { get; set; } } }
/*----- 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);
... [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中聽說已經修復這個問題.
修改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, parameters.PageSize, query.Count(), query); }
思路:
namespace 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; } } }
namespace ApiStudy.Infrastructure.Services { using System.Collections.Generic; public interface IPropertyMapping { Dictionary<string, List<MappedProperty>> MappingDictionary { get; } } }
namespace 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; } } }
namespace ApiStudy.Infrastructure.Services { public interface IPropertyMappingContainer { void Register<T>() where T : IPropertyMapping, new(); IPropertyMapping Resolve<TSource, TDestination>(); bool ValidateMappingExistsFor<TSource, TDestination>(string fields); } }
namespace 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; } } }
namespace 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; } } }
[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", JsonConvert.SerializeObject( meta, 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); }
返回 資源的指定字段
namespace 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; } } }
namespace 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, object>).Add( propertyInfo.Name, propertyInfo.GetValue(source)); } catch { continue; } } return expandoObject; } } }
namespace 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; } } }
namespace 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; } } }
[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>(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>()); //modified code IEnumerable<ExpandoObject> sharpedApiUserResources = sortedApiUserResources.ToDynamicObject(parameters.Fields); 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", JsonConvert.SerializeObject( meta, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() })); //modified code return Ok(sharpedApiUserResources); }
配置返回的json名稱風格爲CamelCase
services.AddMvc(options => { options.ReturnHttpNotAcceptable = true; options.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter()); }) .AddJsonOptions(options => { //added code options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); });
REST裏最複雜的約束,構建成熟RESTAPI的核心
private 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 }), "self", "get")); } linkResources.Add( new LinkResource(Url.Link("DeleteApiUser", new { guid }), "self", "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, ResourceUriType.NextPage), "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>(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>()); 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, 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", JsonConvert.SerializeObject( meta, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() })); return Ok(result); }
//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中註冊這個媒體類型,上面的代碼給出了具體操做.
[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>(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>()); 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> 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, apiUsers.PageSize, apiUsers.PageCount, apiUsers.TotalItemsCount, }; 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}]."); }
[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; }
[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>(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>()); 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, apiUsers.PageSize, apiUsers.PageCount, apiUsers.TotalItemsCount, }; Response.Headers.Add( "X-Pagination", JsonConvert.SerializeObject( meta, 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>(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>()); 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, 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); IDictionary<string, object> shapedApiUserResourceWithLink = shapedApiUserResource as IDictionary<string, object>; shapedApiUserResourceWithLink.Add("links", CreateLinksForApiUser(guid, fields)); return Ok(shapedApiUserResourceWithLink); } [HttpGet("{guid}", Name = "GetApiUser")] [RequestHeaderMatchingMediaType("Accept", new string[] { "application/json" })] public async Task<IActionResult> Get(string guid, 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); return Ok(shapedApiUserResource); }