Search code examples
c#.netdockerredis

How to set Redis ports to be accessed from a .NET app being run by docker compose?


I have this container running on my localhost:5005 docker:

enter image description here

It was created with: docker run --name mtg-redis -p 5005:6379 -d redis

I have my .NET application running on localhost:5002

Docker Compose:

version: '3.4'

services:
  mtgmvc:
    image: ${DOCKER_REGISTRY-}mtgmvc
    build:
      context: .
      dockerfile: MTGMVC/Dockerfile

Docker compose override:

version: '3.4'

services:
  mtgmvc:
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - ASPNETCORE_HTTP_PORTS=8080
      - ASPNETCORE_HTTPS_PORTS=8081
    ports:
      - "5001:8080"
      - "5002:8081"
    volumes:
      - ${APPDATA}/Microsoft/UserSecrets:/home/app/.microsoft/usersecrets:ro
      - ${APPDATA}/ASP.NET/Https:/home/app/.aspnet/https:ro

Dockerfile:

#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.

FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
USER app
WORKDIR /app
EXPOSE 8080
EXPOSE 8081

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["MTG/MTGMVC.csproj", "MTGMVC/"]
RUN dotnet restore "./MTG/MTGMVC.csproj"
COPY . .
WORKDIR "/src/MTGMVC"
RUN dotnet build "./MTGMVC.csproj" -c $BUILD_CONFIGURATION -o /app/build

FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./MTGMVC.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "MTGMVC.dll"]

appsettings:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=mylocaldb;Initial Catalog=MTGCards;Integrated Security=True",
    "Redis": "localhost:5005"
  }
}

Program:

using MTGMVC.Clients;
using MTGMVC.Repositories;
using MTGMVC.Repositories.CommandText;
using MTGMVC.Services;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddHttpClient("MTG", (serviceProvider, httpClient) =>
{
    //var mtgSettings = serviceProvider.GetRequiredService<IOptions<MtgSettings>>().Value;
    httpClient.BaseAddress = new Uri("https://api.magicthegathering.io/v1/");
});
builder.Services.AddHttpClient("Scryfall", (serviceProvider, httpClient) =>
{
    //var mtgSettings = serviceProvider.GetRequiredService<IOptions<MtgSettings>>().Value;
    httpClient.BaseAddress = new Uri("https://api.scryfall.com/");
});

builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = builder.Configuration.GetConnectionString("Redis");
    options.InstanceName = "mtg-redis";
});

//Register own services
builder.Services.AddScoped<IMTGService, MTGService>();
builder.Services.AddScoped<IMTGClient, MTGClient>();
builder.Services.AddScoped<IScryfallClient, ScryfallClient>();
builder.Services.AddTransient<ICommandText, CommandText>();
builder.Services.AddTransient<ISetRepository, SetRepository>();
builder.Services.AddTransient<IMagicDataWriterService, MagicDataWriterService>();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

Extension File for crud redis methods:

using Microsoft.Extensions.Caching.Distributed;
using System.Text.Json;

namespace MTGMVC.Extensions
{
    public static class DistributedCacheExtensions
    {
        public static async Task SetRecordAsync<T>(this IDistributedCache cache, string recordId, T data, TimeSpan? absoluteExpireTime = null, TimeSpan? unusedExpireTime = null)
        {
            var options = new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = absoluteExpireTime ?? TimeSpan.FromMinutes(1),
                SlidingExpiration = unusedExpireTime
            };
            var jsonData = JsonSerializer.Serialize(data);
            await cache.SetStringAsync(recordId, jsonData, options);
        }

        public static async Task<T?> GetRecordAsync<T>(this IDistributedCache cache, string recordId)
        {
            var jsonData = await cache.GetStringAsync(recordId);

            if (jsonData is null)
            {
                return default;
            }

            return JsonSerializer.Deserialize<T>(jsonData);
        }
    }
}

I'm trying to call the redis cache to perform a GET from here:

using Microsoft.Extensions.Caching.Distributed;
using MTGMVC.Clients;
using MTGMVC.DTOs.Scryfall.Cards;
using MTGMVC.Extensions;
using MTGMVC.Models;
using MTGMVC.Repositories;

namespace MTGMVC.Services
{
    public interface IMagicDataWriterService
    {
        Task<IList<SetModel>> GetAllSetNamesAsync();
        Task<ScryfallCardDto> GetRandomCardBySet(string setCode);
    }

    public class MagicDataWriterService : IMagicDataWriterService
    {
        private ISetRepository _setRepository;
        private IScryfallClient _scryfallClient;
        private ILogger<MagicDataWriterService> _logger;
        private IDistributedCache _cache;

        public MagicDataWriterService(ISetRepository setRepository, IScryfallClient scryfallClient, ILogger<MagicDataWriterService> logger, IDistributedCache cache)
        {
            _setRepository = setRepository;
            _scryfallClient = scryfallClient;
            _logger = logger;
            _cache = cache;
        }

        public async Task<IList<SetModel>> GetAllSetNamesAsync()
        {
            try
            {
                var cachedRecords = await _cache.GetRecordAsync<IList<SetModel>>("sets");

                if (cachedRecords == null || !cachedRecords.Any())
                {
                    var persistedSets = await _setRepository.CountSets();
                    if (persistedSets == 0)
                    {
                        var scryfallSets = await _scryfallClient.GetAllScryfallSetsAsync();
                        foreach (var set in scryfallSets.ScryfallSets)
                        {
                            await _setRepository.InsertSet(set);
                        }
                    }

                    var setNames = await _setRepository.GetAllSetNames();
                    await _cache.SetRecordAsync("sets", setNames, TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(60));
                    return setNames;
                }

                return cachedRecords;
            }
            catch (Exception ex)
            {
                _logger.LogError("Unable to get sets {ex}", ex);
                throw;
            }
        }
    (..)

But I'm getting a timeout from the redis cache. As far as I understand my app is being run by docker compose on port localhost:5002 and my redis container is up and running on localhost:5005 which is what I'm pointing at on appsettings...I'm unsure of what I'm doing wrong to reach it.

Exception message:

The message timed out in the backlog attempting to send because no connection became available (5000ms) - Last Connection Exception: UnableToConnect on localhost:5005/Interactive, Initializing/NotStarted, last: NONE, origin: BeginConnectAsync, outstanding: 0, last-read: 0s ago, last-write: 0s ago, keep-alive: 60s, state: Connecting, mgr: 10 of 10 available, last-heartbeat: never, global: 0s ago, v: 2.7.33.41805, command=HMGET, timeout: 5000, inst: 0, qu: 0, qs: 0, aw: False, bw: CheckingForTimeout, last-in: 0, cur-in: 0, sync-ops: 0, async-ops: 1, serverEndpoint: localhost:5005, conn-sec: n/a, aoc: 0, mc: 1/1/0, mgr: 10 of 10 available, clientName: 14c8ae52ba5a(SE.Redis-v2.7.33.41805), IOCP: (Busy=0,Free=1000,Min=1,Max=1000), WORKER: (Busy=0,Free=32767,Min=12,Max=32767), POOL: (Threads=12,QueuedItems=0,CompletedItems=195,Timers=14), v: 2.7.33.41805 (Please take a look at this article for some common client-side issues that can cause timeouts: https://stackexchange.github.io/StackExchange.Redis/Timeouts)

Solution

  • Your localhost is not the localhost of your container. Try to use this:

    host.docker.internal or your default ip of your network (192...* etc.) on your backend application for redis.

    {
      "Logging": {
        "LogLevel": {
          "Default": "Information",
          "Microsoft.AspNetCore": "Warning"
        }
      },
      "AllowedHosts": "*",
      "ConnectionStrings": {
        "DefaultConnection": "Data Source=mylocaldb;Initial Catalog=MTGCards;Integrated Security=True",
        "Redis": "host.docker.internal:5005"
      }
    }
    

    For more check this out