Skip to content

[FEATURE] 🔺 Add .NET Aspire support #237

@jodydonetti

Description

@jodydonetti

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:

  • registering an IDistributedCache in the DI contaienr via builder.AddRedisDistributedCache("cache") (an ext method in the specific Redis package for Aspire support)
  • using the registered IDistributedCache in FusionCache, by taking it from the DI container via the WithRegisteredDistributedCache() standard builder method of FusionCache

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!

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions