ZiggyCreatures / FusionCache

FusionCache is an easy to use, fast and robust hybrid cache with advanced resiliency features.
MIT License
1.87k stars 96 forks source link

[FEATURE] Add ability to set cache key prefix separately from cache name #300

Open casperOne opened 2 months ago

casperOne commented 2 months ago

Problem

In my apps (and I would think others), I may be using a shared server for distributed caching across multiple environments.

I may also be using named caches.

In order to generate keys that don't conflict, I would need to have a compound key prefix, something like:

prod:Products:<key>

However, when performing dependency injection, there are some things that are the concern of the host, and some that are the concern of the core logic.

In this case, the environment is a concern of the host, while the cache name is a concern of the logic.

If I am using dependency injection, I prefer to keep registration of services related to my core logic in an assembly separate from the host assembly, while keeping host registration concerns in the hosting assembly.

Currently, I have no way of setting both a cache name and a cache key prefix in separate areas of the code.

Solution

Separate out cache name and cache key prefix, such that when keys are generated, it does the composition at key generation time.

Currently, if the cache name is set, it simply sets the cache key prefix. These should be separate until key generation time.

Alternatives

I will stream environment details through to my DI in my core logic assembly.

jodydonetti commented 2 months ago

Hi @casperOne

Separate out cache name and cache key prefix, such that when keys are generated, it does the composition at key generation time.

Currently, if the cache name is set, it simply sets the cache key prefix. These should be separate until key generation time.

It's already like that, the 2 are not forced to be linked.

If, when building the cache, you use WithCacheKeyPrefix() you are explicitly opting in to this behavior.

By NOT doing it, there will be no automatic cache key prefix, and you'll need to build the cache key yourself, however you want.

Try it and let me know if you have any problem.

Hope this helps.

jodydonetti commented 2 months ago

Also:

Currently, I have no way of setting both a cache name and a cache key prefix in separate areas of the code.

You can use WithCacheKeyPrefix("myprefix") and build your own prefix by combining the environment and the cache name. You can also create your own DI registration ext method with a param to accept the env part, so you can have your assembly where you do the DI registration that is now parametrized, and pass the env as part the param from the main program.

Hope this helps.

casperOne commented 2 months ago

@jodydonetti Thanks for the response, but I think I'm not communicating my scenario accurately enough.

Say I have a web app, entry point is an assembly:

Application.Web.EntryPoint

There's also a logic assembly. In an ideal world, this assembly is agnostic to hosting, it could be a web container, a windows service, desktop, etc.

Additionally, many of the interfaces have a single implementation, and we place the registration code near the interface and implementation so that everything around that specific functionality is colocated as much as possible.

Let's say this assembly is called:

Application.Core

In that assembly, we will have a structure like this:

/
    ServiceCollectionExtensions.cs
    /FibonaciSequence
        ServiceCollectionExtensions.cs
        IFibonaciSequenceGenerator.cs
        FibonaciSequenceGenerator.cs        

The top-level ServiceCollectionExtensions.cs looks like this:

public static IServiceCollection AddCore(
    this IServiceCollection serviceCollection
)
{
    // For ease-of-use.
    var sc = serviceCollection;

    // Call sub folders for registration.
   sc = sc.AddFibonaciSequence();

    // Return the service collection.
    return sc;
}

Say FibonaciSequenceGenerator uses Fusion Cache to store previous generations.

Let's also say that currently, UAT and TEST environments share the same redis instance we want to use for backplaning.

In order to do that, my /FibonaciSequence/ServiceCollectionExtensions.cs file needs to look like this:

public static IServiceCollection AddFibonaciSequence(
    this IServiceCollection serviceCollection
)
{
    // For ease-of-use.
    var sc = serviceCollection;

    // Add the cache.
    sc
        // Calling this overload becomes somewhat irrelevant
        // given the next line
        .AddFusionCache(CacheName)
        // The environment, which is a hosting concern, has bled in
        .WithCacheKeyPrefix($"{environment}:{CacheName}:")

        // All of these are delegated to the host's definition
        // as how serialization, distributed caching and backplane
        // are not specific to the core logic's concern.
        .WithRegisteredSerializer()
        .WithRegisteredDistributedCache()
        .WithRegisteredBackplane()

        // This is tied specifically to *this* cache, which is a
        // concern of *this* component.
        .WithDefaultEntryOptions(o => {
              // Reference data *rarely* changes
              // One day is fine and there will
              // be a backplane on top of that.
              o.Duration = TimeSpan.FromDays(1);
          });

    // Return the service collection.
    return sc;
}

The comments paint the picture, basically, we are bleeding hosting concerns (the environment) into the core logic.

Fusion Cache already opens the door to this separation through the WithRegistered... calls.

Ideally, in the hosting DLL, we set up the specific distributed cache, backplane, etc. as well as how to handle concerns around prefixing cache keys due to environment.

In the core DLL, there may be additional concerns around key generation, but those concerns are for the purpose of discriminating between different business/logic concerns, not hosting concerns.

Right now, I have to thread environment to every single cache (and I plan on having a bunch) which makes for a very leaky abstraction, IMO, as AddCore now has to look like this:

public static IServiceCollection AddCore(
    this IServiceCollection serviceCollection
    , string environment
)
{
    // For ease-of-use.
    var sc = serviceCollection;

    // Call sub folders for registration.
   sc = sc.AddFibonaciSequence(environment);

   // New caches, nested deeply?  Have to thread enviromment
   // to all of them.

    // Return the service collection.
    return sc;
}

I could have probably said "this is the same as property drilling in React" as well to explain all this 🤣

jodydonetti commented 2 months ago

Hi @casperOne , sorry for the delay.

I think I had already understood your scenario, you've been clear enough and maybe it was me who was not able to clearly articulate my thoughts 🙂

I think the main issue may revolve around the name of the string environment param.

Let me explain.

When you think about it, the fact that each cache needs a prefix is a concern of the cache itself, so it is correct that the param is there, meaning that it cannot be hidden away.

The cache is registered via the AddCore method, so this method somehow needs to know about the prefix.

The host app on the other hand is responsible to register the services (via the AddCore method), so it needs to somehow pass that, too.

In a way is the same when in a common web app you register some core aspnet service that requires some configuration, usually via options (see IOptions<T>).

The AddCore and AddFibonaciSequence methods though should not need to know about the environment itself, so maybe just changing the param name to string prefix would logically detach the 2 things: then, in the host app, you can set the prefix using the value of the environment, creating the "link" in the right place (the host setup, where things get wired together).

Maybe using the options pattern it can even become clearer.

Let me know what you think.

dotTrench commented 1 month ago

Hello, to chip in on this we're currently configuring our CacheKeyPrefix in our tests to ensure that our tests do not interfere with eachother without having to use a separate redis instance for each test.

// Program.cs
builder.Services.AddFusionCache()
    .WithDistributedCache(_ => new RedisCache(...))
    .WithBackplane(_ => ...);
public class MyTest : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;

    public MyTest(WebApplicationFactory<Program> factory)
    {
        _factory.WithWebHostBuilder(builder => 
            builder.ConfigureTestServices(services => 
                {
                    services.Configure(
                        FusionCacheOptions.DefaultCacheName, 
                        opts => 
                        {
                            opts.CacheKeyPrefix = $"MyTest:{Guid.NewGuid()}:";
                        });
                })
            );
    }

    [Fact]
    public async Task SomeTest()
    {
        using var client = _factory.CreateClient();
        // Test stuff here
    }
}

I assume you could do something similar to this in your code to achieve this? You might be able to configure the cache key prefix for all FusionCache instances using ConfigureAll. e.g.

services.ConfigureAll<FusionCacheOptions>(opts => opts.CacheKeyPrefix = $"{environment}:{opts.CacheName}:");

I assume this is what @jodydonetti is alluding to in his comment about using the options pattern.