简体   繁体   中英

How to Provide a Single Web Api Endpoint that takes either Multipart Form data or json Content

I'm building a Web Api, and I'd like to provide a single endpoint per post regardless of whether the client is posting multipart form data or a post using json content.

Edit: The code can process either multipart form data or json. I'm looking to provide a single url to the customer to hit regardless of the content type. Posting a dog should be posting a dog regardless of how.

Edit 2: The only issue was setting the dogDTO parameter to null (DogDTO dog = null). If the controller method signature is like below, it works fine.

public async Task<IHttpActionResult> PostDog(DogDTO dog)

I'll use the dogs as an example. I have a dogDTO:

public class DogDTO
{
  [Key]
  public int DogId {get;set;}
  [Required]
  public string Name {get;set}
  public string Breed {get;set}
  public byte[] FileAboutDog {get;set}
}

I would like the Controller method to look something like this: (pretend error handling and other useful stuff):

[ResponseType(typeof(DogDTO))]
[HttpPost]
[Route("api/Dogs")]
public async Task<IHttpActionResult> PostDog(DogDTO dog = null)
{
  if (Request.Content.IsMimeMultipartContent())
  {
    //Parse the Form Data to build the dog
    return await PersistDogFormData();
  }
  //Use standard json Content
  else return PersistDog(dog);              
}

Using the code above I get the following error (mentioned in this question with no answer):

{"Message":"An error has occurred.","ExceptionMessage":"Optional parameter 'dog' is not supported by 'FormatterParameterBinding'.","ExceptionType":"System.InvalidOperationException","StackTrace":" at System.Web.Http.Controllers.HttpActionBinding.ExecuteBindingAsync(HttpActionContext actionContext, CancellationToken cancellationToken)\\r\\n at System.Web.Http.Controllers.ActionFilterResult.d__2.MoveNext()\\r\\n--- End of stack trace from previous location where exception was thrown ---\\r\\n at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)\\r\\n at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\\r\\n at System.Web.Http.Dispatcher.HttpControllerDispatcher.d__1.MoveNext()"}

Is there another way to provide a single endpoint? I cannot use method overloading because I get ambiguous route runtime errors.

Thank you very much.

You should use a custom MediaTypeFormatter. More information here: http://www.asp.net/web-api/overview/formats-and-model-binding/media-formatters

public class CustomMediaTypeFormatter : MediaTypeFormatter
{
    public CustomMediaTypeFormatter ()
    {
        SupportedMediaTypes.Add(new MediaTypeHeaderValue("multipart/form-data"));
    }

    public override bool CanReadType(Type type)
    {
        return type == typeof (DogDTO);
    }

    public override bool CanWriteType(Type type)
    {
        return false;
    }

    public async override Task<object> ReadFromStreamAsync(
        Type type,
        Stream readStream, 
        HttpContent content, 
        IFormatterLogger formatterLogger)
    {
        var provider = await content.ReadAsMultipartAsync();

        var modelContent = provider.Contents
            .FirstOrDefault(c => c.Headers.ContentDisposition.Name.NormalizeName() == "dog");

        var dogDTO = await modelContent.ReadAsAsync<DogDTO>();

        var fileContent = provider.Contents
            .Where(c => c.Headers.ContentDisposition.Name.NormalizeName() == "image"))
            .FirstOrDefault();

        dogDTO.FileAboutDog = await fileContent.ReadAsByteArrayAsync();

        return dogDTO;

    }
}

public static class StringExtensions
{
    public static string NormalizeName(this string text)
    {
        return text.Replace("\"", "");
    }
}

Register the custom media formatter:

public static void ConfigureApis(HttpConfiguration config)
{
    config.Formatters.Add(new CustomMediaTypeFormatter()); 
}

Update the client code because the JSON serialization needs to ignore the file as it will be in a separate request part:

   public class Dog
    {
      public string Name {get;set}
      public string Breed {get;set}
      [JsonIgnore]
      public byte[] FileAboutDog {get;set}
    }

Example of a POST:

POST http://www.example.com/
Content-Type: multipart/form-data; boundary=-------------------------acebdf13572468


---------------------------acebdf13572468
Content-Type: application/json; charset=utf-8
Content-Disposition: form-data; name=dog

{"name":"DogName", "breed":"DogBreed"}

---------------------------acebdf13572468
Content-Disposition: form-data; name="image"; filename="image.jpg"
Content-Type: image/jpeg

image content
---------------------------acebdf13572468--

The Json part of the request must be named: "dog", the server looks up for the json part by this name. The image part must be named "image". You can drop this restriction by checking the content type instead of looking for the name.

I haven't tested the code, it is possible that you need to make some adjustments.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM