Restructure solution layout by module

This commit is contained in:
master
2025-10-28 15:10:40 +02:00
parent 95daa159c4
commit d870da18ce
4103 changed files with 192899 additions and 187024 deletions

View File

@@ -0,0 +1,49 @@
using Microsoft.Extensions.Logging;
using MongoDB.Driver;
using StellaOps.Notify.Storage.Mongo.Internal;
namespace StellaOps.Notify.Storage.Mongo.Migrations;
internal sealed class EnsureNotifyCollectionsMigration : INotifyMongoMigration
{
private readonly ILogger<EnsureNotifyCollectionsMigration> _logger;
public EnsureNotifyCollectionsMigration(ILogger<EnsureNotifyCollectionsMigration> logger)
=> _logger = logger ?? throw new ArgumentNullException(nameof(logger));
public string Id => "20251019_notify_collections_v1";
public async ValueTask ExecuteAsync(NotifyMongoContext context, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(context);
var requiredCollections = new[]
{
context.Options.RulesCollection,
context.Options.ChannelsCollection,
context.Options.TemplatesCollection,
context.Options.DeliveriesCollection,
context.Options.DigestsCollection,
context.Options.LocksCollection,
context.Options.AuditCollection,
context.Options.MigrationsCollection
};
var cursor = await context.Database
.ListCollectionNamesAsync(cancellationToken: cancellationToken)
.ConfigureAwait(false);
var existingNames = await cursor.ToListAsync(cancellationToken).ConfigureAwait(false);
foreach (var collection in requiredCollections)
{
if (existingNames.Contains(collection, StringComparer.Ordinal))
{
continue;
}
_logger.LogInformation("Creating Notify Mongo collection '{CollectionName}'.", collection);
await context.Database.CreateCollectionAsync(collection, cancellationToken: cancellationToken).ConfigureAwait(false);
}
}
}

View File

@@ -0,0 +1,165 @@
using System;
using System.Threading.Tasks;
using MongoDB.Bson;
using MongoDB.Driver;
using StellaOps.Notify.Storage.Mongo.Internal;
namespace StellaOps.Notify.Storage.Mongo.Migrations;
internal sealed class EnsureNotifyIndexesMigration : INotifyMongoMigration
{
public string Id => "20251019_notify_indexes_v1";
public async ValueTask ExecuteAsync(NotifyMongoContext context, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(context);
await EnsureRulesIndexesAsync(context, cancellationToken).ConfigureAwait(false);
await EnsureChannelsIndexesAsync(context, cancellationToken).ConfigureAwait(false);
await EnsureTemplatesIndexesAsync(context, cancellationToken).ConfigureAwait(false);
await EnsureDeliveriesIndexesAsync(context, cancellationToken).ConfigureAwait(false);
await EnsureDigestsIndexesAsync(context, cancellationToken).ConfigureAwait(false);
await EnsureLocksIndexesAsync(context, cancellationToken).ConfigureAwait(false);
await EnsureAuditIndexesAsync(context, cancellationToken).ConfigureAwait(false);
}
private static async Task EnsureRulesIndexesAsync(NotifyMongoContext context, CancellationToken cancellationToken)
{
var collection = context.Database.GetCollection<BsonDocument>(context.Options.RulesCollection);
var keys = Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Ascending("enabled");
var model = new CreateIndexModel<BsonDocument>(keys, new CreateIndexOptions
{
Name = "tenant_enabled"
});
await collection.Indexes.CreateOneAsync(model, cancellationToken: cancellationToken).ConfigureAwait(false);
}
private static async Task EnsureChannelsIndexesAsync(NotifyMongoContext context, CancellationToken cancellationToken)
{
var collection = context.Database.GetCollection<BsonDocument>(context.Options.ChannelsCollection);
var keys = Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Ascending("type")
.Ascending("enabled");
var model = new CreateIndexModel<BsonDocument>(keys, new CreateIndexOptions
{
Name = "tenant_type_enabled"
});
await collection.Indexes.CreateOneAsync(model, cancellationToken: cancellationToken).ConfigureAwait(false);
}
private static async Task EnsureTemplatesIndexesAsync(NotifyMongoContext context, CancellationToken cancellationToken)
{
var collection = context.Database.GetCollection<BsonDocument>(context.Options.TemplatesCollection);
var keys = Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Ascending("channelType")
.Ascending("key")
.Ascending("locale");
var model = new CreateIndexModel<BsonDocument>(keys, new CreateIndexOptions
{
Name = "tenant_channel_key_locale",
Unique = true
});
await collection.Indexes.CreateOneAsync(model, cancellationToken: cancellationToken).ConfigureAwait(false);
}
private static async Task EnsureDeliveriesIndexesAsync(NotifyMongoContext context, CancellationToken cancellationToken)
{
var collection = context.Database.GetCollection<BsonDocument>(context.Options.DeliveriesCollection);
var keys = Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Descending("sortKey");
var sortModel = new CreateIndexModel<BsonDocument>(keys, new CreateIndexOptions
{
Name = "tenant_sortKey"
});
await collection.Indexes.CreateOneAsync(sortModel, cancellationToken: cancellationToken).ConfigureAwait(false);
var statusModel = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys.Ascending("tenantId").Ascending("status"),
new CreateIndexOptions
{
Name = "tenant_status"
});
await collection.Indexes.CreateOneAsync(statusModel, cancellationToken: cancellationToken).ConfigureAwait(false);
if (context.Options.DeliveryHistoryRetention > TimeSpan.Zero)
{
var ttlModel = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys.Ascending("completedAt"),
new CreateIndexOptions
{
Name = "completedAt_ttl",
ExpireAfter = context.Options.DeliveryHistoryRetention
});
await collection.Indexes.CreateOneAsync(ttlModel, cancellationToken: cancellationToken).ConfigureAwait(false);
}
}
private static async Task EnsureDigestsIndexesAsync(NotifyMongoContext context, CancellationToken cancellationToken)
{
var collection = context.Database.GetCollection<BsonDocument>(context.Options.DigestsCollection);
var keys = Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Ascending("actionKey");
var model = new CreateIndexModel<BsonDocument>(keys, new CreateIndexOptions
{
Name = "tenant_actionKey"
});
await collection.Indexes.CreateOneAsync(model, cancellationToken: cancellationToken).ConfigureAwait(false);
}
private static async Task EnsureLocksIndexesAsync(NotifyMongoContext context, CancellationToken cancellationToken)
{
var collection = context.Database.GetCollection<BsonDocument>(context.Options.LocksCollection);
var uniqueModel = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys.Ascending("tenantId").Ascending("resource"),
new CreateIndexOptions
{
Name = "tenant_resource",
Unique = true
});
await collection.Indexes.CreateOneAsync(uniqueModel, cancellationToken: cancellationToken).ConfigureAwait(false);
var ttlModel = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys.Ascending("expiresAt"),
new CreateIndexOptions
{
Name = "expiresAt_ttl",
ExpireAfter = TimeSpan.Zero
});
await collection.Indexes.CreateOneAsync(ttlModel, cancellationToken: cancellationToken).ConfigureAwait(false);
}
private static async Task EnsureAuditIndexesAsync(NotifyMongoContext context, CancellationToken cancellationToken)
{
var collection = context.Database.GetCollection<BsonDocument>(context.Options.AuditCollection);
var keys = Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Descending("timestamp");
var model = new CreateIndexModel<BsonDocument>(keys, new CreateIndexOptions
{
Name = "tenant_timestamp"
});
await collection.Indexes.CreateOneAsync(model, cancellationToken: cancellationToken).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,12 @@
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Notify.Storage.Mongo.Internal;
namespace StellaOps.Notify.Storage.Mongo.Migrations;
internal interface INotifyMongoMigration
{
string Id { get; }
ValueTask ExecuteAsync(NotifyMongoContext context, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,16 @@
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace StellaOps.Notify.Storage.Mongo.Migrations;
internal sealed class NotifyMongoMigrationRecord
{
[BsonId]
public ObjectId Id { get; init; }
[BsonElement("migrationId")]
public required string MigrationId { get; init; }
[BsonElement("appliedAt")]
public required DateTimeOffset AppliedAt { get; init; }
}

View File

@@ -0,0 +1,78 @@
using Microsoft.Extensions.Logging;
using MongoDB.Bson;
using MongoDB.Driver;
using StellaOps.Notify.Storage.Mongo.Internal;
namespace StellaOps.Notify.Storage.Mongo.Migrations;
internal sealed class NotifyMongoMigrationRunner
{
private readonly NotifyMongoContext _context;
private readonly IReadOnlyList<INotifyMongoMigration> _migrations;
private readonly ILogger<NotifyMongoMigrationRunner> _logger;
public NotifyMongoMigrationRunner(
NotifyMongoContext context,
IEnumerable<INotifyMongoMigration> migrations,
ILogger<NotifyMongoMigrationRunner> logger)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
ArgumentNullException.ThrowIfNull(migrations);
_migrations = migrations.OrderBy(migration => migration.Id, StringComparer.Ordinal).ToArray();
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async ValueTask RunAsync(CancellationToken cancellationToken)
{
if (_migrations.Count == 0)
{
return;
}
var collection = _context.Database.GetCollection<NotifyMongoMigrationRecord>(_context.Options.MigrationsCollection);
await EnsureMigrationIndexAsync(collection, cancellationToken).ConfigureAwait(false);
var applied = await collection
.Find(FilterDefinition<NotifyMongoMigrationRecord>.Empty)
.Project(record => record.MigrationId)
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
var appliedSet = applied.ToHashSet(StringComparer.Ordinal);
foreach (var migration in _migrations)
{
if (appliedSet.Contains(migration.Id))
{
continue;
}
_logger.LogInformation("Applying Notify Mongo migration {MigrationId}.", migration.Id);
await migration.ExecuteAsync(_context, cancellationToken).ConfigureAwait(false);
var record = new NotifyMongoMigrationRecord
{
Id = ObjectId.GenerateNewId(),
MigrationId = migration.Id,
AppliedAt = DateTimeOffset.UtcNow
};
await collection.InsertOneAsync(record, cancellationToken: cancellationToken).ConfigureAwait(false);
_logger.LogInformation("Completed Notify Mongo migration {MigrationId}.", migration.Id);
}
}
private static async Task EnsureMigrationIndexAsync(
IMongoCollection<NotifyMongoMigrationRecord> collection,
CancellationToken cancellationToken)
{
var keys = Builders<NotifyMongoMigrationRecord>.IndexKeys.Ascending(record => record.MigrationId);
var model = new CreateIndexModel<NotifyMongoMigrationRecord>(keys, new CreateIndexOptions
{
Name = "migrationId_unique",
Unique = true
});
await collection.Indexes.CreateOneAsync(model, cancellationToken: cancellationToken).ConfigureAwait(false);
}
}