Class RequiredGuid<TSelf>
- Namespace
- Trellis
- Assembly
- Trellis.Core.dll
Base class for creating strongly-typed GUID value objects. Rejects only null by
default; Empty rejection is opt-in via the
NotDefaultAttribute attribute. Recommended for any GUID type used as an
Aggregate<TId> / Entity<TId> ID or as an EF-mapped property to
preserve the database invariant guarantee enforced by TrellisScalarConverter on
rehydration.
public abstract class RequiredGuid<TSelf> : ScalarValueObject<TSelf, Guid>, IComparable<ValueObject>, IComparable, IEquatable<ValueObject>, IConvertible, IFormattable where TSelf : RequiredGuid<TSelf>, IScalarValue<TSelf, Guid>
Type Parameters
TSelf
- Inheritance
-
ScalarValueObject<TSelf, Guid>RequiredGuid<TSelf>
- Implements
- Inherited Members
- Extension Methods
Examples
Creating a strongly-typed entity identifier:
// Define the value object (partial keyword enables source generation)
public partial class CustomerId : RequiredGuid<CustomerId>
{
}
// The source generator automatically creates:
// - IScalarValue<CustomerId, Guid> interface implementation
// - public static CustomerId NewUniqueV4() => new(Guid.NewGuid()); // Version 4 (random)
// - public static CustomerId NewUniqueV7() => new(Guid.CreateVersion7()); // Version 7 (time-ordered)
// - public static CustomerId NewUniqueV7(TimeProvider timeProvider) => new(Guid.CreateVersion7(timeProvider.GetUtcNow()));
// - public static Result<CustomerId> TryCreate(Guid value)
// - public static Result<CustomerId> TryCreate(Guid? value, string? fieldName = null)
// - public static Result<CustomerId> TryCreate(string? value, string? fieldName = null)
// - public static CustomerId Parse(string s, IFormatProvider? provider)
// - public static bool TryParse(string? s, IFormatProvider? provider, out CustomerId result)
// - public static explicit operator CustomerId(Guid value)
// - private CustomerId(Guid value) : base(value) { }
// Usage examples:
// Generate a new unique ID (Version 4 - random)
var customerId = CustomerId.NewUniqueV4();
// Generate a new unique ID (Version 7 - time-ordered, better for database indexes)
var customerId = CustomerId.NewUniqueV7();
// Create from existing GUID with validation
var result1 = CustomerId.TryCreate(existingGuid);
// Default (no [NotDefault]): Success regardless of value, including Guid.Empty.
// With [NotDefault] on CustomerId: Success when guid != Guid.Empty;
// Failure("Customer Id cannot be Guid.Empty.") when guid == Guid.Empty.
// Create from string with validation
var result2 = CustomerId.TryCreate("550e8400-e29b-41d4-a716-446655440000");
// Returns: Success(CustomerId) if valid GUID format
// Returns: Failure(Error.InvalidInput) if invalid format
// (or, when [NotDefault] is applied, also Failure if the parsed GUID is Guid.Empty)
// With custom field name for validation errors
var result3 = CustomerId.TryCreate(input, "customer.id");
// Error field will be "customer.id" instead of default "customerId"
// In entity constructors
public class Customer : Entity<CustomerId>
{
public EmailAddress Email { get; }
private Customer(CustomerId id, EmailAddress email) : base(id)
{
Email = email;
}
public static Result<Customer> Create(EmailAddress email) =>
Result.Ok(email)
.Map(e => new Customer(CustomerId.NewUniqueV7(), e));
}
ASP.NET Core automatic validation with route parameters:
// 1. Register automatic validation in Program.cs
builder.Services
.AddControllers()
.AddScalarValueObjectValidation(); // Enables automatic validation!
// 2. Use value objects directly in controller actions
[ApiController]
[Route("api/customers")]
public class CustomersController : ControllerBase
{
[HttpGet("{id}")]
public async Task<ActionResult<Customer>> Get(CustomerId id) // Automatically validated!
{
// If we reach here, 'id' is a non-null CustomerId (and, with [NotDefault], non-Guid.Empty)
// Model binding validated it automatically
var customer = await _repository.GetByIdAsync(id);
return Ok(customer);
}
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(CustomerId id) // Also works here!
{
await _repository.DeleteAsync(id);
return NoContent();
}
}
// All-zero GUID (with [NotDefault] on CustomerId) is rejected as the sentinel value:
// GET /api/customers/00000000-0000-0000-0000-000000000000
// Response: 422 Unprocessable Content
// {
// "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
// "title": "One or more validation errors occurred.",
// "status": 422,
// "errors": {
// "id": ["Customer Id cannot be Guid.Empty."]
// }
// }
Using in API endpoints with automatic JSON serialization (manual approach):
// Request DTO
public record GetCustomerRequest(CustomerId CustomerId);
// API endpoint
app.MapGet("/customers/{id}", (string id) =>
CustomerId.TryCreate(id)
.Bind(_customerRepository.GetAsync)
.Map(customer => new CustomerDto(customer))
.ToHttpResponse());
// JSON request/response examples:
// Request: GET /customers/550e8400-e29b-41d4-a716-446655440000
// Response: { "customerId": "550e8400-e29b-41d4-a716-446655440000", "name": "John" }
//
// All-zero GUID (rejected as the sentinel only when [NotDefault] is applied): GET /customers/00000000-0000-0000-0000-000000000000
// Response: 422 Unprocessable Content with "Customer Id cannot be Guid.Empty." detail
Multiple strongly-typed IDs in the same domain:
public partial class CustomerId : RequiredGuid<CustomerId> { }
public partial class OrderId : RequiredGuid<OrderId> { }
public partial class ProductId : RequiredGuid<ProductId> { }
public class Order : Entity<OrderId>
{
public CustomerId CustomerId { get; }
private readonly List<OrderLine> _lines = [];
public Result<Order> AddLine(ProductId productId, int quantity) =>
Result.Ok(this)
.Ensure(_ => quantity > 0, new Error.InvalidInput(EquatableArray<FieldViolation>.Empty) { Detail = "Quantity must be positive" })
.Tap(_ => _lines.Add(new OrderLine(productId, quantity)));
// Compiler prevents mixing IDs:
// AddLine(customerId, 5); // Won't compile - type safety!
}
Remarks
This class extends ScalarValueObject<TSelf, T> to provide a specialized base for GUID-based value objects.
When used with the partial keyword,
the PrimitiveValueObjectGenerator source generator automatically creates:
IScalarValue<TSelf, Guid>implementation for ASP.NET Core automatic validationNewUniqueV4()- Factory method for generating new unique Version 4 (random) identifiersNewUniqueV7()/NewUniqueV7(TimeProvider)- Factory methods for generating new unique Version 7 (time-ordered) identifiersTryCreate(Guid)- Factory method for non-nullable GUIDs (required by IScalarValue)TryCreate(Guid?, string?)- Factory method with null-only rejection by default (add NotDefaultAttribute for Empty rejection) and custom field nameTryCreate(string?, string?)- Factory method for parsing strings with validationIParsable<T>implementation (Parse,TryParse)- JSON serialization support via
ParsableJsonConverter<T> - Explicit cast operator from Guid
- OpenTelemetry activity tracing
Common use cases:
- Entity identifiers (CustomerId, OrderId, ProductId)
- Correlation IDs for distributed tracing
- Session or transaction identifiers
- Any domain concept requiring a globally unique, non-empty identifier
Benefits over plain GUIDs:
- Type safety: Cannot accidentally use CustomerId where OrderId is expected
- Validation: Prevents null at creation time; opt into Empty rejection with
[NotDefault] - Domain clarity: Makes code more self-documenting and expressive
- Serialization: Consistent JSON and database representation
- Factory methods: Clean API for creating new unique identifiers
Constructors
RequiredGuid(Guid)
Initializes a new instance of the RequiredGuid<TSelf> class with the specified GUID value.
protected RequiredGuid(Guid value)
Parameters
valueGuidThe GUID value.
Remarks
This constructor is protected and should be called by derived classes.
When using the source generator (with partial keyword), a private constructor
is automatically generated that includes validation.
Direct instantiation should be avoided. Instead, use the generated factory methods:
NewUniqueV4()- Generate a new random version 4 GUIDNewUniqueV7()/NewUniqueV7(TimeProvider)- Generate a new time-ordered version 7 GUIDTryCreate(Guid?, string?)- Create from GUID with validationTryCreate(string?, string?)- Create from string with validation