Fast ASP.NET Core development with Azure Cosmos DB and DocumentDB package

Posted by: Francesco Abbruzzese , on 2/8/2018, in Category Microsoft Azure
Views: 46547
Abstract: Use the DocumentDB Nuget package to quickly build a simple but complete ASP.NET MVC core application based on Azure Cosmos DB.

The Mvc Controls Toolkit Core is a free collection of tools for building professional level ASP.NET Core MVC web applications that encompasses all layers a professional MVC application is composed of: data and business layer tools, controller tools and View tools.

In this ‘how to’ article, I will demonstrate how to use the MvcControlsToolkit.Business.DocumentDB Nuget package to quickly build a simple but complete ASP.NET MVC core application based on CosmosDB.

Are you keeping up with new developer technologies? Advance your IT career with our Free Developer magazines covering C#, Patterns, .NET Core, MVC, Azure, Angular, React, and more. Subscribe to the DotNetCurry (DNC) Magazine for FREE and download all previous, current and upcoming editions.

What is Azure Cosmos DB?

Cosmos DB is a distributed database available on Azure. Data can be replicated in several geographical areas, and can benefit from the redundancy and scalability features, typically offered by cloud services.

Cosmos DB is not a relational database. Its data elements are called “Documents” that may be represented as JSON objects. These documents have no predefined structure: each document may have different properties, and may contain nested JSON objects and arrays.

Documents are grouped into “Collections” that in general, are sets of heterogeneous documents. Finally, collections are grouped into databases. All the document properties are automatically indexed, but it is possible to perform some tuning on the way indexes are built, and on how the properties are to be indexed.

Cosmos DB not only offers a different data model, but is also cheaper than the SQL Azure Database, that is a distributed relational database,

In fact, Cosmos DB offers somewhat different data models, namely:

1. The strict JSON Document/Collection data model that we just discussed

2. A Graph data model, where documents may be either vertices or labels of an overall graph. This model is useful in representing the semantic relations that connects real-life entities, similar to relationships in a social application. This model may be queried with the Gremlin language.

3. Two different Table/Rows/Columns based data models, namely the old Azure tables and Cassandra that is based on the Apache Cassandra Api.

In this article, I will focus on the strict JSON Document/Collections data model that may be queried either with the same API of the old DocumentDB Azure service, or with the same API of the MongoDB database.

We will focus on the DocumentDB interface that is based on a subset of SQL, and may be queried with LINQ too. In order to use LINQ, JSON objects are mapped to .NET classes with the help of JSON.NET, as we will see later in this article.

Cosmos DB DocumentDB Interface Limitations

Cosmos DB DocumentDB interface has some limitations that are quite relevant in most practical applications.

First of all, documents can’t be partially updated, but can only be replaced completely.

Thus, for instance, suppose we would like to change the “Name” property of a Person complex object that might also have other properties like “Surname”, “Spouse”. These properties might further contain a nested Person object, and “Children” that might contain a nested enumeration of Person objects.

As you can see, we are forced to read the entire object modifying it and finally replacing the previous version, with the fully modified object. This limitation comes from the design choice of avoiding document “locks” to improve performance.

Another limitation is the ability to perform “select” operations on nested enumerations.

Thus, if we consider the previous Person document example again in a LINQ query, we may project the root Person object in a DTO class that contains just some of the Person properties, but then we are forced to take the whole Person object, contained in its Children property.

This limitation is quite “troubling” because either we export the whole Person object from the data layer to the presentation layer, thus breaking the separation between the two layers; or we are forced to define a chain of intermediate objects and perform a chain of copy operation to get an object tree that doesn’t contain data layer objects.

Another shortcoming of performance constraints is the absence of support for the LINQ “skip” operation that is needed for paging efficiently query results.

A minor, but relevant limitation is the support for just one property in sorting. Thus, for instance, it is not possible to sort Person documents first by Surname and then by Name, but one has to choose between either Surname or Name.

Creating an Azure Cosmos DB account

If you already have an Azure account, login to your account. Alternatively create a free trial account following the instructions on this page. Once logged in, go to the portal (by clicking the portal link in the top of the page). Then click the “Create a resource button” (“+ New” in the top of the page) and search “database”.

select-cosmosdb

Now click on “Azure Cosmos DB”. In the info’s page that appears, click the “create” button at the end of the page. You should see a page similar to this one:

define-cosmosdb

Choose an ID for your Cosmos DB account (different from the one shown in the image), and select the SQL API. You may create a new resource group or you may use an existing one.

Then select a location next to your country, and check the “Pin to dashboard” so your Cosmos DB will appear in your Azure dashboard. Avoid checking “Enable geo-redundancy” since this might quickly diminish your free trial credit. You may add geo-redundancy at a later time. Finally click “Create”.

The account creation takes a few minutes. During account creation, the portal displays the “Deploying Azure Cosmos DB tile” on the right side, you may need to scroll right on your dashboard to see the tile. There is also a progress bar displayed near the top of the screen. You can watch either to track progress.

deploying-azure-cosmosdb

Once deployed, click on the newly created resource, and then find and click the “Keys” menu item.

keys

This will show a page containing connection information. You need the Cosmos DB URI and the PRIMARY KEY. Store them somewhere, or simply keep that page opened.

 

The Mvc Controls Toolkit Core DocumentDB package

MvcControlsToolkit.Business.DocumentDB Nuget package is part of the Mvc Controls Toolkit Core tools. Though this article does not require a priori knowledge of the Mvc Controls Toolkit, a complete introduction to the Mvc Controls Toolkit may be found at  www.dotnetcurry.com/aspnet-mvc/1376/full-stack-development-using-aspnet-mvc-core-toolkit.

All DocumentDB specific details are taken care of by the Update, Add, Delete, UpdateList, GetById, and GetPage methods of a DocumentDB specific implementation of an ICRUDRepository interface.

All repository methods operate on DTO/ViewModels and take care of mapping DTO/ViewModels from/to the actual database objects.

Mappings are recursive and involve objects and collections recursively contained in the DTO/ViewModels. Mappings are based on naming conventions and specifications furnished through LINQ expressions.

The naming conventions automatically handle object flattening and de-flattening, so LINQ expression are usually very simple since they don’t need to specify how each property is mapped, but just: 1) exceptions to the name conventions, and 2) which part of the object tree must be mapped. In a few words GetById and GetPage specify only the subtree of each DocumentDB entry needed by the application into DTO/ViewModels.

The “DocumentDBCRUDRepository” class of the MvcControlsToolkit.Business.DocumentDB Nuget package implements the “ICRUDRepository” interface that in the Mvc Controls Toolkit, defines an extensible standard for connecting controllers with the DB layer. It contains a few common update and query operations expressed directly in terms of DTO/ViewModels, so that data objects are hidden to the presentation layer.

All the update/add/delete and query methods of “ICRUDRepository” will be demonstrated later in this article.

All “DocumentDBCRUDRepository” updates are stacked till the “async SaveChanges” “ICRUDRepository” method is called, then all operations are executed in parallel to maximize performance. All failed operations are reported by the “DocumentsUpdateException<M>” aggregated exceptions and may be retried by passing this exception to the “RetryChanges” method.

Overcoming the Limitations of DocumentDB

“DocumentDBCRUDRepository” also contains some utility methods for building complex LINQ queries overcoming some DocumentDB limitations we discussed, namely:

1. It allows partial updates of documents, that are not allowed natively by the DocumentDB interface (only a full document replace is allowed). Partial updates are achieved by automatically retrieving the full document and by copying all changes contained in the DTOs/ViewModels tree on it. Our simulated partial update supports optimistic concurrency. Concurrency is discussed in a dedicated section.

2. It allows projection of nested collections into DTOs/ViewModels collections. Since SELECT operation on nested collections are not allowed by the DocumentDB SQL interface, DocumentDBCRUDRepository first computes all the properties to retrieve from the database, in order to build an efficient query, and then project the results on the DTOs/ViewModels with efficient in-memory projections. Performance is achieved by compiling all projection operations at the start of the program.

3. It allows classical paging of results by simulating the skip operation that is not allowed by the DocumentDB SQL interface. More specifically, it retrieves all document keys up to the current page, takes just the needed keys and then performs a query to retrieve all documents associated to those keys.

In the remainder of the article, I will demonstrate the following:

  • how to create a CosmosDB database,
  • how to configure your ASP.NET Core project for the usage of MvcControlsToolkit.Business.DocumentDB,
  • how to define projections from/to DTO classes,
  • how to use the ICRUDRepository methods of the DocumentDBCRUDRepository, and finally
  • how to build some Select Many based queries with the DocumentDBCRUDRepository LINQ helper methods.

Setup the project

Open Visual Studio 2017 and select File > New > Project. Then select ASP.NET Core Web Application. Name the project “CosmosDBUtilitiesExample”. In the popup window that appears, select “.NET Core” as the framework, and “ASP.NET Core 2.0” as the ASP.NET version. Finally, select MVC application, and “Individual accounts” as authentication (as we need users to filter documents according to the logged user).

Now run the project and register a user with the e-mail: frank@fake.com (there is no email verification), we need this user name to filter our data. When hitting the Register button, you will be asked to apply all pending database migrations. Do it, and refresh the page.

We are ready to install our utilities package. Right click on the dependencies node in the solution explorer and open the Nuget package installer. Find and install the last version of MvcControlsToolkit.Business.DocumentDB.

Setup the business layer

As a first step, we must prepare the code that builds our Cosmos DB database, and our Cosmos DB collections (we use a single collection).

Under the “Models” folder, add a new folder called “CosmosDB” for our CosmosDB data models. Then, under this new folder, add a new class called “Person” with the following code:

public class Person
{
    [JsonProperty(PropertyName = "name")]
    public string Name{ get; set; }
    [JsonProperty(PropertyName = "surname")]
    public string Surname { get; set; }
    [JsonProperty(PropertyName = "id")]
    public string Id { get; set; }
}

Add all the using clauses as suggested by Visual Studio. The JsonProperty attributes specifies how to serialize/deserialize each property to communicate with Cosmos DB.

Add also a ToDoItem class with the following code:

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

    public string Name { get; set; }

    public string Description { get; set; }

    public string Owner { get; set; }

    [JsonProperty(PropertyName = "subItems"), 
        CollectionKey("Id")]
    public IEnumerable<ToDoItem> SubItems { get; set; }

    [JsonProperty(PropertyName = "assignedTo")]
    public Person AssignedTo { get; set; }

    [JsonProperty(PropertyName = "team"),
        CollectionKey("Id")]
    public IEnumerable<Person> Team { get; set; }

    [JsonProperty(PropertyName = "isComplete")]
    public bool Completed { get; set; }
}

A “CollectionKey” attribute is applied to all collections, it declares which property of the collection elements to use for verifying, when two elements represent the same entity. It is also needed to match collection elements when updates are applied to an existing CosmosDB document.

The Owner encodes the connection with the permissions system. In our simple application, it is just the name of the user that created the record, who is also the only user with read/write permissions on it.

A more realistic application might contain the name of the group whose users have read permission on the document, and the name of the group whose users have update permission on the document, or more complex claims about the logged user.

Now we are ready to write some code that will initialize and populate the Cosmos DB database.

Under the project “Data” folder, add a new “CosmosDBDefinitions” class with the following code:

public static class CosmosDBDefinitions
{
    private static string accountURI = "Your URI";
    private static string accountKey = "Your Key";
    public static string DatabaseId { get; private set; } = "ToDoList";
    public static string ToDoItemsId { get; private set; } = "ToDoItems";

    public static IDocumentDBConnection GetConnection()
    {
        return new DefaultDocumentDBConnection(accountURI, accountKey, DatabaseId);
    }

    public static async Task Initialize()
    {
        var connection = GetConnection();

        await connection.Client
            .CreateDatabaseIfNotExistsAsync(
                new Database { Id = DatabaseId });

        DocumentCollection myCollection = new DocumentCollection();
        myCollection.Id = ToDoItemsId;
        myCollection.IndexingPolicy = 
           new IndexingPolicy(new RangeIndex(DataType.String) 
                                  { Precision = -1 });
        myCollection.IndexingPolicy.IndexingMode = IndexingMode.Consistent;
        var res=await connection.Client.CreateDocumentCollectionIfNotExistsAsync(
            UriFactory.CreateDatabaseUri(DatabaseId),
            myCollection);
        if (res.StatusCode== System.Net.HttpStatusCode.Created)
            await InitData(connection);
    }
    private static async Task InitData(IDocumentDBConnection connection)
    {
        List<ToDoItem> allItems = new List<ToDoItem>();
        for (int i = 0; i < 6; i++)
        {
            var curr = new ToDoItem();
            allItems.Add(curr);
            curr.Name = "Name" + i;
            curr.Description = "Description" + i;
            curr.Completed = i % 2 == 0;
            curr.Id = Guid.NewGuid().ToString();
            curr.Owner = i > 3 ? "frank@fake.com" : "John@fake.com";
            if (i > 1)
                curr.AssignedTo = new Person
                {
                    Name = "Francesco",
                    Surname = "Abbruzzese",
                    Id = Guid.NewGuid().ToString()
                };
            else
                curr.AssignedTo = new Person
                {
                    Name = "John",
                    Surname = "Black",
                    Id = Guid.NewGuid().ToString()
                };
            var innerlList = new List<ToDoItem>();
            for (var j = 0; j < 4; j++)
            {
                innerlList.Add(new ToDoItem
                {
                    Name = "ChildrenName" + i + "_" + j,
                    Description = "ChildrenDescription" + i + "_" + j,
                    Id = Guid.NewGuid().ToString()
                });
            }
            curr.SubItems = innerlList;
            var team = new List<Person>();
            for (var j = 0; j < 4; j++)
            {
                team.Add(new Person
                {
                    Name = "TeamMemberName" + i + "_" + j,
                    Surname = "TeamMemberSurname" + i + "_" + j,
                    Id = Guid.NewGuid().ToString()
                });
            }
            curr.Team = team;
        }
        foreach (var item in allItems)
        {
            await connection.Client
                .CreateDocumentAsync(
                UriFactory
                    .CreateDocumentCollectionUri(
                        DatabaseId, ToDoItemsId), 
                item);
                
        }
    }
}

“Your URI” and “Your Key” must be substituted with the URI and primary key of your CosmosDB account. The static class defines the database name and all collection names (in our case just one collection).

The GetConnection factory method creates an IDocumentDBConnection object based on the data contained in the class.

The “Initialize” method first ensures that a database with the required name already exists, then it ensures all required collections are created (in our case just one).

The “CreateDocumentCollectionIfNotExistsAsync” returns a Created status code if the database doesn’t exist and so it must be created. In this case the “InitData” method is called to populate the collection with test data.

During both database and collection creations, several options may be specified to configure them. Please refer to the official documentation for more details (it is enough to google the name of the methods). I changed just the indexing policy of the collection for strings to allow sorting on string fields, since sorting is possible only with the non-default “Range” policy that uses tree-based indexes instead of hash-based indexes.

The “Initialize” method must be called at program start. This may be achieved by placing its call in the “Configure” method of Startup.cs immediately after the “UseMvc” call as shown here:

app.UseMvc(routes =>
{
    routes.MapRoute(
        name: "default",
        template: "{controller=Home}/{action=Index}/{id?}");
});
var res=CosmosDBDefinitions.Initialize();
res.Wait();

Run the project. In your azure portal, you should see the newly created database, collection, and the data contained in the collection.

Coding the business/data layer

Since our application is quite simple, we will not define separate business and data layers, but a unique business/data layer.

Create a “Repository” folder in the project root, and add it a “ToDoItemsRepository” class:

public class ToDoItemsRepository: DocumentDBCRUDRepository<ToDoItem>
{
    private string loggedUser;
    public ToDoItemsRepository(
        IDocumentDBConnection connection,
        string userName
        ): base(connection, 
            CosmosDBDefinitions.ToDoItemsId,
            m => m.Owner == userName,
            m => m.Owner == userName
            )
    {
        loggedUser = userName;
    }
    static ToDoItemsRepository()
    {

    }
}

Our repository inherits from the DocumentDBCRUDRepository<ToDoItem> generic repository and does not do much, but the following:

1. Passing the connection object received as first constructor argument to the base constructor

2. Using the username of the logged user received as a second argument to build two filter clauses that it passes to the base constructor. The first filter clause will be applied automatically to all search operations, while the second filter will be applied to all update and delete operations; thus preventing the current user from modifying documents that he/she is not the owner of.

In a real-life application, we would have passed a claim extracted from the logged user like, for instance, a “company-name”, instead of a simple user name.

I have also added an empty static constructor. Later on, in the article we will add declarations specifying how to project the ToDoItem data class to and from our DTO classes.

In order to make our repository available to all controllers needing it, we must add it to the “ConfigureServices” method of the startup class:

services.AddSingleton<IDocumentDBConnection>(x =>
    CosmosDBDefinitions.GetConnection()
    );
services.AddTransient<ToDoItemsRepository>(x =>
    new ToDoItemsRepository(
        x.GetService<IDocumentDBConnection>(),
        x.GetService<IHttpContextAccessor>()
            .HttpContext?.User?.Identity?.Name
        ));

The connection object is added as a singleton since it doesn’t contain status information.

The repository object, instead, is created each time an instance is required (since it is stateful).

In our example application, the repository receives the logged user name extracted from the HttpContext to handle permissions. However, in a real-life application, we should pass it one or more claims extracted from the HttpContext?.User?.Claims enumeration. Claims may be added when a new user is registered, or at a later time, with the UserManager<ApplicationUser>.AddClaimAsync method.

Coding the presentation layer

The first step in the implementation of the presentation layer is the definition of the DTO classes that are needed.

Let add a “DTOs” folder to the project root. We basically need two DTOs - a short one to be used for listing search results, and the second one for edit/detail/add purposes. The short DTO might contain just the item name and the surname of the person the “to do item” has been assigned to:

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

    public string Name { get; set; }

    public string AssignedToSurname { get; set; }
}

The item Id is needed to connect the list page with detail pages. We put the “Surname” of the person the “to do item” has been assigned to, in a property. The property name is the concatenation of the “AssignedTo” property name of the “ToDoItem” data model, and of the Surname property name of the Person data model. This convention for assigning names to DTO properties is called “Flattening”.

In order to specify how to project data models to “ListItemDTO” objects, it is enough to add the instruction shown here to the static constructor of the “ToDoItemsRepository” class:

DeclareProjection
    (m =>
    new ListItemDTO
    {

                    
    }, m => m.Id
);

The first argument is a lambda expression specifying that each “ToDoItem” must be transformed into a “ListItemDTO”, while the second argument specifies the property that plays the role of the key in the “ListItemDTO” class.

The lambda expression doesn’t specify how to fill the “ListItemDTO” properties with the “ToDoItem” class properties. This is because the “DeclareProjection” method is able to automatically create all property assignments of simple properties (the ones that do not contain collections or nested object), according to a same-name convention that also supports the flattening convention defined above.

For the detail DTO, we assume that our application manipulates just some of the properties contained in the “ToDoItem”. Specifically, we discard the “Team” collection but keep the “SubItems” one, discard the “Completed” property and finally keep just “Surname” and “Id” of the person the “to do item” has been assigned to:

public class DetailItemDTO
{
    public string Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public string Owner { get; set; }
    public IEnumerable<SubItemDTO> SubItems { get; set; }
    public string AssignedToSurname { get; set; }
    public string AssignedToId { get; set; }
}
public class SubItemDTO
{
    public string Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
}

We used “Flattening” for the “AssignedTo” nested object properties, and used a shorter class to represent sub-items.

In this case, the projection is a little bit more complex, since we need to specify which collection to include and how to project collection items:

DeclareProjection
    (m =>
    new DetailItemDTO
    {
        SubItems = m.SubItems
            .Select(l => new SubItemDTO { })
    }, m => m.Id
);

Also in this case, most of properties are inferred automatically with the same-name convention enriched with flattening.

Since we will use the “DetailItemDTO” in the update and addition operations, in this case, we also need to specify how to project back “DetailItemDTO” properties into “ToDoItem” properties:

DeclareUpdateProjection<DetailItemDTO>
    (m =>
        new ToDoItem
        {
            SubItems = m.SubItems
             .Select(l => new ToDoItem { }),
            AssignedTo = m.AssignedToId == null ? 
            null : new Person { }
        }
    );

Over here, assignments of the “Person” class properties nested in the “AssignedTo” property are inferred with the “Unflattening” conventions that is just the converse of the “Flattening” convention.

Nested objects must always be preceded by a check that verifies if the nested object must be created or not. In this case, we use the “AssignedToId” property to verify the existence of a nested person object.

The omission of checks on nested objects might result either in the creation of empty objects or in “Null object” exceptions. Null checks on nested collections are created automatically since they are standard and don’t depend on the application logic, and must not be included in the projection definitions.

We don’t need to write any repository method since we will use the methods inherited from “DocumentDBCRUDRepository<ToDoItem>”, so we can move to the controller definition. We use the already existing “HomeController” for both list and detail pages.

We must add a constructor to get our “ToDoItemsRepository” from dependency injection:

ToDoItemsRepository repo;
public HomeController(ToDoItemsRepository repo)
{
    this.repo = repo;
}
For the Index page, we will add a new repository method:
public async Task<DataPage<ListItemDTO>> GetAllItems()
{
    var vm = await GetPage<ListItemDTO>
        (null,
            x => x.OrderBy(m => m.Name),
            -1, 100);
    return vm;
}

Then we redefine the already existing Index method:

public async Task<IActionResult> Index()
{

    var vm = await repo.GetAllItems();
    return View(vm);
}

The “GetPage” method returns a page of data projected on the DTO specified in the method’s generic argument.

The first method argument is a filter expression (in our case null), while the second argument is an optional sorting (“ThenBy” is not supported by CosmosDB).

The third argument is the desired page: in our case the first data page: the minus sign prevents the calculation of the total number of results that in case of distributed databases, might be a very expensive operation.

The fourth argument is the page length: we specified 100 since we don’t want to actually page results but just want to list all of them by putting a limit on the maximum number of results.

At this stage, we have also added empty placeholders for the delete, edit and create action methods:

public async Task<IActionResult> Delete(string id)
{
    return RedirectToAction("Index");
}
public async Task<IActionResult> Edit(string id)
{
    return View();
}
[HttpPost]
public async Task<IActionResult> Edit(DetailItemDTO  vm)
{
    return View(vm);
}
public async Task<IActionResult> Create()
{
    return View();
}
[HttpPost]
public async Task<IActionResult> Create(DetailItemDTO  vm)
{
    return View(vm);
}

We will use them for both edit and additions of new items.

Now go to the Views/Home/Index.cshtml view and replace its content with the following code:

@using CosmosDBUtilitiesExample.DTOs
@using MvcControlsToolkit.Core.Business.Utilities
@model DataPage<ListItemDTO>
@{
    ViewData["Title"] = "To do items";
    string error = ViewData["error"] as string;
}
<h3>@ViewData["Title"]</h3>
@if (error != null)
{
<div class="alert alert-danger" role="alert">
    <strong>@error</strong>
</div>
}
<table class="table table-striped table-bordered 
       table-hover table-condensed">
    <thead>
        <tr class="info">
            <th></th>
            <th>Name</th>
            <th>Assigned to</th>
        </tr>
    </thead>
    <tbody class="items-container">
        @foreach (var item in Model.Data)
        {
            <tr>
                <td>
                    <a asp-action="Delete" asp-controller="Home" 
                       asp-route-id="@item.Id"
                       class="btn btn-xs btn-primary" title="delete">
                        <span class="glyphicon glyphicon-minus"></span>
                        &nbsp;
                    </a>
                    <a asp-action="Edit" asp-controller="Home" 
                       asp-route-id="@item.Id" 
                       class="btn btn-xs btn-primary" title="edit">
                        <span class="glyphicon glyphicon-edit"></span>
                        &nbsp;
                    </a>
                </td>
                <td>@item.Name</td>
                <td>@item.AssignedToSurname</td>
            </tr>
        }
    </tbody>
</table>
<a asp-action="Create" asp-controller="Home"
   class="btn btn-primary" 
   title="add new item">
    Add new item
</a>

Run the project.

If you are not logged in, you should see no items at all. As soon as you log in with the frank@fake.com user, you should see the two items owned by frank@fake.com; since in the definition of the “ToDoItemsRepository”, we filtered the items owned by the currently logged user.

In a real-life application, the ownership of a record would have been added to a user group and not to a single user.

You may also test the filtering capability of the “GetPage” method contained in the “GetAllItems()” repository method by adding a filter clause as shown below:

var vm = await repo.GetPage<ListItemDTO>
    (m => m.Name=="Name5",
        x => x.OrderBy(m => m.Name),
        -1, 100);

Now we may complete the “Delete” action method:

public async Task<IActionResult> Delete(string id)
{
    try
    {
        if (id != null)
        {
            repo.Delete<string>(id);
            await repo.SaveChanges();
        }
                    
    }
    catch {
        ViewData["error"] = "an error occurred";
    }
    return RedirectToAction("Index");
}

The code is self-explanatory. We have used the “Delete” repository method to remove the item, and the “SaveChanges” method to commit the delete operation.

In an actual application, a better error handling approach would be required. For instance, in case of network error, the operation might be retried another time before informing the user of connection problems. While in case the item to delete is not found ,the user might be informed that the item is not existing or is already deleted.

Moreover, it is worth to also point out that delete operations should never be invoked with an Http “Get. This is both to prevent certain attacks, and for a clean REST semantic as discussed here.

Accordingly, in an actual application, the best option should be an AJAX “Post”. Please don’t test the delete operation now, but wait after we have implemented the add operation in order to avoid being left with an empty database.

The “Edit” and “Create” action methods may use the same View:

public async Task<IActionResult> Edit(string id)
{
    try
    {
            var vm = await 
            repo.GetById<DetailItemDTO, string>(id);
        return View(vm);
    }
    catch
    {
        return RedirectToAction("Index");
    }
}

public IActionResult Create()
{
        return View("Edit");
}

The “Create” action method returns a page with a null ViewModel, that is with empty input fields. Whereas the “Edit” action method return a page filled with data, from the “to do item” whose id has been passed.

The two generic arguments of the “GetById” repository method are respectively the DTO type, and the type of the principal key “id” passed to the method. When the user submits the form contained in this page, the “HttpPost” overload of the “Edit” method is invoked to perform the needed database modifications:

[HttpPost]
public async Task<IActionResult> Edit(DetailItemDTO  vm)
{
    if (ModelState.IsValid)
    {
        ModelState.Clear();
     if (vm.SubItems != null)
            vm.SubItems.Where(x => x.Id == null)
                .All(x =>
                {
                    x.Id = Guid.NewGuid().ToString();
                    return true;
                });

        try
        {
            vm.Owner = User.Identity.Name;
            repo.Update(false, vm);
            
            await repo.SaveChanges();
            return RedirectToAction("Index");
        }
        catch
        {
            ViewData["error"] = "an error occurred";
        }
    }
    return View(vm);
}
public async Task<IActionResult> Create(DetailItemDTO vm)
{
    if (ModelState.IsValid)
    {
        ModelState.Clear();
        if (vm.SubItems != null)
            vm.SubItems.Where(x => x.Id == null)
                .All(x =>
                {
                    x.Id = Guid.NewGuid().ToString();
                    return true;
                });
        try
        {
            vm.Owner = User.Identity.Name;
            vm.Id = Guid.NewGuid().ToString();

            repo.Add(false, vm);
                    
            await repo.SaveChanges();
            return RedirectToAction("Index");
        }
        catch
        {
            ViewData["error"] = "an error occurred";
        }
    }
    return View("Edit", vm);
}

In the “Create” method, we will create a fresh principal key, and then perform an addition operation; while in the “Edit” method, the operation is an update. In both cases we call “SaveChanges” to commit the operation.

A fresh new principal key is also added to all newly added subitems, by first filtering them and then modifying them.

The “false” argument passed to all repository methods declares that the DTO is not a “full data item”, i.e. its properties do not define all properties of the database data item. Therefore, in case of an update, the modified item must be merged with the original data item instead of replacing it.

In case the argument is set to “true”, all properties that are not specified in the ViewModel, are reset to their default values. At the moment, the argument is ignored for creations of new items, but it is reserved for future extensions that will enable the user to specify how to compute all missing properties when this argument is set to false.

If several operations are pending when “SaveChanges” is called, they are executed in parallel to maximize performance. Operation errors are returned in a unique “DocumentsUpdateException<M>” aggregated exception that contains the list of all specific exceptions thrown, and information on both successfully executed, as well as failed operations. It is enough to pass the “DocumentsUpdateException<M>” exception to the repository “RetryChanges” method to retry all failed operations.

The edit View

Right click on the “Home” “Edit” method to create the “Edit” View, then select the “empty without model” scaffolding option. Finally replace the scaffolded code with:

@using CosmosDBUtilitiesExample.DTOs
@model DetailItemDTO
@{
    ViewData["Title"] = "To do item";
    string error = ViewData["error"] as string;
    List<SubItemDTO> SubItems = null;
    if(Model != null && Model.SubItems != null)
    {
        SubItems = Model.SubItems.ToList();
    }
}

<h3>@ViewData["Title"]</h3>
@if (error != null)
{
    <div class="alert alert-danger" role="alert">
        <strong>@error</strong>
    </div>
}
<form method="post">
    <div asp-validation-summary="All" class="text-danger"></div>
    <div class="row">
        <div class="col-xs-5">
            <div class="form-group">
                <label asp-for="Name" 
                       class="col-xs-12 control-label"></label>
                <div class="col-xs-12">
                    <input asp-for="Name" 
                           class="form-control" />
                    <input type="hidden" asp-for="Id"
                           class="form-control" />
                    <span asp-validation-for="Name"
                          class="text-danger"></span>
                </div>
            </div>
        </div>
        <div class="col-xs-4">
            <div class="form-group">
                <label asp-for="AssignedToSurname" 
                       class="col-xs-12 control-label"></label>
                <div class="col-xs-12">
                    <input asp-for="AssignedToSurname"
                           class="form-control" />
                    <span asp-validation-for="AssignedToSurname"
                          class="text-danger"></span>
                </div>
            </div>
        </div>
        <div class="col-xs-3">
            <div class="form-group">
                <label asp-for="AssignedToId" 
                       class="col-xs-12 control-label"></label>
                <div class="col-xs-12">
                    <input asp-for="AssignedToId"
                           class="form-control" />
                    <span asp-validation-for="AssignedToId"
                          class="text-danger"></span>
                </div>
            </div>
        </div>
    </div>
    <div class="row">
        <div class="col-xs-12">
            <div class="form-group">
                <label asp-for="Description" 
                       class="col-xs-12 control-label"></label>
                <div class="col-xs-12">
                    <input asp-for="Description" 
                           class="form-control" />
                    <input type="hidden" asp-for="Id"
                           class="form-control" />
                    <span asp-validation-for="Description"
                          class="text-danger"></span>
                </div>
            </div>
        </div>

    </div>
    <h4>Subitems</h4>
    
    <div class="row">
        <div class="col-xs-12">
            <div class="form-group">
                <button type="submit"
                        class="btn btn-primary">
                    Submit
                </button>
            </div>
        </div>
    </div>
</form>

This is quite a standard Bootstrap based “Edit” View for the “DetailItemDTO” class. It doesn’t contain any code to render the “Subitems” IEnumerable in edit mode (additions, updates, and deletes).

You may place any editable grid between the “<h4>Subitems</h4>” header and the submit button form group. For instance, you may use an Mvc Controls Toolkit grid if you configure the project for the usage of the Mvc Controls Toolkit as explained in this tutorial. You may also use Angular or Knockout.js based grids.

In the code we will just see, I propose a simple edit in-line grid implementation that relies on the ASP.NET MVC standard model binder for “IEnumerables”, whose basic principles are explained in a famous Phil Haack post.

Basically, if grid items input fields have names containing consecutive indexes like “Subitems[0].Name”, “Subitems[1].Name”, “Subitems[2].Name”, etc. it is easy to model bind the whole IEnumerable. However if there are “holes/gaps” because of some rows that have been deleted, the model binder has no easy way to determine if and when to resume counting items.

Moreover, it is very difficult to add new items with the right index from JavaScript. Therefore, a hidden field named “Subitems.Index” is added to each row containing the row index, so that the model binder receives a “Subitems.Index” field whose value is the comma-separated list of all indexes used by the grid in the right order, such as “1,2,3,4, 7,added1,added2”. This way the holes problem is overcome, since, for instance, the model binder knows that after the “4”th index it must look for the “7”th index.

Moreover indexes need not to be a sequence of growing integers anymore, but they may be generic strings, so we may assign indices to newly added items with a very simple generator of unique names.

Here’s our grid:

<table class="table table-striped table-bordered
    table-hover table-condensed">
    <thead>
        <tr class="info">
            <th>
                <button type="button" class="add-button
                        btn btn-xs btn-primary"
                        title="new sub-item">
                    <span class="glyphicon
                            glyphicon-plus">
                    </span>
                </button>
            </th>
            <th>Name</th>
            <th>Description</th>
        </tr>
    </thead>
    <tbody class="items-container">
        <tr style="display:none">
            <td>
                <button type="button" class="delete-button
                        btn btn-xs btn-primary"
                        title="delete">
                    <span class="delete-button glyphicon
                            glyphicon-minus">
                    </span>
                </button>
                <input class="binding-index" 
                        type="hidden" name="SubItems.Index" />
            </td>
            <td>
                <input type="text" name="Name"
                        class="form-control" />
                <input  type="hidden" name="Id"
                        class="form-control" />
            </td>
            <td>
                <input type="text" name="Description"
                        class="form-control" />
            </td>
        </tr>
    @if (Model != null && Model.SubItems != null)
    {
        for (int i=0;i< Model.SubItems.Count(); i++)
        {
        <tr>
            <td>
                <button 
                    class="delete-button 
                        btn btn-xs btn-primary" 
                    title="delete">
                    <span class="glyphicon 
                            glyphicon-minus">
                    </span>
                </button>
                <input type="hidden" name="SubItems.Index" value="@i" />
            </td>
            <td>
                <input asp-for="@SubItems[i].Name"
                    class="form-control" />
                <input type="hidden" asp-for="@SubItems[i].Id"
                        class="form-control" />
            </td>
            <td>
            <input asp-for="@SubItems[i].Description"
                class="form-control" />
            </td>
        </tr>
        }

    }
    </tbody>
</table>

It is a Bootstrap based table whose first row is a row template for adding new rows (that’s why it is styled as hidden). The table header contains a button to add new items, while the first column of each row contains a button to delete the row. The same column contains the “Subitems.Index” hidden fields.

Here’s the JavaScript needed to operate the grid:

<script type="text/javascript">
(function ($) {
    var addCount = 0;
    var template = $(".items-container")
        .children().first().detach();
    $(document).on("click", ".delete-button", function (evt) {
        $(evt.target).closest("tr").remove();
    });
    $(document).on("click", ".add-button", function (evt) {
        addCount++;
        var index = "_" + addCount;
        var prefix = "SubItems[" + index + "].";
        var item = template.clone();
        item.find(".binding-index").val(index);
        $(".items-container").append(item);
        item.find("input").not(".binding-index")
            .each(function () { 
                input = $(this);
                input.attr("name", prefix + input.attr("name"))
            });
            
        item.show();
    });
})(jQuery);    
</script>
}

The JavaScript is inserted in-line for simplicity, but you may move it as it is, in a separate JavaScript file.

The “addCount” variable is used to generate unique indexes for newly added items.

The template row is removed at program start. When the user clicks a “remove” button, the row containing that button is removed. When the add button is clicked, a new index is generated, the template row is cloned and the clone is appended to the grid. The newly created index becomes the value of the “Subitems.Index” hidden input in the cloned row.

All input fields names are updated properly with the newly generated index, and finally “item.show();” makes the newly added row visible.

Run the program and test any kind of update!

Adding custom retrieval pages

What if we want to execute custom queries that are not covered by the standard retrieval methods defined in the “ICRUDRepository” interface?

“DocumentDBCRUDRepository” contains the “Table” method to start a LINQ DocumentDB query. Then you may add some LINQ, and finally you may use the “ToList”, “ToSequence”, and “FirstOrDefault” repository methods to project the results to your DTOs. The “ToSequence” method extracts a page of results, and a continuation token gets the next page with a similar query.

Suppose, for instance, we would like to list all sub-Items contained in all “to do items” owned by the currently logged user. It is enough to add the following method to our “ToDoItemsRepository” class:

public async Task<IList<SubItemDTO>> AllSubItems()
{
    var query=Table(100)
        .Where(SelectFilter)
        .SelectMany(m => m.SubItems);
    return await ToList<SubItemDTO>(query);  
}

The first argument of the “Table” method is the required page size. It also accepts an optional partition key argument if the collection has a partition key defined on it, and if the query must be confined to a specific partition. Finally, it admits a third “continuation token” string parameter if the query must return the next page of a previous “ToSequence” based query.

“SelectFilter” and “AccessFilter” are read-only properties inherited from DocumentDBCRUDRepository that contains the two filter conditions we passed in the DocumentDBCRUDRepository base constructor.

“ToList<SubItemDTO>” also accepts an optional second argument specifying a number of items to skip, whose default value is 0. In order for “ToList<SubItemDTO>” to work properly, we must define a projection for “SubItemDTO” in the “ToDoItemsRepository” static constructor:

DeclareProjection
    (m =>
    new SubItemDTO
    {
    }, m => m.Id);

What if we want to list all team members of all “to do items” owned by the currently logged user?

First of all, we need a “PersonListDTO” DTO:

public class PersonListDTO
{
    public string Name { get; set; }
        
    public string Surname { get; set; }
        
}

In this case, the code is a little bit different:’'

public async Task<IList<PersonListDTO>> AllMembers()
{            
    var query = 
        Table(100)
        .Where(SelectFilter)
        .SelectMany(m => m.Team);
    return await ToList<PersonListDTO, Person>(query);
}

In fact, since the “IQueryable” passed to “ToList” is a IQueryable<Person> instead of a IQueryable<ToDoItem>, we must use a different overload of “ToList” that contains a second generic argument.

Also, the projection declaration in the “ToDoItemsRepository” static constructor is a little bit different:

DocumentDBCRUDRepository<Person>
    .DeclareProjection
    (m =>
    new PersonListDTO
    {
    }, m => m.Surname
);

In fact, this time we invoke the “DeclareProjection” static method of “DocumentDBCRUDRepository<Person>" instead of the one of “DocumentDBCRUDRepository<ToDoItem>".

We also need two more “HomeController” action methods:

public async Task<IActionResult> AllSubitems ()
{
    var members = await repo.AllMembers();
    var vm=await repo.AllSubItems();
    return View(vm);
}

public async Task<IActionResult> AllTeamMembers()
{
    var vm = await repo.AllMembers();
    return View(vm);
}

..and their associated Views:

@using CosmosDBUtilitiesExample.DTOs
@model IList<SubItemDTO>
@{
    ViewData["Title"] = "To do sub-items";
}
<h3>@ViewData["Title"]</h3>
<table class="table table-striped table-bordered
    table-hover table-condensed">
    <thead>
        <tr class="info">
            <th>Name</th>
            <th>Description</th>
        </tr>
    </thead>
    <tbody class="items-container">
    @foreach (var item in Model)
    {
            <tr>
                <td>@item.Name</td>
                <td>@item.Description</td>
            </tr>
    }
    </tbody>
</table>

..and

@using CosmosDBUtilitiesExample.DTOs
@model IList<PersonListDTO>
@{
    ViewData["Title"] = "To do team members";
}
<h3>@ViewData["Title"]</h3>
<table class="table table-striped table-bordered
    table-hover table-condensed">
    <thead>
        <tr class="info">
            <th>Name</th>
            <th>Surname</th>
        </tr>
    </thead>
    <tbody class="items-container">
        @foreach (var item in Model)
        {
            <tr>
                <td>@item.Name</td>
                <td>@item.Surname</td>
            </tr>
        }
    </tbody>
</table>

Links to the newly added pages may be added in the main menu in the _Layout page:

<li>
    <a asp-area="" asp-controller="Home" asp-action="AllSubitems">
    Sub-items
    </a>
 </li>
<li>
    <a asp-area="" asp-controller="Home" asp-action="AllTeamMembers">
        Team members
    </a>
</li>

Optimistic Concurrency

CosmosDB supports optimistic concurrency through the _ETag property that is added to each document, and that is changed each time the document is modified.

When the document is replaced, we may require to abort the operation and to throw an exception if the current_ ETag value differs from the _ETag value we received before applying our modifications.

The old _ETag value must be passed in the document replacement instruction as detailed here.

MvcControlsToolkit.Business.DocumentDB automatically handles  _ETags. It is enough to add a new property marked as the following to the data class (in our example the Item class):

[JsonProperty(PropertyName = "_ETag", 
        NullValueHandling = NullValueHandling.Ignore)]
public string MyTag { get; set; }

If the tag value is copied and handled in the DTO, an update operation will fail if the document changes during the user receiving the document data in the user interface, and submitting its modifications.

In this case, the first user that applies its modifications succeeds, while all other users that were concurrently modifying the same document, fail, and are forced to re-edit the new changed copy of the document.

Instead, if the _ETag is just added to the data class but not to the DTO, the operation fails only if changes occur in a small interval needed by the library to retrieve the old copy of the document, and to apply all modifications returned by the user in the DTO.

This policy forces the merge of all modifications applied concurrently by all users. If a user fails, it may retry its modifications till he/she eventually succeed merging its modifications and the document.

Conclusion:

This article shows how the MvcControlsToolkit.Business.DocumentDB Nuget package may help writing layered applications based on Cosmos DB, providing out-of-the-box methods for more common operations, and overcoming some limitations of the DocumentDB SQL interface, moving it closer to a full SQL implementation.

Download the entire source code of this article (Github).

This article was technically reviewed by Daniel Jimenez Garcia.

This article has been editorially reviewed by Suprotim Agarwal.

Absolutely Awesome Book on C# and .NET

C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.

We at DotNetCurry are very excited to announce The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).

Organized around concepts, this Book aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on the latest .NET Core 3.0, .NET Standard and C# 8.0 (final release) too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.

Click here to Explore the Table of Contents or Download Sample Chapters!

What Others Are Reading!
Was this article worth reading? Share it with fellow developers too. Thanks!
Share on LinkedIn
Share on Google+

Author
Francesco Abbruzzese (@F_Abbruzzese) implements ASP.NET MVC applications, and offers consultancy services since the beginning of this technology. He is the author of the famous Mvc Controls Toolkit, and his company (www.mvc-controls.com/) offers tools, and services for ASP.NET MVC. He moved from decision support systems for banks and financial institutions, to the Video Games arena, and finally started his .NET adventure with the first .NET release. He also writes about .NET technologies in his blog: www.dotnet-programming.com/


Page copy protected against web site content infringement 	by Copyscape




Feedback - Leave us some adulation, criticism and everything in between!