Skip to content
Vignesh Blog
Go back

Azure tables in Entity framework pattern

Table of contents

Open Table of contents

Intro

The idea is to have entity framework pattern like having an dbContext for Azure Tables, it is not available due to the strategies of saving data into azure tables requires multiple partitions for efficient read, this library is suitable when you have single partition strategy.

Requirements

Installation

Add the NuGet package to your application:

dotnet add package AzureTableContext

Define an entity and context

Entities must have a public parameterless constructor. Public read/write properties are stored as table columns. Supported property types are string, bool, int, long, double, DateTime, DateTimeOffset, Guid, byte[], nullable versions of those types, and enums.

using Azure.Data.Tables;
using AzureTableContext;

public sealed class Order
{
	public string TenantId { get; set; } = string.Empty;
	public Guid Id { get; set; }
	public string Description { get; set; } = string.Empty;
	public decimal Total { get; set; }
}

public sealed class AppDbContext(TableServiceClient serviceClient)
	: AzureDbContext(serviceClient)
{
	public AzureDbSet<Order> Orders { get; private set; } = null!;

	protected override void OnModelCreating(AzureModelBuilder modelBuilder)
	{
		modelBuilder.Entity<Order>()
			.ToTable("Orders")
			.HasPartitionKey(order => order.TenantId)
			.HasRowKey(order => order.Id.ToString());
	}
}

If ToTable is omitted, the AzureDbSet<T> property name is used as the table name. Every set must configure both a partition key and a row key.

Register with dependency injection

The context and TableServiceClient are registered as singletons. Use a connection string from configuration or a secret store rather than hard-coding production credentials.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAzureTableContext<AppDbContext>(
	builder.Configuration.GetConnectionString("Storage")!);

For Azurite, the development connection string is commonly:

UseDevelopmentStorage=true

Inject the context into an application service or endpoint:

app.MapPost("/orders", async (Order order, AppDbContext db, CancellationToken ct) =>
{
	await db.Orders.AddAsync(order, ct);
	return Results.Created($"/orders/{order.Id}", order);
});

CRUD and queries

All operations are immediate requests to Table Storage; there is no change tracking.

await db.Orders.AddAsync(order, ct);       // Fails if the row already exists.
await db.Orders.UpsertAsync(order, ct);    // Inserts or fully replaces the row.

var existing = await db.Orders.FindAsync(
	order.TenantId, order.Id.ToString(), ct);

await db.Orders.DeleteAsync(order, ct);
await db.Orders.DeleteAsync(order.TenantId, order.Id.ToString(), ct);

await foreach (var item in db.Orders.QueryPartitionAsync(order.TenantId, ct))
{
	Console.WriteLine(item.Description);
}

For arbitrary OData filters, use QueryAsync. Escape user input with the Azure SDK helper:

var filter = TableClient.CreateQueryFilter($"PartitionKey eq {tenantId}");
await foreach (var order in db.Orders.QueryAsync(filter, ct))
{
	// Process order.
}

Share this post:

Previous Post
What I like in c# records