NoSQL Zone is brought to you in partnership with:

Baxter Denney is Director of Marketing at Couchbase. Couchbase is the NoSQL leader, with production deployments at AOL, Deutsche Post, NTT Docomo, Salesforce.com, Starbucks, Zynga, and hundreds of other global enterprises. Couchbase Server, our NoSQL database offering, delivers a more scalable, high-performance and cost-effective approach to data management than relational database technology. It is particularly well suited for storing the data behind web applications deployed on modern virtualized or cloud infrastructures. Baxter has posted 4 posts at DZone. You can read more from them at their website. View Full User Profile

Using C# Domain Objects to Define Couchbase Views

10.29.2012
| 2894 views |
  • submit to reddit

The Couchbase Client Library 1.2-Beta includes a new API for some basic cluster management.  There are methods for creating, removing and listing buckets.  There are similar methods for managing design documents.  These new features are found in the new class CouchbaseCluster, under the Couchbase.Management namespace.  The main benefit of this new API is that it's now possible to allow your application to create its bucket and set its design documents when it starts up. 

One of the overloads for creating new design documents allows you to specify a Stream as the source of the design document.  With this version, it's easy to create design documents from a set of files.  You can also just specify a string containing the document. 

I recently had an idea to create a simple command line utility to use these design doc management methods to automate the creation of basic views.  The result of this idea is up on GitHub at https://github.com/jzablocki/couchbase-model-views.  Using this framework, you'll be able to automate the creation of views simply by decorating your existing model classes with custom attributes. 

Consider a Beer class with a properties for Name, Description, Brewery and ABV. 

public class Beer
{
    public string Id { get; set; }

    public string Name { get; set; }

    public string Description { get; set; }

    public float ABV { get; set; }

    public string Brewery { get; set; }
}

This class maps to "beer" documents in your Couchbase bucket. 

{
   "name": "Samuel Adams Summer Ale",
   "abv": 5.2,
   "type": "beer",
   "brewery_id": "110f04db06",
   "description": "Bright and citrusy, brewed with mysterious grains of...
}

To achieve this mapping, you'd likely use Newtonsoft.Json's property mapping attributes. 

public class Beer
{
    public string Id { get; set; }

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

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

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

    [JsonProperty("breweryId")]
    public string Brewery { get; set; }
}

Using two new attributes found in the CouchbaseModelViews.Framework project, you could further decorate this class to declare which properties in this class should be indexed by Couchbase Server.  These attributes are CouchbaseDesignDocument and CouchbaseViewKey.

[CouchbaseDesignDoc("beers", "beer")]
public class Beer
{
    public string Id { get; set; }

    [JsonProperty("name")]
    [CouchbaseViewKey("by_abv_and_name", "name", 1)]
    [CouchbaseViewKey("by_name", "name")]
    public string Name { get; set; }

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

    [JsonProperty("abv")]
    [CouchbaseViewKey("by_abv_and_name", "abv", 0)]
    public float ABV { get; set; }

    [JsonProperty("breweryId")]
    [CouchbaseViewKey("by_brewery", "breweryId")]
    public string Brewery { get; set; }
}

The CouchbaseDesignDoc attribute is set on a model class.  It uses a plain-old-CLR-object (POCO) to define a design document.  If you omit the name argument, a design document with the same name (lowercased) as the class will be created.  If you omit the type argument, views will check that documents have a type property with the value of the name (lowercased) of the class. 

The CouchbaseViewKey attribute is set on properties in a POCO class that should be indexed.  For example, the Name property in the Beer class above has a CouchbaseViewKey attribute on it with the arguments "by_name" and "name."  The view that will result from these values is as follows:

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

The "by_name" argument is the name of this view and the "name" argument defines what property is checked for existence and emitted.

It's also possible to include composite keys by decorating multiple properties with CouchbaseViewKey attributes containing the same value for the viewName parameter.  Composite keys are demonstrated with "by_abv_and_name" on the ABV and Name properties.  Notice also that there is an optional order parameter that allows you to set the order in which properties are emitted. 

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

Once you've decorated your class with the appropriate attributes, you can use the CouchbaseModelViewsGenerator project to run the models through the view generator.  This is a simple command line project that requires an app.config with a section listing all assemblies containing models that will be used to create views.

<sectionGroup name="modelViews">
  <section name="assemblies" type="System.Configuration.DictionarySectionHandler"/>      
</sectionGroup>

<modelViews>
    <assemblies>
      <add key="DemoModels" value="CouchbaseModelViews.DemoModels" />
    </assemblies>
</modelViews>

Additionally, you will need to configure the CouchbaseClusterCouchbaseCluster instances are creating using the same configuration (code or app.config) as the existing CouchbaseClient.  However, there are now an additional two properties you can set to provide the administrator credentials. 

<couchbase>
    <servers bucket="default" bucketPassword="" username="Administrator" password="qwerty">
        <add uri="http://localhost:8091/pools" />
    </servers>    
</couchbase>

Once you've setup your app.config, make sure you have the listed assemblies in the bin directory where they can be loaded.  The easiest way to achieve this discover-ability is of course to reference them.  However, if you just want to use a compiled version of the console app, then simply copy them into the bin directory.

When executed, the framework will create a "beers" design document that looks like the following:

{
  "views": {
    "by_abv_and_name": {
      "map": "function(doc, meta) { \r\n\t if (doc.type == \"beer\" && doc.abv && doc.name) { \r\n\t\t emit([doc.abv, doc.name], null); \r\n\t } \r\n }"
    },
    "by_name": {
      "map": "function(doc, meta) { \r\n\t if (doc.type == \"beer\" && doc.name) { \r\n\t\t emit(doc.name, null); \r\n\t } \r\n }"
    },
    "by_brewery": {
      "map": "function(doc, meta) { \r\n\t if (doc.type == \"beer\" && doc.breweryId) { \r\n\t\t emit(doc.breweryId, null); \r\n\t } \r\n }"
    }
  }
}

The console contains no actual view creation logic.  It just orchestrates calls to the various plumbing components inside the Framework project.  If you prefer to include the view creation inside of Global.asax or some other app start event, you could simply make these calls yourself.  There are four primary components of the framework. 

  • The ConfigParser simply reads the list of assemblies from the config section and generates an enumerable list of assemblies. 
  • The ViewBuilder class takes an assembly or enumerable list of assemblies and iterates over each of the types.  For each type found with a CouchbaseDesignDoc attribute, views are constructed.  The Build method of the ViewBuilder returns a dictionary, where the keys are design doc names and the values are the actual design docs. 
  • The DesignDocManager takes a dictionary with doc name, design doc pairs and uses the CouchbaseCluster to create design documents. 
  • There's also a ViewRunner class, which will run the newly created views.
    var assemblies = ConfigParser.GetAssemblies();
    var builder = new ViewBuilder();
    builder.AddAssemblies(assemblies.ToList());
    var designDocs = builder.Build();
    var ddManager = new DesignDocManager();
    ddManager.Create(designDocs, (s) => Console.WriteLine("Created {0} design doc", s));
    var runner = new ViewRunner();
    runner.Run(designDocs, (k, v, s) => Console.WriteLine("[{0}::{1}] Key {2}", k, v, s["key"]), 5);

There is no coupling between any two of these three components and you can use them as you wish.  If you have your own assembly gathering facility for example, simply pass it to the ViewBuilder.Build to render the JSON needed to create the views. 

At this point, the framework is mostly complete and reasonably tested.  The code has a few more loops than I'd like and I might have overused Tuples , but it works.  Feel free to use it in your projects.  Keep in mind, this is a Couchbase Labs project and isn't officially supported.  Remember too that it's using the Beta version of the .NET Couchbase Client Library and the API is subject to change.

 

 

 

Published at DZone with permission of its author, Baxter Denney. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)