diff --git a/Documentation/integration/mssql.md b/Documentation/integration/mssql.md index 1c6f2b5b4..cc76744df 100644 --- a/Documentation/integration/mssql.md +++ b/Documentation/integration/mssql.md @@ -48,6 +48,21 @@ public void ConfigureServices(IServiceCollection services) `ConfigureMsSql` registers the `IMsSqlConfiguration` and the database migrator that is reused by the event, snapshot, and read model stores. You can fine-tune the configuration (timeouts, retry counts, schema names) via the fluent helpers on `MsSqlConfiguration`. +### Configure the schema name + +`MsSqlConfiguration` exposes a schema option: + +```csharp +var config = MsSqlConfiguration + .New + .SetConnectionString(connectionString) + .SetSchema(new Schema("eventflow")); +``` + +If you do not set a schema explicitly, EventFlow uses `dbo` by default. The value passed to `Schema` is validated to ensure it is a legal SQL Server schema identifier. + +When using a custom schema, create it before running EventFlow migrations. Event flow migration scripts create tables, indexes, and types inside the configured schema, but they do not create the schema itself. + ## Event store ### Enable the MSSQL event store @@ -70,7 +85,7 @@ var migrator = serviceProvider.GetRequiredService(); await EventFlowEventStoresMsSql.MigrateDatabaseAsync(migrator, cancellationToken); ``` -Run this during deployment or application startup. The migrator is idempotent, so reruns simply ensure the schema is present. If your SQL login does not have `CREATE TYPE` rights, grant them explicitly; otherwise batch appends will fail at runtime. +Run this during deployment or application startup. The migrator is idempotent, so reruns simply ensure required objects are present. If you configured a custom schema, ensure that schema already exists before migration execution. If your SQL login does not have `CREATE TYPE` rights, grant them explicitly; otherwise batch appends will fail at runtime. ### Recommended database settings diff --git a/Source/EventFlow.MsSql.Tests/EventFlow.MsSql.Tests.csproj b/Source/EventFlow.MsSql.Tests/EventFlow.MsSql.Tests.csproj index 61c64eff9..0dd83526a 100644 --- a/Source/EventFlow.MsSql.Tests/EventFlow.MsSql.Tests.csproj +++ b/Source/EventFlow.MsSql.Tests/EventFlow.MsSql.Tests.csproj @@ -1,6 +1,6 @@  - netcoreapp3.1;net6.0 + netcoreapp3.1;net6.0;net10.0 False diff --git a/Source/EventFlow.MsSql.Tests/IntegrationTests/EventStores/MsSqlEventStoreWithCustomSchemaTests.cs b/Source/EventFlow.MsSql.Tests/IntegrationTests/EventStores/MsSqlEventStoreWithCustomSchemaTests.cs new file mode 100644 index 000000000..10f46698f --- /dev/null +++ b/Source/EventFlow.MsSql.Tests/IntegrationTests/EventStores/MsSqlEventStoreWithCustomSchemaTests.cs @@ -0,0 +1,70 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2025 Rasmus Mikkelsen +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System; +using System.Threading; +using EventFlow.MsSql.EventStores; +using EventFlow.MsSql.Extensions; +using EventFlow.TestHelpers; +using EventFlow.TestHelpers.MsSql; +using EventFlow.TestHelpers.Suites; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; + +namespace EventFlow.MsSql.Tests.IntegrationTests.EventStores +{ + [Category(Categories.Integration)] + public class MsSqlEventStoreWithCustomSchemaTests : TestSuiteForEventStore + { + private const string CustomSchema = "eventflow"; + private IMsSqlDatabase _testDatabase; + + protected override IServiceProvider Configure(IEventFlowOptions eventFlowOptions) + { + _testDatabase = MsSqlHelpz.CreateDatabase("eventflow-custom-schema"); + _testDatabase.Execute($"CREATE SCHEMA [{CustomSchema}]"); + + eventFlowOptions + .ConfigureMsSql(MsSqlConfiguration.New + .SetConnectionString(_testDatabase.ConnectionString.Value) + .SetSchema(new Schema(CustomSchema))) + .UseMssqlEventStore(); + + var serviceProvider = base.Configure(eventFlowOptions); + + var databaseMigrator = serviceProvider.GetRequiredService(); + EventFlowEventStoresMsSql.MigrateDatabaseAsync(databaseMigrator, CancellationToken.None).Wait(); + databaseMigrator.MigrateDatabaseUsingEmbeddedScriptsAsync( + GetType().Assembly, + null, /* TODO */ + CancellationToken.None).Wait(); + + return serviceProvider; + } + + [TearDown] + public void TearDown() + { + _testDatabase.Dispose(); + } + } +} diff --git a/Source/EventFlow.MsSql.Tests/IntegrationTests/EventStores/MsSqlScriptsTests.cs b/Source/EventFlow.MsSql.Tests/IntegrationTests/EventStores/MsSqlScriptsTests.cs index 282832340..44e18fc96 100644 --- a/Source/EventFlow.MsSql.Tests/IntegrationTests/EventStores/MsSqlScriptsTests.cs +++ b/Source/EventFlow.MsSql.Tests/IntegrationTests/EventStores/MsSqlScriptsTests.cs @@ -45,7 +45,7 @@ public void SqlScriptsAreIdempotent() { foreach (var sqlScript in sqlScripts) { - _msSqlDatabase.Execute(sqlScript.Content); + _msSqlDatabase.Execute(sqlScript.Content.Replace("$MsSqlSchema$", "dbo")); } } } diff --git a/Source/EventFlow.MsSql.Tests/IntegrationTests/ReadStores/MsSqlReadModelStoreWithCustomSchemaTests.cs b/Source/EventFlow.MsSql.Tests/IntegrationTests/ReadStores/MsSqlReadModelStoreWithCustomSchemaTests.cs new file mode 100644 index 000000000..75939d98e --- /dev/null +++ b/Source/EventFlow.MsSql.Tests/IntegrationTests/ReadStores/MsSqlReadModelStoreWithCustomSchemaTests.cs @@ -0,0 +1,83 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2025 Rasmus Mikkelsen +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System; +using System.Threading; +using EventFlow.Extensions; +using EventFlow.MsSql.EventStores; +using EventFlow.MsSql.Extensions; +using EventFlow.MsSql.Tests.IntegrationTests.ReadStores.QueryHandlers; +using EventFlow.MsSql.Tests.IntegrationTests.ReadStores.ReadModels; +using EventFlow.TestHelpers; +using EventFlow.TestHelpers.Aggregates.Entities; +using EventFlow.TestHelpers.MsSql; +using EventFlow.TestHelpers.Suites; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; + +namespace EventFlow.MsSql.Tests.IntegrationTests.ReadStores +{ + [Category(Categories.Integration)] + public class MsSqlReadModelStoreWithCustomSchemaTests : TestSuiteForReadModelStore + { + private const string CustomSchema = "eventflow"; + + protected override Type ReadModelType { get; } = typeof(MsSqlThingyReadModel); + + private IMsSqlDatabase _testDatabase; + + protected override IServiceProvider Configure(IEventFlowOptions eventFlowOptions) + { + _testDatabase = MsSqlHelpz.CreateDatabase("eventflow-readmodels-custom-schema"); + _testDatabase.Execute($"CREATE SCHEMA [{CustomSchema}]"); + + eventFlowOptions + .RegisterServices(sr => sr.AddTransient(typeof(ThingyMessageLocator))) + .ConfigureMsSql(MsSqlConfiguration.New + .SetConnectionString(_testDatabase.ConnectionString.Value) + .SetSchema(new Schema(CustomSchema))) + .UseMssqlReadModel() + .UseMssqlReadModel() + .AddQueryHandlers( + typeof(MsSqlThingyGetQueryHandler), + typeof(MsSqlThingyGetVersionQueryHandler), + typeof(MsSqlThingyGetMessagesQueryHandler)); + + var serviceProvider = base.Configure(eventFlowOptions); + + var databaseMigrator = serviceProvider.GetRequiredService(); + EventFlowEventStoresMsSql.MigrateDatabaseAsync(databaseMigrator, CancellationToken.None).Wait(); + databaseMigrator.MigrateDatabaseUsingEmbeddedScriptsAsync( + GetType().Assembly, + null /* TODO */, + CancellationToken.None).Wait(); + + return serviceProvider; + } + + [TearDown] + public void TearDown() + { + _testDatabase.DisposeSafe(Logger, "Failed to delete database"); + } + } +} \ No newline at end of file diff --git a/Source/EventFlow.MsSql.Tests/IntegrationTests/ReadStores/QueryHandlers/MsSqlThingyGetMessagesQueryHandler.cs b/Source/EventFlow.MsSql.Tests/IntegrationTests/ReadStores/QueryHandlers/MsSqlThingyGetMessagesQueryHandler.cs index 65b246aae..2ef2c42be 100644 --- a/Source/EventFlow.MsSql.Tests/IntegrationTests/ReadStores/QueryHandlers/MsSqlThingyGetMessagesQueryHandler.cs +++ b/Source/EventFlow.MsSql.Tests/IntegrationTests/ReadStores/QueryHandlers/MsSqlThingyGetMessagesQueryHandler.cs @@ -36,11 +36,14 @@ namespace EventFlow.MsSql.Tests.IntegrationTests.ReadStores.QueryHandlers public class MsSqlThingyGetMessagesQueryHandler : IQueryHandler> { private readonly IMsSqlConnection _msSqlConnection; + private readonly IMsSqlConfiguration _configuration; public MsSqlThingyGetMessagesQueryHandler( - IMsSqlConnection msSqlConnection) + IMsSqlConnection msSqlConnection, + IMsSqlConfiguration configuration) { _msSqlConnection = msSqlConnection; + _configuration = configuration; } public async Task> ExecuteQueryAsync(ThingyGetMessagesQuery query, CancellationToken cancellationToken) @@ -49,7 +52,7 @@ public async Task> ExecuteQueryAsync(ThingyGe Label.Named("mssql-fetch-thingy-message-read-model"), ReadModelExtensions.GetConnectionStringName(), cancellationToken, - "SELECT * FROM [ReadModel-ThingyMessage] WHERE ThingyId = @ThingyId", + $"SELECT * FROM [{_configuration.Schema}].[ReadModel-ThingyMessage] WHERE ThingyId = @ThingyId", new { ThingyId = query.ThingyId.Value }) .ConfigureAwait(false); return readModels diff --git a/Source/EventFlow.MsSql.Tests/IntegrationTests/ReadStores/QueryHandlers/MsSqlThingyGetQueryHandler.cs b/Source/EventFlow.MsSql.Tests/IntegrationTests/ReadStores/QueryHandlers/MsSqlThingyGetQueryHandler.cs index 412c3cfe7..56487c92b 100644 --- a/Source/EventFlow.MsSql.Tests/IntegrationTests/ReadStores/QueryHandlers/MsSqlThingyGetQueryHandler.cs +++ b/Source/EventFlow.MsSql.Tests/IntegrationTests/ReadStores/QueryHandlers/MsSqlThingyGetQueryHandler.cs @@ -35,11 +35,14 @@ namespace EventFlow.MsSql.Tests.IntegrationTests.ReadStores.QueryHandlers public class MsSqlThingyGetQueryHandler : IQueryHandler { private readonly IMsSqlConnection _msSqlConnection; + private readonly IMsSqlConfiguration _configuration; public MsSqlThingyGetQueryHandler( - IMsSqlConnection msSqlConnection) + IMsSqlConnection msSqlConnection, + IMsSqlConfiguration configuration) { _msSqlConnection = msSqlConnection; + _configuration = configuration; } public async Task ExecuteQueryAsync(ThingyGetQuery query, CancellationToken cancellationToken) @@ -48,7 +51,7 @@ public async Task ExecuteQueryAsync(ThingyGetQuery query, CancellationTo Label.Named("mssql-fetch-test-read-model"), ReadModelExtensions.GetConnectionStringName(), cancellationToken, - "SELECT * FROM [ReadModel-ThingyAggregate] WHERE AggregateId = @AggregateId", + $"SELECT * FROM [{_configuration.Schema}].[ReadModel-ThingyAggregate] WHERE AggregateId = @AggregateId", new { AggregateId = query.ThingyId.Value }) .ConfigureAwait(false); return readModels.SingleOrDefault()?.ToThingy(); diff --git a/Source/EventFlow.MsSql.Tests/IntegrationTests/ReadStores/QueryHandlers/MsSqlThingyGetVersionQueryHandler.cs b/Source/EventFlow.MsSql.Tests/IntegrationTests/ReadStores/QueryHandlers/MsSqlThingyGetVersionQueryHandler.cs index 963277486..20f1593cc 100644 --- a/Source/EventFlow.MsSql.Tests/IntegrationTests/ReadStores/QueryHandlers/MsSqlThingyGetVersionQueryHandler.cs +++ b/Source/EventFlow.MsSql.Tests/IntegrationTests/ReadStores/QueryHandlers/MsSqlThingyGetVersionQueryHandler.cs @@ -34,11 +34,14 @@ namespace EventFlow.MsSql.Tests.IntegrationTests.ReadStores.QueryHandlers public class MsSqlThingyGetVersionQueryHandler : IQueryHandler { private readonly IMsSqlConnection _msSqlConnection; + private readonly IMsSqlConfiguration _configuration; public MsSqlThingyGetVersionQueryHandler( - IMsSqlConnection msSqlConnection) + IMsSqlConnection msSqlConnection, + IMsSqlConfiguration configuration) { _msSqlConnection = msSqlConnection; + _configuration = configuration; } public async Task ExecuteQueryAsync(ThingyGetVersionQuery query, CancellationToken cancellationToken) @@ -47,7 +50,7 @@ public MsSqlThingyGetVersionQueryHandler( Label.Named("mssql-fetch-test-read-model"), ReadModelExtensions.GetConnectionStringName(), cancellationToken, - "SELECT * FROM [ReadModel-ThingyAggregate] WHERE AggregateId = @AggregateId", + $"SELECT * FROM [{_configuration.Schema}].[ReadModel-ThingyAggregate] WHERE AggregateId = @AggregateId", new { AggregateId = query.ThingyId.Value }) .ConfigureAwait(false); return readModels.SingleOrDefault()?.LastAggregateSequenceNumber; diff --git a/Source/EventFlow.MsSql.Tests/IntegrationTests/ReadStores/Scripts/0001 - Create table ReadModel-ThingyAggregate.sql b/Source/EventFlow.MsSql.Tests/IntegrationTests/ReadStores/Scripts/0001 - Create table ReadModel-ThingyAggregate.sql index abfc9fd77..3b12f4c29 100644 --- a/Source/EventFlow.MsSql.Tests/IntegrationTests/ReadStores/Scripts/0001 - Create table ReadModel-ThingyAggregate.sql +++ b/Source/EventFlow.MsSql.Tests/IntegrationTests/ReadStores/Scripts/0001 - Create table ReadModel-ThingyAggregate.sql @@ -1,4 +1,4 @@ -CREATE TABLE [dbo].[ReadModel-ThingyAggregate]( +CREATE TABLE [$MsSqlSchema$].[ReadModel-ThingyAggregate]( [PingsReceived] [int] NOT NULL, [DomainErrorAfterFirstReceived] [bit] NOT NULL, @@ -14,7 +14,7 @@ ) ) -CREATE UNIQUE NONCLUSTERED INDEX [IX_ReadModel-ThingyAggregate_AggregateId] ON [dbo].[ReadModel-ThingyAggregate] +CREATE UNIQUE NONCLUSTERED INDEX [IX_ReadModel-ThingyAggregate_AggregateId] ON [$MsSqlSchema$].[ReadModel-ThingyAggregate] ( [AggregateId] ASC ) diff --git a/Source/EventFlow.MsSql.Tests/IntegrationTests/ReadStores/Scripts/0002 - Create table ReadModel-ThingyMessage.sql b/Source/EventFlow.MsSql.Tests/IntegrationTests/ReadStores/Scripts/0002 - Create table ReadModel-ThingyMessage.sql index 8cb0cb627..0fd55df44 100644 --- a/Source/EventFlow.MsSql.Tests/IntegrationTests/ReadStores/Scripts/0002 - Create table ReadModel-ThingyMessage.sql +++ b/Source/EventFlow.MsSql.Tests/IntegrationTests/ReadStores/Scripts/0002 - Create table ReadModel-ThingyMessage.sql @@ -1,4 +1,4 @@ -CREATE TABLE [dbo].[ReadModel-ThingyMessage]( +CREATE TABLE [$MsSqlSchema$].[ReadModel-ThingyMessage]( [Id] [bigint] IDENTITY(1,1) NOT NULL, [ThingyId] [nvarchar](64) NOT NULL, [MessageId] [nvarchar](64) NOT NULL, @@ -9,12 +9,12 @@ ) ) -CREATE UNIQUE NONCLUSTERED INDEX [IX_ReadModel-ThingyMessage_AggregateId] ON [dbo].[ReadModel-ThingyMessage] +CREATE UNIQUE NONCLUSTERED INDEX [IX_ReadModel-ThingyMessage_AggregateId] ON [$MsSqlSchema$].[ReadModel-ThingyMessage] ( [MessageId] ASC ) -CREATE NONCLUSTERED INDEX [IX_ReadModel-ThingyMessage_ThingyId] ON [dbo].[ReadModel-ThingyMessage] +CREATE NONCLUSTERED INDEX [IX_ReadModel-ThingyMessage_ThingyId] ON [$MsSqlSchema$].[ReadModel-ThingyMessage] ( [ThingyId] ASC ) diff --git a/Source/EventFlow.MsSql.Tests/IntegrationTests/SnapshotStores/MsSqlSnapshotStoreWithCustomSchemaTests.cs b/Source/EventFlow.MsSql.Tests/IntegrationTests/SnapshotStores/MsSqlSnapshotStoreWithCustomSchemaTests.cs new file mode 100644 index 000000000..b9ab7bcc2 --- /dev/null +++ b/Source/EventFlow.MsSql.Tests/IntegrationTests/SnapshotStores/MsSqlSnapshotStoreWithCustomSchemaTests.cs @@ -0,0 +1,70 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2025 Rasmus Mikkelsen +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System; +using System.Threading; +using EventFlow.Extensions; +using EventFlow.MsSql.Extensions; +using EventFlow.MsSql.SnapshotStores; +using EventFlow.TestHelpers; +using EventFlow.TestHelpers.MsSql; +using EventFlow.TestHelpers.Suites; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; + +namespace EventFlow.MsSql.Tests.IntegrationTests.SnapshotStores +{ + [Category(Categories.Integration)] + public class MsSqlSnapshotStoreWithCustomSchemaTests : TestSuiteForSnapshotStore + { + private const string CustomSchema = "eventflow"; + + private IMsSqlDatabase _testDatabase; + + protected override IServiceProvider Configure(IEventFlowOptions eventFlowOptions) + { + _testDatabase = MsSqlHelpz.CreateDatabase("eventflow-snapshots-custom-schema"); + _testDatabase.Execute($"CREATE SCHEMA [{CustomSchema}]"); + + eventFlowOptions + .ConfigureMsSql(MsSqlConfiguration.New + .SetConnectionString(_testDatabase.ConnectionString.Value) + .SetSchema(new Schema(CustomSchema))) + .UseMsSqlSnapshotStore(); + + var serviceProvider = base.Configure(eventFlowOptions); + + var databaseMigrator = serviceProvider.GetRequiredService(); + EventFlowSnapshotStoresMsSql.MigrateDatabaseAsync( + databaseMigrator, + CancellationToken.None).Wait(); + + return serviceProvider; + } + + [TearDown] + public void TearDown() + { + _testDatabase.DisposeSafe(Logger, "Failed to delete database"); + } + } +} diff --git a/Source/EventFlow.MsSql.Tests/UnitTests/MsSqlConfigurationTests.cs b/Source/EventFlow.MsSql.Tests/UnitTests/MsSqlConfigurationTests.cs new file mode 100644 index 000000000..125dd53f2 --- /dev/null +++ b/Source/EventFlow.MsSql.Tests/UnitTests/MsSqlConfigurationTests.cs @@ -0,0 +1,50 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2025 Rasmus Mikkelsen +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using EventFlow.TestHelpers; +using NUnit.Framework; + +namespace EventFlow.MsSql.Tests.UnitTests +{ + [Category(Categories.Unit)] + public class MsSqlConfigurationTests + { + [Test] + public void NewUsesDboSchemaByDefault() + { + var configuration = MsSqlConfiguration.New; + + Assert.That(configuration.Schema.Value, Is.EqualTo("dbo")); + } + + [Test] + public void SetSchemaUpdatesSchemaAndReturnsSameInstance() + { + var configuration = MsSqlConfiguration.New; + + var updatedConfiguration = configuration.SetSchema(new Schema("eventflow")); + + Assert.That(updatedConfiguration.Schema.Value, Is.EqualTo("eventflow")); + Assert.That(updatedConfiguration, Is.SameAs(configuration)); + } + } +} \ No newline at end of file diff --git a/Source/EventFlow.MsSql.Tests/UnitTests/SchemaTests.cs b/Source/EventFlow.MsSql.Tests/UnitTests/SchemaTests.cs new file mode 100644 index 000000000..71862c98e --- /dev/null +++ b/Source/EventFlow.MsSql.Tests/UnitTests/SchemaTests.cs @@ -0,0 +1,65 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2025 Rasmus Mikkelsen +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System; +using EventFlow.TestHelpers; +using NUnit.Framework; + +namespace EventFlow.MsSql.Tests.UnitTests +{ + [Category(Categories.Unit)] + public class SchemaTests + { + [TestCase("dbo")] + [TestCase("eventflow")] + [TestCase("_eventflow")] + [TestCase("schema1")] + [TestCase("a@b$c#d_e")] + public void ValidSchemaNameDoesNotThrow(string value) + { + Assert.DoesNotThrow(() => new Schema(value)); + } + + [Test] + public void MaximumLengthSchemaNameDoesNotThrow() + { + var value = new string('a', 128); + Assert.DoesNotThrow(() => new Schema(value)); + } + + [TestCase("")] + [TestCase("1eventflow")] + [TestCase("event-flow")] + [TestCase("event flow")] + public void InvalidSchemaNameThrowsArgumentException(string value) + { + Assert.Throws(() => new Schema(value)); + } + + [Test] + public void SchemaNameLongerThanMaximumLengthThrowsArgumentException() + { + var value = new string('a', 129); + Assert.Throws(() => new Schema(value)); + } + } +} \ No newline at end of file diff --git a/Source/EventFlow.MsSql/EventStores/MsSqlEventPersistence.cs b/Source/EventFlow.MsSql/EventStores/MsSqlEventPersistence.cs index f4e4e9221..d1b328053 100644 --- a/Source/EventFlow.MsSql/EventStores/MsSqlEventPersistence.cs +++ b/Source/EventFlow.MsSql/EventStores/MsSqlEventPersistence.cs @@ -49,13 +49,16 @@ public class EventDataModel : ICommittedDomainEvent private readonly ILogger _logger; private readonly IMsSqlConnection _connection; + private readonly IMsSqlConfiguration _configuration; public MsSqlEventPersistence( ILogger logger, - IMsSqlConnection connection) + IMsSqlConnection connection, + IMsSqlConfiguration configuration) { _logger = logger; _connection = connection; + _configuration = configuration; } public async Task LoadAllCommittedEvents( @@ -67,10 +70,10 @@ public async Task LoadAllCommittedEvents( ? 0 : long.Parse(globalPosition.Value); - const string sql = @" + var sql = $@" SELECT TOP(@pageSize) GlobalSequenceNumber, BatchId, AggregateId, AggregateName, Data, Metadata, AggregateSequenceNumber - FROM EventFlow + FROM [{_configuration.Schema}].EventFlow WHERE GlobalSequenceNumber >= @startPosition ORDER BY @@ -121,9 +124,9 @@ public async Task> CommitEventsAsync( eventDataModels.Count, id); - const string sql = @" + var sql = $@" INSERT INTO - EventFlow + [{_configuration.Schema}].EventFlow (BatchId, AggregateId, AggregateName, Data, Metadata, AggregateSequenceNumber) OUTPUT CAST(INSERTED.GlobalSequenceNumber as bigint) SELECT @@ -171,10 +174,10 @@ public async Task> LoadCommittedEvent int fromEventSequenceNumber, CancellationToken cancellationToken) { - const string sql = @" + var sql = $@" SELECT GlobalSequenceNumber, BatchId, AggregateId, AggregateName, Data, Metadata, AggregateSequenceNumber - FROM EventFlow + FROM [{_configuration.Schema}].EventFlow WHERE AggregateId = @AggregateId AND AggregateSequenceNumber >= @FromEventSequenceNumber @@ -200,10 +203,10 @@ public async Task> LoadCommittedEvent int toEventSequenceNumber, CancellationToken cancellationToken) { - const string sql = @" + var sql = $@" SELECT GlobalSequenceNumber, BatchId, AggregateId, AggregateName, Data, Metadata, AggregateSequenceNumber - FROM EventFlow + FROM [{_configuration.Schema}].EventFlow WHERE AggregateId = @AggregateId AND AggregateSequenceNumber >= @FromEventSequenceNumber AND @@ -227,7 +230,7 @@ ORDER BY public async Task DeleteEventsAsync(IIdentity id, CancellationToken cancellationToken) { - const string sql = @"DELETE FROM EventFlow WHERE AggregateId = @AggregateId"; + var sql = $@"DELETE FROM [{_configuration.Schema}].EventFlow WHERE AggregateId = @AggregateId"; var affectedRows = await _connection.ExecuteAsync( Label.Named("mssql-delete-aggregate"), null, diff --git a/Source/EventFlow.MsSql/EventStores/Scripts/0001 - Create table EventFlow.sql b/Source/EventFlow.MsSql/EventStores/Scripts/0001 - Create table EventFlow.sql index 3de13bdc2..63737fc7e 100644 --- a/Source/EventFlow.MsSql/EventStores/Scripts/0001 - Create table EventFlow.sql +++ b/Source/EventFlow.MsSql/EventStores/Scripts/0001 - Create table EventFlow.sql @@ -1,6 +1,6 @@ -IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE table_schema = N'dbo' AND table_name = N'EventFlow') +IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE table_schema = N'$MsSqlSchema$' AND table_name = N'EventFlow') BEGIN - CREATE TABLE [dbo].[EventFlow]( + CREATE TABLE [$MsSqlSchema$].[EventFlow]( [GlobalSequenceNumber] [bigint] IDENTITY(1,1) NOT NULL, [BatchId] [uniqueidentifier] NOT NULL, [AggregateId] [nvarchar](255) NOT NULL, @@ -14,7 +14,7 @@ BEGIN ) ) - CREATE UNIQUE NONCLUSTERED INDEX [IX_EventFlow_AggregateId_AggregateSequenceNumber] ON [dbo].[EventFlow] + CREATE UNIQUE NONCLUSTERED INDEX [IX_EventFlow_AggregateId_AggregateSequenceNumber] ON [$MsSqlSchema$].[EventFlow] ( [AggregateId] ASC, [AggregateSequenceNumber] ASC diff --git a/Source/EventFlow.MsSql/EventStores/Scripts/0002 - Create eventdatamodel_list_type.sql b/Source/EventFlow.MsSql/EventStores/Scripts/0002 - Create eventdatamodel_list_type.sql index 5835a1229..08f6e6c45 100644 --- a/Source/EventFlow.MsSql/EventStores/Scripts/0002 - Create eventdatamodel_list_type.sql +++ b/Source/EventFlow.MsSql/EventStores/Scripts/0002 - Create eventdatamodel_list_type.sql @@ -1,6 +1,12 @@ -IF NOT EXISTS (SELECT * FROM SYS.TYPES WHERE is_table_type = 1 AND name = 'eventdatamodel_list_type') +IF NOT EXISTS ( + SELECT * + FROM sys.table_types table_types + INNER JOIN sys.schemas schemas ON table_types.schema_id = schemas.schema_id + WHERE + table_types.name = N'eventdatamodel_list_type' AND + schemas.name = N'$MsSqlSchema$') BEGIN - CREATE TYPE dbo.eventdatamodel_list_type AS TABLE + CREATE TYPE [$MsSqlSchema$].eventdatamodel_list_type AS TABLE ( [AggregateId] [nvarchar](255) NOT NULL, [AggregateName] [nvarchar](255) NOT NULL, diff --git a/Source/EventFlow.MsSql/Extensions/EventFlowOptionsMsSqlReadStoreExtensions.cs b/Source/EventFlow.MsSql/Extensions/EventFlowOptionsMsSqlReadStoreExtensions.cs index 7267900db..b609ed9ed 100644 --- a/Source/EventFlow.MsSql/Extensions/EventFlowOptionsMsSqlReadStoreExtensions.cs +++ b/Source/EventFlow.MsSql/Extensions/EventFlowOptionsMsSqlReadStoreExtensions.cs @@ -69,7 +69,7 @@ private static void RegisterMssqlReadStore( IServiceCollection serviceCollection) where TReadModel : class, IReadModel { - serviceCollection.TryAddSingleton(); + serviceCollection.TryAddSingleton(); serviceCollection.TryAddTransient, MssqlReadModelStore>(); serviceCollection.TryAddTransient>(p => p.GetRequiredService>()); } diff --git a/Source/EventFlow.MsSql/IMsSqlConfiguration.cs b/Source/EventFlow.MsSql/IMsSqlConfiguration.cs index 801a2e1ef..913e32e00 100644 --- a/Source/EventFlow.MsSql/IMsSqlConfiguration.cs +++ b/Source/EventFlow.MsSql/IMsSqlConfiguration.cs @@ -27,8 +27,10 @@ namespace EventFlow.MsSql { public interface IMsSqlConfiguration : ISqlConfiguration { + Schema Schema { get; } RetryDelay ServerBusyRetryDelay { get; } + IMsSqlConfiguration SetSchema(Schema schema); IMsSqlConfiguration SetServerBusyRetryDelay(RetryDelay retryDelay); } } \ No newline at end of file diff --git a/Source/EventFlow.MsSql/Integrations/TableParameter.cs b/Source/EventFlow.MsSql/Integrations/TableParameter.cs index a54648198..cb54517e5 100644 --- a/Source/EventFlow.MsSql/Integrations/TableParameter.cs +++ b/Source/EventFlow.MsSql/Integrations/TableParameter.cs @@ -36,6 +36,7 @@ internal class TableParameter : SqlMapper.IDynamicParameters where TRow : class { private readonly string _name; + private readonly string _schema; private readonly IEnumerable _rows; private readonly SqlMapper.IDynamicParameters _otherParameters; @@ -75,14 +76,15 @@ static TableParameter() .ToArray(); } - public TableParameter(string name, IEnumerable rows, object otherParameters) - : this(name, rows, new DynamicParameters(otherParameters)) + public TableParameter(string name, string schema, IEnumerable rows, object otherParameters) + : this(name, schema, rows, new DynamicParameters(otherParameters)) { } - public TableParameter(string name, IEnumerable rows, SqlMapper.IDynamicParameters otherParameters) + public TableParameter(string name, string schema, IEnumerable rows, SqlMapper.IDynamicParameters otherParameters) { _name = name; + _schema = schema; _rows = rows; _otherParameters = otherParameters; } @@ -100,12 +102,12 @@ public void AddParameters(IDbCommand command, SqlMapper.Identity identity) _otherParameters.AddParameters(command, identity); } - private static SqlParameter CreateSqlParameter(string name, IDbCommand command, List sqlDataRecords) + private SqlParameter CreateSqlParameter(string name, IDbCommand command, List sqlDataRecords) { var sqlParameter = (SqlParameter)command.CreateParameter(); sqlParameter.SqlDbType = SqlDbType.Structured; sqlParameter.ParameterName = name; - sqlParameter.TypeName = $"{typeof(TRow).Name.ToLowerInvariant()}_list_type"; + sqlParameter.TypeName = $"{_schema}.{typeof(TRow).Name.ToLowerInvariant()}_list_type"; sqlParameter.Value = sqlDataRecords; return sqlParameter; } diff --git a/Source/EventFlow.MsSql/MsSqlConfiguration.cs b/Source/EventFlow.MsSql/MsSqlConfiguration.cs index ff65cf61c..b775650cf 100644 --- a/Source/EventFlow.MsSql/MsSqlConfiguration.cs +++ b/Source/EventFlow.MsSql/MsSqlConfiguration.cs @@ -34,11 +34,20 @@ private MsSqlConfiguration() { } + public Schema Schema { get; private set; } = new Schema("dbo"); + // From official documentation on MSDN: "The service is currently busy. Retry the request after 10 seconds" public RetryDelay ServerBusyRetryDelay { get; private set; } = RetryDelay.Between( TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(15)); + public IMsSqlConfiguration SetSchema(Schema schema) + { + Schema = schema; + + return this; + } + public IMsSqlConfiguration SetServerBusyRetryDelay(RetryDelay retryDelay) { ServerBusyRetryDelay = retryDelay; diff --git a/Source/EventFlow.MsSql/MsSqlConnection.cs b/Source/EventFlow.MsSql/MsSqlConnection.cs index 218e3fc3e..783f2f293 100644 --- a/Source/EventFlow.MsSql/MsSqlConnection.cs +++ b/Source/EventFlow.MsSql/MsSqlConnection.cs @@ -53,7 +53,7 @@ public override Task> InsertMultipleAsync("@rows", rows, new {}); + var tableParameter = new TableParameter("@rows", Configuration.Schema.Value, rows, new {}); return QueryAsync(label, connectionStringName, cancellationToken, sql, tableParameter); } } diff --git a/Source/EventFlow.MsSql/MsSqlDatabaseMigrator.cs b/Source/EventFlow.MsSql/MsSqlDatabaseMigrator.cs index e78f8eedb..4911e635b 100644 --- a/Source/EventFlow.MsSql/MsSqlDatabaseMigrator.cs +++ b/Source/EventFlow.MsSql/MsSqlDatabaseMigrator.cs @@ -37,7 +37,9 @@ public MsSqlDatabaseMigrator( protected override UpgradeEngineBuilder For(SupportedDatabases supportedDatabases, string connectionString) { - return supportedDatabases.SqlDatabase(connectionString); + return supportedDatabases.SqlDatabase(connectionString) + .WithVariable("MsSqlSchema", Configuration.Schema.Value) + .JournalToSqlTable(Configuration.Schema.Value, "SchemaVersions"); } } } diff --git a/Source/EventFlow.MsSql/ReadStores/MssqlReadModelSqlGenerator.cs b/Source/EventFlow.MsSql/ReadStores/MssqlReadModelSqlGenerator.cs new file mode 100644 index 000000000..dc2136d51 --- /dev/null +++ b/Source/EventFlow.MsSql/ReadStores/MssqlReadModelSqlGenerator.cs @@ -0,0 +1,63 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2025 Rasmus Mikkelsen +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Concurrent; +using System.ComponentModel.DataAnnotations.Schema; +using System.Reflection; +using EventFlow.Sql.ReadModels; + +namespace EventFlow.MsSql.ReadStores +{ + public class MssqlReadModelSqlGenerator : ReadModelSqlGenerator + { + private readonly IMsSqlConfiguration _configuration; + private readonly ConcurrentDictionary TableNames = new ConcurrentDictionary(); + + public MssqlReadModelSqlGenerator(IMsSqlConfiguration configuration) + { + _configuration = configuration; + } + + protected override string GetTableName(Type readModelType) + { + return TableNames.GetOrAdd( + readModelType, + t => + { + var qip = Configuration.TableQuotedIdentifierPrefix; + var qis = Configuration.TableQuotedIdentifierSuffix; + + var tableAttribute = t.GetTypeInfo().GetCustomAttribute(false); + var table = string.IsNullOrEmpty(tableAttribute?.Name) + ? $"ReadModel-{t.Name.Replace("ReadModel", string.Empty)}" + : tableAttribute.Name; + + var schema = string.IsNullOrEmpty(tableAttribute?.Schema) + ? _configuration.Schema.Value + : tableAttribute.Schema; + + return $"{qip}{schema}{qis}.{qip}{table}{qis}"; + }); + } + } +} \ No newline at end of file diff --git a/Source/EventFlow.MsSql/Schema.cs b/Source/EventFlow.MsSql/Schema.cs new file mode 100644 index 000000000..66d09c73f --- /dev/null +++ b/Source/EventFlow.MsSql/Schema.cs @@ -0,0 +1,50 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2025 Rasmus Mikkelsen +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System; +using System.Text.RegularExpressions; +using EventFlow.ValueObjects; + +namespace EventFlow.MsSql +{ + /// + /// Represents an MSSQL Server schema name. + /// + public class Schema : SingleValueObject + { + /// + /// Creates an MSSQL Server schema name value object. + /// + /// + /// Schema name that starts with a letter or underscore, followed by up to 127 letters, + /// digits, or one of '@', '$', '#', '_'. + /// + public Schema(string value) : base(value) + { + var regex = @"^[\p{L}_][\p{L}\p{N}@$#_]{0,127}$"; + if (!Regex.IsMatch(value, regex)) + { + throw new ArgumentException("Invalid MSSQL schema name provided", nameof(value)); + } + } + } +} \ No newline at end of file diff --git a/Source/EventFlow.MsSql/SnapshotStores/MsSqlSnapshotPersistence.cs b/Source/EventFlow.MsSql/SnapshotStores/MsSqlSnapshotPersistence.cs index b0d5b4232..177151a01 100644 --- a/Source/EventFlow.MsSql/SnapshotStores/MsSqlSnapshotPersistence.cs +++ b/Source/EventFlow.MsSql/SnapshotStores/MsSqlSnapshotPersistence.cs @@ -35,11 +35,14 @@ namespace EventFlow.MsSql.SnapshotStores public class MsSqlSnapshotPersistence : ISnapshotPersistence { private readonly IMsSqlConnection _msSqlConnection; + private readonly IMsSqlConfiguration _configuration; public MsSqlSnapshotPersistence( - IMsSqlConnection msSqlConnection) + IMsSqlConnection msSqlConnection, + IMsSqlConfiguration configuration) { _msSqlConnection = msSqlConnection; + _configuration = configuration; } public async Task GetSnapshotAsync( @@ -51,7 +54,7 @@ public async Task GetSnapshotAsync( Label.Named("fetch-snapshot"), null, cancellationToken, - "SELECT TOP 1 * FROM [dbo].[EventFlowSnapshots] WHERE AggregateName = @AggregateName AND AggregateId = @AggregateId ORDER BY AggregateSequenceNumber DESC", + $"SELECT TOP 1 * FROM [{_configuration.Schema}].[EventFlowSnapshots] WHERE AggregateName = @AggregateName AND AggregateId = @AggregateId ORDER BY AggregateSequenceNumber DESC", new { AggregateId = identity.Value, @@ -91,7 +94,7 @@ await _msSqlConnection.ExecuteAsync( Label.Named("set-snapshot"), null, cancellationToken, - @"INSERT INTO [dbo].[EventFlowSnapshots] + $@"INSERT INTO [{_configuration.Schema}].[EventFlowSnapshots] (AggregateId, AggregateName, AggregateSequenceNumber, Metadata, Data) VALUES (@AggregateId, @AggregateName, @AggregateSequenceNumber, @Metadata, @Data)", @@ -113,7 +116,7 @@ public Task DeleteSnapshotAsync( Label.Named("delete-snapshots-for-aggregate"), null, cancellationToken, - "DELETE FROM [dbo].[EventFlowSnapshots] WHERE AggregateName = @AggregateName AND AggregateId = @AggregateId", + $"DELETE FROM [{_configuration.Schema}].[EventFlowSnapshots] WHERE AggregateName = @AggregateName AND AggregateId = @AggregateId", new { AggregateId = identity.Value, @@ -129,7 +132,7 @@ public Task PurgeSnapshotsAsync( Label.Named("purge-snapshots-for-aggregate"), null, cancellationToken, - "DELETE FROM [dbo].[EventFlowSnapshots] WHERE AggregateName = @AggregateName", + $"DELETE FROM [{_configuration.Schema}].[EventFlowSnapshots] WHERE AggregateName = @AggregateName", new { AggregateName = aggregateType.GetAggregateName().Value @@ -143,7 +146,7 @@ public Task PurgeSnapshotsAsync( Label.Named("purge-all-snapshots"), null, cancellationToken, - "DELETE FROM [dbo].[EventFlowSnapshots]"); + $"DELETE FROM [{_configuration.Schema}].[EventFlowSnapshots]"); } public class MsSqlSnapshotDataModel diff --git a/Source/EventFlow.MsSql/SnapshotStores/Scripts/0001 - Create EventFlowSnapshots.sql b/Source/EventFlow.MsSql/SnapshotStores/Scripts/0001 - Create EventFlowSnapshots.sql index fa2fc9136..4219dae03 100644 --- a/Source/EventFlow.MsSql/SnapshotStores/Scripts/0001 - Create EventFlowSnapshots.sql +++ b/Source/EventFlow.MsSql/SnapshotStores/Scripts/0001 - Create EventFlowSnapshots.sql @@ -1,6 +1,6 @@ -IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE table_schema = N'dbo' AND table_name = N'EventFlowSnapshots') +IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE table_schema = N'$MsSqlSchema$' AND table_name = N'EventFlowSnapshots') BEGIN - CREATE TABLE [dbo].[EventFlowSnapshots]( + CREATE TABLE [$MsSqlSchema$].[EventFlowSnapshots]( [Id] [bigint] IDENTITY(1,1) NOT NULL, [AggregateId] [nvarchar](128) NOT NULL, [AggregateName] [nvarchar](128) NOT NULL, @@ -13,7 +13,7 @@ BEGIN ) ) - CREATE UNIQUE NONCLUSTERED INDEX [IX_EventFlowSnapshots_AggregateId_AggregateSequenceNumber] ON [dbo].[EventFlowSnapshots] + CREATE UNIQUE NONCLUSTERED INDEX [IX_EventFlowSnapshots_AggregateId_AggregateSequenceNumber] ON [$MsSqlSchema$].[EventFlowSnapshots] ( [AggregateName] ASC, [AggregateId] ASC,