Guide

Esiur for .NET

๐Ÿ“ฆ NuGet: Esiur, Esiur.AspNetCore ๐ŸŽฏ .NET Standard 2.0 runtime ยท .NET 10 SDK to build ๐Ÿ“œ MIT License

Esiur is a distributed resource framework for building real-time services without maintaining a separate RPC contract. A resource can expose properties, methods, events, records, and related resources over the self-describing EP protocol. Wire concepts and protocol names stay consistent across languages, while each implementation presents idiomatic awaitables โ€” .NET uses AsyncReply<T>, wrapped so it's await-compatible like a Task<T>.

Install

For an ASP.NET Core service:

shell
dotnet add package Esiur.AspNetCore --version 3.0.0

For the standalone runtime or a client-only application:

shell
dotnet add package Esiur --version 3.0.0

The core runtime targets .NET Standard 2.0, so it can be consumed from any compatible application. Projects using the bundled source generator need a Roslyn 4.8+ toolchain (.NET SDK 8 or newer). Building this repository from source requires .NET SDK 10.

Define a resource

Mark the class partial so Esiur's source generator can implement the resource lifecycle and generate exported properties from fields.

csharp
using Esiur.Resource;

[Resource]
public partial class CounterResource
{
    [Export]
    int count;

    [Export]
    public event ResourceEventHandler<int>? Changed;

    [Export]
    public string Hello(string name) => $"Hello, {name}!";

    [Export]
    public int Increment()
    {
        Count++;
        Changed?.Invoke(Count);
        return Count;
    }
}

The private count field becomes the exported Count property. Assigning it after the resource is attached publishes the modification to subscribed peers. Methods and events explicitly marked with [Export] become part of the remote definition.

ASP.NET Core quick start

Esiur.AspNetCore is the recommended way to host Esiur in a web application. The host owns startup and shutdown, while Esiur owns session authentication, encryption, resource authorization, and the resource graph.

csharp
using Esiur.AspNetCore;

var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseUrls("http://localhost:8080");

builder.Services.AddEsiur(esiur =>
{
    esiur
        .AddMemoryStore("sys")
        .AddResource<CounterResource>("sys/counter")
        .AllowAnonymous()             // Development only.
        .IncludeExceptionMessages();  // Development only.
});

var app = builder.Build();

var webSockets = new WebSocketOptions
{
    KeepAliveInterval = TimeSpan.FromSeconds(30),
};
webSockets.AllowedOrigins.Add("http://localhost:8080");

app.UseWebSockets(webSockets);
app.MapEsiur("/esiur");

await app.RunAsync();

AddEsiur registers one host-managed Warehouse and EpServer. Resources are constructed through dependency injection, configuration is validated at host startup, and active WebSockets are drained or aborted before resources are terminated during shutdown.

The endpoint returned by MapEsiur supports normal ASP.NET Core conventions:

csharp
app.MapEsiur("/esiur")
    .RequireAuthorization("EsiurUpgrade")
    .RequireRateLimiting("EsiurHandshakes");
Two authorization layers. ASP.NET Core authorization protects the HTTP upgrade only โ€” it does not replace Esiur's own session authentication or per-resource permissions.

Connect a .NET client

Use the logical ep:// resource URL and select the WebSocket transport with an EpConnectionContext:

csharp
using Esiur.Protocol;
using Esiur.Resource;

var client = new Warehouse();

dynamic counter = await client.Get<IResource>(
    "ep://localhost/sys/counter",
    new EpConnectionContext
    {
        WebSocketUri = new Uri("ws://localhost:8080/esiur"),
    });

Console.WriteLine(await counter.Hello("Ada"));
Console.WriteLine(await counter.Increment());

await client.Close();

The ep:// URL identifies the logical connection and resource path; WebSocketUri identifies the transport endpoint. Esiur automatically requests the case-sensitive EP WebSocket subprotocol โ€” see the protocol spec.

For native TCP, omit WebSocketUri and include the EP port in the logical URL:

csharp
dynamic counter = await client.Get<IResource>(
    "ep://localhost:10518/sys/counter");

Standalone hosting

Applications that don't use ASP.NET Core can compose the runtime directly. Warehouse is the root container for stores, resources, protocol handlers, security providers, policies, and lifecycle state.

csharp
using Esiur.Protocol;
using Esiur.Resource;
using Esiur.Stores;

var warehouse = new Warehouse();

await warehouse.Put("sys", new MemoryStore());
await warehouse.Put("sys/counter", new CounterResource());
await warehouse.Put("sys/server", new EpServer
{
    Port = 10518,
    AllowUnauthorizedAccess = true, // Development only.
});

await warehouse.Open();

try
{
    await Task.Delay(Timeout.InfiniteTimeSpan);
}
finally
{
    await warehouse.Close();
}

Use a separate Warehouse for each isolated server or client runtime โ€” avoid relying on a static default warehouse in applications that host more than one Esiur environment.

Authentication and encryption

Anonymous access is opt-in. Production services should omit AllowAnonymous, register an authentication provider, register an encryption provider, and require encryption.

csharp
using Esiur.AspNetCore;
using Esiur.Security.Authority.Providers;
using Esiur.Security.Cryptography;

var credentials =
    new Dictionary<(string Domain, string Identity), PasswordHash>();

builder.Services.AddEsiur(esiur =>
{
    esiur
        .AddMemoryStore("sys")
        .AddResource<CounterResource>("sys/counter")
        .UsePasswordAuthentication((identity, domain) =>
            credentials.TryGetValue((domain, identity), out var credential)
                ? credential
                : null)
        .UseEncryption(new AesEncryptionProvider())
        .RequireEncryption()
        .AllowWebSocketOrigins("https://app.example.com");
});

Create a PasswordHash once during enrollment with PasswordAuthenticationProvider.CreateCredential(passwordBytes). Clear the plaintext bytes after use and protect the stored verifier like a password โ€” it is sufficient to authenticate through the password-sha3-v1 protocol if stolen.

ProtocolRole
password-sha3-v1Simple password challenge-response for initializer, responder, or dual identity authentication
ppap-mlkem768-v1ML-KEM-768 (post-quantum) authentication with password-derived or static identities
aes-gcmAES-256-GCM record protection derived from the authenticated session key

The authentication provider abstraction supports multi-message handshakes. PPAP uses that pipeline for initializer, responder, or dual identity authentication, and supports password-derived Argon2id identities as well as static ML-KEM identities. Applications with custom protocols can implement IAuthenticationProvider and register it through UseAuthentication. Protocol names are exact and case-sensitive โ€” v3 does not negotiate legacy aliases.

PPAP is described in A. K. Zamil and A. D. Jasim, "A Multi-Factor Quantum-Resistant and Privacy-Preserving Authentication Protocol for Decentralized Systems," Mesopotamian Journal of CyberSecurity, vol. 5, no. 3, pp. 1272โ€“1291, 2025. Available online.

Entity Framework Core 10

Esiur.Stores.EntityCore integrates Esiur resource materialization and paths with EF Core 10.

shell
dotnet add package Esiur.Stores.EntityCore --version 3.0.0
dotnet add package Microsoft.EntityFrameworkCore.Sqlite --version 10.0.10

Configure the database provider first, then attach the EntityStore with the typed UseEsiur extension:

csharp
using Esiur.Resource;
using Esiur.Stores.EntityCore;
using Microsoft.EntityFrameworkCore;

var warehouse = new Warehouse();
var store = await warehouse.Put("database", new EntityStore());

DbContextOptions<AppDbContext>? options = null;
options = new DbContextOptionsBuilder<AppDbContext>()
    .UseSqlite("Data Source=app.db")
    .UseEsiur(store, () => new AppDbContext(options!))
    .Options;

await warehouse.Open();

await using var db = new AppDbContext(options);
await db.Database.EnsureCreatedAsync();
var device = await db.Devices.AddResourceAsync(new Device { Name = "sensor-1" });
csharp
using System.ComponentModel.DataAnnotations;
using Esiur.Resource;
using Microsoft.EntityFrameworkCore;

[Resource]
public partial class Device
{
    [Key, Export]
    int id;

    [Export]
    string name = string.Empty;
}

public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
    : DbContext(options)
{
    public DbSet<Device> Devices => Set<Device>();
}

The repository test suite runs a complete SQLite schema, insert, materialization, and Warehouse lookup cycle. PostgreSQL (via Npgsql) and MySQL (via Oracle's EF provider) are also checked for model creation and SQL translation, without requiring external database servers.

Generate typed client models

EP is self-describing, so clients can work dynamically or generate strongly typed models. Install the v3 CLI as a .NET global tool:

shell
dotnet tool install --global Esiur.CLI --version 3.0.0
esiur get-template ep://localhost:10518/sys/counter --dir Generated

Use --async-setters to generate asynchronous property setters. The CLI also accepts --username and --password, but command-line secrets may be visible to other processes or shell history โ€” prefer a safer credential workflow when the environment is shared.

Runtime limits and security defaults

WarehouseConfiguration bounds work performed on untrusted input: packet size, decoded allocation size, collection items and type-metadata depth, attached/pending resources per connection, concurrent connections globally and per IP address, encrypted record size, and repeated rate-control denials.

csharp
builder.Services.AddEsiur(esiur => esiur
    .AddMemoryStore("sys")
    .AddResource<CounterResource>("sys/counter")
    .LimitPendingWebSocketSendBytes(2 * 1024 * 1024)
    .LimitTotalPendingWebSocketSendBytes(256L * 1024 * 1024)
    .ConfigureWarehouse(configuration =>
    {
        configuration.Connections.MaximumConnections = 500;
        configuration.Connections.MaximumConnectionsPerIpAddress = 20;
        configuration.Connections.MaximumConnectionAttempts = 2_000;
    }));

When deploying behind a reverse proxy, trust only known proxy addresses and apply forwarded headers before UseWebSockets and MapEsiur. Browser origin checks, ASP.NET Core endpoint authorization, Esiur session authentication, and resource permissions are separate layers โ€” configure all of them, rather than relying on one.

Build and test

The repository is pinned to .NET SDK 10:

shell
dotnet restore Esiur.sln
dotnet build Esiur.sln --configuration Release
dotnet test Esiur.sln --configuration Release
dotnet list Esiur.sln package --vulnerable --include-transitive

The main automated suite covers packet parsing, serialization, authentication, PPAP registration rotation, encrypted records, resource attachment limits, connection admission, WebSockets, ASP.NET Core hosting, Warehouse lifecycle, and the EF Core provider.

Packages

All first-party v3 packages share major version 3, to make compatibility clear.

PackagePurposeTarget framework
EsiurCore runtime, EP protocol, stores, security, and source generator.NET Standard 2.0
Esiur.AspNetCoreASP.NET Core hosting, dependency injection, endpoint routing, WebSockets.NET 8 / .NET 10
Esiur.Stores.EntityCoreEntity Framework Core 10 resource store.NET 10
Esiur.Stores.MongoDBMongoDB resource store.NET 10
Esiur.CLIGenerate typed C# models from a remote EP resource.NET 10 tool
Wire-compatible. Every Esiur implementation speaks the same EP protocol โ€” see the protocol specification for the packet format, type encoding, and authentication handshake, or the TypeScript guide to connect from Node.js or a browser.