The Shared.Domain module contains common domain building blocks used across all business modules. It provides base classes, value objects, interfaces, and the Result pattern implementation.
namespace Shared.Domain.Entities;public abstract class BaseEntity{ public Guid Id { get; private set; }}
The Id property uses a private setter to enforce immutability. Entity IDs are typically generated by the database or using Guid.NewGuid() in factory methods.
using System.Diagnostics;using System.Net;namespace Shared.Domain.Models;[DebuggerDisplay("{DebuggerDisplay,nq}")]public sealed class Result<TValue>{ private string DebuggerDisplay => IsSuccess ? $"Success: {Value}" : $"Failure: {ErrorMessage} StatusCode: {StatusCode}"; public TValue? Value { get; } public string? ErrorMessage { get; } public bool IsSuccess => ErrorMessage == null; public bool IsFailure => !IsSuccess; public HttpStatusCode StatusCode { get; } private Result(TValue value, HttpStatusCode statusCode) { Value = value; ErrorMessage = null; StatusCode = statusCode; } private Result(string errorMessage, HttpStatusCode statusCode) { Value = default; ErrorMessage = errorMessage; StatusCode = statusCode; } private Result(TValue? value, HttpStatusCode statusCode, string? errorMessage) { Value = value; ErrorMessage = errorMessage; StatusCode = statusCode; } // Factory methods public static Result<TValue> Success( TValue value, HttpStatusCode statusCode = HttpStatusCode.OK) => new(value, statusCode); public static Result<TValue> Failure( string errorMessage, HttpStatusCode statusCode = HttpStatusCode.BadRequest) => new(errorMessage, statusCode); // Convert from another Result type public static Result<TValue> Failure<TResult>(Result<TResult> failedResult) { if (failedResult.IsSuccess) throw new ArgumentException("Result is success", nameof(failedResult)); return new Result<TValue>(failedResult.ErrorMessage!, failedResult.StatusCode); } public static Result<TValue> Failure(VoidResult failedResult) { if (failedResult.IsSuccess) throw new ArgumentException("Result is success", nameof(failedResult)); return new Result<TValue>(failedResult.ErrorMessage!, failedResult.StatusCode); } public static Result<TValue> Copy<TResult>(Result<TResult> result) where TResult : TValue { return new Result<TValue>(result.Value, result.StatusCode, result.ErrorMessage); } // Functional mapping public TResult Map<TResult>( Func<TValue, TResult> onSuccess, Func<string, TResult> onFailure) { return IsSuccess ? onSuccess(Value!) : onFailure(ErrorMessage!); }}
Usage Examples:
public Result<Product> CreateProduct(string title, decimal price){ if (string.IsNullOrWhiteSpace(title)) return Result<Product>.Failure("Title is required"); if (price <= 0) return Result<Product>.Failure("Price must be positive"); var product = new Product(title, price); return Result<Product>.Success(product, HttpStatusCode.Created);}
using System.Text.RegularExpressions;using Shared.Domain.Models;namespace Shared.Domain.ValueObjects;public sealed class Email{ private static readonly Regex Regex = new(@"^[^@\s]+@[^@\s]+\.[^@\s]+$", RegexOptions.Compiled); public string Value { get; private set; } private Email(string value) { Value = value; } public static Result<Email> Create(string value) { if (string.IsNullOrWhiteSpace(value)) return Result<Email>.Failure("Email cannot be null or empty"); if (!Regex.IsMatch(value)) return Result<Email>.Failure("Email is not valid"); return Result<Email>.Success(new Email(value)); } // Explicit operators for convenience public static explicit operator Email(string emailString) { Result<Email> createEmailResult = Create(emailString); if (!createEmailResult.IsSuccess) throw new ArgumentException(createEmailResult.ErrorMessage, nameof(emailString)); return createEmailResult.Value!; } public static explicit operator string(Email email) => email.Value;}
FullName
ValueObjects/FullName.cs
using Shared.Domain.Models;namespace Shared.Domain.ValueObjects;public sealed class FullName{ public string FirstName { get; private set; } public string LastName { get; private set; } public string? MiddleName { get; private set; } private FullName(string firstName, string lastName, string? middleName = null) { FirstName = firstName; LastName = lastName; MiddleName = middleName; } public static Result<FullName> Create( string firstName, string lastName, string? middleName = null) { if (string.IsNullOrWhiteSpace(firstName)) return Result<FullName>.Failure("First name is required"); if (string.IsNullOrWhiteSpace(lastName)) return Result<FullName>.Failure("Last name is required"); return Result<FullName>.Success(new FullName(firstName, lastName, middleName)); } public override string ToString() { return string.IsNullOrWhiteSpace(MiddleName) ? $"{FirstName} {LastName}" : $"{FirstName} {MiddleName} {LastName}"; }}
PhoneNumber
ValueObjects/PhoneNumber.cs
using System.Text.RegularExpressions;using Shared.Domain.Models;namespace Shared.Domain.ValueObjects;public sealed class PhoneNumber{ private static readonly Regex Regex = new(@"^\+?[1-9]\d{1,14}$", RegexOptions.Compiled); public string Value { get; private set; } private PhoneNumber(string value) { Value = value; } public static Result<PhoneNumber> Create(string value) { if (string.IsNullOrWhiteSpace(value)) return Result<PhoneNumber>.Failure("Phone number cannot be null or empty"); if (!Regex.IsMatch(value)) return Result<PhoneNumber>.Failure("Phone number is not valid"); return Result<PhoneNumber>.Success(new PhoneNumber(value)); } public static explicit operator PhoneNumber(string phoneString) { Result<PhoneNumber> result = Create(phoneString); if (!result.IsSuccess) throw new ArgumentException(result.ErrorMessage, nameof(phoneString)); return result.Value!; } public static explicit operator string(PhoneNumber phone) => phone.Value;}
Address
ValueObjects/Address.cs
using Shared.Domain.Models;namespace Shared.Domain.ValueObjects;public sealed class Address{ public string City { get; private set; } public string Street { get; private set; } public uint HouseNumber { get; private set; } public uint? ApartmentNumber { get; private set; } private Address( string city, string street, uint houseNumber, uint? apartmentNumber) { City = city; Street = street; HouseNumber = houseNumber; ApartmentNumber = apartmentNumber; } public static Result<Address> Create( string city, string street, uint houseNumber, uint? apartmentNumber = null) { if (string.IsNullOrWhiteSpace(city)) return Result<Address>.Failure("City is required"); if (string.IsNullOrWhiteSpace(street)) return Result<Address>.Failure("Street is required"); if (houseNumber == 0) return Result<Address>.Failure("House number must be greater than 0"); return Result<Address>.Success( new Address(city, street, houseNumber, apartmentNumber)); } public override string ToString() { return ApartmentNumber.HasValue ? $"{City}, {Street} {HouseNumber}/{ApartmentNumber.Value}" : $"{City}, {Street} {HouseNumber}"; }}
BirthDate
ValueObjects/BirthDate.cs
using Shared.Domain.Models;namespace Shared.Domain.ValueObjects;public sealed class BirthDate{ public DateOnly Value { get; private set; } private BirthDate(DateOnly value) { Value = value; } public static Result<BirthDate> Create(DateOnly value) { // Must be at least 13 years old int age = DateTime.UtcNow.Year - value.Year; if (age < 13) return Result<BirthDate>.Failure("Must be at least 13 years old"); // Can't be in the future if (value > DateOnly.FromDateTime(DateTime.UtcNow)) return Result<BirthDate>.Failure("Birth date cannot be in the future"); return Result<BirthDate>.Success(new BirthDate(value)); } public int GetAge() { var today = DateTime.UtcNow; int age = today.Year - Value.Year; if (Value.ToDateTime(TimeOnly.MinValue) > today.AddYears(-age)) age--; return age; }}
namespace Shared.Domain.Interfaces;public interface IPaginationRepository<TProjection>{ Task<(IReadOnlyCollection<TProjection> Items, int TotalCount)> GetPagedAsync( int pageNumber, int pageSize, CancellationToken cancellationToken);}
Value objects prevent primitive obsession. Instead of passing strings everywhere, you pass Email, PhoneNumber, etc., making the code more expressive and preventing errors.
Validation Centralization
Validation logic lives in one place (the value object’s Create method), ensuring consistency across the application.
No Exceptions for Flow Control
The Result pattern eliminates exceptions for expected failures, making error handling explicit and predictable.
Testability
Pure domain logic with no dependencies on infrastructure makes unit testing straightforward.