Skip to content

Commit 7fa8a8f

Browse files
authored
Create a legacy event when a new SupportTaskCreatedEvent is created (#2870)
We're moving support tasks to the new style events but we still have a bunch of places looking for the legacy events (Change History UI, reporting etc.). This adds an event handler that will create a legacy `SupportTaskCreatedEvent` when a new `SupportTaskCreatedEvent` is published.
1 parent 63baf70 commit 7fa8a8f

File tree

8 files changed

+221
-0
lines changed

8 files changed

+221
-0
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using TeachingRecordSystem.Core.DataStore.Postgres;
2+
3+
namespace TeachingRecordSystem.Core.EventHandlers;
4+
5+
public class CreateLegacySupportTaskEvents(TrsDbContext dbContext) : IEventHandler<SupportTaskCreatedEvent>
6+
{
7+
public async Task HandleEventAsync(SupportTaskCreatedEvent @event, ProcessContext processContext)
8+
{
9+
var legacyEvent = new LegacyEvents.SupportTaskCreatedEvent
10+
{
11+
EventId = @event.EventId,
12+
CreatedUtc = processContext.Now,
13+
RaisedBy = processContext.Process.UserId!,
14+
SupportTask = @event.SupportTask
15+
};
16+
17+
dbContext.AddEventWithoutBroadcast(legacyEvent);
18+
19+
await dbContext.SaveChangesAsync();
20+
}
21+
}

TeachingRecordSystem/src/TeachingRecordSystem.Core/EventPublisher.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,23 @@ private async Task InvokeEventHandlersAsync(IEvent @event, ProcessContext proces
4444
{
4545
await handler.HandleEventAsync(@event, processContext);
4646
}
47+
48+
var eventType = @event.GetType();
49+
var typeSpecificHandlers = serviceProvider.GetServices(typeof(IEventHandler<>).MakeGenericType(eventType));
50+
51+
foreach (var handler in typeSpecificHandlers)
52+
{
53+
var wrapper = (IEventHandler)Activator.CreateInstance(typeof(TypedHandlerWrapper<>).MakeGenericType(eventType), handler)!;
54+
await wrapper.HandleEventAsync(@event, processContext);
55+
}
56+
}
57+
58+
private class TypedHandlerWrapper<TEvent>(IEventHandler<TEvent> innerHandler) : IEventHandler where TEvent : IEvent
59+
{
60+
public Task HandleEventAsync(IEvent @event, ProcessContext processContext)
61+
{
62+
return innerHandler.HandleEventAsync(((TEvent)@event), processContext);
63+
}
4764
}
4865
}
4966

TeachingRecordSystem/src/TeachingRecordSystem.Core/Extensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ public static IServiceCollection AddEventPublisher(this IServiceCollection servi
146146

147147
services.Scan(s => s.FromAssemblyOf<IEvent>()
148148
.AddClasses(c => c.AssignableTo<IEventHandler>())
149+
.AddClasses(c => c.AssignableTo(typeof(IEventHandler<>)))
149150
.AsImplementedInterfaces()
150151
.WithTransientLifetime());
151152

TeachingRecordSystem/src/TeachingRecordSystem.Core/IEventHandler.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,8 @@ public interface IEventHandler
66
{
77
Task HandleEventAsync(IEvent @event, ProcessContext processContext);
88
}
9+
10+
public interface IEventHandler<TEvent> where TEvent : IEvent
11+
{
12+
Task HandleEventAsync(TEvent @event, ProcessContext processContext);
13+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using Microsoft.Extensions.Configuration;
2+
using Microsoft.Extensions.DependencyInjection;
3+
using TeachingRecordSystem.Core.Tests.EventPipelineTests;
4+
using TeachingRecordSystem.TestCommon.Infrastructure;
5+
6+
[assembly: AssemblyFixture(typeof(EventPipelineFixture))]
7+
8+
namespace TeachingRecordSystem.Core.Tests.EventPipelineTests;
9+
10+
public class EventPipelineFixture : ServiceProviderFixture
11+
{
12+
protected override void ConfigureServices(IServiceCollection services, IConfiguration configuration)
13+
{
14+
services
15+
.AddSingleton<TestData>()
16+
.AddSingleton<ReferenceDataCache>()
17+
.AddEventPublisher();
18+
19+
PublishEventsDbCommandInterceptor.ConfigureServices(services);
20+
21+
TestScopedServices.ConfigureServices(services);
22+
}
23+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using TeachingRecordSystem.Core.DataStore.Postgres;
3+
4+
namespace TeachingRecordSystem.Core.Tests.EventPipelineTests;
5+
6+
public class EventPipelineTestBase
7+
{
8+
private readonly EventPipelineFixture _fixture;
9+
10+
protected EventPipelineTestBase(EventPipelineFixture fixture)
11+
{
12+
_fixture = fixture;
13+
14+
TestScopedServices.Reset();
15+
}
16+
17+
protected TestableClock Clock => TestScopedServices.GetCurrent().Clock;
18+
19+
protected IDbContextFactory<TrsDbContext> DbContextFactory => _fixture.Services.GetRequiredService<IDbContextFactory<TrsDbContext>>();
20+
21+
protected EventCapture Events => _fixture.Services.GetRequiredService<EventCapture>();
22+
23+
protected IEventPublisher EventPublisher => _fixture.Services.GetRequiredService<IEventPublisher>();
24+
25+
protected CaptureEventObserver LegacyEventObserver => TestScopedServices.GetCurrent().LegacyEventObserver;
26+
27+
protected IServiceProvider Services => _fixture.Services;
28+
29+
protected TestData TestData => Services.GetRequiredService<TestData>();
30+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using Optional.Unsafe;
2+
using TeachingRecordSystem.Core.DataStore.Postgres.Models;
3+
using TeachingRecordSystem.Core.Models.SupportTasks;
4+
5+
namespace TeachingRecordSystem.Core.Tests.EventPipelineTests;
6+
7+
public class SupportTaskEventPipelineTests(EventPipelineFixture fixture) : EventPipelineTestBase(fixture)
8+
{
9+
[Fact]
10+
public async Task SupportTaskCreatedEventPublished_EmitsLegacySupportTaskCreatedEvent()
11+
{
12+
// Arrange
13+
var processContext = new ProcessContext(default, Clock.UtcNow, SystemUser.SystemUserId);
14+
15+
var @event = new SupportTaskCreatedEvent
16+
{
17+
EventId = Guid.NewGuid(),
18+
SupportTask = new EventModels.SupportTask
19+
{
20+
SupportTaskReference = "ABC-123",
21+
SupportTaskType = SupportTaskType.ChangeDateOfBirthRequest,
22+
Status = SupportTaskStatus.Open,
23+
OneLoginUserSubject = null,
24+
PersonId = Guid.NewGuid(),
25+
Data = new ChangeDateOfBirthRequestData
26+
{
27+
DateOfBirth = new(2000, 1, 1),
28+
EvidenceFileId = Guid.NewGuid(),
29+
EvidenceFileName = "evidence.jpeg",
30+
EmailAddress = TestData.GenerateUniqueEmail(),
31+
ChangeRequestOutcome = null
32+
}
33+
}
34+
};
35+
36+
// Act
37+
await EventPublisher.PublishEventAsync(@event, processContext);
38+
39+
// Assert
40+
LegacyEventObserver.AssertEventsSaved(
41+
e =>
42+
{
43+
var legacyEvent = Assert.IsType<LegacyEvents.SupportTaskCreatedEvent>(e);
44+
Assert.Equal(@event.EventId, legacyEvent.EventId);
45+
Assert.Equal(@event.SupportTask, legacyEvent.SupportTask);
46+
Assert.Equal(processContext.Now, legacyEvent.CreatedUtc);
47+
Assert.Equal(@event.PersonIds.SingleOrDefault(), legacyEvent.PersonId.ToNullable());
48+
});
49+
}
50+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
using Microsoft.Extensions.DependencyInjection;
3+
using TeachingRecordSystem.TestCommon.Infrastructure;
4+
5+
namespace TeachingRecordSystem.Core.Tests.EventPipelineTests;
6+
7+
public class TestScopedServices
8+
{
9+
private static readonly AsyncLocal<TestScopedServices> _current = new();
10+
11+
public TestScopedServices()
12+
{
13+
Clock = new();
14+
Events = new();
15+
LegacyEventObserver = new();
16+
}
17+
18+
public TestableClock Clock { get; }
19+
20+
public EventCapture Events { get; }
21+
22+
public CaptureEventObserver LegacyEventObserver { get; }
23+
24+
public static void ConfigureServices(IServiceCollection services) =>
25+
services
26+
.AddSingleton<IClock>(new ForwardToTestScopedClock())
27+
.AddTestScoped<EventCapture>(tss => tss.Events)
28+
.AddTransient<IEventHandler>(sp => sp.GetRequiredService<EventCapture>())
29+
.AddSingleton<IEventObserver>(new ForwardToTestScopedEventObserver());
30+
31+
public static TestScopedServices GetCurrent() =>
32+
TryGetCurrent(out var current) ? current : throw new InvalidOperationException("No current instance has been set.");
33+
34+
public static TestScopedServices Reset()
35+
{
36+
if (_current.Value is not null)
37+
{
38+
throw new InvalidOperationException("Current instance has already been set.");
39+
}
40+
41+
return _current.Value = new();
42+
}
43+
44+
public static bool TryGetCurrent([NotNullWhen(true)] out TestScopedServices? current)
45+
{
46+
if (_current.Value is TestScopedServices tss)
47+
{
48+
current = tss;
49+
return true;
50+
}
51+
52+
current = default;
53+
return false;
54+
}
55+
56+
private class ForwardToTestScopedClock : IClock
57+
{
58+
public DateTime UtcNow => GetCurrent().Clock.UtcNow;
59+
}
60+
61+
private class ForwardToTestScopedEventObserver : IEventObserver
62+
{
63+
public void OnEventCreated(LegacyEvents.EventBase @event) => GetCurrent().LegacyEventObserver.OnEventCreated(@event);
64+
}
65+
}
66+
67+
file static class ServiceCollectionExtensions
68+
{
69+
public static IServiceCollection AddTestScoped<T>(this IServiceCollection services, Func<TestScopedServices, T> resolveService)
70+
where T : class
71+
{
72+
return services.AddTransient(_ => resolveService(TestScopedServices.GetCurrent()));
73+
}
74+
}

0 commit comments

Comments
 (0)