dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.

Home Page:https://asp.net

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Expose `OutputCachePolicyBuilder.AddPolicy(IOutputCachePolicy)` as `public`

julealgon opened this issue · comments

Background and Motivation

I want to create a parameterized custom policy to dynamically generate tags based on route values so that I can evict only a subset of the cached contents later on on one of my APIs.

To allow me to tag cache entries based on route values, I need to pass the name of the route parameter to my custom policy, as well as the tag prefix to use. Both values are primitive strings.

However, due to the way OutputCachePolicyBuilder works today, it only allows one to pass a custom policy object by specifying a Type:

public OutputCachePolicyBuilder AddPolicy(Type)

Or by specifing the type via generics and letting the container resolve the policy automatically:

public OutputCachePolicyBuilder AddPolicy<T>()
    where T : IOutputCachePolicy

This makes it extremely convoluted to pass a parameterized policy like this:

public sealed class TagPerRouteOutputCachePolicy : IOutputCachePolicy
{
    private readonly string tagPrefix;
    private readonly string routeValueName;

    public TagPerRouteOutputCachePolicy(string tagPrefix, string routeValueName)
    {
        ArgumentNullException.ThrowIfNull(tagPrefix);
        ArgumentNullException.ThrowIfNull(routeValueName);

        this.tagPrefix = tagPrefix;
        this.routeValueName = routeValueName;
    }

    ...

The only way I see to allow for my use case would be to somehow pre-register the type in the container with a lambda factory and hardcoded values, but that's not possible since the values I'm passing are coming from the policy setup logic which happens in a different place.

I'm now forced to completely change the design of my object to use some sort of fake mutable container object so I can pass in values from the outside using the container in a convoluted manner.

I'd rather simply pass the instance directly using the currently internal overload:

internal OutputCachePolicyBuilder AddPolicy(IOutputCachePolicy)

Proposed API

public sealed class OutputCachePolicyBuilder
{
-    internal OutputCachePolicyBuilder AddPolicy(IOutputCachePolicy)
+    public OutputCachePolicyBuilder AddPolicy(IOutputCachePolicy)
}

Usage Examples

public static class OutputCachePolicyBuilderExtensions
{
    public static OutputCachePolicyBuilder TagByRouteValue(
        this OutputCachePolicyBuilder builder,
        string tagPrefix,
        string routeValueName)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentNullException.ThrowIfNull(tagPrefix);
        ArgumentNullException.ThrowIfNull(routeValueName);

        return builder.AddPolicy(new TagPerRouteOutputCachePolicy(tagPrefix, routeValueName));
    }
}

This API is also used extensively for all existing extensions and policy operations as the way policy builder works relies on composing several smaller implementations of IOutputCachePolicy.

Alternative Designs

I don't see any alternative design that allows me to add a parameterized policy from the builder perspective. I'd have to forcefully introduce hard-to-use mutable fake "parameter container" objects in DI, then pass those in as my parameters in the policy. Then, before I call AddPolicy<MyPolicy>(), I'd have to resolve those fake container objects and set the values in them so that I can later fetch the values from the DI-injected instance.

Something like this which is absolutely terrible design:

public interface IParameterPosition;

public sealed class FirstParameterPosition : IParameterPosition;
public sealed class SecondParameterPosition : IParameterPosition;

public sealed class TagPerRouteOutputCachePolicy : IOutputCachePolicy
{
    private readonly string tagPrefix;
    private readonly string routeValueName;

    public TagPerRouteOutputCachePolicy(
        ParameterContainer<TagPerRouteOutputCachePolicy, string, FirstParameterPosition> tagPrefix,
        ParameterContainer<TagPerRouteOutputCachePolicy, string, SecondParameterPosition>routeValueName)
    {
        this.tagPrefix = tagPrefix.Value;
        this.routeValueName = routeValueName.Value;
    }

And even then, this would only work if I have a single instance of this class. It is completely unmanageable.

Risks

I see zero risk in exposing the widely used internal method as it relies on public-facing interfaces already.