Introduction
In this post, we will create a single page application in Blazor. We will store the data in Azure Table Storage. Also, we will see all the CRUD operations in this Blazor project. For that, we will create a Person app by storing the Id, Name, Country, Age, and Gender. We will generate the Id column value as GUID.
About Azure Table Storage
Azure Table Storage is a service that stores the structured NoSQL data in the cloud, providing a key/attribute store with a schemaless design. Table storage is always schema-less. Access to Table Storage data is fast and cost-effective for many types of applications and it is typically lower in cost than traditional SQL for similar volumes of data.
You can use table storage to store the flexible datasets like user data for web applications, device information, address books, or any other types of metadata that your service requires. You can store any number of entities in a table, and a storage account may contain any number of tables, up to the capacity limit of your storage account.
The “PartitionKey” and “RowKey” are major elements in Azure Table Storage and are used to save the data in a structured way, like indexing in SQL. “RowKey” mainly acts as a primary key in SQL.
You can refer to the below article to get more details on Azure Table Storage.
About Blazor Framework
Blazor is (still an experimental) .NET web framework from Microsoft using C#/Razor and HTML that runs in the browser with Web Assembly. Blazor provides all the benefits of a client-side web UI framework using .NET on the client and optionally on the server.
I have already written many articles on Blazor for you good people on C# Corner. If you are new to it, please refer to the below articles to get started with Blazor.
Create Azure Table Storage
Log into Azure Portal.
Create a Resource -> choose Storage Account.
After choosing the resource group, you can give a valid and unique (globally) name to the storage account. Click the “Review + create” button to validate the service details and then click the “Create” button to start the deployment of the storage account.
After a few moments, our storage account service will be ready.
Go to the Resource and choose “Configuration” blade and select “Disabled” option under "Secure transfer required". Click the “Save” button to save the settings. If you do not choose this, you can’t connect Azure Table Storage to Blazor application without HTTPS.
Go to “Overview” blade and choose “Tables” option to create a new table.
We can add multiple tables to the same storage account depending on the capacity of the storage account. Each table can contain multiple entities and each entity can store multiple documents. We will create a “Person” entity using C# in our Blazor project later.
You can click the “Access keys” blade and copy the connection string and save that to a secure place. We will use this connection string later in our Blazor application.
Create a Blazor project in Visual Studio 2017
We can create a new Blazor project using Visual Studio 2017 (I am using free community edition). Currently, there are three types of templates available for Blazor. We can choose Blazor (ASP.NET Core hosted) template.
Our solution will be ready shortly. Please note that there are three projects created in our solution - “Client”, “Server”, and “Shared”.
By default, Blazor creates many files in these three projects. We can remove all the unwanted files like “Counter.cshtml”, “FetchData.cshtml”, “SurveyPrompt.cshtml” from the Client project, “SampleDataController.cs” file from the Server project, and remove “WeatherForecast.cs” file from the Shared project.
We are going to process Azure Table Storage operations using “WindowsAzure.Storage” NuGet package. We must install this package in “Shared” project.
Create a “Models” folder in “Shared” project and create a “Person” class inside this folder.
Person.cs
using Microsoft.WindowsAzure.Storage.Table;
namespace BlazorAzureTableStorage.Shared.Models {
public class Person: TableEntity {
public string Id {
get;
set;
}
public string Name {
get;
set;
}
public string Country {
get;
set;
}
public int Age {
get;
set;
}
public string Gender {
get;
set;
}
}
}
Please note that we have inherited this class from the “TableEntity” class. We will use the “PartitionKey” and “RowKey” properties from the TableEntity class.
We can create a “DataAccess” folder in the Shared project. We then create “AzureTableSettings” inside this folder.
AzureTableSettings.cs
namespace BlazorAzureTableStorage.Shared.DataAccess {
public class AzureTableSettings {
public AzureTableSettings(string connectionString, string tableName) {
TableName = tableName;
ConnectionString = connectionString;
}
public string TableName {
get;
}
public string ConnectionString {
get;
set;
}
}
}
This file contains the ConnectionString and table name properties which we will later use to establish the CRUD operations.
Create an “IAzureTableStorage” interface to define the method declaration for CRUD operations.
IAzureTableStorage.cs
using Microsoft.WindowsAzure.Storage.Table;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace BlazorAzureTableStorage.Shared.DataAccess {
public interface IAzureTableStorage < T > where T: TableEntity, new() {
Task Delete(string rowKey);
Task < T > GetItem(string rowKey);
Task < List < T >> GetList();
Task Insert(T item);
Task Update(T item);
}
}
We can now implement the above interface in “AzureTableStorage” class.
AzureTableStorage.cs
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Table;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace BlazorAzureTableStorage.Shared.DataAccess {
public class AzureTableStorage < T > : IAzureTableStorage < T > where T: TableEntity, new() {
private readonly AzureTableSettings settings;
public AzureTableStorage(AzureTableSettings settings) {
this.settings = settings;
}
public async Task < List < T >> GetList() {
//Table
CloudTable table = await GetTableAsync();
//Query
TableQuery < T > query = new TableQuery < T > ();
List < T > results = new List < T > ();
TableContinuationToken continuationToken = null;
do {
TableQuerySegment < T > queryResults = await table.ExecuteQuerySegmentedAsync(query, continuationToken);
continuationToken = queryResults.ContinuationToken;
results.AddRange(queryResults.Results);
} while (continuationToken != null);
return results;
}
public async Task < T > GetItem(string rowKey) {
//Table
CloudTable table = await GetTableAsync();
//Operation
TableOperation operation = TableOperation.Retrieve < T > ("", rowKey);
//Execute
TableResult result = await table.ExecuteAsync(operation);
return (T)(dynamic) result.Result;
}
public async Task Insert(T item) {
//Table
CloudTable table = await GetTableAsync();
//Operation
TableOperation operation = TableOperation.Insert(item);
//Execute
await table.ExecuteAsync(operation);
}
public async Task Update(T item) {
//Table
CloudTable table = await GetTableAsync();
//Operation
TableOperation operation = TableOperation.InsertOrReplace(item);
//Execute
await table.ExecuteAsync(operation);
}
public async Task Delete(string rowKey) {
//Item
T item = await GetItem(rowKey);
//Table
CloudTable table = await GetTableAsync();
//Operation
TableOperation operation = TableOperation.Delete(item);
//Execute
await table.ExecuteAsync(operation);
}
private async Task < CloudTable > GetTableAsync() {
//Account
CloudStorageAccount storageAccount = CloudStorageAccount.Parse(this.settings.ConnectionString);
//Client
CloudTableClient tableClient = storageAccount.CreateCloudTableClient();
//Table
CloudTable table = tableClient.GetTableReference(this.settings.TableName);
await table.CreateIfNotExistsAsync();
return table;
}
}
}
We can create another interface “IPersonService” for the Person service. It will contain all the method declarations in the previous interface IAzureTableStorage. Only the method names are changed.
IPersonService.cs
using BlazorAzureTableStorage.Shared.Models;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace BlazorAzureTableStorage.Shared.DataAccess {
public interface IPersonService {
Task AddPerson(Person item);
Task DeletePerson(string id);
Task < Person > GetPerson(string id);
Task < List < Person >> GetPeople();
Task UpdatePerson(Person item);
}
}
Create a service class “PersonService” and implement the interface IPersonService.
PersonService.cs
using BlazorAzureTableStorage.Shared.Models;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace BlazorAzureTableStorage.Shared.DataAccess {
public class PersonService: IPersonService {
private readonly IAzureTableStorage < Person > repository;
public PersonService(IAzureTableStorage < Person > repository) {
this.repository = repository;
}
public async Task < List < Person >> GetPeople() {
return await this.repository.GetList();
}
public async Task < Person > GetPerson(string id) {
return await this.repository.GetItem(id);
}
public async Task AddPerson(Person item) {
await this.repository.Insert(item);
}
public async Task UpdatePerson(Person item) {
await this.repository.Update(item);
}
public async Task DeletePerson(string id) {
await this.repository.Delete(id);
}
}
}
We can go to the “Server” project and add “appsettings.json”. This JSON file will be used for configuration settings. We will keep the Azure Table Storage connection string and table name inside this JSON.
appsettings.json
{
"ConnectionString": "DefaultEndpointsProtocol=https;AccountName=sarathlal;AccountKey=<Your Access Key>;EndpointSuffix=core.windows.net",
"TableName": "SarathTable"
}
We can modify the “Startup” class with the below code.
Startup.cs
using BlazorAzureTableStorage.Shared.DataAccess;
using BlazorAzureTableStorage.Shared.Models;
using Microsoft.AspNetCore.Blazor.Server;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.ResponseCompression;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System.Linq;
using System.Net.Mime;
namespace BlazorAzureTableStorage.Server {
public class Startup {
public static IConfiguration Configuration;
public Startup(IConfiguration configuration) {
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services) {
services.AddScoped < IAzureTableStorage < Person >> (factory => {
return new AzureTableStorage < Person > (new AzureTableSettings(connectionString: Configuration["ConnectionString"], tableName: Configuration["TableName"]));
});
services.AddScoped < IPersonService, PersonService > ();
services.AddMvc();
services.AddResponseCompression(options => {
options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new [] {
MediaTypeNames.Application.Octet,
WasmMediaTypeNames.Application.Wasm,
});
});
}
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 have injected the dependencies for Person service in this class.
Create the “PeopleController” class inside the “Controllers” folder. Copy the below code to this class.
PeopleController.cs
using BlazorAzureTableStorage.Shared.DataAccess;
using BlazorAzureTableStorage.Shared.Models;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BlazorAzureTableStorage.Server.Controllers {
[Route("api/[controller]")]
[ApiController]
public class PeopleController: Controller {
private readonly IPersonService service;
public PeopleController(IPersonService service) {
this.service = service;
}
[HttpGet]
public async Task < List < Person >> Get() {
var model = await service.GetPeople();
return model.ToList();
}
[HttpGet("{id}")]
public async Task < Person > Get(string id) {
var model = await service.GetPerson(id);
return model;
}
[HttpPost]
public async Task < bool > Create([FromBody] Person person) {
if (person == null) return false;
person.Id = Guid.NewGuid().ToString();
person.PartitionKey = "";
person.RowKey = person.Id;
if (!ModelState.IsValid) return false;
await service.AddPerson(person);
return true;
}
[HttpPut("{id}")]
public async Task < bool > Update(string id, [FromBody] Person person) {
person.Id = id;
person.PartitionKey = "";
person.RowKey = person.Id;
if (!ModelState.IsValid) return false;
await service.UpdatePerson(person);
return true;
}
[HttpDelete("{id}")]
public async Task Delete(string id) {
await service.DeletePerson(id);
}
}
}
We have defined all the HTTP methods inside the above API class.
We can go to “Client” project and modify the “NavMenu.cshtml” Razor View file inside “Shared” folder with the below code.
NavMenu.cshtml
<div class="top-row pl-4 navbar navbar-dark">
<a class="navbar-brand" href="">Blazor Azure Table Storage</a>
<button class="navbar-toggler" onclick=@ToggleNavMenu>
<span class="navbar-toggler-icon"></span>
</button>
</div>
<div 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="/listpeople">
<span class="oi oi-plus" aria-hidden="true"></span> People Details </NavLink>
</li>
</ul>
</div> @functions { bool collapseNavMenu = true; void ToggleNavMenu() { collapseNavMenu = !collapseNavMenu; } }
The above file handles the page navigation. We will show a “People Details” menu along with the “Home” menu.
Create a new Razor View file, such as “ListPeople.cshtml” inside the “Pages” folder and add the below code.
ListPeople.cshtml
@using BlazorAzureTableStorage.Shared.Models
@page "/listpeople"
@inject HttpClient Http
<h1>People Details</h1>
<p>
<a href="/addperson">Create New Person</a>
</p>
@if (people == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class='table'>
<thead>
<tr>
<th>Name</th>
<th>Country</th>
<th>Age</th>
<th>Gender</th>
</tr>
</thead>
<tbody> @foreach (var person in people) { <tr>
<td>@person.Name</td>
<td>@person.Country</td>
<td>@person.Age</td>
<td>@person.Gender</td>
<td>
<a href='/editperson/@person.Id'>Edit</a>
<a href='/deleteperson/@person.Id'>Delete</a>
</td>
</tr> } </tbody>
</table>
}
@functions {
Person[] people;
protected override async Task OnInitAsync() {
people = await Http.GetJsonAsync < Person[] > ("/api/people");
}
}
The above Razor View will display all the Person details from Azure Table Storage.
We can add other three Razor View files for adding, editing, and deleting the Person data.
AddPerson.cshtml
@using BlazorAzureTableStorage.Shared.Models
@page "/addperson"
@inject HttpClient Http
@inject Microsoft.AspNetCore.Blazor.Services.IUriHelper UriHelper
<h3>Create Person</h3>
<hr />
<form>
<div class="row">
<div class="col-md-8">
<div class="form-group">
<label for="Name" class="control-label">Name</label>
<input for="Name" class="form-control" bind="@person.Name" />
</div>
<div class="form-group">
<label for="Country" class="control-label">Country</label>
<input for="Country" class="form-control" bind="@person.Country" />
</div>
<div class="form-group">
<label for="Age" class="control-label">Age</label>
<input for="Age" class="form-control" bind="@person.Age" />
</div>
<div class="form-group">
<label for="Gender" class="control-label">Gender</label>
<select for="Gender" class="form-control" bind="@person.Gender">
<option value="">-- Select Gender --</option>
<option value="Male">Male</option>
<option value="Female">Female</option>
</select>
</div>
</div>
</div>
<div class="row">
<div class="col-md-4">
<div class="form-group">
<input type="button" class="btn btn-default" onclick="@(async () => await CreatePerson())" value="Save" />
<input type="button" class="btn" onclick="@Cancel" value="Cancel" />
</div>
</div>
</div>
</form>
@functions {
Person person = new Person();
protected async Task CreatePerson() {
await Http.SendJsonAsync(HttpMethod.Post, "/api/people", person);
UriHelper.NavigateTo("/listpeople");
}
void Cancel() {
UriHelper.NavigateTo("/listpeople");
}
}
EditPerson.cshtml
@using BlazorAzureTableStorage.Shared.Models
@page "/editperson/{id}"
@inject HttpClient Http
@inject Microsoft.AspNetCore.Blazor.Services.IUriHelper UriHelper
<h3>Edit Person</h3>
<hr />
<form>
<div class="row">
<div class="col-md-8">
<div class="form-group">
<label for="Name" class="control-label">Name</label>
<input for="Name" class="form-control" bind="@person.Name" />
</div>
<div class="form-group">
<label for="Country" class="control-label">Country</label>
<input for="Country" class="form-control" bind="@person.Country" />
</div>
<div class="form-group">
<label for="Age" class="control-label">Age</label>
<input for="Age" class="form-control" bind="@person.Age" />
</div>
<div class="form-group">
<label for="Gender" class="control-label">Gender</label>
<select for="Gender" class="form-control" bind="@person.Gender">
<option value="">-- Select Gender --</option>
<option value="Male">Male</option>
<option value="Female">Female</option>
</select>
</div>
</div>
</div>
<div class="row">
<div class="form-group">
<input type="button" class="btn btn-default" onclick="@(async () => await UpdatePerson())" value="Save" />
<input type="button" class="btn" onclick="@Cancel" value="Cancel" />
</div>
</div>
</form>
@functions {
[Parameter]
string id {
get;
set;
}
Person person = new Person();
protected override async Task OnInitAsync() {
person = await Http.GetJsonAsync < Person > ("/api/people/" + id);
}
protected async Task UpdatePerson() {
await Http.SendJsonAsync(HttpMethod.Put, "api/people/" + id, person);
UriHelper.NavigateTo("/listpeople");
}
void Cancel() {
UriHelper.NavigateTo("/listpeople");
}
}
DeletePerson.cshtml
@using BlazorAzureTableStorage.Shared.Models
@page "/deleteperson/{id}"
@inject HttpClient Http
@inject Microsoft.AspNetCore.Blazor.Services.IUriHelper UriHelper
<h2>Delete</h2>
<p>Are you sure you want to delete this Person with Id :<b> @id</b></p>
<br />
<div class="col-md-4">
<table class="table">
<tr>
<td>Name</td>
<td>@person.Name</td>
</tr>
<tr>
<td>Country</td>
<td>@person.Country</td>
</tr>
<tr>
<td>Age</td>
<td>@person.Age</td>
</tr>
<tr>
<td>Gender</td>
<td>@person.Gender</td>
</tr>
</table>
<div 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" />
</div>
</div>
@functions {
[Parameter]
string id {
get;
set;
}
Person person = new Person();
protected override async Task OnInitAsync() {
person = await Http.GetJsonAsync < Person > ("/api/people/" + id);
}
protected async Task Delete() {
await Http.DeleteAsync("api/people/" + id);
UriHelper.NavigateTo("/listpeople");
}
void Cancel() {
UriHelper.NavigateTo("/listpeople");
}
}
We can also modify the “Index.cshtml” file with the below code.
Index.cshtml
@page "/"
<h3>Blazor Application With Azure Table Storage</h3>
<p>
We will create a single page application in Blazor. We will store the data in Azure Table Storage.
We will see all the CRUD operation in this Blazor project.
We will create a Person app with Id, Name, Country, Age, and Gender.
</p>
Run the application.
We have completed all the coding part. We can run the Blazor application.
We can click the “People Details” link and open the "List People" page. Click the “Create New Person” link to add a new person.
After saving the data, we can get the Person details in the grid. We can click “Edit” to modify the data.
I have modified the person name. We can add some more persons and their details. We get all the persons' data in a grid format.
We can also delete the data by clicking the “Delete” link. It will ask for confirmation before deleting the data.
After clicking the “Delete” button, our persons' data will be deleted.
In this post, we have created a storage account in Azure portal and created table storage. We have created a Blazor project in Visual Studio and performed all CRUD operations with Azure Table Storage.
References
- https://docs.microsoft.com/en-us/azure/cosmos-db/table-storage-overview
- https://docs.microsoft.com/en-us/azure/cosmos-db/table-storage-how-to-use-dotnet
- https://www.c-sharpcorner.com/article/azure-table-storage-in-asp-net-core-2-0/
We can see more exciting features of Blazor in upcoming posts.