Table of Contents

Class RequiredString<TSelf>

Namespace
Trellis
Assembly
Trellis.Primitives.dll

Base class for creating strongly-typed string value objects that cannot be null or empty. Provides a foundation for domain primitives like names, descriptions, codes, and other textual concepts.

public abstract class RequiredString<TSelf> : ScalarValueObject<TSelf, string>, IComparable<ValueObject>, IComparable, IEquatable<ValueObject>, IConvertible, IFormattable where TSelf : RequiredString<TSelf>, IScalarValue<TSelf, string>

Type Parameters

TSelf
Inheritance
RequiredString<TSelf>
Implements
Inherited Members
Extension Methods

Examples

Creating a strongly-typed name value object:

// Define the value object (partial keyword enables source generation)
public partial class FirstName : RequiredString<FirstName>
{
}

// The source generator automatically creates:
// - IScalarValue<FirstName, string> interface implementation
// - public static Result<FirstName> TryCreate(string value)
// - public static Result<FirstName> TryCreate(string? value, string? fieldName = null)
// - public static FirstName Parse(string s, IFormatProvider? provider)
// - public static bool TryParse(string? s, IFormatProvider? provider, out FirstName result)
// - public static explicit operator FirstName(string value)
// - private FirstName(string value) : base(value) { }

// Usage examples:

// Create with validation
var result1 = FirstName.TryCreate("John");
// Returns: Success(FirstName("John"))

var result2 = FirstName.TryCreate("");
// Returns: Failure(ValidationError("First Name cannot be empty."))

var result3 = FirstName.TryCreate(null);
// Returns: Failure(ValidationError("First Name cannot be empty."))

var result4 = FirstName.TryCreate("  John  ");
// Returns: Success(FirstName("John")) - automatically trimmed

// With custom field name for validation errors
var result5 = FirstName.TryCreate(input, "user.firstName");
// Error field will be "user.firstName" instead of default "firstName"

// Using in entity creation
public class Person : Entity<PersonId>
{
    public FirstName FirstName { get; }
    public LastName LastName { get; }

    public static Result<Person> Create(string firstName, string lastName) =>
        FirstName.TryCreate(firstName)
            .Combine(LastName.TryCreate(lastName))
            .Map((first, last) => new Person(PersonId.NewUnique(), first, last));

    private Person(PersonId id, FirstName firstName, LastName lastName) 
        : base(id)
    {
        FirstName = firstName;
        LastName = lastName;
    }
}

ASP.NET Core automatic validation (no manual Result.Combine needed):

// 1. Register automatic validation in Program.cs
builder.Services
    .AddControllers()
    .AddScalarValueObjectValidation(); // Enables automatic validation!

// 2. Define your DTO with value objects
public record RegisterUserDto
{
    public FirstName FirstName { get; init; } = null!;
    public LastName LastName { get; init; } = null!;
    public EmailAddress Email { get; init; } = null!;
}

// 3. Use in controllers - automatic validation!
[ApiController]
[Route("api/users")]
public class UsersController : ControllerBase
{
    [HttpPost]
    public IActionResult Register(RegisterUserDto dto)
    {
        // If we reach here, dto is FULLY validated!
        // No Result.Combine() needed - validation happens automatically during model binding
        var user = new User(dto.FirstName, dto.LastName, dto.Email);
        return Ok(user);
    }
}

// Invalid request automatically returns 400 Bad Request:
// POST /api/users with { "firstName": "", "lastName": "Doe", "email": "test@example.com" }
// 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": {
//     "firstName": ["First Name cannot be empty."]
//   }
// }

Using in API validation (manual approach):

// Request DTO
public record CreateUserRequest(string FirstName, string LastName, string Email);

// API endpoint with automatic validation
app.MapPost("/users", (CreateUserRequest request) =>
    FirstName.TryCreate(request.FirstName, nameof(request.FirstName))
        .Combine(LastName.TryCreate(request.LastName, nameof(request.LastName)))
        .Combine(EmailAddress.TryCreate(request.Email, nameof(request.Email)))
        .Bind((first, last, email) => User.Create(first, last, email))
        .ToHttpResult());

// POST /users with empty FirstName:
// 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": {
//     "firstName": ["First Name cannot be empty."]
//   }
// }

Multiple string-based value objects:

public partial class FirstName : RequiredString<FirstName> { }
public partial class LastName : RequiredString<LastName> { }
public partial class CompanyName : RequiredString<CompanyName> { }
public partial class ProductName : RequiredString<ProductName> { }
public partial class Description : RequiredString<Description> { }

public class Product : Entity<ProductId>
{
    public ProductName Name { get; private set; }
    public Description Description { get; private set; }

    public Result<Product> UpdateName(ProductName newName) =>
        newName.ToResult()
            .Tap(name => Name = name)
            .Map(_ => this);

    // Compiler prevents mixing types:
    // UpdateName(description); // Won't compile!
    // UpdateName(firstName);   // Won't compile!
}

Advanced: Adding custom validation to derived types:

// Use [StringLength] for length, add custom validation for format rules
[StringLength(20)]
public partial class ProductSKU : RequiredString<ProductSKU>
{
    // Additional format validation via custom factory method
    public static Result<ProductSKU> TryCreateWithValidation(string? value) =>
        TryCreate(value) // Generated: validates non-empty + length <= 20
            .Ensure(sku => sku.Value.All(c => char.IsLetterOrDigit(c) || c == '-'),
                   Error.Validation("SKU can only contain letters, digits, and hyphens", "sku"));
}

// Usage
var result = ProductSKU.TryCreateWithValidation("PROD-12345");
// Success

var tooLong = ProductSKU.TryCreateWithValidation(new string('A', 21));
// Failure: "Product SKU must be 20 characters or fewer."

var invalid = ProductSKU.TryCreateWithValidation("PROD@12345");
// Failure: "SKU can only contain letters, digits, and hyphens"

String length constraints with StringLengthAttribute:

// Maximum length only — rejects strings longer than 50 characters
[StringLength(50)]
public partial class FirstName : RequiredString<FirstName> { }

var ok = FirstName.TryCreate("John");      // Success
var tooLong = FirstName.TryCreate(new string('x', 51)); // Failure: "First Name must be 50 characters or fewer."

// Both minimum and maximum length
[StringLength(500, MinimumLength = 10)]
public partial class Description : RequiredString<Description> { }

var tooShort = Description.TryCreate("Hi");        // Failure: "Description must be at least 10 characters."
var tooLong2 = Description.TryCreate(new string('x', 501)); // Failure: "Description must be 500 characters or fewer."

Remarks

This class extends ScalarValueObject<TSelf, T> to provide a specialized base for string-based value objects with automatic validation that prevents null or empty strings. When used with the partial keyword, the PrimitiveValueObjectGenerator source generator automatically creates:

  • IScalarValue<TSelf, string> implementation for ASP.NET Core automatic validation
  • TryCreate(string) - Factory method for non-nullable strings (required by IScalarValue)
  • TryCreate(string?, string?) - Factory method with null/empty/whitespace validation and custom field name
  • IParsable<T> implementation (Parse, TryParse)
  • JSON serialization support via ParsableJsonConverter<T>
  • Explicit cast operator from string
  • OpenTelemetry activity tracing

Common use cases:

  • Person names (FirstName, LastName, FullName)
  • Product attributes (ProductName, Description, SKU)
  • Location data (City, State, Country, PostalCode)
  • Business identifiers (CompanyName, TaxId, AccountNumber)
  • Any domain concept represented by required text

String length constraints: Apply the StringLengthAttribute to enforce minimum and/or maximum lengths at creation time:

[StringLength(50)]
public partial class FirstName : RequiredString<FirstName> { }

[StringLength(500, MinimumLength = 10)]
public partial class Description : RequiredString<Description> { }

Benefits over plain strings:

  • Type safety: Cannot mix FirstName with LastName
  • Validation: Prevents null/empty strings at creation time
  • Length constraints: Optional min/max length via StringLengthAttribute
  • Domain clarity: Self-documenting code that expresses intent
  • Consistency: Centralized trimming and normalization
  • Testability: Easy to test validation rules in isolation

Constructors

RequiredString(string)

Initializes a new instance of the RequiredString<TSelf> class with the specified string value.

protected RequiredString(string value)

Parameters

value string

The string value. Must not be null or 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 and trimming.

Direct instantiation should be avoided. Instead, use the generated factory method:

  • TryCreate(string?, string?) - Create from string with validation and trimming

The generated TryCreate method automatically:

  • Returns validation error for null values
  • Returns validation error for empty strings
  • Returns validation error for whitespace-only strings
  • Trims leading and trailing whitespace from valid strings

Properties

Length

Gets the length of the string value. Enables natural LINQ queries like c.Name.Length > 5 without accessing .Value.

public int Length { get; }

Property Value

int

Methods

Contains(string)

Returns whether the string value contains the specified substring. Enables natural LINQ queries like c.Name.Contains("li") without accessing .Value.

public bool Contains(string value)

Parameters

value string

Returns

bool

EndsWith(string)

Returns whether the string value ends with the specified suffix. Enables natural LINQ queries like c.Name.EndsWith("ce") without accessing .Value.

public bool EndsWith(string value)

Parameters

value string

Returns

bool

StartsWith(string)

Returns whether the string value starts with the specified prefix. Enables natural LINQ queries like c.Name.StartsWith("Al") without accessing .Value.

public bool StartsWith(string value)

Parameters

value string

Returns

bool

See Also