September 20, 2012

Strongly Typed Views with the .NET Client Library

The latest bits of the Couchbase .NET Client Library support a few different view querying options.  In this post, I'll describe those options in detail.  To play along at home, make sure you have the latest Couchbase Server installed with the beer-sample sample bucket. 

I've added a view named "by_name" to the beer-sample bucket in the "beer" design doc.  This view simply creates a secondary index on the "name" property of "beer" documents.

function (doc, meta) {
  if (doc.name && doc.type && doc.type == "beer") {
    emit(doc.name, null);
  }    
}

Querying this view with the non-generics version of GetView will yield an enumerable collection of IViewRow instances which contain information about the rows in the view.

Setup your client code as follows:

var config = new CouchbaseClientConfiguration();
config.Urls.Add(new Uri("http://localhost:8091/pools"));
config.Bucket = "beer-sample";            

var client = new CouchbaseClient(config);

Then query the view:

var view = client.GetView("beer", "by_name");
foreach (var row in view)
{
    Console.WriteLine("Row ID: " + row.ItemId);
    Console.WriteLine("Row Key: " + row.ViewKey[0]);
    Console.WriteLine("Row Value: " + row.Info["value"]);
}

The IViewRow interface defines properties for the "id" and "key" properties in the row and additionally provides a dictionary with access to each property in the row. 

If you wanted to retrieve the original documents associated with each of the rows, you would take the row.ItemId and query using the client's Get method.

foreach (var row in view)
{
    var item = client.Get(row.ItemId);
    Console.WriteLine(item);
}

Alternatively, you could do a multi-get to retrieve all documents in one call. Note that with this version, the view is queried when the LINQ Select method is called, as opposed to above when the enumeration of view queries the view.  In either case, it's the enumeration of the IView instance that triggers the request to the view on the server.

var docs = client.Get(view.Select(r => r.ItemId));
foreach (var doc in docs)
{
    Console.WriteLine(doc);
}

Now, let's say you have a Beer class in your application and you want to get instances of Beers when you iterate over the view. 

public class Beer
{
    [JsonProperty("name")]
    public string Name { get; set; }

    [JsonProperty("abc")]        
    public float ABV { get; set; }

    [JsonProperty("brewery_id")]
    public string BreweryId { get; set; }

    [JsonProperty("type")]
    public string Type { get; set; }

    [JsonProperty("description")]
    public string Description { get; set; }

}

The Beer class makes use of Newtonsoft.Json for serializing and deserializing JSON.  The client library also has a dependency on this assembly.

With the new Beer class, it's possible to query the view and tell the client that Beer instances should be the item returned by each enumeration of a row in the view.

var view = client.GetView<Beer>("beer", "by_name", true);

foreach(var beer in view)
{
    Console.WriteLine(beer.Name);
}

Two important changes to note in the snippet above.  First, when GetView is called, Beer is specified as the generic type.  Second, the third argument supplied to GetView tells the client to Get the original document and deserialize it to an instance of T or in this case, a Beer.

In the case where the value returned by a row is a projection of the indexed document, then strongly typed views are still possible. 

function (doc, meta) {
  if (doc.name && doc.type && doc.type == "beer") {
    emit(doc.name, { "beer_name" : doc.name, "beer_style" : doc.style });
  }    
}

Since the view now includes the beer name and style (this is an admittedly contrived use of projections) it is possible to strongly type the view query results - in this case to a BeerProjection class. 

public class BeerProjection
{
    [JsonProperty("beer_name")]
    public string Name { get; set; }

    [JsonProperty("beer_style")]
    public string Style { get; set; }
}

Removing the Boolean argument from the GetView call leaves the default of false and the client will then attempt to deserialize the value property of each view row into an instance of T, or in this case a BeerProjection.

var view = client.GetView<BeerProjection>("beer", "by_name");

foreach(var beer in view)
{
    Console.WriteLine(beer.Name);
}

Finally, if you wanted to perform a generic multi-get and use your own deserialization techniques, you could do something like the following:

var view = client.GetView("beer", "by_name");
var beers = client.Get(view.Select(v => v.ItemId)).Select(d =>
        JsonConvert.DeserializeObject<Beer>(d.Value as string)
    );

foreach(var beer in beers)
{
    Console.WriteLine(beer.Name);
}

 

Comments