DEV Community

Cover image for Stop Writing the Same Null Checks Over and Over Again. There Is a Better Way.
Tunahan Ali Ozturk
Tunahan Ali Ozturk

Posted on

Stop Writing the Same Null Checks Over and Over Again. There Is a Better Way.

Every .NET developer has written code like this at some point:

public void ProcessOrder(Order order, string email, decimal amount)
{
    if (order == null)
        throw new ArgumentNullException(nameof(order));
    if (string.IsNullOrEmpty(email))
        throw new ArgumentException("Email cannot be empty", nameof(email));
    if (amount <= 0)
        throw new ArgumentOutOfRangeException(nameof(amount), "Amount must be positive");
    if (!Regex.IsMatch(email, @"^[^@]+@[^@]+\.[^@]+$"))
        throw new ArgumentException("Email is not valid", nameof(email));
    // ... now the actual logic starts
}
Enter fullscreen mode Exit fullscreen mode

Four guards. Four throw statements. Zero business logic. And this is just a simple method. In a real-world application with dozens of services, commands, and API endpoints, this kind of boilerplate compounds fast — cluttering your methods, burying intent, and making code harder to read and maintain.

OrionGuard is a lightweight, fluent guard clause library for .NET that eliminates this noise entirely. It provides a clean, expressive API for input validation, security guards, domain guards, async validation, result accumulation, and much more — all in a single NuGet package.


What Is OrionGuard?

OrionGuard is a production-grade validation and guard library for .NET that covers the full spectrum of input validation needs:

  • Fluent API for synchronous and asynchronous validation
  • Security guards against injection attacks (SQL, XSS, path traversal, command injection)
  • Universal format guards (coordinates, MAC, CIDR, JWT, country codes, time zones)
  • ThrowHelper-optimized hot paths for maximum JIT performance
  • Span-based zero-allocation validation
  • Result pattern for collecting multiple errors without throwing
  • Conditional validation (when/unless)
  • Object-level validation with property expressions and compiled expression caching
  • Business domain guards (money, e-commerce, scheduling)
  • High-performance guards optimized for hot paths
  • Attribute-based validation
  • Dependency injection support
  • Thread-safe localization for 8 languages

It is available on NuGet as OrionGuard and targets .NET 8.0 and .NET 9.0.


Installation

dotnet add package OrionGuard
Enter fullscreen mode Exit fullscreen mode

That is all. No configuration required to get started.


The Fluent API: Ensure.That()

The primary entry point is the Ensure.That() API. It captures the parameter name automatically via CallerArgumentExpression, meaning you do not have to pass nameof() manually.

using Moongazing.OrionGuard.Core;

Ensure.That(email).NotNull().NotEmpty().Email();
Ensure.That(password).NotNull().MinLength(8);
Ensure.That(age).NotNegative().InRange(0, 150);
Enter fullscreen mode Exit fullscreen mode

Each call throws immediately on the first failed validation — ideal for guard clauses at the top of methods or constructors. The fluent chain reads almost like a sentence, making intent immediately clear to anyone reading the code.

For quick inline checks that also return the validated value, shorthand methods are available:

string validEmail = Ensure.NotNull(userInput.Email);
string validName  = Ensure.NotNullOrEmpty(userInput.Name);
int    validAge   = Ensure.InRange(userInput.Age, 18, 120);
Enter fullscreen mode Exit fullscreen mode

The full list of available validations covers strings, numerics, collections, and dates — including Email(), Url(), Matches(pattern), GreaterThan(), InRange(), InPast(), InFuture(), MinCount(), NoNullItems(), and custom predicates via Must().


Security Guards

New in v5.0. One of the most critical additions in this release. OrionGuard now provides built-in protection against the most common web application attack vectors. All pattern sets are stored in FrozenSet<string> for O(1) lookup performance.

SQL Injection Protection

userInput.AgainstSqlInjection(nameof(userInput));
Enter fullscreen mode Exit fullscreen mode

Detects 28 common SQL injection patterns including keywords (SELECT, DROP, UNION, EXEC), comment sequences (--, /*), system objects (INFORMATION_SCHEMA, sysobjects), and function calls (CHAR(, CAST(, CONVERT().

Cross-Site Scripting (XSS) Protection

userInput.AgainstXss(nameof(userInput));
Enter fullscreen mode Exit fullscreen mode

Detects 28 XSS vectors including <script> tags, JavaScript URI schemes, event handlers (onload=, onerror=, onclick=), embedded objects (<iframe>, <embed>), and DOM manipulation methods (document.cookie, document.write, innerHTML).

Path Traversal Protection

filePath.AgainstPathTraversal(nameof(filePath));
Enter fullscreen mode Exit fullscreen mode

Catches directory traversal sequences (../, ..\), URL-encoded variants (%2e%2e%2f, %2e%2e%5c), and known sensitive paths (/etc/passwd, C:\Windows).

Command Injection Protection

command.AgainstCommandInjection(nameof(command));
Enter fullscreen mode Exit fullscreen mode

Blocks shell metacharacters (|, &&, ;, backtick), subshell operators ($(), and known interpreter paths (/bin/sh, cmd.exe, powershell).

Additional Security Guards

// LDAP injection — span-based detection of LDAP-special characters
ldapFilter.AgainstLdapInjection(nameof(ldapFilter));

// XXE attacks — detects DOCTYPE and ENTITY declarations
xmlInput.AgainstXxe(nameof(xmlInput));

// Unsafe filenames — validates against traversal and invalid OS characters
fileName.AgainstUnsafeFileName(nameof(fileName));

// Open redirects — validates redirect URLs against trusted domain allow-list
redirectUrl.AgainstOpenRedirect(nameof(redirectUrl), trustedDomains);
Enter fullscreen mode Exit fullscreen mode

Combined Check

If you want to run all common security checks in a single call:

userInput.AgainstInjection(nameof(userInput));
Enter fullscreen mode Exit fullscreen mode

This runs SQL injection, XSS, path traversal, and command injection detection in one method.


Format Guards

New in v5.0. Universal format validation guards that cover internationally applicable data formats. These replace the previous country-specific validators with a truly global approach.

Geographic Coordinates

latitude.AgainstInvalidLatitude(nameof(latitude));     // -90 to 90
longitude.AgainstInvalidLongitude(nameof(longitude));   // -180 to 180

// Or validate both at once
FormatGuards.AgainstInvalidCoordinates(lat, lng, "location");
Enter fullscreen mode Exit fullscreen mode

Network Formats

"AA:BB:CC:DD:EE:FF".AgainstInvalidMacAddress(nameof(mac));
"api.example.com".AgainstInvalidHostname(nameof(host));
"192.168.1.0/24".AgainstInvalidCidr(nameof(cidr));
Enter fullscreen mode Exit fullscreen mode

Hostname validation follows RFC 1123 rules including label length limits and valid character sets. CIDR validation supports both IPv4 and IPv6 notation.

International Standards

"US".AgainstInvalidCountryCode(nameof(country));          // ISO 3166-1 alpha-2 (249 codes)
"Europe/Istanbul".AgainstInvalidTimeZoneId(nameof(tz));   // IANA/Windows time zone database
"en-US".AgainstInvalidLanguageTag(nameof(lang));          // BCP 47 / IETF language tags
Enter fullscreen mode Exit fullscreen mode

Token and Encoding Formats

token.AgainstInvalidJwtFormat(nameof(token));              // Three Base64URL segments
connStr.AgainstInvalidConnectionString(nameof(connStr));   // Key=value pair format
encoded.AgainstInvalidBase64String(nameof(encoded));       // Base64 structure and padding
Enter fullscreen mode Exit fullscreen mode

ThrowHelper Pattern

New in v5.0. All hot-path guard methods now delegate exception throwing to a centralized ThrowHelper static class. This is not just an internal refactor — it has a direct impact on your application's runtime performance.

Why It Matters

When the JIT compiler encounters a throw statement inside a method, it marks that method as less likely to be inlined. By moving all throw logic into a separate static class annotated with [DoesNotReturn] and [StackTraceHidden], the actual guard methods stay small enough for the JIT to inline aggressively.

// What the JIT sees — a tiny method it can inline
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void NotNull<T>(T value, string parameterName)
{
    if (value is null)
        ThrowHelper.ThrowNullValue(parameterName);
}
Enter fullscreen mode Exit fullscreen mode

The result: your validation checks on the happy path have near-zero overhead. The throw path is cold code that only executes when validation actually fails — and even then, [StackTraceHidden] produces cleaner stack traces by hiding framework-internal frames.


Span-Based FastGuard

Enhanced in v5.0. For methods in hot paths — tight loops, high-throughput pipelines, middleware — FastGuard provides aggressively inlined guards with zero unnecessary overhead.

FastGuard.NotNull(user, nameof(user));
FastGuard.NotNullOrEmpty(email, nameof(email));
FastGuard.InRange(age, 0, 150, nameof(age));
FastGuard.Positive(quantity, nameof(quantity));
Enter fullscreen mode Exit fullscreen mode

New Span-Based Methods (v5.0)

These methods operate on ReadOnlySpan<char> for zero-allocation validation:

FastGuard.Email(email, nameof(email));                        // Span-based, no regex
FastGuard.Ascii(input.AsSpan(), nameof(input));               // ASCII range check
FastGuard.AlphaNumeric(code.AsSpan(), nameof(code));          // Letters and digits only
FastGuard.NumericString(digits.AsSpan(), nameof(digits));     // Digits only
FastGuard.MaxLength(name.AsSpan(), 100, nameof(name));        // Length check on span
FastGuard.ValidGuid(id.AsSpan(), nameof(id));                 // GUID format + non-empty
FastGuard.Finite(price, nameof(price));                       // Rejects NaN and Infinity
Enter fullscreen mode Exit fullscreen mode

FastGuard.Email validates email format using pure span parsing — no regex allocation, no string splitting. For high-throughput scenarios like API gateways or message processors, this can make a measurable difference.

Regex patterns used in other guards are compiled and cached automatically through RegexCache (bounded at 1,000 entries), so repeated validations against the same pattern incur no recompilation cost.


Result Pattern: Collect All Errors First

When you are validating user input — say, a registration form — throwing on the first error is a poor user experience. Users should see all validation problems at once, not one at a time.

OrionGuard solves this with the GuardResult pattern:

var result = GuardResult.Combine(
    Ensure.Accumulate(email,    "Email")   .NotNull().Email()        .ToResult(),
    Ensure.Accumulate(password, "Password").MinLength(8)             .ToResult(),
    Ensure.Accumulate(age,      "Age")     .InRange(18, 120)         .ToResult()
);

if (result.IsInvalid)
{
    foreach (var error in result.Errors)
    {
        Console.WriteLine($"[{error.ParameterName}] {error.Message} (Code: {error.ErrorCode})");
    }
}
Enter fullscreen mode Exit fullscreen mode

For API responses, a single call converts all errors into a dictionary suitable for returning as a BadRequest body:

Dictionary<string, string[]> errors = result.ToErrorDictionary();
// {
//   "Email":    ["Email must be a valid email address."],
//   "Password": ["Password must be at least 8 characters."]
// }
Enter fullscreen mode Exit fullscreen mode

GuardResult also supports ThrowIfInvalid() to throw an AggregateValidationException when you want to validate everything and then fail atomically.


Async Validation

Some validations are inherently asynchronous — checking whether an email is already registered, or verifying a value against an external service. OrionGuard provides first-class async support:

var result = await EnsureAsync.That(email, "Email")
    .UniqueAsync(async e => await db.IsEmailUniqueAsync(e))
    .ExistsAsync(async e => await emailService.IsDeliverableAsync(e))
    .MustAsync(async e => await IsNotBlacklistedAsync(e), "Email is blacklisted")
    .ValidateAsync();
Enter fullscreen mode Exit fullscreen mode

Multiple async validators can be combined in parallel:

var result = await EnsureAsync.AllAsync(
    userValidator.ValidateAsync(user),
    addressValidator.ValidateAsync(address),
    paymentValidator.ValidateAsync(payment)
);
Enter fullscreen mode Exit fullscreen mode

Conditional Validation

Not every field always needs to be validated. OrionGuard supports When and Unless conditions to apply guards only when certain criteria are met:

Ensure.That(alternateEmail)
    .When(isPrimaryEmailInvalid)
    .NotNull()
    .Email()
    .Unless(isGuestUser)
    .MinLength(5);
Enter fullscreen mode Exit fullscreen mode

Conditions can also be expressed as lambdas against the value being validated, making them composable and reusable.


Object Validation

When you want to validate an entire DTO in one place, the Validate.For() API provides property-level validation with expression trees:

var result = Validate.For(userDto)
    .Property(u => u.Email,    g => g.NotNull().Email())
    .Property(u => u.Password, g => g.NotNull().MinLength(8))
    .Property(u => u.Age,      g => g.InRange(18, 120))
    .NotNull(u => u.CreatedAt)
    .NotEmpty(u => u.Username)
    .Must(u => u.Role, r => ValidRoles.Contains(r), "Invalid role")
    .ToResult();
Enter fullscreen mode Exit fullscreen mode

New in v5.0: Cross-Property Validation

Validate rules that depend on multiple properties:

var result = Validate.For(order)
    .Property(o => o.StartDate, g => g.NotNull())
    .Property(o => o.EndDate, g => g.NotNull())
    .CrossProperty<DateTime, DateTime>(
        o => o.StartDate, o => o.EndDate,
        (start, end) => start < end,
        "StartDate must be before EndDate")
    .When(order.IsExpress, v =>
        v.Property(o => o.Priority, g => g.InRange(1, 3)))
    .ToResult();
Enter fullscreen mode Exit fullscreen mode

Compiled Expression Caching

Property accessors are now compiled once via Expression.Lambda and stored in a ConcurrentDictionary. After the first validation of a type, subsequent validations use cached compiled delegates instead of reflection — eliminating repeated compilation overhead entirely.


FluentGuard Enhancements

New in v5.0. The fluent pipeline now supports in-place transformations and default values:

// Transform: trim and lowercase before validating
var email = Ensure.That(rawEmail)
    .Transform(e => e.Trim().ToLowerInvariant())
    .NotEmpty()
    .Email()
    .Value;

// Default: replace null with a fallback value
var role = Ensure.That(userRole)
    .Default("User")
    .NotEmpty()
    .Value;
Enter fullscreen mode Exit fullscreen mode

All date comparisons across the library now use DateTime.UtcNow instead of DateTime.Now, avoiding subtle time zone bugs in server environments.


Sealed Exceptions with Structured Data

New in v5.0. All custom exception classes are now sealed and include two new properties for programmatic error handling:

  • ErrorCode — A machine-readable string (e.g., "NULL_VALUE", "INVALID_EMAIL", "SQL_INJECTION")
  • ParameterName — The name of the parameter that failed validation
try
{
    Guard.AgainstNull(value, nameof(value));
}
catch (GuardException ex)
{
    // ex.ErrorCode = "NULL_VALUE"
    // ex.ParameterName = "value"
    logger.LogWarning(
        "Validation failed: {ErrorCode} on {Param}",
        ex.ErrorCode,
        ex.ParameterName);
}
Enter fullscreen mode Exit fullscreen mode

This makes it straightforward to build structured error responses, telemetry, and alerting around validation failures.


Business Domain Guards

One of OrionGuard's most distinctive features is its library of domain-specific guards that go beyond primitive validation. These cover real-world business scenarios that you would otherwise have to implement yourself.

Financial

price.AgainstInvalidMonetaryAmount("amount", maxDecimalPlaces: 2);
"USD".AgainstInvalidCurrencyCode("currency");
discount.AgainstInvalidPercentage("discount", allowOver100: false);
Enter fullscreen mode Exit fullscreen mode

E-commerce

quantity.AgainstInvalidQuantity("qty", minQuantity: 1);
"PROD-12345-XL".AgainstInvalidSku("sku");
total.AgainstOrderBelowMinimum(50m, "orderTotal");
"SUMMER2025".AgainstInvalidCouponCode("coupon");
Enter fullscreen mode Exit fullscreen mode

Scheduling and Availability

orderDate.AgainstWeekend("deliveryDate");
appointmentTime.AgainstOutsideBusinessHours("time", startHour: 9, endHour: 17);
(startDate, endDate).AgainstInvalidDateRange("period");
Enter fullscreen mode Exit fullscreen mode

User Accounts

age.AgainstInvalidAccountAge("age", minAge: 13, maxAge: 120);
role.AgainstInvalidRole("role", "User", "Admin", "Moderator");
currentStatus.AgainstInvalidStatusTransition(newStatus, validTransitions, "status");
Enter fullscreen mode Exit fullscreen mode

These guards encapsulate business rules that would otherwise scatter across your domain layer as custom validation logic. By centralizing them here, they become testable, consistent, and reusable.


Advanced Validators

OrionGuard also includes validators for data formats and identification standards that are difficult to validate correctly from scratch.

Financial Identifiers

"4111111111111111".AgainstInvalidCreditCard("cardNumber");   // Luhn algorithm
"DE89370400440532013000".AgainstInvalidIban("iban");
Enter fullscreen mode Exit fullscreen mode

Data Formats

"{\"name\": \"test\"}".AgainstInvalidJson("jsonData");
"<root><item/></root>".AgainstInvalidXml("xmlData");
"SGVsbG8gV29ybGQ=".AgainstInvalidBase64("encoded");
Enter fullscreen mode Exit fullscreen mode

Communication

"+905551234567".AgainstInvalidPhoneNumber("phone");
"my-article-title".AgainstInvalidSlug("slug");
"1.2.3-beta.1+build.456".AgainstInvalidSemVer("version");
Enter fullscreen mode Exit fullscreen mode

Attribute-Based Validation

For teams that prefer a declarative approach, OrionGuard provides validation attributes that can be applied directly to model properties:

public class CreateUserRequest
{
    [NotNull]
    [Email(ErrorMessage = "Please provide a valid email")]
    public string Email { get; set; }

    [NotEmpty]
    [Length(8, 128, ErrorCode = "PASS_LENGTH")]
    public string Password { get; set; }

    [Range(18, 120)]
    [Positive]
    public int Age { get; set; }

    [Regex(@"^\+?[1-9]\d{1,14}$", ErrorMessage = "Invalid phone format")]
    public string? Phone { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Validation is then triggered with:

var result = AttributeValidator.Validate(request);
if (result.IsInvalid)
    return BadRequest(result.ToErrorDictionary());
Enter fullscreen mode Exit fullscreen mode

Dependency Injection

OrionGuard integrates cleanly with ASP.NET Core's dependency injection container. Registration is a single line:

services.AddOrionGuard();
services.AddValidator<CreateUserRequest, CreateUserRequestValidator>();
Enter fullscreen mode Exit fullscreen mode

Validators are strongly typed and support constructor injection:

public class CreateUserRequestValidator : AbstractValidator<CreateUserRequest>
{
    public CreateUserRequestValidator(IUserRepository userRepo)
    {
        RuleFor(x => x.Email, "Email",
            v => v.NotNull().NotEmpty().Email());
        RuleFor(x => x.Password, "Password",
            v => v.NotNull().MinLength(8));
        RuleForAsync(
            async x => await userRepo.IsEmailUniqueAsync(x.Email),
            "Email already registered",
            "Email");
    }
}
Enter fullscreen mode Exit fullscreen mode

In a controller:

public class UsersController : ControllerBase
{
    private readonly IValidator<CreateUserRequest> _validator;

    public UsersController(IValidator<CreateUserRequest> validator)
        => _validator = validator;

    [HttpPost]
    public async Task<IActionResult> Create(CreateUserRequest request)
    {
        var result = await _validator.ValidateAsync(request);
        if (result.IsInvalid)
            return BadRequest(result.ToErrorDictionary());

        return Ok();
    }
}
Enter fullscreen mode Exit fullscreen mode

Thread-Safe Localization

Enhanced in v5.0. OrionGuard ships with built-in support for 8 languages: English, Turkish, German, French, Spanish, Portuguese, Arabic, and Japanese. Each language includes 30+ message keys.

The localization system has been rewritten with ConcurrentDictionary and AsyncLocal<CultureInfo> for safe per-request culture scoping in multi-threaded server environments:

// Set culture for the current async scope — does not affect other threads
ValidationMessages.SetCultureForCurrentScope(new CultureInfo("de"));

var message = ValidationMessages.Get("NotNull", "Email");
// German: "Email darf nicht null sein."
Enter fullscreen mode Exit fullscreen mode

Global culture setting is still available:

ValidationMessages.SetCulture("tr");
Enter fullscreen mode Exit fullscreen mode

Custom translations for any language can be registered:

ValidationMessages.AddMessages("es", new Dictionary<string, string>
{
    ["NotNull"]   = "{0} no puede ser nulo.",
    ["NotEmpty"]  = "{0} no puede estar vacio.",
    ["Email"]     = "{0} debe ser una direccion de correo valida.",
    ["MinLength"] = "{0} debe tener al menos {1} caracteres.",
    ["InRange"]   = "{0} debe estar entre {1} y {2}."
});
Enter fullscreen mode Exit fullscreen mode

Missing entries automatically fall back to English, so partial translations are safe to deploy.

For projects that already use ASP.NET Core's IStringLocalizer, a custom resolver can bridge the two systems:

ValidationMessages.SetMessageResolver(
    (key, culture) => _localizer[key].Value);
Enter fullscreen mode Exit fullscreen mode

Common Profiles

For the most frequently occurring validation scenarios, OrionGuard ships with pre-built profiles that bundle sensible defaults:

// Authentication
var emailResult    = CommonProfiles.Email("user@example.com");
var passwordResult = CommonProfiles.Password("SecureP@ss1",
    requireUppercase: true,
    requireDigit: true,
    requireSpecialChar: true);
var usernameResult = CommonProfiles.Username("john_doe",
    minLength: 3, maxLength: 30);

// Personal information
var nameResult      = CommonProfiles.PersonName("John Doe");
var ageResult       = CommonProfiles.Age(25, minAge: 18, maxAge: 120);
var birthDateResult = CommonProfiles.BirthDate(birthDate, minAge: 18);

// Financial
var amountResult  = CommonProfiles.MonetaryAmount(99.99m, min: 0, max: 10000);
var percentResult = CommonProfiles.Percentage(15.5m);

// Identifiers
var guidResult = CommonProfiles.GuidId(userId);
var slugResult = CommonProfiles.Slug("my-article-title");

// Collections
var listResult = CommonProfiles.NonEmptyList(items);
Enter fullscreen mode Exit fullscreen mode

Performance at a Glance

OrionGuard v5.0 includes several performance optimizations that make a real difference in production workloads:

  • ThrowHelper pattern — Smaller JIT-compiled method bodies, better inlining on the happy path
  • FrozenSet for pattern matching — O(1) lookups for security and business guard patterns
  • Compiled expression caching — No repeated Expression.Compile() in ObjectValidator
  • RegexCache (bounded at 1,000) — No regex recompilation on repeated validations
  • ReadOnlySpan FastGuard methods — Zero-allocation validation on hot paths
  • ICollection.Count before enumeration — No unnecessary LINQ allocation in collection guards
  • Explicit StringComparison on all string ops — No culture-sensitive overhead in validation logic

Why OrionGuard Instead of FluentValidation?

This is a fair question. FluentValidation is a mature and widely-used library. The two libraries serve overlapping but distinct purposes.

OrionGuard is optimized for guard clauses and inline validation. It is designed to be used at method boundaries, in domain constructors, and in tight validation chains — scenarios where FluentValidation's class-based validator structure would be overkill.

OrionGuard includes domain-specific guards that FluentValidation does not. Credit card validation (Luhn), IBAN checking, business hours enforcement, SKU validation, coupon code validation, status transition guards — these live in OrionGuard out of the box. With FluentValidation, you would implement these yourself.

OrionGuard provides security guards. SQL injection, XSS, path traversal, command injection, LDAP injection, and XXE detection are built into the library. FluentValidation has no equivalent.

OrionGuard provides high-performance paths with FastGuard. When you need guards in hot-path code with zero allocations and aggressive inlining, that is a first-class concern in OrionGuard.

The two libraries can also coexist. OrionGuard's guard clause style pairs naturally with FluentValidation's class-based validators for complex cross-property rules.


Quick Reference

Here is the full API surface at a glance:

// Throw-on-fail guard
Ensure.That(email).NotNull().Email();

// Collect all errors
var result = Ensure.Accumulate(email, "Email").Email().ToResult();

// Async validation
await EnsureAsync.That(email, "Email")
    .UniqueAsync(checkUnique).ValidateAsync();

// Conditional validation
Ensure.That(value).When(condition).NotNull();

// Object validation with cross-property rules
Validate.For(dto)
    .Property(x => x.Email, g => g.Email())
    .CrossProperty<DateTime, DateTime>(
        x => x.Start, x => x.End,
        (s, e) => s < e, "msg")
    .ToResult();

// Design-by-contract
Contract.Requires(value > 0, "Must be positive");

// High-performance path (span-based)
FastGuard.NotNull(value, nameof(value));
FastGuard.Email(email, nameof(email));
FastGuard.Ascii(input.AsSpan(), nameof(input));

// Security guards
userInput.AgainstInjection(nameof(userInput));
userInput.AgainstSqlInjection(nameof(userInput));
userInput.AgainstXss(nameof(userInput));
filePath.AgainstPathTraversal(nameof(filePath));

// Format guards
"US".AgainstInvalidCountryCode(nameof(country));
"Europe/Istanbul".AgainstInvalidTimeZoneId(nameof(tz));
token.AgainstInvalidJwtFormat(nameof(token));

// Fluent transforms
Ensure.That(raw)
    .Transform(s => s.Trim())
    .Default("fallback")
    .NotEmpty();

// OR logic
value.EitherOr("value")
    .Or(v => test1(v), "msg1")
    .Or(v => test2(v), "msg2")
    .Validate();

// Attribute-based
[NotNull, Email] public string Email { get; set; }

// Pre-built profiles
CommonProfiles.Email("user@example.com");
Enter fullscreen mode Exit fullscreen mode

Getting Started

dotnet add package OrionGuard
Enter fullscreen mode Exit fullscreen mode

GitHub: github.com/Moongazing/OrionGuard

NuGet: nuget.org/packages/OrionGuard

Changelog: github.com/Moongazing/OrionGuard/blob/master/CHANGELOG.md


Validation boilerplate is one of those things that quietly accumulates until it is everywhere. OrionGuard gives you a consistent, expressive, and well-structured way to handle it — from simple null checks all the way up to injection attack detection, async database lookups, and business rule enforcement. It is the kind of library that makes your code smaller, clearer, and more honest about what it is actually doing.

Give it a try. Your methods will thank you.


Written by Tunahan Ali Ozturk — .NET developer, open source contributor, and creator of OrionGuard.

Top comments (0)