diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bfaa76..8d7eb4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -Nothing yet. +### Added + +- Database migration tools. + +### Changed + +- `DomainEvent` is now a class, and uses an `EventId` struct. +- Using `ErrorMessageBuilder` in exceptions. +- Recreated scripts and migrations. +- Refactored the `LoadFromChanges` method to remove Reflection. +- Core package now targets .NET Standard 2.1. +- `ToString` methods now include ID prefix. + +### Fixed + +- README files and migration commands. +- Docker Compose file. ## [5.2.0] - 2024-03-25 diff --git a/EventSourcing.sln b/EventSourcing.sln index b3c78a0..861ba3f 100644 --- a/EventSourcing.sln +++ b/EventSourcing.sln @@ -63,6 +63,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Logitar.EventSourcing.Mongo EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Logitar.EventSourcing.MongoDB.IntegrationTests", "tests\Logitar.EventSourcing.MongoDB.IntegrationTests\Logitar.EventSourcing.MongoDB.IntegrationTests.csproj", "{CEEFD05A-DC09-4756-8473-527539E478F1}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{9C6D5EFE-FF9F-4D87-B70B-916701A1CD6B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Logitar.EventSourcing.Database", "tools\Logitar.EventSourcing.Database\Logitar.EventSourcing.Database.csproj", "{7D816D68-A25C-466C-B1D1-5722AE64F9CB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -161,6 +165,10 @@ Global {CEEFD05A-DC09-4756-8473-527539E478F1}.Debug|Any CPU.Build.0 = Debug|Any CPU {CEEFD05A-DC09-4756-8473-527539E478F1}.Release|Any CPU.ActiveCfg = Release|Any CPU {CEEFD05A-DC09-4756-8473-527539E478F1}.Release|Any CPU.Build.0 = Release|Any CPU + {7D816D68-A25C-466C-B1D1-5722AE64F9CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7D816D68-A25C-466C-B1D1-5722AE64F9CB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7D816D68-A25C-466C-B1D1-5722AE64F9CB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7D816D68-A25C-466C-B1D1-5722AE64F9CB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -179,6 +187,7 @@ Global {5FBFB540-E143-4C73-837E-5672E71D18B6} = {D092FA68-D3D5-460E-9A38-61033D6F9692} {FD1A7AF8-FEDF-4EA1-9966-C46677584BB2} = {D092FA68-D3D5-460E-9A38-61033D6F9692} {CEEFD05A-DC09-4756-8473-527539E478F1} = {D092FA68-D3D5-460E-9A38-61033D6F9692} + {7D816D68-A25C-466C-B1D1-5722AE64F9CB} = {9C6D5EFE-FF9F-4D87-B70B-916701A1CD6B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B0AA9E53-02AD-4000-8ED5-53AFF5D1B32D} diff --git a/docker-compose.yml b/docker-compose.yml index 4450c94..b6baef9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,9 +1,9 @@ -version: '3.8' name: event_sourcing services: event_sourcing_mongo: image: mongo container_name: Logitar.EventSourcing_mongo + restart: unless-stopped environment: MONGO_INITDB_ROOT_USERNAME: demo MONGO_INITDB_ROOT_PASSWORD: AwHE5MRKBeY9CsJu @@ -13,6 +13,7 @@ services: event_sourcing_mssql: image: mcr.microsoft.com/mssql/server:2022-latest container_name: Logitar.EventSourcing_mssql + restart: unless-stopped environment: ACCEPT_EULA: 'Y' SA_PASSWORD: mWGEgJcrV5dzRyqb @@ -22,6 +23,7 @@ services: event_sourcing_postgres: image: postgres container_name: Logitar.EventSourcing_postgres + restart: unless-stopped environment: POSTGRES_PASSWORD: cptBg3hZ9qC6a5Vb ports: diff --git a/src/Logitar.EventSourcing.EntityFrameworkCore.PostgreSQL/Migrations/20230804163231_CreateEventTable.Designer.cs b/src/Logitar.EventSourcing.EntityFrameworkCore.PostgreSQL/Migrations/20240607013711_CreateEventTable.Designer.cs similarity index 91% rename from src/Logitar.EventSourcing.EntityFrameworkCore.PostgreSQL/Migrations/20230804163231_CreateEventTable.Designer.cs rename to src/Logitar.EventSourcing.EntityFrameworkCore.PostgreSQL/Migrations/20240607013711_CreateEventTable.Designer.cs index a22ff77..9db7283 100644 --- a/src/Logitar.EventSourcing.EntityFrameworkCore.PostgreSQL/Migrations/20230804163231_CreateEventTable.Designer.cs +++ b/src/Logitar.EventSourcing.EntityFrameworkCore.PostgreSQL/Migrations/20240607013711_CreateEventTable.Designer.cs @@ -11,7 +11,7 @@ namespace Logitar.EventSourcing.EntityFrameworkCore.PostgreSQL.Migrations { [DbContext(typeof(EventContext))] - [Migration("20230804163231_CreateEventTable")] + [Migration("20240607013711_CreateEventTable")] partial class CreateEventTable { /// @@ -19,7 +19,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "7.0.9") + .HasAnnotation("ProductVersion", "8.0.6") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -56,8 +56,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasMaxLength(255) .HasColumnType("character varying(255)"); - b.Property("Id") - .HasColumnType("uuid"); + b.Property("Id") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); b.Property("IsDeleted") .HasColumnType("boolean"); diff --git a/src/Logitar.EventSourcing.EntityFrameworkCore.PostgreSQL/Migrations/20230804163231_CreateEventTable.cs b/src/Logitar.EventSourcing.EntityFrameworkCore.PostgreSQL/Migrations/20240607013711_CreateEventTable.cs similarity index 96% rename from src/Logitar.EventSourcing.EntityFrameworkCore.PostgreSQL/Migrations/20230804163231_CreateEventTable.cs rename to src/Logitar.EventSourcing.EntityFrameworkCore.PostgreSQL/Migrations/20240607013711_CreateEventTable.cs index 678b0db..71e8a4a 100644 --- a/src/Logitar.EventSourcing.EntityFrameworkCore.PostgreSQL/Migrations/20230804163231_CreateEventTable.cs +++ b/src/Logitar.EventSourcing.EntityFrameworkCore.PostgreSQL/Migrations/20240607013711_CreateEventTable.cs @@ -17,7 +17,7 @@ protected override void Up(MigrationBuilder migrationBuilder) { EventId = table.Column(type: "bigint", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Id = table.Column(type: "uuid", nullable: false), + Id = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), ActorId = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), IsDeleted = table.Column(type: "boolean", nullable: true), OccurredOn = table.Column(type: "timestamp with time zone", nullable: false), diff --git a/src/Logitar.EventSourcing.EntityFrameworkCore.PostgreSQL/Migrations/EventContextModelSnapshot.cs b/src/Logitar.EventSourcing.EntityFrameworkCore.PostgreSQL/Migrations/EventContextModelSnapshot.cs index 4433aef..0a267d6 100644 --- a/src/Logitar.EventSourcing.EntityFrameworkCore.PostgreSQL/Migrations/EventContextModelSnapshot.cs +++ b/src/Logitar.EventSourcing.EntityFrameworkCore.PostgreSQL/Migrations/EventContextModelSnapshot.cs @@ -16,7 +16,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "7.0.9") + .HasAnnotation("ProductVersion", "8.0.6") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -53,8 +53,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(255) .HasColumnType("character varying(255)"); - b.Property("Id") - .HasColumnType("uuid"); + b.Property("Id") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); b.Property("IsDeleted") .HasColumnType("boolean"); diff --git a/src/Logitar.EventSourcing.EntityFrameworkCore.PostgreSQL/README.md b/src/Logitar.EventSourcing.EntityFrameworkCore.PostgreSQL/README.md index 8e86c7f..0468d67 100644 --- a/src/Logitar.EventSourcing.EntityFrameworkCore.PostgreSQL/README.md +++ b/src/Logitar.EventSourcing.EntityFrameworkCore.PostgreSQL/README.md @@ -1,21 +1,25 @@ -# Logitar.EventSourcing.EntityFrameworkCore.PostgreSQL +# Logitar.EventSourcing.EntityFrameworkCore.PostgreSQL -Provides an implementation of a relational event store to be used with the Event Sourcing -architecture pattern, Entity Framework Core and PostgreSQL. +Provides an implementation of a relational event store to be used with the Event Sourcing architecture pattern, Entity Framework Core and PostgreSQL. ## Migrations -This project is setup to use migrations. You must execute the following commands in the solution -directory. +This project is setup to use migrations. You must execute the following commands in the solution directory. + +⚠️ Ensure the `EntityFrameworkCorePostgreSQL` database provider has been set in the database project user secrets. ### Create a new migration Execute the following command to create a new migration. Do not forget to specify a migration name! -`dotnet ef migrations add --context EventContext --project src/EventSourcing/Logitar.EventSourcing.EntityFrameworkCore.PostgreSQL --startup-project src/Demo/Logitar.Demo.Ui` +```sh +dotnet ef migrations add --context EventContext --project src/Logitar.EventSourcing.EntityFrameworkCore.PostgreSQL --startup-project tools/Logitar.EventSourcing.Database +``` ### Generate a script Execute the following command to generate a new script. Do not forget to specify a source migration name! -`dotnet ef migrations script --context EventContext --project src/EventSourcing/Logitar.EventSourcing.EntityFrameworkCore.PostgreSQL --startup-project src/Demo/Logitar.Demo.Ui` +```sh +dotnet ef migrations script --context EventContext --project src/Logitar.EventSourcing.EntityFrameworkCore.PostgreSQL --startup-project tools/Logitar.EventSourcing.Database +``` diff --git a/src/Logitar.EventSourcing.PostgreSQL/Scripts/20230804163231_CreateEventTable.sql b/src/Logitar.EventSourcing.EntityFrameworkCore.PostgreSQL/Scripts/20240607013711_CreateEventTable.sql similarity index 91% rename from src/Logitar.EventSourcing.PostgreSQL/Scripts/20230804163231_CreateEventTable.sql rename to src/Logitar.EventSourcing.EntityFrameworkCore.PostgreSQL/Scripts/20240607013711_CreateEventTable.sql index 68731f5..4574399 100644 --- a/src/Logitar.EventSourcing.PostgreSQL/Scripts/20230804163231_CreateEventTable.sql +++ b/src/Logitar.EventSourcing.EntityFrameworkCore.PostgreSQL/Scripts/20240607013711_CreateEventTable.sql @@ -8,9 +8,9 @@ START TRANSACTION; CREATE TABLE "Events" ( "EventId" bigint GENERATED BY DEFAULT AS IDENTITY, - "Id" uuid NOT NULL, + "Id" character varying(255) NOT NULL, "ActorId" character varying(255) NOT NULL, - "IsDeleted" boolean NULL, + "IsDeleted" boolean, "OccurredOn" timestamp with time zone NOT NULL, "Version" bigint NOT NULL, "AggregateType" character varying(255) NOT NULL, @@ -37,6 +37,6 @@ CREATE INDEX "IX_Events_OccurredOn" ON "Events" ("OccurredOn"); CREATE INDEX "IX_Events_Version" ON "Events" ("Version"); INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") -VALUES ('20230804163231_CreateEventTable', '7.0.9'); +VALUES ('20240607013711_CreateEventTable', '8.0.6'); -COMMIT; \ No newline at end of file +COMMIT; diff --git a/src/Logitar.EventSourcing.EntityFrameworkCore.Relational/EventConfiguration.cs b/src/Logitar.EventSourcing.EntityFrameworkCore.Relational/EventConfiguration.cs index f7fd9d8..bc03a00 100644 --- a/src/Logitar.EventSourcing.EntityFrameworkCore.Relational/EventConfiguration.cs +++ b/src/Logitar.EventSourcing.EntityFrameworkCore.Relational/EventConfiguration.cs @@ -26,9 +26,10 @@ public void Configure(EntityTypeBuilder builder) builder.HasIndex(x => x.EventType); builder.HasIndex(x => new { x.AggregateType, x.AggregateId }); - builder.Property(x => x.ActorId).HasMaxLength(byte.MaxValue); + builder.Property(x => x.Id).IsRequired().HasMaxLength(EventId.MaximumLength); + builder.Property(x => x.ActorId).HasMaxLength(ActorId.MaximumLength); builder.Property(x => x.AggregateType).HasMaxLength(byte.MaxValue); - builder.Property(x => x.AggregateId).HasMaxLength(byte.MaxValue); + builder.Property(x => x.AggregateId).HasMaxLength(AggregateId.MaximumLength); builder.Property(x => x.EventType).HasMaxLength(byte.MaxValue); } } diff --git a/src/Logitar.EventSourcing.EntityFrameworkCore.Relational/EventContext.cs b/src/Logitar.EventSourcing.EntityFrameworkCore.Relational/EventContext.cs index 70c0be6..f6c4cf4 100644 --- a/src/Logitar.EventSourcing.EntityFrameworkCore.Relational/EventContext.cs +++ b/src/Logitar.EventSourcing.EntityFrameworkCore.Relational/EventContext.cs @@ -18,7 +18,7 @@ public EventContext(DbContextOptions options) : base(options) /// /// Gets or sets the data set of events. /// - public DbSet Events { get; private set; } = null!; + public DbSet Events { get; private set; } /// /// Configures the specified model builder. diff --git a/src/Logitar.EventSourcing.EntityFrameworkCore.Relational/EventEntity.cs b/src/Logitar.EventSourcing.EntityFrameworkCore.Relational/EventEntity.cs index 08b20ed..8463c25 100644 --- a/src/Logitar.EventSourcing.EntityFrameworkCore.Relational/EventEntity.cs +++ b/src/Logitar.EventSourcing.EntityFrameworkCore.Relational/EventEntity.cs @@ -21,7 +21,7 @@ private EventEntity() /// /// Gets or sets the identifier of the event. /// - public Guid Id { get; private set; } + public string Id { get; private set; } = string.Empty; /// /// Gets or sets the identifier of the actor who triggered the event. @@ -71,7 +71,7 @@ public static IEnumerable FromChanges(AggregateRoot aggregate, IEve return aggregate.Changes.Select(change => new EventEntity { - Id = change.Id, + Id = change.Id.Value, ActorId = change.ActorId.Value, IsDeleted = change.IsDeleted, OccurredOn = change.OccurredOn.ToUniversalTime(), diff --git a/src/Logitar.EventSourcing.EntityFrameworkCore.Relational/README.md b/src/Logitar.EventSourcing.EntityFrameworkCore.Relational/README.md index 77c7bf2..56dd7ae 100644 --- a/src/Logitar.EventSourcing.EntityFrameworkCore.Relational/README.md +++ b/src/Logitar.EventSourcing.EntityFrameworkCore.Relational/README.md @@ -1,4 +1,3 @@ # Logitar.EventSourcing.EntityFrameworkCore.Relational -Provides an abstraction of a relational event store to be used with the Event Sourcing architecture -pattern and Entity Framework Core. +Provides an abstraction of a relational event store to be used with the Event Sourcing architecture pattern and Entity Framework Core. diff --git a/src/Logitar.EventSourcing.EntityFrameworkCore.SqlServer/Migrations/20230804163434_CreateEventTable.Designer.cs b/src/Logitar.EventSourcing.EntityFrameworkCore.SqlServer/Migrations/20240607013010_CreateEventTable.Designer.cs similarity index 91% rename from src/Logitar.EventSourcing.EntityFrameworkCore.SqlServer/Migrations/20230804163434_CreateEventTable.Designer.cs rename to src/Logitar.EventSourcing.EntityFrameworkCore.SqlServer/Migrations/20240607013010_CreateEventTable.Designer.cs index 51dce62..3226e24 100644 --- a/src/Logitar.EventSourcing.EntityFrameworkCore.SqlServer/Migrations/20230804163434_CreateEventTable.Designer.cs +++ b/src/Logitar.EventSourcing.EntityFrameworkCore.SqlServer/Migrations/20240607013010_CreateEventTable.Designer.cs @@ -11,7 +11,7 @@ namespace Logitar.EventSourcing.EntityFrameworkCore.SqlServer.Migrations { [DbContext(typeof(EventContext))] - [Migration("20230804163434_CreateEventTable")] + [Migration("20240607013010_CreateEventTable")] partial class CreateEventTable { /// @@ -19,7 +19,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "7.0.9") + .HasAnnotation("ProductVersion", "8.0.6") .HasAnnotation("Relational:MaxIdentifierLength", 128); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -56,8 +56,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasMaxLength(255) .HasColumnType("nvarchar(255)"); - b.Property("Id") - .HasColumnType("uniqueidentifier"); + b.Property("Id") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); b.Property("IsDeleted") .HasColumnType("bit"); diff --git a/src/Logitar.EventSourcing.EntityFrameworkCore.SqlServer/Migrations/20230804163434_CreateEventTable.cs b/src/Logitar.EventSourcing.EntityFrameworkCore.SqlServer/Migrations/20240607013010_CreateEventTable.cs similarity index 96% rename from src/Logitar.EventSourcing.EntityFrameworkCore.SqlServer/Migrations/20230804163434_CreateEventTable.cs rename to src/Logitar.EventSourcing.EntityFrameworkCore.SqlServer/Migrations/20240607013010_CreateEventTable.cs index 06f7399..2fd3945 100644 --- a/src/Logitar.EventSourcing.EntityFrameworkCore.SqlServer/Migrations/20230804163434_CreateEventTable.cs +++ b/src/Logitar.EventSourcing.EntityFrameworkCore.SqlServer/Migrations/20240607013010_CreateEventTable.cs @@ -16,7 +16,7 @@ protected override void Up(MigrationBuilder migrationBuilder) { EventId = table.Column(type: "bigint", nullable: false) .Annotation("SqlServer:Identity", "1, 1"), - Id = table.Column(type: "uniqueidentifier", nullable: false), + Id = table.Column(type: "nvarchar(255)", maxLength: 255, nullable: false), ActorId = table.Column(type: "nvarchar(255)", maxLength: 255, nullable: false), IsDeleted = table.Column(type: "bit", nullable: true), OccurredOn = table.Column(type: "datetime2", nullable: false), diff --git a/src/Logitar.EventSourcing.EntityFrameworkCore.SqlServer/Migrations/EventContextModelSnapshot.cs b/src/Logitar.EventSourcing.EntityFrameworkCore.SqlServer/Migrations/EventContextModelSnapshot.cs index 7acc93c..a2b74c1 100644 --- a/src/Logitar.EventSourcing.EntityFrameworkCore.SqlServer/Migrations/EventContextModelSnapshot.cs +++ b/src/Logitar.EventSourcing.EntityFrameworkCore.SqlServer/Migrations/EventContextModelSnapshot.cs @@ -16,7 +16,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "7.0.9") + .HasAnnotation("ProductVersion", "8.0.6") .HasAnnotation("Relational:MaxIdentifierLength", 128); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -53,8 +53,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(255) .HasColumnType("nvarchar(255)"); - b.Property("Id") - .HasColumnType("uniqueidentifier"); + b.Property("Id") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); b.Property("IsDeleted") .HasColumnType("bit"); diff --git a/src/Logitar.EventSourcing.EntityFrameworkCore.SqlServer/README.md b/src/Logitar.EventSourcing.EntityFrameworkCore.SqlServer/README.md index 4c4c88c..ed69eba 100644 --- a/src/Logitar.EventSourcing.EntityFrameworkCore.SqlServer/README.md +++ b/src/Logitar.EventSourcing.EntityFrameworkCore.SqlServer/README.md @@ -1,21 +1,25 @@ -# Logitar.EventSourcing.EntityFrameworkCore.SqlServer +# Logitar.EventSourcing.EntityFrameworkCore.SqlServer -Provides an implementation of a relational event store to be used with the Event Sourcing -architecture pattern, Entity Framework Core and Microsoft SQL Server. +Provides an implementation of a relational event store to be used with the Event Sourcing architecture pattern, Entity Framework Core and Microsoft SQL Server. ## Migrations -This project is setup to use migrations. You must execute the following commands in the solution -directory. +This project is setup to use migrations. You must execute the following commands in the solution directory. + +⚠️ Ensure the `EntityFrameworkCoreSqlServer` database provider has been set in the database project user secrets. ### Create a new migration Execute the following command to create a new migration. Do not forget to specify a migration name! -`dotnet ef migrations add --context EventContext --project src/EventSourcing/Logitar.EventSourcing.EntityFrameworkCore.SqlServer --startup-project src/Demo/Logitar.Demo.Ui` +```sh +dotnet ef migrations add --context EventContext --project src/Logitar.EventSourcing.EntityFrameworkCore.SqlServer --startup-project tools/Logitar.EventSourcing.Database +``` ### Generate a script Execute the following command to generate a new script. Do not forget to specify a source migration name! -`dotnet ef migrations script --context EventContext --project src/EventSourcing/Logitar.EventSourcing.EntityFrameworkCore.SqlServer --startup-project src/Demo/Logitar.Demo.Ui` +```sh +dotnet ef migrations script --context EventContext --project src/Logitar.EventSourcing.EntityFrameworkCore.SqlServer --startup-project tools/Logitar.EventSourcing.Database +``` diff --git a/src/Logitar.EventSourcing.EntityFrameworkCore.SqlServer/Scripts/20230804163434_CreateEventTable.sql b/src/Logitar.EventSourcing.EntityFrameworkCore.SqlServer/Scripts/20240607013010_CreateEventTable.sql similarity index 90% rename from src/Logitar.EventSourcing.EntityFrameworkCore.SqlServer/Scripts/20230804163434_CreateEventTable.sql rename to src/Logitar.EventSourcing.EntityFrameworkCore.SqlServer/Scripts/20240607013010_CreateEventTable.sql index dd269ed..3b01e04 100644 --- a/src/Logitar.EventSourcing.EntityFrameworkCore.SqlServer/Scripts/20230804163434_CreateEventTable.sql +++ b/src/Logitar.EventSourcing.EntityFrameworkCore.SqlServer/Scripts/20240607013010_CreateEventTable.sql @@ -1,4 +1,4 @@ -IF OBJECT_ID(N'[__EFMigrationsHistory]') IS NULL +IF OBJECT_ID(N'[__EFMigrationsHistory]') IS NULL BEGIN CREATE TABLE [__EFMigrationsHistory] ( [MigrationId] nvarchar(150) NOT NULL, @@ -13,7 +13,7 @@ GO CREATE TABLE [Events] ( [EventId] bigint NOT NULL IDENTITY, - [Id] uniqueidentifier NOT NULL, + [Id] nvarchar(255) NOT NULL, [ActorId] nvarchar(255) NOT NULL, [IsDeleted] bit NULL, [OccurredOn] datetime2 NOT NULL, @@ -51,8 +51,8 @@ CREATE INDEX [IX_Events_Version] ON [Events] ([Version]); GO INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) -VALUES (N'20230804163434_CreateEventTable', N'7.0.9'); +VALUES (N'20240607013010_CreateEventTable', N'8.0.6'); GO COMMIT; -GO \ No newline at end of file +GO diff --git a/src/Logitar.EventSourcing.InMemory/EventEntity.cs b/src/Logitar.EventSourcing.InMemory/EventEntity.cs index ec70d8e..90d6226 100644 --- a/src/Logitar.EventSourcing.InMemory/EventEntity.cs +++ b/src/Logitar.EventSourcing.InMemory/EventEntity.cs @@ -8,7 +8,7 @@ private EventEntity() { } - public Guid Id { get; private set; } + public string Id { get; private set; } = string.Empty; public long Version { get; private set; } @@ -25,7 +25,7 @@ public static IEnumerable FromChanges(AggregateRoot aggregate, IEve return aggregate.Changes.Select(change => new EventEntity { - Id = change.Id, + Id = change.Id.Value, Version = change.Version, AggregateType = aggregateType, AggregateId = aggregateId, diff --git a/src/Logitar.EventSourcing.Infrastructure/AggregateRepository.cs b/src/Logitar.EventSourcing.Infrastructure/AggregateRepository.cs index 55d3b7a..e06f08f 100644 --- a/src/Logitar.EventSourcing.Infrastructure/AggregateRepository.cs +++ b/src/Logitar.EventSourcing.Infrastructure/AggregateRepository.cs @@ -33,7 +33,7 @@ protected AggregateRepository(IEventBus eventBus, IEventSerializer eventSerializ /// The cancellation token. /// The loaded aggregate. public virtual async Task LoadAsync(AggregateId id, CancellationToken cancellationToken) - where T : AggregateRoot + where T : AggregateRoot, new() { return await LoadAsync(id, includeDeleted: false, cancellationToken); } @@ -46,7 +46,7 @@ protected AggregateRepository(IEventBus eventBus, IEventSerializer eventSerializ /// The cancellation token. /// The loaded aggregate. public virtual async Task LoadAsync(AggregateId id, long? version, CancellationToken cancellationToken) - where T : AggregateRoot + where T : AggregateRoot, new() { return await LoadAsync(id, version, includeDeleted: false, cancellationToken); } @@ -59,7 +59,7 @@ protected AggregateRepository(IEventBus eventBus, IEventSerializer eventSerializ /// The cancellation token. /// The loaded aggregate. public virtual async Task LoadAsync(AggregateId id, bool includeDeleted, CancellationToken cancellationToken) - where T : AggregateRoot + where T : AggregateRoot, new() { return await LoadAsync(id, version: null, includeDeleted, cancellationToken); } @@ -73,7 +73,7 @@ protected AggregateRepository(IEventBus eventBus, IEventSerializer eventSerializ /// The cancellation token. /// The loaded aggregate. public virtual async Task LoadAsync(AggregateId id, long? version, bool includeDeleted, CancellationToken cancellationToken) - where T : AggregateRoot + where T : AggregateRoot, new() { IEnumerable changes = await LoadChangesAsync(id, version, cancellationToken); return Load(changes, includeDeleted).SingleOrDefault(); @@ -95,7 +95,7 @@ protected AggregateRepository(IEventBus eventBus, IEventSerializer eventSerializ /// The cancellation token. /// The list of loaded aggregates. public virtual async Task> LoadAsync(CancellationToken cancellationToken) - where T : AggregateRoot + where T : AggregateRoot, new() { return await LoadAsync(includeDeleted: false, cancellationToken); } @@ -107,7 +107,7 @@ public virtual async Task> LoadAsync(CancellationToken cancell /// The cancellation token. /// The list of loaded aggregates. public virtual async Task> LoadAsync(bool includeDeleted, CancellationToken cancellationToken) - where T : AggregateRoot + where T : AggregateRoot, new() { IEnumerable changes = await LoadChangesAsync(cancellationToken); return Load(changes, includeDeleted); @@ -128,7 +128,7 @@ public virtual async Task> LoadAsync(bool includeDeleted, Canc /// The cancellation token. /// The list of loaded aggregates. public virtual async Task> LoadAsync(IEnumerable ids, CancellationToken cancellationToken) - where T : AggregateRoot + where T : AggregateRoot, new() { return await LoadAsync(ids, includeDeleted: false, cancellationToken); } @@ -141,7 +141,7 @@ public virtual async Task> LoadAsync(IEnumerable /// The cancellation token. /// The list of loaded aggregates. public virtual async Task> LoadAsync(IEnumerable ids, bool includeDeleted, CancellationToken cancellationToken) - where T : AggregateRoot + where T : AggregateRoot, new() { IEnumerable changes = await LoadChangesAsync(ids, cancellationToken); return Load(changes, includeDeleted); @@ -163,7 +163,7 @@ public virtual async Task> LoadAsync(IEnumerable /// The asynchronous operation. public virtual async Task SaveAsync(AggregateRoot aggregate, CancellationToken cancellationToken) { - await SaveAsync(new[] { aggregate }, cancellationToken); + await SaveAsync([aggregate], cancellationToken); } /// /// Persists a list of aggregates to the event store. @@ -193,7 +193,7 @@ public virtual async Task SaveAsync(IEnumerable aggregates, Cance /// A value indicating whether or not deleted aggregates will be returned. /// The loaded aggregates. protected virtual IEnumerable Load(IEnumerable events, bool includeDeleted = false) - where T : AggregateRoot + where T : AggregateRoot, new() { List aggregates = new(events.Count()); diff --git a/src/Logitar.EventSourcing.Infrastructure/EventDataDeserializationFailedException.cs b/src/Logitar.EventSourcing.Infrastructure/EventDataDeserializationFailedException.cs index 8fe3be4..28b6be5 100644 --- a/src/Logitar.EventSourcing.Infrastructure/EventDataDeserializationFailedException.cs +++ b/src/Logitar.EventSourcing.Infrastructure/EventDataDeserializationFailedException.cs @@ -6,22 +6,16 @@ public class EventDataDeserializationFailedException : Exception { /// - /// Initializes a new instance of the class. + /// The detailed error message. /// - /// The invalid event. - internal EventDataDeserializationFailedException(IEventEntity entity) : base(BuildMessage(entity)) - { - EventId = entity.Id; - EventType = entity.EventType; - EventData = entity.EventData; - } + private const string ErrorMessage = "The specified event data could not be deserialized."; /// /// Gets or sets the identifier of the invalid event. /// - public Guid EventId + public string EventId { - get => (Guid)Data[nameof(EventId)]!; + get => (string)Data[nameof(EventId)]!; private set => Data[nameof(EventId)] = value; } /// @@ -42,19 +36,24 @@ public string EventData } /// - /// Builds the exception message. + /// Initializes a new instance of the class. /// /// The invalid event. - /// The exception message - private static string BuildMessage(IEventEntity entity) + internal EventDataDeserializationFailedException(IEventEntity entity) : base(BuildMessage(entity)) { - StringBuilder message = new(); - - message.AppendLine("The specified event data could not be deserialized."); - message.Append("EventId: ").Append(entity.Id).AppendLine(); - message.Append("EventType: ").AppendLine(entity.EventType); - message.Append("EventData: ").AppendLine(entity.EventData); - - return message.ToString(); + EventId = entity.Id; + EventType = entity.EventType; + EventData = entity.EventData; } + + /// + /// Builds the exception message. + /// + /// The invalid event. + /// The exception message + private static string BuildMessage(IEventEntity entity) => new ErrorMessageBuilder(ErrorMessage) + .AddData(nameof(EventId), entity.Id) + .AddData(nameof(EventType), entity.EventType) + .AddData(nameof(EventData), entity.EventData) + .Build(); } diff --git a/src/Logitar.EventSourcing.Infrastructure/EventIdConverter.cs b/src/Logitar.EventSourcing.Infrastructure/EventIdConverter.cs new file mode 100644 index 0000000..1f44a62 --- /dev/null +++ b/src/Logitar.EventSourcing.Infrastructure/EventIdConverter.cs @@ -0,0 +1,32 @@ +namespace Logitar.EventSourcing.Infrastructure; + +/// +/// Represents a JSON converter for instances of structs. +/// +public class EventIdConverter : JsonConverter +{ + /// + /// Reads an from the specified JSON reader. + /// + /// The JSON reader. + /// The type to convert to. + /// The serializer options. + /// The resulting event identifier. + public override EventId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + string? value = reader.GetString(); + + return value == null ? default : new EventId(value); + } + + /// + /// Writes an to the specified JSON writer. + /// + /// The JSON writer. + /// The event identifier to write. + /// The serializer options. + public override void Write(Utf8JsonWriter writer, EventId eventId, JsonSerializerOptions options) + { + writer.WriteStringValue(eventId.Value); + } +} diff --git a/src/Logitar.EventSourcing.Infrastructure/EventSerializer.cs b/src/Logitar.EventSourcing.Infrastructure/EventSerializer.cs index baca4af..9356443 100644 --- a/src/Logitar.EventSourcing.Infrastructure/EventSerializer.cs +++ b/src/Logitar.EventSourcing.Infrastructure/EventSerializer.cs @@ -17,6 +17,7 @@ public EventSerializer() { RegisterConverter(new ActorIdConverter()); RegisterConverter(new AggregateIdConverter()); + RegisterConverter(new EventIdConverter()); RegisterConverter(new JsonStringEnumConverter()); } /// diff --git a/src/Logitar.EventSourcing.Infrastructure/EventTypeNotFoundException.cs b/src/Logitar.EventSourcing.Infrastructure/EventTypeNotFoundException.cs index 709e0a2..59ffb36 100644 --- a/src/Logitar.EventSourcing.Infrastructure/EventTypeNotFoundException.cs +++ b/src/Logitar.EventSourcing.Infrastructure/EventTypeNotFoundException.cs @@ -6,21 +6,16 @@ public class EventTypeNotFoundException : Exception { /// - /// Initializes a new instance of the class. + /// The detailed error message. /// - /// The invalid event. - internal EventTypeNotFoundException(IEventEntity entity) : base(BuildMessage(entity)) - { - EventId = entity.Id; - TypeName = entity.EventType; - } + private const string ErrorMessage = "The specified event type could not be found."; /// /// Gets or sets the identifier of the invalid event. /// - public Guid EventId + public string EventId { - get => (Guid)Data[nameof(EventId)]!; + get => (string)Data[nameof(EventId)]!; private set => Data[nameof(EventId)] = value; } /// @@ -33,18 +28,22 @@ public string TypeName } /// - /// Builds the exception message. + /// Initializes a new instance of the class. /// /// The invalid event. - /// The exception message. - private static string BuildMessage(IEventEntity entity) + internal EventTypeNotFoundException(IEventEntity entity) : base(BuildMessage(entity)) { - StringBuilder message = new(); - - message.AppendLine("The specified event type could not be found."); - message.Append("EventId: ").Append(entity.Id).AppendLine(); - message.Append("TypeName: ").AppendLine(entity.EventType); - - return message.ToString(); + EventId = entity.Id; + TypeName = entity.EventType; } + + /// + /// Builds the exception message. + /// + /// The invalid event. + /// The exception message. + private static string BuildMessage(IEventEntity entity) => new ErrorMessageBuilder(ErrorMessage) + .AddData(nameof(EventId), entity.Id) + .AddData(nameof(TypeName), entity.EventType) + .Build(); } diff --git a/src/Logitar.EventSourcing.Infrastructure/IEventEntity.cs b/src/Logitar.EventSourcing.Infrastructure/IEventEntity.cs index 86bbafb..b0bddc6 100644 --- a/src/Logitar.EventSourcing.Infrastructure/IEventEntity.cs +++ b/src/Logitar.EventSourcing.Infrastructure/IEventEntity.cs @@ -8,7 +8,7 @@ public interface IEventEntity /// /// Gets the identifier of the event. /// - Guid Id { get; } + string Id { get; } /// /// Gets the type of the event. diff --git a/src/Logitar.EventSourcing.MongoDB/EventEntity.cs b/src/Logitar.EventSourcing.MongoDB/EventEntity.cs index 90d89f6..103c2a0 100644 --- a/src/Logitar.EventSourcing.MongoDB/EventEntity.cs +++ b/src/Logitar.EventSourcing.MongoDB/EventEntity.cs @@ -6,7 +6,7 @@ namespace Logitar.EventSourcing.MongoDB; /// /// Represents the MongoDB storage model for events. /// -public record EventEntity : IEventEntity +public class EventEntity : IEventEntity { /// /// Initializes a new instance of the class. @@ -22,7 +22,7 @@ private EventEntity() /// /// Gets or sets the identifier of the event. /// - public Guid Id { get; private set; } + public string Id { get; private set; } = string.Empty; /// /// Gets or sets the identifier of the actor who triggered the event. @@ -72,7 +72,7 @@ public static IEnumerable FromChanges(AggregateRoot aggregate, IEve return aggregate.Changes.Select(change => new EventEntity { - Id = change.Id, + Id = change.Id.Value, ActorId = change.ActorId.Value, IsDeleted = change.IsDeleted, OccurredOn = change.OccurredOn.ToUniversalTime(), diff --git a/src/Logitar.EventSourcing.MongoDB/README.md b/src/Logitar.EventSourcing.MongoDB/README.md index 5227b72..020550d 100644 --- a/src/Logitar.EventSourcing.MongoDB/README.md +++ b/src/Logitar.EventSourcing.MongoDB/README.md @@ -1,4 +1,3 @@ # Logitar.EventSourcing.MongoDB -Provides an implementation of an event store to be used with the Event Sourcing architecture -pattern, and MongoDB. +Provides an implementation of an event store to be used with the Event Sourcing architecture pattern, and MongoDB. diff --git a/src/Logitar.EventSourcing.PostgreSQL/README.md b/src/Logitar.EventSourcing.PostgreSQL/README.md index ab383e9..619247b 100644 --- a/src/Logitar.EventSourcing.PostgreSQL/README.md +++ b/src/Logitar.EventSourcing.PostgreSQL/README.md @@ -1,4 +1,3 @@ # Logitar.EventSourcing.PostgreSQL -Provides an implementation of a relational event store to be used with the Event Sourcing -architecture pattern and PostgreSQL. +Provides an implementation of a relational event store to be used with the Event Sourcing architecture pattern and PostgreSQL. diff --git a/src/Logitar.EventSourcing.EntityFrameworkCore.PostgreSQL/Scripts/20230804163231_CreateEventTable.sql b/src/Logitar.EventSourcing.PostgreSQL/Scripts/20240607013711_CreateEventTable.sql similarity index 91% rename from src/Logitar.EventSourcing.EntityFrameworkCore.PostgreSQL/Scripts/20230804163231_CreateEventTable.sql rename to src/Logitar.EventSourcing.PostgreSQL/Scripts/20240607013711_CreateEventTable.sql index 68731f5..4574399 100644 --- a/src/Logitar.EventSourcing.EntityFrameworkCore.PostgreSQL/Scripts/20230804163231_CreateEventTable.sql +++ b/src/Logitar.EventSourcing.PostgreSQL/Scripts/20240607013711_CreateEventTable.sql @@ -8,9 +8,9 @@ START TRANSACTION; CREATE TABLE "Events" ( "EventId" bigint GENERATED BY DEFAULT AS IDENTITY, - "Id" uuid NOT NULL, + "Id" character varying(255) NOT NULL, "ActorId" character varying(255) NOT NULL, - "IsDeleted" boolean NULL, + "IsDeleted" boolean, "OccurredOn" timestamp with time zone NOT NULL, "Version" bigint NOT NULL, "AggregateType" character varying(255) NOT NULL, @@ -37,6 +37,6 @@ CREATE INDEX "IX_Events_OccurredOn" ON "Events" ("OccurredOn"); CREATE INDEX "IX_Events_Version" ON "Events" ("Version"); INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") -VALUES ('20230804163231_CreateEventTable', '7.0.9'); +VALUES ('20240607013711_CreateEventTable', '8.0.6'); -COMMIT; \ No newline at end of file +COMMIT; diff --git a/src/Logitar.EventSourcing.Relational/AggregateRepository.cs b/src/Logitar.EventSourcing.Relational/AggregateRepository.cs index 7020079..ae82828 100644 --- a/src/Logitar.EventSourcing.Relational/AggregateRepository.cs +++ b/src/Logitar.EventSourcing.Relational/AggregateRepository.cs @@ -123,7 +123,7 @@ protected virtual async Task> ReadChangesAsync(IQuery q { EventEntity entity = new() { - Id = reader.GetGuid(0), + Id = reader.GetString(0), EventType = reader.GetString(1), EventData = reader.GetString(2) }; @@ -155,7 +155,7 @@ protected override async Task SaveChangesAsync(IEnumerable aggreg foreach (DomainEvent change in aggregate.Changes) { - builder = builder.Value(change.Id, change.ActorId.Value, change.IsDeleted, + builder = builder.Value(change.Id.Value, change.ActorId.Value, change.IsDeleted, change.OccurredOn.ToUniversalTime(), change.Version, aggregateType, aggregateId, change.GetType().GetNamespaceQualifiedName(), EventSerializer.Serialize(change)); } diff --git a/src/Logitar.EventSourcing.Relational/EventEntity.cs b/src/Logitar.EventSourcing.Relational/EventEntity.cs index 2eaa42b..e7ca2ee 100644 --- a/src/Logitar.EventSourcing.Relational/EventEntity.cs +++ b/src/Logitar.EventSourcing.Relational/EventEntity.cs @@ -14,7 +14,7 @@ public class EventEntity : IEventEntity /// /// Gets or sets the identifier of the event. /// - public Guid Id { get; set; } + public string Id { get; set; } = string.Empty; /// /// Gets or sets the identifier of the actor who triggered the event. diff --git a/src/Logitar.EventSourcing.Relational/README.md b/src/Logitar.EventSourcing.Relational/README.md index a5ab6a9..d4db08b 100644 --- a/src/Logitar.EventSourcing.Relational/README.md +++ b/src/Logitar.EventSourcing.Relational/README.md @@ -1,4 +1,3 @@ # Logitar.EventSourcing.Relational -Provides an abstraction of a relational event store to be used with the Event Sourcing architecture -pattern. +Provides an abstraction of a relational event store to be used with the Event Sourcing architecture pattern. diff --git a/src/Logitar.EventSourcing.SqlServer/README.md b/src/Logitar.EventSourcing.SqlServer/README.md index b195253..b89a75a 100644 --- a/src/Logitar.EventSourcing.SqlServer/README.md +++ b/src/Logitar.EventSourcing.SqlServer/README.md @@ -1,4 +1,3 @@ # Logitar.EventSourcing.SqlServer -Provides an implementation of a relational event store to be used with the Event Sourcing -architecture pattern and Microsoft SQL Server. +Provides an implementation of a relational event store to be used with the Event Sourcing architecture pattern and Microsoft SQL Server. diff --git a/src/Logitar.EventSourcing.SqlServer/Scripts/20230804163434_CreateEventTable.sql b/src/Logitar.EventSourcing.SqlServer/Scripts/20240607013010_CreateEventTable.sql similarity index 90% rename from src/Logitar.EventSourcing.SqlServer/Scripts/20230804163434_CreateEventTable.sql rename to src/Logitar.EventSourcing.SqlServer/Scripts/20240607013010_CreateEventTable.sql index dd269ed..3b01e04 100644 --- a/src/Logitar.EventSourcing.SqlServer/Scripts/20230804163434_CreateEventTable.sql +++ b/src/Logitar.EventSourcing.SqlServer/Scripts/20240607013010_CreateEventTable.sql @@ -1,4 +1,4 @@ -IF OBJECT_ID(N'[__EFMigrationsHistory]') IS NULL +IF OBJECT_ID(N'[__EFMigrationsHistory]') IS NULL BEGIN CREATE TABLE [__EFMigrationsHistory] ( [MigrationId] nvarchar(150) NOT NULL, @@ -13,7 +13,7 @@ GO CREATE TABLE [Events] ( [EventId] bigint NOT NULL IDENTITY, - [Id] uniqueidentifier NOT NULL, + [Id] nvarchar(255) NOT NULL, [ActorId] nvarchar(255) NOT NULL, [IsDeleted] bit NULL, [OccurredOn] datetime2 NOT NULL, @@ -51,8 +51,8 @@ CREATE INDEX [IX_Events_Version] ON [Events] ([Version]); GO INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) -VALUES (N'20230804163434_CreateEventTable', N'7.0.9'); +VALUES (N'20240607013010_CreateEventTable', N'8.0.6'); GO COMMIT; -GO \ No newline at end of file +GO diff --git a/src/Logitar.EventSourcing/ActorId.cs b/src/Logitar.EventSourcing/ActorId.cs index fb5e298..d59135c 100644 --- a/src/Logitar.EventSourcing/ActorId.cs +++ b/src/Logitar.EventSourcing/ActorId.cs @@ -18,6 +18,10 @@ public readonly struct ActorId /// The value of the identifier. /// private readonly string? _value = null; + /// + /// Gets the value of the identifier. + /// + public string Value => _value ?? DefaultValue; /// /// Initializes a new instance of the struct. @@ -26,6 +30,7 @@ public readonly struct ActorId public ActorId(Guid value) : this(Convert.ToBase64String(value.ToByteArray()).ToUriSafeBase64()) { } + /// /// Initializes a new instance of the struct. /// @@ -49,9 +54,16 @@ public ActorId(string value) } /// - /// Gets the value of the identifier. + /// Creates a new instance of the struct from a random . /// - public string Value => _value ?? DefaultValue; + /// The created instance. + public static ActorId NewId() => new(Guid.NewGuid()); + + /// + /// Converts the identifier to a . The conversion will fail if the identifier has not been created from a . + /// + /// The resulting Guid. + public Guid ToGuid() => new(Convert.FromBase64String(Value.FromUriSafeBase64())); /// /// Returns a value indicating whether or not the specified identifiers are equal. @@ -68,17 +80,6 @@ public ActorId(string value) /// True if the identifiers are different. public static bool operator !=(ActorId left, ActorId right) => !left.Equals(right); - /// - /// Creates a new instance of the struct from a random . - /// - /// The created instance. - public static ActorId NewId() => new(Guid.NewGuid()); - /// - /// Converts the identifier to a . The conversion will fail if the identifier has not been created from a . - /// - /// The resulting Guid. - public Guid ToGuid() => new(Convert.FromBase64String(Value.FromUriSafeBase64())); - /// /// Returns a value indicating whether or not the specified object is equal to the identifier. /// diff --git a/src/Logitar.EventSourcing/AggregateConstructionFailedException.cs b/src/Logitar.EventSourcing/AggregateConstructionFailedException.cs deleted file mode 100644 index 3e25925..0000000 --- a/src/Logitar.EventSourcing/AggregateConstructionFailedException.cs +++ /dev/null @@ -1,74 +0,0 @@ -namespace Logitar.EventSourcing; - -/// -/// The exception thrown when the construction of an aggregate failed, or returned null. -/// -public class AggregateConstructionFailedException : Exception -{ - /// - /// Initializes a new instance of the class. - /// - /// The type of the aggregate. - /// The identifier of the aggregate. - /// The specified type is not a subclass of the type. - public AggregateConstructionFailedException(Type type, AggregateId id) - : base(BuildMessage(type, id)) - { - if (!type.IsSubclassOf(typeof(AggregateRoot))) - { - throw new ArgumentOutOfRangeException(nameof(type), $"The type must be a subclass of the '{nameof(AggregateRoot)}' type."); - } - - AggregateType = type.GetNamespaceQualifiedName(); - AggregateId = id.ToString(); - } - - /// - /// Gets or sets the type of the aggregate. - /// - public string AggregateType - { - get => (string)Data[nameof(AggregateType)]!; - private set => Data[nameof(AggregateType)] = value; - } - /// - /// Gets or sets the identifier of the aggregate. - /// - public string AggregateId - { - get => (string)Data[nameof(AggregateId)]!; - private set => Data[nameof(AggregateId)] = value; - } - - /// - /// Builds the exception message. - /// - /// The type of the aggregate. - /// The identifier of the aggregate. - /// The exception message. - private static string BuildMessage(Type type, AggregateId id) - { - StringBuilder message = new(); - - message.AppendLine("The aggregate construction failed."); - message.Append("TypeName: ").AppendLine(type.GetNamespaceQualifiedName()); - message.Append("AggregateId: ").Append(id).AppendLine(); - - return message.ToString(); - } -} - -/// -/// The typed exception thrown when the construction of an aggregate failed, or returned null. -/// -/// The type of the aggregate. -public class AggregateConstructionFailedException : AggregateConstructionFailedException where T : AggregateRoot -{ - /// - /// Initializes a new instance of the class. - /// - /// Th identifier of the aggregate. - public AggregateConstructionFailedException(AggregateId id) : base(typeof(T), id) - { - } -} diff --git a/src/Logitar.EventSourcing/AggregateId.cs b/src/Logitar.EventSourcing/AggregateId.cs index 620eed3..1b1b73c 100644 --- a/src/Logitar.EventSourcing/AggregateId.cs +++ b/src/Logitar.EventSourcing/AggregateId.cs @@ -14,6 +14,10 @@ public readonly struct AggregateId /// The value of the identifier. /// private readonly string? _value = null; + /// + /// Gets the value of the identifier. + /// + public string Value => _value ?? string.Empty; /// /// Initializes a new instance of the struct. @@ -22,6 +26,7 @@ public readonly struct AggregateId public AggregateId(Guid value) : this(Convert.ToBase64String(value.ToByteArray()).ToUriSafeBase64()) { } + /// /// Initializes a new instance of the struct. /// @@ -45,9 +50,16 @@ public AggregateId(string value) } /// - /// Gets the value of the identifier. + /// Creates a new instance of the struct from a random . /// - public string Value => _value ?? string.Empty; + /// The created instance. + public static AggregateId NewId() => new(Guid.NewGuid()); + + /// + /// Converts the identifier to a . The conversion will fail if the identifier has not been created from a . + /// + /// The resulting Guid. + public Guid ToGuid() => new(Convert.FromBase64String(Value.FromUriSafeBase64())); /// /// Returns a value indicating whether or not the specified identifiers are equal. @@ -64,17 +76,6 @@ public AggregateId(string value) /// True if the identifiers are different. public static bool operator !=(AggregateId left, AggregateId right) => !left.Equals(right); - /// - /// Creates a new instance of the struct from a random . - /// - /// The created instance. - public static AggregateId NewId() => new(Guid.NewGuid()); - /// - /// Converts the identifier to a . The conversion will fail if the identifier has not been created from a . - /// - /// The resulting Guid. - public Guid ToGuid() => new(Convert.FromBase64String(Value.FromUriSafeBase64())); - /// /// Returns a value indicating whether or not the specified object is equal to the identifier. /// diff --git a/src/Logitar.EventSourcing/AggregateRoot.cs b/src/Logitar.EventSourcing/AggregateRoot.cs index 322c59d..e2bbd32 100644 --- a/src/Logitar.EventSourcing/AggregateRoot.cs +++ b/src/Logitar.EventSourcing/AggregateRoot.cs @@ -59,31 +59,33 @@ public abstract class AggregateRoot /// The identifier value is missing. protected AggregateRoot(AggregateId? id = null) { - id ??= AggregateId.NewId(); - if (string.IsNullOrWhiteSpace(id.Value.Value)) + if (id.HasValue) { - throw new ArgumentException("The identifier value is required.", nameof(id)); - } + if (string.IsNullOrWhiteSpace(id.Value.Value)) + { + throw new ArgumentException("The identifier value is required.", nameof(id)); + } - Id = id.Value; + Id = id.Value; + } + else + { + Id = AggregateId.NewId(); + } } /// /// Loads an aggregate from its changes and assign its identifier. /// - /// The type of the aggregate to load. /// The identifier of the aggregate. /// The changes of the aggregate. - /// The aggregate construction failed. - /// The aggregate does not declare a public identifier constructor. /// The loaded aggregate. - public static T LoadFromChanges(AggregateId id, IEnumerable changes) where T : AggregateRoot + public static T LoadFromChanges(AggregateId id, IEnumerable changes) where T : AggregateRoot, new() { - ConstructorInfo constructor = typeof(T).GetConstructor([typeof(AggregateId)]) - ?? throw new MissingAggregateConstructorException(); - - T aggregate = (T?)constructor.Invoke([id]) - ?? throw new AggregateConstructionFailedException(id); + T aggregate = new() + { + Id = id + }; IOrderedEnumerable ordered = changes.OrderBy(e => e.Version); foreach (DomainEvent change in ordered) @@ -174,6 +176,7 @@ private void Handle(DomainEvent change) UpdatedBy = change.ActorId; UpdatedOn = change.OccurredOn; } + /// /// Dispatches the specified change to be applied through the current aggregate. This method can be overriden to provide a more efficient way of applying /// changes, such as using type pattern matching @@ -182,7 +185,7 @@ private void Handle(DomainEvent change) /// The change to apply. protected virtual void Dispatch(DomainEvent change) { - MethodInfo? apply = GetType().GetMethod("Apply", BindingFlags.Instance | BindingFlags.NonPublic, [change.GetType()]); + MethodInfo? apply = GetType().GetMethod("Apply", BindingFlags.Instance | BindingFlags.NonPublic, Type.DefaultBinder, [change.GetType()], modifiers: []); apply?.Invoke(this, new[] { change }); } @@ -204,5 +207,5 @@ public override bool Equals(object? obj) /// Returns a string representation of the aggregate. /// /// The string representation. - public override string ToString() => $"{GetType()} ({Id})"; + public override string ToString() => $"{GetType()} (Id={Id})"; } diff --git a/src/Logitar.EventSourcing/CannotApplyPastEventException.cs b/src/Logitar.EventSourcing/CannotApplyPastEventException.cs index 189740c..5d4ec29 100644 --- a/src/Logitar.EventSourcing/CannotApplyPastEventException.cs +++ b/src/Logitar.EventSourcing/CannotApplyPastEventException.cs @@ -6,19 +6,9 @@ public class CannotApplyPastEventException : Exception { /// - /// Initializes a new instance of the class. + /// The detailed error message. /// - /// The aggregate in a future state. - /// The event of a past state. - public CannotApplyPastEventException(AggregateRoot aggregate, DomainEvent change) : base(BuildMessage(aggregate, change)) - { - Aggregate = aggregate.ToString(); - AggregateId = aggregate.Id.ToString(); - AggregateVersion = aggregate.Version; - Event = change.ToString(); - EventId = change.Id; - EventVersion = change.Version; - } + private const string ErrorMessage = "The specified event is past the current state of the specified aggregate."; /// /// Gets or sets the string representation of the aggregate. @@ -55,9 +45,9 @@ public string Event /// /// Gets or sets the identifier of the event. /// - public Guid? EventId + public string? EventId { - get => (Guid?)Data[nameof(EventId)]; + get => (string?)Data[nameof(EventId)]; private set => Data[nameof(EventId)] = value; } /// @@ -70,23 +60,32 @@ public long? EventVersion } /// - /// Builds the exception message. + /// Initializes a new instance of the class. /// /// The aggregate in a future state. /// The event of a past state. - /// The exception message. - private static string BuildMessage(AggregateRoot aggregate, DomainEvent change) + public CannotApplyPastEventException(AggregateRoot aggregate, DomainEvent change) : base(BuildMessage(aggregate, change)) { - StringBuilder message = new(); - - message.AppendLine("The specified event is past the current state of the specified aggregate."); - message.Append("Aggregate: ").Append(aggregate).AppendLine(); - message.Append("AggregateId: ").Append(aggregate.Id).AppendLine(); - message.Append("AggregateVersion: ").Append(aggregate.Version).AppendLine(); - message.Append("Event: ").Append(change).AppendLine(); - message.Append("EventId: ").Append(change.Id).AppendLine(); - message.Append("EventVersion: ").Append(change.Version).AppendLine(); - - return message.ToString(); + Aggregate = aggregate.ToString(); + AggregateId = aggregate.Id.ToString(); + AggregateVersion = aggregate.Version; + Event = change.ToString(); + EventId = change.Id.ToString(); + EventVersion = change.Version; } + + /// + /// Builds the exception message. + /// + /// The aggregate in a future state. + /// The event of a past state. + /// The exception message. + private static string BuildMessage(AggregateRoot aggregate, DomainEvent change) => new ErrorMessageBuilder(ErrorMessage) + .AddData(nameof(Aggregate), aggregate) + .AddData(nameof(AggregateId), aggregate.Id) + .AddData(nameof(AggregateVersion), aggregate.Version) + .AddData(nameof(Event), change) + .AddData(nameof(EventId), change.Id) + .AddData(nameof(EventVersion), change.Version) + .Build(); } diff --git a/src/Logitar.EventSourcing/DomainEvent.cs b/src/Logitar.EventSourcing/DomainEvent.cs index 2ff630f..b111bd5 100644 --- a/src/Logitar.EventSourcing/DomainEvent.cs +++ b/src/Logitar.EventSourcing/DomainEvent.cs @@ -3,12 +3,12 @@ /// /// Represents a domain event that has been raised by an and can be applied to it. /// -public abstract record DomainEvent +public abstract class DomainEvent { /// /// Gets or sets the identifier of the event. /// - public Guid Id { get; set; } = Guid.NewGuid(); + public EventId Id { get; set; } = EventId.NewId(); /// /// Gets or sets the identifier of the aggregate to which the event belongs to. @@ -32,4 +32,21 @@ public abstract record DomainEvent /// Gets or sets a value indicating whether or not the aggregate is deleted. /// public bool? IsDeleted { get; set; } + + /// + /// Returns a value indicating whether or not the specified object is equal to the domain event. + /// + /// The object to be compared to. + /// True if the object is equal to the domain event. + public override bool Equals(object obj) => obj is DomainEvent @event && @event.Id == Id; + /// + /// Returns the hash code of the current domain event. + /// + /// The hash code. + public override int GetHashCode() => HashCode.Combine(GetType(), Id); + /// + /// Returns a string representation of the domain event. + /// + /// The string representation. + public override string ToString() => $"{GetType()} (Id={Id})"; } diff --git a/src/Logitar.EventSourcing/EventAggregateMismatchException.cs b/src/Logitar.EventSourcing/EventAggregateMismatchException.cs index 24647f2..5741ddf 100644 --- a/src/Logitar.EventSourcing/EventAggregateMismatchException.cs +++ b/src/Logitar.EventSourcing/EventAggregateMismatchException.cs @@ -6,18 +6,9 @@ public class EventAggregateMismatchException : Exception { /// - /// Initializes a new instance of the class. + /// The detailed error message. /// - /// The aggregate unto which the event was applied. - /// The event belonging to another aggregate. - public EventAggregateMismatchException(AggregateRoot aggregate, DomainEvent change) : base(BuildMessage(aggregate, change)) - { - Aggregate = aggregate.ToString(); - AggregateId = aggregate.Id.ToString(); - Event = change.ToString(); - EventId = change.Id; - EventAggregateId = change.AggregateId.ToString(); - } + private const string ErrorMessage = "The specified event does not belong to the specified aggregate."; /// /// Gets or sets the string representation of the aggregate. @@ -46,9 +37,9 @@ public string Event /// /// Gets or sets the identifier of the event. /// - public Guid EventId + public string EventId { - get => (Guid)Data[nameof(EventId)]!; + get => (string)Data[nameof(EventId)]!; private set => Data[nameof(EventId)] = value; } /// @@ -61,22 +52,31 @@ public string EventAggregateId } /// - /// Builds the exception message. + /// Initializes a new instance of the class. /// /// The aggregate unto which the event was applied. /// The event belonging to another aggregate. - /// The exception message. - private static string BuildMessage(AggregateRoot aggregate, DomainEvent change) + public EventAggregateMismatchException(AggregateRoot aggregate, DomainEvent change) : base(BuildMessage(aggregate, change)) { - StringBuilder message = new(); - - message.AppendLine("The specified event does not belong to the specified aggregate."); - message.Append("Aggregate: ").Append(aggregate).AppendLine(); - message.Append("AggregateId: ").Append(aggregate.Id).AppendLine(); - message.Append("Event: ").Append(change).AppendLine(); - message.Append("EventId: ").Append(change.Id).AppendLine(); - message.Append("EventAggregateId: ").Append(change.AggregateId).AppendLine(); - - return message.ToString(); + Aggregate = aggregate.ToString(); + AggregateId = aggregate.Id.ToString(); + Event = change.ToString(); + EventId = change.Id.ToString(); + EventAggregateId = change.AggregateId.ToString(); } + + /// + /// Builds the exception message. + /// + /// The aggregate unto which the event was applied. + /// The event belonging to another aggregate. + /// The exception message. + private static string BuildMessage(AggregateRoot aggregate, DomainEvent change) => new ErrorMessageBuilder(ErrorMessage) + .AddData(nameof(Aggregate), aggregate) + .AddData(nameof(AggregateId), aggregate.Id) + .AddData(nameof(Event), change) + .AddData(nameof(EventId), change.Id) + .AddData(nameof(EventAggregateId), change.AggregateId) + .Build(); } + diff --git a/src/Logitar.EventSourcing/EventId.cs b/src/Logitar.EventSourcing/EventId.cs new file mode 100644 index 0000000..a45f1b1 --- /dev/null +++ b/src/Logitar.EventSourcing/EventId.cs @@ -0,0 +1,95 @@ +namespace Logitar.EventSourcing; + +/// +/// Represents the identifier of a . +/// +public readonly struct EventId +{ + /// + /// The maximum length of the identifier value. + /// + public const int MaximumLength = byte.MaxValue; + + /// + /// The value of the identifier. + /// + private readonly string? _value = null; + /// + /// Gets the value of the identifier. + /// + public string Value => _value ?? string.Empty; + + /// + /// Initializes a new instance of the struct. + /// + /// The value of the identifier. + public EventId(Guid value) : this(Convert.ToBase64String(value.ToByteArray()).ToUriSafeBase64()) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The value of the identifier. + /// The value is null, empty or only white space. + /// The value exceeds the maximum length. + public EventId(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentException("The value is required.", nameof(value)); + } + + value = value.Trim(); + if (value.Length > MaximumLength) + { + throw new ArgumentOutOfRangeException(nameof(value), $"The value may contain up to {MaximumLength} characters."); + } + + _value = value; + } + + /// + /// Creates a new instance of the struct from a random . + /// + /// The created instance. + public static EventId NewId() => new(Guid.NewGuid()); + + /// + /// Converts the identifier to a . The conversion will fail if the identifier has not been created from a . + /// + /// The resulting Guid. + public Guid ToGuid() => new(Convert.FromBase64String(Value.FromUriSafeBase64())); + + /// + /// Returns a value indicating whether or not the specified identifiers are equal. + /// + /// The first identifier to compare. + /// The other identifier to compare. + /// True if the identifiers are equal. + public static bool operator ==(EventId left, EventId right) => left.Equals(right); + /// + /// Returns a value indicating whether or not the specified identifiers are different. + /// + /// The first identifier to compare. + /// The other identifier to compare. + /// True if the identifiers are different. + public static bool operator !=(EventId left, EventId right) => !left.Equals(right); + + /// + /// Returns a value indicating whether or not the specified object is equal to the identifier. + /// + /// The object to be compared to. + /// True if the object is equal to the identifier. + public override bool Equals([NotNullWhen(true)] object? obj) => obj is EventId id && id.Value == Value; + /// + /// Returns the hash code of the current identifier. + /// + /// The hash code. + public override int GetHashCode() => Value.GetHashCode(); + /// + /// Returns a string representation of the identifier. + /// + /// The string representation. + public override string ToString() => Value; +} diff --git a/src/Logitar.EventSourcing/IAggregateRepository.cs b/src/Logitar.EventSourcing/IAggregateRepository.cs index f7b988b..9adeae1 100644 --- a/src/Logitar.EventSourcing/IAggregateRepository.cs +++ b/src/Logitar.EventSourcing/IAggregateRepository.cs @@ -12,7 +12,7 @@ public interface IAggregateRepository /// The identifier of the aggregate. /// The cancellation token. /// The loaded aggregate. - Task LoadAsync(AggregateId id, CancellationToken cancellationToken = default) where T : AggregateRoot; + Task LoadAsync(AggregateId id, CancellationToken cancellationToken = default) where T : AggregateRoot, new(); /// /// Loads an aggregate from the event store. /// @@ -21,7 +21,7 @@ public interface IAggregateRepository /// The version at which the aggregate shall be retrieved. /// The cancellation token. /// The loaded aggregate. - Task LoadAsync(AggregateId id, long? version, CancellationToken cancellationToken = default) where T : AggregateRoot; + Task LoadAsync(AggregateId id, long? version, CancellationToken cancellationToken = default) where T : AggregateRoot, new(); /// /// Loads an aggregate from the event store. /// @@ -30,7 +30,7 @@ public interface IAggregateRepository /// A value indicating whether or not a deleted aggregate will be returned. /// The cancellation token. /// The loaded aggregate. - Task LoadAsync(AggregateId id, bool includeDeleted, CancellationToken cancellationToken = default) where T : AggregateRoot; + Task LoadAsync(AggregateId id, bool includeDeleted, CancellationToken cancellationToken = default) where T : AggregateRoot, new(); /// /// Loads an aggregate from the event store. /// @@ -40,7 +40,7 @@ public interface IAggregateRepository /// A value indicating whether or not a deleted aggregate will be returned. /// The cancellation token. /// The loaded aggregate. - Task LoadAsync(AggregateId id, long? version, bool includeDeleted, CancellationToken cancellationToken = default) where T : AggregateRoot; + Task LoadAsync(AggregateId id, long? version, bool includeDeleted, CancellationToken cancellationToken = default) where T : AggregateRoot, new(); /// /// Loads all the aggregates of a specific type from the event store. @@ -48,7 +48,7 @@ public interface IAggregateRepository /// The type of the aggregates. /// The cancellation token. /// The list of loaded aggregates. - Task> LoadAsync(CancellationToken cancellationToken = default) where T : AggregateRoot; + Task> LoadAsync(CancellationToken cancellationToken = default) where T : AggregateRoot, new(); /// /// Loads all the aggregates of a specific type from the event store. /// @@ -56,7 +56,7 @@ public interface IAggregateRepository /// A value indicating whether or not deleted aggregates will be returned. /// The cancellation token. /// The list of loaded aggregates. - Task> LoadAsync(bool includeDeleted, CancellationToken cancellationToken = default) where T : AggregateRoot; + Task> LoadAsync(bool includeDeleted, CancellationToken cancellationToken = default) where T : AggregateRoot, new(); /// /// Loads a list of aggregates from the event store. @@ -65,7 +65,7 @@ public interface IAggregateRepository /// The identifier of the aggregates. /// The cancellation token. /// The list of loaded aggregates. - Task> LoadAsync(IEnumerable ids, CancellationToken cancellationToken = default) where T : AggregateRoot; + Task> LoadAsync(IEnumerable ids, CancellationToken cancellationToken = default) where T : AggregateRoot, new(); /// /// Loads a list of aggregates from the event store. /// @@ -74,7 +74,7 @@ public interface IAggregateRepository /// A value indicating whether or not deleted aggregates will be returned. /// The cancellation token. /// The list of loaded aggregates. - Task> LoadAsync(IEnumerable ids, bool includeDeleted, CancellationToken cancellationToken = default) where T : AggregateRoot; + Task> LoadAsync(IEnumerable ids, bool includeDeleted, CancellationToken cancellationToken = default) where T : AggregateRoot, new(); /// /// Persists an aggregate to the event store. diff --git a/src/Logitar.EventSourcing/Logitar.EventSourcing.csproj b/src/Logitar.EventSourcing/Logitar.EventSourcing.csproj index 5a94478..448aa50 100644 --- a/src/Logitar.EventSourcing/Logitar.EventSourcing.csproj +++ b/src/Logitar.EventSourcing/Logitar.EventSourcing.csproj @@ -1,8 +1,8 @@ - net8.0 - enable + netstandard2.1 + 12 enable True Logitar.EventSourcing @@ -35,13 +35,18 @@ - + + + + + + diff --git a/src/Logitar.EventSourcing/MissingAggregateConstructorException.cs b/src/Logitar.EventSourcing/MissingAggregateConstructorException.cs deleted file mode 100644 index d1c0a59..0000000 --- a/src/Logitar.EventSourcing/MissingAggregateConstructorException.cs +++ /dev/null @@ -1,60 +0,0 @@ -namespace Logitar.EventSourcing; - -/// -/// The exception thrown when an aggregate is missing its identifier public constructor. -/// -public class MissingAggregateConstructorException : Exception -{ - /// - /// Initializes a new instance of the class. - /// - /// The type of the aggregate. - /// The specified type is not a subclass of the type. - public MissingAggregateConstructorException(Type type) : base(BuildMessage(type)) - { - if (!type.IsSubclassOf(typeof(AggregateRoot))) - { - throw new ArgumentOutOfRangeException(nameof(type), $"The type must be a subclass of the '{nameof(AggregateRoot)}' type."); - } - - AggregateType = type.GetNamespaceQualifiedName(); - } - - /// - /// Gets or sets the type of the aggregate. - /// - public string AggregateType - { - get => (string)Data[nameof(AggregateType)]!; - private set => Data[nameof(AggregateType)] = value; - } - - /// - /// Builds the exception message. - /// - /// The type of the aggregate. - /// The exception message. - private static string BuildMessage(Type type) - { - StringBuilder message = new(); - - message.AppendLine("The specified aggregate type does not declare a public constructor receiving an AggregateId as its only argument."); - message.Append("TypeName: ").AppendLine(type.GetNamespaceQualifiedName()); - - return message.ToString(); - } -} - -/// -/// The typed exception thrown when an aggregate is missing its identifier public constructor. -/// -/// The type of the aggregate. -public class MissingAggregateConstructorException : MissingAggregateConstructorException where T : AggregateRoot -{ - /// - /// Initializes a new instance of the class. - /// - public MissingAggregateConstructorException() : base(typeof(T)) - { - } -} diff --git a/tests/Logitar.EventSourcing.EFCore.Relational.UnitTests/EventEntityTests.cs b/tests/Logitar.EventSourcing.EFCore.Relational.UnitTests/EventEntityTests.cs index 4d12605..c4c6169 100644 --- a/tests/Logitar.EventSourcing.EFCore.Relational.UnitTests/EventEntityTests.cs +++ b/tests/Logitar.EventSourcing.EFCore.Relational.UnitTests/EventEntityTests.cs @@ -25,7 +25,7 @@ public void FromChanges_it_should_return_the_correct_changes() private void AssertEqual(EventEntity entity, DomainEvent change, AggregateRoot aggregate) { - Assert.Equal(entity.Id, change.Id); + Assert.Equal(entity.Id, change.Id.Value); Assert.Equal(entity.ActorId, change.ActorId.Value); Assert.Equal(entity.IsDeleted, change.IsDeleted); Assert.Equal(entity.OccurredOn, change.OccurredOn.ToUniversalTime()); diff --git a/tests/Logitar.EventSourcing.Infrastructure.IntegrationTests/AggregateRepositoryTests.cs b/tests/Logitar.EventSourcing.Infrastructure.IntegrationTests/AggregateRepositoryTests.cs index 9957ec7..f144138 100644 --- a/tests/Logitar.EventSourcing.Infrastructure.IntegrationTests/AggregateRepositoryTests.cs +++ b/tests/Logitar.EventSourcing.Infrastructure.IntegrationTests/AggregateRepositoryTests.cs @@ -100,7 +100,7 @@ public async Task It_should_persist_and_publish_the_correct_events_from_aggregat PersonAggregate[] aggregates = new[] { _person1, _person2, _person3 }; DomainEvent[] changes = aggregates.SelectMany(p => p.Changes).ToArray(); - Dictionary events = aggregates.SelectMany(GetEventEntities) + Dictionary events = aggregates.SelectMany(GetEventEntities) .ToDictionary(e => e.Id, e => e); await _repository.SaveAsync(aggregates, _cancellationToken); @@ -126,7 +126,7 @@ public async Task It_should_persist_and_publish_the_correct_events_from_an_aggre await AssertDatabaseIsEmptyAsync(); DomainEvent[] changes = _deleted.Changes.ToArray(); - Dictionary events = GetEventEntities(_deleted) + Dictionary events = GetEventEntities(_deleted) .ToDictionary(e => e.Id, e => e); await _repository.SaveAsync(_deleted, _cancellationToken); diff --git a/tests/Logitar.EventSourcing.Infrastructure.UnitTests/AggregateRepositoryTests.cs b/tests/Logitar.EventSourcing.Infrastructure.UnitTests/AggregateRepositoryTests.cs index 82cd105..480adf5 100644 --- a/tests/Logitar.EventSourcing.Infrastructure.UnitTests/AggregateRepositoryTests.cs +++ b/tests/Logitar.EventSourcing.Infrastructure.UnitTests/AggregateRepositoryTests.cs @@ -41,7 +41,7 @@ public async Task SaveAsync_it_should_publish_and_clear_aggregates_changes() { PersonAggregate person = new(new Faker().Person.FullName); PersonAggregate deleted = new(new Faker().Person.FullName); - PersonAggregate noChange = new(AggregateId.NewId()); + PersonAggregate noChange = new(); deleted.Delete(); diff --git a/tests/Logitar.EventSourcing.Infrastructure.UnitTests/DefaultLanguageChangedEvent.cs b/tests/Logitar.EventSourcing.Infrastructure.UnitTests/DefaultLanguageChangedEvent.cs index da9bce7..6b6b554 100644 --- a/tests/Logitar.EventSourcing.Infrastructure.UnitTests/DefaultLanguageChangedEvent.cs +++ b/tests/Logitar.EventSourcing.Infrastructure.UnitTests/DefaultLanguageChangedEvent.cs @@ -1,3 +1,11 @@ namespace Logitar.EventSourcing.Infrastructure; -internal record DefaultLanguageChangedEvent(CultureInfo Culture) : DomainEvent; +internal class DefaultLanguageChangedEvent : DomainEvent +{ + public CultureInfo Culture { get; } + + public DefaultLanguageChangedEvent(CultureInfo culture) + { + Culture = culture; + } +} diff --git a/tests/Logitar.EventSourcing.Infrastructure.UnitTests/EventEntityMock.cs b/tests/Logitar.EventSourcing.Infrastructure.UnitTests/EventEntityMock.cs index 39080f4..516d69c 100644 --- a/tests/Logitar.EventSourcing.Infrastructure.UnitTests/EventEntityMock.cs +++ b/tests/Logitar.EventSourcing.Infrastructure.UnitTests/EventEntityMock.cs @@ -2,7 +2,7 @@ internal class EventEntityMock : IEventEntity { - public Guid Id { get; init; } + public string Id { get; init; } = string.Empty; public string EventType { get; init; } = string.Empty; public string EventData { get; init; } = string.Empty; diff --git a/tests/Logitar.EventSourcing.Infrastructure.UnitTests/EventSerializerTests.cs b/tests/Logitar.EventSourcing.Infrastructure.UnitTests/EventSerializerTests.cs index c6e3482..ce71552 100644 --- a/tests/Logitar.EventSourcing.Infrastructure.UnitTests/EventSerializerTests.cs +++ b/tests/Logitar.EventSourcing.Infrastructure.UnitTests/EventSerializerTests.cs @@ -34,6 +34,7 @@ public void Ctor_it_should_construct_the_correct_EventSerializer_from_a_list_of_ Assert.Contains(options.Converters, converter => converter is ActorIdConverter); Assert.Contains(options.Converters, converter => converter is AggregateIdConverter); + Assert.Contains(options.Converters, converter => converter is EventIdConverter); Assert.Contains(options.Converters, converter => converter is JsonStringEnumConverter); Assert.Contains(options.Converters, converter => converter is CultureInfoConverter); } @@ -43,7 +44,7 @@ public void Deserialize_it_should_deserialize_the_correct_domain_event() { DefaultLanguageChangedEvent expected = new(CultureInfo.GetCultureInfo("en-CA")) { - Id = Guid.NewGuid(), + Id = EventId.NewId(), AggregateId = AggregateId.NewId(), Version = 5, ActorId = new ActorId("fpion"), @@ -51,7 +52,7 @@ public void Deserialize_it_should_deserialize_the_correct_domain_event() }; EventEntityMock entity = new() { - Id = expected.Id, + Id = expected.Id.Value, EventType = expected.GetType().GetNamespaceQualifiedName(), EventData = _serializer.Serialize(expected) }; @@ -65,7 +66,7 @@ public void Deserialize_it_should_throw_EventDataDeserializationFailedException_ { EventEntityMock entity = new() { - Id = Guid.NewGuid(), + Id = EventId.NewId().Value, EventType = typeof(DefaultLanguageChangedEvent).GetNamespaceQualifiedName(), EventData = "null" }; @@ -80,7 +81,7 @@ public void Deserialize_it_should_throw_EventTypeNotFoundException_when_event_ty { EventEntityMock entity = new() { - Id = Guid.NewGuid(), + Id = EventId.NewId().Value, EventType = "Test" }; var exception = Assert.Throws(() => _serializer.Deserialize(entity)); @@ -110,7 +111,7 @@ public void Serialize_it_should_serialize_events_correctly() { DefaultLanguageChangedEvent e = new(CultureInfo.GetCultureInfo("en-CA")) { - Id = Guid.NewGuid(), + Id = EventId.NewId(), AggregateId = AggregateId.NewId(), Version = 5, ActorId = new ActorId("fpion"), diff --git a/tests/Logitar.EventSourcing.PostgreSQL.IntegrationTests/Init.sql b/tests/Logitar.EventSourcing.PostgreSQL.IntegrationTests/Init.sql index 012af25..ca0b022 100644 --- a/tests/Logitar.EventSourcing.PostgreSQL.IntegrationTests/Init.sql +++ b/tests/Logitar.EventSourcing.PostgreSQL.IntegrationTests/Init.sql @@ -11,9 +11,9 @@ START TRANSACTION; CREATE TABLE "Events" ( "EventId" bigint GENERATED BY DEFAULT AS IDENTITY, - "Id" uuid NOT NULL, + "Id" character varying(255) NOT NULL, "ActorId" character varying(255) NOT NULL, - "IsDeleted" boolean NULL, + "IsDeleted" boolean, "OccurredOn" timestamp with time zone NOT NULL, "Version" bigint NOT NULL, "AggregateType" character varying(255) NOT NULL, @@ -40,6 +40,6 @@ CREATE INDEX "IX_Events_OccurredOn" ON "Events" ("OccurredOn"); CREATE INDEX "IX_Events_Version" ON "Events" ("Version"); INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") -VALUES ('20230804163231_CreateEventTable', '7.0.9'); +VALUES ('20240607013711_CreateEventTable', '8.0.6'); COMMIT; diff --git a/tests/Logitar.EventSourcing.Relational.IntegrationTests/AggregateRepositoryTests.cs b/tests/Logitar.EventSourcing.Relational.IntegrationTests/AggregateRepositoryTests.cs index 947e97f..8939a66 100644 --- a/tests/Logitar.EventSourcing.Relational.IntegrationTests/AggregateRepositoryTests.cs +++ b/tests/Logitar.EventSourcing.Relational.IntegrationTests/AggregateRepositoryTests.cs @@ -34,7 +34,7 @@ protected override IEnumerable GetEventEntities(AggregateRoot aggr return aggregate.Changes.Select(change => new EventEntity { - Id = change.Id, + Id = change.Id.Value, ActorId = change.ActorId.Value, IsDeleted = change.IsDeleted, OccurredOn = change.OccurredOn.ToUniversalTime(), @@ -67,7 +67,7 @@ protected override async Task> LoadEventsAsync(Cancell { entities.Add(new EventEntity { - Id = reader.GetGuid(0), + Id = reader.GetString(0), EventType = reader.GetString(1), EventData = reader.GetString(2) }); @@ -104,7 +104,7 @@ protected override async Task SeedDatabaseAsync(IEnumerable aggre foreach (DomainEvent change in aggregate.Changes) { - builder = builder.Value(change.Id, change.ActorId.Value, change.IsDeleted, + builder = builder.Value(change.Id.Value, change.ActorId.Value, change.IsDeleted, change.OccurredOn.ToUniversalTime(), change.Version, aggregateType, aggregateId, change.GetType().GetNamespaceQualifiedName(), EventSerializer.Serialize(change)); } diff --git a/tests/Logitar.EventSourcing.SqlServer.IntegrationTests/Init.sql b/tests/Logitar.EventSourcing.SqlServer.IntegrationTests/Init.sql index ef8b63c..c85109f 100644 --- a/tests/Logitar.EventSourcing.SqlServer.IntegrationTests/Init.sql +++ b/tests/Logitar.EventSourcing.SqlServer.IntegrationTests/Init.sql @@ -22,7 +22,7 @@ GO CREATE TABLE [Events] ( [EventId] bigint NOT NULL IDENTITY, - [Id] uniqueidentifier NOT NULL, + [Id] nvarchar(255) NOT NULL, [ActorId] nvarchar(255) NOT NULL, [IsDeleted] bit NULL, [OccurredOn] datetime2 NOT NULL, @@ -60,7 +60,7 @@ CREATE INDEX [IX_Events_Version] ON [Events] ([Version]); GO INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) -VALUES (N'20230804163434_CreateEventTable', N'7.0.9'); +VALUES (N'20240607013010_CreateEventTable', N'8.0.6'); GO COMMIT; diff --git a/tests/Logitar.EventSourcing.UnitTests/AggregateRootTests.cs b/tests/Logitar.EventSourcing.UnitTests/AggregateRootTests.cs index ae24687..fdc12fd 100644 --- a/tests/Logitar.EventSourcing.UnitTests/AggregateRootTests.cs +++ b/tests/Logitar.EventSourcing.UnitTests/AggregateRootTests.cs @@ -17,7 +17,7 @@ public AggregateRootTests() [Fact(DisplayName = "ClearChanges: it should clear uncommitted changes correctly.")] public void ClearChanges_it_should_clear_uncommitted_changes_correctly() { - PersonAggregate person = new(AggregateId.NewId()); + PersonAggregate person = new(); person.ClearChanges(); person.Delete(); @@ -116,7 +116,7 @@ public void Handle_it_throws_CannotApplyPastEventException_when_event_version_is Assert.Equal(_person.Id.ToString(), exception.AggregateId); Assert.Equal(_person.Version, exception.AggregateVersion); Assert.Equal(e.ToString(), exception.Event); - Assert.Equal(e.Id, exception.EventId); + Assert.Equal(e.Id.ToString(), exception.EventId); Assert.Equal(e.Version, exception.EventVersion); } @@ -131,7 +131,7 @@ public void Handle_it_throws_EventAggregateMismatchException_when_event_does_not var exception = Assert.Throws(() => _person.Handle(e)); Assert.Equal(_person.Id.ToString(), exception.AggregateId); Assert.Equal(e.ToString(), exception.Event); - Assert.Equal(e.Id, exception.EventId); + Assert.Equal(e.Id.ToString(), exception.EventId); Assert.Equal(e.AggregateId.ToString(), exception.EventAggregateId); } @@ -183,7 +183,7 @@ public void Handle_method_Apply_is_missing_does_nothing() [Fact(DisplayName = "It should track changes correctly.")] public void It_should_track_changes_correctly() { - PersonAggregate person = new(AggregateId.NewId()); + PersonAggregate person = new(); Assert.False(person.HasChanges); Assert.Empty(person.Changes); @@ -192,41 +192,6 @@ public void It_should_track_changes_correctly() Assert.NotEmpty(person.Changes); } - [Fact(DisplayName = "LoadFromChanges: it constructs the correct aggregate.")] - public void LoadFromChanges_it_constructs_the_correct_aggregate() - { - AggregateId id = AggregateId.NewId(); - DomainEvent[] events = - [ - new PersonCreatedEvent(_faker.Person.FullName) - { - AggregateId = id, - Version = 1, - OccurredOn = DateTime.Now.AddYears(-20) - }, - new PersonDeletedChangedEvent(isDeleted: true) - { - AggregateId = id, - Version = 2, - OccurredOn = DateTime.Now - } - ]; - - PersonAggregate person = AggregateRoot.LoadFromChanges(id, events); - Assert.Equal(id, person.Id); - Assert.Equal(2, person.Version); - Assert.True(person.IsDeleted); - Assert.Equal(_faker.Person.FullName, person.FullName); - } - - [Fact(DisplayName = "LoadFromChanges: it throws MissingAggregateConstructorException when public identifier constructor is missing.")] - public void LoadFromChanges_it_throws_MissingAggregateConstructorException_when_public_identifier_constructor_is_missing() - { - AggregateId id = AggregateId.NewId(); - List changes = []; - Assert.Throws>(() => AggregateRoot.LoadFromChanges(id, changes)); - } - [Fact(DisplayName = "Raise: it applies the change correctly.")] public void Raise_it_applies_the_change_correctly() { @@ -245,7 +210,7 @@ public void Raise_it_applies_the_change_correctly() Assert.NotNull(changes); DomainEvent e = changes.Single(); - Assert.NotEqual(Guid.Empty, e.Id); + Assert.NotEqual(string.Empty, e.Id.Value); Assert.Equal(person.Id, e.AggregateId); Assert.Equal(person.Version, e.Version); Assert.Equal(actorId, e.ActorId); @@ -256,7 +221,7 @@ public void Raise_it_applies_the_change_correctly() [Fact(DisplayName = "ToString: it returns the correct string representation.")] public void ToString_it_returns_the_correct_string_representation() { - string s = string.Concat(_person.GetType(), " (", _person.Id, ')'); + string s = string.Concat(_person.GetType(), " (Id=", _person.Id, ')'); Assert.Equal(s, _person.ToString()); } } diff --git a/tests/Logitar.EventSourcing.UnitTests/ContactAggregate.cs b/tests/Logitar.EventSourcing.UnitTests/ContactAggregate.cs index 1b250d1..f107fb1 100644 --- a/tests/Logitar.EventSourcing.UnitTests/ContactAggregate.cs +++ b/tests/Logitar.EventSourcing.UnitTests/ContactAggregate.cs @@ -2,10 +2,6 @@ public class ContactAggregate : AggregateRoot { - public ContactAggregate(AggregateId id) : base(id) - { - } - public ContactAggregate(PersonAggregate person, ContactType type, string value) : base() { Raise(new ContactCreatedEvent(person.Id, type, value)); diff --git a/tests/Logitar.EventSourcing.UnitTests/ContactCreatedEvent.cs b/tests/Logitar.EventSourcing.UnitTests/ContactCreatedEvent.cs index 82c5784..dec8a4e 100644 --- a/tests/Logitar.EventSourcing.UnitTests/ContactCreatedEvent.cs +++ b/tests/Logitar.EventSourcing.UnitTests/ContactCreatedEvent.cs @@ -1,3 +1,15 @@ namespace Logitar.EventSourcing; -public record ContactCreatedEvent(AggregateId PersonId, ContactType Type, string Value) : DomainEvent; +public class ContactCreatedEvent : DomainEvent +{ + public AggregateId PersonId { get; } + public ContactType Type { get; } + public string Value { get; } + + public ContactCreatedEvent(AggregateId personId, ContactType contactType, string value) + { + PersonId = personId; + Type = contactType; + Value = value; + } +} diff --git a/tests/Logitar.EventSourcing.UnitTests/EventIdTests.cs b/tests/Logitar.EventSourcing.UnitTests/EventIdTests.cs new file mode 100644 index 0000000..115735b --- /dev/null +++ b/tests/Logitar.EventSourcing.UnitTests/EventIdTests.cs @@ -0,0 +1,139 @@ +using Bogus; + +namespace Logitar.EventSourcing; + +[Trait(Traits.Category, Categories.Unit)] +public class EventIdTests +{ + private readonly Faker _faker = new(); + + private readonly EventId _id = EventId.NewId(); + + [Theory(DisplayName = "Ctor: it constructs the correct Guid identifier.")] + [InlineData("00000000-0000-0000-0000-000000000000")] + [InlineData("2123679f-d2e7-4dea-8856-13d5cbb25c54")] + public void Ctor_it_constructs_the_correct_Guid_identifier(string value) + { + Guid guid = Guid.Parse(value); + EventId id = new(guid); + string idValue = Convert.ToBase64String(guid.ToByteArray()).ToUriSafeBase64(); + Assert.Equal(idValue, id.Value); + } + + [Theory(DisplayName = "Ctor: it constructs the correct string identifier.")] + [InlineData("123456")] + [InlineData(" 123456 ")] + public void Ctor_it_constructs_the_correct_string_identifier(string value) + { + EventId id = new(value); + Assert.Equal(value.Trim(), id.Value); + } + + [Theory(DisplayName = "Ctor: it throws ArgumentException when value is null or white space.")] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void Ctor_it_throws_ArgumentException_when_value_is_null_or_white_space(string? value) + { + var exception = Assert.Throws(() => new EventId(value!)); + Assert.Equal("value", exception.ParamName); + } + + [Theory(DisplayName = "Ctor: it throws ArgumentOutOfRangeException when value is too long.")] + [InlineData(1000)] + public void Ctor_it_throws_ArgumentOutOfRangeException_when_value_is_too_long(int length) + { + string value = _faker.Random.String(length, minChar: 'A', maxChar: 'Z'); + var exception = Assert.Throws(() => new EventId(value)); + Assert.Equal("value", exception.ParamName); + } + + [Fact(DisplayName = "EqualOperator: it returns false when they are different.")] + public void EqualOperator_it_returns_false_when_they_are_different() + { + EventId id = EventId.NewId(); + EventId other = new(id.Value[1..]); + Assert.False(id == other); + } + + [Fact(DisplayName = "EqualOperator: it returns true when they are equal.")] + public void EqualOperator_it_returns_true_when_they_are_equal() + { + EventId id = EventId.NewId(); + EventId other = new(id.ToGuid()); + Assert.True(id == other); + } + + [Fact(DisplayName = "Equals: it returns false when other is a different EventId.")] + public void Equals_it_returns_false_when_other_is_a_different_EventId() + { + EventId other = new(_id.Value[1..]); + Assert.False(_id.Equals(other)); + } + + [Fact(DisplayName = "Equals: it returns false when other is not an EventId.")] + public void Equals_it_returns_false_when_other_is_not_an_EventId() + { + Assert.False(_id.Equals(_id.Value)); + } + + [Fact(DisplayName = "Equals: it returns false when other is null.")] + public void Equals_it_returns_false_when_other_is_null() + { + Assert.False(_id.Equals(null)); + } + + [Fact(DisplayName = "Equals: it returns true when other is equal.")] + public void Equals_it_returns_false_when_other_is_equal() + { + EventId other = new(_id.Value); + Assert.True(_id.Equals(other)); + } + + [Fact(DisplayName = "GetHashCode: it returns the correct hash code.")] + public void GetHashCode_it_returns_the_correct_hash_code() + { + Assert.Equal(_id.Value.GetHashCode(), _id.GetHashCode()); + } + + [Fact(DisplayName = "NewId: it is constructed using a Guid.")] + public void NewId_it_is_constructed_using_a_Guid() + { + _ = EventId.NewId().ToGuid(); + } + + [Fact(DisplayName = "NotEqualOperator: it returns false when they are equal.")] + public void NotEqualOperator_it_returns_false_when_they_are_equal() + { + EventId id = EventId.NewId(); + EventId other = new(id.ToGuid()); + Assert.False(id != other); + } + + [Fact(DisplayName = "NotEqualOperator: it returns true when they are different.")] + public void NotEqualOperator_it_returns_true_when_they_are_different() + { + EventId id = EventId.NewId(); + EventId other = new(id.Value[1..]); + Assert.True(id != other); + } + + [Fact(DisplayName = "ToGuid: it returns the correct Guid.")] + public void ToGuid_it_returns_the_correct_Guid() + { + Guid guid = new(Convert.FromBase64String(_id.Value.FromUriSafeBase64())); + Assert.Equal(guid, _id.ToGuid()); + } + + [Fact(DisplayName = "ToString: it returns the correct string.")] + public void ToString_it_returns_the_correct_string() + { + Assert.Equal(_id.Value, _id.ToString()); + } + + [Fact(DisplayName = "Value: it should never be null.")] + public void Value_it_should_never_be_null() + { + Assert.NotNull(new EventId().Value); + } +} diff --git a/tests/Logitar.EventSourcing.UnitTests/PersonAggregate.cs b/tests/Logitar.EventSourcing.UnitTests/PersonAggregate.cs index f741823..4db6a3a 100644 --- a/tests/Logitar.EventSourcing.UnitTests/PersonAggregate.cs +++ b/tests/Logitar.EventSourcing.UnitTests/PersonAggregate.cs @@ -2,6 +2,10 @@ public class PersonAggregate : AggregateRoot { + public PersonAggregate() : base() + { + } + public PersonAggregate(AggregateId id) : base(id) { } diff --git a/tests/Logitar.EventSourcing.UnitTests/PersonCreatedEvent.cs b/tests/Logitar.EventSourcing.UnitTests/PersonCreatedEvent.cs index 4f4da01..0546844 100644 --- a/tests/Logitar.EventSourcing.UnitTests/PersonCreatedEvent.cs +++ b/tests/Logitar.EventSourcing.UnitTests/PersonCreatedEvent.cs @@ -1,3 +1,11 @@ namespace Logitar.EventSourcing; -public record PersonCreatedEvent(string FullName) : DomainEvent; +public class PersonCreatedEvent : DomainEvent +{ + public string FullName { get; } + + public PersonCreatedEvent(string fullName) + { + FullName = fullName; + } +} diff --git a/tests/Logitar.EventSourcing.UnitTests/PersonDeletedChangedEvent.cs b/tests/Logitar.EventSourcing.UnitTests/PersonDeletedChangedEvent.cs index d224477..95bd19f 100644 --- a/tests/Logitar.EventSourcing.UnitTests/PersonDeletedChangedEvent.cs +++ b/tests/Logitar.EventSourcing.UnitTests/PersonDeletedChangedEvent.cs @@ -1,6 +1,6 @@ namespace Logitar.EventSourcing; -public record PersonDeletedChangedEvent : DomainEvent +public class PersonDeletedChangedEvent : DomainEvent { public PersonDeletedChangedEvent(bool? isDeleted) { diff --git a/tools/Logitar.EventSourcing.Database/DatabaseProvider.cs b/tools/Logitar.EventSourcing.Database/DatabaseProvider.cs new file mode 100644 index 0000000..dd8136a --- /dev/null +++ b/tools/Logitar.EventSourcing.Database/DatabaseProvider.cs @@ -0,0 +1,7 @@ +namespace Logitar.EventSourcing.Database; + +public enum DatabaseProvider +{ + EntityFrameworkCorePostgreSQL, + EntityFrameworkCoreSqlServer +} diff --git a/tools/Logitar.EventSourcing.Database/Dockerfile b/tools/Logitar.EventSourcing.Database/Dockerfile new file mode 100644 index 0000000..83ef60e --- /dev/null +++ b/tools/Logitar.EventSourcing.Database/Dockerfile @@ -0,0 +1,23 @@ +#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging. + +FROM mcr.microsoft.com/dotnet/runtime:8.0 AS base +USER app +WORKDIR /app + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["tools/Logitar.EventSourcing.Database/Logitar.EventSourcing.Database.csproj", "tools/Logitar.EventSourcing.Database/"] +RUN dotnet restore "./tools/Logitar.EventSourcing.Database/Logitar.EventSourcing.Database.csproj" +COPY . . +WORKDIR "/src/tools/Logitar.EventSourcing.Database" +RUN dotnet build "./Logitar.EventSourcing.Database.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./Logitar.EventSourcing.Database.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Logitar.EventSourcing.Database.dll"] \ No newline at end of file diff --git a/tools/Logitar.EventSourcing.Database/Logitar.EventSourcing.Database.csproj b/tools/Logitar.EventSourcing.Database/Logitar.EventSourcing.Database.csproj new file mode 100644 index 0000000..4854b3a --- /dev/null +++ b/tools/Logitar.EventSourcing.Database/Logitar.EventSourcing.Database.csproj @@ -0,0 +1,41 @@ + + + + net8.0 + enable + enable + dotnet-Logitar.EventSourcing.Database-e8a0757a-6351-4c7a-9d1c-5605c9249044 + Linux + ..\.. + + + + True + + + + True + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + diff --git a/tools/Logitar.EventSourcing.Database/Program.cs b/tools/Logitar.EventSourcing.Database/Program.cs new file mode 100644 index 0000000..e92a1a8 --- /dev/null +++ b/tools/Logitar.EventSourcing.Database/Program.cs @@ -0,0 +1,27 @@ +using Logitar.EventSourcing.EntityFrameworkCore.Relational; +using Microsoft.EntityFrameworkCore; + +namespace Logitar.EventSourcing.Database; + +internal class Program +{ + public static void Main(string[] args) + { + HostApplicationBuilder builder = Host.CreateApplicationBuilder(args); + builder.Services.AddHostedService(); + + DatabaseProvider? databaseProvider = builder.Configuration.GetValue("DatabaseProvider"); + switch (databaseProvider) + { + case DatabaseProvider.EntityFrameworkCorePostgreSQL: + builder.Services.AddDbContext(builder => builder.UseNpgsql(b => b.MigrationsAssembly("Logitar.EventSourcing.EntityFrameworkCore.PostgreSQL"))); + break; + case DatabaseProvider.EntityFrameworkCoreSqlServer: + builder.Services.AddDbContext(builder => builder.UseSqlServer(b => b.MigrationsAssembly("Logitar.EventSourcing.EntityFrameworkCore.SqlServer"))); + break; + } + + IHost host = builder.Build(); + host.Run(); + } +} diff --git a/tools/Logitar.EventSourcing.Database/Properties/launchSettings.json b/tools/Logitar.EventSourcing.Database/Properties/launchSettings.json new file mode 100644 index 0000000..3ac7629 --- /dev/null +++ b/tools/Logitar.EventSourcing.Database/Properties/launchSettings.json @@ -0,0 +1,15 @@ +{ + "profiles": { + "Logitar.EventSourcing.Database": { + "commandName": "Project", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true + }, + "Container (Dockerfile)": { + "commandName": "Docker" + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json" +} \ No newline at end of file diff --git a/tools/Logitar.EventSourcing.Database/Worker.cs b/tools/Logitar.EventSourcing.Database/Worker.cs new file mode 100644 index 0000000..ec82d2a --- /dev/null +++ b/tools/Logitar.EventSourcing.Database/Worker.cs @@ -0,0 +1,20 @@ +namespace Logitar.EventSourcing.Database; + +public class Worker : BackgroundService +{ + private readonly ILogger _logger; + + public Worker(ILogger logger) + { + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now); + await Task.Delay(1000, cancellationToken); + } + } +} diff --git a/tools/Logitar.EventSourcing.Database/appsettings.Development.json b/tools/Logitar.EventSourcing.Database/appsettings.Development.json new file mode 100644 index 0000000..b2dcdb6 --- /dev/null +++ b/tools/Logitar.EventSourcing.Database/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/tools/Logitar.EventSourcing.Database/appsettings.json b/tools/Logitar.EventSourcing.Database/appsettings.json new file mode 100644 index 0000000..b2dcdb6 --- /dev/null +++ b/tools/Logitar.EventSourcing.Database/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/tools/Logitar.EventSourcing.Database/secrets.example.json b/tools/Logitar.EventSourcing.Database/secrets.example.json new file mode 100644 index 0000000..9ee30fa --- /dev/null +++ b/tools/Logitar.EventSourcing.Database/secrets.example.json @@ -0,0 +1,3 @@ +{ + "DatabaseProvider": "EntityFrameworkCoreSqlServer" +}