Принять x-www-form-urlencoded в Asp .net core Web Api

У меня есть веб-API .Net Core (2.1), который должен адаптироваться к существующей системе .Net framework (4.6.2), и существующая система отправляет запрос, который принимает Api.

Вот в чем проблема. В системе .Net framework он вызывает 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; }
}

Согласно этой статье: Принятие необработанного содержимого тела запроса в контроллерах API ASP.NET Core

Я создал собственный 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 18.12.2018 05:33

@itminus Необходимо преобразовать в объект, который передан HTTP-запросом, который выполняется завершенным клиентом (старая система .Net framework), который я не могу изменить.

Isaac.S 18.12.2018 05:41
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
4
2
7 991
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий
  1. Поскольку вам нужно читать необработанный Request.Body, лучше включить функцию перемотки.
  2. InputFormatter является излишним для этого сценария. InputFormatter заботится о согласовании контента. Обычно мы используем это так: если клиент отправляет полезную нагрузку application/json, мы должны сделать A; если клиент отправляет полезную нагрузку application/xml, мы должны сделать B. Но ваш клиент (устаревшая система) отправляет только x-www-form-urlencoded. Вместо того, чтобы создавать InputFormatter, вы могли бы создать мертвый простой ModelBinder для десериализации полезной нагрузки.
  3. Взлом: ваша устаревшая система .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 класс Legacy системы:

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, который включает Rewind для Request.Body

    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());
});

Кроме того, вы должны десериализовать объект person таким же образом, как мы это делаем в Model Binder.

Но будьте осторожны с исполнением!

Я знаю, что есть уже принятый ответ, но я придумал способ разобрать запрос. Сформировать данные и перестроить содержимое в исходный запрос. Формат тела:

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();
}

Другие вопросы по теме