NForza.Wolverine.ValueTypes
1.0.0
dotnet add package NForza.Wolverine.ValueTypes --version 1.0.0
NuGet\Install-Package NForza.Wolverine.ValueTypes -Version 1.0.0
<PackageReference Include="NForza.Wolverine.ValueTypes" Version="1.0.0" />
<PackageVersion Include="NForza.Wolverine.ValueTypes" Version="1.0.0" />
<PackageReference Include="NForza.Wolverine.ValueTypes" />
paket add NForza.Wolverine.ValueTypes --version 1.0.0
#r "nuget: NForza.Wolverine.ValueTypes, 1.0.0"
#:package NForza.Wolverine.ValueTypes@1.0.0
#addin nuget:?package=NForza.Wolverine.ValueTypes&version=1.0.0
#tool nuget:?package=NForza.Wolverine.ValueTypes&version=1.0.0
Wolverine.ValueTypes
A Roslyn source generator that produces strongly-typed value type wrappers for use with Wolverine and Marten.
Get rid of primitive obsession. Define a one-line value type and the generator takes care of the rest.
Quick Start
1. Define your value types
[GuidValue]
public partial record struct CustomerId;
[StringValue(2, 100)]
public partial record struct CustomerName;
[IntValue(1, 10000)]
public partial record struct OrderAmount;
[DoubleValue(0.0, 5.0)]
public partial record struct Rating;
That's it. The generator produces a full record struct for each, including:
- A single
Valueproperty with the underlying type - Implicit conversion to the underlying type
- Explicit conversion from the underlying type
TryParsefor Wolverine HTTP route bindingJsonConverterfor System.Text.Json serializationIsValid()with optional min/max/regex validationToString(),IComparable,IEquatableEmptyfield (for Guid and String types)
2. Use them in your domain
public class Customer
{
public CustomerId Id { get; set; }
public CustomerName Name { get; set; }
public int Version { get; set; }
public void Apply(CustomerCreated e)
{
Id = e.Id;
Name = e.Name;
}
}
public record CustomerCreated(CustomerId Id, CustomerName Name);
3. Use them in Wolverine HTTP endpoints
Value types work directly as route parameters, request/response properties, and Marten aggregate IDs:
public record CreateCustomerRequest(CustomerName Name);
public record CustomerResponse(CustomerId Id, CustomerName Name);
public static class CustomerEndpoints
{
[WolverinePost("/api/customers")]
public static (CustomerResponse, IStartStream) Post(CreateCustomerRequest request)
{
var customerId = new CustomerId();
var @event = new CustomerCreated(customerId, request.Name);
var startStream = MartenOps.StartStream<Customer>(customerId, @event);
return (new CustomerResponse(customerId, request.Name), startStream);
}
[WolverineGet("/api/customers/{customerId}")]
public static async Task<CustomerResponse?> Get(CustomerId customerId, IQuerySession session)
{
var customer = await session.Events.AggregateStreamAsync<Customer>(customerId);
if (customer is null) return null;
return new CustomerResponse(customer.Id, customer.Name);
}
}
4. Register the Wolverine extension
The generator produces a WolverineValueTypeExtension that registers all JSON converters. Include it in your Wolverine configuration:
builder.Host.UseWolverine(opts =>
{
opts.Policies.AutoApplyTransactions();
opts.Include<NForza.Wolverine.ValueTypes.WolverineValueTypeExtension>();
});
Available Attributes
| Attribute | Underlying Type | Parameters |
|---|---|---|
[GuidValue] |
Guid |
None |
[StringValue] |
string |
minimumLength, maximumLength, validationRegex (all optional) |
[IntValue] |
int |
minimum, maximum (optional) |
[DoubleValue] |
double |
minimum, maximum (optional) |
What Gets Generated
For each value type, the generator produces:
- {Name}.g.cs - The record struct with value semantics, conversions, parsing, and validation.
- {Name}JsonConverter.g.cs - A
JsonConverter<T>for System.Text.Json serialization.
Once per project, it also generates:
- WolverineValueTypeExtension.g.cs - An
IWolverineExtensionthat registers all generated JSON converters with Wolverine's serializer.
Marten Compatibility
Guid value types work as Marten aggregate IDs. When using them with Marten event sourcing, use FetchForWriting<T>() instead of the [Aggregate] attribute to avoid code generation conflicts:
[WolverinePost("/api/orders/{orderId}/rate")]
public static async Task<OrderResponse?> Rate(
OrderId orderId,
RateOrderRequest request,
IDocumentSession session)
{
var stream = await session.Events.FetchForWriting<Order>(orderId);
if (stream.Aggregate is null) return null;
stream.AppendOne(new OrderRated(request.Rating));
await session.SaveChangesAsync();
return new OrderResponse(
stream.Aggregate.Id,
stream.Aggregate.CustomerId,
stream.Aggregate.Amount,
request.Rating);
}
Running the Example
The samples/ folder contains a complete Wolverine WebAPI with Marten event sourcing that demonstrates all value type features.
Run the API
docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=postgres postgres:16-alpine
dotnet run --project samples/NForza.Wolverine.ValueTypes.Sample.WebApi
The API will be available at http://localhost:5000.
Endpoints
| Method | URL | Description |
|---|---|---|
POST |
/api/customers |
Create a customer. Body: { "name": "Alice" } |
GET |
/api/customers/{customerId} |
Get a customer by ID. |
POST |
/api/orders |
Create an order. Body: { "customerId": "...", "amount": 500 } |
GET |
/api/orders/{orderId} |
Get an order by ID. |
POST |
/api/orders/{orderId}/rate |
Rate an order. Body: { "rating": 4.5 } |
Try it out
Bash:
# Create a customer and capture the ID
CUSTOMER=$(curl -s -X POST http://localhost:5000/api/customers \
-H "Content-Type: application/json" \
-d '{"name": "Alice"}')
CUSTOMER_ID=$(echo $CUSTOMER | jq -r '.id')
echo $CUSTOMER
# Get the customer
curl -s http://localhost:5000/api/customers/$CUSTOMER_ID
# Create an order and capture the ID
ORDER=$(curl -s -X POST http://localhost:5000/api/orders \
-H "Content-Type: application/json" \
-d "{\"customerId\": \"$CUSTOMER_ID\", \"amount\": 500}")
ORDER_ID=$(echo $ORDER | jq -r '.id')
echo $ORDER
# Rate the order
curl -s -X POST http://localhost:5000/api/orders/$ORDER_ID/rate \
-H "Content-Type: application/json" \
-d '{"rating": 4.5}'
PowerShell:
# Create a customer and capture the ID
$customer = Invoke-RestMethod -Method Post -Uri http://localhost:5000/api/customers `
-ContentType "application/json" -Body '{"name": "Alice"}'
$customerId = $customer.id
$customer
# Get the customer
Invoke-RestMethod http://localhost:5000/api/customers/$customerId
# Create an order and capture the ID
$order = Invoke-RestMethod -Method Post -Uri http://localhost:5000/api/orders `
-ContentType "application/json" -Body "{`"customerId`": `"$customerId`", `"amount`": 500}"
$orderId = $order.id
$order
# Rate the order
Invoke-RestMethod -Method Post -Uri http://localhost:5000/api/orders/$orderId/rate `
-ContentType "application/json" -Body '{"rating": 4.5}'
Run the tests
The integration tests use Testcontainers to spin up PostgreSQL automatically (requires Docker):
dotnet test
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.