Building a Software Survey using Blazor - Part 6 - Azure CosmosDB

I've decided to write a small survey site using Blazor. Part of this is an excuse to learn Blazor.

As I learn with Blazor I will blog about it as a series of articles.

This series of articles is not intended to be a training course for Blazor - rather my thought process as I go through learning to use the product.

Earlier articles in this series:


When it came to storing the survey results, I wanted something simple.

But in the same respect I wanted something cheap - and that would allow me report on the collected data.

About this time, Azure launched a free tier of Cosmos DB (or at least about this time I noticed it).

I'd used Cosmos DB in its easlier incarnation - DocumentDB - but hadn't used Cosmos DB itself due to the cost.

The free tier allows for a reasonable amount of data - more than enough for the survey, so I decided to trial it for the app.

Local Development

Firstly, for development, its good to know that Microsoft provide a local Cosmos DB emulator.

This can be downloaded from here and is a good way to get started with Cosmos DB without the danger of incurring any Azure charges.

IPersistanceManager

So I could inject Cosmos DB in, I wanted to hide the persistance being an interface:


    public interface IPersistanceManager
    {
        Task<bool> Persist(SurveyResponse surveyResponse);
    }

Which of course allowed me to mock out the PersistanceManager for integration testing. So for example;


    public class FakePersistanceManager : IPersistanceManager
    {
        public async Task<bool> Persist(SurveyResponse surveyResponse)
        {
            await Task.Run(() => Thread.Sleep(5000));
            return true;
        }
    }

CosmosDbPersistanceManager.cs

The actual concrete implementation for Azure Cosmos DB was fairly simple. On Persist, it would create a CosmosClient (using credentials from config), create the database & container if either didn't exist, and then save passed in model:


using Microsoft.Azure.Cosmos;
using Microsoft.Extensions.Logging;
using SoftwareSurvey.Models;
using System;
using System.Net;
using System.Threading.Tasks;

namespace SoftwareSurvey.Services
{
    public class CosmosDbPersistanceManager : IPersistanceManager
    {
        private const string APPLICATION_NAME = "SoftwareSurvery";
        private const string DATABASE_NAME = "SoftwareSurvey";
        private const string CONTAINER_NAME = "Responses";

        private readonly PersistanceConfiguration _configuration;
        private readonly ILogger _logger;

        public CosmosDbPersistanceManager(PersistanceConfiguration configuration, ILogger<CosmosDbPersistanceManager> logger)
        {
            _configuration = configuration;
            _logger = logger;
        }

        public async Task<bool> Persist(SurveyResponse surveyResponse)
        {
            try
            {
                var client = new CosmosClient(_configuration.CosmosDbEndpoint,
                                              _configuration.CosmosDbPrimaryKey,
                                              new CosmosClientOptions() { ApplicationName = APPLICATION_NAME });

                Database database = await client.CreateDatabaseIfNotExistsAsync(DATABASE_NAME);
                Container container = await database.CreateContainerIfNotExistsAsync(CONTAINER_NAME, "/year");

                ItemResponse<SurveyResponse> response = await container.CreateItemAsync<SurveyResponse>(surveyResponse, new PartitionKey(surveyResponse.Year));

                if (response.StatusCode == HttpStatusCode.Created)
                {
                    return true;
                }
                else
                {
                    _logger.LogError($"Unable to save record to CosmosDB - received {response.StatusCode} - {response.ActivityId}");
                    return false;
                }
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed to save to CosmosDB");

                return false;
            }
        }
    }
}

ThankYou.razor

The CosmosDbPersistanceManager could then be injected into the ThankYou page (the final page).

The ThankYou page would show a "saving" message while the response was being saved to Cosmos DB and provide retry prompt if the save failed:


@page "/ThankYou"

@if (Saved)
{
    if (Error)
    {
        <h1 class="temporary-header">SORRY</h1>
        <p>An error has occurred while trying to save your details.</p>
        <p>Please press the button below to retry:</p>
        <button type="submit" class="btn retry-button" @onclick="HandleRetry">Retry</button>
    }
    else
    {
        <PageTitle Title="THANK YOU" Position="7" />
        ...
    }
}
else
{
    <LoadingSpinner Title="SAVING ..."></LoadingSpinner>
}

@code
{
    [Inject]
    private Models.SurveyResponse Model { get; set; }
    [Inject]
    private Services.IPersistanceManager PersistanceManager { get; set; }

    private bool Saved { get; set; }
    private bool Error { get; set; }

    protected override void OnAfterRender(bool firstRender)
    {
        if (firstRender)
        {
            SaveData();
        }
    }

    private void HandleRetry(EventArgs eventArgs)
    {
        Saved = false;
        Error = false;
        StateHasChanged();

        SaveData();
    }

    private void SaveData()
    {
        var task = PersistanceManager.Persist(Model);
        task.ContinueWith(async x =>
        {
            Error = !x.Result;
            Saved = true;
            await InvokeAsync(StateHasChanged);
        });

        Task.Run(() => task);
    }
}

Note that the actual Persist is run as a background task so that the user is shown the "Saving" message. On completion of the Persist then the UI is updated with with the Thank You message or the retry button.

And that's it

Fairly simple really.

And because I'm using the IPersistanceManager interface, I can subsitute fakes/ mocks during both development and testing.

It would also allow me to easily replace the persistance choice - moving to Azure SQL or even Blob storage if appropriate.

But I did feel that Azure Cosmos DB would have provided me with the a really good platform for data analysis. I'd certainly have liked to spend sometime with the Jupyter Notebook functionality - described by Microsoft as:

"Jupyter Notebook is an open-source web application that allows you to create and share documents that contain live code, equations, visualizations, and narrative text."

Unfortunately due to the lack of survey repondents, that isn't going to happen. Maybe next time.

About the author:

Mark Taylor is an experience IT Consultant passionate about helping his clients get better ROI from their Software Development.

He has over 20 years Software Development experience - over 15 of those leading teams. He has experience in a wide variety of technologies and holds certification in Microsoft Development and Scrum.

He operates through Red Folder Consultancy Ltd.