Code
·
144 lines
·
4447 bytes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Collections.Concurrent;
using System.Threading;
namespace Microsoft.Extensions.DependencyInjection;
/// <summary>
/// A background service that manages session-scoped hosted services.
/// </summary>
/// <typeparam name="T">The type of the hosted service.</typeparam>
public class SessionScopedFactory<T>(
IServiceProvider serviceProvider,
IHttpContextAccessor httpContextAccessor,
ILoggerFactory loggerFactory) : BackgroundService where T : IHostedService
{
/// <summary>
/// Logger for the session-scoped factory.
/// </summary>
private readonly ILogger Logger = loggerFactory.CreateLogger<SessionScopedFactory<T>>();
/// <summary>
/// A dictionary of session-scoped services, keyed by session ID.
/// </summary>
private readonly ConcurrentDictionary<string, SessionScopedWrapper> services = new();
/// <summary>
/// A dictionary to track session timeouts.
/// </summary>
private readonly ConcurrentDictionary<string, DateTimeOffset> timeout = new();
/// <summary>
/// Creates an instance of the scoped service.
/// </summary>
/// <returns>A session-scoped service.</returns>
private T GetScopedService()
{
using var scope = serviceProvider.CreateScope();
return scope.ServiceProvider.GetRequiredService<T>();
}
/// <summary>
/// Gets or creates the instance of the hosted service for the current session.
/// </summary>
public T Instance
{
get
{
var sessionId = (httpContextAccessor.HttpContext?.Session?.Id) ??
throw new InvalidOperationException("Session ID is not available.");
timeout.AddOrUpdate(sessionId, DateTimeOffset.UtcNow,
(key, oldValue) => DateTimeOffset.UtcNow);
if (!services.TryGetValue(sessionId, out SessionScopedWrapper? wrapper))
{
var service = GetScopedService() ??
throw new InvalidOperationException("Cannot get scoped service.");
wrapper = new SessionScopedWrapper(service);
if (services.TryAdd(sessionId, wrapper))
{
wrapper.Start();
Logger.LogInformation("Added started service with ID: {sessionId}", sessionId);
}
}
return (T)wrapper.hostedService;
}
}
/// <summary>
/// Removes a service associated with the given session ID.
/// </summary>
/// <param name="sessionId">The session ID to remove.</param>
/// <returns>A task representing the asynchronous operation.</returns>
private async Task RemoveServiceAsync(string sessionId)
{
// Remove service if it exists
if (services.TryRemove(sessionId, out SessionScopedWrapper? service))
{
await service.StopAsync();
Logger.LogInformation("Removed stopped service with ID: {sessionId}", sessionId);
service = null; // Let the GC collect it
}
// Remove the timeout entry
timeout.TryRemove(sessionId, out _);
}
/// <summary>
/// Stops all services and cleans up.
/// </summary>
public override async Task StopAsync(CancellationToken cancellationToken)
{
foreach (var id in services.Keys.ToArray())
{
await RemoveServiceAsync(id);
}
}
/// <summary>
/// Executes the background service and cleans up expired sessions.
/// </summary>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
Logger.LogInformation($"{nameof(SessionScopedFactory<T>)} starting.");
var sessionOptions = serviceProvider.GetRequiredService<IOptions<SessionOptions>>();
var sessionTimeout = sessionOptions.Value.IdleTimeout;
while (!cancellationToken.IsCancellationRequested)
{
try
{
// Non-Configurable interval duration (good sampling)
await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
// Determine new threshold for expired items
var expirationThreshold = DateTimeOffset.UtcNow.Subtract(sessionTimeout);
var sessionIds = timeout.Where(x => x.Value < expirationThreshold).Select(x => x.Key).ToArray();
foreach (var sessionId in sessionIds)
{
await RemoveServiceAsync(sessionId);
}
}
catch (TaskCanceledException)
{
}
catch (Exception ex)
{
// Log errors during the loop
Logger.LogError(ex, "An error occurred during timeout cleanup.");
}
}
Logger.LogInformation($"{nameof(SessionScopedFactory<T>)} has stopped.");
}
}