OrionGuard 7.0.0

dotnet add package OrionGuard --version 7.0.0
                    
NuGet\Install-Package OrionGuard -Version 7.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="OrionGuard" Version="7.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="OrionGuard" Version="7.0.0" />
                    
Directory.Packages.props
<PackageReference Include="OrionGuard" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add OrionGuard --version 7.0.0
                    
#r "nuget: OrionGuard, 7.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package OrionGuard@7.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=OrionGuard&version=7.0.0
                    
Install as a Cake Addin
#tool nuget:?package=OrionGuard&version=7.0.0
                    
Install as a Cake Tool

OrionGuard

Guard clauses, object validation, and DDD building blocks in one package, for .NET services that want bad input rejected at the boundary and every problem reported at once.

dotnet add package OrionGuard
using Moongazing.OrionGuard.Core;
using Moongazing.OrionGuard.Extensions;

public static class Registration
{
    public static void Register(string email, string password, int age, string displayName)
    {
        // Throws on the first failure. The parameter name ("email", "age", ...) is captured
        // automatically through CallerArgumentExpression.
        Ensure.That(email).NotNull().NotEmpty().Email();
        Ensure.That(password).NotNull().MinLength(8);
        Ensure.That(age).InRange(18, 120);

        // Span-based guard for hot paths.
        FastGuard.NotNullOrEmpty(displayName, nameof(displayName));

        // Heuristic: rejects input that matches known SQL injection patterns. It is not a
        // defence; the query that uses displayName must still be parameterized.
        displayName.AgainstSqlInjection(nameof(displayName));
    }
}

You get back a GuardException (or one of its subclasses: NullValueException, OutOfRangeException, InvalidEmailException, ...) carrying the parameter name and a message in the calling thread's culture. Nothing is registered, nothing is configured, and no reflection runs on this path.

When you would rather collect the failures than throw, the same rules return a GuardResult that lists every error, carries a severity and an optional HTTP status, and converts to the dictionary shape ValidationProblem expects.

Throw on the first bad value

Ensure.That(value) is the fluent entry point; Guard holds the same checks as plain static methods, and FastGuard is a span-based subset for hot paths.

using Moongazing.OrionGuard.Core;

public static class ThrowingGuards
{
    public static void Check(string sku, decimal price, DateTime shipOn, IReadOnlyList<string> tags)
    {
        Ensure.That(sku).NotNull().Matches("^[A-Z0-9-]+$");
        Ensure.That(price).Positive();
        Ensure.That(shipOn).InFuture();
        Ensure.That(tags).NotEmpty().MaxCount(10);

        // When / Unless gate every rule after them in the chain; Always() re-enables them.
        Ensure.That(sku).When(s => s.Length > 3).MaxLength(32).Always().NotNull();
    }
}

Numeric comparisons (GreaterThan, LessThan, InRange, Positive, NotNegative, NotZero) compare by value across the built-in numeric types, so GreaterThan(0) on a decimal or long behaves the way it reads. A value that cannot be compared with the threshold, and NaN, fail the rule rather than slipping through.

Date guards normalize to UTC before comparing. A DateTimeKind.Unspecified value is treated as UTC; a local value is converted first, so a machine east of UTC does not reject its own DateTime.Now.

Collect every error instead

using Moongazing.OrionGuard.Core;

public static class SignUpValidation
{
    public static Dictionary<string, string[]>? Check(string email, string password)
    {
        GuardResult result = GuardResult.Combine(
            Ensure.Accumulate(email).NotNull().Email().ToResult(),
            Ensure.Accumulate(password).NotNull().MinLength(8).ToResult());

        // e.g. { "email": [...], "password": [...] }, ready for a ValidationProblem response
        return result.IsInvalid ? result.ToErrorDictionary() : null;
    }
}

GuardResult exposes Errors, Warnings and Infos separately (each ValidationError carries a Severity), IsValid / IsInvalid, GetErrorSummary(), ToErrorDictionary() and ThrowIfInvalid(). GuardResult.FailureWithStatus(409, ...) suggests an HTTP status, which Combine and Merge preserve and which the ASP.NET Core filters use for the response code.

Validate an object

using Moongazing.OrionGuard.Core;

public sealed record CreateUser(string Email, string Password, int Age);

public interface IUserRepository
{
    Task<bool> EmailExistsAsync(string email, CancellationToken cancellationToken);
}

public sealed class CreateUserValidation(IUserRepository users)
{
    public Task<GuardResult> ValidateAsync(CreateUser input, CancellationToken cancellationToken) =>
        Validate.For(input)
            .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))
            .MustAsync(
                u => u.Email,
                async (email, ct) => !await users.EmailExistsAsync(email, ct),
                "Email is already registered.",
                "EMAIL_TAKEN")
            .ToResultAsync(cancellationToken);
}

Once a validator has a MustAsync rule you must finish with an async terminal (ToResultAsync, BuildAsync, ThrowIfInvalidAsync); the synchronous ToResult() throws InvalidOperationException rather than quietly skipping the async rules.

Other entry points for the same job:

Entry point Use it for
Validate.For(obj) / Validate.ForStrict(obj) Property rules on one object
Validate.Nested(obj) Deep object graphs and collections
Validate.CrossProperties(obj) Rules that span two properties
Validate.Delta(original, updated) Rules about what changed
Validate.Polymorphic<T>() A different rule set per subtype
AbstractValidator<T> A reusable, injectable validator class
AttributeValidator.Validate(obj) [NotNull], [NotEmpty], [Length], [Email], [Range], [Regex], [Positive] on the model
DynamicValidator.FromJson(json) Rules that arrive at runtime from a database or config
FluentStyleValidator<T> FluentValidation's RuleFor(...) syntax during a migration

AbstractValidator<T> supports named rule sets — declare them with RuleSet("create", ...) and select one with Validate(value, RuleSet.Create).

Any IValidator<T> can be wrapped in a result cache with validator.WithCaching(). Without a key selector, results are cached only for records with compiler-synthesized equality, because those are the only models whose equality is known to cover every field; anything else, including a type with a hand-written IEquatable<T>, runs the inner validator every time. Pass a key when you know what identity means: validator.WithCaching(order => (order.Id, order.Version)). A call carrying a non-empty ValidationContext is never served from the cache.

Register validators with services.AddOrionGuard() plus services.AddValidator<CreateUser, CreateUserValidator>(). ValidatorInvoker.ValidateAsync(serviceProvider, instance) runs every IValidator<T> registered for an object's runtime type and combines the results, returning null when none is registered; that is the shared path the ASP.NET Core, gRPC, SignalR, Hangfire and MassTransit integrations use.

Domain model, rules and events

namespace Shipping;

using Moongazing.OrionGuard.Domain.Events;
using Moongazing.OrionGuard.Domain.Primitives;
using Moongazing.OrionGuard.Domain.Rules;

public sealed record OrderId(Guid Value) : StronglyTypedId<Guid>(Value);

public sealed record OrderShipped(OrderId OrderId) : DomainEventBase;

public sealed class OrderMustBePaid(bool isPaid) : BusinessRule
{
    public override bool IsBroken() => !isPaid;
    public override string DefaultMessage => "An order must be paid before it ships.";
}

public sealed class Order : AggregateRoot<OrderId>
{
    public Order(OrderId id) : base(id) { }

    public bool IsPaid { get; private set; }

    public void MarkPaid() => IsPaid = true;

    public void Ship()
    {
        CheckRule(new OrderMustBePaid(IsPaid)); // throws BusinessRuleValidationException when broken
        RaiseEvent(new OrderShipped(Id));
    }
}

RaiseEvent only records the event. Something has to pull and publish it:

namespace Shipping;

using Microsoft.Extensions.DependencyInjection;
using Moongazing.OrionGuard.DependencyInjection;
using Moongazing.OrionGuard.Domain.Events;

public sealed class SendShippingEmail : IDomainEventHandler<OrderShipped>
{
    public Task HandleAsync(OrderShipped @event, CancellationToken cancellationToken) => Task.CompletedTask;
}

public sealed class ShipOrder(IDomainEventDispatcher dispatcher)
{
    public async Task HandleAsync(Order order, CancellationToken cancellationToken)
    {
        order.Ship();
        // ...persist the order, then publish what it raised:
        await dispatcher.DispatchAsync(order.PullDomainEvents(), cancellationToken);
    }
}

public static class DomainEventSetup
{
    public static IServiceCollection AddShipping(this IServiceCollection services) =>
        services
            .AddOrionGuardDomainEvents()
            .AddOrionGuardDomainEventHandlers(typeof(SendShippingEmail).Assembly)
            .AddScoped<ShipOrder>();
}

AddOrionGuardDomainEvents() takes a dispatch mode: SequentialFailFast (the default — the first handler that throws stops the rest), SequentialContinueOnError, or Parallel. OrionGuard.EntityFrameworkCore does the pull-and-dispatch step for you on SaveChanges, inline or through a transactional outbox.

The rest of the domain surface: Entity<TId> (identity equality), ValueObject and the IValueObject marker, StronglyTypedId<TValue> with IStronglyTypedId<TValue> and the AgainstDefaultStronglyTypedId guard, and BusinessRule / AsyncBusinessRule enforced with CheckRule / CheckRuleAsync inside an entity or Guard.AgainstBrokenRule / AgainstBrokenRuleAsync anywhere else.

Security and format guards

In Moongazing.OrionGuard.Extensions, as extension methods on the value:

  • Injection heuristics — AgainstSqlInjection, AgainstXss, AgainstCommandInjection, AgainstLdapInjection, AgainstXxe, and AgainstInjection (all of them, for free text). Read the limits below before you rely on these.
  • Paths and redirects — AgainstPathTraversal (checked as given, after up to three URL decodes, and after NFKC normalization), AgainstPathEscape(root) (resolves the path and returns it only if it stays inside root), AgainstUnsafeFileName, AgainstOpenRedirect (ASP.NET Core IsLocalUrl rules plus an allow-list of absolute hosts).
  • Files — AgainstDangerousFileExtension, AgainstDisallowedExtension, AgainstMaliciousContent (scans the whole array, or a stream with an explicit maxScanBytes), AgainstFakeMimeType.
  • Secrets and PII — AgainstContainsCreditCardNumber, AgainstContainsSecret, AgainstContainsPii.
  • Formats — latitude/longitude, MAC address, hostname, CIDR, ISO 3166 country code, IANA time zone, BCP 47 language tag, JWT structure, connection string, Base64, credit card (Luhn plus a 12–19 ASCII digit check).
  • International — SWIFT/BIC, ISBN, VIN, EAN, EU VAT number, IMEI.
  • Business — monetary amount, currency code, SKU, coupon code, discount, status transitions, business hours, date ranges.
  • Quotas — AgainstRateLimitExceeded, AgainstSlidingWindowExceeded, AgainstDailyQuotaExceeded: threshold checks over a count you supply.

Moongazing.OrionGuard.Utilities.LdapEncoding has the actual LDAP defence the guards are not: EscapeFilterValue (RFC 4515) and EscapeDistinguishedNameValue (RFC 4514).

Every built-in pattern is a [GeneratedRegex], compiled at build time rather than at runtime: anchored patterns end at \z rather than $ (a trailing newline does not slip through), and [0-9] is used instead of \d so non-ASCII digits are not accepted as numbers.

A pattern you supply is a different path. AgainstRegexMismatch, Ensure.That(...).Matches(pattern) and [Regex("...")] hand the pattern to RegexCache, which calls new Regex(pattern, RegexOptions.Compiled, …) the first time it sees it and keeps the instance. So there is runtime regex construction for custom patterns — once per distinct pattern, not per call. The cache is bounded at RegexCache.MaxCacheSize (default 1000) and evicts the least recently used entry on overflow, so a pattern built per request from user input will churn it rather than grow without limit; prefer a constant pattern, or [GeneratedRegex] of your own.

Matching is bounded at one second either way. In the result-returning APIs a timeout is reported as a validation error; in the throwing APIs it becomes that guard's own exception rather than RegexMatchTimeoutException.

Messages and culture

Messages ship in 14 languages: English, Turkish, German, French, Spanish, Portuguese, Arabic, Japanese, Chinese, Korean, Russian, Dutch, Polish, Italian. With no culture set, messages follow the calling thread's CurrentCulture. Set one per request with ValidationMessages.SetCultureForCurrentScope(culture) (AsyncLocal, so it does not leak between requests), process-wide with SetCulture, or replace the lookup entirely with SetMessageResolver. AddMessages(cultureName, ...) adds or overrides keys.

With the rest of OrionGuard

Package What it adds
OrionGuard.AspNetCore Middleware, Minimal API endpoint filters, MVC action filters, RFC 9457 ProblemDetails
OrionGuard.MediatR Validation pipeline behaviors for requests and streams, and a MediatR-backed event dispatcher
OrionGuard.MassTransit Consume filter that validates a message before the consumer sees it
OrionGuard.Blazor EditForm validation components
OrionGuard.Grpc Server interceptor for unary and streaming calls
OrionGuard.SignalR Hub filter that validates hub method arguments
OrionGuard.Hangfire Rejects an invalid background job at enqueue time
OrionGuard.Swagger Writes OrionGuard attribute constraints into Swashbuckle schemas
OrionGuard.OpenApi Generates a validator from an OpenAPI 3 schema at build time
OrionGuard.SchemaExport Exports a model's attribute rules as JSON Schema or a TypeScript interface
OrionGuard.Generators [GenerateValidator] compile-time validators, no reflection
OrionGuard.OpenTelemetry Metrics and spans for validation and event dispatch
OrionGuard.Aspire builder.AddOrionGuardDefaults(): every OrionGuard meter, activity source and health check in an Aspire app
OrionGuard.EntityFrameworkCore Dispatches domain events on SaveChanges, inline or through a transactional outbox
OrionGuard.Outbox.PostgresNotify Wakes the outbox dispatcher on PostgreSQL LISTEN/NOTIFY
OrionGuard.Outbox.SqlServerBroker Wakes the outbox dispatcher on SQL Server Service Broker
OrionGuard.Outbox.Dashboard Endpoints to list, replay and discard failed outbox messages
OrionGuard.Locks.Redis Redis lease so several outbox dispatchers do not overlap
OrionGuard.Testing Domain-event capture, an in-memory dispatcher, and assertions
OrionGuard.Migration dotnet tool that rewrites FluentValidation validators onto OrionGuard
OrionGuard.Templates dotnet new templates that start a project with this wiring already done

What this does not do

  • The injection guards are not a defence. AgainstSqlInjection, AgainstXss, AgainstCommandInjection, AgainstLdapInjection, AgainstXxe and AgainstInjection are denylists. They miss payloads they do not list, and they reject some ordinary text. Keep the real controls: parameterized queries, contextual output encoding, ProcessStartInfo.ArgumentList without a shell, RFC 4515/4514 escaping (LdapEncoding), and an XmlReader with DTD processing prohibited. Use the guards as a tripwire in front of those, never instead of them.

  • AgainstFakeMimeType only knows file types that have a magic-number signature. .txt, .csv, .html and anything else without one pass whatever their content is. The control for those is an allow-list of extensions (AgainstDisallowedExtension).

  • AgainstInvalidHostname is ASCII-only — pass internationalized names in punycode. That is deliberate: it rejects look-alike hosts such as a Cyrillic exаmple.com.

  • The NFKC step does not survive every runtime, and it fails two different ways. AgainstPathTraversal normalizes with NormalizationForm.FormKC to catch look-alike forms such as the fullwidth ../; AgainstUnsafeFileName and AgainstInjection share that step.

    • Globalization-invariant mode (InvariantGlobalization=true) skips normalization entirely: the string comes back unchanged and IsNormalized always returns true. Nothing throws, so nothing tells you the check weakened — the literal and URL-decoded passes still run while the normalized one quietly stops catching anything.
    • Browser and WASI (the default, non-invariant Blazor WebAssembly build) do the opposite: ASCII input is returned by a fast path and works, and any non-ASCII input throws PlatformNotSupportedException, because browser ICU does not carry the data for FormKC and FormKD. Setting BlazorWebAssemblyLoadAllGlobalizationData does not help: the check is on the platform and the normalization form, not on which ICU shard was loaded. The guards do not swallow it either — they catch only the ArgumentException that ill-formed UTF-16 raises — so it reaches your code.

    Run these guards on the server, which is the side that has to be convinced anyway. The playground demonstrates the browser arm: the fullwidth ../secret.txt sample reports the guard as unavailable rather than as a pass.

  • Attribute and dynamic validation use reflection. AttributeValidator and DynamicValidator read properties at runtime, so they are not NativeAOT- or trimming-safe. OrionGuard.Generators exists for exactly that case.

  • "No runtime regex" holds for the built-in patterns only. A caller-supplied pattern is built through RegexCache with RegexOptions.Compiled, and that option is honoured only where dynamic code compilation is available — under NativeAOT it is silently ignored and the pattern runs on the interpreter. Nothing breaks; you simply do not get the performance the option name promises. If regex throughput is why you chose this package for an AOT build, use the built-in guards or your own [GeneratedRegex], not Matches(pattern).

  • DynamicValidator has a fixed rule vocabulary — NotNull/Required, NotEmpty, Length, MinLength, MaxLength, Range, GreaterThan, LessThan, Regex/Pattern, Email, Url, In, NotIn (matched case-insensitively), each with a WhenProperty/WhenValue condition. There is no way to express a custom predicate in JSON; anything else needs code.

  • FluentStyleValidator<T> is a migration surface, not a FluentValidation clone. It matches FluentValidation on the rules it implements, and OrionGuard.Migration reports rather than rewrites the ones where the semantics differ. It is not a drop-in for the whole FluentValidation API.

  • Nothing here dispatches domain events by itself. RaiseEvent records; you (or OrionGuard.EntityFrameworkCore) must pull and publish.

  • IExceptionFactory never did anything and is obsolete along with ExceptionFactoryProvider, DefaultExceptionFactory and AddOrionGuardExceptionFactory<TFactory>(); no guard has ever called it. They are removed in the next major version. Catch GuardException — or the specific subclass — at your boundary instead.

  • The rate-limit guards do not rate-limit. AgainstRateLimitExceeded(currentCount, maxAllowed, ...), AgainstSlidingWindowExceeded and AgainstDailyQuotaExceeded compare a count you already have against a limit and throw. Counting requests per key over a window is your store's job, or ASP.NET Core's rate-limiting middleware.

Targets

net8.0, net9.0, net10.0. One dependency: Microsoft.Extensions.DependencyInjection.Abstractions.

Documentation

License

MIT. See LICENSE.txt.

Product 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 is compatible.  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 is compatible.  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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (19)

Showing the top 5 NuGet packages that depend on OrionGuard:

Package Downloads
OrionGuard.EntityFrameworkCore

EF Core integration for OrionGuard. Provides a SaveChanges interceptor that dispatches domain events from AggregateRoot<TId> instances after commit (Inline mode) or via a transactional outbox (Outbox mode) consumed by a hosted background worker. Includes W3C trace context propagation across the outbox boundary.

OrionGuard.AspNetCore

ASP.NET Core integration for OrionGuard validation library. Provides middleware, Minimal API endpoint filters, MVC action filters, and RFC 9457 ProblemDetails support.

OrionGuard.Swagger

Swagger/OpenAPI integration for OrionGuard. Auto-generates OpenAPI constraints from OrionGuard validation attributes.

OrionGuard.OpenTelemetry

OpenTelemetry integration for OrionGuard validation library. Provides validation metrics (success/failure rates, latency) and distributed tracing spans.

OrionGuard.Blazor

Blazor integration for OrionGuard validation library. Provides EditForm validation components that run OrionGuard attribute rules or a registered IValidator<TModel>.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
7.0.0 391 9/20/2026
6.7.0 723 7/20/2026 6.7.0 is deprecated because it has critical bugs.
6.6.2 1,518 6/20/2026 6.6.2 is deprecated because it has critical bugs.
6.6.1 1,455 6/20/2026 6.6.1 is deprecated because it has critical bugs.
6.6.0 1,451 6/19/2026 6.6.0 is deprecated because it has critical bugs.
6.5.30 1,948 6/17/2026 6.5.30 is deprecated because it has critical bugs.
6.5.29 2,205 6/15/2026 6.5.29 is deprecated because it has critical bugs.
6.5.28 2,201 6/15/2026 6.5.28 is deprecated because it has critical bugs.
6.5.27 2,201 6/15/2026 6.5.27 is deprecated because it has critical bugs.
6.5.26 2,203 6/13/2026 6.5.26 is deprecated because it has critical bugs.
6.5.25 2,213 6/13/2026 6.5.25 is deprecated because it has critical bugs.
6.5.24 2,196 6/12/2026 6.5.24 is deprecated because it has critical bugs.
6.5.23 2,214 6/12/2026 6.5.23 is deprecated because it has critical bugs.
6.5.22 2,200 6/11/2026 6.5.22 is deprecated because it has critical bugs.
6.5.21 2,208 6/11/2026 6.5.21 is deprecated because it has critical bugs.
6.5.20 2,196 6/11/2026 6.5.20 is deprecated because it has critical bugs.
6.5.19 2,198 6/11/2026 6.5.19 is deprecated because it has critical bugs.
6.5.18 2,192 6/11/2026 6.5.18 is deprecated because it has critical bugs.
6.5.16 2,189 6/11/2026 6.5.16 is deprecated because it has critical bugs.
6.5.15 2,196 6/11/2026 6.5.15 is deprecated because it has critical bugs.
Loading failed

v6.5.5 - Outbox operator mutation surface

NEW: POST /_orion/outbox/{id}/replay clears RetryCount + Error + ProcessedOnUtc so the next dispatcher pass re-attempts the row; returns 404 if the id is unknown.
NEW: POST /_orion/outbox/{id}/discard stamps ProcessedOnUtc=UtcNow without re-dispatch; Error and RetryCount stay intact so later operators see the history. Idempotent for already-processed rows (returns 200 with note).
NEW: OutboxDashboardOptions.OnMutation Func<OutboxMutationEvent, Task> audit hook fires after a successful replay/discard; consumers stamp User.Identity.Name and store wherever they want. Hook does NOT roll back the commit.
NEW: OutboxDashboardOptions.EnableMutations (default true) - set false for read-only mounts; the POST endpoints are simply not registered.

Full changelog: https://github.com/tunahanaliozturk/OrionGuard/blob/master/CHANGELOG.md

---

v6.5.4 - Outbox operator read surface

NEW: Moongazing.OrionGuard.Outbox.Dashboard add-on package. MapOutboxDashboard<TDbContext>() registers a route group; GET /_orion/outbox/failed?page=N&size=M returns paginated failed-message metadata (RetryCount >= threshold AND Error != null - covers both still-failing and dispatcher-dead-lettered rows). Payload deliberately excluded from the projection; error text truncated to ErrorTruncationLength.

CHANGED: Default authorization wiring on the dashboard group falls through to the host's AuthorizationOptions.FallbackPolicy when no AuthorizationPolicyName is supplied, instead of calling RequireAuthorization() unconditionally (which would shadow a stricter fallback policy).

DEFERRED: Replay / discard mutation actions on the dashboard group -> v6.5.5.

Full changelog: https://github.com/tunahanaliozturk/OrionGuard/blob/master/CHANGELOG.md

---

v6.5.1 - Push-Dispatch Contract

NEW: IOutboxWakeSignal abstraction in Moongazing.OrionGuard.EntityFrameworkCore.Outbox.Push. Lets consumers swap the dispatcher's between-batch wait from polling to push without touching the rest of the dispatch loop. Two methods: WaitForNextTickAsync(pollingInterval, ct) and SignalAsync(ct).

NEW: NullOutboxWakeSignal default - polling-only, byte-for-byte v6.5.0 behaviour. ChannelOutboxWakeSignal in-process Channel-backed implementation for single-process deployments and unit tests; repeated SignalAsync calls coalesce into one wake.

CHANGED: DomainEventSaveChangesInterceptor fires SignalAsync on the registered IOutboxWakeSignal after every SaveChanges that runs in Outbox mode, so an opted-in consumer sees mid-poll wakes automatically. Polling interval still upper-bounds wake latency.

CHANGED: OutboxDispatcherHostedService gains an optional 8th IOutboxWakeSignal parameter; positional callers using the previous 6-argument constructor with logger last continue to compile.

DEFERRED: Moongazing.OrionGuard.Outbox.PostgresNotify add-on (Postgres LISTEN/NOTIFY) -> v6.5.2. Moongazing.OrionGuard.Outbox.SqlServerBroker (SQL Server Service Broker) -> v6.5.3. Outbox dead-letter UI -> v6.5.4.

MIGRATION: Source-compatible. No DI changes are required to stay on polling. Opt in to in-process push by registering ChannelOutboxWakeSignal as IOutboxWakeSignal before AddOrionGuardEfCore.

v6.5.0 - Family Integration

NEW PACKAGE: Moongazing.OrionGuard.Locks.Redis - bridges OrionGuard's IDistributedLock primitive to the standalone OrionLock.Redis backend.

Full changelog: https://github.com/tunahanaliozturk/OrionGuard/blob/master/CHANGELOG.md

PRIOR RELEASES (kept for NuGet history readers)

v6.3.0 - Release Notes

NEW: Domain event dispatcher — IDomainEventDispatcher and IDomainEventHandler<TEvent> abstractions. Default ServiceProviderDomainEventDispatcher resolves handlers from IServiceProvider; supports SequentialFailFast (default), SequentialContinueOnError, and Parallel dispatch modes. DI: services.AddOrionGuardDomainEvents() + services.AddOrionGuardDomainEventHandlers(assembly).

NEW: MediatR bridge (OrionGuard.MediatR) — MediatRDomainEventDispatcher delegates to MediatR's IPublisher. Consumer events opt in by adding ': INotification' to their record declaration; the bridge throws InvalidOperationException for events that don't. No wrapper types — handlers stay as natural INotificationHandler<TEvent>. DI: services.AddOrionGuardMediatRDomainEvents() swaps the registered dispatcher.

NEW PACKAGE: OrionGuard.EntityFrameworkCore — DomainEventSaveChangesInterceptor pulls events from tracked IAggregateRoot instances at SavingChangesAsync. Two modes: Inline (default — dispatches post-commit) and Outbox (persists to OutboxMessage rows in the same transaction, dispatched by a hosted background worker). OutboxOptions configures PollingInterval / BatchSize / MaxRetries / TableName. Worker increments RetryCount on failure and dead-letters after MaxRetries. W3C trace context (TraceParent / TraceState) propagates across the outbox boundary so end-to-end traces span the worker. DI: services.AddOrionGuardEfCore<TDbContext>(o => o.UseInline() | o.UseOutbox()).

NEW PACKAGE: OrionGuard.Testing — DomainEventCapture and DomainEventAssertions for fluent unit-test assertions over aggregate-raised events. InMemoryDomainEventDispatcher for integration tests. Framework-agnostic — no xUnit / NUnit / FluentAssertions dependency. Throws DomainEventAssertionException, which any test runner treats as a failure.

NEW: OrionGuard.OpenTelemetry domain-event instrumentation — OrionGuardDomainEventTelemetry exposes ActivitySource and Meter under 'Moongazing.OrionGuard.DomainEvents', with counters orionguard.domain_events.dispatched / failed / outbox.processed / outbox.retries plus the orionguard.domain_events.duration histogram. InstrumentedDomainEventDispatcher decorator opens a span per dispatch and sets ActivityStatusCode.Error on exception. DI: services.WithOpenTelemetryDomainEvents() (call after AddOrionGuardDomainEvents()).

MIGRATION: No breaking changes from v6.2.0. Existing aggregates raise events the same way; nothing dispatches until AddOrionGuardDomainEvents() is wired. MediatR consumers add ', INotification' to their event records (one-line per event). Outbox consumers add an EF Core migration for the OrionGuard_Outbox table.

ROADMAP: v6.4.0 = BusinessRule base class + Guard.Against.BrokenRule + ASP.NET Core ProblemDetails mapping; distributed locking for multi-instance outbox workers; OutboxTypeMapRegistry (alias system for safe type renames); archival job. v6.5+ = push-based outbox dispatch (LISTEN/NOTIFY, SqlDependency); event sourcing primitives.

Full changelog: https://github.com/tunahanaliozturk/OrionGuard/blob/master/CHANGELOG.md

v6.2.0 — Release Notes

NEW: IStronglyTypedId<TValue> marker interface — implemented by both the StronglyTypedId<TValue> abstract record (manual style) AND source-generated readonly partial struct ids. The AgainstDefaultStronglyTypedId guard now accepts this interface as its receiver, so both id styles work with the same guard.

NEW: DomainEventBase abstract record — consumers can write `public sealed record OrderPlaced(OrderId Id) : DomainEventBase;` instead of hand-rolling EventId and OccurredOnUtc. Both properties use init accessors so tests can pin them via `with` expressions.

NEW: Generated strongly-typed ids implement IParsable<TSelf> and ISpanParsable<TSelf> — ASP.NET Core minimal APIs bind them from route/query/form parameters without a custom TypeConverter hop. Standard .NET FormatException semantics on Parse failure.

IMPROVED: The StronglyTypedId source generator now detects whether the consumer project references EF Core and skips emitting the ValueConverter companion when it does not — console apps, Blazor WASM, and class libraries no longer need a spurious EF Core PackageReference just to build.

PACKAGE RENAME: Sub-package NuGet PackageIds drop the 'Moongazing.' brand prefix — install as OrionGuard.AspNetCore, OrionGuard.Blazor, OrionGuard.Generators, OrionGuard.Grpc, OrionGuard.MediatR, OrionGuard.OpenTelemetry, OrionGuard.SignalR, OrionGuard.Swagger. The old Moongazing.OrionGuard.* package IDs remain on NuGet.org for v6.1.0 and earlier; v6.2.0 ships under the new names. C# namespaces are unchanged (using Moongazing.OrionGuard.AspNetCore; still works) so source code does not break.

MIGRATION: Source-compatible with v6.1.0 — no user code change required. Receiver and return type of AgainstDefaultStronglyTypedId widened to IStronglyTypedId<TValue> (manual records still implement this interface). Recompile recommended. Consumers should update their package references from Moongazing.OrionGuard.* to OrionGuard.* (same assemblies, same namespaces, new package IDs).

ROADMAP: v6.3.0 = domain event dispatcher + MediatR bridge + EF Core SaveChanges interceptor. v6.4.0 = full BusinessRule base class + Guard.Against.BrokenRule + AspNetCore ProblemDetails mapping.

Full changelog: https://github.com/tunahanaliozturk/OrionGuard/blob/master/CHANGELOG.md

v6.1.0 — Release Notes

NEW: DDD Domain Primitives — ValueObject (hybrid: abstract base + IValueObject marker for records), Entity<TId> with identity equality and CheckRule / CheckRuleAsync helpers, AggregateRoot<TId> with IAggregateRoot marker and PullDomainEvents() dispatching buffer.

NEW: StronglyTypedId<TValue> — abstract record base for manual ids, plus [StronglyTypedId<TValue>] incremental source generator (Moongazing.OrionGuard.Generators) that emits the partial struct body, EF Core ValueConverter, System.Text.Json JsonConverter, and TypeConverter for Guid, int, long, string, and Ulid (net9.0+).

NEW: AgainstDefaultStronglyTypedId — extension method that throws when a strongly-typed id is null or wraps the default of its underlying type.

NEW: services.AddOrionGuardStronglyTypedIds() — scans assemblies for generated EF Core converters and registers them.

NEW: IDomainEvent, IBusinessRule, IAsyncBusinessRule, BusinessRuleValidationException, DomainInvariantException — abstractions landing in v6.1.0 so primitives are immediately usable; full base classes and dispatcher arrive in v6.2.0 and v6.3.0.

NEW: Localization — DefaultStronglyTypedId, BusinessRuleBroken, DomainInvariantViolated keys added for all 14 bundled languages.

Full changelog: https://github.com/tunahanaliozturk/OrionGuard/blob/master/CHANGELOG.md

v6.0.0 — Release Notes

NEW: GeneratedRegex Migration — All 20+ regex patterns migrated to .NET 8+ [GeneratedRegex] source-generated code. Zero runtime compilation, NativeAOT compatible.

NEW: 14-Language Localization — Added Chinese (zh), Korean (ko), Russian (ru), Dutch (nl), Polish (pl). Completed all 30 message keys for German, French, Spanish, Portuguese, Arabic, Japanese.

NEW: Rate Limit Guards — AgainstRateLimitExceeded(), AgainstTooManyRequests(), AgainstSlidingWindowExceeded(), AgainstConcurrentLimitExceeded(), AgainstDailyQuotaExceeded().

NEW: IRequestValidator interface — Pipeline-ready validator interface for ASP.NET Core and MediatR integration.

NEW: GuardResult.SuggestedHttpStatusCode — HTTP status code hints for ProblemDetails responses.

NEW PACKAGE: Moongazing.OrionGuard.AspNetCore — Middleware, Minimal API endpoint filters (.WithValidation), MVC filters, RFC 9457 ProblemDetails, IExceptionHandler.

NEW PACKAGE: Moongazing.OrionGuard.MediatR — ValidationBehavior pipeline behavior for automatic CQRS request validation.

NEW PACKAGE: Moongazing.OrionGuard.Generators — Compile-time source generator for reflection-free NativeAOT validation. Roslyn analyzers included.

NEW PACKAGE: Moongazing.OrionGuard.Swagger — Auto-generate OpenAPI constraints from OrionGuard validation attributes.

NEW PACKAGE: Moongazing.OrionGuard.OpenTelemetry — Validation metrics and distributed tracing spans.

DEPRECATED: RegexPatterns class — Use GeneratedRegexPatterns instead.

Full changelog: https://github.com/tunahanaliozturk/OrionGuard/blob/master/CHANGELOG.md

v5.0.1 — Previous Release Notes

NEW: Security Guards — SQL injection, XSS, path traversal, command injection, LDAP injection, XXE detection, unsafe filename, and open redirect validation. All patterns use FrozenSet for O(1) lookups.

NEW: Format Guards (replaces TurkishGuards) — Geographic coordinates, MAC address, hostname (RFC 1123), CIDR notation, ISO 3166-1 country codes, IANA time zones, BCP 47 language tags, JWT structure, connection strings, and Base64 validation.

NEW: ThrowHelper Pattern — All hot-path guards delegate throwing to a centralized ThrowHelper with [DoesNotReturn] and [StackTraceHidden] for smaller JIT-compiled method bodies and cleaner stack traces.

NEW: Span-Based FastGuard — Email, ASCII, AlphaNumeric, NumericString, MaxLength, ValidGuid, and Finite validators using ReadOnlySpan with zero allocations.

IMPROVED: Thread-Safe Localization — Rewritten with ConcurrentDictionary and AsyncLocal. Now supports 8 languages: EN, TR, DE, FR, ES, PT, AR, JA. Per-request culture scoping via SetCultureForCurrentScope.

IMPROVED: ObjectValidator — Compiled expression caching, CrossProperty validation, conditional When() blocks.

IMPROVED: FluentGuard — Transform() and Default() pipeline methods. All date comparisons use DateTime.UtcNow.

IMPROVED: All exceptions are sealed with ErrorCode and ParameterName properties.

IMPROVED: RegexCache with bounded size (1000), FrozenSet for BusinessGuards currency codes, ICollection.Count optimization in CollectionGuards.

FIXED: AgainstNotAllLowercase was comparing string to itself (always passed).
FIXED: AgainstEmptyCollection was throwing EmptyStringException instead of NullValueException.
FIXED: GuardBuilderExtensions was passing .Value instead of .ParameterName.

BREAKING: Validate.Object renamed to Validate.For. FastGuard.Guid renamed to FastGuard.ValidGuid. TurkishGuards removed.

Full changelog: https://github.com/tunahanaliozturk/OrionGuard/blob/master/CHANGELOG.md