Tutorial: generate your first values
You are building a small shop. Customers need ids, orders need ids that carry a time, invitations need short codes you can read out loud,
and integrations need API keys. Today the code is full of Guid.NewGuid(). In this tutorial you replace each one with a concept that
generates its own value, and you see a result after every step.
You need the .NET 8 SDK or later. Create a folder for the solution with a console project inside it, and add the package.
Every command in this tutorial runs from that shop folder:
mkdir shopcd shopdotnet new console -n Shopdotnet add Shop package Cratis.FundamentalsYou will add the .cs files to the Shop folder and run the program with dotnet run --project Shop. The console template turns on implicit
usings, so Program.cs needs no using for System, System.Linq, System.Collections.Generic or System.Threading.Tasks.
The printed values below are samples. Yours differ because the values are random, which is the point.
Step 1: a customer id that generates itself
Section titled “Step 1: a customer id that generates itself”Add CustomerId.cs:
using Cratis.Concepts;
public record CustomerId(Guid Value) : ConceptAs<Guid>(Value), IGeneratable<CustomerId>{ public static CustomerId New() => new(GenerateValue.Uuid());}Replace Shop/Program.cs with Console.WriteLine(CustomerId.New()); and run dotnet run --project Shop:
054092b2-48e9-4a54-9bc2-d2e8ccfa04e1GenerateValue.Uuid() returns an RFC 9562 version 4 UUID from a cryptographically secure generator. IGeneratable<CustomerId> is the
promise that CustomerId.New() exists, checked by the compiler. A ConceptAs<Guid> prints its inner value, which is why you see a plain UUID.
Step 2: a public id that sorts by age
Section titled “Step 2: a public id that sorts by age”Customers will see their id in URLs and invoices, so make it self-describing and sortable. Replace CustomerId.cs:
using Cratis.Concepts;
public record CustomerId(string Value) : ConceptAs<string>(Value), IGeneratable<CustomerId>{ public static CustomerId New() => new($"cus_{GenerateValue.Ulid().ToLowerInvariant()}");}Replace Program.cs to create three ids a few milliseconds apart and print them in sorted order:
var ids = new List<CustomerId>();for (var i = 0; i < 3; i++){ ids.Add(CustomerId.New()); await Task.Delay(5);}
foreach (var id in ids.OrderBy(id => id.Value, StringComparer.Ordinal)){ Console.WriteLine(id);}cus_01m4tq3wcw1fn5912rd8rsmgcdcus_01m4tq3wd2ap7q35madm22wktecus_01m4tq3wekqn178wa6k9xeck0xA ULID starts with 48 bits of Unix time in milliseconds, so ids sort by creation time as plain strings. The cus_ prefix and the lower case are
ordinary string operations; the result is a custom-prefixed ULID, not a TypeID. Two ids created in the same millisecond have no guaranteed order,
and every id reveals when it was created.
Step 3: a short invite code
Section titled “Step 3: a short invite code”Invitations are typed by people, so remove the characters that look alike. Add InviteCode.cs:
using Cratis.Concepts;
public record InviteCode(string Value) : ConceptAs<string>(Value), IGeneratable<InviteCode>{ const string Alphabet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
public static InviteCode New() => new(GenerateValue.NanoId(10, Alphabet));}The alphabet has 32 characters, so each character carries 5 bits and a 10-character code carries 50. Print three codes, regenerating on a clash:
var issued = new HashSet<string>();for (var i = 0; i < 3; i++){ InviteCode code; do { code = InviteCode.New(); } while (!issued.Add(code.Value));
Console.WriteLine(code);}SA65PT7D2G5GPGMEGTLS6KSESF9B9GStep 4: an API key
Section titled “Step 4: an API key”An API key is a secret, so use enough bytes. Add ApiKey.cs:
using Cratis.Concepts;
public record ApiKey(string Value) : ConceptAs<string>(Value), IGeneratable<ApiKey>{ public static ApiKey New() => new(GenerateValue.Bytes(32));}Bytes(32) draws 256 random bits and encodes them as unpadded URL-safe base64, 43 characters:
Console.WriteLine(ApiKey.New());gziGp2TEJPHWl6ztCXqiKMj7NVcyO3JhGD7t59Qe204Printing a key is fine in a tutorial and a leak in production: the concept prints its value. The attribute that keeps a value out of
the command causation chain comes from the Cratis.Arc.Chronicle package, not from Fundamentals, and the tutorial project does not use it.
In an Arc application with Chronicle, you mark the concept like this:
using Cratis.Arc.Chronicle.Commands;using Cratis.Concepts;
[NotAudited]public record ApiKey(string Value) : ConceptAs<string>(Value), IGeneratable<ApiKey>{ public static ApiKey New() => new(GenerateValue.Bytes(32));}[NotAudited] covers the command causation chain only. It does not protect an event that carries the key, or a log line.
Secrets and tokens explains what it covers and how to handle the key.
Step 5: an order id that follows the operation
Section titled “Step 5: an order id that follows the operation”An order id should carry the time the order was received, even if the code takes a few milliseconds to reach the point where it
creates the id. Add OrderId.cs:
using Cratis.Concepts;
public record OrderId(string Value) : ConceptAs<string>(Value), IGeneratableInContext<OrderId>{ public static OrderId New(GenerationContext context) => new(GenerateValue.TypeId("order", context));}This concept implements IGeneratableInContext<OrderId>, so New takes a GenerationContext holding the captured time and the tenant.
Replace Shop/Program.cs to create two ids from one context and one from a context an hour later:
using Cratis.Concepts;
var received = new DateTimeOffset(2024, 6, 15, 12, 34, 56, 789, TimeSpan.Zero);var context = new GenerationContext(received, GenerationContext.DefaultTenant);
Console.WriteLine(OrderId.New(context));Console.WriteLine(OrderId.New(context));Console.WriteLine(OrderId.New(context with { Now = received.AddHours(1) }));order_01j0dy9j4nej4saxp79mmcqts7order_01j0dy9j4ne2xvhv8rhe3n72zeorder_01j0e1qdrnfvm9xabkp66m09gwThe first ten characters after order_ encode the time. The first two ids share them because they share one Now; the third differs.
The remaining characters are random, so the two ids are still different. The built-in strategies read Now only, never Tenant; a concept can
use the tenant itself, as Context-dependent values shows.
Step 6: create values from generic code
Section titled “Step 6: create values from generic code”Frameworks create your values without knowing their types. You can write the same shape in a few lines:
using Cratis.Concepts;
static T Create<T>() where T : IGeneratable<T> => T.New();
static T CreateInContext<T>(GenerationContext context) where T : IGeneratableInContext<T> => T.New(context);
var context = new GenerationContext(DateTimeOffset.UtcNow, GenerationContext.DefaultTenant);
Console.WriteLine(Create<CustomerId>());Console.WriteLine(Create<InviteCode>());Console.WriteLine(CreateInContext<OrderId>(context));T.New() is a static abstract interface member, so the call is resolved at compile time with no reflection. You get back a CustomerId,
an InviteCode and an OrderId, not untyped strings. Create<OrderId>() does not compile, because OrderId implements only the context interface.
Step 7: pin the clock in a spec
Section titled “Step 7: pin the clock in a spec”Random values make poor expectations, but the time part of OrderId is yours to fix. From the shop folder, create a spec project
next to Shop and reference it:
dotnet new xunit -n Shop.Specsdotnet add Shop.Specs package Cratis.Specifications.XUnitdotnet add Shop.Specs reference Shop/Shop.csprojrm Shop.Specs/UnitTest1.csThe rm deletes the template’s sample test, which the spec project does not need. Add Shop.Specs/when_creating_two_orders_at_a_known_time.cs:
using Cratis.Concepts;using Cratis.Specifications;using Xunit;
namespace Shop.Specs;
public class when_creating_two_orders_at_a_known_time : Specification{ OrderId _first = null!; OrderId _second = null!;
void Because() { var received = new DateTimeOffset(2024, 6, 15, 12, 34, 56, 789, TimeSpan.Zero); var context = new GenerationContext(received, GenerationContext.DefaultTenant); _first = OrderId.New(context); _second = OrderId.New(context); }
[Fact] void should_use_the_order_prefix() => _first.Value.StartsWith("order_").ShouldBeTrue(); [Fact] void should_encode_the_received_time() => _first.Value[6..16].ShouldEqual("01j0dy9j4n"); [Fact] void should_share_the_time_between_orders() => _second.Value[6..16].ShouldEqual(_first.Value[6..16]); [Fact] void should_still_create_distinct_ids() => (_first == _second).ShouldBeFalse();}Run dotnet test Shop.Specs. All four pass on every run: the spec asserts the parts you control and does not assert the random part.
What you built
Section titled “What you built”| Concept | Strategy | Why |
|---|---|---|
CustomerId | cus_ + lowercase Ulid() | Readable, sortable public id |
InviteCode | NanoId(10, alphabet) | Short, typeable, needs a uniqueness check |
ApiKey | Bytes(32) | 256 random bits, a secret |
OrderId | TypeId("order", context) | Time follows the operation |
Next steps
Section titled “Next steps”- Choose strategies for your own ids with Choosing a strategy.
- Learn the rules for the context in Context-dependent values.
- Handle your API keys safely with Secrets and tokens.