Added Distributed Caching with Redis by ScriptSage001 · Pull Request #15 · ScriptSage001/Shortify.NET · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Shortify.NET.API/appsettings.Development.json
3 changes: 2 additions & 1 deletion Shortify.NET.API/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
},
"AllowedHosts": "*",
"ConnectionStrings": {
"Shortify.NETDB": "${DB_CONNECTION}"
"Shortify.NETDB": "${DB_CONNECTION}",
"Shortify.NET_Redis": "${REDIS_CONNECTION}"
},
"AppSettings": {
"Secret": "${APP_SECRET}",
Expand Down
60 changes: 60 additions & 0 deletions Shortify.NET.Applicaion/Abstractions/ICachingServices.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
namespace Shortify.NET.Applicaion.Abstractions
{
/// <summary>
/// Defines the interface for caching services, providing methods for getting, setting,
/// and removing cached items.
/// </summary>
public interface ICachingServices
{
/// <summary>
/// Retrieves a cached item by its key.
/// </summary>
/// <typeparam name="T">The type of the cached item.</typeparam>
/// <param name="key">The key of the cached item.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>
/// A task that represents the asynchronous operation. The task result contains the cached item,
/// or null if the item does not exist.
/// </returns>
Task<T?> GetAsync<T>(string key, CancellationToken cancellationToken = default) where T : class;

/// <summary>
/// Retrieves a cached item by its key. If the item does not exist, it is created using the specified factory function
/// and added to the cache.
/// </summary>
/// <typeparam name="T">The type of the cached item.</typeparam>
/// <param name="key">The key of the cached item.</param>
/// <param name="factory">A function to create the item if it does not exist in the cache.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>
/// A task that represents the asynchronous operation. The task result contains the cached item.
/// </returns>
Task<T> GetAsync<T>(string key, Func<Task<T>> factory, CancellationToken cancellationToken = default) where T : class;

/// <summary>
/// Adds or updates a cached item with the specified key and value.
/// </summary>
/// <typeparam name="T">The type of the item to be cached.</typeparam>
/// <param name="key">The key of the cached item.</param>
/// <param name="value">The value of the cached item.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task SetAsync<T>(string key, T value, CancellationToken cancellationToken = default) where T : class;

/// <summary>
/// Removes a cached item by its key.
/// </summary>
/// <param name="key">The key of the cached item to remove.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task RemoveAsync(string key, CancellationToken cancellationToken = default);

/// <summary>
/// Removes cached items by their key prefix.
/// </summary>
/// <param name="prefix">The prefix of the keys of the cached items to remove.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task RemoveByPrefixAsync(string prefix, CancellationToken cancellationToken = default);
}
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,57 @@
using Shortify.NET.Applicaion.Abstractions.Repositories;
using Shortify.NET.Applicaion.Abstractions;
using Shortify.NET.Applicaion.Abstractions.Repositories;
using Shortify.NET.Common.FunctionalTypes;
using Shortify.NET.Common.Messaging.Abstractions;
using Shortify.NET.Core.Errors;

namespace Shortify.NET.Applicaion.Url.Queries.GetOriginalUrl
{
internal sealed class GetOriginalUrlQueryHandler(IShortenedUrlRepository shortenedUrlRepository)
internal sealed class GetOriginalUrlQueryHandler(
IShortenedUrlRepository shortenedUrlRepository,
ICachingServices cachingServices)
: IQueryHandler<GetOriginalUrlQuery, string>
{
private readonly IShortenedUrlRepository _shortenedUrlRepository = shortenedUrlRepository;
private readonly ICachingServices _cachingServices = cachingServices;

public async Task<Result<string>> Handle(GetOriginalUrlQuery query, CancellationToken cancellationToken)
{
#region GetFromCache

string cacheKey = $"Original_Url_{query.Code}";
var originalUrl = await _cachingServices
.GetAsync<string>(
cacheKey,
cancellationToken);

#endregion

if (originalUrl is not null)
{
return originalUrl;
}

#region GetFromDB

var shortenedUrl = await _shortenedUrlRepository.GetByCodeAsync(query.Code, cancellationToken);

#endregion

if (shortenedUrl == null)
{
return Result.Failure<string>(DomainErrors.ShortenedUrl.ShortenedUrlNotFound);
}

#region SetCache

await _cachingServices
.SetAsync(
cacheKey,
shortenedUrl.OriginalUrl,
cancellationToken);

#endregion

return shortenedUrl.OriginalUrl;
}
}
Expand Down
92 changes: 92 additions & 0 deletions Shortify.NET.Infrastructure/CachingServices.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using Microsoft.Extensions.Caching.Distributed;
using Newtonsoft.Json;
using Shortify.NET.Applicaion.Abstractions;
using System.Collections.Concurrent;

namespace Shortify.NET.Infrastructure
{
/// <summary>
/// Implementation of <see cref="ICachingServices"/> that provides caching functionality
/// using an <see cref="IDistributedCache"/>.
/// </summary>
public class CachingServices(IDistributedCache distributedCache) : ICachingServices
{
private static readonly ConcurrentDictionary<string, bool> CacheKeys = new();

private readonly IDistributedCache _distributedCache = distributedCache;

/// <inheritdoc/>
public async Task<T?> GetAsync<T>(
string key,
CancellationToken cancellationToken = default)
where T : class
{
var cachedValue = await _distributedCache
.GetStringAsync(key, cancellationToken);

return cachedValue is null ?
null :
JsonConvert.DeserializeObject<T>(cachedValue);
}

/// <inheritdoc/>
public async Task<T> GetAsync<T>(
string key,
Func<Task<T>> factory,
CancellationToken cancellationToken = default)
where T : class
{
T? cachedValue = await GetAsync<T>(key, cancellationToken);

if (cachedValue is not null)
{
return cachedValue;
}

cachedValue = await factory();

await SetAsync(key, cachedValue, cancellationToken);

return cachedValue;
}

/// <inheritdoc/>
public async Task SetAsync<T>(
string key,
T value,
CancellationToken cancellationToken = default)
where T : class
{
await _distributedCache
.SetStringAsync(
key,
JsonConvert.SerializeObject(value),
cancellationToken);

CacheKeys.TryAdd(key, true);
}

/// <inheritdoc/>
public async Task RemoveAsync(
string key,
CancellationToken cancellationToken = default)
{
await _distributedCache.RemoveAsync(key, cancellationToken);

CacheKeys.TryRemove(key, out bool _);
}

/// <inheritdoc/>
public async Task RemoveByPrefixAsync(
string prefix,
CancellationToken cancellationToken = default)
{
IEnumerable<Task> removeTasks = CacheKeys
.Keys
.Where(k => k.StartsWith(prefix))
.Select(k => RemoveAsync(k, cancellationToken));

await Task.WhenAll(removeTasks);
}
}
}
13 changes: 13 additions & 0 deletions Shortify.NET.Infrastructure/DependencyInjection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi
services.AddServices();
services.AddBackgroundJobs(configuration);
services.AddMiddleware();
services.AddCaching(configuration);

return services;
}
Expand Down Expand Up @@ -73,5 +74,17 @@ private static void AddMiddleware(this IServiceCollection services)
{
services.Decorate(typeof(INotificationHandler<>), typeof(IdempotentDomainEventHandler<>));
}

private static void AddCaching(this IServiceCollection services, IConfiguration configuration)
{
services.AddStackExchangeRedisCache(options =>
{
var redisConnection = configuration
.GetConnectionString("Shortify.NET_Redis");
options.Configuration = redisConnection;
});

services.AddSingleton<ICachingServices, CachingServices>();
}
}
}