Quantcast
Channel: chsakell's Blog
Viewing all articles
Browse latest Browse all 42

Azure Cosmos DB: DocumentDB API in Action

$
0
0
During tha last years, there has been an significant change regarding the amount of data being produced and consumed by applications while data-models and scemas are evolving more frequently than used to. Assuming a traditional application (makes use of relational databases) goes viral, those two combinations could easily bring it to an unfortunate state due to lack of scalability capabilities. This is where Azure Cosmos DB, planet NoSQL database as a service comes into the scene. In a nutchel, Azure Cosmos DB or DocumentDB if you prefer, is a fully managed by Microsoft Azure, incredibly scalable, queryable and last but not least, schema-free JSON document database. Here are the most important features that Azure Cosmos DB offers via the DocumentDB API:
  • Elastically scalable throughput and storage
  • Multi-region replication
  • Ad hoc queries with familiar SQL syntax
  • JavaScript execution within the database
  • Tunable consistency levels
  • Fully managed
  • Automatic indexing

Azure Cosmos DB is document based and when we refer to documents we mean JSON objects that can be managed through the Document API. If you are not familiar with the Azure Cosmos DB and its resources, here is the relationship between them.

This post will show you how to use the Document DB API to manipulate JSON documents in NoSQL DocumentDB collections. Let’s see in more detail what we are gonna see:

  • Create DocumentDB database and collections
  • CRUD operations: we will see in detail several ways to query, create, update and delete JSON documents
  • Create and consume JavaScript stored procedures, triggers and user defined functions
  • Add attachments to JSON documents
  • Create Generic Data DocumentDB repositories: ideally, we would like to have a single repository that could target a specific DocumentDB database with multiple collections
  • Use the famous Automapper to map DocumentDB documents to application domain models

What do you need to follow along with this tutorial? In case you don’t have an Azure Subscription, simply install the Azure Cosmos DB Emulator with which you can develop and test your DocumentDB based application locally. Are you ready? Let’s start!

Clone and explore the GitHub repository

I have already built an application that makes use of the DocumentDB API and the Azure Cosmos DB Emulator. Clone the repository from here and either open the solution in Visual Studio 2017 or run the following command to restore the required packages.

dotnet restore

Apparently the project is built in ASP.NET Core. Build the solution but before firing it up, make sure the Azure Comsos DB Emulator is up and running. In case you don’t know how to do this, search in the apps for Azure Cosmos DB Emulator and open the app. It will ask you to grant admimistrator permissions in order to start.

When the emulator starts, it will automatically open its Data Explorer on the browser at https://localhost:8081/_explorer/index.html. If it doesn’t, right click on the tray icon and select Open Data Explorer... Now you can run the app and initiate a DocumentDB database named Gallery and two collections, Pictures and Categories. The initializer class which we ‘ll examine in a moment, will also populate some mock data for you. At this point, what matters is to understand what exactly a collection and a document is, throught the emulator’s interface. Before examine what really happened on the emulator’s database, notice that the app is a Photo Gallery app.

Each picture, has a title and belongs to a category. Now let’s take a look at the emulator’s data explorer.

You can see how a collection and a JSON document looks like. A collection may have Stored Procedures, User Defined Functions and Triggers. A JSON document is of type Document and can be converted to an application’s domain model quite easily. Now let’s switch to code and see how to connect to a DocumentDB account and initiate database and collections.

Create Database and Collections

The first thing we need to do before create anything in a DocumentDB account is connect to it. The appsettings.json file contains the default DocumentDB Endpoint and Key to connect to the Azure DocumentDB Emulator. In case you had a Microsoft Azure DocumentDB account, you would have to place the relative endpoint and key here. Now open the DocumentDBInitializer class inside the Data folder. First of all, you need to install the Microsoft.Azure.DocumentDB.Core NuGet package. You create a DocumentClient instance using the endpoint and key of the DocumentDB account:

  Endpoint = configuration["DocumentDBEndpoint"];
  Key = configuration["DocumentDBKey"];

  client = new DocumentClient(new Uri(Endpoint), Key);
  

Before creating a database you have to check that doesn’t already exist.

  private static async Task CreateDatabaseIfNotExistsAsync(string DatabaseId)
  {
      try
      {
          await client.ReadDatabaseAsync(UriFactory.CreateDatabaseUri(DatabaseId));
      }
      catch (DocumentClientException e)
      {
          if (e.StatusCode == System.Net.HttpStatusCode.NotFound)
          {
              await client.CreateDatabaseAsync(new Database { Id = DatabaseId });
          }
          else
          {
              throw;
          }
      }
  }
  

The DatabaseId parameter is the database’s name and will be used for all queries against a database. When creating a database collection you may or may not provide a partitionkey. Partition keys are specified in the form of a JSON path, for example in our case and for the collection Pictures, we specified the partition key /category which represents the property Category in the PictureItem class.

  public class PictureItem
  {
      [JsonProperty(PropertyName = "id")]
      public string Id { get; set; }

      [Required]
      [JsonProperty(PropertyName = "title")]
      public string Title { get; set; }

      [Required]
      [JsonProperty(PropertyName = "category")]
      public string Category { get; set; }

      [JsonProperty(PropertyName = "dateCreated")]
      public DateTime DateCreated { get; set; }
  }
  

Partitioning in DocumentDB is an instrument to make a collection massively scale in terms of storage and throughput needs. Documents with the same partition key values are stored in the same physical partition (grouped together) and this is done and managed automatically for you by calculating and assign a hash of a partitionkey to the relative physical location. In order to understand the relationship between a partition and a collection, think that while a partition hosts one or more partition keys, a collection acts as the logical container of these physical partitions.

Documents with the same partition key are grouped together always in the same physical partition and if that group needs to grow more, Azure will automatically make any required transformations for this to happen (e.g shrink another group or move to a different partition). Always pick a partition key that leverages the maximum throughput of your DocumentDB account. In our case, assuming thousands of people uploading pictures with with different category at the same rate, then we would leverage the maximum throughput. On the other hand, if you pick a partition key such as the DateCreated, all pictures uploaded on the same date would end up to the same partition. Here is how you create a collection.

  private static async Task CreateCollectionIfNotExistsAsync(string DatabaseId, string CollectionId, string partitionkey = null)
  {
      try
      {
          await client.ReadDocumentCollectionAsync(UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId));
      }
      catch (DocumentClientException e)
      {
          if (e.StatusCode == System.Net.HttpStatusCode.NotFound)
          {
              if (string.IsNullOrEmpty(partitionkey))
              {
                  await client.CreateDocumentCollectionAsync(
                      UriFactory.CreateDatabaseUri(DatabaseId),
                      new DocumentCollection { Id = CollectionId },
                      new RequestOptions { OfferThroughput = 1000 });
              }
              else
              {
                  await client.CreateDocumentCollectionAsync(
                      UriFactory.CreateDatabaseUri(DatabaseId),
                      new DocumentCollection
                      {
                          Id = CollectionId,
                          PartitionKey = new PartitionKeyDefinition
                          {
                              Paths = new Collection<string> { "/" + partitionkey }
                          }
                      },
                      new RequestOptions { OfferThroughput = 1000 });
              }

          }
          else
          {
              throw;
          }
      }
  }
 

Generic Data DocumentDB repositories Overview

Now that we have created the database and collections, we need an elegant way to CRUD against them. The requirements for the repositories are the following.

  • A repository should be able to target a specific DocumentDB database
  • A repository should be able to target all collections inside a DocumentDB database
  • The repository’s actions are responsible for converting documents to domain models, such as the PictureItem and the CategoryItem

Having these requirements in mind, I used the following repository pattern: At the top level there is a generic interface named IDocumentDBRepository.

public interface IDocumentDBRepository<DatabaseDB>
  {
      Task<T> GetItemAsync<T>(string id) where T : class;

      Task<T> GetItemAsync<T>(string id, string partitionKey) where T : class;

      Task<Document> GetDocumentAsync(string id, string partitionKey);

      Task<IEnumerable<T>> GetItemsAsync<T>() where T : class;

      Task<IEnumerable<T>> GetItemsAsync<T>(Expression<Func<T, bool>> predicate) where T : class;

      IEnumerable<T> CreateDocumentQuery<T>(string query, FeedOptions options) where T : class;

      Task<Document> CreateItemAsync<T>(T item) where T : class;

      Task<Document> CreateItemAsync<T>(T item, RequestOptions options) where T : class;

      Task<Document> UpdateItemAsync<T>(string id, T item) where T : class;

      Task<ResourceResponse<Attachment>> CreateAttachmentAsync(string attachmentsLink, object attachment, RequestOptions options);

      Task<ResourceResponse<Attachment>> ReadAttachmentAsync(string attachmentLink, string partitionkey);

      Task<ResourceResponse<Attachment>> ReplaceAttachmentAsync(Attachment attachment, RequestOptions options);

      Task DeleteItemAsync(string id);

      Task DeleteItemAsync(string id, string partitionKey);

      Task<StoredProcedureResponse<dynamic>> ExecuteStoredProcedureAsync(string procedureName, string query, string partitionKey);

      Task InitAsync(string collectionId);
  }

There is a base abstract class that implements all of the interface’s methods except the InitAsync.

public abstract class DocumentDBRepositoryBase<DatabaseDB> : IDocumentDBRepository<DatabaseDB>
  {
      #region Repository Configuration

      protected string Endpoint = string.Empty;
      protected string Key = string.Empty;
      protected string DatabaseId = string.Empty;
      protected string CollectionId = string.Empty;
      protected DocumentClient client;
      protected DocumentCollection collection;

      #endregion

      public DocumentDBRepositoryBase()
      {

      }

      // Code ommitted

      public abstract Task InitAsync(string collectionId);

Don’t worry about the implementation, we ‘ll check it later on the CRUD section. Last but not least, there are the concrete classes that can finally target specific DocumentDB database. In our case, we want a repository to target the Gallery database and its collections we created on the first step.

public class GalleryDBRepository : DocumentDBRepositoryBase<GalleryDBRepository>, IDocumentDBRepository<GalleryDBRepository>
  {
      public GalleryDBRepository(IConfiguration configuration)
      {
          Endpoint = configuration["DocumentDBEndpoint"];
          Key = configuration["DocumentDBKey"];
          DatabaseId = "Gallery";
      }

      public override async Task InitAsync(string collectionId)
      {
          if (client == null)
              client = new DocumentClient(new Uri(Endpoint), Key);

          if (CollectionId != collectionId)
          {
              CollectionId = collectionId;
              collection = await client.ReadDocumentCollectionAsync(UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId));
          }
      }
  }

When we want to CRUD agains a specific collection we call the InitAsync method passing as a parameter the collection id. Make sure you register your repositories in the dependcy injection on the Starup class.

public void ConfigureServices(IServiceCollection services)
{
    // Repositories
    services.AddScoped<IDocumentDBRepository<GalleryDBRepository>, GalleryDBRepository>();

    // Add framework services.
    services.AddMvc();

    // Code omitted
}

It is more likely that you wont need more that two or three DocumentDB databases, so a single repository should be more than enough.

CRUD operations using the DocumentDB API

The Index action of the PicturesController reads all the pictures inside the Pictures collection. First of all we get an instance of the repository. As we previously saw, its constructor will also initiate the credentials to connect to the Gallery DocumentDB database.

public class PicturesController : Controller
{
    private IDocumentDBRepository<GalleryDBRepository> galleryRepository;

    public PicturesController(IDocumentDBRepository<GalleryDBRepository> galleryRepository)
    {
        this.galleryRepository = galleryRepository;
    }

    // code omitted

The Index action may or may not receive a parameter to filter the picture results based on their title. This means that we want to be able either query all the items of a collection or pass a predicate and filter them. Here are both the implementations.

public async Task<IEnumerable<T>> GetItemsAsync<T>() where T : class
{
    IDocumentQuery<T> query = client.CreateDocumentQuery<T>(
        UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId),
        new FeedOptions { MaxItemCount = -1, EnableCrossPartitionQuery = true })
        .AsDocumentQuery();

    List<T> results = new List<T>();
    while (query.HasMoreResults)
    {
        results.AddRange(await query.ExecuteNextAsync<T>());
    }

    return results;
}

public async Task<IEnumerable<T>> GetItemsAsync<T>(Expression<Func<T, bool>> predicate) where T : class
{
    IDocumentQuery<T> query = client.CreateDocumentQuery<T>(
        UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId),
        new FeedOptions { MaxItemCount = -1, EnableCrossPartitionQuery = true })
        .Where(predicate)
        .AsDocumentQuery();

    List<T> results = new List<T>();
    while (query.HasMoreResults)
    {
        results.AddRange(await query.ExecuteNextAsync<T>());
    }

    return results;
}

The DatabaseId has already defined on the repository’s constructor but the CollectionId needs to be initiated using the InitAsync method as follow:

[ActionName("Index")]
public async Task<IActionResult> Index(int page = 1, int pageSize = 8, string filter = null)
{
    await this.galleryRepository.InitAsync("Pictures");
    IEnumerable<PictureItem> items;

    if (string.IsNullOrEmpty(filter))
        items = await this.galleryRepository.GetItemsAsync<PictureItem>();
    else
    {
        items = await this.galleryRepository
            .GetItemsAsync<PictureItem>(picture => picture.Title.ToLower().Contains(filter.Trim().ToLower()));
        ViewBag.Message = "We found " + (items as ICollection<PictureItem>).Count + " pictures for term " + filter.Trim();
    }
    return View(items.ToPagedList(pageSize, page));
}

Here you can see for the first time, how we convert a Document item to a Domain model class. Using the same repository but targeting the Categories collection, we will be able to query CategoryItem items.

Create a Trigger

Let’s switch gears for a moment and see how to create a JavaScript trigger. We want our picture documents to get a DateCreated value when being added on the collection. For this we create a function that can read the document object from the request. This is the Triggers/createDate.js file.

function createDate() {
    var context = getContext();
    var request = context.getRequest();

    // document to be created in the current operation
    var documentToCreate = request.getBody();

    //if (!("dateCreated" in documentToCreate)) {
        var date = new Date();
        documentToCreate.dateCreated = date.toUTCString();
    //}

    // update the document
    request.setBody(documentToCreate);
}

The documentDB initializer class has the following method, that registers a Pre-Trigger type.

private static async Task CreateTriggerIfNotExistsAsync(string databaseId, string collectionId, string triggerName, string triggerPath)
{
    DocumentCollection collection = await client.ReadDocumentCollectionAsync(UriFactory.CreateDocumentCollectionUri(databaseId, collectionId));

    string triggersLink = collection.TriggersLink;
    string TriggerName = triggerName;

    Trigger trigger = client.CreateTriggerQuery(triggersLink)
                            .Where(sp => sp.Id == TriggerName)
                            .AsEnumerable()
                            .FirstOrDefault();

    if (trigger == null)
    {
        // Register a pre-trigger
        trigger = new Trigger
        {
            Id = TriggerName,
            Body = File.ReadAllText(Path.Combine(Config.ContentRootPath, triggerPath)),
            TriggerOperation = TriggerOperation.Create,
            TriggerType = TriggerType.Pre
        };

        await client.CreateTriggerAsync(triggersLink, trigger);
    }
}

One important thing to notice here is that a trigger is registered on a collection level (collection.TriggersLink). Now when we want to create a document and also require a trigger to run, we need to pass it in the RequestOptions. Here is how you create a document with or without request options.

public async Task<Document> CreateItemAsync<T>(T item) where T : class
{
    return await client.CreateDocumentAsync(UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId), item);
}

public async Task<Document> CreateItemAsync<T>(T item, RequestOptions options) where T : class
{
    return await client.CreateDocumentAsync(UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId), item, options);
}

And here is how the CreateAsync action creates a PictureItem document.

await this.galleryRepository.InitAsync("Pictures");

RequestOptions options = new RequestOptions { PreTriggerInclude = new List<string> { "createDate" } };

Document document = await this.galleryRepository.CreateItemAsync<PictureItem>(item, options);

The picture item instance parameter, has a Category value which will be used as the partition key value. You can confirm this in the DocumentDB emulator interface.

Create attachments

Each document has an AttachmentsLink where you can store attachments, in our case we ‘ll store a file attachment. Mind that you should avoid storing images attachments but instead you should store their links, otherwise you ‘ll probably face performance issues. In our application we store the images because we just want to see how to store files. In a production application we would store the images as blobs in an Azure Blog Storage account and store the blob’s link as an attachment to the document. Here is how we create, read and update a document attachment.

public async Task<ResourceResponse<Attachment>> CreateAttachmentAsync(string attachmentsLink, object attachment, RequestOptions options)
{
    return await client.CreateAttachmentAsync(attachmentsLink, attachment, options);
}

public async Task<ResourceResponse<Attachment>> ReadAttachmentAsync(string attachmentLink, string partitionkey)
{
    return await client.ReadAttachmentAsync(attachmentLink, new RequestOptions() { PartitionKey = new PartitionKey(partitionkey) });
}

public async Task<ResourceResponse<Attachment>> ReplaceAttachmentAsync(Attachment attachment, RequestOptions options)
{
    return await client.ReplaceAttachmentAsync(attachment, options);
}

The CreateAsync action method checks if there’s a file uploaded while posting to action and if so, creates the attachment.

if (file != null)
{
    var attachment = new Attachment { ContentType = file.ContentType, Id = "wallpaper", MediaLink = string.Empty };
    var input = new byte[file.OpenReadStream().Length];
    file.OpenReadStream().Read(input, 0, input.Length);
    attachment.SetPropertyValue("file", input);
    ResourceResponse<Attachment> createdAttachment = await this.galleryRepository.CreateAttachmentAsync(document.AttachmentsLink, attachment, new RequestOptions() { PartitionKey = new PartitionKey(item.Category) });
}

Since you create an attachment to a collection that uses partition keys, you also need to provide the one related to the document at which the attachment will be created.

Reading Documents & Automapper

When reading a document, you can either get a domain model instance or the generic Document.

public async Task<T> GetItemAsync<T>(string id, string partitionKey) where T : class
    {
        try
        {
            Document document = await client.ReadDocumentAsync(UriFactory.CreateDocumentUri(DatabaseId, CollectionId, id), new RequestOptions { PartitionKey = new PartitionKey(partitionKey) });
            return (T)(dynamic)document;
        }
        catch (DocumentClientException e)
        {
            if (e.StatusCode == System.Net.HttpStatusCode.NotFound)
            {
                return null;
            }
            else
            {
                throw;
            }
        }
    }

public async Task<Document> GetDocumentAsync(string id, string partitionKey)
{
    try
    {
        Document document = await client.ReadDocumentAsync(UriFactory.CreateDocumentUri(DatabaseId, CollectionId, id), new RequestOptions { PartitionKey = new PartitionKey(partitionKey) });
        return document;
    }
    catch (DocumentClientException e)
    {
        if (e.StatusCode == System.Net.HttpStatusCode.NotFound)
        {
            return null;
        }
        else
        {
            throw;
        }
    }
}

When you need to use the domain’s properties, the first one is preferred while when you need to access document based properties such as the document’s attachments link, the second one fits best. But what if you need both? Should you query twice? Fortunately no. The generic Document instance has all the properties you need to get a domain model instance. All you have to do, is use the GetPropertyValue value for all domain model’s properties. However, instead of doing this every time you want to create a domain model you can use AutoMapper as follow:

public DocumentMappingProfile()
{
    CreateMap<Document, PictureItem>()
        .ForAllMembers(opt =>
        {
            opt.MapFrom(doc => doc.GetPropertyValue<object>(opt.DestinationMember.Name.ToLower()));
        });


    // Could be something like this..

    /*
    CreateMap<Document, PictureItem>()
        .ForMember(vm => vm.Id, map => map.MapFrom(doc => doc.GetPropertyValue<string>("id")))
        .ForMember(vm => vm.Title, map => map.MapFrom(doc => doc.GetPropertyValue<string>("title")))
        .ForMember(vm => vm.Category, map => map.MapFrom(doc => doc.GetPropertyValue<string>("category")))
        .ForMember(vm => vm.DateCreated, map => map.MapFrom(doc => doc.GetPropertyValue<DateTime>("dateCreated")));
    */
}

The comments show how you would do manually the mapping for each property. But we figured out a more generic way didn’t we? Here’s how the EditAsync action reads a picture document.

[ActionName("Edit")]
public async Task<ActionResult> EditAsync(string id, string category)
{
    if (id == null)
    {
        return BadRequest();
    }

    await this.galleryRepository.InitAsync("Pictures");

    Document document = await this.galleryRepository.GetDocumentAsync(id, category);

    // No need for this one - AutoMapper will make the trick
    //PictureItem item = await this.galleryRepository.GetItemAsync<PictureItem>(id, category);
    PictureItem item = Mapper.Map<PictureItem>(document);

    if (item == null)
    {
        return NotFound();
    }

    await FillCategoriesAsync(category);

    return View(item);
}

In case you don’t want to use AutoMapper, you could achieve the deserialization using Newtonsoft.Json or the dynamic keyword. The following three statements will return the same result.

PictureItem itemWithAutoMapper = Mapper.Map<PictureItem>(document);
PictureItem itemWithNewtonSoft = JsonConvert.DeserializeObject<PictureItem>(document.ToString());
PictureItem itemWithDynamic = (dynamic)document;

Updating and deleting documents

Updating a document is prety simple.

public async Task<Document> UpdateItemAsync<T>(string id, T item) where T : class
    {
        return await client.ReplaceDocumentAsync(UriFactory.CreateDocumentUri(DatabaseId, CollectionId, id), item);
    }

Mind though that in case you change the value for the partition key, you cannot just simply update the document, since the new partition key may be stored in a different physical partition. In this case as you can see in the EditAsync POST method, you need to delete and re-created the item from scratch using the new partition key value.

[HttpPost]
[ActionName("Edit")]
[ValidateAntiForgeryToken]
public async Task<ActionResult> EditAsync(PictureItem item, [Bind("oldCategory")] string oldCategory, IFormFile file)
{
    if (ModelState.IsValid)
    {
        await this.galleryRepository.InitAsync("Pictures");

        Document document = null;

        if (item.Category == oldCategory)
        {
            document = await this.galleryRepository.UpdateItemAsync(item.Id, item);

            if (file != null)
            {
                var attachLink = UriFactory.CreateAttachmentUri("Gallery", "Pictures", document.Id, "wallpaper");
                Attachment attachment = await this.galleryRepository.ReadAttachmentAsync(attachLink.ToString(), item.Category);

                var input = new byte[file.OpenReadStream().Length];
                file.OpenReadStream().Read(input, 0, input.Length);
                attachment.SetPropertyValue("file", input);
                ResourceResponse<Attachment> createdAttachment = await this.galleryRepository.ReplaceAttachmentAsync(attachment, new RequestOptions() { PartitionKey = new PartitionKey(item.Category) });
            }
        }
        else
        {
            await this.galleryRepository.DeleteItemAsync(item.Id, oldCategory);

            document = await this.galleryRepository.CreateItemAsync(item);

            if (file != null)
            {
                var attachment = new Attachment { ContentType = file.ContentType, Id = "wallpaper", MediaLink = string.Empty };
                var input = new byte[file.OpenReadStream().Length];
                file.OpenReadStream().Read(input, 0, input.Length);
                attachment.SetPropertyValue("file", input);
                ResourceResponse<Attachment> createdAttachment = await this.galleryRepository.CreateAttachmentAsync(document.AttachmentsLink, attachment, new RequestOptions() { PartitionKey = new PartitionKey(item.Category) });
            }
        }

        return RedirectToAction("Index");
    }

    return View(item);
    }

Like most of the methods, depending if the collection created using a partition key or not, the DeleteItemAsync may or may not require a partition key value.

public async Task DeleteItemAsync(string id)
{
    await client.DeleteDocumentAsync(UriFactory.CreateDocumentUri(DatabaseId, CollectionId, id));
}

public async Task DeleteItemAsync(string id, string partitionKey)
{
    await client.DeleteDocumentAsync(UriFactory.CreateDocumentUri(DatabaseId, CollectionId, id), new RequestOptions { PartitionKey = new PartitionKey(partitionKey) });
}

If you try to query a collection that requires a partition key and you don’t provide one, you ‘ll get an exception. On the other hand if your query indeed must search among all partitions then all you have to do use the EnableCrossPartitionQuery = true in the FeedOptions.

Stored Procedures

A collection may have stored procedures as well. Our application uses the sample bulkDelete stored procedure from the official Azure DocumentDB repository, to remove pictures from the Pictures collection. The SP accepts an SQL query as a parameter. First, let’s register the stored procedure on the collection.

private static async Task CreateStoredProcedureIfNotExistsAsync(string databaseId, string collectionId, string procedureName, string procedurePath)
{
    DocumentCollection collection = await client.ReadDocumentCollectionAsync(UriFactory.CreateDocumentCollectionUri(databaseId, collectionId));

    string storedProceduresLink = collection.StoredProceduresLink;
    string StoredProcedureName = procedureName;

    StoredProcedure storedProcedure = client.CreateStoredProcedureQuery(storedProceduresLink)
                            .Where(sp => sp.Id == StoredProcedureName)
                            .AsEnumerable()
                            .FirstOrDefault();

    if (storedProcedure == null)
    {
        // Register a stored procedure
        storedProcedure = new StoredProcedure
        {
            Id = StoredProcedureName,
            Body = File.ReadAllText(Path.Combine(Config.ContentRootPath, procedurePath))
        };
        storedProcedure = await client.CreateStoredProcedureAsync(storedProceduresLink,
    storedProcedure);
    }
}

You can execute a stored procedure as follow:

public async Task<StoredProcedureResponse<dynamic>> ExecuteStoredProcedureAsync(string procedureName, string query, string partitionKey)
    {
        StoredProcedure storedProcedure = client.CreateStoredProcedureQuery(collection.StoredProceduresLink)
                                .Where(sp => sp.Id == procedureName)
                                .AsEnumerable()
                                .FirstOrDefault();

        return await client.ExecuteStoredProcedureAsync<dynamic>(storedProcedure.SelfLink, new RequestOptions { PartitionKey = new PartitionKey(partitionKey) }, query);

    }

The DeleteAll action method deletes either all pictures from a selected category or all the pictures in the collection. As you ‘ll see, the query passed to the bulkDelete stored procedure is the same, what changes is the partition key that can target pictures on an individual category.

[ActionName("DeleteAll")]
public async Task<ActionResult> DeleteAllAsync(string category)
{
    await this.galleryRepository.InitAsync("Categories");

    var categories = await this.galleryRepository.GetItemsAsync<CategoryItem>();

    await this.galleryRepository.InitAsync("Pictures");

    if (category != "All")
    {
        var response = await this.galleryRepository.ExecuteStoredProcedureAsync("bulkDelete", "SELECT * FROM c", categories.Where(cat => cat.Title.ToLower() == category.ToLower()).First().Title);
    }
    else
    {

        foreach (var cat in categories)
        {
            await this.galleryRepository.ExecuteStoredProcedureAsync("bulkDelete", "SELECT * FROM c", cat.Title);
        }
    }

    if (category != "All")
    {
        await FillCategoriesAsync("All");
        ViewBag.CategoryRemoved = category;

        return View();
    }
    else
        return RedirectToAction("Index");
}

User Defined Functions

You can register a UDF as follow:

private static async Task CreateUserDefinedFunctionIfNotExistsAsync(string databaseId, string collectionId, string udfName, string udfPath)
{
    DocumentCollection collection = await client.ReadDocumentCollectionAsync(UriFactory.CreateDocumentCollectionUri(databaseId, collectionId));

    UserDefinedFunction userDefinedFunction =
                client.CreateUserDefinedFunctionQuery(collection.UserDefinedFunctionsLink)
                    .Where(udf => udf.Id == udfName)
                    .AsEnumerable()
                    .FirstOrDefault();

    if (userDefinedFunction == null)
    {
        // Register User Defined Function
        userDefinedFunction = new UserDefinedFunction
        {
            Id = udfName,
            Body = System.IO.File.ReadAllText(Path.Combine(Config.ContentRootPath, udfPath))
        };

        await client.CreateUserDefinedFunctionAsync(collection.UserDefinedFunctionsLink, userDefinedFunction);
    }
}

Our application registres the toUpperCase UDF that returns a value in upper case.

function toUpperCase(item) {
    return item.toUpperCase();
}

The FillCategoriesAsync method can return each category title in upper case if required.

private async Task FillCategoriesAsync(string selectedCategory = null, bool toUpperCase = false)
{
    IEnumerable<CategoryItem> categoryItems = null;

    await this.galleryRepository.InitAsync("Categories");

    List<SelectListItem> items = new List<SelectListItem>();

    if (!toUpperCase)
        categoryItems = await this.galleryRepository.GetItemsAsync<CategoryItem>();
    else
        categoryItems = this.galleryRepository.CreateDocumentQuery<CategoryItem>("SELECT c.id, udf.toUpperCase(c.title) as Title FROM Categories c", new FeedOptions() { EnableCrossPartitionQuery = true });

    // code omitted

Here we can see, an alternative and powerful way for quering JSON documents using a combination of SQL and JavaScript syntax. You can read more about the Azure Cosmos DB Query syntax here.

That’s it we finished! We have seen many things related to the DocumentDB API, starting from installing the Azure Cosmos DB Emulator and creating a Database with some collections to CRUD operations and generic data repositories. You can download the project for this post, here.

In case you find my blog’s content interesting, register your email to receive notifications of new posts and follow chsakell’s Blog on its Facebook or Twitter accounts.

Facebook Twitter
.NET Web Application Development by Chris S.
facebook twitter-small
twitter-small


Viewing all articles
Browse latest Browse all 42

Trending Articles