Skip to content

Commit

Permalink
Added tests and docs to NpgsqlDataSource configuration
Browse files Browse the repository at this point in the history
Bumped language version to 12
  • Loading branch information
oskardudycz committed Dec 14, 2023
1 parent a560188 commit da7f07a
Show file tree
Hide file tree
Showing 8 changed files with 192 additions and 34 deletions.
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<Project>
<PropertyGroup>
<VersionPrefix>7.0.0-beta.2</VersionPrefix>
<LangVersion>11.0</LangVersion>
<LangVersion>12.0</LangVersion>
<Authors>Jeremy D. Miller;Babu Annamalai;Oskar Dudycz;Joona-Pekka Kokko</Authors>
<PackageIconUrl>https://martendb.io/logo.png</PackageIconUrl>
<PackageProjectUrl>https://martendb.io/</PackageProjectUrl>
Expand Down
49 changes: 43 additions & 6 deletions docs/configuration/hostbuilder.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@ builder.Services.AddMarten(options =>

The `AddMarten()` method will add these service registrations to your application:

1. `IDocumentStore` with a *Singleton* lifetime. The document store can be used to create sessions, query the configuration of Marten, generate schema migrations, and do bulk inserts.
1. `IDocumentSession` with a *Scoped* lifetime for all read and write operations. **By default**, this is done with the `IDocumentStore.OpenSession()` method and the session created will have the identity map behavior
1. `IDocumentStore` with a
*Singleton* lifetime. The document store can be used to create sessions, query the configuration of Marten, generate schema migrations, and do bulk inserts.
1. `IDocumentSession` with a *Scoped* lifetime for all read and write operations. **By default
**, this is done with the `IDocumentStore.OpenSession()` method and the session created will have the identity map behavior
1. `IQuerySession` with a *Scoped* lifetime for all read operations against the document store.

For more information, see:
Expand Down Expand Up @@ -116,17 +118,51 @@ services.AddMarten(options)
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/AspNetCoreWithMarten/Samples/ByStoreOptions/Startup.cs#L23-L37' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_addmartenbystoreoptions' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

The last option may be best for more complicated Marten configuration just to keep the configuration code cleaner as `Startup` classes can become convoluted.
## Using NpgsqlDataSource

Since version 7, you can also use the [NpgsqlDataSource](https://www.npgsql.org/doc/basic-usage.html#data-source) to configure Marten connection settings. From [Npgsql docs](https://www.npgsql.org/doc/basic-usage.html#data-source):

> The data source represents your PostgreSQL database and can hand out connections to it or support direct execution of SQL against it. The data source encapsulates the various Npgsql configuration needed to connect to PostgreSQL, as well the connection pooling which makes Npgsql efficient.
You can use the `AddNpgsqlDataSource` method from [Npgsql.DependencyInjection package](https://www.nuget.org/packages/Npgsql.DependencyInjection) to perform a setup by calling the `UseNpgsqlDataSourceMethod`:

<!-- snippet: sample_using_UseNpgsqlDataSource -->
<a id='snippet-sample_using_usenpgsqldatasource'></a>
```cs
services.AddNpgsqlDataSource(ConnectionSource.ConnectionString);

services.AddMarten()
.UseLightweightSessions()
.UseNpgsqlDataSource();
```
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/CoreTests/MartenServiceCollectionExtensionsTests.cs#L307-L315' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_usenpgsqldatasource' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

If you're on .NET 8 (and above), you can also use a dedicated [keyed registration](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8#keyed-di-services). This can be useful for scenarios where you need more than one data source registered:

<!-- snippet: sample_using_UseNpgsqlDataSource -->
<a id='snippet-sample_using_usenpgsqldatasource'></a>
```cs
services.AddNpgsqlDataSource(ConnectionSource.ConnectionString);

services.AddMarten()
.UseLightweightSessions()
.UseNpgsqlDataSource();
```
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/CoreTests/MartenServiceCollectionExtensionsTests.cs#L307-L315' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_usenpgsqldatasource' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

## Composite Configuration with ConfigureMarten()

The `AddMarten()` mechanism introduced in later versions of Marten v3 assumes that you are expressing all of the Marten configuration in one place and "know" what that configuration is upfront. Consider these possibilities where that isn't necessarily possible or desirable:
The `AddMarten()` mechanism assumes that you are expressing all of the Marten configuration in one place and "know" what that configuration is upfront. Consider these possibilities where that isn't necessarily possible or desirable:

1. You want to override Marten configuration in integration testing scenarios (I do this quite commonly)
2. Many users have expressed the desire to keep parts of Marten configuration in potentially separate assemblies or subsystems in such a way that they could later break up the current service into smaller services

Fear not, Marten V5.0 introduced a new way to add or modify the Marten configuration from `AddMarten()`. Let's assume
that we're building a system that has a subsystem related to *users* and want to segregate all the service registrations and Marten configuration related to *users* into a single place like this extension
that we're building a system that has a subsystem related to
*users* and want to segregate all the service registrations and Marten configuration related to
*users* into a single place like this extension
method:

<!-- snippet: sample_AddUserModule -->
Expand Down Expand Up @@ -477,7 +513,8 @@ The database management tools in Marten.CommandLine are able to work with the se
document stores along with the default store from `AddMarten()`.
:::

Marten V5.0 introduces a new feature to register additional Marten databases into a .Net system. `AddMarten()` continues to work as it has, but we can now register and resolve additional store services. To utilize the type system and your application's underlying IoC container, the first step is to create a custom *marker* interface for your separate document store like this one below targeting
Marten V5.0 introduces a new feature to register additional Marten databases into a .Net system. `AddMarten()` continues to work as it has, but we can now register and resolve additional store services. To utilize the type system and your application's underlying IoC container, the first step is to create a custom
*marker* interface for your separate document store like this one below targeting
a separate "invoicing" database:

<!-- snippet: sample_IInvoicingStore -->
Expand Down
2 changes: 1 addition & 1 deletion docs/configuration/storeoptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public static DocumentStore For(Action<StoreOptions> configure)
return new DocumentStore(options);
}
```
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/Marten/DocumentStore.cs#L517-L527' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_documentstore.for' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/Marten/DocumentStore.cs#L504-L514' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_documentstore.for' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

The major parts of `StoreOptions` are shown in the class diagram below:
Expand Down
4 changes: 2 additions & 2 deletions docs/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ Welcome to the Marten documentation! Join our friendly [Discord channel](https:/

**Marten is a .NET library for building applications using
a [document-based approach](https://en.wikipedia.org/wiki/Document-oriented_database)
and [Event Sourcing](https://martinfowler.com/eaaDev/EventSourcing.html).
** We're committed to removing boilerplate work and letting you focus on delivering business value.
and [Event Sourcing](https://martinfowler.com/eaaDev/EventSourcing.html).**
We're committed to removing boilerplate work and letting you focus on delivering business value.

Under the hood, we're using [Postgresql](https://www.postgresql.org/), changing it into:

Expand Down
2 changes: 2 additions & 0 deletions src/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ trim_trailing_whitespace = true
[*.md]
max_line_length = off
trim_trailing_whitespace = false
ij_markdown_wrap_text_if_long = false
ij_markdown_wrap_text_inside_blockquotes = false

###############################
# .NET Coding Conventions #
Expand Down
133 changes: 125 additions & 8 deletions src/CoreTests/MartenServiceCollectionExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -302,20 +302,137 @@ public void apply_configure_marten_options()
[Fact]
public async Task use_npgsql_data_source()
{
await using var container = Container.For(x =>
{
x.AddNpgsqlDataSource(ConnectionSource.ConnectionString);
var services = new ServiceCollection();

x.AddMarten()
.UseLightweightSessions()
.UseNpgsqlDataSource();
});
#region sample_using_UseNpgsqlDataSource

services.AddNpgsqlDataSource(ConnectionSource.ConnectionString);

services.AddMarten()
.UseLightweightSessions()
.UseNpgsqlDataSource();

#endregion

var serviceProvider = services.BuildServiceProvider();

await using var session = serviceProvider.GetService<IDocumentSession>();
Func<bool> Call(IDocumentSession s) => () => s.Query<Target>().Any();
Call(session).ShouldNotThrow();
}

#if NET8_0
[Fact]
public async Task use_npgsql_data_source_with_keyed_registration()
{
var services = new ServiceCollection();

#region sample_using_UseNpgsqlDataSource_keyed

const string dataSourceKey = "marten_data_source";

await using var session = container.GetInstance<IDocumentSession>();
services.AddNpgsqlDataSource(ConnectionSource.ConnectionString, serviceKey: dataSourceKey);

services.AddMarten()
.UseLightweightSessions()
.UseNpgsqlDataSource(dataSourceKey);

#endregion

var serviceProvider = services.BuildServiceProvider();

await using var session = serviceProvider.GetService<IDocumentSession>();
Func<bool> Call(IDocumentSession s) => () => s.Query<Target>().Any();
Call(session).ShouldNotThrow();
}

[Fact]
public void use_npgsql_data_source_with_keyed_registration_should_fail_if_key_is_not_passed()
{
var services = new ServiceCollection();

services.AddNpgsqlDataSource(ConnectionSource.ConnectionString, serviceKey: "marten_data_source");

services.AddMarten()
.UseLightweightSessions()
.UseNpgsqlDataSource();

var serviceProvider = services.BuildServiceProvider();

Action GetStore(IServiceProvider c) => () =>
{
using var store = c.GetService<IDocumentStore>();
};

var exc = GetStore(serviceProvider).ShouldThrow<InvalidOperationException>();
exc.Message.Contains("NpgsqlDataSource").ShouldBeTrue();
}

[Fact]
public void use_npgsql_data_source_with_registration_should_fail_if_key_is_passed()
{
var services = new ServiceCollection();

services.AddNpgsqlDataSource(ConnectionSource.ConnectionString);

services.AddMarten()
.UseLightweightSessions()
.UseNpgsqlDataSource("marten_data_source");

var serviceProvider = services.BuildServiceProvider();

Action GetStore(IServiceProvider c) => () =>
{
using var store = c.GetService<IDocumentStore>();
};

var exc = GetStore(serviceProvider).ShouldThrow<InvalidOperationException>();
exc.Message.Contains("NpgsqlDataSource").ShouldBeTrue();
}
#endif

[Fact]
public void use_npgsql_data_source_should_fail_if_data_source_is_not_registered()
{
var services = new ServiceCollection();

services.AddMarten()
.UseLightweightSessions()
.UseNpgsqlDataSource();

var serviceProvider = services.BuildServiceProvider();

Action GetStore(IServiceProvider c) => () =>
{
using var store = c.GetService<IDocumentStore>();
};

var exc = GetStore(serviceProvider).ShouldThrow<InvalidOperationException>();
exc.Message.Contains("NpgsqlDataSource").ShouldBeTrue();
}


[Fact]
public void AddMarten_with_no_params_should_fail_if_UseNpgsqlDataSource_was_not_called()
{
var services = new ServiceCollection();

services.AddNpgsqlDataSource(ConnectionSource.ConnectionString);

services.AddMarten()
.UseLightweightSessions();

var serviceProvider = services.BuildServiceProvider();

Action GetStore(IServiceProvider c) => () =>
{
using var store = c.GetService<IDocumentStore>();
};

var exc = GetStore(serviceProvider).ShouldThrow<InvalidOperationException>();
exc.Message.Contains("UseNpgsqlDataSource").ShouldBeTrue();
}

public class SpecialBuilder: ISessionFactory
{
private readonly IDocumentStore _store;
Expand Down
15 changes: 15 additions & 0 deletions src/CoreTests/StoreOptionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,21 @@ public void SettingCustomDataSourceFactory_ShouldRespectCurrentTenancySettings()
options.Tenancy.ShouldBeOfType<SingleServerMultiTenancy>();
}

[Fact]
public void SettingCustomDataSourceFactory_ShouldSetTenancyIfItsNotDefinedYet()
{
// Given
var options = new StoreOptions();

// When
options.DataSourceFactory(new DummyNpgsqlDataSourceFactory(), ConnectionSource.ConnectionString);

// Then
options.NpgsqlDataSourceFactory.ShouldBeOfType<DummyNpgsqlDataSourceFactory>();
options.Tenancy.ShouldBeOfType<DefaultTenancy>();
}


private class DummyNpgsqlDataSourceFactory: INpgsqlDataSourceFactory
{
public NpgsqlDataSource Create(string connectionString) =>
Expand Down
19 changes: 3 additions & 16 deletions src/Marten/DocumentStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,23 +100,10 @@ public virtual void Dispose()
(Options.Events as IDisposable)?.SafeDispose();
}

public async ValueTask DisposeAsync()
public ValueTask DisposeAsync()
{
if (dataSourceFactory is IAsyncDisposable dsad)
{
await dsad.DisposeAsync().ConfigureAwait(false);
}

if (Options.Events is IAsyncDisposable ad)
{
await ad.DisposeAsync().ConfigureAwait(false);
return;
}

if (Options.Events is IDisposable d)
{
d.Dispose();
}
return DisposableExtensions
.MaybeDisposeAllAsync<object>([dataSourceFactory, Options.Events]);
}

public AdvancedOperations Advanced { get; }
Expand Down

0 comments on commit da7f07a

Please sign in to comment.