En-Garde.Testing 1.3.3

dotnet add package En-Garde.Testing --version 1.3.3
                    
NuGet\Install-Package En-Garde.Testing -Version 1.3.3
                    
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="En-Garde.Testing" Version="1.3.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="En-Garde.Testing" Version="1.3.3" />
                    
Directory.Packages.props
<PackageReference Include="En-Garde.Testing" />
                    
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 En-Garde.Testing --version 1.3.3
                    
#r "nuget: En-Garde.Testing, 1.3.3"
                    
#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 En-Garde.Testing@1.3.3
                    
#: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=En-Garde.Testing&version=1.3.3
                    
Install as a Cake Addin
#tool nuget:?package=En-Garde.Testing&version=1.3.3
                    
Install as a Cake Tool

Engarde

Feature-flag guards for methods, constructors, types, properties, fields and parameters, plus generated injectable queries for inline checks.

dotnet add package En-Garde

Examples

[Engarde("new-experience-enabled")]
public sealed class MyService
{
    [MyCustomGuardThatThrows("beta-testers")]
    public Task NewMethod();

    [AllowEditingNameGuard]
    public string? Name { get; set; }

    public void Update(string field, string value, [Engarde("version-update-allowed")] int version) { }
}

Method fallbacks

public sealed class Checkout
{
    [Engarde(nameof(PlaceOrderWithoutDiscount), typeof(Checkout), "discount-checkout")]
    public Task<Order> PlaceOrderAsync(Cart cart) => PlaceDiscountedOrderAsync(cart);

    private Task<Order> PlaceOrderWithoutDiscount(Cart cart) => PlaceRegularOrderAsync(cart);
}

DI-backed type fallbacks

[Engarde(typeof(LegacyWeatherService), "new-weather-service")]
public interface IWeatherService
{
    Task<Forecast> GetForecastAsync();
}

public sealed class NewWeatherService : IWeatherService
{
    public Task<Forecast> GetForecastAsync() => GetNewForecastAsync();
}

public sealed class LegacyWeatherService : IWeatherService
{
    public Task<Forecast> GetForecastAsync() => GetLegacyForecastAsync();
}

// Register primary implementation in DI
// AddEngarde will automatically register fallback with the same scoped lifetime.
builder.Services.AddScoped<IWeatherService, NewWeatherService>();

Static methods can be guarded like instance methods.

public static class ReportExporter
{
    [Engarde("pdf-export")]
    public static Task<byte[]> ExportPdfAsync(Report report) => RenderPdfAsync(report);
}

Value-based guards

public enum PizzaType
{
    Mushroom,
    Pineapple
}

public sealed class PizzaOrder
{
    [Engarde(PizzaType.Pineapple, "new-flavor")]
    public PizzaType? Type { get; set; }

    public void ChangeType(
        [Engarde(PizzaType.Pineapple, "new-flavor")] PizzaType type)
    {
        Type = type;
    }
}

Setup

Register a feature store and, optionally, how to resolve the scope ID (here, the current user) that features are checked for.

builder.Services.AddScoped<CurrentUser>();
builder.Services.AddSingleton<FeatureStore>();
builder.Services.AddEngarde(engarde =>
    {
        engarde.SkipInlineFailures = true;
        engarde.SkipFallbackFailures = true;
        engarde.UseFeatureStore<FeatureStore, CurrentUser, string>(
            (store, featureName, userId) => store.IsEnabledAsync(featureName, userId),
            user => ValueTask.FromResult(user.Id));
        engarde.OnFeatureMissing((featureName, context) =>
            throw new InvalidOperationException($"'{featureName}' is required by {context.Member.Name}"));
    });

var app = builder.Build();
app.UseEngarde(); // checks use the request's services

Use UseFeatureStore<FeatureStore>((store, featureName) => ...) when features don't depend on a scope. Every check calls the store; nothing is cached.

OnFeatureMissing is optional. Without it nothing happens for a disabled feature: calls proceed, and property and field writes are skipped.

Setup in apps without DI

Derive from EngardeAttribute<TStore> and override ResolveFeatureStore to use a store that isn't resolved from DI. Guards that override both IsEnabledAsync and OnFeatureMissing, or return a fixed store, work without AddEngarde.

public sealed partial class AppWithoutDiCustomGuard(params string[] featureNames)
    : EngardeAttribute<FeatureStore>(featureNames)
{
    protected override FeatureStore ResolveFeatureStore(IServiceProvider services) => new FeatureStore();

    protected override ValueTask<bool> IsEnabledAsync(FeatureStore store, string featureName, CancellationToken cancellationToken) =>
        store.IsEnabledAsync(featureName, "app");

    protected override void OnFeatureMissing(string featureName, EngardeContext context) =>
        throw new InvalidOperationException(featureName);
}

A guard is checked when a method or constructor is called, and when a property, field or parameter receives a non-empty value (not null, default, an empty string or an empty collection). A disabled feature calls OnFeatureMissing; throwing blocks the call, and a disabled property or field write is skipped.

Inherited guards

Guards are inherited down the type hierarchy:

  • An interface guard works like a class guard on every implementing class or struct: it is checked on construction and on every method call, including the interface's default implementations.
  • A class guard also guards the methods of derived classes (it already ran in the base constructor).
  • A method, property or parameter guard also guards the members that override or implement it.
[Engarde("my-feature")]
public interface IMyFeature
{
    [Engarde("my-feature-run")]
    Task RunAsync([Engarde("my-feature-option")] int option);
}

[Engarde("my-feature-beta")]
public sealed class MyFeature : IMyFeature // constructing it checks "my-feature-beta" and "my-feature"
{
    // checks "my-feature-run", "my-feature-beta" and "my-feature", plus "my-feature-option" for a non-zero option
    public Task RunAsync(int option) => Task.CompletedTask;
}

Inherited guards add up: every guard along the hierarchy runs, in addition to the member's and type's own, even when several guards name the same feature.

When a class re-declares an interface whose members are implemented by methods inherited from a base class that doesn't implement it, Engarde adds an explicit implementation to the class. A call through the interface checks the interface member's guards, then calls the inherited method, which checks its own member and type guards.

Inline checks

public sealed class Navigation(IEngarde engarde)
{
    public ValueTask<bool> ShowPreviewAsync() => engarde.IsFeatureEnabledAsync("new-experience");

    public ValueTask<bool> ShowPreviewForAsync(string userId) =>
        engarde.IsFeatureEnabledAsync("new-experience", userId);
}

IEngarde calls the configured OnFeatureMissing for a disabled feature and returns false when the handler returns normally.

Custom guards

A partial guard gets a generated interface, registered by AddEngarde, for checking it inline.

public sealed partial class ReportsGuard() : EngardeAttribute("reports")
{
    protected override void OnFeatureMissing(string featureName, EngardeContext context) =>
        throw new UnauthorizedAccessException(featureName);
}

public sealed class Reports
{
    [ReportsGuard]
    public Task OpenAsync() => Task.CompletedTask;
}

public sealed class ReportsMenu(IReportsGuard reports)
{
    public ValueTask<bool> IsVisibleAsync() => reports.IsFeatureEnabledAsync();
}

A guard that takes feature names, such as PermissionGuard(params string[] featureNames), gets IsFeatureEnabledAsync(string featureFlagName) instead. A guard with a fixed feature name must be sealed.

GenerateFeatureGuards creates one sealed guard and interface per feature name:

[GenerateFeatureGuards("reports-view", "reports-edit")]
public partial class AccessGuard : EngardeAttribute;

//Generated: ReportsViewAccessGuard / IReportsViewAccessGuard, ReportsEditAccessGuard / IReportsEditAccessGuard 
// that automatically registered in DI if UseEngarde setup

Failures in inline checks

Every inline query calls OnFeatureMissing for each disabled feature by default; add [SkipInlineFailures] to one custom guard or set engarde.SkipInlineFailures = true during registration to suppress failures for all inline queries.

[SkipInlineFailures]
public sealed partial class MenuGuard() : EngardeAttribute("menu");

builder.Services.AddEngarde(engarde =>
{
    engarde.SkipInlineFailures = true;
    engarde.UseFeatureStore<FeatureStore>((store, feature) => store.IsEnabledAsync(feature));
});

Failures before fallbacks

A valid method or type fallback calls OnFeatureMissing before dispatch by default, so throwing prevents the fallback; add [SkipFallbackFailures] to one custom guard or set engarde.SkipFallbackFailures = true during registration to suppress failures for all fallbacks.

[SkipFallbackFailures]
public sealed partial class FallbackMenuGuard(params string[] featureNames)
    : EngardeAttribute(featureNames);

public sealed class Menu
{
    [FallbackMenuGuard(nameof(OpenLegacy), typeof(Menu), "new-menu")]
    public void Open() => OpenNew();

    private void OpenLegacy() => OpenOld();
}

builder.Services.AddEngarde(engarde =>
{
    engarde.SkipFallbackFailures = true;
    engarde.UseFeatureStore<FeatureStore>((store, feature) => store.IsEnabledAsync(feature));
});

Custom scope

public sealed partial class UserGuard(params string[] featureNames)
    : EngardeAttribute<CurrentUser, string>(featureNames)
{
    protected override ValueTask<string> GetScopeIdAsync(CurrentUser user, CancellationToken cancellationToken) =>
        ValueTask.FromResult(user.Id);
}

A guard with its own scope ID type also gets a query overload that takes the scope ID, to check another scope instead of the current one:

public sealed class Admin(IUserGuard guard)
{
    public ValueTask<bool> CanExportAsync(string userId) => guard.IsFeatureEnabledAsync("export", userId);
}

Any guard can be checked for a scope ID with Engarde.CreateInstance().IsFeatureEnabledAsync<TScopeId>(guard, scopeId); the type must match the guard's or the registered store's.

IEngarde and generated interfaces are scoped by default; change that with .UseQueryLifetime(ServiceLifetime.Singleton).

Testing

dotnet add package En-Garde.Testing
[DisableAllEngarde]
public abstract class UnitTestBase;

[EnableAllEngarde("new-experience")]
public sealed class NewExperienceTests : UnitTestBase
{
    [DisableReportsGuard]
    [Fact]
    public Task Opens_reports_without_the_guard() => reports.OpenAsync();
}

Each guard gets Disable<Guard> attributes that stub matching checks as passed and Enable<Guard> attributes that stub them as failed through the usual missing-feature or fallback path, optionally filtered by feature name; unmatched checks evaluate normally, and overrides also cover test class constructors and field initializers.

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 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.3.3 213 10/21/2026
1.3.2 96 10/21/2026
1.3.1 106 10/21/2026
1.3.0 99 10/20/2026
1.2.0 136 10/19/2026
1.1.2 93 10/19/2026
1.1.1 97 10/18/2026
1.1.0 106 10/17/2026
1.0.0 103 10/16/2026