Word has reached the .NET community on the wings of a raven about a new option for a NoSQL-type data-layer implementation. RavenDB (ravendb.net) is a document database designed for the .NET/Windows platform, packaged with everything you need to start working with a nonrelational data store. RavenDB stores documents as schema-less JSON. A RESTful API exists for direct interaction with the data store, but the real advantage lies within the .NET client API that comes bundled with the install. It implements the Unit of Work pattern and leverages LINQ syntax to work with documents and queries. If you’ve worked with an object-relational mapper (ORM)—such as the Entity Framework (EF) or NHibernate—or consumed a WCF Data Service, you’ll feel right at home with the API architecture for working with documents in RavenDB.
The learning curve for getting up and running with an instance of RavenDB is short and sweet. In fact, the piece that may require the most planning is the licensing strategy (but even that’s minimal). RavenDB offers an open source license for projects that are also open source, but a commercial license is required for closed source commercial projects. Details of the license and the pricing can be found at ravendb.net/licensing. The site states that free licensing is available for startup companies or those looking to use it in a noncommercial, closed source project. Either way, it’s worthwhile to quickly review the options to understand the long-term implementation potential before any prototyping or sandbox development.
RavenDB Embedded and MVC
RavenDB can be run in three different modes:- As a Windows service
- As an IIS application
- Embedded in a .NET application
Install-Package RavenDB-Embedded
Adding the embedded version of RavenDB to an ASP.NET MVC 3 application is as simple as adding the package via NuGet and giving the data store files a directory location. Because ASP.NET applications have a known data directory in the framework named App_Data, and most hosting companies provide read/write access to that directory with little or no configuration required, it’s a good place to store the data files. When RavenDB creates its file storage, it builds a handful of directories and files in the directory path provided to it. It won’t create a top-level directory to store everything. Knowing that, it’s worthwhile to add the ASP.NET folder named App_Data via the Project context menu in Visual Studio 2010 and then create a subdirectory in the App_Data directory for the RavenDB data (see Figure 1).
Figure 1 App_Data Directory Structure
A document data store is schema-less by nature, hence there’s no need to create an instance of a database or set up any tables. Once the first call to initialize the data store is made in code, the files required to maintain the data state will be created.
Working with the RavenDB Client API to interface with the data store requires an instance of an object that implements the Raven.Client.IDocumentStore interface to be created and initialized. The API has two classes, DocumentStore and EmbeddedDocumentStore, that implement the interface and can be used depending on the mode in which RavenDB is running. There should only be one instance per data store during the lifecycle of an application. I can create a class to manage a single connection to my document store that will let me access the instance of the IDocumentStore object via a static property and have a static method to initialize the instance (see Figure 2).
Figure 2 Class for 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;
}
}
<connectionStrings>
<add name="RavenDB " connectionString="DataDir = ~\App_Data\Database" />
</connectionStrings>
RavenDB will create document ID keys in a REST-like format by default. An “Item” object would get a key in the format “items/104.” The object model name is converted to lowercase and is pluralized, and a unique tracking identity number is appended after a forward slash with each new document creation. This can be problematic in an MVC application, as the forward slash will cause a new route parameter to be parsed. The RavenDB Client API provides a way to change the forward slash by setting the IdentityPartsSeparator value. In my DataDocumentStore.Initialize method, I’m setting the IdentityPartsSeparator value to a dash before I call the Initialize method on the EmbeddableDocumentStore object, to avoid the routing issue.
Adding a call to the DataDocumentStore.Initialize static method from the Application_Start method in the Global.asax.cs file of my MVC application will establish the IDocumentStore instance at the first run of the application, which looks like this:
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
DataDocumentStore.Initialize();
}
RavenDB Objects
To get a better understanding of RavenDB in action, I’ll create a prototype application to store and manage bookmarks. RavenDB is designed to work with Plain Old CLR Objects (POCOs), so there’s no need to add property attributes to guide serialization. Creating a class to represent a bookmark is pretty straightforward. Figure 3 shows the Bookmark class.Figure 3 Bookmark Class
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>();
}
}
The JSON serialization of a sample Bookmark document is represented in the following structure:
{
"Title": "The RavenDB site",
"Url": "http://www.ravendb.net",
"Description": "A test bookmark",
"Tags": ["mvc","ravendb"],
"DateCreated": "2011-08-04T00:50:40.3207693Z"
}
Figure 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();
}
}
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
ModelBinders.Binders.Add(typeof(Bookmark), new BookmarkModelBinder());
DataDocumentStore.Initialize();
}
public static string ToCommaSeparatedString(this List<string> list)
{
return list == null ? string.Empty : string.Join(", ", list);
}
Unit of Work
The RavenDB Client API is based on the Unit of Work pattern. To work on documents from the document store, a new session needs to be opened; work needs to be done and saved; and the session needs to close. The session handles change tracking and operates in a manner that’s similar to a data context in the EF. Here’s an example of creating a new document:using (var session = documentStore.OpenSession())
{
session.Store(bookmark);
session.SaveChanges();
}
Figure 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);
}
}
MVC Controller and View Implementation
The BookmarksController actions will work directly with the IDocumentSession object from the base class and manage all of the Create, Read, Update and Delete (CRUD) operations for the documents. Figure 6 shows the code for the bookmarks controller.Figure 6 BookmarksController Class
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");
}
}
The Create method with the HttpPost verb attribute sets the CreateDate property on the bookmark item and calls the IDocumentSession.Store method off of the session object to add a new document record to the document store. The Update method with the HttpPost verb can call the IDocumentSession.Store method as well, because the Bookmark object will have the Id value already set. RavenDB will recognize that Id and update the existing document with the matching key instead of creating a new one. The DeleteConfirmed action calls a Delete method off of the IDocumentSession.Advanced.DatabaseCommands object, which provides a way to delete a document by key without having to load the object first. I don’t need to call the IDocumentSession.SaveChanges method from within any of these actions, because I have the base controller making that call on action executed.
All of the views are pretty straightforward. They can be strongly typed to the Bookmark class in the Create, Edit and Delete markups, and to a list of bookmarks in the Index markup. Each view can directly reference the model properties for display and input fields. The one place where I’ll need to vary on object property reference is with the input field for the tags. I’ll use the ToCommaSeparatedString extension method in the Create and Edit views with the following code:
@Html.TextBox("TagsAsString", Model.Tags.ToCommaSeparatedString())
Searching Objects
With all of my CRUD operations in place, I can turn my attention to adding one last bit of functionality: the ability to filter the bookmark list by tags. In addition to implementing the IEnumerable interface, the return object from the IDocumentSession.Query method also implements the IOrderedQueryable and IQueryable interfaces from the .NET Framework. This allows me to use LINQ to filter and sort my queries. For example, here’s a query of the bookmarks created in the past five days:var bookmarks = session.Query<Bookmark>()
.Where( i=> i.DateCreated >= DateTime.UtcNow.AddDays(-5))
.OrderByDescending(i => i.DateCreated)
.ToList();
var bookmarks = session.Query<Bookmark>()
.OrderByDescending(i => i.DateCreated)
.Skip(pageCount * (pageNumber – 1))
.Take(pageCount)
.ToList();
I can add the following action method to my BookmarksController class to handle getting bookmarks by 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);
}
No comments:
Post a Comment
Note: only a member of this blog may post a comment.