2012-04-05 36 views
20

Considere un servicio web escrito en ASP.NET Web API para aceptar cualquier número de archivos como una solicitud 'multiparte/mixto'. La estera método de ayuda mirada como sigue (suponiendo _client es una instancia de System.Net.Http.HttpClient):¿Cómo se implementa correctamente un MediaTypeFormatter para manejar las solicitudes de tipo 'multipart/mixed'?

public T Post<T>(string requestUri, T value, params Stream[] streams) 
{ 
    var requestMessage = new HttpRequestMessage(); 
    var objectContent = requestMessage.CreateContent(
     value, 
     MediaTypeHeaderValue.Parse("application/json"), 
     new MediaTypeFormatter[] {new JsonMediaTypeFormatter()}, 
     new FormatterSelector()); 

    var content = new MultipartContent(); 
    content.Add(objectContent); 
    foreach (var stream in streams) 
    { 
     var streamContent = new StreamContent(stream); 
     streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); 
     streamContent.Headers.ContentDisposition = 
      new ContentDispositionHeaderValue("form-data") 
      { 
       Name = "file", 
       FileName = "mystream.doc" 
      }; 
     content.Add(streamContent); 
    } 

    return _httpClient.PostAsync(requestUri, content) 
     .ContinueWith(t => t.Result.Content.ReadAsAsync<T>()).Unwrap().Result; 
} 

El método que acepta la petición en la subclase de ApiController tiene una firma como sigue:

public HttpResponseMessage Post(HttpRequestMessage request) 
{ 
    /* parse request using MultipartFormDataStreamProvider */ 
} 

Idealmente, Me gustaría definirlo así, donde el contacto, la fuente y el objetivo se extraen del contenido 'multipart/mixed' basado en la propiedad 'name' del encabezado 'Content-Disposition'.

public HttpResponseMessage Post(Contact contact, Stream source, Stream target) 
{ 
    // process contact, source and target 
} 

Sin embargo, con mi firma existente, la publicación de los datos a los resultados de servidor en un InvalidOperationException con un mensaje de error de:

n 'MediaTypeFormatter' está disponible para leer un objeto de tipo ' HttpRequestMessage 'con el tipo de medio' multipart/mixed '.

Hay una serie de ejemplos en Internet sobre cómo enviar y recibir archivos utilizando ASP.NET Web API y HttpClient. Sin embargo, no he encontrado ninguno que muestre cómo tratar este problema.

Empecé a buscar la implementación de un MediaTypeFormatter personalizado y lo registro con la configuración global. Sin embargo, si bien es fácil lidiar con la serialización de XML y JSON en un MediaTypeFormatter personalizado, no está claro cómo lidiar con solicitudes 'multipart/mixed' que pueden ser prácticamente cualquier cosa.

Respuesta

13

Tenga una mirada en este foro: http://forums.asp.net/t/1777847.aspx/1?MVC4+Beta+Web+API+and+multipart+form+data

Aquí es un fragmento de código (Publicado por imran_ku07) que podría ayudarle a implementar un formateador personalizado para manejar la multipart/form-data:

public class MultiFormDataMediaTypeFormatter : FormUrlEncodedMediaTypeFormatter 
{ 
    public MultiFormDataMediaTypeFormatter() : base() 
    { 
     this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("multipart/form-data")); 
    } 

    protected override bool CanReadType(Type type) 
    { 
     return true; 
    } 

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

    protected override Task<object> OnReadFromStreamAsync(Type type, Stream stream, HttpContentHeaders contentHeaders, FormatterContext formatterContext) 
    { 
     var contents = formatterContext.Request.Content.ReadAsMultipartAsync().Result; 
     return Task.Factory.StartNew<object>(() => 
     { 
      return new MultiFormKeyValueModel(contents); 
     }); 
    } 

    class MultiFormKeyValueModel : IKeyValueModel 
    { 
     IEnumerable<HttpContent> _contents; 
     public MultiFormKeyValueModel(IEnumerable<HttpContent> contents) 
     { 
      _contents = contents; 
     } 


     public IEnumerable<string> Keys 
     { 
      get 
      { 
       return _contents.Cast<string>(); 
      } 
     } 

     public bool TryGetValue(string key, out object value) 
     { 
      value = _contents.FirstDispositionNameOrDefault(key).ReadAsStringAsync().Result; 
      return true; 
     } 
    } 
} 

Luego necesita agregar este formateador a su aplicación. Si haciendo auto-anfitrión simplemente puede añadir mediante la inclusión:

config.Formatters.Insert(0, new MultiFormDataMediaTypeFormatter()); 

antes de crear instancias de la clase HttpSelfHostServer.

- EDITAR -

para analizar los flujos binarios que necesita otro formateador. Aquí hay uno que estoy usando para analizar imágenes en uno de mis proyectos de trabajo.

class JpegFormatter : MediaTypeFormatter 
{ 
    protected override bool CanReadType(Type type) 
    { 
     return (type == typeof(Binary)); 
    } 

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

    public JpegFormatter() 
    { 
     SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/jpeg")); 
     SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/jpg")); 
     SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/png")); 
    } 

    protected override Task<object> OnReadFromStreamAsync(Type type, Stream stream, HttpContentHeaders contentHeaders, FormatterContext formatterContext) 
    { 
     return Task.Factory.StartNew(() => 
      { 
       byte[] fileBytes = new byte[stream.Length]; 
       stream.Read(fileBytes, 0, (int)fileBytes.Length); 

       return (object)new Binary(fileBytes); 
      }); 
    } 

    protected override Task OnWriteToStreamAsync(Type type, object value, Stream stream, HttpContentHeaders contentHeaders, FormatterContext formatterContext, TransportContext transportContext) 
    { 
     throw new NotImplementedException(); 
    } 
} 

En su controlador/acción que querrá hacer algo en la línea de:

+0

parece que funciona muy bien con datos de formularios estándar, pero estoy teniendo algunos desafíos cuando la carga contiene secuencias binarias. Abandoné esta idea y analicé la carga útil como se describe [aquí] (http://www.asp.net/web-api/overview/formats-and-model-binding/html-forms-and-multipart-mime). El servicio fue alojado en IIS. – bloudraak

+0

Las soluciones que proporcioné se aplicaban a .Net 4.5 Beta. Ahora que MS está enviando .Net 4.5RC, existe una forma similar pero más limpia de manejar datos binarios. Consulte esta publicación en el blog: http://byterot.blogspot.com/2012/04/aspnet-web-api-series-part-5.html – Jed

0

Tome un vistazo a este post https://stackoverflow.com/a/17073113/1944993 la respuesta de Kiran Challa es muy agradable!

La parte esencial:

personalizado en memoria MultiaprtFormDataStreamProvider:

public class InMemoryMultipartFormDataStreamProvider : MultipartStreamProvider 
{ 
    private NameValueCollection _formData = new NameValueCollection(); 
    private List<HttpContent> _fileContents = new List<HttpContent>(); 

    // Set of indexes of which HttpContents we designate as form data 
    private Collection<bool> _isFormData = new Collection<bool>(); 

    /// <summary> 
    /// Gets a <see cref="NameValueCollection"/> of form data passed as part of the multipart form data. 
    /// </summary> 
    public NameValueCollection FormData 
    { 
     get { return _formData; } 
    } 

    /// <summary> 
    /// Gets list of <see cref="HttpContent"/>s which contain uploaded files as in-memory representation. 
    /// </summary> 
    public List<HttpContent> Files 
    { 
     get { return _fileContents; } 
    } 

    public override Stream GetStream(HttpContent parent, HttpContentHeaders headers) 
    { 
     // For form data, Content-Disposition header is a requirement 
     ContentDispositionHeaderValue contentDisposition = headers.ContentDisposition; 
     if (contentDisposition != null) 
     { 
      // We will post process this as form data 
      _isFormData.Add(String.IsNullOrEmpty(contentDisposition.FileName)); 

      return new MemoryStream(); 
     } 

     // If no Content-Disposition header was present. 
     throw new InvalidOperationException(string.Format("Did not find required '{0}' header field in MIME multipart body part..", "Content-Disposition")); 
    } 

    /// <summary> 
    /// Read the non-file contents as form data. 
    /// </summary> 
    /// <returns></returns> 
    public override async Task ExecutePostProcessingAsync() 
    { 
     // Find instances of non-file HttpContents and read them asynchronously 
     // to get the string content and then add that as form data 
     for (int index = 0; index < Contents.Count; index++) 
     { 
      if (_isFormData[index]) 
      { 
       HttpContent formContent = Contents[index]; 
       // Extract name from Content-Disposition header. We know from earlier that the header is present. 
       ContentDispositionHeaderValue contentDisposition = formContent.Headers.ContentDisposition; 
       string formFieldName = UnquoteToken(contentDisposition.Name) ?? String.Empty; 

       // Read the contents as string data and add to form data 
       string formFieldValue = await formContent.ReadAsStringAsync(); 
       FormData.Add(formFieldName, formFieldValue); 
      } 
      else 
      { 
       _fileContents.Add(Contents[index]); 
      } 
     } 
    } 

    /// <summary> 
    /// Remove bounding quotes on a token if present 
    /// </summary> 
    /// <param name="token">Token to unquote.</param> 
    /// <returns>Unquoted token.</returns> 
    private static string UnquoteToken(string token) 
    { 
     if (String.IsNullOrWhiteSpace(token)) 
     { 
      return token; 
     } 

     if (token.StartsWith("\"", StringComparison.Ordinal) && token.EndsWith("\"", StringComparison.Ordinal) && token.Length > 1) 
     { 
      return token.Substring(1, token.Length - 2); 
     } 

     return token; 
    }} 

Puede entonces el "MemoryMultiPartDataStreamProvider" en que WebAPI así:

var provider = await Request.Content.ReadAsMultipartAsync<InMemoryMultipartFormDataStreamProvider>(new InMemoryMultipartFormDataStreamProvider()); 

    //access form data 
    NameValueCollection formData = provider.FormData; 

    //access files 
    IList<HttpContent> files = provider.Files; 
Cuestiones relacionadas