Engarde.Testing 1.0.0

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

Engarde

Engarde compiles feature-toggle guards into methods, constructors, types, properties, fields, parameters, and injectable asynchronous queries.

Quick start

Register an ASP.NET Core feature store with a scoped ID provider, add the request evaluation scope, then use guards as attributes or inject IEngarde for inline checks.

dotnet add package Engarde
using System.Security.Claims;
using Engarde;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEngarde(engarde => engarde
    //Engarde needs a couple of things to run: 
    //1. a feature toggle service to use (the feature service is resolved with DI, so it has to be registered in the service colletion)
    //(optional) 2. a factory to resolve scope ID (a user resolver in this example) to check feature availability for
    .UseFeatureStore<AppFeatureStore, CurrentUserService, string>(
        user => ValueTask.FromResult(user.Id),
        (store, featureName, userId) =>
            store.IsEnabledAsync(featureName, userId))
        
    //Also, you can define global behavior what happens if the flag check is not passed
    .OnFeatureMissing((featureName, context) =>
        throw new InvalidOperationException(
            $"Feature '{featureName}' is required by {context.Member.Name}.")));
    
var app = builder.Build();

app.UseEngarde();

//Can be any service that resolves scope relevant to feature limiting
public sealed class CurrentUserService(...)
{
    public string Id => return SomeService.GetCurrentUserId();
}

//A service to perform feature check against your scope (user Id in this example)
public class AppFeatureStore
{
    ...
    public ValueTask<bool> IsEnabledAsync(string featureName, string scopeId) =>
        SomeFeatureApi.GetFeatureStatusForScope(scopeId);
}

And then you simply put your guards anywhere you want! They will automatically check if feature flag is enabled for a user in the scope of operation (say, an HTTP request in ASP.Net) when either method is invoked or property is set to non-default value, or simply do an inline check

app.MapGet(
    "/preview",
    [Engarde("new-experience")] () => Results.Ok());

[Engarde("new-experience")]
public sealed class FeatureService
{
    [Engarde("operation-enabled")]
    public Task RunAsync() => Task.CompletedTask;

    [Engarde("editable-name")]
    public string? Name { get; set; }

    [Engarde("editable-state")]
    public string? State;

    public void Update([Engarde("valid-input")] string value) { }
}
public sealed class NavigationService(IEngarde engarde)
{
    public ValueTask<bool> IsNewExperienceEnabledAsync() =>
        engarde.IsEnabledAsync("new-experience");
}

Custom feature toggle names

GenerateFeatureGuards converts feature-name strings to PascalCase and generates a sealed, feature-bound guard and injectable interface for each name.

[GenerateFeatureGuards("reports-view", "reports-edit")]
public partial class AccessGuard : EngardeAttribute
{
    protected override void OnFeatureMissing(
        string featureName,
        EngardeContext context) =>
        throw new UnauthorizedAccessException(featureName);
}

// Generated at compile time:
// sealed class ReportsViewAccessGuard : AccessGuard
// interface IReportsViewAccessGuard { ValueTask<bool> IsEnabledAsync(); }
// sealed class ReportsEditAccessGuard : AccessGuard
// interface IReportsEditAccessGuard { ValueTask<bool> IsEnabledAsync(); }

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

public sealed class ReportMenu(IReportsViewAccessGuard reports)
{
    public ValueTask<bool> IsVisibleAsync() => reports.IsEnabledAsync();
}

Define a custom guard manually

A regular guard receives a feature name at each use, while a feature-bound guard fixes its feature name, must be sealed, and exposes a parameterless generated query.

public sealed partial class RuntimePermissionGuard(
    params string[] featureNames)
    : EngardeAttribute(featureNames)
{
    protected override void OnFeatureMissing(
        string featureName,
        EngardeContext context) =>
        throw new UnauthorizedAccessException(featureName);
}

public sealed class RuntimePermissionOperations
{
    [RuntimePermissionGuard("reports-view")]
    public Task OpenAsync() => Task.CompletedTask;
}

public sealed class RuntimePermissionQuery(
    IRuntimePermissionGuard permission)
{
    public ValueTask<bool> CanOpenAsync() =>
        permission.IsEnabledAsync("reports-view");
}
public sealed partial class ReportsViewGuard()
    : EngardeAttribute("reports-view")
{
    protected override void OnFeatureMissing(
        string featureName,
        EngardeContext context) =>
        throw new UnauthorizedAccessException(featureName);
}

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

public sealed class ReportsViewQuery(IReportsViewGuard permission)
{
    public ValueTask<bool> CanOpenAsync() =>
        permission.IsEnabledAsync();
}

Custom feature toggle store

Derive from EngardeAttribute<TStore> to share custom store behavior and override ResolveFeatureStore in a child when it needs a different or fixed instance.

public abstract class StoreBackedGuard(
    params string[] featureNames)
    : EngardeAttribute<AppFeatureStore>(featureNames)
{
    protected override ValueTask<bool> IsEnabledAsync(
        AppFeatureStore store,
        string featureName) =>
        store.IsEnabledAsync(featureName, "global");
}

public sealed partial class SearchGuard()
    : StoreBackedGuard("search");

public sealed partial class PreviewGuard()
    : StoreBackedGuard("preview")
{
    private static readonly AppFeatureStore Store = new();

    protected override AppFeatureStore ResolveFeatureStore(
        IServiceProvider services) =>
        Store;
}

Custom scope ID

Derive from EngardeAttribute<TScopeProvider, TScopeId> to share scope resolution and override GetFeatureScopeIdAsync in a child when its scope differs.

public abstract class TenantScopedGuard(
    params string[] featureNames)
    : EngardeAttribute<CurrentTenant, string>(featureNames)
{
    protected override ValueTask<string> GetFeatureScopeIdAsync(
        CurrentTenant tenant) =>
        ValueTask.FromResult(tenant.Id);
}

public sealed partial class TenantSettingsGuard()
    : TenantScopedGuard("tenant-settings");

public sealed partial class SharedSettingsGuard()
    : TenantScopedGuard("shared-settings")
{
    protected override ValueTask<string> GetFeatureScopeIdAsync(
        CurrentTenant tenant) =>
        ValueTask.FromResult($"shared:{tenant.Id}");
}

.NET Core DI setup

AddEngarde registers IEngarde and every compile-time generated guard interface automatically as scoped services unless UseQueryLifetime selects another lifetime.

builder.Services.AddSingleton<AppFeatureStore>();
builder.Services.AddScoped<CurrentTenant>();
builder.Services.AddEngarde(engarde => engarde
    .UseFeatureStore<AppFeatureStore, CurrentTenant, string>(
        tenant => ValueTask.FromResult(tenant.Id),
        (store, featureName, tenantId) =>
            store.IsEnabledAsync(featureName, tenantId))
    .OnFeatureMissing((featureName, _) =>
        throw new InvalidOperationException(featureName)));

public sealed class ReportService(
    IEngarde engarde,
    IReportsViewAccessGuard reportsView,
    IRuntimePermissionGuard runtimePermission)
{
    public ValueTask<bool> IsAnyReportEntryEnabledAsync() =>
        reportsView.IsEnabledAsync();
}
builder.Services.AddEngarde(engarde => engarde
    .UseFeatureStore<AppFeatureStore>(
        (store, featureName) =>
            store.IsEnabledAsync(featureName, "global"))
    .OnFeatureMissing((featureName, _) =>
        throw new InvalidOperationException(featureName))
    .UseQueryLifetime(ServiceLifetime.Transient));

DI in legacy apps

For a .NET Framework 4.7.2-style application, populate Autofac from IServiceCollection, activate Engarde once, and borrow each Autofac lifetime scope for woven and inline checks.

<package id="Autofac" version="6.5.0" targetFramework="net472" />
<package id="Autofac.Extensions.DependencyInjection" version="8.0.0" targetFramework="net472" />
<package id="Engarde" version="1.0.0" targetFramework="net472" />
using Autofac;
using Autofac.Extensions.DependencyInjection;
using Engarde;
using Microsoft.Extensions.DependencyInjection;
using Guard = Engarde.Engarde;

public static class Bootstrapper
{
    public static async Task RunAsync()
    {
        var services = new ServiceCollection();
        services.AddSingleton<AppFeatureStore>();
        services.AddScoped<LegacyTenant>();
        services.AddEngarde(engarde => engarde
            .UseFeatureStore<AppFeatureStore, LegacyTenant, string>(
                tenant => ValueTask.FromResult(tenant.Id),
                (store, featureName, tenantId) =>
                    store.IsEnabledAsync(featureName, tenantId))
            .OnFeatureMissing((featureName, _) =>
                throw new InvalidOperationException(featureName)));

        var builder = new ContainerBuilder();
        builder.Populate(services);

        using (var container = builder.Build())
        {
            var rootServices = new AutofacServiceProvider(container);
            Guard.Activate(rootServices);

            using (var lifetime = container.BeginLifetimeScope())
            {
                var requestServices = new AutofacServiceProvider(lifetime);
                using (Guard.BeginScope(requestServices, createScope: false))
                using (Guard.BeginEvaluationScope(requestServices))
                {
                    var engarde = requestServices.GetRequiredService<IEngarde>();
                    var reports = requestServices.GetRequiredService<IReportsViewAccessGuard>();

                    bool first = await engarde.IsEnabledAsync("new-experience");
                    bool second = await reports.IsEnabledAsync();
                }
            }
        }
    }
}

public sealed class LegacyTenant
{
    public string Id => Thread.CurrentPrincipal?.Identity?.Name ?? "anonymous";
}

How to test

Engarde.Testing generates class and method overrides for all guards, with optional feature-name filters and inherited test-class behavior.

dotnet add package Engarde.Testing
using Engarde.Testing;

[DisableAllEngarde]
public abstract class UnitTestBase;

public sealed class ServiceTests : UnitTestBase
{
    [Fact]
    public Task Runs_without_guards() =>
        service.RunAsync();
}
[DisableAllEngarde]
public abstract class DisabledGuardTests;

[EnableAllEngarde("new-experience")]
public sealed class NewExperienceTests : DisabledGuardTests;

[DisableIRuntimePermissionGuard("reports-edit")]
public sealed class ReportsEditTests;

public sealed class MethodOverrideTests
{
    [DisableEngarde("operation-enabled")]
    [Fact]
    public Task Runs_without_the_selected_default_guard() =>
        service.RunAsync();
}
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.0.0 112 10/16/2026