Quick Takeaways
What you'll learn in this article
- 1
CrashBytes NuGet packages address genuine holes in the
- 2
NET ecosystem โ from guard clauses and Result types to source-generated mediators and AOT-safe enum utilities
Keep reading for detailed implementation, code examples, and real-world results
The .NET ecosystem is mature. It has world-class tooling, a performant runtime, and a massive standard library. But maturity doesn't mean completeness. There are recurring gaps โ small, specific utility areas where the BCL offers nothing or where the existing solutions drag in transitive dependencies, rely on runtime reflection, or require more ceremony than the problem warrants.
You know the pattern. You start a new project and immediately copy over the same guard clause helpers, the same Result<T> type, the same LINQ extensions you've written three times before. Or you pull in a framework that does far more than you need, bringing dozens of transitive packages along for the ride.
CrashBytes built 11 focused, zero-dependency, MIT-licensed NuGet packages to address these gaps. Each one targets a specific problem, ships with no transitive dependencies, and stays small enough that you can read the entire source in a single sitting. The philosophy is simple: minimal surface area, strong typing, thorough test coverage, and nothing you didn't ask for.
Validation and Error Handling
Every application needs input validation and structured error handling. The BCL gives you ArgumentNullException and ArgumentException, but no ergonomic way to use them. And when you want to move beyond exceptions entirely, there's no built-in Result type to reach for.
CrashBytes.Guards
The gap: Validating method arguments in .NET requires verbose if/throw boilerplate that clutters every public method. You check for null, check for empty strings, check for negative numbers โ and the actual logic gets buried under six lines of ceremony.
// Before: the same pattern copied into every method
public void CreateUser(string name, string email, int age)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("Cannot be null or whitespace.", nameof(name));
if (string.IsNullOrWhiteSpace(email))
throw new ArgumentException("Cannot be null or whitespace.", nameof(email));
if (age <= 0)
throw new ArgumentException("Must be positive.", nameof(age));
// actual logic starts here
}
Ardalis.GuardClauses exists, but it brings opinions about package structure and extension patterns that not every team shares. CrashBytes.Guards takes a simpler approach: one static entry point, fluent chaining, and every guard returns the validated value so you can assign inline.
// After: validation is concise and the intent is clear
public void CreateUser(string name, string email, int age)
{
name = Guard.Against.NullOrWhiteSpace(name, nameof(name));
email = Guard.Against.InvalidEmail(email, nameof(email));
age = Guard.Against.NegativeOrZero(age, nameof(age));
}
The library covers null checks, numeric ranges, string format validation (email, URL, regex), GUID emptiness, date boundaries (past/future), file and directory existence, and custom predicates. Every method throws the appropriate exception type โ ArgumentNullException for nulls, ArgumentOutOfRangeException for ranges, ArgumentException for format violations.
CrashBytes.Results
The gap: Exceptions are expensive, opaque, and don't compose. When a method can fail for expected business reasons โ validation errors, not-found conditions, permission denials โ throwing exceptions forces callers into try/catch blocks and hides the failure path from the type system. You can return null, but that tells you nothing about why something failed.
Languages like Rust and F# solve this with Result types. C# doesn't have one in the BCL. Libraries like FluentResults and OneOf exist, but they either bring excessive API surface or model the problem differently than the Ok/Err pattern most teams actually want.
CrashBytes.Results provides a Result and Result<T> with Map, Bind, Match, and implicit conversions:
public Result<UserDto> GetUser(int id)
{
var user = _db.Find(id);
if (user is null)
return new Error("USER_NOT_FOUND", $"No user with ID {id}");
return new UserDto(user.Name, user.Email);
}
// Composing results with Map and Bind
var result = GetUser(42)
.Map(user => user with { Name = user.Name.ToUpper() })
.Bind(user => ValidateUser(user));
// Pattern matching for exhaustive handling
string message = result.Match(
onSuccess: user => $"Found {user.Name}",
onFailure: errors => string.Join("; ", errors.Select(e => e.Message))
);
The Error type carries a Code and Message, making it straightforward to map to HTTP ProblemDetails or gRPC status codes. Implicit conversions let you return a value directly as a success or an Error as a failure โ no Result<T>.Success(value) ceremony required unless you prefer the explicit form.
Architecture and Patterns
Two patterns dominate modern .NET backends: mediator-based CQRS and object-to-object mapping. The dominant libraries for these โ MediatR and AutoMapper โ rely on runtime reflection, which means they break under Native AOT, produce opaque errors, and require service registration boilerplate.
CrashBytes.Mediator
The gap: MediatR is the de facto mediator in .NET, but it relies on runtime reflection to resolve handlers. In AOT-compiled applications, this breaks entirely. Even in traditional deployments, the reflection-based dispatch means you don't discover missing handlers until runtime, and the pipeline behavior model requires careful registration ordering.
CrashBytes.Mediator provides the same IRequest<TResponse>, IRequestHandler<TRequest, TResponse>, INotification, and INotificationHandler<T> abstractions, plus IPipelineBehavior<TRequest, TResponse> for cross-cutting concerns. Handler resolution goes through IServiceProvider, so it works with any DI container:
// Define a query
public record GetUserQuery(int UserId) : IRequest<UserDto>;
// Implement the handler
public class GetUserHandler : IRequestHandler<GetUserQuery, UserDto>
{
private readonly IUserRepository _repo;
public GetUserHandler(IUserRepository repo) => _repo = repo;
public async Task<UserDto> Handle(GetUserQuery request, CancellationToken ct)
{
var user = await _repo.GetByIdAsync(request.UserId, ct);
return new UserDto(user.Name, user.Email);
}
}
// Cross-cutting pipeline behavior
public class LoggingBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken ct)
{
Console.WriteLine($"Handling {typeof(TRequest).Name}");
var response = await next();
Console.WriteLine($"Handled {typeof(TRequest).Name}");
return response;
}
}
// Dispatch
var user = await mediator.Send(new GetUserQuery(42));
The notification model supports fan-out to multiple handlers, and Unit provides a void-equivalent return type for commands that don't return data.
CrashBytes.Mapper
The gap: AutoMapper is powerful but opaque. Mappings are configured at startup and resolved at runtime via reflection. When a property name changes, you don't find out until the mapping fails โ or worse, silently maps to null. The configuration API is large, and the transitive dependency chain is non-trivial.
CrashBytes.Mapper provides convention-based mapping with explicit configuration and custom property resolvers:
var config = new MapperConfiguration()
.CreateMap<UserEntity, UserDto>()
.ForMember(
dto => dto.FullName,
entity => $"{entity.FirstName} {entity.LastName}")
.CreateMap<OrderEntity, OrderDto>();
IMapper mapper = config.BuildMapper();
var dto = mapper.Map<UserEntity, UserDto>(entity);
var order = mapper.Map<OrderDto>(orderEntity);
Property matching is case-insensitive by convention, and mappings are cached after the first resolution. The ForMember API uses lambda expressions, so refactoring tools catch renames immediately. You can also map into existing instances with the three-argument Map<TSource, TDest>(source, destination) overload, which is useful for update scenarios where you don't want to allocate a new object.
Extensions and Utilities
The BCL's LINQ, string, collection, DateTime, and enum APIs are solid but deliberately conservative. Microsoft ships only what works for everyone, which means you're left implementing the same twenty extension methods in every project.
CrashBytes.Linq
The gap: LINQ covers the 80% case but stops short of operations that experienced developers use daily. There's no Batch, no Pairwise, no Partition, no conditional WhereIf. You end up writing these yourself, testing them yourself, and copying them between repositories.
CrashBytes.Linq adds the missing operations:
var items = Enumerable.Range(1, 10); // Conditional filtering โ avoid if/else branching on query construction bool applyFilter = true; var filtered = items.WhereIf(applyFilter, x => x > 5); // Batch processing โ chunk a sequence into fixed-size groups var batches = items.Batch(3); // [[1,2,3], [4,5,6], [7,8,9], [10]] // Sliding window over consecutive pairs var deltas = items.Pairwise((a, b) => b - a); // [1, 1, 1, 1, 1, 1, 1, 1, 1] // Split a sequence by predicate var (evens, odds) = items.Partition(x => x % 2 == 0); // Running accumulator (like fold, but yields intermediate results) var runningSum = items.Scan((acc, x) => acc + x); // [1, 3, 6, 10, 15, 21, 28, 36, 45, 55]
The library also includes Shuffle, PickRandom, Interleave, Flatten, Traverse (depth-first recursive), DistinctBy, Page, IndexOf, ForEach (with and without index), WhereNotNull, and ToDictionarySafe (keeps first value on duplicate keys instead of throwing).
CrashBytes.Collections
The gap: Dictionary<TKey, TValue> doesn't have GetOrAdd. IList<T> doesn't have RemoveWhere. Checking if a collection is null or empty requires collection == null || collection.Count == 0 every time. These are one-liners, but you write them hundreds of times across a codebase.
// Dictionary operations
var cache = new Dictionary<string, List<int>>();
var list = cache.GetOrAdd("key", _ => new List<int>());
cache.TryAdd("key2", new List<int> { 1, 2, 3 });
var readOnly = cache.AsReadOnly();
// List operations
var items = new List<int> { 1, 2, 3, 4, 5 };
int removed = items.RemoveWhere(x => x > 3); // returns 2
items.Shuffle();
items.AddRange(new[] { 6, 7, 8 });
// Null safety
ICollection<int>? maybeNull = null;
bool empty = maybeNull.IsNullOrEmpty(); // true
var safe = maybeNull.EmptyIfNull(); // Enumerable.Empty<int>()
The GetOrAdd overload with a factory function is particularly useful for caches and lookup tables โ it only creates the value if the key doesn't exist, avoiding unnecessary allocations. The With method returns the collection for fluent chaining.
CrashBytes.Collections on NuGet
CrashBytes.Strings
The gap: .NET gives you string.ToUpper() and string.ToLower(), but nothing for slugification, snake_case conversion, masking, or HTML stripping. Every web application needs at least three of these, and every team writes them from scratch or pulls in a utility library that does too much.
// Case conversion
"MyPropertyName".ToSnakeCase(); // "my_property_name"
"my-api-endpoint".ToPascalCase(); // "MyApiEndpoint"
"HelloWorld".ToKebabCase(); // "hello-world"
"some_variable".ToCamelCase(); // "someVariable"
// URL-safe slugs with diacritic removal
"Hรฉllo Wรถrld! โ A Story".ToSlug(); // "hello-world-a-story"
// Truncation
"This is a long sentence".Truncate(10); // "This is..."
"This is a long sentence".TruncateWords(3); // "This is a..."
// Masking sensitive data
"4111111111111111".Mask(4); // "************1111"
"user@email.com".Mask(4); // "**********l.com"
// Extraction
"[start]content[end]".Between("[start]", "[end]"); // "content"
"user@domain.com".Before("@"); // "user"
"user@domain.com".After("@"); // "domain.com"
// Search
"Hello World".ContainsAll("hello", "world"); // true (case-insensitive)
"Hello World".ContainsAny("goodbye", "world"); // true
"abcabcabc".CountOccurrences("abc"); // 3
The library also includes RemoveDiacritics, StripHtml, Repeat, IsValidEmail, IsValidUrl, and fluent IsNullOrEmpty/IsNullOrWhiteSpace wrappers. Every method is null-safe โ pass null and you get a sensible default (empty string, false, or 0) rather than a NullReferenceException.
CrashBytes.DateTime
The gap: Working with dates in .NET means writing the same boundary calculations repeatedly. What's the start of this month? The end of last quarter? How many business days between two dates? What's this person's age? The BCL has DateTime.DaysInMonth and that's about it.
var now = DateTime.Now; // Period boundaries var monthStart = now.StartOfMonth(); // first day, 00:00:00 var monthEnd = now.EndOfMonth(); // last day, 23:59:59.9999999 var weekStart = now.StartOfWeek(); // Monday (configurable) var quarterStart = now.StartOfQuarter(); int quarter = now.Quarter(); // 1, 2, 3, or 4 // Business day calculations var deadline = now.AddBusinessDays(10); // skips weekends bool isWorkday = now.IsBusinessDay(); bool isWeekend = now.IsWeekend(); // Age and lifecycle var birthDate = new DateTime(1990, 6, 15); int age = birthDate.Age(); // calculates correctly across leap years // Relative time (for UI display) DateTime.UtcNow.AddMinutes(-5).ToRelativeString(); // "5 minutes ago" DateTime.UtcNow.AddHours(2).ToRelativeString(); // "in 2 hours" DateTime.UtcNow.AddDays(-1).ToRelativeString(); // "yesterday" // Utility bool inRange = now.IsBetween(startDate, endDate); int weekNum = now.WeekOfYear(); // ISO 8601 long unix = now.ToUnixTimestamp();
AddBusinessDays handles negative values correctly, walking backward through the calendar and skipping weekends in both directions. ToRelativeString accepts an optional relativeTo parameter for deterministic testing.
CrashBytes.Enums
The gap: Getting the [Description] attribute from an enum value requires four lines of reflection. Iterating all values of an enum requires Enum.GetValues(typeof(T)).Cast<T>(). Parsing a string to an enum with a fallback default requires a try/catch or Enum.TryParse with manual default handling. These operations should be one-liners.
public enum OrderStatus
{
[Description("Waiting for payment")]
Pending = 0,
[Description("Payment confirmed")]
Confirmed = 1,
[Description("Shipped to customer")]
Shipped = 2
}
// Metadata retrieval
OrderStatus.Pending.GetDescription(); // "Waiting for payment"
OrderStatus.Pending.GetDisplayName(); // uses [Display(Name = "...")] or falls back to name
// Collection operations
var allValues = EnumExtensions.GetValues<OrderStatus>();
// [Pending, Confirmed, Shipped]
var lookup = EnumExtensions.ToDescriptionDictionary<OrderStatus>();
// { Pending: "Waiting for payment", Confirmed: "Payment confirmed", ... }
// Safe parsing with fallback
var status = EnumExtensions.Parse<OrderStatus>("confirmed"); // Confirmed (case-insensitive)
var unknown = EnumExtensions.Parse("invalid", OrderStatus.Pending); // Pending (fallback)
// Navigation
var next = OrderStatus.Pending.Next(); // Confirmed
var prev = OrderStatus.Shipped.Previous(); // Confirmed
// Flags support
[Flags]
public enum Permissions { Read = 1, Write = 2, Execute = 4 }
var flags = (Permissions.Read | Permissions.Write).GetFlags();
// [Read, Write]
Every method is generic and constrained to struct, Enum, so you get compile-time safety. The library handles enums with and without [Description] attributes โ when the attribute is missing, it falls back to the member name.
Infrastructure
The final two packages address infrastructure concerns: HTTP client configuration and application startup validation.
CrashBytes.Http
The gap: HttpClient is deliberately low-level. Every team builds the same wrapper: JSON serialization/deserialization, bearer token attachment, retry logic with exponential backoff, correlation ID propagation. Some teams pull in Polly for retries and Refit for typed clients, but both are significant dependencies for what often amounts to a few extension methods.
CrashBytes.Http provides fluent configuration and typed JSON methods:
// Fluent client setup
var client = new HttpClient()
.WithBaseAddress("https://api.example.com")
.WithBearerToken(accessToken)
.WithCorrelationId()
.WithTimeout(TimeSpan.FromSeconds(30))
.WithHeader("X-Api-Version", "2");
// Typed JSON operations (System.Text.Json, camelCase by default)
var user = await client.GetJsonAsync<UserDto>("/users/42");
var created = await client.PostJsonAsync<CreateUserRequest, UserDto>(
"/users", new CreateUserRequest("Jane", "jane@example.com"));
await client.PutJsonAsync<UpdateUserRequest, UserDto>(
"/users/42", updateRequest);
await client.PatchJsonAsync<PatchRequest, UserDto>(
"/users/42", patchRequest);
bool deleted = await client.DeleteAsync("/users/42");
// Retry with exponential backoff on 5xx responses
var response = await client.SendWithRetryAsync(
() => new HttpRequestMessage(HttpMethod.Get, "/fragile-endpoint"),
maxRetries: 3,
initialDelay: TimeSpan.FromMilliseconds(200));
All JSON methods use System.Text.Json with sensible defaults (case-insensitive deserialization, camelCase serialization). The WithCorrelationId method generates a new GUID if none is provided, or accepts an existing correlation ID for propagating through distributed systems. The retry logic only retries on 5xx status codes and transport exceptions โ it won't retry 4xx client errors.
CrashBytes.Configuration
The gap: .NET's IConfiguration and IOptions<T> are powerful but require Microsoft.Extensions.Configuration and friends. For lightweight applications, console tools, or scenarios where you want configuration validation without the full options pattern, there's no built-in way to bind a dictionary to a POCO and validate it with DataAnnotations.
CrashBytes.Configuration bridges this gap with dictionary-based binding, validation, and environment variable helpers:
var config = new Dictionary<string, string>
{
{ "Database:Host", "localhost" },
{ "Database:Port", "5432" },
{ "Database:Name", "myapp" },
{ "Database:Ssl", "true" }
};
// Type-safe binding with prefix support
public class DatabaseConfig
{
[Required]
public string Host { get; set; } = "";
public int Port { get; set; }
public string Name { get; set; } = "";
public bool Ssl { get; set; }
}
var dbConfig = config.Bind<DatabaseConfig>("Database");
// dbConfig.Host = "localhost", dbConfig.Port = 5432, etc.
// DataAnnotations validation
var errors = config.Validate<DatabaseConfig>("Database");
if (errors.Count > 0)
throw new InvalidOperationException(string.Join(", ", errors));
// Required values with clear error messages
string connString = config.Require("ConnectionString");
// Typed defaults
int port = config.GetOrDefault("Port", 8080);
bool debug = config.GetOrDefault("Debug", false);
// Environment variables
string apiKey = ConfigurationExtensions.RequireEnvironmentVariable("API_KEY");
string env = ConfigurationExtensions.GetEnvironmentVariableOrDefault("ENV", "development");
// Connection string parsing
var parts = ConfigurationExtensions.ParseConnectionString(
"Server=localhost;Port=5432;Database=myapp");
// { "Server": "localhost", "Port": "5432", "Database": "myapp" }
The Bind<T> method handles type conversion for strings, integers, longs, doubles, decimals, booleans, TimeSpan, and Uri. Property matching is case-insensitive, and the prefix parameter lets you scope bindings to a section of the configuration dictionary โ similar to how IConfiguration.GetSection works, but without the dependency.
Design Philosophy
Every CrashBytes NuGet package follows the same principles:
Zero dependencies. No package pulls in transitive dependencies. Your dependency graph stays clean, version conflicts don't happen, and dotnet publish output stays small. The only "dependencies" are the BCL types that ship with .NET itself.
AOT compatibility. The packages that could break under Native AOT โ mediator dispatch, object mapping, enum metadata โ are designed with AOT constraints in mind. No MakeGenericType, no Activator.CreateInstance in hot paths, no dynamic assembly generation.
MIT licensed. Every package is MIT licensed. Use them in commercial projects, modify them, redistribute them. No license gymnastics.
Tested. Each package ships with comprehensive unit tests covering happy paths, edge cases, null inputs, and boundary conditions. If a method accepts null, its behavior on null is tested and documented.
Small API surface. Each package does one thing. CrashBytes.Guards doesn't include a Result type. CrashBytes.Linq doesn't include string utilities. If you only need guard clauses, you install one package and get guard clauses โ not a Swiss Army knife.
Conventional .NET patterns. Extension methods where extension methods make sense. Static factories where static factories make sense. Fluent APIs where chaining is natural. No framework-specific abstractions, no custom DI containers, no mandatory base classes.
Getting Started
Install any package individually via the .NET CLI:
dotnet add package CrashBytes.Guards dotnet add package CrashBytes.Results dotnet add package CrashBytes.Mediator
Or add all of them โ they're small enough that the combined footprint is negligible.
Package Reference
| Package | Purpose | Key API | | ----------------------------------------------------------------------------------- | -------------------- | -------------------------------------- | | CrashBytes.Guards | Input validation | Guard.Against.NullOrEmpty() | | CrashBytes.Results | Error handling | Result<T>.Map().Bind().Match() | | CrashBytes.Mediator | CQRS mediator | IRequest<T> + IRequestHandler<,> | | CrashBytes.Mapper | Object mapping | MapperConfiguration.BuildMapper() | | CrashBytes.Linq | LINQ extensions | .Batch().Partition().Pairwise() | | CrashBytes.Collections | Collection utilities | .GetOrAdd().RemoveWhere() | | CrashBytes.Strings | String manipulation | .ToSlug().Mask().Truncate() | | CrashBytes.DateTime | Date/time utilities | .AddBusinessDays().Quarter() | | CrashBytes.Enums | Enum metadata | .GetDescription().GetFlags() | | CrashBytes.Http | HttpClient helpers | .WithBearerToken().GetJsonAsync<T>() | | CrashBytes.Configuration | Config validation | .Bind<T>().Validate<T>() |
All packages target .NET 8+ and are available on NuGet. Source code is on GitHub.
Related Reading:
- CrashBytes Open Source โ Full list of CrashBytes open-source projects

