What does this method signature tell you?
public Task<Order> GetOrder(Guid customerId, Guid orderId)
Nothing that stops you from calling it like this:
await repository.GetOrder(orderId, customerId);
It compiles, it runs, and it returns nothing (or worse, the wrong thing). Every ID is a Guid, so the compiler can’t help you. This problem is called primitive obsession. We don't take real advantage of our type system but use any of the built-in primitives instead.
The solution
The obvious fix is a small wrapper type per ID. A readonly struct or a record does the job, but you end up writing the same boilerplate for every ID: equality, ToString, JSON serialization, a type converter for route binding, an EF Core value converter,
Luckily someone already did all the hard work for us and created Vogen.

Vogen is a source generator that does all the hard work. Add the package:
dotnet add package Vogen
And declare your ID:
using Vogen;
[ValueObject<Guid>]
public readonly partial struct OrderId { }
[ValueObject<Guid>]
public readonly partial struct CustomerId { }
Now the method becomes:
public Task<Order> GetOrder(CustomerId customerId, OrderId orderId)
Swapping the arguments is now a compile error.
Remark: Vogen forces you to create instances through From(). Using new OrderId() or default is blocked by the analyzer, so you can’t end up with an uninitialized ID by accident.
Generating new IDs
So, the question becomes: how do you create a new ID? One solution is to wrap a Guid yourself:
var orderId = OrderId.From(Guid.NewGuid());
That gets repetitive. Vogen can generate a FromNewGuid() and FromNewVersion7Guid methods for Guid-backed types through the AddFactoryMethodForGuids customization setting.
using Vogen;
[ValueObject<Guid>(customizations: Customizations.AddFactoryMethodForGuids)]
public readonly partial struct OrderId { }
[ValueObject<Guid>(customizations: Customizations.AddFactoryMethodForGuids)]
public readonly partial struct CustomerId { }
If you want something different, add your own factory method to the partial struct:
[ValueObject<Guid>]
public readonly partial struct OrderId
{
public static OrderId New() => From(Guid.CreateVersion7());
}
Now creating an ID reads like what it is:
var order = new Order(OrderId.New(), customerId);
Because New() goes through From(), any validation you add to the value object still applies.
Remark: Guid.CreateVersion7() is time-ordered, but that doesn’t make it sequential from SQL Server’s point of view. If the ID is your clustered key, you can still end up with a fragmented index. I wrote about this here.
Using it with EF Core and System.Text.Json
By default Vogen generates a JSON converter and a TypeConverter. For EF Core, you have to ask for the value converter:
[ValueObject<Guid>(conversions: Conversions.Default | Conversions.EfCoreValueConverter)]
public readonly partial struct OrderId
{
public static OrderId New() => From(Guid.CreateVersion7());
}
Then register it once in your DbContext, so you don’t have to configure every property:
protected override void ConfigureConventions(ModelConfigurationBuilder builder)
{
builder.Properties<OrderId>().HaveConversion<OrderId.EfCoreValueConverter>();
}
That’s it! Your entities use OrderId and the database still stores a plain uniqueidentifier.

