<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          ASP.NET Core WebAPI 中處理 Patch 請求

          共 12856字,需瀏覽 26分鐘

           ·

          2023-06-21 21:53

          一、概述

          PUT 和 PATCH 方法用于更新現(xiàn)有資源。它們之間的區(qū)別是,PUT 會(huì)替換整個(gè)資源,而 PATCH 僅指定更改。

          在 ASP.NET Core Web API 中,由于 C# 是一種靜態(tài)語言(dynamic 在此不表),當(dāng)我們定義了一個(gè)類型用于接收 HTTP Patch 請求參數(shù)的時(shí)候,在 Action 中無法直接從實(shí)例中得知客戶端提供了哪些參數(shù)。

          比如定義一個(gè)輸入模型和數(shù)據(jù)庫實(shí)體:

          public class PersonInput
          {
              public string? Name { getset; }

              public int? Age { getset; }

              public string? Gender { getset; }
          }

          public class PersonEntity
          {
              public string Name { getset; }

              public int Age { getset; }

              public string Gender { getset; }
          }

          再定義一個(gè)以 FromForm 形式接收參數(shù)的 Action:

          [HttpPatch]
          [Route("patch")]
          public ActionResult Patch([FromForm] PersonInput input)
          {
              // 測試代碼暫時(shí)將 AutoMapper 配置放在方法內(nèi)。
              var config = new MapperConfiguration(cfg =>
              {
                  cfg.CreateMap<PersonInput, PersonEntity>());
              });
              var mapper = config.CreateMapper();

              // entity 從數(shù)據(jù)庫讀取,這里僅演示。
              var entity = new PersonEntity
              {
                  Name = "姓名"// 可能會(huì)被改變
                  Age = 18// 可能會(huì)被改變
                  Gender = "我可能會(huì)被改變",
              };

              // 如果客戶端只輸入 Name 字段,entity 的 Age 和 Gender 將不能被正確映射或被置為 null。
              mapper.Map(input, entity);

              return Ok();
          }
          curl --location --request PATCH 'http://localhost:5094/test/patch' \
          --form 'Name="foo"'

          如果客戶端只提供了 Name 而沒有其他參數(shù),從 HttpContext.Request.Form.Keys 可以得知這一點(diǎn)。如果不使用 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!;
          }
          // ...

          本文提供一種方式來簡化這個(gè)步驟。

          二、將 Keys 保存在 Input Model 中

          定義一個(gè)名為 PatchInput 的類:

          public abstract class PatchInput
          {
              [BindNever]
              public ICollection<string>? PatchKeys { getset; }
          }

          PatchKeys 屬性不由客戶端提供,不參與默認(rèn)綁定。

          PersonInput 繼承自 PatchInput:

          public class PersonInput : PatchInput
          {
              public string? Name { getset; }

              public int? Age { getset; }

              public string? Gender { getset; }
          }

          三、定義 ModelBinderFactory 和 ModelBinder

          public 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 項(xiàng)目中替換 ModelBinderFactory

          var builder = WebApplication.CreateBuilder(args);

          // Add services to the container.
          builder.Services.AddPatchMapper();

          AddPatchMapper 是一個(gè)簡單的擴(kuò)展方法:

          public static class PatchMapperExtensions
          {
              public static IServiceCollection AddPatchMapper(this IServiceCollection services)
              {
                  services.Replace(ServiceDescriptor.Singleton<IModelBinderFactory, PatchModelBinderFactory>());
                  return services;
              }
          }

          到目前為止,在 Action 中已經(jīng)能獲取到請求的 Key 了。

          [HttpPatch]
          [Route("patch")]
          public ActionResult Patch([FromForm] PersonInput input)
          {
              // 不需要手工給 input.PatchKeys 賦值。
              return Ok();
          }

          PatchKeys 的作用是利用 AutoMapper。

          五、定義 AutoMapper 的 TypeConverter

          public class PatchConverter<T> : ITypeConverter<PatchInputTwhere T : new()
          {
              private static readonly IDictionary<string, Action<T, object>> _propertySetters;

              static PatchConverter()
              {
                  _propertySetters = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public)
                      .ToDictionary(p => p.Name, CreatePropertySetter);
              }

              private static Action<T, objectCreatePropertySetter(PropertyInfo propertyInfo)
              {
                  var targetType = propertyInfo.DeclaringType!;
                  var parameterExpression = Expression.Parameter(typeof(object), "value");
                  var castExpression = Expression.Convert(parameterExpression, propertyInfo.PropertyType);
                  var targetExpression = Expression.Parameter(targetType, "target");
                  var propertyExpression = Expression.Property(targetExpression, propertyInfo);
                  var assignExpression = Expression.Assign(propertyExpression, castExpression);
                  var lambdaExpression = Expression.Lambda<Action<T, object>>(assignExpression, targetExpression, parameterExpression);
                  return lambdaExpression.Compile();
              }

              /// <inheritdoc />
              public T Convert(PatchInput source, T destination, ResolutionContext context)
              {
                  if (destination == null)
                  {
                      destination = new T();
                  }

                  if (source.PatchKeys == null)
                  {
                      return destination;
                  }

                  var sourceType = source.GetType();
                  foreach (var key in source.PatchKeys)
                  {
                      if (_propertySetters.TryGetValue(key, out var propertySetter))
                      {
                          var sourceValue = sourceType.GetProperty(key)?.GetValue(source)!;
                          propertySetter(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 創(chuàng)建 ModelBinder 而不是 ModelBinderProvider 以便于未來支持更多的輸入格式。
              // 3. 目前還沒有支持多級結(jié)構(gòu)。
              // 4. 測試代碼暫時(shí)將 AutoMapper 配置放在方法內(nèi)。

              var config = new MapperConfiguration(cfg =>
              {
                  cfg.CreateMap<PersonInput, PersonEntity>().ConvertUsing(new PatchConverter<PersonEntity>());
              });
              var mapper = config.CreateMapper();

              // PersonEntity 有 3 個(gè)屬性,客戶端如果提供的參數(shù)參數(shù)不足 3 個(gè),在 Map 時(shí)未提供參數(shù)的屬性值不會(huì)被改變。
              var entity = new PersonEntity
              {
                  Name = "姓名",
                  Age = 18,
                  Gender = "如果客戶端沒有提供本參數(shù),那我的值不會(huì)被改變"
              };
              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

          https://github.com/albyho/Tubumu.PatchMapper


          支持 FromForm,即 x-www-form_urlencodedform-data
          支持 FromBodyraw 等。
          支持多級結(jié)構(gòu)。

          轉(zhuǎn)自:alby

          鏈接:cnblogs.com/alby/p/Patch-in-ASP-NET-Core-web-API.html







          回復(fù) 【關(guān)閉】學(xué)永久關(guān)閉App開屏廣告
          回復(fù) 【刪除】學(xué)自動(dòng)檢測那個(gè)微信好友刪除、拉黑
          回復(fù) 【福利】學(xué)查看微粒貸額度獲取20元微信紅包
          回復(fù) 【手冊】獲取3萬字.NET、C#工程師面試手冊
          回復(fù) 【幫助】獲取100+個(gè)常用的C#幫助類庫
          回復(fù) 【加群】加入DotNet學(xué)習(xí)交流群

          瀏覽 28
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  99热最新网站 | 一级毛片日韩 | 色欲影视淫香淫色 | 欧美,操视频 | 麻豆乱婬一区二区三区 |