Validated.Primitives
0.1.20
See the version list below for details.
dotnet add package Validated.Primitives --version 0.1.20
NuGet\Install-Package Validated.Primitives -Version 0.1.20
<PackageReference Include="Validated.Primitives" Version="0.1.20" />
<PackageVersion Include="Validated.Primitives" Version="0.1.20" />
<PackageReference Include="Validated.Primitives" />
paket add Validated.Primitives --version 0.1.20
#r "nuget: Validated.Primitives, 0.1.20"
#:package Validated.Primitives@0.1.20
#addin nuget:?package=Validated.Primitives&version=0.1.20
#tool nuget:?package=Validated.Primitives&version=0.1.20
Validated Primitives
A .NET library that provides strongly-typed, self-validating primitive value objects to eliminate primitive obsession and enforce domain constraints at compile-time.
What are Validated Primitives?
Validated Primitives are value objects that encapsulate primitive types (strings, dates, numbers) with built-in validation rules. Instead of passing raw strings or dates throughout your application, you use strongly-typed objects that guarantee their validity.
The Problem: Primitive Obsession
// ❌ Traditional approach - primitive obsession
public class User
{
public string Email { get; set; } // Could be null, empty, or invalid
public string PhoneNumber { get; set; } // No format validation
public DateTime DateOfBirth { get; set; } // Could be in the future!
public string PostalCode { get; set; } // No country-specific validation
}
// Validation logic scattered everywhere
if (string.IsNullOrWhiteSpace(user.Email) || !IsValidEmail(user.Email))
{
throw new ArgumentException("Invalid email");
}
The Solution: Validated Primitives
// ✅ With Validated Primitives
public class User
{
public EmailAddress Email { get; set; } // Always valid or null
public PhoneNumber PhoneNumber { get; set; } // Always properly formatted
public DateOfBirth DateOfBirth { get; set; } // Always in the past
public PostalCode PostalCode { get; set; } // Country-specific validation
}
// Validation happens at creation - guaranteed valid everywhere else
var (result, email) = EmailAddress.TryCreate(userInput);
if (!result.IsValid)
{
// Handle validation errors
Console.WriteLine(result.ToBulletList());
return;
}
// email is guaranteed to be valid here
user.Email = email;
Key Benefits
- Type Safety: Compiler prevents mixing up different string types
- Self-Validating: Validation logic lives with the data
- Immutable: Value objects are records - thread-safe by default
- Explicit Intent: Code clearly communicates business rules
- Centralized Validation: No scattered validation logic
- Rich Error Messages: Detailed validation feedback
Installation
dotnet add package Validated.Primitives
Available Value Objects
Email & Communication
EmailAddress- Valid email format, max 256 charactersPhoneNumber- Valid phone number formatWebsiteUrl- Valid HTTP/HTTPS URLs
Location
PostalCode- Country-specific postal code validation for 30+ countries
Network
IpAddress- Valid IPv4 or IPv6 addresses
Date & Time
DateOfBirth- Must be in the pastFutureDate- Must be in the futureBetweenDatesSelection- Date within a specified rangeDateRange- Represents a range between two DateTimesDateOnlyRange- Represents a range between two DateOnly valuesTimeOnlyRange- Represents a range between two TimeOnly values
Usage Examples
Basic Usage
using Validated.Primitives.ValueObjects;
// Create a validated email address
var (result, email) = EmailAddress.TryCreate("user@example.com");
if (result.IsValid)
{
Console.WriteLine($"Email: {email.Value}");
// Use email.Value to access the underlying string
}
else
{
// Handle validation errors
foreach (var error in result.Errors)
{
Console.WriteLine($"Error: {error.Message}");
}
}
Postal Codes with Country Validation
using Validated.Primitives.ValueObjects;
// Validate a US ZIP code
var (result, usZip) = PostalCode.TryCreate(CountryCode.UnitedStates, "12345");
if (result.IsValid)
{
Console.WriteLine($"Valid US ZIP: {usZip.Value}");
Console.WriteLine($"Country: {usZip.GetCountryName()}"); // "United States"
}
// Validate a UK postal code
var (result2, ukCode) = PostalCode.TryCreate(CountryCode.UnitedKingdom, "SW1A 1AA");
if (result2.IsValid)
{
Console.WriteLine($"Valid UK postcode: {ukCode.Value}");
}
// Accept any format with Unknown or All
var (result3, genericCode) = PostalCode.TryCreate(CountryCode.All, "XYZ-123");
if (result3.IsValid)
{
Console.WriteLine($"Generic postal code: {genericCode.Value}");
}
// Country-specific validation will fail for wrong formats
var (result4, invalid) = PostalCode.TryCreate(CountryCode.UnitedStates, "ABCDE");
if (!result4.IsValid)
{
Console.WriteLine(result4.ToSingleMessage());
// "PostalCode is not a valid postal code format for UnitedStates."
}
Supported Countries (30+):
- United States, United Kingdom, Canada, Australia
- Germany, France, Italy, Spain, Netherlands, Belgium, Switzerland, Austria
- Sweden, Norway, Denmark, Finland, Poland, Czech Republic, Hungary, Portugal, Ireland
- Japan, China, India, Brazil, Mexico, South Africa, New Zealand, Singapore, South Korea, Russia
Special Country Codes:
CountryCode.Unknown- Accepts any postal code format (basic validation only)CountryCode.All- Accepts any postal code format (basic validation only)
Handling Validation Errors
var (result, phoneNumber) = PhoneNumber.TryCreate("invalid");
if (!result.IsValid)
{
// Single line message
Console.WriteLine(result.ToSingleMessage());
// Bullet list format
Console.WriteLine(result.ToBulletList());
// Dictionary format (useful for APIs)
var errors = result.ToDictionary();
foreach (var kvp in errors)
{
Console.WriteLine($"{kvp.Key}: {string.Join(", ", kvp.Value)}");
}
}
Domain Models
public class Customer
{
public Guid Id { get; init; }
public EmailAddress Email { get; init; }
public PhoneNumber Phone { get; init; }
public DateOfBirth BirthDate { get; init; }
public PostalCode PostalCode { get; init; }
public WebsiteUrl? Website { get; init; }
public static (ValidationResult, Customer?) Create(
string email,
string phone,
DateTime birthDate,
CountryCode country,
string postalCode,
string? website = null)
{
var validationResult = ValidationResult.Success();
var (emailResult, emailValue) = EmailAddress.TryCreate(email);
validationResult.Merge(emailResult);
var (phoneResult, phoneValue) = PhoneNumber.TryCreate(phone);
validationResult.Merge(phoneResult);
var (birthResult, birthValue) = DateOfBirth.TryCreate(birthDate);
validationResult.Merge(birthResult);
var (postalResult, postalValue) = PostalCode.TryCreate(country, postalCode);
validationResult.Merge(postalResult);
WebsiteUrl? websiteValue = null;
if (!string.IsNullOrWhiteSpace(website))
{
var (websiteResult, webValue) = WebsiteUrl.TryCreate(website);
validationResult.Merge(websiteResult);
websiteValue = webValue;
}
if (!validationResult.IsValid)
return (validationResult, null);
var customer = new Customer
{
Id = Guid.NewGuid(),
Email = emailValue!,
Phone = phoneValue!,
BirthDate = birthValue!,
PostalCode = postalValue!,
Website = websiteValue
};
return (validationResult, customer);
}
}
// Usage
var (result, customer) = Customer.Create(
"john.doe@example.com",
"+1-555-123-4567",
new DateTime(1990, 5, 15),
CountryCode.UnitedStates,
"12345",
"https://johndoe.com"
);
if (result.IsValid)
{
// customer is guaranteed to have valid data
Console.WriteLine($"Created customer: {customer.Email}");
}
else
{
Console.WriteLine("Validation failed:");
Console.WriteLine(result.ToBulletList());
}
Date Ranges
// Working with date ranges
var startDate = DateTime.Now;
var endDate = DateTime.Now.AddDays(7);
var (result, dateRange) = DateRange.TryCreate(startDate, endDate);
if (result.IsValid)
{
Console.WriteLine($"Range: {dateRange.Start} to {dateRange.End}");
Console.WriteLine($"Duration: {dateRange.Duration.TotalDays} days");
// Check if a date is within the range
var isInRange = dateRange.Contains(DateTime.Now.AddDays(3));
}
// DateOnly ranges
var (result2, dateOnlyRange) = DateOnlyRange.TryCreate(
DateOnly.FromDateTime(startDate),
DateOnly.FromDateTime(endDate)
);
Custom Property Names
// Customize validation error property names for better error messages
var (result, email) = EmailAddress.TryCreate(input, "CustomerEmail");
// Error message will reference "CustomerEmail" instead of default "Email"
if (!result.IsValid)
{
// Output: "CustomerEmail: Invalid email format"
Console.WriteLine(result.ToSingleMessage());
}
// Also works with postal codes
var (result2, postalCode) = PostalCode.TryCreate(
CountryCode.UnitedStates,
input,
"ShippingPostalCode"
);
API Integration Example
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
[HttpPost]
public IActionResult CreateUser([FromBody] CreateUserRequest request)
{
var (emailResult, email) = EmailAddress.TryCreate(request.Email, nameof(request.Email));
if (!emailResult.IsValid)
{
return BadRequest(emailResult.ToDictionary());
}
var (phoneResult, phone) = PhoneNumber.TryCreate(request.Phone, nameof(request.Phone));
if (!phoneResult.IsValid)
{
return BadRequest(phoneResult.ToDictionary());
}
var (postalResult, postalCode) = PostalCode.TryCreate(
request.CountryCode,
request.PostalCode,
nameof(request.PostalCode)
);
if (!postalResult.IsValid)
{
return BadRequest(postalResult.ToDictionary());
}
// Create user with validated primitives
var user = new User
{
Email = email,
Phone = phone,
PostalCode = postalCode
};
// Save user...
return Ok(user);
}
}
Creating Custom Validated Primitives
You can easily create your own validated primitives by inheriting from ValidatedValueObject<T>:
using Validated.Primitives.Core;
using Validated.Primitives.Validation;
using Validated.Primitives.Validators;
public sealed record TaxId : ValidatedValueObject<string>
{
private TaxId(string value, string propertyName = "TaxId")
: base(value)
{
Validators.Add(CommonValidators.NotNullOrWhitespace(propertyName));
Validators.Add(CommonValidators.Length(propertyName, 9, 11));
// Add your custom validation logic
}
public static (ValidationResult Result, TaxId? Value) TryCreate(
string value,
string propertyName = "TaxId")
{
var taxId = new TaxId(value, propertyName);
var validationResult = taxId.Validate();
var result = validationResult.IsValid ? taxId : null;
return (validationResult, result);
}
}
Built-in Validators
The library includes several validator helpers:
CommonValidators
NotNullOrWhitespace- Ensures string is not null or whitespaceMaxLength- Maximum string lengthMinLength- Minimum string lengthLength- Exact or range of acceptable lengths
DateTimeValidators
BeforeToday- Date must be in the pastAfterToday- Date must be in the futureBetween- Date within a range
EmailValidators
EmailFormat- Valid email format
PhoneValidators
PhoneNumber- Valid phone number format
UrlValidators
ValidUrl- Valid URL formatHttpOrHttps- Must be HTTP or HTTPS scheme
IpValidators
ValidIpAddress- Valid IPv4 or IPv6 address
PostalCodeValidators
ValidFormat- Basic postal code format (alphanumeric, spaces, hyphens)ValidateCountryFormat- Country-specific postal code validation
Design Principles
- Immutability: All value objects are records, ensuring thread safety
- Fail Fast: Validation happens at creation time
- Explicit over Implicit:
TryCreatepattern makes validation explicit - Rich Feedback: Detailed validation errors with member names and codes
- Composability: Easy to combine multiple validators
- Zero Magic: No reflection, attributes, or hidden behavior
Requirements
- .NET 8.0 or higher
Contributing
Contributions are welcome! Please feel free to submit pull requests or open issues.
License
MIT
Resources
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- No dependencies.
NuGet packages (3)
Showing the top 3 NuGet packages that depend on Validated.Primitives:
| Package | Downloads |
|---|---|
|
Validated.Primitives.Domain
Domain models and complex types built from Validated.Primitives value objects. Provides strongly-typed, self-validating domain entities for common use cases like Address, ContactInformation, PersonName, and CreditCardDetails. |
|
|
Mayordomo.Web.Extractor
Small, modular .NET 10 library to extract article content, images and metadata from web pages. |
|
|
ReadableWeb
ReadableWeb extracts readable article content, images and metadata from HTML. Small, modular .NET 10 library for article extraction and readability parsing. |
GitHub repositories
This package is not used by any popular GitHub repositories.
Validated primitives including EmailAddress, PhoneNumber, WebsiteUrl, IpAddress, DateOfBirth, FutureDate, BetweenDatesSelection, DateRange, DateOnlyRange, TimeOnlyRange, and PostalCode.