Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion Documentation/integration/mssql.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -70,7 +85,7 @@ var migrator = serviceProvider.GetRequiredService<IMsSqlDatabaseMigrator>();
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

Expand Down
2 changes: 1 addition & 1 deletion Source/EventFlow.MsSql.Tests/EventFlow.MsSql.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp3.1;net6.0</TargetFrameworks>
<TargetFrameworks>netcoreapp3.1;net6.0;net10.0</TargetFrameworks>
<IsPackable>False</IsPackable>
</PropertyGroup>
<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<IMsSqlDatabaseMigrator>();
EventFlowEventStoresMsSql.MigrateDatabaseAsync(databaseMigrator, CancellationToken.None).Wait();
databaseMigrator.MigrateDatabaseUsingEmbeddedScriptsAsync(
GetType().Assembly,
null, /* TODO */
CancellationToken.None).Wait();

return serviceProvider;
}

[TearDown]
public void TearDown()
{
_testDatabase.Dispose();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public void SqlScriptsAreIdempotent()
{
foreach (var sqlScript in sqlScripts)
{
_msSqlDatabase.Execute(sqlScript.Content);
_msSqlDatabase.Execute(sqlScript.Content.Replace("$MsSqlSchema$", "dbo"));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<MsSqlThingyReadModel>()
.UseMssqlReadModel<MsSqlThingyMessageReadModel, ThingyMessageLocator>()
.AddQueryHandlers(
typeof(MsSqlThingyGetQueryHandler),
typeof(MsSqlThingyGetVersionQueryHandler),
typeof(MsSqlThingyGetMessagesQueryHandler));

var serviceProvider = base.Configure(eventFlowOptions);

var databaseMigrator = serviceProvider.GetRequiredService<IMsSqlDatabaseMigrator>();
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");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,14 @@ namespace EventFlow.MsSql.Tests.IntegrationTests.ReadStores.QueryHandlers
public class MsSqlThingyGetMessagesQueryHandler : IQueryHandler<ThingyGetMessagesQuery, IReadOnlyCollection<ThingyMessage>>
{
private readonly IMsSqlConnection _msSqlConnection;
private readonly IMsSqlConfiguration _configuration;

public MsSqlThingyGetMessagesQueryHandler(
IMsSqlConnection msSqlConnection)
IMsSqlConnection msSqlConnection,
IMsSqlConfiguration configuration)
{
_msSqlConnection = msSqlConnection;
_configuration = configuration;
}

public async Task<IReadOnlyCollection<ThingyMessage>> ExecuteQueryAsync(ThingyGetMessagesQuery query, CancellationToken cancellationToken)
Expand All @@ -49,7 +52,7 @@ public async Task<IReadOnlyCollection<ThingyMessage>> ExecuteQueryAsync(ThingyGe
Label.Named("mssql-fetch-thingy-message-read-model"),
ReadModelExtensions.GetConnectionStringName<MsSqlThingyMessageReadModel>(),
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,14 @@ namespace EventFlow.MsSql.Tests.IntegrationTests.ReadStores.QueryHandlers
public class MsSqlThingyGetQueryHandler : IQueryHandler<ThingyGetQuery, Thingy>
{
private readonly IMsSqlConnection _msSqlConnection;
private readonly IMsSqlConfiguration _configuration;

public MsSqlThingyGetQueryHandler(
IMsSqlConnection msSqlConnection)
IMsSqlConnection msSqlConnection,
IMsSqlConfiguration configuration)
{
_msSqlConnection = msSqlConnection;
_configuration = configuration;
}

public async Task<Thingy> ExecuteQueryAsync(ThingyGetQuery query, CancellationToken cancellationToken)
Expand All @@ -48,7 +51,7 @@ public async Task<Thingy> ExecuteQueryAsync(ThingyGetQuery query, CancellationTo
Label.Named("mssql-fetch-test-read-model"),
ReadModelExtensions.GetConnectionStringName<MsSqlThingyReadModel>(),
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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,14 @@ namespace EventFlow.MsSql.Tests.IntegrationTests.ReadStores.QueryHandlers
public class MsSqlThingyGetVersionQueryHandler : IQueryHandler<ThingyGetVersionQuery, long?>
{
private readonly IMsSqlConnection _msSqlConnection;
private readonly IMsSqlConfiguration _configuration;

public MsSqlThingyGetVersionQueryHandler(
IMsSqlConnection msSqlConnection)
IMsSqlConnection msSqlConnection,
IMsSqlConfiguration configuration)
{
_msSqlConnection = msSqlConnection;
_configuration = configuration;
}

public async Task<long?> ExecuteQueryAsync(ThingyGetVersionQuery query, CancellationToken cancellationToken)
Expand All @@ -47,7 +50,7 @@ public MsSqlThingyGetVersionQueryHandler(
Label.Named("mssql-fetch-test-read-model"),
ReadModelExtensions.GetConnectionStringName<MsSqlThingyReadModel>(),
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;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
CREATE TABLE [dbo].[ReadModel-ThingyAggregate](
CREATE TABLE [$MsSqlSchema$].[ReadModel-ThingyAggregate](
[PingsReceived] [int] NOT NULL,
[DomainErrorAfterFirstReceived] [bit] NOT NULL,

Expand All @@ -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
)
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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
)
Original file line number Diff line number Diff line change
@@ -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<IMsSqlDatabaseMigrator>();
EventFlowSnapshotStoresMsSql.MigrateDatabaseAsync(
databaseMigrator,
CancellationToken.None).Wait();

return serviceProvider;
}

[TearDown]
public void TearDown()
{
_testDatabase.DisposeSafe(Logger, "Failed to delete database");
}
}
}
Loading
Loading