ZiggyCreatures / FusionCache

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

[FEATURE] 🔺 Add .NET Aspire support #237

Open jodydonetti opened 2 months ago

jodydonetti commented 2 months ago

What's this .NET Aspire thing?

Microsoft is about to release .NET Aspire, which is "an opinionated, cloud ready stack for building observable, production ready, distributed applications".

The project is getting quite a good amount of traction, and the community is liking it, so I thought about looking into it to see if there was a space for a FusionCache integration/support of some sort.

The First Steps

While taking a look at it, I saw this:

// Create a distributed application builder given the command line arguments.
var builder = DistributedApplication.CreateBuilder(args);

// Add a Redis server to the application.
var cache = builder.AddRedis("cache"); // OOH, ADD REDIS? NICE 😏

// Add the frontend project to the application and configure it to use the 
// Redis server, defined as a referenced dependency.
builder.AddProject<Projects.MyFrontend>("frontend")
       .WithReference(cache);

As soon as I saw that AddRedis("cache") I thought "cool, I can see something like AddFusionCache("cache") there, too!".

But then I looked into Aspire components, what they are, how they works, and I understood that an Aspire component exists when there's a server resource to handle, connect to, etc and this is not what FusionCache does: FusionCache may use such a thing, but it is not such a thing.

So I installed the necessary Visual Studio (2022 Preview, not the standard one) components and templates and just started playing with it to see what the experience would be.

It Works!

In no time I've been able to make it work, which I think is a testament to the team's effort in the overall design of the development experience.

Now, with just this little piece of code in the AppHost (the project where Aspire components are configured):

var builder = DistributedApplication.CreateBuilder(args);

var cache = builder.AddRedis("cache");

var apiService = builder.AddProject<Projects.AspireApp1_ApiService>("apiservice")
    .WithReference(cache); // THE PART I ADDED

builder.AddProject<Projects.AspireApp1_Web>("webfrontend")
     .WithExternalHttpEndpoints()
     .WithReference(cache)
     .WithReference(apiService);

builder.Build().Run();

and this little piece of code in the ApiService (the project that uses some of those Aspire components):

var builder = WebApplication.CreateBuilder(args);

// Add service defaults & Aspire components.
builder.AddServiceDefaults();

// Add services to the container.
builder.Services.AddProblemDetails();

// THE PART I ADDED - BEGIN
builder.AddRedisDistributedCache("cache");

builder.Services.AddFusionCache()
    .WithSerializer(new FusionCacheSystemTextJsonSerializer())
    .WithRegisteredDistributedCache();

builder.Services.AddOpenTelemetry()
    .WithTracing(builder =>
    {
        builder.AddFusionCacheInstrumentation(o =>
        {
            o.IncludeMemoryLevel = true;
        });
    }).WithMetrics(builder =>
    {
        builder.AddFusionCacheInstrumentation(o =>
        {
            o.IncludeMemoryLevel = true;
            o.IncludeDistributedLevel = true;
            o.IncludeBackplane = true;
        });
    });
// THE PART I ADDED - END

var app = builder.Build();

So yeah, it all worked and I've been able to launch the thing and see the results in the Aspire dashboard:

Traces image

Metrics image

Nice, really nice 😬

Ok so it works, but is it as nice as it could be? Nah, it could be better.

Development Experience

One of my personal mantra with FusionCache is that it should be easy to use and the overall development experience should be as adherent as possible to the classic "it just works".

For example when I added support for OpenTelemetry I've been able to make it work and the setup was doable, but with some manual steps, referencing some public consts for the ActivitySource etc: by creating instead a specific package I made it work seamlessly and with ease (or so I hope 😅).

I'd like to do the same here with Aspire, by adding native Aspire support.

But, as I've been asked "what does Aspire support mean though?", the answer is an additional package to ease the development experience for having FusionCache work with Aspire components. An example can be having FusionCache using the Redis component as a 2nd level distributed cache or a backplane.

Does this make sense? Is it worth it?

I'm trying to find out, and I'll use this issue to explore it and get some community response.

How can it work

As can be seen in the example above it already "works", by simply:

Would it be possible to have an additional ext method on the FusionCache builder to make things smoother? If so, how?

For example note that there's also an builder.AddKeyedRedisDistributedCache("cache") for dealing with multiple keyed services: since FusionCache already natively supports multiple named caches, that would work well together, right?

(I'll open another gh issue to explore adding support in FusionCache for keyed services, even though that has a different set of problems to deal with).

Something I noticed is that Aspire-related ext methods are not tied to the usual IServiceCollection but to the "uppermost" IHostApplicationBuilder: that gives ext methods access to the inner IServiceCollection, but also direct access to the IConfigurationManager, IMetricsBuilder, IHostEnvironment and so on.

So, should I add IHostApplicationBuilder-based ext methods? Should it work with the existing IServiceCollection-based builder or replace it (I'd go with the former).

Another thing I'd like to smooth out is the observability setup, basically this part:

builder.Services.AddOpenTelemetry()
    .WithTracing(builder =>
    {
        builder.AddFusionCacheInstrumentation(o =>
        {
            o.IncludeMemoryLevel = true;
        });
    }).WithMetrics(builder =>
    {
        builder.AddFusionCacheInstrumentation(o =>
        {
            o.IncludeMemoryLevel = true;
            o.IncludeDistributedLevel = true;
            o.IncludeBackplane = true;
        });
    });

For example, when registering the Redis IDistributedCache we saw above, instead of just saying builder.AddRedisDistributedCache("cache") we could go a step further and use:

builder.AddRedisDistributedCache(
    "cache",
    settings =>
    {
        settings.Tracing = true;
        settings.HealthChecks = true;
    }
);

So in the same vein I can see something like:

builder.AddFusionCache(
    "cache",
    settings =>
    {
        settings.Tracing = true;
        settings.Metrics = true;
        settings.HealthChecks = true; // NOTE: SHOULD FUSIONCACHE HAVE ALSO HEALTHCHECKS?
    }
);

And so on.

I'd Like To Know

So I'd like to use this issue to gather ideas, suggestions and more to see first IF it makes sense to have a dedicated package to smooth out the developer experience for having FusionCache work with Aspire (I think so), and in that case how.

Any suggestion would be greatly appreciated, thanks!

davidfowl commented 2 months ago

cc @eerhardt

celluj34 commented 1 month ago

Migrating some of our larger apps to Aspire is something our team is investigating. Being able to natively integrate with open telemetry and metrics is one of the biggest benefits we are looking forward to, so being able to get this "for free" would be a huge boon for us.

jodydonetti commented 1 month ago

Hi @celluj34 and thanks for chipping in. Apart for an hypothetical more "native feeling" ext method, did the code above work well? Are you missing something?