ASP.NET: Truyền Dữ Liệu Từ Form Đến Web API

Đa phần dữ liệu được gửi từ client đến Web API thông qua HTML forms. HTML forms có hai attribute là action dùng để xác định nơi dữ liệu sẽ được gửi đến và method dùng để xác định phương thức gửi dữ liệu

<form action="api/values" method="post">

HTML forms sử dụng một trong hai phương thức là GET và POST để gửi dữ liệu tới server. Nếu form sử dụng phương thức GET (là phương thức mặc định) thì dữ liệu của form sẽ được gửi dưới dạng query string. Nếu form sử dụng phương thức POST thì dữ liệu của form được đặt trong HTTP request body.

Khi form sử dụng phương thức POST, dữ liệu sẽ được encode, và định dạng của dữ liệu được encode tùy thuộc vào giá trị được mô tả trong attribute enctype của form. Attribute này nhận một trong hai giá trị sau

application/x-www-form-urlencoded. Đây là giá trị mặc định. Nó cho biết dữ liệu sẽ được encode thành các cặp name/value tương tự như query string. Sử dụng giá trị này nếu dữ liệu của form ở dạng text. multipart/form-data. Giá trị này cho biết dữ liệu sẽ được encode thành multipart MIME message. Sử dụng giá trị này nếu dữ liệu của form ở dạng binary chẳng hạn như khi gửi file

Khi dữ liệu cần gửi là kiểu đơn giản (ví dụ như string, boolean), developer có hai lựa chọn. Lựa chọn đơn giản nhất là gửi dữ liệu dưới dạng query string bởi vì mặc định Web API sẽ parse URI để lấy ra giá trị cho các kiểu dữ liệu đơn giản. Giả sử có một form cần submit status là một chuỗi đến server như sau

# Simple Type

<form id="form1">
<div>
<label for="status">Status</label>
</div>
<div>
<input id="status" name="status" type="text" />
</div>
<div>
<input type="submit" value="Submit" />
</div>
</form>

Khi đó, developer có thể viết script để submit form dưới dạng query string như sau

$('#form1').submit(function () {
    $.get('api/updates/simple', $('#form1').serialize())
    .success(function () { alert('success'); })
    .error(function () { alert('error'); });
    return false;
});

Lựa chọn thứ hai là POST dữ liệu thay vì GET. Trong trường hợp đó, có hai sự thay đổi trong code script và code server. Đầu tiên, trong controller, developer phải gắn thêm attribute FromBodyAttribute trước tên tham số mà sẽ nhận giá trị của kiểu dữ liệu đơn giản. Attribute này chỉ dẫn cho Web API đọc giá trị từ request body thay vì từ URI. Ví dụ dưới đây minh họa cách sử dụng FromBodyAttribute

[HttpPost]
[ActionName("Simple")]
public HttpResponseMessage PostSimple([FromBody] string value)
{
    if (value != null)
    {
        //...
    }
    else
    {
        return Request.CreateResponse(HttpStatusCode.BadRequest);
    }
}

Tiếp theo, trong code script, client cần gửi dữ liệu dưới dạng POST bằng cách gọi hàm post()

$('#form1').submit(function () {
    $.post('api/updates/simple', $('#form1').serialize())
    .success(function () { alert('success'); })
    .error(function () { alert('error'); });
    return false;
});

Mặc dù truyền dữ liệu kiểu đơn giản bằng cách POST có thể thực hiện được nhưng nên tránh vì nó đòi hỏi phải sử dụng thêm attribute FromBodyAttribute . Tuy nhiên với kiểu dữ liệu phức tạp thì POST là phương thức được lựa chọn.

Khi dữ liệu cần gửi là kiểu phức tạp (ví dụ như model object), được cấu thành từ giá trị của các HTML inputs có trong form, developer sẽ post dữ liệu của form về lại server. Giả sử có một model được định nghĩa ở server như sau

namespace FormEncode.Models
{
    using System;
    using System.ComponentModel.DataAnnotations;
    public class Update
    {
        [Required]
        [MaxLength(140)]
        public string Status { get; set; }

        public DateTime Date { get; set; }
    }
}

Và một form được hiển thị ở client như sau

# Complex Type

<form id="form2" method="post" action="api/updates/complex">
    <div>
        <label for="status">Status</label>
    </div>

    <div>
        <input name="status" type="text" />
    </div>
    <div>
        <label for="date">Date</label>
    </div>
    <div>
        <input name="date" type="text" />
    </div>
    <div>
        <input type="submit" value="Submit" />
    </div>
</form>

Để Web API nhận dữ liệu của form dưới dạng một model object, developer cần định nghĩa controller như sau

namespace FormEncode.Controllers
{
    using FormEncode.Models;
    using System;
    using System.Web;
    using System.Web.Http;

    public class UpdatesController : ApiController
    {
        [HttpPost]
        [ActionName("Complex")]
        public HttpResponseMessage PostComplex(Update update)
        {
            if (ModelState.IsValid && update != null)
            {
                // ...
            }
            else
            {
                return Request.CreateResponse(HttpStatusCode.BadRequest);
            }
        }
    }
}

Khi đó, nếu user điền thông tin vào form như sau

Và sau đó click Submit, browser sẽ gửi một HTTP request như sau

POST http://localhost:38899/api/updates/complex HTTP/1.1
Accept: text/html, application/xhtml+xml, */*
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)
Content-Type: application/x-www-form-urlencoded
Content-Length: 47
status=Shopping+at+the+mall.&date=6%2F15%2F2012

Mặc dù request body chứa dữ liệu form dưới dạng cặp name/value tương tự như query string nhưng Web API sẽ tự động parse và convert dữ liệu thành model object trong action method. Nếu cần post form bằng ajax, developer sẽ viết thêm code script như sau

<script type="text/javascript">
$("#form2").submit(function () {
    $.post('api/updates/complex', $('#form2').serialize())
    .success(function () { alert('Success'); })
    .error(function () { alert('Error'); });
    return false;
});
</script>

Trong các ví dụ ở trên, dữ liệu của form ở dạng text thuần túy. Nếu trong form có chứa dữ liệu dạng binary chẳng hạn như khi user cần upload file lên server thì attribute enctype của form phải được khai báo tường minh là “multipart/form-data” như ví dụ dưới đây

Khi đó, nếu user click Submit, browser sẽ gửi một HTTP request với định dạng tương tự như dưới đây

POST http://localhost:50460/api/values/1 HTTP/1.1
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:12.0) Gecko/20100101 Firefox/12.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------41184676334
Content-Length: 29278
-----------------------------41184676334
Content-Disposition: form-data; name="caption"
Summer vacation

-----------------------------41184676334
Content-Disposition: form-data; name="image1"; filename="GrandCanyon.jpg"
Content-Type: image/jpeg
(Binary data not shown)
-----------------------------41184676334--

Message trong request body sẽ gồm nhiều phần (multipart), mỗi phần chứa dữ liệu của một form control tương ứng. Trong ví dụ trên, message gồm hai phần, một cái chứa dữ liệu của input có name là caption và cái còn lại chứa dữ liệu của input có name là image1. Giữa các phần được ngăn cách bởi một chuỗi boundary gồm một đường gạch ngang và cuối mỗi đường là một dãy số ngẫu nhiên. Dãy số ngẫu nhiên (trong ví dụ là dãy “41184676334”) giúp cho chuỗi boundary không xuất hiện trong nội dụng của mỗi phần.

Nội dung của mỗi phần gồm một hoặc nhiều headers và theo sau là content. Header Content-Disposition chứa tên của control. Nếu control là file thì nó cũng chứa tên file. Header Content-Type mô tả loại dữ liệu của control. Nếu header này không được sử dụng thì loại dữ liệu sẽ là text/plain. Trong ví dụ trên, user đã upload một file tên là “GrandCanyon.jpg” với loại dữ liệu là image/jpeg và giá trị của text input là “Summer Vacation”.

Khác với ASP.NET MVC, Web API không có sẵn một media-type formatter mà hỗ trợ việc upload file nên không thể sử dụng HttpPostedFileBase như là tham số cho action method trong controller. Nếu muốn sử dụng HttpPostedFileBase như đã từng sử dụng trong ASP.NET MVC, developer phải tự viết custom media-type formatter. Thay vì vậy, Web API sử dụng một class tên là MultipartFormDataStreamProvider như là một helper để giúp đọc dữ liệu từ một form sử dụng định dạng multipart/form-data và dĩ nhiên trong đó có cả dữ liệu của file được upload. Với MultipartFormDataStreamProvider, developer phải đọc dữ liệu trong ngữ cảnh asynchronous. Ví dụ dưới đây minh họa cách đọc dữ liệu từ multipart/form-data form

public Task<HttpResponseMessage> PostFormData()
    {
        // Check if the request contains multipart/form-data.
        if (!Request.Content.IsMimeMultipartContent())
        {
            throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
        }

        string root = HttpContext.Current.Server.MapPath("~/App_Data");
        var provider = new MultipartFormDataStreamProvider(root);

        // Read the form data and return an async task.
        var task = Request.Content.ReadAsMultipartAsync(provider).
        ContinueWith<HttpResponseMessage>(t =>
        {
            if (t.IsFaulted || t.IsCanceled)
            {
                Request.CreateErrorResponse(HttpStatusCode.InternalServerError, t.Exception);
            }

            // This illustrates how to get the file names.
            foreach (MultipartFileData file in provider.FileData)
            {
                Trace.WriteLine(file.Headers.ContentDisposition.FileName);
                Trace.WriteLine("Server file path: " + file.LocalFileName);
            }

            // Show all the key-value pairs.
            foreach (var key in provider.FormData.AllKeys)
            {
                foreach (var val in provider.FormData.GetValues(key))
                {
                    Trace.WriteLine(string.Format("{0}: {1}", key, val));
                }
            }

            return Request.CreateResponse(HttpStatusCode.OK);
        });

        return task;
    }

Trong đoạn code trên, đầu tiên action method kiểm tra xem dữ liệu có phải đến từ form được định dạng là multipart/form-data. Nếu không, action method ném ra exception với HTTP status code 415, Unsupported Media Type. Để đọc dữ liệu từ form thông qua MultipartFormDataStreamProvider, gọi method Request.Content.ReadAsMultipartAsync(). Method này lấy ra nội dung của HTTP request body và ghi chúng xuống stream được quản lý bởi MultipartFormDataStreamProvider. Sau khi method ReadAsMultipartAsync() hoàn thành công việc của nó, developer được bảo đảm rằng file đã được upload lên server và có thể lấy ra tất cả thông tin về các file được upload từ property provider.FileData mà là một collection các object MultipartFileData. Ngoài ra, MultipartFormDataStreamProvider còn hỗ trợ lấy ra giá trị của các input khác có trong form nhờ vào property provider.FormData mà là kiểu NameValueCollection.

Nguồn: http://nguyenvutuan.blogspot.com/2013/03/truyen-du-lieu-tu-html-form-en-aspnet.html