Bạn hiện đang ngoại tuyến, hãy chờ internet để kết nối lại

Nhúng CSDL RavenDB vào ứng dụng ASP.NET MVC 3

Về tác giả
Bài viết này được cung cấp bởi MVP Lê Hoàng Dũng. Microsoft chân thành cảm ơn những MVP đã chia xẻ những kinh nghiệm chuyên môn của mình với những người sử dụng khác. Bài viết này sẽ được đăng trên website hoặc blog của MVP sau đây. Nếu bạn muốn xem các bài viết khác được chia xẻ bởi MVP, vui lòng nháy chuột vào đây.
Tóm tắt thông tin
Tác giả: Justin Schwartzenberger

Biên dịch: Lê Hoàng Dũng

Tải về mã ví dụ

Hiện nay, làn sóng NoSQL đang phát triển bên trong cộng đồng Microsoft .NET, một số công ty đã chia sẽ kinh nghiệp về việc sử dụng NoSQL trong các ứng dụng khả phổ biến mà chúng ta thường sử dụng. Điều giới lập trình tò mò đó là Dữ liệu NoSQL sẽ mang lại lợi ích gì và nó liệu có phải là giải pháp tiềm năng cho các phần mềm mà lập trình viên đang phải đối mặt hay không. Và quan trọng không kém, đó là liệu có khó để áp dụng không, liệu rằng có mất quá nhiều thời gian và công sức để viết một giải pháp dựa trên NoSQL không?

Cộng đồng lập trình .NET hiện nay đã có thêm một lựa chọn mới cho việc cài đặt lớp truy xuất dữ liệu theo kiểu NoSQL là RavenDB. RavenDB (ravendb.net) là cơ sở dữ liệu NoSQL được thiết kế cho nền tảng .NET/Windows, nó bao gồm tất cả những gì bạn cần để có thể làm việc với một CSDL “phi quan hệ” (nonrelational data store). RavenDB lưu trữ dữ liệu theo định dạng JSON. Nó cũng có các RESTful API để tương tác trực tiếp với dữ liệu, nhưng lợi ích thực sự nằm trong thư viện dành cho client (trình khách) đi kèm với bộ cài đặt RavenDB. Nó cài đặt mẫu Unit of Work và cài đặt cú pháp LINQ để tương tác với dữ liệu và các câu truy vấn. Nếu như bạn đã từng làm việc với các thư viện ánh xã dữ liệu quan hệ (ORM) như Entity Framework (EF) hoặc NHibernate hoặc triệu gọi đến các WCF Data Service, bạn sẽ cảm thấy quen thuộc khi lập trình với RavenDB.Các thao tác chạy một thực thể RavenDB rất đơn giản và nhẹ nhàng. Thực tế thì chúng ta còn cần quan tâm đến một việc đó là cơ chế cấp phép của RavenDB. RavenDb cấp phép theo dạng mã nguồn mở cho các dự án mã nguồn mở, nhưng đối với các dự án thương mại nguồn đóng thì cần phải được cấp phép thương mại. Bạn có thể tham khảo thông tin chi tiết và giá của các giấy phép thương mại tại ravendb.net/licensing. Trang web còn cung cấp giấy phép miễn phí cho các công ty có mục đích sử dụng nó cho các dự án phi thương mại và mã nguồn đóng. Và rõ ràng, chúng ta cần phải tham khảo các lựa chọn về giấy phép trước khi có lựa chọn cài đặt cụ thể.

RavenDB Embedded và MVC

RavenDB có thể được chạy dưới ba chế độ:
  1. Như là một Windows service
  2. Như là một ứng dụng chạy trên IIS
  3. Nhúng trong một ứng dụng .NET
Hai phương án đầu khá đơn giản nhưng cũng tốn một chút công sức để cài đặt. Phương án thứ ba, nhúng vào ứng dụng, là cách dễ nhất để áp dụng. Thực tế thì có cả một gói NuGet hỗ trợ việc này. Chỉ cần sử dụng câu lệnh dưới đây trong cửa sổ Package Manager Console ở Visual Studio 2010 (hoặc tìm “ravendb” ở trong cửa sổ Manage NuGet Packages) sẽ giúp cho chúng ta tham chiếu đến tất cả các thư viện cần thiết để làm việc với một phiên bản RavenDB được nhúng trong ứng dụng:

Install-Package RavenDB-Embedded

Chi tiết về gói này có thể tìm thấy ở trang web thư viện các gói NuGet tại bit.ly/ns64W1.Việc thêm một phiên bản nhúng của RavenDB dễ dàng y hệt việc thêm một gói thư viên thông qua NuGet, sau đó thì thiết lập một thư mục để chứa dữ liệu lưu bởi RavenDB. Bởi vì các ứng dụng ASP.NET có một thư mục thường dùng để lưu trữ dữ liệu là App_Data, và phần lớn các công ty cung cấp dịch vụ hosting cung cấp quyền đọc ghi cho thư mục này mà hầu như khoogn cần phải cấu hình nên đó là nơi hợp lý đễ lưu trữ các tập tin dữ liệu. Khi RavenDB tạo nơi lưu trữ dữ liệu, nó sẽ xây dựng một thư mục và dữ liệu trong thư mục mà chúng ta cấu hình cho nó. Chúng ta nên tạo thưc mục App_Data bằng Visual Studio 2010 và tạo một thư mục con trong thư mục này để lưu dữ liệu cho RavenDB (xem Hình 1).

(Hình 1)

Hình1. Cấu trúc thư mục App_Data

Vì RavenDB là cơ sở dữ liệu phi quan hệ, nên không cần phải thực hiện các thao tác tạo CSDL hay thiết lập các bảng. Chỉ cần gọi lần đầu tiên để khởi tạo kho dữ liệu bằng mã lệnh và tất cả các tập tin cần thiết để quản lý dữ liệu sẽ được tạo ra.

Làm việc với RavenDB Client API cần phải khởi tạo một thực thể của đối tượng có áp dụng giao diện Raven.Client.IDocumentStore. RavenDB Client API có hai lớp, DocumentStore và EmbeddedDocumentStore, hai lớp này cài đặt giao diện nói trên và có thể sử dụng lệ thuộc vào chế độ mà RavenDB đang chạy. Và chỉ chạy một thực thể cho mỗi kho dữ liệu trong suốt vòng đời của một ứng dụng. Ta có thể tạo một lớp để quản lý một kết nối đến kho dữ liệu, lớp này cho phép chúng ta truy xuất đến đối tượng IDocumentStore thông qua thuộc tính tĩnh và có phương thức tĩnh để khởi tạo cho thực thể đó (xem Hình 2).

Hình 2. Lớp dành cho DocumentStore
public class DataDocumentStore{  private static IDocumentStore instance;   public static IDocumentStore Instance  {    get    {      if(instance == null)        throw new InvalidOperationException(          "IDocumentStore has not been initialized.");      return instance;    }  }   public static IDocumentStore Initialize()  {    instance = new EmbeddableDocumentStore { ConnectionStringName = "RavenDB" };    instance.Conventions.IdentityPartsSeparator = "-";    instance.Initialize();    return instance;  }}
Phương thức getter cho thuộc tính tĩnh Instance kiểm tra trường instance, trong trường hợp trường instance là null, nó sẽ ném ra mỗi ngoại lệ InvalidOperationException. Chúng ta ném ngoại lệ ở đó, hơn là gọi phương thức Initialize để đảm bảo thread-safe. Trong trường hợp ngoại lệ được ném, có khả năng sẽ có nhiều lời gọi đồng thời đến phương thức Initialize. Bên trong phương thức Initialize, chúng ta một thực thể mới của Raven.Client.Embedded.EmbeddableDocumentStore và thiết lập giá trị cho thuộc tính ConnectionStringName là chuỗi kết nối mà ta để trong tập tin web.config khi cài đặt gói RavenDB từ NuGet. Trong tập tin web.config, chúng ta thiếp lập giá trị của chuỗi kết nối theo cú pháp mà RavenDB có thể hiểu được để sử dụng phiên bản nhúng của kho dữ liệu. Chúng ta cũng cần phải ánh xạ đến thư mục lưu CSDL mà chúng ta tạo bên trong thư mục App_Data của dự án MVC:

<connectionStrings>  <add name="RavenDB " connectionString="DataDir = ~\App_Data\Database" /></connectionStrings>
Giao diện IDocumentStore có đầy đủ các phương thức để làm việc với kho dữ liệu. Chúng ta trả về và lưu đối tượng EmbeddableDocumentStore như là một thực thể của giao diện IDocumentStore và như vậy chúng ta sẽ thuận tiện khi thay đổi đối tượng EmbeddedDocumentStore thành phiên bản server (DocumentStore) nếu như chúng ta không muốn sử dụng phiên bản nhũng nữa. Và như vậy sẽ giúp cho mã lệnh của chúng ta sẽ không cần phải quan tâm là phiên bản RavenDB nào đang được sử dụng.

RavenDB sẽ mặc định tạo các ID cho văn bản (document ID keys) theo định dạng tựa như REST (REST-like format). Một đối tượng “Item” sẽ có khía theo định dạng “items/104”. Tên của kiểu sẽ được chuyển thành chữ thường và có chuyển thành số nhiều, cộng thêm đó là một số định day duy nhất được nối vào phía sau dấu “/” với mỗi văn bản được tạo. Cách làm như vậy sẽ tạo nên một vấn đề trong các ứng dụng MVC, vì dâu “/” sẽ được hiểu là một tham số định tuyến được truyền vào. May thay, RavenDB Client API cung cấp một cách để thay ký tự “/” bằng một ký tự khác bằng cách quy định giá trị cho thuộc tính IdentityPartsSeparator. Trong phương thức DataDocumentStore.Initialize, chúng thiếp lập giá trị cho thuộc tính IdentityPartsSeparator là ký tự gạch ngang “-“ trước khi gọi phương thức Initialize của đối tượng EmbeddableDocumentStore, để tránh vấn đề về định tuyến.

Chúng đặt một lời gọi đến phương thức tĩnh DataDocumentStore.Initialize ở trong phương thức Application_Start của Global.asax.cs của ứng dụng MVC sẽ giúp thiết lập một thực thể IDocumentStore tại thời điểm thực thi lần đầu của ứng dụng, như sau:

protected void Application_Start(){  AreaRegistration.RegisterAllAreas();  RegisterGlobalFilters(GlobalFilters.Filters);  RegisterRoutes(RouteTable.Routes);   DataDocumentStore.Initialize();}
Từ lúc này chúng ta có thể sử dụng đối tượng IDocumentStore với lời gọi tĩnh đến thuộc tính DataDocumentStore.Instance để làm việc với các đối tượng văn bản của kho dữ liệu nhúng.

RavenDB Objects

Để hiểu có thể hiểu rõ hơn về RavenDB, chúng ta tạo một ứng dụng mẫu để lưu và quản lý các bookmarks. RavenDB được thiết kế để làm việc với các Plain Old CLR Objecs (POCO), do đó không cần phải thêm các thuộc tính để hướng dẫn việc kết xuất (serialization). Tạo một lớp để lưu bookmark khá là đơn giản. Hình 3 mô tả mã cài đặt lớp Bookmark.

Hình 3. Lớp Bookmark
public class Bookmark{  public string Id { get; set; }  public string Title { get; set; }  public string Url { get; set; }  public string Description { get; set; }  public List<string> Tags { get; set; }  public DateTime DateCreated { get; set; }   public Bookmark()  {    this.Tags = new List<string>();  }}
RavenDB sẽ kết xuất đối tượng theo kiến trúc JSON khi nó lưu văn bản. Thuộc tính “Id” sẽ được dùng để lưu mã ID của văn bản. RavenDB sẽ gán cho trường Id bằng null hoặc empty khi có lời gọi tạo mới một văn bản và sẽ lưu nó trong phần tử @metadata dành cho văn bản (phần tử này được dùng để quản lý mã văn bản ở mức kho dữ liệu (data-store level). Khi yêu cầu một văn bản, mã RavenDB Client API sẽ gán giá trị mã văn bản cho thuộc tính Id khi nó nạp đối tượng văn bản.

Kết xuất JSON của một văn bản Bookmark sẽ được hiển thị theo cấu trúc sau:

{  "Title": "The RavenDB site",  "Url": "http://www.ravendb.net",  "Description": "A test bookmark",  "Tags": ["mvc","ravendb"],  "DateCreated": "2011-08-04T00:50:40.3207693Z"}
Lớp Bookmark thì hoạt động ổn với kho văn bản, như thuộc tính Tags sẽ tạo nên một thách thức đối với tầng giao diện. Chúng ta sẽ cho phép người dùng diện một danh sách các tags phân tách bởi dấu “,” trong cùng một hộp văn bản và để cho ứng dụng MVC tự ánh xạ vào trường dữ liệu mà không cần phải cài đặt mã lệnh nào trong các views lần controller actions. Thay vì vậy chúng ta dùng một model bindẻ tùy biến để ánh xạ trường “TagsAsString” thành trường Bookmark.Tags. Đầu tiên, chúng ta lớp model binder tùy biến (như Hình 4).

Hình 4. BookmarkModelBinder.cs
public class BookmarkModelBinder : DefaultModelBinder{  protected override void OnModelUpdated(ControllerContext controllerContext,    ModelBindingContext bindingContext)  {    var form = controllerContext.HttpContext.Request.Form;    var tagsAsString = form["TagsAsString"];    var bookmark = bindingContext.Model as Bookmark;    bookmark.Tags = string.IsNullOrEmpty(tagsAsString)      ? new List<string>()      : tagsAsString.Split(',').Select(i => i.Trim()).ToList();  }}
Sau đó chúng ta cập nhật tập tin Globals.asax.cs để thêm BookmarkModelBinder vào danh sách các model binders khi khởi chạy ứng dụng:
protected void Application_Start(){  AreaRegistration.RegisterAllAreas();  RegisterGlobalFilters(GlobalFilters.Filters);  RegisterRoutes(RouteTable.Routes);   ModelBinders.Binders.Add(typeof(Bookmark), new BookmarkModelBinder());  DataDocumentStore.Initialize();}
Để quản lý việc chuyển các tags về một chuỗi, chúng ta thêm một extention method (phương thức mở rộng) để chuyển đối tượng List<string> thành một chuỗi được phân tách bởi ký tự ',':
public static string ToCommaSeparatedString(this List<string> list){  return list == null ? string.Empty : string.Join(", ", list);}
Unit Of Work

RavenDB Client API được xây dựng dựa trên mẫu Unit of Work. Để làm việc với các văn bản từ kho văn bản, một phiên làm việc cần được mở, sau khi việc truy xuất được thực hiện xong, thì phiên làm việc cần phải đóng lại. Phiên làm việc quản lý việc theo dõi các thay đổi và hoạt động tương tự đối tượng data context (ngữ cảnh dữ liệu) trong EF.

using (var session = documentStore.OpenSession()){  session.Store(bookmark);  session.SaveChanges();}
Sẽ là tối ưu nếu cho phép một phiên làm việc sống trong suốt một HTTP request, và nhờ vậy nó có thể theo dõi các thay đổi, sử dụng cache lớp thứ nhất và hơn thế nữa. Chúng ta sẽ tạo một base controller sử dụng DocumentDataStore để tạo một phiên làm việc mới tại thời điểm action executing, và sẽ lưu lại thay đổi đồng thời hủy đối tượng phiên làm việc vào lúc action executed (xem Hình 5).

Hình 5. BaseDocumentStoreController
public class BaseDocumentStoreController : Controller{  public IDocumentSession DocumentSession { get; set; }   protected override void OnActionExecuting(ActionExecutingContext filterContext)  {    if (filterContext.IsChildAction)      return;    this.DocumentSession = DataDocumentStore.Instance.OpenSession();    base.OnActionExecuting(filterContext);  }   protected override void OnActionExecuted(ActionExecutedContext filterContext)  {    if (filterContext.IsChildAction)      return;    if (this.DocumentSession != null && filterContext.Exception == null)      this.DocumentSession.SaveChanges();    this.DocumentSession.Dispose();    base.OnActionExecuted(filterContext);  }}
Cài đặt cho MVC Controllers và các Views

Các action của BookmarksController actions sẽ làm việc trực tiếp với đối tượng IDocumentSession và quản lý tất cả các thao tác Create, Read, Update và Delete (CRUD) đến các văn bản.

Hình 6. Lớp BookmarksController
public class BookmarksController : BaseDocumentStoreController{  public ViewResult Index()  {    var model = this.DocumentSession.Query<Bookmark>()      .OrderByDescending(i => i.DateCreated)      .ToList();    return View(model);  }   public ViewResult Details(string id)  {    var model = this.DocumentSession.Load<Bookmark>(id);    return View(model);  }   public ActionResult Create()  {    var model = new Bookmark();    return View(model);  }   [HttpPost]  public ActionResult Create(Bookmark bookmark)  {    bookmark.DateCreated = DateTime.UtcNow;    this.DocumentSession.Store(bookmark);    return RedirectToAction("Index");  }     public ActionResult Edit(string id)  {    var model = this.DocumentSession.Load<Bookmark>(id);    return View(model);  }   [HttpPost]  public ActionResult Edit(Bookmark bookmark)  {    this.DocumentSession.Store(bookmark);    return RedirectToAction("Index");  }   public ActionResult Delete(string id)  {    var model = this.DocumentSession.Load<Bookmark>(id);    return View(model);  }   [HttpPost, ActionName("Delete")]  public ActionResult DeleteConfirmed(string id)  {    this.DocumentSession.Advanced.DatabaseCommands.Delete(id, null);    return RedirectToAction("Index");  }}
Phương thức IDocumentSession.Query<T> ở action Index sẽ trả về đối tượng IEnumerable, và nhờ vậy chúng ta có thể sử dụng biểu thức LINQ OrderByDescending để sắp xếp các đối tượng và gọi phương thức ToList để trả dữ liệu về. Lời gọi IDocumentSession.Load trong action Details nhận mã văn bản (ID) và trả về văn bản tương ứng qua đối tượng Bookmark.

Action Create sẽ gán giá trị cho thuộc tính CreateDate của đối tượng Bookmark và gọi phương thức IDocumentSession.Store để thêm văn bản mới vào kho văn bản. Action Update cũng dùng phương thức IDocumentSession.Store để cập nhật những thay đổi đối với một văn bản. RavanDB sẽ xác nhận mã Id và cập nhật văn bản có sẵn thay vì tạo một cái mới. Action DeleteConfirmed gọi phương thức Delete của đối tượng IDocumentSession.Advanced.DatabaseCommands, đối tượng này sẽ giúp xóa một văn bản dựa vào mã của nó mà không cần phải nạp văn bản vào bộ nhớ. Chúng ta không cần gọi phương thức IDocumentSession.SaveChanges đối với các văn bản này bởi vì chúng ta đã gọi nó khi action executed (xem cài đặt của base controller).

Các views đều là strongly typed view với model là lớp Bookmark hoặc IEnumerable<Bookmark>. Ngoài ra chúng ta dùng phương thưc mở rộng ToCommaSeparatedString trong các view Create và Edit như mã dưới dây:

@Html.TextBox("TagsAsString", Model.Tags.ToCommaSeparatedString())
Như thế, người dùng có thể nhập và thay đối các tag đi kèm với bookmark một cách dễ dàng.

Tìm kiếm

Tiếp theo chúng ta sẽ thêm một chức năng nữa, đó là khả năng lọc danh sách bookmark dựa vào các tags. Chúng ta dùng LINQ để lọc và sắp xếp dữ liệu, và dưới đây là danh sách bookmark được tạo trong vòng năm ngày:

var bookmarks = session.Query<Bookmark>()  .Where( i=> i.DateCreated >= DateTime.UtcNow.AddDays(-5))  .OrderByDescending(i => i.DateCreated)  .ToList();
Và dưới đây là một trang của danh sách đầy đủ các bookmark:

var bookmarks = session.Query<Bookmark>()  .OrderByDescending(i => i.DateCreated)  .Skip(pageCount * (pageNumber – 1))  .Take(pageCount)  .ToList();
RavenDB sẽ xây dựng các chỉ mục động (dynamic indexes) dựa vào việc thực thy các câu truy vấn và lưu các bảng chỉ mục này lại một khoảng thời gian nhất định trước khi hủy đi. Khi một câu truy vấn tương tự được gọi với các tham số tương tự, chỉ mục tạm thời sẽ được dùng đến. Nếu chỉ mục được dùng đi dùng lại trong một khoảng thời gian đủ lâu thì nó sẽ được lưu hẳn lại trong suốt vòng đời ứng dụng.

Chúng ta có thể action sau vào lớp BookmarksController để lấy về các bookmark theo tag:

public ViewResult Tag(string tag){  var model = new BookmarksByTagViewModel { Tag = tag };  model.Bookmarks = this.DocumentSession.Query<Bookmark>()    .Where(i => i.Tags.Any(t => t == tag))    .OrderByDescending(i => i.DateCreated)    .ToList();  return View(model);}
Qua đây, chúng ta biết được rằng các chỉ mục được lập và được lưu tạm thời hoặc lưu hẳn theo vòng đời của ứng dụng mà chúng ta chẳng cần phải thao tác gì cả.

Sự xuất hiện của "Con quạ" (raven) đã thức tỉnh chúng ta

Sự xuất hiện của RavenDB, cộng đồng .NET cuối cùng đã có một giải phải lưu văn bản theo kiểu NoSQL để áp dụng, và giúp cho các lập trình viên .NET thêm nhập vào thế giới CSDL phi quan hệ, nơi mà có nhiều framework và nhiều ngôn ngữ đã khám phá trong những năm gần đấy. Và như vậy chúng ta sẽ không phải nghe những lời than thở rằng thiếu những người yêu thích lập trình CSDL phi quan hệ cho mãng ứng dụng liên quan đến Microsoft. RavenDB giúp cho các lập trình .NET dễ dàng thao tác với dữ liệu NoSQL với các bộ cài đặt và các API đơn giản cho việc quản lý dữ liệu. Và hẳn RavenDb cũng giúp cho chúng ta nghiên cứu để xem các giải pháp CSDL phi quan hệ sẽ được ứng dụng ở đâu và học cách thiết kế kiến trúc ứng dụng cho phù hợp.

Về tác giả Justin Schwartzenberger

Justin Schwartzenberger, ông là CTO tại DealerHosts, ông có kinh nghiệp lập trình Web trong khoảng thời gian khá dài, và đã kinh qua PHP, classic ASP, Visual Basic, VB.NET và ASP.NET Web Forms. Ông cũng là người đã sớm áp dụng ASP.NET MVC từ năm 2007, và ông đã quyết định tập trung vào MVC. Ông viết nhiều bài viết, thuyết trình, duy trì một blog tại iwantmymvc.com và bạn có thể theo dõi ông qua twitter tại twitter.com/schwarty.

Tuyên bố Không chịu trách nhiệm Nội dung Giải pháp Cộng đồng

CÔNG TY MICROSOFT VÀ/HOẶC CÁC NHÀ CUNG CẤP CỦA HỌ KHÔNG BẢO ĐẢM VỀ TÍNH PHÙ HỢP, ĐỘ TIN CẬY HOẶC TÍNH CHÍNH XÁC CỦA THÔNG TIN VÀ HÌNH ẢNH LIÊN QUAN Ở ĐÂY. MỌI THÔNG TIN VÀ HÌNH ẢNH NHƯ VẬY ĐƯỢC CUNG CẤP “NHƯ NGUYÊN MẪU” MÀ KHÔNG CÓ BẤT KỲ BẢO ĐẢM NÀO. MICROSOFT VÀ/HOẶC CÁC NHÀ CUNG CẤP CỦA HỌ KHÔNG CHỊU TRÁCH NHIỆM ĐỐI VỚI MỌI BẢO ĐẢM VÀ ĐIỀU KIỆN VỀ THÔNG TIN VÀ HÌNH ẢNH LIÊN QUAN NÀY, BAO GỒM CẢ MỌI BẢO ĐẢM VÀ ĐIỀU KIỆN LIÊN QUAN VỀ TÍNH THƯƠNG MẠI, PHÙ HỢP CHO MỘT MỤC ĐÍCH ĐẶC BIỆT, NỖ LỰC CỦA CÔNG VIỆC, TƯ CÁCH VÀ CAM KẾT KHÔNG VI PHẠM. BẠN ĐỒNG Ý MỘT CÁCH CỤ THỂ LÀ KHÔNG CÓ TRƯỜNG HỢP NÀO MÀ MICROSOFT VÀ/HOẶC CÁC NHÀ CUNG CẤP CỦA HỌ BỊ RÀNG BUỘC VÀO BẤT KỲ THIỆT HẠI TRỰC TIẾP, GIÁN TIẾP, TRỪNG PHẠT, TÌNH CỜ, ĐẶC BIỆT, HỆ QUẢ HOẶC BẤT KỲ THIỆT HẠI DẠNG NÀO, BAO GỒM NHƯNG KHÔNG GIỚI HẠN THIỆT HẠI DO MẤT MÁT, DỮ LIỆU HOẶC LỢI ÍCH, XẢY RA HOẶC TRONG MỌI CÁCH LIÊN QUAN ĐẾN VIỆC SỬ DỤNG HOẶC KHÔNG THỂ SỬ DỤNG THÔNG TIN VÀ HÌNH ẢNH LIÊN QUAN CÓ Ở ĐÂY, DÙ LÀ DỰA VÀO HỢP ĐỒNG, LỖI GÂY THIỆT HẠI, SƠ SUẤT, NGHĨA VỤ PHÁP LÝ HOẶC BẤT KỲ CƠ SỞ NÀO KHÁC, NGAY CẢ NẾU MICROSOFT HOẶC BẤT KỲ NHÀ CUNG CẤP NÀO CỦA HỌ ĐÃ ĐƯỢC TƯ VẤN VỀ KHẢ NĂNG BỊ THIỆT HẠI.
Thuộc tính

ID Bài viết: 2643674 - Xem lại Lần cuối: 12/14/2011 02:03:00 - Bản sửa đổi: 1.0

Microsoft ASP.NET MVC 3

  • kbprb kbtshoot kbstepbystep kbgraphxlink kbmvp KB2643674
Phản hồi