0

I am working on an ASP.NET Core Web API that is deployed to AWS. I am trying to integrate Elastic Cache using Valkey and working locally at the moment. This is my Program.cs:

using Amazon;
using Amazon.S3;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using smart_qualify_api.Entities;
using smart_qualify_api.Services.ResumeTemplates;
using StackExchange.Redis;
using System.Security.Authentication;
using System.Text;

var builder = WebApplication.CreateBuilder(args);
var config = builder.Configuration;

// In Program.cs, after other service configurations
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
{
    var config = builder.Configuration;
    var valkeyEndpoint = config["ElastiCache:ValkeyEndpoint"]!;
    var useSsl = bool.Parse(config["ElastiCache:UseSsl"] ?? "false");

    var redisConfig = new ConfigurationOptions
    {
        EndPoints = { valkeyEndpoint },
        Ssl = useSsl,
        AbortOnConnectFail = false, // Prevent app from crashing if connection fails
        ConnectTimeout = 30000, // Timeout in milliseconds
        SyncTimeout = 10000,    // 10 seconds for commands
        SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13, // Explicitly allow modern TLS versions
    };

    return ConnectionMultiplexer.Connect(redisConfig);
});

builder.Services.AddSingleton<IAmazonS3>(sp => new AmazonS3Client(RegionEndpoint.AFSouth1));
// Add services to the container.
builder.Services.AddControllers();
// Add IHttpClientFactory
builder.Services.AddHttpClient();
// Add Template Registry
builder.Services.AddSingleton<TemplateRegistry>();
// Add CV Analysis Service
builder.Services.AddScoped<smart_qualify_api.Services.CVAnalysisService>();
// Add Push Notification Service
builder.Services.AddScoped<smart_qualify_api.Services.PushNotificationService>();

builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowNextJsFrontend", builder =>
    {
        builder.AllowAnyOrigin()
        .AllowAnyHeader()
        .AllowAnyMethod();
    });
});

// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();

builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;

}).AddJwtBearer(options =>
{
    options.RequireHttpsMetadata = false;
    options.SaveToken = true;
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        ValidIssuer = config["Jwt:Issuer"],
        ValidAudience = config["Jwt:Audience"],
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["Jwt:Token"]!))
    };
});
builder.Services.AddAuthorization();

builder.Services.AddSwaggerGen(swagger =>
{
    swagger.SwaggerDoc("v1", new OpenApiInfo { Version = "v1" });

    swagger.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme()
    {
        Name = "Authorization",
        Type = SecuritySchemeType.Http,
        Scheme = JwtBearerDefaults.AuthenticationScheme,
        BearerFormat = "JWT",
        In = ParameterLocation.Header,
        Reference = new OpenApiReference
        {
            Id = JwtBearerDefaults.AuthenticationScheme,
            Type = ReferenceType.SecurityScheme,
        }
    });

    swagger.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference
                {
                    Type = ReferenceType.SecurityScheme,
                    Id = "Bearer"
                }
            }, Array.Empty<string>()
        }
    });
});

builder.Services.AddAuthorizationBuilder().AddPolicy("AdminUserPolicy", options =>
{
    options.RequireAuthenticatedUser();
    options.RequireRole("admin", "user", "student");
}).AddPolicy("AdminPolicy", options =>
{
    options.RequireAuthenticatedUser();
    options.RequireRole("admin");
}).AddPolicy("UserPolicy", options =>
{
    options.RequireAuthenticatedUser();
    options.RequireRole("user", "student");
});

builder.Services.AddDbContext<SMQDbContext>(options =>
{
    options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"));
});

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

//app.UseHttpsRedirection();
app.UseCors("AllowNextJsFrontend");
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();

app.Run();

And this is my controller to access the templates:

using Amazon.S3;
using Amazon.S3.Model;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using QuestPDF.Infrastructure;
using smart_qualify_api.Entities;
using smart_qualify_api.Models.Resume;
using smart_qualify_api.Services.ResumeTemplates;
using StackExchange.Redis;
using System.Security.Claims;
using System.Text.Json;

namespace smart_qualify_api.Controllers.Core
{
    [Route("api/[controller]")]
    [ApiController]
    [ApiExplorerSettings(IgnoreApi = false)]
    public class ResumeController : ControllerBase
    {
        private readonly SMQDbContext _dbContext;
        private readonly IConfiguration _config;
        private readonly TemplateRegistry _templateRegistry;
        private readonly IHttpClientFactory _httpClientFactory;
        private readonly IAmazonS3 _s3Client;
        private readonly IConnectionMultiplexer _redis; // Add Redis connection
        private readonly string _bucketName = "smart-qualify-assets";
        private readonly string _resumeFolder = "user-resumes/";
        private readonly string _cacheKey = "resume_templates"; // Cache key for templates
        private readonly TimeSpan _cacheTTL = TimeSpan.FromMinutes(30); // Cache expiration

        public ResumeController(
            SMQDbContext dbContext,
            IConfiguration config,
            TemplateRegistry templateRegistry,
            IHttpClientFactory httpClientFactory,
            IAmazonS3 s3Client,
            IConnectionMultiplexer redis // Inject Redis
            )
        {
            _dbContext = dbContext;
            _config = config;
            _templateRegistry = templateRegistry;
            _httpClientFactory = httpClientFactory;
            _s3Client = s3Client;
            _redis = redis; // Initialize Redis
        }

        [HttpGet("getResumeTemplates")]
        [Authorize]
        public IActionResult GetResumeTemplates()
        {
            try
            {
                // Get Redis database
                var db = _redis.GetDatabase();

                // Try to get templates from cache
                var cachedTemplates = db.StringGet(_cacheKey);

                if (cachedTemplates.HasValue)
                {
                    // Deserialize cached templates
                    var templates = JsonSerializer.Deserialize<List<object>>(cachedTemplates!);
                    return Ok(new { success = true, templates });
                }

                // Cache miss: Fetch templates from TemplateRegistry
                var templatesFromRegistry = _templateRegistry.GetAllTemplates()
                    .Select(t => new { templateId = t.TemplateId, name = t.Name, templateImageUrl = t.TemplateImageUrl })
                    .ToList();

                // Serialize and store in cache with TTL
                var serializedTemplates = JsonSerializer.Serialize(templatesFromRegistry);
                db.StringSet(_cacheKey, serializedTemplates, _cacheTTL);

                return Ok(new { success = true, templates = templatesFromRegistry });
            }
            catch (RedisException ex)
            {
                // Handle Redis errors (e.g., connection issues)
                // Fallback to fetching from TemplateRegistry without caching
                var templates = _templateRegistry.GetAllTemplates()
                    .Select(t => new { templateId = t.TemplateId, name = t.Name, templateImageUrl = t.TemplateImageUrl })
                    .ToList();
                return Ok(new { success = true, templates });
            }
            catch (Exception ex)
            {
                // Handle other errors
                return StatusCode(500, new { success = false, message = "Error retrieving resume/cv templates.", Error = ex.Message });
            }
        }
}

These are my appsettings:

{
  "ConnectionStrings": {
    //"DefaultConnection": "Server=.\\SQLEXPRESS; Database=SmartQualifyDb; Trusted_Connection=True; TrustServerCertificate=true;",
    //"DefaultConnection": "Server=awseb-e-dimv4pgcpg-stack-awsebrdsdatabase-t9atez32kzzv.cxgqqkemg9gv.af-south-1.rds.amazonaws.com; Database=SmartQualifyDEV; User Id=smartqualifydev; Password={mypassword}; TrustServerCertificate=true; Encrypt=False;"
    "DefaultConnection": "Host=awseb-e-tmpi2juhkc-stack-awsebrdsdatabase-xnd8m1nnlxta.cxgqqkemg9gv.af-south-1.rds.amazonaws.com;Port=5432;Database=SmartQualifyDEV;Username=postgres;Password={mypassword};SslMode=Require;"
  },
  "Jwt": {
    "Token": "{mytoken}",
    "Issuer": "https://localhost:7292/",
    "Audience": "https://localhost:7292/"
  },
  "OpenAI": {
    "ApiKey": "your-openai-api-key-here"
  },
  "Firebase": {
    "ServerKey": "your-firebase-server-key-here"
  },
  "ElastiCache": {
    "ValkeyEndpoint": "smartqualif-esmt7q.serverless.afs1.cache.amazonaws.com:6379",
    "UseSsl": true
  },
  "MailOptions": {

  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

I get an error when I run the endpoint:

RedisConnectionException: It was not possible to connect to the redis server(s) smartqualify-esmt7q.serverless.afs1.cache.amazonaws.com:6379/Interactive. ConnectTimeout

StackExchange.Redis.RedisConnectionException: 'The message timed out in the backlog attempting to send because no connection became available (10000ms) - Last Connection Exception: It was not possible to connect to the redis server(s) smartqualify-esmt7q.serverless.afs1.cache.amazonaws.com:6379/Interactive. ConnectTimeout, command=GET, timeout: 10000, inst: 0, qu: 1, qs: 0, aw: False, bw: SpinningDown, rs: NotStarted, ws: Initializing, in: 0, last-in: 0, cur-in: 0, sync-ops: 3, async-ops: 0, serverEndpoint: smartqualify-esmt7q.serverless.afs1.cache.amazonaws.com:6379, conn-sec: n/a, aoc: 0, mc: 1/1/0, mgr: 10 of 10 available, clientName: SIYANDA(SE.Redis-v2.9.17.8862), IOCP: (Busy=0,Free=1000,Min=1,Max=1000), WORKER: (Busy=1,Free=32766,Min=12,Max=32767), POOL: (Threads=5,QueuedItems=0,CompletedItems=1681,Timers=5), v: 2.9.17.8862 (Please take a look at this article for some common client-side issues that can cause timeouts: https://stackexchange.github.io/StackExchange.Redis/Timeouts)'

What I expect is being able to access Valkey and store the cached data there from access, and then retrieve the data when I run the getAllTemplates endpoint again.Any possible solution or help would be greatly appreciated

2
  • Hey There!. Have you checked that your program has access to the elasticache via IAM permission ? Maybe that is the problem. Commented Sep 11 at 20:56
  • Hi @Saleh I have been debugging and what I found is that when I deployed my api to elasticbeanstalk the endpoint works well with ElasticCache, and it only doesn't connect when I am running localhost. Is there a way I could run this with localhost? On a production environment it works and it caches well, I also want to do so on a dev environment with localhost Commented Sep 11 at 22:59

1 Answer 1

0

I managed to come up with a solution, AWS ElastiCache seems like it doesn't support localhost so I ran the api with a docker container so we can setup valkey, and it works like a charm, it also didn't affect the deployed api which is great.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.