Skip to content

Commit

Permalink
Merge Hangfire integration into PROD (#109)
Browse files Browse the repository at this point in the history
* Use Hangfire to schedule tweets. (#108)

Also, create unique index on the NameEntries_Name column.
  • Loading branch information
Zifah authored Sep 13, 2024
1 parent 92220af commit d05328a
Show file tree
Hide file tree
Showing 20 changed files with 212 additions and 133 deletions.
16 changes: 10 additions & 6 deletions Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@
using Core.Events;
using Core.StringObjectConverters;
using FluentValidation;
using Infrastructure;
using Infrastructure.Twitter;
using Infrastructure.MongoDB;
using Infrastructure.Services;
using Microsoft.AspNetCore.Authentication;
using Microsoft.OpenApi.Models;
using MySqlConnector;
using System.Collections.Concurrent;
using System.Text.Json.Serialization;
using Hangfire;
using Infrastructure.Hangfire;

var builder = WebApplication.CreateBuilder(args);
var configuration = builder.Configuration;
Expand Down Expand Up @@ -86,7 +86,7 @@
}
});
});
var mongoDbSettings = configuration.GetSection("MongoDB");
var mongoDbSettings = configuration.GetRequiredSection("MongoDB");
services.InitializeDatabase(mongoDbSettings.GetValue<string>("ConnectionString"), mongoDbSettings.GetValue<string>("DatabaseName"));

builder.Services.AddTransient(x =>
Expand All @@ -112,9 +112,11 @@
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(ExactNameSearchedAdapter).Assembly));

// Twitter integration configuration
services.AddSingleton<ConcurrentQueue<PostPublishedNameCommand>>();
services.AddSingleton<ITwitterService, TwitterService>();
services.AddTwitterClient(configuration);
services.AddHostedService<NamePostingService>();

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


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

app.MapControllers();

app.UseHangfireDashboard("/backJobMonitor");

app.Run();
2 changes: 1 addition & 1 deletion Api/appsettings.Development.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@
},
"Twitter": {
"TweetTemplate": "{name}: \"{meaning}\" {link}",
"TweetIntervalSeconds": 5
"TweetIntervalSeconds": 60
}
}
13 changes: 5 additions & 8 deletions Application/Application.csproj
Original file line number Diff line number Diff line change
@@ -1,29 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">

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

<ItemGroup>
<Compile Remove="Interfaces\**" />
<EmbeddedResource Remove="Interfaces\**" />
<None Remove="Interfaces\**" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="Dapper" Version="2.1.35" />
<PackageReference Include="FluentValidation" Version="11.9.1" />
<PackageReference Include="MediatR" Version="12.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
<PackageReference Include="MongoDB.Driver" Version="2.23.1" />
<PackageReference Include="MySqlConnector" Version="2.3.7" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Core\Core.csproj" />
</ItemGroup>

</Project>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
</Project>
2 changes: 1 addition & 1 deletion Application/EventHandlers/NameIndexedEventHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public NameIndexedEventHandler(
public async Task Handle(NameIndexedAdapter notification, CancellationToken cancellationToken)
{
await _recentIndexesCache.Stack(notification.Name);
await _mediator.Publish(new PostPublishedNameCommand(notification.Name), cancellationToken);
await _mediator.Publish(new PostPublishedNameCommand(notification.Name, notification.Meaning), cancellationToken);
}
}
}
18 changes: 8 additions & 10 deletions Application/EventHandlers/PostPublishedNameCommandHandler.cs
Original file line number Diff line number Diff line change
@@ -1,25 +1,23 @@
using Application.Events;
using Application.Services;
using MediatR;
using System.Collections.Concurrent;

namespace Application.EventHandlers
{
public class PostPublishedNameCommandHandler : INotificationHandler<PostPublishedNameCommand>
{
private readonly ConcurrentQueue<PostPublishedNameCommand> _nameQueue;
private readonly ITwitterService _twitterService;

public PostPublishedNameCommandHandler(ConcurrentQueue<PostPublishedNameCommand> nameQueue)
public PostPublishedNameCommandHandler(
ITwitterService twitterService)
{
_nameQueue = nameQueue;
_twitterService = twitterService;

}

public Task Handle(PostPublishedNameCommand notification, CancellationToken cancellationToken)
public async Task Handle(PostPublishedNameCommand notification, CancellationToken cancellationToken)
{
// Enqueue the indexed name for processing by the BackgroundService
_nameQueue.Enqueue(notification);

// Return a completed task, so it doesn't block the main thread
return Task.CompletedTask;
await _twitterService.PostNewNameAsync(notification.Name, notification.Meaning, cancellationToken);
}
}
}
2 changes: 1 addition & 1 deletion Application/Events/NameIndexedAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace Application.Events;

public record NameIndexedAdapter : NameIndexed, INotification
{
public NameIndexedAdapter(NameIndexed theEvent) : base(theEvent.Name)
public NameIndexedAdapter(NameIndexed theEvent) : base(theEvent.Name, theEvent.Meaning)
{
}
}
2 changes: 1 addition & 1 deletion Application/Events/PostPublishedNameCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace Application.Events
{
public record PostPublishedNameCommand(string Name) : INotification
public record PostPublishedNameCommand(string Name, string Meaning) : INotification
{
}
}
9 changes: 4 additions & 5 deletions Application/Services/BasicAuthHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,21 @@ public BasicAuthenticationHandler(
IUserRepository userRepository,
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock)
: base(options, logger, encoder, clock)
UrlEncoder encoder)
: base(options, logger, encoder)
{
_userRepository = userRepository;
_logger = logger.CreateLogger<BasicAuthenticationHandler>();
}

protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!Request.Headers.ContainsKey("Authorization"))
if (!Request.Headers.TryGetValue("Authorization", out Microsoft.Extensions.Primitives.StringValues value))
return await Task.FromResult(AuthenticateResult.Fail("Missing Authorization Header"));

try
{
(string username, string password) = DecodeBasicAuthToken(Request.Headers["Authorization"]);
(string username, string password) = DecodeBasicAuthToken(value!);

var matchingUser = await AuthenticateUser(username, password);
if (matchingUser == null)
Expand Down
7 changes: 7 additions & 0 deletions Application/Services/ITwitterService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Application.Services
{
public interface ITwitterService
{
Task PostNewNameAsync(string name, string meaning, CancellationToken cancellationToken);
}
}
2 changes: 1 addition & 1 deletion Application/Services/NameEntryService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ public async Task PublishName(NameEntry nameEntry, string username)
await _nameEntryRepository.Update(originalName, nameEntry);

// TODO Later: Use the outbox pattern to enforce event publishing after the DB update (https://www.youtube.com/watch?v=032SfEBFIJs&t=913s).
await _eventPubService.PublishEvent(new NameIndexed(nameEntry.Name));
await _eventPubService.PublishEvent(new NameIndexed(nameEntry.Name, nameEntry.Meaning));
}

public async Task<NameEntry?> UpdateNameWithUnpublish(NameEntry nameEntry)
Expand Down
6 changes: 2 additions & 4 deletions Core/Core.csproj
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

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

</Project>
</Project>
10 changes: 2 additions & 8 deletions Core/Events/NameIndexed.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Core.Events
namespace Core.Events
{
public record NameIndexed(string Name)
public record NameIndexed(string Name, string Meaning)
{
}
}
10 changes: 3 additions & 7 deletions Infrastructure.MongoDB/Infrastructure.MongoDB.csproj
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

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

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
<PackageReference Include="MongoDB.Driver" Version="2.23.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Core\Core.csproj" />
</ItemGroup>

</Project>
</Project>
14 changes: 14 additions & 0 deletions Infrastructure.MongoDB/Repositories/NameEntryRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,20 @@ public NameEntryRepository(
{
_nameEntryCollection = database.GetCollection<NameEntry>("NameEntries");
_eventPubService = eventPubService;

CreateIndexes();
}

private void CreateIndexes()
{
var indexKeys = Builders<NameEntry>.IndexKeys.Ascending(x => x.Name);
var indexOptions = new CreateIndexOptions
{
Unique = true,
Name = "IX_NameEntries_Name_Unique",
Background = true
};
_nameEntryCollection.Indexes.CreateOne(new CreateIndexModel<NameEntry>(indexKeys, indexOptions));
}

public async Task<NameEntry> FindById(string id)
Expand Down
51 changes: 51 additions & 0 deletions Infrastructure/Hangfire/DependencyInjection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using Hangfire;
using Hangfire.Mongo;
using MongoDB.Driver;
using Hangfire.Mongo.Migration.Strategies;
using Hangfire.Mongo.Migration.Strategies.Backup;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Builder;
using Api.Utilities;

namespace Infrastructure.Hangfire
{
public static class DependencyInjection
{
public static IServiceCollection SetupHangfire(this IServiceCollection services, string mongoConnectionString)
{
var mongoUrlBuilder = new MongoUrlBuilder(mongoConnectionString);
var mongoClient = new MongoClient(mongoUrlBuilder.ToMongoUrl());

services.AddHangfire(configuration => configuration
.SetDataCompatibilityLevel(CompatibilityLevel.Version_180)
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UseMongoStorage(mongoClient, mongoUrlBuilder.DatabaseName, new MongoStorageOptions
{
MigrationOptions = new MongoMigrationOptions
{
MigrationStrategy = new MigrateMongoMigrationStrategy(),
BackupStrategy = new CollectionMongoBackupStrategy()
},
Prefix = "hangfire.mongo",
CheckConnection = true,
CheckQueuedJobsStrategy = CheckQueuedJobsStrategy.TailNotificationsCollection
})
);

services.AddHangfireServer(serverOptions =>
{
serverOptions.ServerName = "Hangfire.Mongo server 1";
});
return services;
}

public static void UseHangfireDashboard(this IApplicationBuilder app, string dashboardPath)
{
app.UseHangfireDashboard(dashboardPath, new DashboardOptions
{
Authorization = [new HangfireAuthFilter()]
});
}
}
}
12 changes: 12 additions & 0 deletions Infrastructure/Hangfire/HangfireAuthFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Hangfire.Dashboard;

namespace Api.Utilities
{
public class HangfireAuthFilter : IDashboardAuthorizationFilter
{
public bool Authorize(DashboardContext context)
{
return context.GetHttpContext().Request.Host.Host == "localhost";
}
}
}
2 changes: 2 additions & 0 deletions Infrastructure/Infrastructure.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
</PropertyGroup>

<ItemGroup>
<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" />
Expand Down
Loading

0 comments on commit d05328a

Please sign in to comment.