This guide covers all the built-in validation attributes and how to control validation logic in the Semantics library.
using ktsu.Semantics;
// Email validation using regex
[RegexMatch(@"^[^@\s]+@[^@\s]+\.[^@\s]+$")]
public sealed record EmailAddress : SemanticString<EmailAddress> { }
// Phone number validation
[RegexMatch(@"^\(\d{3}\) \d{3}-\d{4}$")]
public sealed record USPhoneNumber : SemanticString<USPhoneNumber> { }
// Social Security Number
[RegexMatch(@"^\d{3}-\d{2}-\d{4}$")]
public sealed record SSN : SemanticString<SSN> { }
// Usage examples
var email = "user@example.com".As<EmailAddress>();
var phone = "(555) 123-4567".As<USPhoneNumber>();
var ssn = "123-45-6789".As<SSN>();// URL validation with multiple prefixes
[StartsWith("http://", StringComparison.OrdinalIgnoreCase)]
[StartsWith("https://", StringComparison.OrdinalIgnoreCase)]
public sealed record WebUrl : SemanticString<WebUrl> { }
// File extension validation
[EndsWith(".pdf", StringComparison.OrdinalIgnoreCase)]
[EndsWith(".doc", StringComparison.OrdinalIgnoreCase)]
[EndsWith(".docx", StringComparison.OrdinalIgnoreCase)]
public sealed record DocumentFile : SemanticString<DocumentFile> { }
// Combined prefix and suffix
[PrefixAndSuffix("API_", "_v1", StringComparison.OrdinalIgnoreCase)]
public sealed record ApiKey : SemanticString<ApiKey> { }
// Usage examples
var url = "https://example.com".As<WebUrl>();
var document = "report.PDF".As<DocumentFile>(); // Case insensitive
var apiKey = "API_secret_key_v1".As<ApiKey>();// Must contain specific substring
[Contains("@")]
public sealed record EmailString : SemanticString<EmailString> { }
// Must contain hashtag
[Contains("#")]
public sealed record HashtagString : SemanticString<HashtagString> { }
// Usage examples
var email = "user@domain.com".As<EmailString>();
var hashtag = "#programming".As<HashtagString>();// General path validation
[IsPath]
public sealed record FilePath : SemanticString<FilePath> { }
// Relative path only
[IsRelativePath]
public sealed record RelativePath : SemanticString<RelativePath> { }
// Absolute path only
[IsAbsolutePath]
public sealed record AbsolutePath : SemanticString<AbsolutePath> { }
// Directory paths only
[IsDirectoryPath]
public sealed record DirectoryPath : SemanticString<DirectoryPath> { }
// File paths only
[IsFilePath]
public sealed record FileOnlyPath : SemanticString<FileOnlyPath> { }
// Filename only (no path separators)
[IsFileName]
public sealed record FileName : SemanticString<FileName> { }
// File extension
[IsExtension]
public sealed record FileExtension : SemanticString<FileExtension> { }
// Usage examples
var absolutePath = @"C:\Users\John\Documents\file.txt".As<AbsolutePath>();
var relativePath = @"docs\readme.txt".As<RelativePath>();
var fileName = "document.pdf".As<FileName>();
var extension = ".txt".As<FileExtension>();// Must exist on filesystem
[DoesExist]
public sealed record ExistingPath : SemanticString<ExistingPath> { }
// Example with temporary file
string tempFile = Path.GetTempFileName();
File.WriteAllText(tempFile, "test content");
try
{
var existingPath = tempFile.As<ExistingPath>(); // ✅ Works
Console.WriteLine($"File exists: {existingPath}");
}
finally
{
File.Delete(tempFile);
}By default, ALL validation attributes must pass:
// All attributes must pass (default behavior)
[StartsWith("ID-")]
[RegexMatch(@"^ID-\d{6}$")]
public sealed record StrictIdentifier : SemanticString<StrictIdentifier> { }
// Explicit ValidateAll (same as default)
[ValidateAll]
[StartsWith("PRD-")]
[RegexMatch(@"^PRD-\d{4}$")]
public sealed record ProductCode : SemanticString<ProductCode> { }
// Usage examples
var strictId = "ID-123456".As<StrictIdentifier>(); // Must satisfy ALL rules
var productCode = "PRD-1234".As<ProductCode>();Any single attribute can pass:
// Any of these extensions is acceptable
[ValidateAny]
[EndsWith(".jpg", StringComparison.OrdinalIgnoreCase)]
[EndsWith(".png", StringComparison.OrdinalIgnoreCase)]
[EndsWith(".gif", StringComparison.OrdinalIgnoreCase)]
[EndsWith(".bmp", StringComparison.OrdinalIgnoreCase)]
public sealed record ImageFileName : SemanticString<ImageFileName> { }
// Any of these domains is acceptable
[ValidateAny]
[EndsWith("@company.com", StringComparison.OrdinalIgnoreCase)]
[EndsWith("@contractor.com", StringComparison.OrdinalIgnoreCase)]
[EndsWith("@partner.org", StringComparison.OrdinalIgnoreCase)]
public sealed record AuthorizedEmail : SemanticString<AuthorizedEmail> { }
// Usage examples
var image1 = "photo.jpg".As<ImageFileName>(); // ✅ Valid (.jpg)
var image2 = "icon.PNG".As<ImageFileName>(); // ✅ Valid (.png, case insensitive)
var email1 = "john@company.com".As<AuthorizedEmail>(); // ✅ Valid// Custom validation for color names
public sealed class IsValidColorAttribute : SemanticStringValidationAttribute
{
private static readonly string[] ValidColors =
{
"red", "green", "blue", "yellow", "orange", "purple",
"black", "white", "gray", "pink", "brown", "cyan"
};
public override bool Validate(ISemanticString semanticString)
{
string color = semanticString.ToString().ToLowerInvariant();
return ValidColors.Contains(color);
}
}
[IsValidColor]
public sealed record ColorName : SemanticString<ColorName> { }
// Usage examples
var red = "Red".As<ColorName>(); // ✅ Valid (case insensitive)
var blue = "BLUE".As<ColorName>(); // ✅ Valid
// var invalid = "magenta".As<ColorName>(); // ❌ Not in allowed list// Custom validation with parameters
public sealed class IsInRangeAttribute : SemanticStringValidationAttribute
{
private readonly int _min;
private readonly int _max;
public IsInRangeAttribute(int min, int max)
{
_min = min;
_max = max;
}
public override bool Validate(ISemanticString semanticString)
{
if (int.TryParse(semanticString.ToString(), out int value))
{
return value >= _min && value <= _max;
}
return false;
}
}
[IsInRange(1, 100)]
public sealed record Percentage : SemanticString<Percentage> { }
[IsInRange(1900, 2100)]
public sealed record Year : SemanticString<Year> { }
// Usage examples
var percentage = "85".As<Percentage>(); // ✅ Valid (85 is between 1-100)
var year = "2024".As<Year>(); // ✅ ValidCombine validation with automatic input normalization:
[RegexMatch(@"^[A-Z]{3}-\d{4}$")] // Must match after canonicalization
public sealed record NormalizedCode : SemanticString<NormalizedCode>
{
protected override string MakeCanonical(string input)
{
// Remove spaces and convert to uppercase
var cleaned = input.Replace(" ", "").Replace("-", "").ToUpperInvariant();
if (cleaned.Length == 7)
{
return cleaned.Insert(3, "-"); // Add hyphen at position 3
}
return input.ToUpperInvariant();
}
}
// Usage examples - all become "ABC-1234"
var code1 = "abc1234".As<NormalizedCode>(); // Lowercase input
var code2 = "ABC 1234".As<NormalizedCode>(); // With space
var code3 = "abc-1234".As<NormalizedCode>(); // Mixed case with hyphen
Console.WriteLine(code1); // Output: ABC-1234
Console.WriteLine(code2); // Output: ABC-1234
Console.WriteLine(code3); // Output: ABC-1234// Good: Specific validation for domain
[RegexMatch(@"^[A-Z]{2}\d{6}$")]
public sealed record CustomerCode : SemanticString<CustomerCode> { }
// Better: More specific with multiple constraints
[RegexMatch(@"^CU\d{6}$")]
public sealed record CustomerCodeSpecific : SemanticString<CustomerCodeSpecific> { }// Comprehensive email validation
[Contains("@")]
[Contains(".")]
[RegexMatch(@"^[^@\s]+@[^@\s]+\.[^@\s]+$")]
public sealed record ComprehensiveEmail : SemanticString<ComprehensiveEmail> { }// Support multiple ID formats
[ValidateAny]
[RegexMatch(@"^EMP-\d{6}$")] // Employee ID format
[RegexMatch(@"^CONT-\d{4}$")] // Contractor ID format
[RegexMatch(@"^TEMP-\d{3}$")] // Temporary ID format
public sealed record WorkerId : SemanticString<WorkerId> { }This validation system ensures your semantic strings meet exact business requirements while providing clear error messages when validation fails.