Skip to content

Getting Started with .NET

This guide walks you through building a complete EvidentSource application using the .NET SDK.

  • .NET 10.0 or later
  • For WASM components: .NET WASI workload and Windows
  • An EvidentSource server instance (local or cloud)

The fastest way to get started is to use an existing example as a template:

Terminal window
# Clone the SDKs repository
git clone https://github.com/evidentsystems/evidentsource-sdks
cd evidentsource-sdks/dotnet/examples
# Copy the TodoMvc example as a starting point
cp -r TodoMvc ../my-app
cd ../my-app
# Build and run
dotnet build
./build_all.sh # Build WASM components (Windows only)
./install.sh --all # Deploy to server

The 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

If you prefer to set up manually or need a client-only application:

Terminal window
git clone https://github.com/evidentsystems/evidentsource-sdks.git
cd evidentsource-sdks/dotnet
Terminal window
mkdir -p ../my-banking-app
cd ../my-banking-app
dotnet new console -n BankingApp
cd BankingApp
Terminal window
dotnet add reference ../../evidentsource-sdks/dotnet/src/EvidentSource.Core/EvidentSource.Core.csproj
dotnet add reference ../../evidentsource-sdks/dotnet/src/EvidentSource.Client/EvidentSource.Client.csproj

Update Program.cs:

using EvidentSource.Client;
using EvidentSource.Core.Domain.Identifiers;
using EvidentSource.Core.Domain.Events;
using System.Text.Json;
// Connect to the server
await using var client = await EvidentSourceClient.ConnectAsync("http://localhost:50051");

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 database
var dbName = DatabaseName.Create("banking");
var conn = await client.ConnectAsync(dbName);
Console.WriteLine($"Connected at revision {conn.Revision}");
// Execute a state change
var 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 view
var 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);

State changes handle commands and emit events. Create a new project:

Terminal window
mkdir -p state-changes/OpenAccount
cd state-changes/OpenAccount
dotnet new console -n OpenAccount

Update 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:

OpenAccountStateChange.cs
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):

Terminal window
dotnet publish -c Release -p:BuildingForWasm=true

State views materialize read models from events:

AccountSummaryView.cs
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);
}

Deploy WASM components to the EvidentSource server:

Terminal window
# Create database
curl -X POST http://localhost:8080/api/v1/databases/banking
# Install state change
curl -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 view
curl -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"

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);
}
}