4

I have a .Net Core(2.1) Web API that has to adapt to an existed .Net framework(4.6.2) system, and the existed system send a request that the Api accepts.

Here is the problem. In the .Net framework system, it calls the api like this:

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

Here is the api controller code:

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

Person class:

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

    public int Age { get; set; }
}

According to this article: Accepting Raw Request Body Content in ASP.NET Core API Controllers

I have made a custom InputFormatter to deal with this case:

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

But I found that the data I send(the Person class instance) was not in request.Body but in request.Form, and I can't deserialize it Form.

Any help greatly appreciated.

2
  • I'm wondering why you need read the raw byte[] ? Commented Dec 18, 2018 at 4:33
  • @itminus Need to convert to the object which passed by the http request, which is made by the exsited client(the old .Net framework system) that I can't change. Commented Dec 18, 2018 at 4:41

2 Answers 2

2
  1. Since you need to read the raw Request.Body, it's better to enable rewind feature.
  2. InputFormatter is overkill for this scenario. InputFormatter cares about content negotiation. Typically, we use it in this way : if the client sends a payload of application/json, we shoud do A ; if the client sends a payload of application/xml, we should do B . But your client (legacy system) only sends x-www-form-urlencoded. Rather than creating InputFormatter, you could create a dead simple ModelBinder to deserialize the payload.
  3. Hack: Your legacy .Net framework(4.6.2) system use BinaryFormatter to serialize the Person class, and your .NET Core website needs to deserialize it to an object of Person. Typically, this requires your .NET Core app and the Legacy .NET Framework system share the same Person assembly. But obviously the original Person targets .NET Framewrok 4.6.2, in other words, this assembly cannot be referenced by .NET Core. A walkaround is to create a type that shares the same name of Person, and create a SerializationBinder to bind a new type.

Suppose in your Person class of the Legacy system is :

namespace App.Xyz{

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

        public int Age { get; set; }
    }
}

You should create a same class in your .NET Core WebSite:

namespace App.Xyz{

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

        public int Age { get; set; }
    }
}

Note the namespace should also keep the same.

How to in details.

  1. Create a Filter that enables Rewind for Request.Body

    public class EnableRewindResourceFilterAttribute : Attribute, IResourceFilter
    {
        public void OnResourceExecuted(ResourceExecutedContext context) { }
    
        public void OnResourceExecuting(ResourceExecutingContext context)
        {
            context.HttpContext.Request.EnableRewind();
        }
    }
    
  2. Now you can create a 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. Finally, decorate your action method with the Filter, and use the model binder to retrieve the instance :

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

a demo :

enter image description here


Alternative Approach : Use InputFormatter (not suggested)

Or if you do want to use InputFormatter, you should also enable rewind:

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

        return new JsonResult(bytes);
    }
}

and configure the services :

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

And also, you should deserialize the person object in the same way as we do in Model Binder.

But be careful the performance!

Sign up to request clarification or add additional context in comments.

Comments

2

I know there is an already accepted response, but I came up with a way of parsing the request.Form data and rebuilding the content into an original request.Body format:

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.