如何在?ASP.NET?Core?Web?API?中處理?Patch?請求
PUT 和 PATCH 方法用于更新現有資源。 它們之間的區別是,PUT 會替換整個資源,而 PATCH 僅指定更改。
在 ASP.NET Core Web API 中,由于 C# 是一種靜態語言(dynamic 在此不表),當我們定義了一個類型用于接收 HTTP Patch 請求參數的時候,在 Action 中無法直接從實例中得知客戶端提供了哪些參數。
比如定義一個輸入模型和數據庫實體:
public class PersonInput{ public string? Name { get; set; } public int? Age { get; set; } public string? Gender { get; set; }}public class PersonEntity{ public string Name { get; set; } public int Age { get; set; } public string Gender { get; set; }}再定義一個以 FromForm 形式接收參數的 Action:
[HttpPatch][Route('patch')]public ActionResult Patch([FromForm] PersonInput input){ // 測試代碼暫時將 AutoMapper 配置放在方法內。 var config = new MapperConfiguration(cfg => {cfg.CreateMap<PersonInput, PersonEntity>()); }); var mapper = config.CreateMapper(); // entity 從數據庫讀取,這里僅演示。 var entity = new PersonEntity {Name = '姓名', // 可能會被改變Age = 18, // 可能會被改變Gender = '我可能會被改變', }; // 如果客戶端只輸入 Name 字段,entity 的 Age 和 Gender 將不能被正確映射或被置為 null。 mapper.Map(input, entity); return Ok();}curl --location --request PATCH 'http://localhost:5094/test/patch' \--form 'Name='foo''如果客戶端只提供了 Name 而沒有其他參數,從 HttpContext.Request.Form.Keys 可以得知這一點。如果不使用 AutoMapper,那么接下來是丑陋的判斷:
var keys = _httpContextAccessor.HttpContext.Request.Form.Keys;if(keys.Contains('Name')){ // 更新 Name(這里忽略合法性判斷) entity.Name = input.Name!;}if (keys.Contains('Age')){ // 更新 Age(這里忽略合法性判斷) entity.Age = input.Age!;}// ...本文提供一種方式來簡化這個步驟。
二、將 Keys 保存在 Input Model 中定義一個名為 PatchInput 的類:
public abstract class PatchInput{ [BindNever] public ICollection<string>? PatchKeys { get; set; }}PatchKeys 屬性不由客戶端提供,不參與默認綁定。
PersonInput 繼承自 PatchInput:
public class PersonInput : PatchInput{ public string? Name { get; set; } public int? Age { get; set; } public string? Gender { get; set; }}三、定義 ModelBinderFactory 和 ModelBinderpublic class PatchModelBinder : IModelBinder{ private readonly IModelBinder _internalModelBinder; public PatchModelBinder(IModelBinder internalModelBinder) {_internalModelBinder = internalModelBinder; } public async Task BindModelAsync(ModelBindingContext bindingContext) {await _internalModelBinder.BindModelAsync(bindingContext);if (bindingContext.Model is PatchInput model){ // 將 Form 中的 Keys 保存在 PatchKeys 中 model.PatchKeys = bindingContext.HttpContext.Request.Form.Keys;} }}public class PatchModelBinderFactory : IModelBinderFactory{ private ModelBinderFactory _modelBinderFactory; public PatchModelBinderFactory(IModelMetadataProvider metadataProvider,IOptions<MvcOptions> options,IServiceProvider serviceProvider) {_modelBinderFactory = new ModelBinderFactory(metadataProvider, options, serviceProvider); } public IModelBinder CreateBinder(ModelBinderFactoryContext context) {var modelBinder = _modelBinderFactory.CreateBinder(context);// ComplexObjectModelBinder 是 internal 類if (typeof(PatchInput).IsAssignableFrom(context.Metadata.ModelType) && modelBinder.GetType().ToString().EndsWith('ComplexObjectModelBinder')){ modelBinder = new PatchModelBinder(modelBinder);}return modelBinder; }}四、在 ASP.NET Core 項目中替換 ModelBinderFactoryvar builder = WebApplication.CreateBuilder(args);// Add services to the container.builder.Services.AddPatchMapper();AddPatchMapper 是一個簡單的擴展方法:
public static class PatchMapperExtensions{ public static IServiceCollection AddPatchMapper(this IServiceCollection services) {services.Replace(ServiceDescriptor.Singleton<IModelBinderFactory, PatchModelBinderFactory>());return services; }}到目前為止,在 Action 中已經能獲取到請求的 Key 了。
[HttpPatch][Route('patch')]public ActionResult Patch([FromForm] PersonInput input){ // 不需要手工給 input.PatchKeys 賦值。 return Ok();}PatchKeys 的作用是利用 AutoMapper。
五、定義 AutoMapper 的 TypeConverterpublic class PatchConverter<T> : ITypeConverter<PatchInput, T> where T : new(){ /// <inheritdoc /> public T Convert(PatchInput source, T destination, ResolutionContext context) {destination ??= new T();var sourceType = source.GetType();var destinationType = typeof(T);foreach (var key in source.PatchKeys ?? Enumerable.Empty<string>()){ var sourcePropertyInfo = sourceType.GetProperty(key, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); if (sourcePropertyInfo != null) {var destinationPropertyInfo = destinationType.GetProperty(key, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);if (destinationPropertyInfo != null){ var sourceValue = sourcePropertyInfo.GetValue(source); destinationPropertyInfo.SetValue(destination, sourceValue);} }}return destination; }}上述代碼可用其他手段來代替反射。
六、模型映射[HttpPatch][Route('patch')]public ActionResult Patch([FromForm] PersonInput input){ // 1. 目前僅支持 `FromForm`,即 `x-www-form_urlencoded` 和 `form-data`;暫不支持 `FromBody` 如 `raw` 等。 // 2. 使用 ModelBinderFractory 創建 ModelBinder 而不是 ModelBinderProvider 以便于未來支持更多的輸入格式。 // 3. 目前還沒有支持多級結構。 // 4. 測試代碼暫時將 AutoMapper 配置放在方法內。 var config = new MapperConfiguration(cfg => {cfg.CreateMap<PersonInput, PersonEntity>().ConvertUsing(new PatchConverter<PersonEntity>()); }); var mapper = config.CreateMapper(); // PersonEntity 有 3 個屬性,客戶端如果提供的參數參數不足 3 個,在 Map 時未提供參數的屬性值不會被改變。 var entity = new PersonEntity {Name = '姓名',Age = 18,Gender = '如果客戶端沒有提供本參數,那我的值不會被改變' }; mapper.Map(input, entity); return Ok();}七、測試curl --location --request PATCH 'http://localhost:5094/test/patch' \--form 'Name='foo''或
curl --location --request PATCH 'http://localhost:5094/test/patch' \--header 'Content-Type: application/x-www-form-urlencoded' \--data-urlencode 'Name=foo'源碼Tubumu.PatchMapper
支持 FromForm,即 x-www-form_urlencoded 和 form-data。支持 FromBody 如 raw 等。支持多級結構。參考資料GraphQL.NET
如何在 ASP.NET Core Web API 中處理 JSON Patch 請求
到此這篇關于在 ASP.NET Core Web API 中處理 Patch 請求的文章就介紹到這了,更多相關ASP.NET Core Web API 處理 Patch 請求內容請搜索好吧啦網以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持好吧啦網!