ASP.NET Core namespace resolution
The ASP.NET Core client provides namespace resolution designed for multi-tenant web applications. It integrates with HTTP context and offers built-in resolvers for common request patterns.
If your multi-tenant setup is based on Arc Tenancy, you can map the current tenant to a namespace. See Arc Tenancy.
Built-in resolvers
Section titled “Built-in resolvers”HTTP header resolution (default)
Section titled “HTTP header resolution (default)”The default resolver reads the namespace from an HTTP header. Configure it through ChronicleAspNetCoreOptions:
using Microsoft.AspNetCore.Builder;
public static class NamespacesAspNetCoreHttpHeaderResolver{ public static void Configure(WebApplicationBuilder builder) => builder.AddCratisChronicle(options => { options.EventStore = "my-event-store"; options.WithHttpHeaderNamespaceResolver("x-cratis-tenant-id"); // Default header name });}import io.cratis.chronicle.EventStoreNamespaceNameimport io.cratis.chronicle.namespaces.IEventStoreNamespaceResolver
/** * Takes the namespace from a header on the current HTTP request, falling back to the default * namespace for work happening outside a request. The Spring Boot integration ships this exact * strategy as io.cratis.chronicle.spring.namespaces.HttpHeaderNamespaceResolver, applied * automatically via cratis.chronicle.namespace-resolution.strategy=HTTP_HEADER - this version takes * the header lookup as a parameter so it has no framework dependency. */class HttpHeaderNamespaceResolver( private val headerName: String, private val currentHeaderValue: (String) -> String?) : IEventStoreNamespaceResolver { override fun resolve(): String = currentHeaderValue(headerName)?.takeIf { it.isNotBlank() } ?: EventStoreNamespaceName.default.value}import io.cratis.chronicle.namespaces.IEventStoreNamespaceResolver;
import java.util.function.Function;
// Takes the namespace from a header on the current HTTP request, falling back to the default// namespace for work happening outside a request. The Spring Boot integration ships this exact// strategy as io.cratis.chronicle.spring.namespaces.HttpHeaderNamespaceResolver, applied// automatically via cratis.chronicle.namespace-resolution.strategy=HTTP_HEADER - this version// takes the header lookup as a parameter so it has no framework dependency.class HttpHeaderNamespaceResolver implements IEventStoreNamespaceResolver { private final String headerName; private final Function<String, String> currentHeaderValue;
public HttpHeaderNamespaceResolver(String headerName, Function<String, String> currentHeaderValue) { this.headerName = headerName; this.currentHeaderValue = currentHeaderValue; }
@Override public String resolve() { String value = currentHeaderValue.apply(headerName); return value == null || value.isBlank() ? "Default" : value; }}Elixir does not support this workflow yet.TypeScript does not support this workflow yet.When the header is present, its value becomes the namespace for the request. If the header is missing, the default namespace is used.
Subdomain resolution
Section titled “Subdomain resolution”The subdomain resolver extracts the namespace from the request host (for example, tenant1.example.com).
using Microsoft.AspNetCore.Builder;
public static class NamespacesAspNetCoreSubdomainResolver{ public static void Configure(WebApplicationBuilder builder) => builder.AddCratisChronicle(options => { options.EventStore = "my-event-store"; options.WithSubdomainNamespaceResolver(); });}import io.cratis.chronicle.EventStoreNamespaceNameimport io.cratis.chronicle.namespaces.IEventStoreNamespaceResolver
/** * Takes the namespace from the subdomain of the current request's host - "acme" in * acme.example.com - falling back to the default namespace for a host with no subdomain, or for * work happening outside a request. The Spring Boot integration ships this exact strategy as * io.cratis.chronicle.spring.namespaces.SubdomainNamespaceResolver; this version takes the current * host as a parameter so it has no framework dependency. */class SubdomainNamespaceResolver(private val currentHost: () -> String?) : IEventStoreNamespaceResolver { override fun resolve(): String { val parts = currentHost()?.split('.') ?: return EventStoreNamespaceName.default.value return if (parts.size > 2) parts.first() else EventStoreNamespaceName.default.value }}import io.cratis.chronicle.namespaces.IEventStoreNamespaceResolver;
import java.util.function.Supplier;
// Takes the namespace from the subdomain of the current request's host - "acme" in// acme.example.com - falling back to the default namespace for a host with no subdomain, or for// work happening outside a request. The Spring Boot integration ships this exact strategy as// io.cratis.chronicle.spring.namespaces.SubdomainNamespaceResolver; this version takes the// current host as a parameter so it has no framework dependency.class SubdomainNamespaceResolver implements IEventStoreNamespaceResolver { private final Supplier<String> currentHost;
public SubdomainNamespaceResolver(Supplier<String> currentHost) { this.currentHost = currentHost; }
@Override public String resolve() { String host = currentHost.get(); if (host == null) { return "Default"; } String[] parts = host.split("\\."); return parts.length > 2 ? parts[0] : "Default"; }}Elixir does not support this workflow yet.TypeScript does not support this workflow yet.Custom resolvers
Section titled “Custom resolvers”You can configure a custom resolver either by type (DI) or using the IChronicleBuilder fluent API.
Using the builder (recommended)
Section titled “Using the builder (recommended)”Pass a resolver instance using the IChronicleBuilder configure callback:
using Microsoft.AspNetCore.Builder;using Microsoft.Extensions.Configuration;
public static class NamespacesAspNetCoreBuilderCustomResolver{ public static void Configure(WebApplicationBuilder builder, IConfiguration someConfiguration) => builder.AddCratisChronicle( configureOptions: options => options.EventStore = "my-event-store", configure: b => b.WithNamespaceResolver(new CustomNamespaceResolver(someConfiguration)));}import io.cratis.chronicle.EventStoreNamespaceNameimport io.cratis.chronicle.namespaces.IEventStoreNamespaceResolver
/** * Kotlin has no separate builder step to register a namespace resolver structurally - a resolver * is just an IEventStoreNamespaceResolver implementation, constructed with whatever configuration * it needs and handed to callers directly instead of wired up through a hosting builder. */class ConfiguredNamespaceResolver(private val configuredNamespace: String?) : IEventStoreNamespaceResolver { override fun resolve(): String = configuredNamespace ?: EventStoreNamespaceName.default.value}
fun createConfiguredResolver(tenantNamespace: String?): IEventStoreNamespaceResolver = ConfiguredNamespaceResolver(tenantNamespace)import io.cratis.chronicle.namespaces.IEventStoreNamespaceResolver;
// Java has no separate builder step to register a namespace resolver structurally - a resolver// is just an IEventStoreNamespaceResolver implementation, constructed with whatever configuration// it needs and handed to callers directly instead of wired up through a hosting builder.class ConfiguredNamespaceResolver implements IEventStoreNamespaceResolver { private final String configuredNamespace;
public ConfiguredNamespaceResolver(String configuredNamespace) { this.configuredNamespace = configuredNamespace; }
@Override public String resolve() { return configuredNamespace == null ? "Default" : configuredNamespace; }
public static IEventStoreNamespaceResolver createConfiguredResolver(String tenantNamespace) { return new ConfiguredNamespaceResolver(tenantNamespace); }}Elixir does not support this workflow yet.TypeScript does not support this workflow yet.Using type-based DI resolution
Section titled “Using type-based DI resolution”Configure the resolver type through ChronicleAspNetCoreOptions to let the DI container resolve it:
using Cratis.Chronicle.AspNetCore;using Microsoft.AspNetCore.Builder;using Microsoft.Extensions.DependencyInjection;
public static class NamespacesAspNetCoreTypeBasedCustomResolver{ public static void Configure(WebApplicationBuilder builder) => builder.Services.Configure<ChronicleAspNetCoreOptions>(options => { options.EventStore = "my-event-store"; options.EventStoreNamespaceResolverType = typeof(CustomNamespaceResolver); });}import io.cratis.chronicle.EventStoreNamespaceNameimport io.cratis.chronicle.namespaces.IEventStoreNamespaceResolver
// The Spring Boot integration steps aside for a resolver the application declares as its own bean// (@Component, @Service, or an @Bean method) - annotating this class is enough for it to be// discovered and used automatically, with no explicit wiring. This is the closest Kotlin gets to// setting EventStoreNamespaceResolverType and letting dependency injection resolve the type.class TenantComponentNamespaceResolver(private val tenantContext: ITenantContext) : IEventStoreNamespaceResolver { override fun resolve(): String = tenantContext.currentTenantId.ifBlank { EventStoreNamespaceName.default.value }}import io.cratis.chronicle.namespaces.IEventStoreNamespaceResolver;
// The Spring Boot integration steps aside for a resolver the application declares as its own bean// (@Component, @Service, or an @Bean method) - annotating this class is enough for it to be// discovered and used automatically, with no explicit wiring. This is the closest Java gets to// setting EventStoreNamespaceResolverType and letting dependency injection resolve the type.class TenantComponentNamespaceResolver implements IEventStoreNamespaceResolver { private final ITenantContext tenantContext;
public TenantComponentNamespaceResolver(ITenantContext tenantContext) { this.tenantContext = tenantContext; }
@Override public String resolve() { String tenantId = tenantContext.getCurrentTenantId(); return tenantId == null || tenantId.isBlank() ? "Default" : tenantId; }}Elixir does not support this workflow yet.TypeScript does not support this workflow yet.Configuration priority
Section titled “Configuration priority”- Resolver set via
IChronicleBuilder.WithNamespaceResolver()orIChronicleBuilder.NamespaceResolver - Type configuration (
EventStoreNamespaceResolverType) when provided - Default HTTP header resolver
Example: Web app setup
Section titled “Example: Web app setup”using Cratis.Chronicle.Events;using Cratis.Chronicle.EventSequences;using Microsoft.AspNetCore.Builder;
[EventType]public record NamespacesAspNetCoreItemAddedToCart(string ProductId, int Quantity);
public static class NamespacesAspNetCoreWebAppExample{ public static void ConfigureApp(string[] args) { var builder = WebApplication.CreateBuilder(args);
builder.AddCratisChronicle(options => { options.EventStore = "production-store"; options.WithHttpHeaderNamespaceResolver("x-tenant-id"); });
var app = builder.Build(); app.MapPost("/api/cart/{cartId}/items", async (string cartId, IEventLog eventLog) => { var itemAdded = new NamespacesAspNetCoreItemAddedToCart(ProductId: "product-123", Quantity: 1); await eventLog.Append(cartId, itemAdded); return Microsoft.AspNetCore.Http.Results.Ok(); }); app.Run(); }}import io.cratis.chronicle.ChronicleClientimport io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.namespaces.IEventStoreNamespaceResolver
@EventTypedata class NamespacesItemAddedToCart(val productId: String = "", val quantity: Int = 0)
/** * Kotlin has no built-in web application builder - the same shape applies no matter which web * framework sits in front of it: resolve the namespace for the current request, ask the client for * that event store, and append. This handler works whichever way the resolver above actually reads * the request (a Spring Boot filter, Ktor, or a plain servlet). */class Cart(private val client: ChronicleClient, private val namespaceResolver: IEventStoreNamespaceResolver) { suspend fun addItem(cartId: String, productId: String, quantity: Int) { val eventStore = client.getEventStore("production-store", namespaceResolver.resolve()) eventStore.eventLog.append(cartId, NamespacesItemAddedToCart(productId, quantity)) }}import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.java.BlockingChronicleClient;import io.cratis.chronicle.namespaces.IEventStoreNamespaceResolver;
@EventTyperecord NamespacesItemAddedToCart(String productId, int quantity) {}
// Java has no built-in web application builder - the same shape applies no matter which web// framework sits in front of it: resolve the namespace for the current request, ask the client// for that event store, and append. This handler works whichever way the resolver above actually// reads the request (a Spring Boot filter, or a plain servlet).class Cart { private final BlockingChronicleClient client; private final IEventStoreNamespaceResolver namespaceResolver;
public Cart(BlockingChronicleClient client, IEventStoreNamespaceResolver namespaceResolver) { this.client = client; this.namespaceResolver = namespaceResolver; }
public void addItem(String cartId, String productId, int quantity) { client.getEventStore("production-store", namespaceResolver.resolve()) .getEventLog() .append(cartId, new NamespacesItemAddedToCart(productId, quantity)); }}Elixir does not support this workflow yet.TypeScript does not support this workflow yet.For non-web contexts, see DotNET client usage.