Getting Started with .NET
This guide walks you through building a complete EvidentSource application using the .NET SDK.
Prerequisites
Section titled “Prerequisites”- .NET 10.0 or later
- For WASM components: .NET WASI workload and Windows
- An EvidentSource server instance (local or cloud)
Quick Start with Examples
Section titled “Quick Start with Examples”The fastest way to get started is to use an existing example as a template:
# Clone the SDKs repositorygit clone https://github.com/evidentsystems/evidentsource-sdkscd evidentsource-sdks/dotnet/examples
# Copy the TodoMvc example as a starting pointcp -r TodoMvc ../my-appcd ../my-app
# Build and rundotnet build./build_all.sh # Build WASM components (Windows only)./install.sh --all # Deploy to serverThe examples include:
- TodoMvc/ - Simple CRUD example
- SavingsAccount/ - Complex banking domain with multiple state changes and views
Each example contains:
- Domain project - Shared types for events and commands
- state-changes/ - State change WASM components
- state-views/ - State view WASM components
- server/ - Example HTTP server
- build_all.sh / install.sh - Build and deployment scripts
Manual Project Setup
Section titled “Manual Project Setup”If you prefer to set up manually or need a client-only application:
1. Clone the SDK Repository
Section titled “1. Clone the SDK Repository”git clone https://github.com/evidentsystems/evidentsource-sdks.gitcd evidentsource-sdks/dotnet2. Create a New Project
Section titled “2. Create a New Project”mkdir -p ../my-banking-appcd ../my-banking-appdotnet new console -n BankingAppcd BankingApp3. Add Project References
Section titled “3. Add Project References”dotnet add reference ../../evidentsource-sdks/dotnet/src/EvidentSource.Core/EvidentSource.Core.csprojdotnet add reference ../../evidentsource-sdks/dotnet/src/EvidentSource.Client/EvidentSource.Client.csprojConnecting to EvidentSource
Section titled “Connecting to EvidentSource”Update Program.cs:
using EvidentSource.Client;using EvidentSource.Core.Domain.Identifiers;using EvidentSource.Core.Domain.Events;using System.Text.Json;
// Connect to the serverawait using var client = await EvidentSourceClient.ConnectAsync("http://localhost:50051");Authentication
Section titled “Authentication”For production deployments, you’ll need to authenticate with a JWT token. See Authentication for details on the trust model.
using EvidentSource.Client;
// Bearer token authentication (production)var token = Environment.GetEnvironmentVariable("EVS_TOKEN");await using var client = await EvidentSourceClient.ConnectAsync( "api.example.com:50051", Credentials.BearerToken(token));
// DevMode for local development (no auth required)await using var client = await EvidentSourceClient.ConnectAsync("http://localhost:50051");
// Connect to a databasevar dbName = DatabaseName.Create("banking");var conn = await client.ConnectAsync(dbName);
Console.WriteLine($"Connected at revision {conn.Revision}");
// Execute a state changevar scName = StateChangeName.Create("open-account");var db = await conn.ExecuteStateChangeAsync(scName, 1, new{ AccountId = "acct-001", CustomerName = "Alice Smith", InitialDeposit = 1000.00m});
Console.WriteLine($"New revision: {db.Revision}");
// Query a state viewvar svName = StateViewName.Create("account-summary");var view = await db.ViewStateAsync<AccountSummary>(svName, 1, [("account_id", "acct-001")]);
if (view?.Data is not null){ Console.WriteLine($"Balance: ${view.Data.Balance:F2}");}
record AccountSummary(string Id, string Name, decimal Balance, bool IsOpen);Building a State Change
Section titled “Building a State Change”State changes handle commands and emit events. Create a new project:
mkdir -p state-changes/OpenAccountcd state-changes/OpenAccountdotnet new console -n OpenAccountUpdate OpenAccount.csproj:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net10.0</TargetFramework> <RuntimeIdentifier Condition="'$(BuildingForWasm)' == 'true'">wasi-wasm</RuntimeIdentifier> <OutputType Condition="'$(BuildingForWasm)' == 'true'">Exe</OutputType> <OutputType Condition="'$(BuildingForWasm)' != 'true'">Library</OutputType> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <PublishTrimmed Condition="'$(BuildingForWasm)' == 'true'">true</PublishTrimmed> <SelfContained Condition="'$(BuildingForWasm)' == 'true'">true</SelfContained> <InvariantGlobalization>true</InvariantGlobalization> <UseAppHost>false</UseAppHost> <IlcExportUnmanagedEntrypoints Condition="'$(BuildingForWasm)' == 'true'">true</IlcExportUnmanagedEntrypoints> </PropertyGroup>
<ItemGroup> <ProjectReference Include="../../EvidentSource.Functions/EvidentSource.Functions.csproj" /> <ProjectReference Include="../../Domain/Domain.csproj" /> </ItemGroup>
<ItemGroup Condition="'$(BuildingForWasm)' == 'true'"> <PackageReference Include="BytecodeAlliance.Componentize.DotNet.Wasm.SDK" /> <Wit Include="../../interface/decider.wit" World="state-change" /> </ItemGroup></Project>Create the state change implementation:
using EvidentSource.Functions.Adapters;using EvidentSource.Functions.Database;using EvidentSource.Core.Domain.Events;using System.Text.Json;
namespace OpenAccount;
public record OpenAccountCommand( string AccountId, string CustomerName, decimal InitialDeposit);
public record AccountOpened(string AccountId, string CustomerName);public record AccountCredited(decimal Amount, string Description);
public sealed class OpenAccountStateChange : JsonStateChangeAdapter<OpenAccountCommand>{ protected override DecideResult Decide( IDatabase db, OpenAccountCommand cmd, StateChangeMetadata metadata) { // Validate if (string.IsNullOrWhiteSpace(cmd.CustomerName)) throw StateChangeException.Validation("Customer name is required");
if (cmd.InitialDeposit < 0) throw StateChangeException.Validation("Initial deposit cannot be negative");
// Check if account exists var existing = db.ViewState<object>("account-summary", 1, [("account_id", cmd.AccountId)]);
if (existing?.Data is not null) throw StateChangeException.Conflict("Account already exists");
var events = new List<ProspectiveEvent>();
// AccountOpened event events.Add(new ProspectiveEvent { Id = Guid.NewGuid().ToString(), Stream = $"accounts/{cmd.AccountId}", EventType = "com.banking.account.opened", Subject = cmd.AccountId, Data = EventData.FromString(JsonSerializer.Serialize( new AccountOpened(cmd.AccountId, cmd.CustomerName))), DataContentType = "application/json" });
// Initial deposit event if (cmd.InitialDeposit > 0) { events.Add(new ProspectiveEvent { Id = Guid.NewGuid().ToString(), Stream = $"accounts/{cmd.AccountId}", EventType = "com.banking.account.credited", Subject = cmd.AccountId, Data = EventData.FromString(JsonSerializer.Serialize( new AccountCredited(cmd.InitialDeposit, "Initial deposit"))), DataContentType = "application/json" }); }
return new DecideResult(events); }}Build the WASM component (Windows only):
dotnet publish -c Release -p:BuildingForWasm=trueBuilding a State View
Section titled “Building a State View”State views materialize read models from events:
using EvidentSource.Functions.Adapters;using EvidentSource.Core.Domain.Events;using System.Text.Json;
namespace AccountSummary;
public record AccountSummary(string Id, string Name, decimal Balance, bool IsOpen);
public abstract record AccountEvent{ public sealed record Opened(string AccountId, string CustomerName) : AccountEvent; public sealed record Credited(decimal Amount) : AccountEvent; public sealed record Debited(decimal Amount) : AccountEvent; public sealed record Closed : AccountEvent; public sealed record Unknown : AccountEvent;}
public sealed class AccountSummaryView : JsonStateViewAdapter<AccountSummary, AccountEvent>{ protected override AccountSummary InitialState() => new AccountSummary("", "", 0m, false);
protected override AccountEvent? ParseEvent(Event evt) => evt.EventType switch { "com.banking.account.opened" => ParseEventData<OpenedData>(evt) is { } d ? new AccountEvent.Opened(d.AccountId, d.CustomerName) : null, "com.banking.account.credited" => ParseEventData<CreditedData>(evt) is { } d ? new AccountEvent.Credited(d.Amount) : null, "com.banking.account.debited" => ParseEventData<DebitedData>(evt) is { } d ? new AccountEvent.Debited(d.Amount) : null, "com.banking.account.closed" => new AccountEvent.Closed(), _ => new AccountEvent.Unknown() };
protected override AccountSummary Evolve(AccountSummary state, AccountEvent evt) => evt switch { AccountEvent.Opened o => state with { Id = o.AccountId, Name = o.CustomerName, IsOpen = true }, AccountEvent.Credited c => state with { Balance = state.Balance + c.Amount }, AccountEvent.Debited d => state with { Balance = state.Balance - d.Amount }, AccountEvent.Closed => state with { IsOpen = false }, _ => state };
private record OpenedData(string AccountId, string CustomerName); private record CreditedData(decimal Amount); private record DebitedData(decimal Amount);}Deploying Components
Section titled “Deploying Components”Deploy WASM components to the EvidentSource server:
# Create databasecurl -X POST http://localhost:8080/api/v1/databases/banking
# Install state changecurl -X POST http://localhost:8080/api/v1/databases/banking/state-changes/open-account/versions/1 \ --data-binary @bin/Release/net10.0/wasi-wasm/publish/OpenAccount.wasm \ -H "Content-Type: application/wasm"
# Install state viewcurl -X POST http://localhost:8080/api/v1/databases/banking/state-views/account-summary/versions/1 \ --data-binary @bin/Release/net10.0/wasi-wasm/publish/AccountSummary.wasm \ -H "Content-Type: application/wasm"Testing
Section titled “Testing”Use the mock database for unit testing:
using EvidentSource.Functions.Testing;using Xunit;
public class OpenAccountTests{ [Fact] public void OpenAccount_WithValidData_ReturnsEvents() { var db = new MockDatabase("test-db").WithRevision(100); var stateChange = new OpenAccountStateChange();
var cmd = new OpenAccountCommand("acct-001", "Alice", 100m); var result = stateChange.Execute(db, cmd, new StateChangeMetadata());
Assert.Equal(2, result.Events.Count); // opened + credited }
[Fact] public void OpenAccount_WithNegativeDeposit_ThrowsValidation() { var db = new MockDatabase("test-db"); var stateChange = new OpenAccountStateChange();
var cmd = new OpenAccountCommand("acct-001", "Alice", -50m);
Assert.Throws<StateChangeException>(() => stateChange.Execute(db, cmd, new StateChangeMetadata())); }
[Fact] public void OpenAccount_WhenExists_ThrowsConflict() { var db = new MockDatabase("test-db"); db.InsertStateView("account-summary", 1, [("account_id", "acct-001")], new { Id = "acct-001" }, 50);
var stateChange = new OpenAccountStateChange(); var cmd = new OpenAccountCommand("acct-001", "Alice", 100m);
var ex = Assert.Throws<StateChangeException>(() => stateChange.Execute(db, cmd, new StateChangeMetadata()));
Assert.Equal(StateChangeErrorCode.Conflict, ex.Code); }}Next Steps
Section titled “Next Steps”- Explore the Savings Account example for a complete banking application
- Learn about bi-temporal queries for historical analysis
- Read about DCB constraints for optimistic concurrency control