25

ASP.NET MVC Web APIのマルチパート フォーム データ要求からモデルを提供するモデル バインディング (または何でも) を取得できる方法はありますか?

さまざまなブログ投稿を目にしますが、投稿と実際のリリースの間で変更があったか、モデル バインディングが機能していません。

これは古い投稿です: HTML フォーム データの送信

これもそうです:ASP.NET Web APIを使用した非同期ファイルアップロード

値を手動で読み取るこのコードをどこかで見つけました(そして少し変更しました):

モデル:

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

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

コントローラ:

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

テスト:

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

このコードは基本的に壊れやすく、保守できず、さらにモデル バインディングやデータ注釈の制約を適用しません。

これを行うより良い方法はありますか?

更新:この投稿を見て考えさせられました。サポートしたいモデルごとに新しいフォーマッターを作成する必要があるのでしょうか?

4

2 に答える 2

9

http://lonetechie.com/2012/09/23/web-api-generic-mediatypeformatter-for-file-upload/に、ファイル アップロード用の一般的なフォーマッタの良い例があります。ファイルのアップロードを受け入れる複数のコントローラーを使用する場合、これが私がとるアプローチになります。

PSこれを見回すと、コントローラー内のアップロードのより良い例のように思えますhttp://www.strathweb.com/2012/08/a-guide-to-asynchronous-file-uploads-in-asp-net-web- api-rtm/

アップデート

Re: マルチパート アプローチの有用性、これはここでカバーされています が、事実上、これは非常に大きなサイズのバイナリ ペイロードなどに対して適切に構築されているマルチパート アプローチに要約されます...

DEFAULT モデル バインディングは機能しますか?

WebApi の標準/既定のモデル バインダーは、指定したモデル、つまり単純な型とストリームとバイト配列 (それほど単純ではない) を組み合わせたモデルに対応するように構築されていませ

「単純型」はモデル バインディングを使用します。複合型はフォーマッタを使用します。「単純な型」には、プリミティブ、TimeSpan、DateTime、Guid、Decimal、String、または文字列から変換する TypeConverter を持つものが含まれます。

モデルでバイト配列を使用し、リクエストのストリーム/コンテンツからそれを作成する必要があるため、代わりにフォーマッターを使用するように指示されます。

モデルとファイルを別々に送信しますか?

個人的には、ファイルのアップロードをモデルから分離することを検討します...おそらくあなたのオプションではありません...この方法では、同じコントローラーに POST し、マルチパート データ コンテンツ タイプを使用するときにルーティングします。これにより、ファイル アップロード フォーマッターが呼び出されます。 application/json または x-www-form-urlencoded を使用すると、単純なタイプのモデル バインディングが実行されます... 2 つの POST は問題外かもしれませんが、オプションです...

カスタムモデルバインダー?

私はカスタムモデルのバインダーでいくつかの小さな成功を収めました。おそらくこれで何かを行うことができます...これは(適度な努力で)汎用的にすることができ、再利用のためにバインダープロバイダーにグローバルに登録することができます...

これはプレイする価値があるでしょうか?

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)
    {
        //
    }
}

カスタムモデルバインダー:

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"));
        }
    }
}
于 2012-09-26T07:49:49.267 に答える
5

@Mark Jones が私のブログ投稿http://lonetechie.com/2012/09/23/web-api-generic-mediatypeformatter-for-file-upload/にリンクしてくれました。私はあなたがしたいことをする方法について考えなければなりません。

私のメソッドを TryValidateProperty() と組み合わせれば、必要なことを達成できるはずです。私のメソッドはオブジェクトを逆シリアル化しますが、検証は処理しません。リフレクションを使用してオブジェクトのプロパティをループし、それぞれに対して手動で TryValidateProperty() を呼び出す必要がある場合があります。この方法はもう少し実践的ですが、他に方法がわかりません。

http://msdn.microsoft.com/en-us/library/dd382181.aspx http://www.codeproject.com/Questions/310997/TryValidateProperty-not-work-with-generic-function

編集: 他の誰かがこの質問をしたので、動作することを確認するためだけにコーディングすることにしました。検証チェックを使用してブログから更新したコードを次に示します。

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);
    }
}
于 2012-09-26T14:19:24.823 に答える