【问题标题】:Accept x-www-form-urlencoded in Asp .net core Web Api在 Asp .net core Web Api 中接受 x-www-form-urlencoded
【发布时间】:2019-05-18 10:48:43
【问题描述】:

我有一个 .Net Core(2.1) Web API,它必须适应现有的 .Net framework(4.6.2) 系统,并且现有系统发送 Api 接受的请求。

这就是问题所在。在.Net框架系统中,调用api是这样的:

var request = (HttpWebRequest)WebRequest.Create("http://xxx.xxx/CloudApi/RegionsList");
request.KeepAlive = true;
request.Method = "POST";
request.ContentType = "application/x-www-form-urlencoded";
request.Accept = "*/*";

var data = new Person()
{
    Name = "Alex",
    Age = 40
};
byte[] dataBuffer;

using (MemoryStream ms = new MemoryStream())
{
    IFormatter formatter = new BinaryFormatter(); formatter.Serialize(ms, data);
    dataBuffer = ms.GetBuffer();
}

request.ContentLength = dataBuffer.Length;
Stream requestStream = request.GetRequestStream();
requestStream.Write(dataBuffer, 0, dataBuffer.Length);
requestStream.Close();

try
{
     var response = (HttpWebResponse)request.GetResponse();
     Console.WriteLine("OK");
}
catch (Exception exp)
{
     Console.WriteLine(exp.Message);
}

这里是api控制器代码:

[Route("cloudapi")]
public class LegacyController : ControllerBase
{
    [HttpPost]
    [Route("regionslist")]
    public dynamic RegionsList([FromBody]byte[] value)
    {
        return value.Length;
    }
}

人物类:

[Serializable]
public class Person
{
    public string Name { get; set; }

    public int Age { get; set; }
}

根据这篇文章:Accepting Raw Request Body Content in ASP.NET Core API Controllers

我已经制作了一个自定义的 InputFormatter 来处理这种情况:

public class RawRequestBodyFormatter : IInputFormatter
{
    public RawRequestBodyFormatter()
    {

    }

    public bool CanRead(InputFormatterContext context)
    {
        if (context == null) throw new ArgumentNullException("argument is Null");
        var contentType = context.HttpContext.Request.ContentType;
        if (contentType == "application/x-www-form-urlencoded")
            return true;
        return false;
    }

    public async Task<InputFormatterResult> ReadAsync(InputFormatterContext context)
    {
        var request = context.HttpContext.Request;
        var contentType = context.HttpContext.Request.ContentType;
        if (contentType == "application/x-www-form-urlencoded")
        {
            using (StreamReader reader = new StreamReader(request.Body, Encoding.UTF8))
            {
                using (var ms = new MemoryStream(2048))
                {
                    await request.Body.CopyToAsync(ms);
                    var content = ms.ToArray();

                    return await InputFormatterResult.SuccessAsync(content);
                }
            }
        }
        return await InputFormatterResult.FailureAsync();
    }
}

但是我发现我发送的数据(Person类实例)不在request.Body中,而是在request.Form中,无法反序列化Form。

非常感谢任何帮助。

【问题讨论】:

  • 我想知道你为什么需要阅读原始的byte[]
  • @itminus 需要转换成我无法更改的现有客户端(旧的.Net框架系统)通过http请求传递的对象。

标签: c# asp.net-mvc asp.net-web-api asp.net-core


【解决方案1】:

我知道有一个已经接受的响应,但我想出了一种解析请求的方法。表单数据并将内容重建为原始请求。正文格式:

public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
{
    var request = context.HttpContext.Request;
    var contentType = request.ContentType;
    if (contentType.StartsWith("application/x-www-form-urlencoded")) // in case it ends with ";charset=UTF-8"
    {
        var content = string.Empty;
        foreach (var key in request.Form.Keys)
        {
            if (request.Form.TryGetValue(key, out var value))
            {
                content += $"{key}={value}&";
            }
        }
        content = content.TrimEnd('&');
        return await InputFormatterResult.SuccessAsync(content);
    }
    return await InputFormatterResult.FailureAsync();
}

【讨论】:

    【解决方案2】:
    1. 由于您需要阅读原始的Request.Body,因此最好启用倒带功能。
    2. InputFormatter 在这种情况下是多余的。 InputFormatter 关心内容协商。通常,我们以这种方式使用它:如果客户端发送有效载荷application/json,我们应该做A;如果客户端发送application/xml的有效载荷,我们应该做 B 。但是您的客户(旧系统)只发送x-www-form-urlencoded。除了创建 InputFormatter,您还可以创建一个非常简单的 ModelBinder 来反序列化有效负载。
    3. Hack:您的旧版.Net framework(4.6.2) 系统使用BinaryFormatter 序列化Person 类,而您的.NET Core 网站需要将其反序列化为Person 的对象。通常,这需要您的.NET Core 应用程序和旧版.NET Framework 系统共享相同的Person 程序集。 但是很明显原来的Person是针对.NET Framewrok 4.6.2的,也就是说这个程序集不能被.NET Core引用。一种解决方法是创建一个与Person 共享相同名称的类型,并创建一个SerializationBinder 来绑定一个新类型。

    假设在旧系统的Person 类中是:

    namespace App.Xyz{
    
        [Serializable]
        public class Person
        {
            public string Name { get; set; }
    
            public int Age { get; set; }
        }
    }
    

    您应该在.NET Core 网站中创建相同的类:

    namespace App.Xyz{
    
        [Serializable]
        public class Person
        {
            public string Name { get; set; }
    
            public int Age { get; set; }
        }
    }
    

    注意命名空间也应该保持不变。

    详细说明。

    1. 创建一个Filter,为Request.Body启用Rewind

      public class EnableRewindResourceFilterAttribute : Attribute, IResourceFilter
      {
          public void OnResourceExecuted(ResourceExecutedContext context) { }
      
          public void OnResourceExecuting(ResourceExecutingContext context)
          {
              context.HttpContext.Request.EnableRewind();
          }
      }
      
    2. 现在你可以创建一个ModelBinder

      public class BinaryBytesModelBinder: IModelBinder
      {
          internal class LegacyAssemblySerializationBinder : SerializationBinder 
          {
              public override Type BindToType(string assemblyName, string typeName) {
                  var typeToDeserialize = Assembly.GetEntryAssembly()
                      .GetType(typeName);   // we use the same typename by convention
                  return typeToDeserialize;
              }
          }
      
          public Task BindModelAsync(ModelBindingContext bindingContext)
          {
              if (bindingContext == null) { throw new ArgumentNullException(nameof(bindingContext)); }
              var modelName = bindingContext.BinderModelName?? "LegacyBinaryData";
      
              var req = bindingContext.HttpContext.Request;
              var raw= req.Body;
              if(raw == null){ 
                  bindingContext.ModelState.AddModelError(modelName,"invalid request body stream");
                  return Task.CompletedTask;
              }
              var formatter= new BinaryFormatter();
              formatter.AssemblyFormat = System.Runtime.Serialization.Formatters.FormatterAssemblyStyle.Simple;
              formatter.Binder = new LegacyAssemblySerializationBinder();
              var o = formatter.Deserialize(raw);
              bindingContext.Result = ModelBindingResult.Success(o);
              return Task.CompletedTask;
          }
      }
      
    3. 最后,用Filter 装饰你的动作方法,并使用模型绑定器来检索实例:

      [Route("cloudapi")]
      public class LegacyController : ControllerBase
      {
          [EnableRewindResourceFilter]
          [HttpPost]
          [Route("regionslist")]
          public dynamic RegionsList([ModelBinder(typeof(BinaryBytesModelBinder))] Person person )
          {
              // now we gets the person here
          }
      }
      

    一个演示:


    替代方法:使用InputFormatter(不建议)

    或者如果你确实想使用InputFormatter,你也应该启用倒带:

    [Route("cloudapi")]
    public class LegacyController : ControllerBase
    {
        [HttpPost]
        [EnableRewindResourceFilter]
        [Route("regionslist")]
        public dynamic RegionsList([FromBody] byte[] bytes )
        {
    
            return new JsonResult(bytes);
        }
    }
    

    并配置服务:

    services.AddMvc(o => {
        o.InputFormatters.Insert(0, new RawRequestBodyFormatter());
    });
    

    此外,您应该像在 Model Binder 中那样反序列化 person 对象。

    但要小心性能!

    【讨论】:

      猜你喜欢
      • 2018-08-09
      • 2020-03-11
      • 1970-01-01
      • 2014-05-19
      • 2019-01-03
      • 2012-08-06
      • 2020-01-12
      • 1970-01-01
      • 2020-02-18
      相关资源
      最近更新 更多