Blazor - Work With Cassandra API In Cosmos DB

In this article, we will see how to create a Cosmos DB account with Cassandra API. Later we will create a Blazor application and connect with the Cassandra database using the “CassandraCSharpDriver” NuGet package.

Apache Cassandra is a free and open-source, distributed, wide-column store, NoSQL database management system. It provides high availability with no single point of failure.

Azure Cosmos DB with Cassandra API can be used as the data store for apps written for Apache Cassandra.

Blazor is an experimental .NET web framework using C#/Razor and HTML that runs in the browser with WebAssembly. Blazor provides the benefits of a client-side web UI framework. NET. I have already written some articles about Blazor in C# Corner.

Please refer to the below articles to get a basic idea about the Blazor framework.

We will create an Address Book application. We will see all CRUD operations in Blazor with the Cassandra database.

Step 1. Create a Cosmos DB account with Cassandra API

Please login to the Azure portal and choose to Create New Resource, Databases, and Azure Cosmos DB.

DB account

Please choose your resource group (Create a new resource group if you do not have any resource group) and give a name to the Cosmos DB account. Also, choose the geographic location for the Cosmos DB account. Make sure you have chosen Cassandra as API. Currently, there are 5 APIs available in Cosmos DB.

Cosmos DB

You can review the account details and after successful validation please click the “Create” button.

Create

It will take some to finish the deployment. You can see the status.

 Deployment

After some time, Cosmos DB will be successfully deployed.

Deployed

You can click the “Go to resource” button and open the Cosmos DB account. Please click the “Connection String” tab and get the connection details about the Cassandra database. We will use these connection details in our Blazor application later.

Connection String

We can go to the “Data Explorer” tab to create a new Keyspace now. The keyspace is like a database and tables are created under this Keyspace.

Data Explorer

We can create a new Keyspace now.

New Keyspace

After the successful creation of Keyspace, we can create a Table now. For Table creation, we must use CQL (Cassandra Query Language) commands.

 CQL

Most of the CQL commands are like SQL commands.

  1. CREATE TABLE IF NOT EXISTS Sarath Lal. address book
  2. (id text PRIMARY KEY,firstname text,lastname text,
  3. gender text,address text, zip code text,
  4. country text,state text,phone text)

Step 2. Create a Blazor application.

Please open Visual Studio 2017 (I am using a free community edition) and create a Blazor app. Choose an ASP.NET Core Web Application project template.

ASP.NET Core

We can choose the Blazor (ASP.NET Core hosted) template.

 Blazor

Our solution will be ready in a moment. Please note that there are three projects created in our solution - “Client”, “Server”, and “Shared”.

The client project contains all the client-side libraries and Razor Views, while the server project contains the Web API Controller and other business logic. The shared project contains commonly shared files, like models and interfaces.

Razor Views

By default, Blazor created many files in these three projects. We can remove all the unwanted files like “Counter.cshtml”, “FetchData.cshtml”, “SurveyPrompt.cshtml” from the Client project and “SampleDataController.cs” file from the Server project and delete the “WeatherForecast.cs” file from the shared project too.

We can add the “CassandraCSharpDriver” NuGet package to the Shared project and server project. Both projects need this package. This package is used for creating connectivity between the Blazor app and Cassandra.

CassandraCSharpDriver

As I mentioned earlier, we will create an Address Book application. We can create an AddressBook model class now. Please create a “Models” folder in the Shared project and create an AddressBook class inside this folder.

Please copy the code below and paste it into a new class.

AddressBook.cs

using Cassandra.Mapping.Attributes;

namespace BlazorWithCassandraAddressBook.Shared.Models
{
    [Table("addressbook")]
    public class AddressBook
    {
        [Column("id")]
        public string Id
        {
            get;
            set;
        }
        [Column("firstname")]
        public string FirstName
        {
            get;
            set;
        }
        [Column("lastname")]
        public string LastName
        {
            get;
            set;
        }
        [Column("gender")]
        public string Gender
        {
            get;
            set;
        }
        [Column("address")]
        public string Address
        {
            get;
            set;
        }
        [Column("zipcode")]
        public string ZipCode
        {
            get;
            set;
        }
        [Column("country")]
        public string Country
        {
            get;
            set;
        }
        [Column("state")]
        public string State
        {
            get;
            set;
        }
        [Column("phone")]
        public string Phone
        {
            get;
            set;
        }
    }
}

Please note that we have used “Table” and “Column” properties in this class. This is derived from “Cassandra.Mapping.Attributes” and used to mention the Table name and Column names of Cassandra table.

We can create an “IDataAccessProvider” interface now. This interface will contain all the signatures of the DataAccess class and we will implement this interface in our DataAccess class later.

Please copy the code below and paste it into the interface.

IDataAccessProvider.cs

using System.Collections.Generic;
using System.Threading.Tasks;

namespace BlazorWithCassandraAddressBook.Shared.Models
{
    public interface IDataAccessProvider
    {
        Task AddRecord(AddressBook addressBook);
        Task UpdateRecord(AddressBook addressBook);
        Task DeleteRecord(string id);
        Task<AddressBook> GetSingleRecord(string id);
        Task<IEnumerable<AddressBook>> GetAllRecords();
    }
}

We can go to our Server project and create a new folder “DataAccess”. We will create a static class “CassandraInitializer” inside this folder. This class will be called from the application Startup class and this will be used to initialize our Cassandra database.

CassandraInitializer.cs

using Cassandra;
using System.Diagnostics;
using System.Net.Security;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;

namespace BlazorWithCassandraAddressBook.Server.DataAccess
{
    public static class CassandraInitializer
    {
        // Cassandra Cluster Configs        
        private const string UserName = "Fill the details";
        private const string Password = "Fill the details";
        private const string CassandraContactPoint = "Fill the details";
        private static readonly int CassandraPort = 10350;
        public static ISession session;

        public static async Task InitializeCassandraSession()
        {
            // Connect to cassandra cluster  (Cassandra API on Azure Cosmos DB supports only TLSv1.2)
            var options = new SSLOptions(SslProtocols.Tls12, true, ValidateServerCertificate);
            options.SetHostNameResolver((ipAddress) => CassandraContactPoint);
            Cluster cluster = Cluster.Builder()
                .WithCredentials(UserName, Password)
                .WithPort(CassandraPort)
                .AddContactPoint(CassandraContactPoint)
                .WithSSL(options)
                .Build();

            session = await cluster.ConnectAsync("sarathlal");
        }

        public static bool ValidateServerCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
        {
            if (sslPolicyErrors == SslPolicyErrors.None)
                return true;

            Trace.WriteLine("Certificate error: {0}", sslPolicyErrors.ToString());
            // Do not allow this client to communicate with unauthenticated servers.
            return false;
        }
    }
}

We have given the Cosmos DB Cassandra connection details inside the above class. A static Cassandra “Session” will be created at the application startup and will be shared across the entire application.

We can create the class “DataAccessCassandraProvider” inside the DataAccess folder. This class will implement the interface IDataAccessProvider and define all the CRUD operations. We will call these methods from our Web API controller later.

Please copy the below code and paste it into the class.

DataAccessCassandraProvider.cs

using BlazorWithCassandraAddressBook.Shared.Models;
using Cassandra;
using Cassandra.Mapping;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace BlazorWithCassandraAddressBook.Server.DataAccess
{
    public class DataAccessCassandraProvider : IDataAccessProvider
    {
        private readonly ILogger _logger;
        private readonly IMapper mapper;

        public DataAccessCassandraProvider(ILoggerFactory loggerFactory)
        {
            _logger = loggerFactory.CreateLogger("DataAccessCassandraProvider");
            mapper = new Mapper(CassandraInitializer.session);
        }

        public async Task AddRecord(AddressBook addressBook)
        {
            addressBook.Id = Guid.NewGuid().ToString();
            await mapper.InsertAsync(addressBook);
        }

        public async Task UpdateRecord(AddressBook addressBook)
        {
            var updateStatement = new SimpleStatement("UPDATE addressbook SET " +
                " firstname = ? " +
                ",lastname  = ? " +
                ",gender    = ? " +
                ",address   = ? " +
                ",zipcode   = ? " +
                ",country   = ? " +
                ",state     = ? " +
                ",phone     = ? " +
                " WHERE id  = ?", 
                addressBook.FirstName, addressBook.LastName, addressBook.Gender,
                addressBook.Address, addressBook.ZipCode, addressBook.Country,
                addressBook.State, addressBook.Phone, addressBook.Id);

            await CassandraInitializer.session.ExecuteAsync(updateStatement);
        }

        public async Task DeleteRecord(string id)
        {
            var deleteStatement = new SimpleStatement("DELETE FROM addressbook WHERE id = ?", id);
            await CassandraInitializer.session.ExecuteAsync(deleteStatement);
        }

        public async Task<AddressBook> GetSingleRecord(string id)
        {
            AddressBook addressBook = await mapper.SingleOrDefaultAsync<AddressBook>("SELECT * FROM addressbook WHERE id = ?", id);
            return addressBook;
        }

        public async Task<IEnumerable<AddressBook>> GetAllRecords()
        {
            IEnumerable<AddressBook> addressBooks = await mapper.FetchAsync<AddressBook>("SELECT * FROM addressbook");
            return addressBooks;
        }
    }
}

Please note we have defined all the CRUD operations inside the above class. For that, we have used the Mapper object for inserting a Cassandra record and getting records from Cassandra. For update and delete we used the Cassandra session Execute method.

We can initiate this DataAccess provider class inside the “Startup” class using dependency injection. We will also call the “InitializeCassandraSession” method the CassandraInitializer class from this Startup class.

Startup

Startup. cs

using BlazorWithCassandraAddressBook.Server.DataAccess;
using BlazorWithCassandraAddressBook.Shared.Models;
using Microsoft.AspNetCore.Blazor.Server;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.ResponseCompression;
using Microsoft.Extensions.DependencyInjection;
using System.Linq;
using System.Net.Mime;
using System.Threading.Tasks;

namespace BlazorWithCassandraAddressBook.Server
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();

            services.AddResponseCompression(options =>
            {
                options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[]
                {
                    MediaTypeNames.Application.Octet,
                    WasmMediaTypeNames.Application.Wasm,
                });
            });

            services.AddScoped<IDataAccessProvider, DataAccessCassandraProvider>();

            Task t = CassandraInitializer.InitializeCassandraSession();
            t.Wait();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseResponseCompression();

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseMvc(routes =>
            {
                routes.MapRoute(name: "default", template: "{controller}/{action}/{id?}");
            });

            app.UseBlazor<Client.Program>();
        }
    }
}

We can create the Web API controller “AddressBooksController” now.

AddressBooksController.cs

using BlazorWithCassandraAddressBook.Shared.Models;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace BlazorWithCassandraAddressBook.Server.Controllers
{
    public class AddressBooksController : Controller
    {
        private readonly IDataAccessProvider _dataAccessProvider;

        public AddressBooksController(IDataAccessProvider dataAccessProvider)
        {
            _dataAccessProvider = dataAccessProvider;
        }

        [HttpGet]
        [Route("api/AddressBooks/Get")]
        public async Task<IEnumerable<AddressBook>> Get()
        {
            return await _dataAccessProvider.GetAllRecords();
        }

        [HttpPost]
        [Route("api/AddressBooks/Create")]
        public async Task Create([FromBody] AddressBook addressBook)
        {
            if (ModelState.IsValid)
            {
                await _dataAccessProvider.AddRecord(addressBook);
            }
        }

        [HttpGet]
        [Route("api/AddressBooks/Details/{id}")]
        public async Task<AddressBook> Details(string id)
        {
            return await _dataAccessProvider.GetSingleRecord(id);
        }

        [HttpPut]
        [Route("api/AddressBooks/Edit")]
        public async Task Edit([FromBody] AddressBook addressBook)
        {
            if (ModelState.IsValid)
            {
                await _dataAccessProvider.UpdateRecord(addressBook);
            }
        }

        [HttpDelete]
        [Route("api/AddressBooks/Delete/{id}")]
        public async Task Delete(string id)
        {
            await _dataAccessProvider.DeleteRecord(id);
        }
    }
}

We have initialized the “IDataAccessProvider” interface in the controller constructor.

We have defined all the HTTP GET, PUT, DELETE, and POST methods for our CRUD actions inside this controller. Please note all these methods are asynchronous.

We have completed the coding part for Shared and Server projects. We can go to the Client project and modify the existing “NavMenu.cshtml” file. This is a Razor view file and is used for navigation purposes.

NavMenu.cshtml

<p class="top-row pl-4 navbar navbar-dark">
    <a class="navbar-brand" href="">Address Book App</a>
    <button class="navbar-toggler" onclick=@ToggleNavMenu>
        <span class="navbar-toggler-icon"></span>
    </button>
</p>

<p class=@(collapseNavMenu ? "collapse" : null) onclick=@ToggleNavMenu>
    <ul class="nav flex-column">
    
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="" Match=NavLinkMatch.All>
                <span class="oi oi-home" aria-hidden="true"></span> Home
            </NavLink>
        </li>
    
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="/listaddressbooks">
                <span class="oi oi-list-rich" aria-hidden="true"></span> Address Book Details
            </NavLink>
        </li>
    </ul>
</p>

@functions {
    bool collapseNavMenu = true;

    void ToggleNavMenu()
    {
        collapseNavMenu = !collapseNavMenu;
    }
}

We can add the “ListAddresBooks.cshtml” Razor view now. This view will be used to display all Address Book details.

ListAddresBooks.cshtml

@using BlazorWithCassandraAddressBook.Shared.Models
@page "/listaddressbooks"
@inject HttpClient Http

<h1>Address Book Details</h1>
<p>
    <a href="/addaddressbook">Create New Address Book</a>
</p>
@if (addressBooks == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class='table'>
        <thead>
            <tr>
                <th>First Name</th>
                <th>Last Name</th>
                <th>Gender</th>
                <th>Address</th>
                <th>ZipCode</th>
                <th>Phone</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var address in addressBooks)
            {
                <tr>
                    <td>@address.FirstName</td>
                    <td>@address.LastName</td>
                    <td>@address.Gender</td>
                    <td>@address.Address</td>
                    <td>@address.ZipCode</td>
                    <td>@address.Phone</td>
                    <td>
                        <a href='/editaddressbook/@address.Id'>Edit</a>
                        <a href='/deleteaddressbook/@address.Id'>Delete</a>
                    </td>
                </tr>
            }
        </tbody>
    </table>
}

@functions {
    AddressBook[] addressBooks;

    protected override async Task OnInitAsync()
    {
        addressBooks = await Http.GetJsonAsync<AddressBook[]>("/api/AddressBooks/Get");
    }
}

Please add the below three Razor views also.

AddAddressBook.cshtml

@using BlazorWithCassandraAddressBook.Shared.Models
@page "/addaddressbook"
@inject HttpClient Http
@inject Microsoft.AspNetCore.Blazor.Services.IUriHelper UriHelper

<h2>Create Address Book</h2>
<hr />
<form>
    <p class="row">
        <p class="col-md-4">
            <p class="form-group">
                <label for="FirstName" class="control-label">First Name</label>
                <input for="FirstName" class="form-control" bind="@addressBook.FirstName" />
            </p>
            <p class="form-group">
                <label for="Gender" class="control-label">Gender</label>
                <select for="Gender" class="form-control" bind="@addressBook.Gender">
                    <option value="">-- Select Gender --</option>
                    <option value="Male">Male</option>
                    <option value="Female">Female</option>
                </select>
            </p>
            <p class="form-group">
                <label for="ZipCode" class="control-label">ZipCode</label>
                <input for="ZipCode" class="form-control" bind="@addressBook.ZipCode" />
            </p>
            <p class="form-group">
                <label for="State" class="control-label">State</label>
                <input for="State" class="form-control" bind="@addressBook.State" />
            </p>
        </p>
        <p class="col-md-4">
            <p class="form-group">
                <label for="LastName" class="control-label">Last Name</label>
                <input for="LastName" class="form-control" bind="@addressBook.LastName" />
            </p>
            <p class="form-group">
                <label for="Address" class="control-label">Address</label>
                <input for="Address" class="form-control" bind="@addressBook.Address" />
            </p>
            <p class="form-group">
                <label for="Country" class="control-label">Country</label>
                <input for="Country" class="form-control" bind="@addressBook.Country" />
            </p>
            <p class="form-group">
                <label for="Phone" class="control-label">Phone</label>
                <input for="Phone" class="form-control" bind="@addressBook.Phone" />
            </p>
        </p>
    </p>
    <p class="row">
        <p class="col-md-4">
            <p class="form-group">
                <input type="button" class="btn btn-default" onclick="@(async () => await CreateAddressBook())" value="Save" />
                <input type="button" class="btn" onclick="@Cancel" value="Cancel" />
            </p>
        </p>
    </p>
</form>

@functions {

    AddressBook addressBook = new AddressBook();

    protected async Task CreateAddressBook()
    {
        await Http.SendJsonAsync(HttpMethod.Post, "/api/AddressBooks/Create", addressBook);
        UriHelper.NavigateTo("/listaddressbooks");
    }

    void Cancel()
    {
        UriHelper.NavigateTo("/listaddressbooks");
    }
}  

EditAddressBook.cshtml

@using BlazorWithCassandraAddressBook.Shared.Models
@page "/editaddressbook/{Id}"
@inject HttpClient Http
@inject Microsoft.AspNetCore.Blazor.Services.IUriHelper UriHelper

<h2>Edit</h2>
<h4>Address Book</h4>
<hr />

<form>
    <p class="row">
        <p class="col-md-4">
            <p class="form-group">
                <label for="FirstName" class="control-label">FirstName</label>
                <input for="FirstName" class="form-control" bind="@addressBook.FirstName" />
            </p>
            <p class="form-group">
                <label for="Gender" class="control-label">Gender</label>
                <select for="Gender" class="form-control" bind="@addressBook.Gender">
                    <option value="">-- Select Gender --</option>
                    <option value="Male">Male</option>
                    <option value="Female">Female</option>
                </select>
            </p>
            <p class="form-group">
                <label for="ZipCode" class="control-label">ZipCode</label>
                <input for="ZipCode" class="form-control" bind="@addressBook.ZipCode" />
            </p>
            <p class="form-group">
                <label for="State" class="control-label">State</label>
                <input for="State" class="form-control" bind="@addressBook.State" />
            </p>
        </p>
        <p class="col-md-4">
            <p class="form-group">
                <label for="LastName" class="control-label">LastName</label>
                <input for="LastName" class="form-control" bind="@addressBook.LastName" />
            </p>
            <p class="form-group">
                <label for="Address" class="control-label">Address</label>
                <input for="Address" class="form-control" bind="@addressBook.Address" />
            </p>
            <p class="form-group">
                <label for="Country" class="control-label">Country</label>
                <input for="Country" class="form-control" bind="@addressBook.Country" />
            </p>
            <p class="form-group">
                <label for="Phone" class="control-label">Phone</label>
                <input for="Phone" class="form-control" bind="@addressBook.Phone" />
            </p>
        </p>
    </p>
    <p class="row">
        <p class="form-group">
            <input type="button" class="btn btn-default" onclick="@(async () => await UpdateAddressBook())" value="Save" />
            <input type="button" class="btn" onclick="@Cancel" value="Cancel" />
        </p>
    </p>
</form>

@functions {

    [Parameter]
    string Id { get; set; }

    AddressBook addressBook = new AddressBook();

    protected override async Task OnInitAsync()
    {
        addressBook = await Http.GetJsonAsync<AddressBook>("/api/AddressBooks/Details/" + Id);
    }

    protected async Task UpdateAddressBook()
    {
        await Http.SendJsonAsync(HttpMethod.Put, "api/AddressBooks/Edit", addressBook);
        UriHelper.NavigateTo("/listaddressbooks");
    }

    void Cancel()
    {
        UriHelper.NavigateTo("/listaddressbooks");
    }
}

DeleteAddressBook.cshtml

@using BlazorWithCassandraAddressBook.Shared.Models
@page "/deleteaddressbook/{Id}"
@inject HttpClient Http
@inject Microsoft.AspNetCore.Blazor.Services.IUriHelper UriHelper

<h2>Delete</h2>
<p>Are you sure you want to delete this Address with Id: <b>@Id</b></p>
<br />
<p class="col-md-4">
    <table class="table">
        <tr>
            <td>FirstName</td>
            <td>@addressBook.FirstName</td>
        </tr>
        <tr>
            <td>LastName</td>
            <td>@addressBook.LastName</td>
        </tr>
        <tr>
            <td>Gender</td>
            <td>@addressBook.Gender</td>
        </tr>
        <tr>
            <td>Country</td>
            <td>@addressBook.Country</td>
        </tr>
        <tr>
            <td>Phone</td>
            <td>@addressBook.Phone</td>
        </tr>
    </table>
    <p class="form-group">
        <input type="button" value="Delete" onclick="@(async () => await Delete())" class="btn btn-default" />
        <input type="button" value="Cancel" onclick="@Cancel" class="btn" />
    </p>
</p>

@functions {

    [Parameter]
    string Id { get; set; }

    AddressBook addressBook = new AddressBook();

    protected override async Task OnInitAsync()
    {
        addressBook = await Http.GetJsonAsync<AddressBook>("/api/AddressBooks/Details/" + Id);
    }

    protected async Task Delete()
    {
        await Http.DeleteAsync("api/AddressBooks/Delete/" + Id);
        UriHelper.NavigateTo("/listaddressbooks");
    }

    void Cancel()
    {
        UriHelper.NavigateTo("/listaddressbooks");
    }

}

We have completed all the coding parts now. We can run the Address Book app now.

Address Book app

We can add a new Address Book record now.

 Book record

We can add one more record and display the records.

 Display the records.

We can edit one record.

Edit

I have changed the address field.

Now we can delete one record.

Delete

We can check the data in the Azure Cosmos DB account also. Please open the Data Explorer tab click CQL Query Text and Run. We will see one address book record created by our Blazor application.

CQL Query Text

In this article, we have seen how to create a Cosmos DB account with Cassandra API and we created a Blazor application with an ASP.NET Core-hosted template. We have used the CassandraCSharpDriver NuGet package to connect Blazor with Cassandra. We used some CQL (Cassandra Query Language) commands inside our DataAccess provider class. We have seen all CRUD operations with our Address Book application.

We will see more Blazor features in upcoming articles.


Similar Articles