YARP IP Filters

Advanced

Programmatic configuration, dynamic reload, custom providers

Programmatic configuration

Instead of binding from appsettings.json, you can build policies in code by passing an Action<List<IPFilterPolicy>> to AddIPFilterPolicies:

builder.Services.AddIPFilterPolicies(policies =>
{
    policies.Add(new IPFilterPolicy
    {
        PolicyName = "Global",
        Mode = IPFilterPolicyMode.BlockList,
        IPAddresses = ["198.51.100.23"]
    });

    policies.Add(new IPFilterPolicy
    {
        PolicyName = "Intranet",
        Mode = IPFilterPolicyMode.AllowList,
        IPNetworks = ["192.168.0.0/24", "10.0.0.0/8"]
    });
});

This overload configures the Policies list only. To enable the global policy alongside code-defined policies, also configure the surrounding IPFilterPoliciesConfiguration:

builder.Services.Configure<IPFilterPoliciesConfiguration>(config =>
{
    config.EnableGlobalPolicy = true;
    config.GlobalPolicyName = "Global";
});

IPFilterPolicy properties are init-only, so construct each policy with an object initialiser as shown. Mode defaults to AllowList and both list properties default to empty.

Dynamic reload

IPFilterPolicyProvider subscribes to IOptionsMonitor<IPFilterPoliciesConfiguration>.OnChange. When the bound configuration changes — for example the configuration provider detects an edited appsettings.json with reloadOnChange enabled — the provider rebuilds its policy dictionary and re-evaluates the global policy, without a restart.

Two things to know:

  • The provider indexes policies by PolicyName. Duplicate names in a reloaded configuration will throw while

building the dictionary — keep names unique.

  • If a reload enables the global policy but the named global policy is missing, the provider throws during the

reload callback. Validate configuration before shipping it to avoid a broken reload.

Because parsing of addresses and networks is lazy and cached per policy instance, a reload transparently picks up new values: the next request evaluates freshly built policy objects.

IPNetworkCollection

CIDR matching is backed by IPNetworkCollection, a binary trie keyed on the bits of each network prefix. Rather than testing a client IP against every configured range one by one, Contains walks the trie bit by bit and returns as soon as it reaches a terminal node (a configured prefix). This keeps per-request cost proportional to the prefix length, not to the number of networks, which matters when a policy lists many ranges on a hot path.

The collection is built once per policy instance (lazily, on first use) from the policy's IPNetworks strings and reused for the lifetime of that instance.

Exceptions

ExceptionThrown when
IPFilterPolicyExceptionBase type for the library's exceptions.
IPFilterPolicyNotFoundExceptionA route's IPFilterPolicy metadata names a policy that does not exist. The middleware sets 500 Internal Server Error and throws this. Its message includes the policy name and the YARP route id.

A missing global policy (when enabled) surfaces as a plain exception thrown by the provider at load/reload time, not as IPFilterPolicyNotFoundException.

Replacing the policy provider

The middleware depends on the IIPFilterPolicyProvider abstraction:

public interface IIPFilterPolicyProvider
{
    IPFilterPolicy? GetPolicy(string policyName);
    IPFilterPolicy? GetGlobalPolicy();
}

To source policies from somewhere other than configuration — a database, a feature-flag service, a remote API — implement this interface and register your implementation as a singleton instead of calling AddIPFilterPolicies:

builder.Services.AddSingleton<IIPFilterPolicyProvider, MyCustomPolicyProvider>();

GetGlobalPolicy() should return the active global policy or null when none applies; GetPolicy(name) should return the named policy or null when it is not found (which the middleware treats as a configuration error). Still register the middleware with UseIPFilterPolicies().

Performance notes

The critical path is deliberately lean: individual addresses are matched via HashSet<IPAddress>.Contains, networks via the trie described above, and every log call uses the compiled LoggerMessage pattern to avoid allocations and boxing. As of v1.4.0 most types are sealed, and the middleware wraps each evaluation in a single lightweight activity.