web-dev-qa-db-fra.com

Liaison de modèle d'API Web avec des données de formulaire en plusieurs parties

Existe-t-il un moyen d'obtenir une liaison de modèle (ou autre) pour distribuer le modèle à partir d'une demande de données de formulaire en plusieurs parties dans API Web ASP.NET MVC ?

Je vois divers articles de blog, mais les choses ont changé entre le message et la version réelle ou ils ne montrent pas que la liaison de modèle fonctionne.

Ceci est une publication obsolète: Envoi de données de formulaire HTML

et il en est de même: Téléchargement de fichier asynchrone à l'aide de l'API Web ASP.NET

J'ai trouvé ce code (et modifié un peu) quelque part qui lit les valeurs manuellement:

Modèle:

public class TestModel
{
    [Required]
    public byte[] Stream { get; set; }

    [Required]
    public string MimeType { get; set; }
}

Contrôleur:

    public HttpResponseMessage Post()
    {
        if (!Request.Content.IsMimeMultipartContent("form-data"))
        {
            throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
        }

        IEnumerable<HttpContent> parts = Request.Content.ReadAsMultipartAsync().Result.Contents;


        string mimeType;
        if (!parts.TryGetFormFieldValue("mimeType", out mimeType))
        {
            return Request.CreateResponse(HttpStatusCode.BadRequest);
        }

        var media = parts.ToArray()[1].ReadAsByteArrayAsync().Result;

        // create the model here
        var model = new TestModel()
            {
                MimeType = mimeType,
                Stream = media
            };
        // save the model or do something with it
        // repository.Save(model)

        return Request.CreateResponse(HttpStatusCode.OK);
    }

Test:

[DeploymentItem("test_sound.aac")]
[TestMethod]
public void CanPostMultiPartData()
{
    var content = new MultipartFormDataContent { { new StringContent("audio/aac"),  "mimeType"}, new ByteArrayContent(File.ReadAllBytes("test_sound.aac")) };

    this.controller.Request = new HttpRequestMessage {Content = content};
    var response = this.controller.Post();

    Assert.AreEqual(response.StatusCode, HttpStatusCode.OK);
}

Ce code est fondamentalement fragile, non maintenable et, en outre, n'applique pas les contraintes de liaison de modèle ou d'annotation de données.

Y a-t-il une meilleure manière de faire cela?

Mise à jour: J'ai vu cela post et cela me fait penser - dois-je écrire un nouveau formateur pour chaque single modèle que je veux soutenir?

24
Mrchief

@Mark Jones lié à mon article de blog http://lonetechie.com/2012/09/23/web-api-generic-mediatypeformatter-for-file-upload/ qui m'a conduit ici. Je dois réfléchir à comment faire ce que tu veux.

Je crois que si vous combinez ma méthode avec TryValidateProperty (), vous devriez pouvoir accomplir ce dont vous avez besoin. Ma méthode obtiendra un objet désérialisé, mais il ne gère aucune validation. Vous devrez éventuellement utiliser la réflexion pour parcourir les propriétés de l'objet, puis appeler manuellement TryValidateProperty () sur chacun d'eux. Cette méthode est un peu plus pratique, mais je ne sais pas comment faire autrement.

http://msdn.Microsoft.com/en-us/library/dd382181.aspxhttp://www.codeproject.com/Questions/310997/TryValidateProperty-not-work- avec-fonction-générique

Edit: Quelqu'un d'autre a posé cette question et j'ai décidé de la coder juste pour m'assurer que cela fonctionnerait. Voici mon code mis à jour de mon blog avec des contrôles de validation.

public class FileUpload<T>
{
    private readonly string _RawValue;

    public T Value { get; set; }
    public string FileName { get; set; }
    public string MediaType { get; set; }
    public byte[] Buffer { get; set; }

    public List<ValidationResult> ValidationResults = new List<ValidationResult>(); 

    public FileUpload(byte[] buffer, string mediaType, 
                      string fileName, string value)
    {
        Buffer = buffer;
        MediaType = mediaType;
        FileName = fileName.Replace("\"","");
        _RawValue = value;

        Value = JsonConvert.DeserializeObject<T>(_RawValue);

        foreach (PropertyInfo Property in Value.GetType().GetProperties())
        {
            var Results = new List<ValidationResult>();
            Validator.TryValidateProperty(Property.GetValue(Value),
                                          new ValidationContext(Value) 
                                          {MemberName = Property.Name}, Results);
            ValidationResults.AddRange(Results);
        }
    }

    public void Save(string path, int userId)
    {
        if (!Directory.Exists(path))
        {
            Directory.CreateDirectory(path);
        }

        var SafeFileName = Md5Hash.GetSaltedFileName(userId,FileName);
        var NewPath = Path.Combine(path, SafeFileName);

        if (File.Exists(NewPath))
        {
            File.Delete(NewPath);
        }

        File.WriteAllBytes(NewPath, Buffer);

        var Property = Value.GetType().GetProperty("FileName");
        Property.SetValue(Value, SafeFileName, null);
    }
}
5
Particleman

Il existe un bon exemple de formateur générique pour les téléchargements de fichiers ici http://lonetechie.com/2012/09/23/web-api-generic-mediatypeformatter-for-file-upload/ . Si j'allais avoir plusieurs contrôleurs acceptant les téléchargements de fichiers, ce serait l'approche que j'adopterais.

P.S. Après avoir regardé cela, cela semble être un meilleur exemple pour votre téléchargement dans le contrôleur http://www.strathweb.com/2012/08/a-guide-to-asynchronous-file-uploads-in-asp-net -web-api-rtm /

Mettre à jour

Re: L'utilité de l'approche multipartite , cela est couvert ici mais en fait cela se résume à l'approche multipartite étant bien construite pour des charges utiles binaires de taille significative etc ...

La liaison de modèle DEFAULT fonctionnera-t-elle?

Le classeur de modèle standard/par défaut pour WebApi n'est pas conçu pour faire face au modèle que vous avez spécifié, c'est-à-dire qui mélange des types simples et des tableaux Streams & byte (pas si simples) ... Ceci est une citation du article qui a inspiré la lonetechie's:

Les "types simples" utilisent la liaison de modèle. Les types complexes utilisent les formateurs. Un "type simple" comprend: primitives, TimeSpan, DateTime, Guid, Decimal, String ou quelque chose avec un TypeConverter qui convertit à partir de chaînes

Votre utilisation d'un tableau d'octets sur votre modèle et la nécessité de le créer à partir d'un flux/contenu de la demande vous orienteront plutôt vers l'utilisation de formateurs.

Envoyer le modèle et les fichiers séparément?

Personnellement, je chercherais à séparer le téléchargement du fichier du modèle ... peut-être pas une option pour vous ... de cette façon, vous feriez POST vers le même contrôleur et acheminer lorsque vous utilisez des données MultiPart type de contenu cela invoquera le formateur de téléchargement de fichiers et lorsque vous utilisez application/json ou x-www-form-urlencoded, cela fera une liaison de modèle de type simple ... Deux POST peuvent être hors de question pour vous, mais c'est une option ...

Classeur de modèle personnalisé?

J'ai eu quelques succès mineurs avec un classeur de modèle personnalisé , vous pouvez peut-être faire quelque chose avec cela ... cela pourrait être générique (avec un effort modéré) et pourrait être enregistré globalement dans le fournisseur de classeur pour être réutilisé ...

Cela peut valoir la peine d'être joué?

public class Foo
{
    public byte[] Stream { get; set; }
    public string Bar { get; set; }
}

public class FoosController : ApiController
{

    public void Post([ModelBinder(typeof(FileModelBinder))] Foo foo)
    {
        //
    }
}

Classeur de modèle personnalisé:

public class FileModelBinder : System.Web.Http.ModelBinding.IModelBinder
{
    public FileModelBinder()
    {

    }

    public bool BindModel(
        System.Web.Http.Controllers.HttpActionContext actionContext,
        System.Web.Http.ModelBinding.ModelBindingContext bindingContext)
    {
        if (actionContext.Request.Content.IsMimeMultipartContent())
        {
            var inputModel = new Foo();

            inputModel.Bar = "";  //From the actionContext.Request etc
            inputModel.Stream = actionContext.Request.Content.ReadAsByteArrayAsync()
                                            .Result;

            bindingContext.Model = inputModel;
            return true;
        }
        else
        {
            throw new HttpResponseException(actionContext.Request.CreateResponse(
             HttpStatusCode.NotAcceptable, "This request is not properly formatted"));
        }
    }
}
8
Mark Jones