Skip to content

Commit 349ebd3

Browse files
committed
Introduce base EF Core persistence abstractions
1 parent 57b79d0 commit 349ebd3

File tree

3 files changed

+138
-0
lines changed

3 files changed

+138
-0
lines changed
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
namespace ServiceControl.Persistence.Sql.Core.Abstractions;
2+
3+
using Microsoft.Extensions.DependencyInjection;
4+
using ServiceControl.Persistence;
5+
using ServiceControl.Persistence.MessageRedirects;
6+
using ServiceControl.Persistence.UnitOfWork;
7+
using Implementation;
8+
using Implementation.UnitOfWork;
9+
10+
public abstract class BasePersistence
11+
{
12+
protected static void RegisterDataStores(IServiceCollection services, bool maintenanceMode)
13+
{
14+
if (maintenanceMode)
15+
{
16+
return;
17+
}
18+
19+
services.AddSingleton<MinimumRequiredStorageState>();
20+
services.AddSingleton<ITrialLicenseDataProvider, TrialLicenseDataProvider>();
21+
services.AddSingleton<IEndpointSettingsStore, EndpointSettingsStore>();
22+
services.AddSingleton<IEventLogDataStore, EventLogDataStore>();
23+
services.AddSingleton<IMessageRedirectsDataStore, MessageRedirectsDataStore>();
24+
services.AddSingleton<IServiceControlSubscriptionStorage, ServiceControlSubscriptionStorage>();
25+
services.AddSingleton<IQueueAddressStore, QueueAddressStore>();
26+
services.AddSingleton<IMonitoringDataStore, MonitoringDataStore>();
27+
services.AddSingleton<ICustomChecksDataStore, CustomChecksDataStore>();
28+
services.AddSingleton<Operations.BodyStorage.IBodyStorage, BodyStorage>();
29+
services.AddSingleton<IRetryHistoryDataStore, RetryHistoryDataStore>();
30+
services.AddSingleton<IFailedErrorImportDataStore, FailedErrorImportDataStore>();
31+
services.AddSingleton<IExternalIntegrationRequestsDataStore, ExternalIntegrationRequestsDataStore>();
32+
services.AddSingleton<IFailedMessageViewIndexNotifications, FailedMessageViewIndexNotifications>();
33+
services.AddSingleton<Recoverability.IArchiveMessages, ArchiveMessages>();
34+
services.AddSingleton<IGroupsDataStore, GroupsDataStore>();
35+
services.AddSingleton<IRetryDocumentDataStore, RetryDocumentDataStore>();
36+
services.AddSingleton<IRetryBatchesDataStore, RetryBatchesDataStore>();
37+
services.AddSingleton<INotificationsManager, NotificationsManager>();
38+
services.AddSingleton<IIngestionUnitOfWorkFactory, IngestionUnitOfWorkFactory>();
39+
services.AddSingleton<IErrorMessageDataStore, ErrorMessageDataStore>();
40+
}
41+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
namespace ServiceControl.Persistence.Sql.Core.Implementation;
2+
3+
using System;
4+
using System.Threading.Tasks;
5+
using DbContexts;
6+
using Microsoft.Extensions.DependencyInjection;
7+
8+
/// <summary>
9+
/// Base class for data stores that provides helper methods to simplify scope and DbContext management
10+
/// </summary>
11+
public abstract class DataStoreBase
12+
{
13+
protected readonly IServiceProvider serviceProvider;
14+
15+
protected DataStoreBase(IServiceProvider serviceProvider)
16+
{
17+
this.serviceProvider = serviceProvider;
18+
}
19+
20+
/// <summary>
21+
/// Executes an operation with a scoped DbContext, returning a result
22+
/// </summary>
23+
protected async Task<T> ExecuteWithDbContext<T>(Func<ServiceControlDbContextBase, Task<T>> operation)
24+
{
25+
using var scope = serviceProvider.CreateScope();
26+
var dbContext = scope.ServiceProvider.GetRequiredService<ServiceControlDbContextBase>();
27+
return await operation(dbContext);
28+
}
29+
30+
/// <summary>
31+
/// Executes an operation with a scoped DbContext, without returning a result
32+
/// </summary>
33+
protected async Task ExecuteWithDbContext(Func<ServiceControlDbContextBase, Task> operation)
34+
{
35+
using var scope = serviceProvider.CreateScope();
36+
var dbContext = scope.ServiceProvider.GetRequiredService<ServiceControlDbContextBase>();
37+
await operation(dbContext);
38+
}
39+
40+
/// <summary>
41+
/// Creates a scope for operations that need to manage their own scope lifecycle (e.g., managers)
42+
/// </summary>
43+
protected IServiceScope CreateScope() => serviceProvider.CreateScope();
44+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
namespace ServiceControl.Persistence.Sql.Core.Infrastructure;
2+
3+
using System;
4+
5+
/// <summary>
6+
/// Generates sequential GUIDs for database primary keys to minimize page fragmentation
7+
/// and improve insert performance while maintaining security benefits of GUIDs.
8+
/// </summary>
9+
/// <remarks>
10+
/// This implementation creates time-ordered GUIDs similar to .NET 9's Guid.CreateVersion7()
11+
/// but compatible with .NET 8. The GUIDs are ordered by timestamp to reduce B-tree page splits
12+
/// in clustered indexes, which significantly improves insert performance compared to random GUIDs.
13+
///
14+
/// Benefits:
15+
/// - Database agnostic (works with SQL Server, PostgreSQL, MySQL, SQLite)
16+
/// - Sequential ordering reduces page fragmentation
17+
/// - Better insert performance than random GUIDs
18+
/// - Can easily migrate to Guid.CreateVersion7() when upgrading to .NET 9+
19+
/// - No external dependencies
20+
///
21+
/// Security:
22+
/// - Still cryptographically secure (uses Guid.NewGuid() as base)
23+
/// - Not guessable (unlike sequential integers)
24+
/// - Safe to expose in APIs
25+
/// </remarks>
26+
public static class SequentialGuidGenerator
27+
{
28+
/// <summary>
29+
/// Generate a sequential GUID with timestamp-based ordering for optimal database performance.
30+
/// </summary>
31+
/// <returns>A new GUID with sequential characteristics.</returns>
32+
public static Guid NewSequentialGuid()
33+
{
34+
var guidBytes = Guid.NewGuid().ToByteArray();
35+
var now = DateTime.UtcNow;
36+
37+
// Get timestamp in milliseconds since Unix epoch (similar to Version 7 GUIDs)
38+
var timestamp = (long)(now - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds;
39+
var timestampBytes = BitConverter.GetBytes(timestamp);
40+
41+
// Reverse if little-endian to get big-endian byte order for proper sorting
42+
if (BitConverter.IsLittleEndian)
43+
{
44+
Array.Reverse(timestampBytes);
45+
}
46+
47+
// Replace last 6 bytes with timestamp for sequential ordering
48+
// This placement works well with SQL Server's GUID comparison semantics
49+
Array.Copy(timestampBytes, 2, guidBytes, 10, 6);
50+
51+
return new Guid(guidBytes);
52+
}
53+
}

0 commit comments

Comments
 (0)