Table of Contents

Class RequiredGuid<TSelf>

Namespace
Trellis
Assembly
Trellis.Primitives.dll

Base class for creating strongly-typed GUID value objects that cannot have the default (empty) GUID value. Provides a foundation for entity identifiers and other domain concepts represented by GUIDs.

public abstract class RequiredGuid<TSelf> : ScalarValueObject<TSelf, Guid>, IComparable<ValueObject>, IEquatable<ValueObject>, IConvertible where TSelf : RequiredGuid<TSelf>, IScalarValue<TSelf, Guid>

Type Parameters

TSelf
Inheritance
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 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);
// Returns: Success(CustomerId) if guid != Guid.Empty
// Returns: Failure(ValidationError) if 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(ValidationError) if invalid format or empty GUID

// 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) =>
        email.ToResult()
            .Map(e => new Customer(CustomerId.NewUnique(), 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 valid, non-empty CustomerId
        // 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();
    }
}

// Invalid GUID is rejected automatically:
// GET /api/customers/00000000-0000-0000-0000-000000000000
// Response: 400 Bad Request
// {
//   "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
//   "title": "One or more validation errors occurred.",
//   "status": 400,
//   "errors": {
//     "id": ["Customer Id cannot be 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))
        .ToHttpResult());

// JSON request/response examples:
// Request: GET /customers/550e8400-e29b-41d4-a716-446655440000
// Response: { "customerId": "550e8400-e29b-41d4-a716-446655440000", "name": "John" }
// 
// Invalid GUID: GET /customers/00000000-0000-0000-0000-000000000000
// Response: 400 Bad Request with ValidationError

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) =>
        this.ToResult()
            .Ensure(_ => quantity > 0, Error.Validation("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 with automatic validation that prevents empty/default GUIDs. When used with the partial keyword, the PrimitiveValueObjectGenerator source generator automatically creates:

  • IScalarValue<TSelf, Guid> implementation for ASP.NET Core automatic validation
  • NewUniqueV4() - Factory method for generating new unique Version 4 (random) identifiers
  • NewUniqueV7() - Factory method for generating new unique Version 7 (time-ordered) identifiers
  • TryCreate(Guid) - Factory method for non-nullable GUIDs (required by IScalarValue)
  • TryCreate(Guid?, string?) - Factory method with empty GUID validation and custom field name
  • TryCreate(string?, string?) - Factory method for parsing strings with validation
  • IParsable<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 empty/default GUIDs at creation time
  • 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

value Guid

The GUID value. Must not be Empty.

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:

  • NewUnique() - Generate a new unique GUID
  • TryCreate(Guid?, string?) - Create from GUID with validation
  • TryCreate(string?, string?) - Create from string with validation

See Also