Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge Redis integration #112

Merged
merged 21 commits into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
904ab9e
Don't show "submitted by" user email public for privacy reasons. (#88)
Zifah Aug 8, 2024
2d2b9ea
Let the variants list link to the variant name. (#89)
Zifah Aug 8, 2024
127767e
Allow privileged users to manage GeoLocations list. (#91)
Zifah Aug 15, 2024
f7475da
Use the CreateDto in all POST/PUT requests to avoid validation proble…
Zifah Aug 15, 2024
b2a4953
Trim string fields during conversion of request DTOs into entities. (…
Zifah Aug 17, 2024
9c9d442
Feature/publish to twitter (#95)
Zifah Aug 23, 2024
709ebb2
Upgrade deployment workflow for the API to.NET 8 on all environments.…
Zifah Aug 23, 2024
219ad16
Maintenance/ivw prep fixes (#98)
Zifah Aug 26, 2024
6ffa9a9
Prevent name posting service from crashing. (#99)
Zifah Aug 30, 2024
51d1136
Merge branch 'PROD' into main
Zifah Aug 30, 2024
4c758d4
Fix social media sharing tags. (#102)
Zifah Sep 2, 2024
ca905a2
Remove unnecessary encoding of base URL in BasePageModel. (#103)
Zifah Sep 2, 2024
4f0732b
Encode name and meaning for social media sharing. (#104)
Zifah Sep 2, 2024
06e57b1
Replace Task.Delay with PeriodicTimer(). (#101)
Zifah Sep 2, 2024
639648d
Merge branch 'PROD' into main
Zifah Sep 2, 2024
d2795de
Don't Encode Text Parts (#106)
Zifah Sep 2, 2024
5cb47b1
Merge branch 'PROD' into main
Zifah Sep 2, 2024
7f40406
Use Hangfire to schedule tweets. (#108)
Zifah Sep 13, 2024
6c6d1b1
Feature/use distributed caching (#110)
Zifah Sep 16, 2024
6867a3e
Reduce tweet attempts to just 3 (#111)
Zifah Sep 16, 2024
20e1acc
Merge branch 'PROD' into main
Zifah Sep 16, 2024
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
1 change: 1 addition & 0 deletions Api/Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<DockerComposeProjectPath>..\docker-compose.dcproj</DockerComposeProjectPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Ardalis.GuardClauses" Version="4.6.0" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.5.0" />
Expand Down
6 changes: 3 additions & 3 deletions Api/Controllers/SearchController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ public async Task<IActionResult> Search([FromQuery(Name = "q"), Required] string
var matches = await _searchService.Search(searchTerm);

// TODO: Check if the comparison here removes takes diacrits into consideration
if (matches.Count() == 1 && matches.First().Name.ToLower() == searchTerm.ToLower())
if (matches.Count() == 1 && matches.First().Name.Equals(searchTerm, StringComparison.CurrentCultureIgnoreCase))
{
await _eventPubService.PublishEvent(new ExactNameSearched(searchTerm));
await _eventPubService.PublishEvent(new ExactNameSearched(matches.First().Name));
}

return Ok(matches.MapToDtoCollection());
Expand Down Expand Up @@ -96,7 +96,7 @@ public async Task<IActionResult> SearchOne(string searchTerm)

if(nameEntry != null)
{
await _eventPubService.PublishEvent(new ExactNameSearched(searchTerm));
await _eventPubService.PublishEvent(new ExactNameSearched(nameEntry.Name));
}

return Ok(nameEntry);
Expand Down
31 changes: 23 additions & 8 deletions Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
using System.Text.Json.Serialization;
using Hangfire;
using Infrastructure.Hangfire;
using Infrastructure.Redis;
using Ardalis.GuardClauses;

var builder = WebApplication.CreateBuilder(args);
var configuration = builder.Configuration;
Expand All @@ -44,8 +46,15 @@
services.AddAuthorization(options =>
{
options.AddPolicy("AdminOnly", policy => policy.RequireRole(Role.ADMIN.ToString()));
options.AddPolicy("AdminAndLexicographers", policy => policy.RequireRole(Role.ADMIN.ToString(), Role.PRO_LEXICOGRAPHER.ToString(), Role.BASIC_LEXICOGRAPHER.ToString()));
options.AddPolicy("AdminAndProLexicographers", policy => policy.RequireRole(Role.ADMIN.ToString(), Role.PRO_LEXICOGRAPHER.ToString()));
options.AddPolicy("AdminAndLexicographers", policy => policy.RequireRole(
Role.ADMIN.ToString(),
Role.PRO_LEXICOGRAPHER.ToString(),
Role.BASIC_LEXICOGRAPHER.ToString()
));
options.AddPolicy("AdminAndProLexicographers", policy => policy.RequireRole(
Role.ADMIN.ToString(),
Role.PRO_LEXICOGRAPHER.ToString()
));
});

services.AddControllers().AddJsonOptions(options =>
Expand Down Expand Up @@ -87,10 +96,12 @@
});
});
var mongoDbSettings = configuration.GetRequiredSection("MongoDB");
services.InitializeDatabase(mongoDbSettings.GetValue<string>("ConnectionString"), mongoDbSettings.GetValue<string>("DatabaseName"));
services.InitializeDatabase(
Guard.Against.NullOrEmpty(mongoDbSettings.GetValue<string>("ConnectionString")),
Guard.Against.NullOrEmpty(mongoDbSettings.GetValue<string>("DatabaseName")));

builder.Services.AddTransient(x =>
new MySqlConnection(builder.Configuration.GetSection("MySQL:ConnectionString").Value));
new MySqlConnection(Guard.Against.NullOrEmpty(configuration.GetSection("MySQL:ConnectionString").Value)));

services.AddSingleton<NameEntryService>();
services.AddSingleton<GeoLocationsService>();
Expand All @@ -103,8 +114,8 @@
services.AddScoped<EmbeddedVideoValidator>();
services.AddScoped<EtymologyValidator>();
services.AddScoped<SqlToMongoMigrator>();
services.AddSingleton<IRecentIndexesCache, RecentIndexesCache>();
services.AddSingleton<IRecentSearchesCache, RecentSearchesCache>();
services.AddSingleton<IRecentIndexesCache, RedisRecentIndexesCache>();
services.AddSingleton<IRecentSearchesCache, RedisRecentSearchesCache>();

//Validation
services.AddValidatorsFromAssemblyContaining<CreateUserValidator>();
Expand All @@ -116,7 +127,8 @@
services.AddTwitterClient(configuration);

builder.Services.AddMemoryCache();
builder.Services.SetupHangfire(configuration.GetRequiredSection("MongoDB:ConnectionString").Value!);
builder.Services.SetupHangfire(Guard.Against.NullOrEmpty(configuration.GetRequiredSection("MongoDB:ConnectionString").Value));
builder.Services.SetupRedis(configuration);


var app = builder.Build();
Expand All @@ -137,6 +149,9 @@

app.MapControllers();

app.UseHangfireDashboard("/backJobMonitor");
if (app.Environment.IsDevelopment())
{
app.UseHangfireDashboard("/backJobMonitor");
}

app.Run();
3 changes: 3 additions & 0 deletions Api/appsettings.Development.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,8 @@
"Twitter": {
"TweetTemplate": "{name}: \"{meaning}\" {link}",
"TweetIntervalSeconds": 60
},
"Redis": {
"DatabaseIndex": 1
}
}
6 changes: 6 additions & 0 deletions Api/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"Redis": "redis:6379"
},
"Twitter": {
"AccessToken": "your-access-token",
"AccessTokenSecret": "your-access-token-secret",
Expand All @@ -15,5 +18,8 @@
"NameUrlPrefix": "https://www.yorubaname.com/entries",
"TweetTemplate": "New name entry: {name}, {meaning}. More here: {link}",
"TweetIntervalSeconds": 180
},
"Redis": {
"DatabaseIndex": 0
}
}
39 changes: 39 additions & 0 deletions Application/Cache/ICacheService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Application.Cache
{
public interface ICacheService<T>
{
/// <summary>
/// Retrieves a collection of items from the cache.
/// </summary>
/// <param name="key">The key under which the items are cached.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a collection of items.</returns>
Task<IEnumerable<T>> GetAsync(string key);

/// <summary>
/// Adds an item to the cache and updates its recency.
/// </summary>
/// <param name="key">The key under which the item is cached.</param>
/// <param name="item">The item to be added to the cache.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task StackAsync(string key, T item);

/// <summary>
/// Removes an item from the cache.
/// </summary>
/// <param name="key">The key of the item to be removed.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a boolean indicating whether the removal was successful.</returns>
Task RemoveAsync(string key, string searchTerm);

/// <summary>
/// Retrieves the most popular items based on their frequency.
/// </summary>
/// <returns>A task that represents the asynchronous operation. The task result contains a collection of the most popular items.</returns>
Task<IEnumerable<string>> GetMostPopularAsync(string key);
}
}
63 changes: 0 additions & 63 deletions Application/Cache/InMemoryCache.cs

This file was deleted.

8 changes: 0 additions & 8 deletions Application/Cache/RecentIndexesCache.cs

This file was deleted.

28 changes: 0 additions & 28 deletions Application/Cache/RecentSearchesCache.cs

This file was deleted.

7 changes: 7 additions & 0 deletions Infrastructure/Configuration/RedisConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Infrastructure.Configuration
{
public record RedisConfig
{
public int DatabaseIndex { get; set; }
}
}
3 changes: 2 additions & 1 deletion Infrastructure/Hangfire/DependencyInjection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Builder;
using Api.Utilities;
using Ardalis.GuardClauses;

namespace Infrastructure.Hangfire
{
Expand All @@ -20,7 +21,7 @@ public static IServiceCollection SetupHangfire(this IServiceCollection services,
.SetDataCompatibilityLevel(CompatibilityLevel.Version_180)
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UseMongoStorage(mongoClient, mongoUrlBuilder.DatabaseName, new MongoStorageOptions
.UseMongoStorage(mongoClient, Guard.Against.NullOrEmpty(mongoUrlBuilder.DatabaseName), new MongoStorageOptions
{
MigrationOptions = new MongoMigrationOptions
{
Expand Down
2 changes: 2 additions & 0 deletions Infrastructure/Infrastructure.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Ardalis.GuardClauses" Version="4.6.0" />
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.14" />
<PackageReference Include="Hangfire.Mongo" Version="1.10.8" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
<PackageReference Include="StackExchange.Redis" Version="2.8.16" />
<PackageReference Include="TweetinviAPI" Version="5.0.4" />
</ItemGroup>

Expand Down
21 changes: 21 additions & 0 deletions Infrastructure/Redis/DependencyInjection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Ardalis.GuardClauses;
using Infrastructure.Configuration;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using StackExchange.Redis;

namespace Infrastructure.Redis
{
public static class DependencyInjection
{
private const string SectionName = "Redis";

public static IServiceCollection SetupRedis(this IServiceCollection services, IConfiguration configuration)
{
services.Configure<RedisConfig>(configuration.GetRequiredSection(SectionName));
var redisConnectionString = Guard.Against.NullOrEmpty(configuration.GetConnectionString(SectionName));
services.AddSingleton<IConnectionMultiplexer>(ConnectionMultiplexer.Connect(redisConnectionString));
return services;
}
}
}
13 changes: 13 additions & 0 deletions Infrastructure/Redis/RedisCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Infrastructure.Configuration;
using Microsoft.Extensions.Options;
using StackExchange.Redis;

namespace Infrastructure.Redis
{
public abstract class RedisCache(
IConnectionMultiplexer connectionMultiplexer,
IOptions<RedisConfig> redisConfig)
{
protected readonly IDatabase _cache = connectionMultiplexer.GetDatabase(redisConfig.Value.DatabaseIndex);
}
}
46 changes: 46 additions & 0 deletions Infrastructure/Redis/RedisRecentIndexesCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using Core.Cache;
using Infrastructure.Configuration;
using Microsoft.Extensions.Options;
using StackExchange.Redis;

namespace Infrastructure.Redis
{
public class RedisRecentIndexesCache(
IConnectionMultiplexer connectionMultiplexer,
IOptions<RedisConfig> redisConfig) : RedisCache(connectionMultiplexer, redisConfig), IRecentIndexesCache
{
private const string Key = "recent_indexes";
private const int MaxItemsToReturn = 5;
private const int MaxItemsToStore = 10;

public async Task<IEnumerable<string>> Get()
{
var results = await _cache.SortedSetRangeByRankAsync(Key, 0, MaxItemsToReturn - 1, Order.Descending);
return results.Select(r => r.ToString());
}

public async Task<bool> Remove(string item)
{
var tran = _cache.CreateTransaction();
_ = tran.SortedSetRemoveAsync(Key, item);
return await tran.ExecuteAsync();
}

public async Task Stack(string item)
{
// Use a Redis transaction to ensure atomicity of both operations
var transaction = _cache.CreateTransaction();

// Add the search term to the front of the Redis list
_ = transaction.SortedSetAddAsync(Key, item, DateTime.UtcNow.Ticks);
_ = transaction.SortedSetRemoveRangeByRankAsync(Key, 0, -(MaxItemsToStore + 1));

// Execute the transaction
bool committed = await transaction.ExecuteAsync();
if (!committed)
{
throw new Exception("Redis Transaction failed");
}
}
}
}
Loading
Loading