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 Hangfire integration into PROD #109

Merged
merged 18 commits into from
Sep 13, 2024
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
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
Loading