.NET Steps – Caching II – Distributed

Introduction

General

In our last post we talked about what caching is and implemented in memory caching. If you recall, we did a couple of things:

  • We created an ICacheService.cs interface.
  • We created Company.Shorts.Infrastructure.Cache project where we implemented that interface.
  • We added two simple REST endpoints for creating and fetching users in order to test our implementation.

If you remember, in the last post I said that in memory cache is a thread safe object that lives in memory from the moment you start your application to the moment you stop it.

So, that in memory cache has some “problems” (for the sake of argument, let’s just call them that):

  • it uses application memory
  • has more limited memory
  • it can’t survive application redeploys
  • it can’t be shared across multiple services
  • it can’t be scaled, you have to scale your application
  • if you use load balancing, every application will have it’s own cache

I mean, it’s called in memory for a reason.

Distributed cache

Up until this point we had just one service. What happens if we want to scale those services (add a couple of more instances) and place them behind load balancer? Now, every service has it own cache and this is something that we don’t want. Otherwise, what is the point of this article?

This is where distributed cache jumps in.

Distributed cache is “external” service that our application can access. This means that cache now doesn’t live inside our application, doesn’t use our application resources and it doesn’t depend on a lifecycle of our application.

Since it is an “external” service it can be scaled independently. As our “external” service we are going to use Redis.

Since we are good colleagues, we are going to leave scaling, setting up redis (with or without redis cluster) to our devops team. That is the least we can do.

Implementation

Prerequisitis
Steps
  • In Infrastructure folder create new project: Company.Shorts.Infrastructure.Cache.Redis
  • In Company.Shorts.Infrastructure.Cache.Redis.csproj add following nuget packages
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>


  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="6.0.0" />
    <PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="6.0.15" />
    <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
    <PackageReference Include="StackExchange.Redis" Version="2.6.104" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\..\Application\Company.Shorts.Application.Contracts\Company.Shorts.Application.Contracts.csproj" />
  </ItemGroup>


</Project>
  • In Company.Shorts.Infrastructure.Cache.Redis create new folder Interal
    • In that folder, add CacheService.cs
namespace Company.Shorts.Infrastructure.Cache.Redis.Internal
{
    using Company.Shorts.Application.Contracts.Cache;
    using Microsoft.Extensions.Caching.Distributed;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Text.Json;
    using System.Threading.Tasks;

    internal sealed class CacheService : ICacheService
    {
        private readonly IDistributedCache cache;

        public CacheService(IDistributedCache cache)
        {
            this.cache = cache;
        }

        public async Task<T> GetOrAdd<T>(string key, Func<Task<T>> fun)
        {
            var item = await this.cache.GetAsync(key);

            if (item is not null)
            {
                return JsonSerializer.Deserialize<T>(item);
            }
       
            var items = await fun();

            var data = JsonSerializer.Serialize(items);
            var byteData = Encoding.UTF8.GetBytes(data);

            await this.cache.SetAsync(key, byteData);

            return items;
        }
    }
}
  • In Company.Shorts.Infrastructure.Cache.Redis add DepdendencyInjection.c.s
namespace Company.Shorts.Infrastructure.Cache.Redis
{
    using Company.Shorts.Application.Contracts.Cache;
    using Company.Shorts.Infrastructure.Cache.Redis.Internal;
    using Microsoft.Extensions.DependencyInjection;

    public static class DependecyInjection
    {
        public static IServiceCollection AddReddisCache(this IServiceCollection services, RedisAdapterSettings settings)
        {
            services.AddStackExchangeRedisCache(opt =>
            {
                opt.Configuration = settings.ConnectionString;
            });

            services.AddScoped<ICacheService, CacheService>();

            return services;
        }
    }

    public class RedisAdapterSettings
    {
        public const string Key = nameof(RedisAdapterSettings);

        public string ConnectionString { get; set; } = default!;

    }
}
  • In Company.Shorts add dependency to Company.Shorts.Infrastructure.Cache.Redis
  • In Company.Shorts modify your Startup.cs
namespace Company.Shorts
{
    using Company.Shorts.Application;
    using Company.Shorts.Blocks.Common.Mapping.Configuration;
    using Company.Shorts.Blocks.Common.Serilog.Configuration;
    using Company.Shorts.Blocks.Common.Swagger.Configuration;
    using Company.Shorts.Blocks.Presentation.Api.Configuration;
    using Company.Shorts.Infrastructure.Db.Postgres;
    using Company.Shorts.Infrastructure.ExampleAdapter;
    using Company.Shorts.Presentation.Api;
    using Hellang.Middleware.ProblemDetails;
    using Company.Shorts.Infrastructure.Cache;
    using Company.Shorts.Infrastructure.Cache.Redis;

    public sealed class Startup
    {
        public Startup(
            IConfiguration configuration,
            IWebHostEnvironment environment)
        {
            Configuration = configuration;
            Environment = environment;
        }

        public IConfiguration Configuration { get; }

        public IWebHostEnvironment Environment { get; }

        public ExampleAdapterSettings ExampleAdapterSettings =>
            Configuration
                .GetSection(ExampleAdapterSettings.Key)
                .Get<ExampleAdapterSettings>();

        public PostgresAdapterSettings PostgresAdapterSettings =>
            Configuration
                .GetSection(PostgresAdapterSettings.Key)
                .Get<PostgresAdapterSettings>();

        public RedisAdapterSettings ReddisAdapterSettings =>
            Configuration
                .GetSection(RedisAdapterSettings.Key)
                .Get<RedisAdapterSettings>();

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddReddisCache(ReddisAdapterSettings);
            services.AddCors();
            services.AddHealthChecks();
            services.AddPostgresDatabaseLayer(PostgresAdapterSettings);
            services.AddInfrastructureExampleAdapter(ExampleAdapterSettings);
            services.AddApplicationLayer();
            services.AddPresentationLayer(Environment);
            services.AddAutoMapperConfiguration(AppDomain.CurrentDomain);
        }

        public void Configure(IApplicationBuilder app)
        {
            app.UseProblemDetails();

            if (!Environment.IsDevelopment())
            {
                app.UseHsts();
            }

            app.MigratePostgresDb();

            app.UseSwaggerConfiguration();

            app.UseHttpsRedirection();

            app.UseCors(options => options
                .AllowAnyOrigin()
                .AllowAnyMethod()
                .AllowAnyHeader());

            app.UseSerilogConfiguration();

            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapDefaultControllerRoute();
                endpoints.MapDefaultHealthCheckRoute();
            });
        }
    }
}
  • Right click on Company.Shorts and select ManageUserSecrets
{
  "PostgresAdapterSettings:Url": "Server=localhost;Port=5432;Database=ShortsUserDb;User Id=postgres;Password=postgres",
  "RedisAdapterSettings:ConnectionString": "localhost:6379,password=eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81",
}

Now you can test your application. First request will get the data from the database and put it in cache. Second request will get it from the cache instead. If you don’t believe me, put a breakpoint in CacheService.cs.

Finally, you’ll put it somewhere.

Github

Branch: feature/caching-distributed-impl