# Esiur for .NET [![CI](https://github.com/esiur/esiur-dotnet/actions/workflows/ci.yml/badge.svg)](https://github.com/esiur/esiur-dotnet/actions/workflows/ci.yml) [![NuGet](https://img.shields.io/nuget/v/Esiur.svg)](https://www.nuget.org/packages/Esiur) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](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. Version 3 focuses on a smaller public API, first-class ASP.NET Core hosting, extensible authentication, authenticated encryption, bounded resource usage, and predictable lifecycle behavior. The protocol remains designed for multiple language implementations: wire concepts and protocol names stay consistent, while each implementation can present idiomatic awaitables such as .NET `AsyncReply`, Dart `Future`, or JavaScript `Promise`. ## Highlights - Live distributed properties, method calls, and events. - Self-describing resource, record, enum, and type metadata. - C# source generation with `[Resource]` and `[Export]`. - Native TCP and ASP.NET Core WebSocket transports. - Isolated `Warehouse` runtimes with memory, Entity Framework Core, and MongoDB stores. - Multi-step authentication providers with initializer, responder, and dual identities. - Password challenge-response and PPAP ML-KEM-768 authentication. - AES-256-GCM encrypted records and protected post-authentication key rotation. - Permissions, rate-control, auditing, parser limits, connection limits, and attachment limits. - Graceful startup and shutdown that settle resource lifecycle operations. ## Packages All first-party v3 packages share major version `3` to make compatibility clear. | Package | Purpose | Target framework | | --- | --- | --- | | [`Esiur`](https://www.nuget.org/packages/Esiur) | Core runtime, EP protocol, stores, security, and source generator | .NET Standard 2.0 | | [`Esiur.AspNetCore`](https://www.nuget.org/packages/Esiur.AspNetCore) | ASP.NET Core hosting, dependency injection, endpoint routing, and WebSockets | .NET 8 / .NET 10 | | [`Esiur.Stores.EntityCore`](https://www.nuget.org/packages/Esiur.Stores.EntityCore) | Entity Framework Core 10 resource store | .NET 10 | | [`Esiur.Stores.MongoDB`](https://www.nuget.org/packages/Esiur.Stores.MongoDB) | MongoDB resource store | .NET 10 | | [`Esiur.CLI`](https://www.nuget.org/packages/Esiur.CLI) | Generate typed C# models from a remote EP resource | .NET 10 tool | The core runtime can be consumed by any compatible .NET Standard 2.0 application. Projects using the bundled source generator need a Roslyn 4.8 or newer toolchain, such as .NET SDK 8 or newer. Building this repository requires .NET SDK 10. ## Install For an ASP.NET Core service: ```shell dotnet add package Esiur.AspNetCore --version 3.0.0 ``` For the standalone runtime or a client: ```shell dotnet add package Esiur --version 3.0.0 ``` ## 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? 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("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"); ``` ASP.NET Core authorization protects the HTTP upgrade only. It does not replace Esiur authentication or per-resource authorization. ## 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( "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. For native TCP, omit `WebSocketUri` and include the EP port in the logical URL: ```csharp dynamic counter = await client.Get( "ep://localhost:10518/sys/counter"); ``` ## Standalone hosting Applications that do not 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 the 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. The ASP.NET Core password shortcut accepts precomputed Esiur credentials from an application-owned cache or account store: ```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("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. Esiur v3 currently includes these canonical protocol names: | Protocol | Role | | --- | --- | | `password-sha3-v1` | Simple password challenge-response for initializer, responder, or dual identity authentication | | `ppap-mlkem768-v1` | ML-KEM-768 authentication with password-derived or static identities | | `aes-gcm` | AES-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. Password registrations use a versioned nonce and encapsulation key. Successful rotation is performed only through a dedicated exchange after encryption is active, preventing the registration nonce from becoming a permanent network identifier. Persistent PPAP registration stores must implement atomic compare-and-rotate behavior. Applications with custom protocols can implement `IAuthenticationProvider` and register the provider through `UseAuthentication`. Protocol names are exact and case-sensitive; v3 does not negotiate legacy aliases. ## 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? options = null; options = new DbContextOptionsBuilder() .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 options) : DbContext(options) { public DbSet Devices => Set(); } ``` The repository test suite runs a complete SQLite schema, insert, materialization, and Warehouse lookup cycle. PostgreSQL through Npgsql and MySQL through 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 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. The default configuration includes limits for: - Packet size, decoded allocation size, collection items, and type-metadata depth. - Attached and pending resources per connection. - Concurrent connections and connection attempts globally and per IP address. - Encrypted record size. - Repeated rate-control denials. ASP.NET Core hosting also provides per-connection and host-wide pending WebSocket send limits. Configure these for the expected workload rather than disabling them: ```csharp builder.Services.AddEsiur(esiur => esiur .AddMemoryStore("sys") .AddResource("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 and should be configured together. ## 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 EF Core provider integration. ## Version 3 compatibility Version 3 is an intentional compatibility boundary: - All distributed first-party packages use major version `3`. - ASP.NET Core applications use `AddEsiur` and `MapEsiur` instead of the legacy integration. - Authentication and encryption providers negotiate canonical protocol names without aliases. - EntityCore targets .NET 10 and EF Core 10. - Legacy packet and hosting APIs removed during the v3 cleanup are not retained as compatibility shims. Within the v3 family, minor and patch package versions may advance independently. Consumers should keep the same major version across Esiur packages. ## License Esiur is available under the [MIT License](LICENSE).