Skip to content

Commit a83b60e

Browse files
committed
New ambient data implementation
1 parent ad61c4c commit a83b60e

18 files changed

+86
-156
lines changed

README.md

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -61,20 +61,17 @@ The default JSON settings structure is as follows:
6161
}
6262
```
6363

64-
# IDatabaseContextService
64+
# DatabaseContextScope
6565

66-
The `DatabaseContextService` provides ambient access to the current `IDatabaseContext` instance. This is useful in situations where you do not want to pass the `IDatabaseContext` instance around.
66+
The `DatabaseContextService` contains a collection of the `DatabaseContext` instances created by `IDatabaseContextFactory`. However, these will not be flowed across async contexts.
6767

68-
``` c#
69-
event EventHandler<DatabaseContextAsyncLocalValueChangedEventArgs> DatabaseContextAsyncLocalValueChanged;
70-
71-
event EventHandler<DatabaseContextAsyncLocalValueAssignedEventArgs> DatabaseContextAsyncLocalValueAssigned;
72-
```
73-
74-
Attach delegates to the above events should you wish to track ambient data changes.
68+
To enable async context flow wrap the initial database context creation in a `using` statement:
7569

7670
``` c#
77-
void BeginScope()
71+
using (new DatabaseContextScope())
72+
{
73+
// database interaction
74+
})
7875
```
7976

8077
# IDatabaseContextFactory
@@ -83,17 +80,21 @@ In order to access a database we need a database connection. A database connect
8380

8481
The `DatabaseContextFactory` implementation makes use of an `IDbConnectionFactory` implementation which creates a `System.Data.IDbConnection` by using the provider name and connection string, which is obtained from the registered connection name. An `IDbCommandFactory` creates a `System.Data.IDbCommand` by using an `IDbConnection` instance.
8582

86-
Before any database context can be created a scope must be started. This is typically done in the integration/application layer (controller/minimal API methods, message handlers). Nested scopes are not permitted as the ambient context will flow across any `async` methods.
87-
8883
``` c#
89-
var databaseContextService = provider.GetRequiredService<IDatabaseContextService>();
9084
var databaseContextFactory = provider.GetRequiredService<IDatabaseContextFactory>();
9185

92-
using (databaseContextService.BeginScope()) // <-- will configure the scope (cannot be nested)
9386
using (var databaseContext = databaseContextFactory.Create("connection-name"))
9487
{
9588
// database interaction
9689
}
90+
91+
// or, in async/await implementations
92+
93+
using (new DatabaseContextScope())
94+
using (var databaseContext = databaseContextFactory.Create("connection-name"))
95+
{
96+
// database interaction that will flow across threads
97+
}
9798
```
9899

99100
# IQuery

Shuttle.Core.Data.Tests/AsyncFixture.cs

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using System.Collections.Generic;
22
using System.Threading;
33
using System.Threading.Tasks;
4-
using Microsoft.Extensions.DependencyInjection;
54
using NUnit.Framework;
65

76
namespace Shuttle.Core.Data.Tests;
@@ -22,20 +21,17 @@ public void Should_be_able_to_use_the_different_database_context_for_separate_th
2221
{
2322
var threads = new List<Thread>();
2423

25-
using (DatabaseContextService.BeginScope())
26-
{
27-
var databaseContext = DatabaseContextService.Find(context => context.Name.Equals(DefaultConnectionStringName));
24+
var databaseContext = DatabaseContextService.Find(context => context.Name.Equals(DefaultConnectionStringName));
2825

29-
Assert.That(databaseContext, Is.Null);
30-
}
26+
Assert.That(databaseContext, Is.Null);
3127

3228
for (var i = 0; i < 10; i++)
3329
{
3430
using (ExecutionContext.SuppressFlow())
3531
{
3632
threads.Add(new Thread(() =>
3733
{
38-
using (DatabaseContextService.BeginScope())
34+
using (new DatabaseContextScope())
3935
using (DatabaseContextFactory.Create())
4036
{
4137
DatabaseGateway.GetRowsAsync(_rowsQuery);
@@ -64,7 +60,7 @@ public void Should_be_able_to_use_the_different_database_context_for_separate_ta
6460
{
6561
tasks.Add(Task.Run(() =>
6662
{
67-
using (DatabaseContextService.BeginScope())
63+
using (new DatabaseContextScope())
6864
using (DatabaseContextFactory.Create())
6965
{
7066
DatabaseGateway.GetRowsAsync(_rowsQuery);
@@ -80,7 +76,6 @@ public void Should_be_able_to_use_the_same_database_context_across_tasks()
8076
{
8177
var tasks = new List<Task>();
8278

83-
using (DatabaseContextService.BeginScope())
8479
using (DatabaseContextFactory.Create())
8580
{
8681
for (var i = 0; i < 10; i++)
@@ -95,7 +90,6 @@ public void Should_be_able_to_use_the_same_database_context_across_tasks()
9590
[Test]
9691
public async Task Should_be_able_to_use_the_same_database_context_across_synchronized_tasks_async()
9792
{
98-
using (DatabaseContextService.BeginScope())
9993
await using (DatabaseContextFactory.Create())
10094
{
10195
await GetRowsAsync(0);

Shuttle.Core.Data.Tests/DatabaseContextFactoryFixture.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ public class DatabaseContextFactoryFixture : Fixture
1212
[Test]
1313
public void Should_be_able_to_create_a_database_context()
1414
{
15-
using (DatabaseContextService.BeginScope())
1615
using (var context = DatabaseContextFactory.Create(DefaultConnectionStringName))
1716
{
1817
Assert.IsNotNull(context);
@@ -24,7 +23,6 @@ public void Should_not_not_be_able_to_create_another_context_with_the_same_name_
2423
{
2524
var factory = DatabaseContextFactory;
2625

27-
using (DatabaseContextService.BeginScope())
2826
using (factory.Create(DefaultConnectionStringName))
2927
{
3028
Assert.That(()=> factory.Create(DefaultConnectionStringName), Throws.InvalidOperationException);

Shuttle.Core.Data.Tests/DatabaseContextFixture.cs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ public void Should_not_be_able_to_create_a_non_existent_connection()
2727
{
2828
Assert.Throws<SqlException>(() =>
2929
{
30-
using (DatabaseContextService.BeginScope())
3130
using (var databaseContext = new DatabaseContext("context", "Microsoft.Data.SqlClient", new SqlConnection("data source=.;initial catalog=idontexist;integrated security=sspi"),
3231
new Mock<IDbCommandFactory>().Object, DatabaseContextService))
3332
{
@@ -50,7 +49,6 @@ public async Task Should_be_able_to_begin_and_commit_a_transaction_async()
5049

5150
private async Task Should_be_able_to_begin_and_commit_a_transaction_async(bool sync)
5251
{
53-
using (DatabaseContextService.BeginScope())
5452
await using (var databaseContext = new DatabaseContext("context", "Microsoft.Data.SqlClient", DbConnectionFactory.Create(DefaultProviderName, DefaultConnectionString), new Mock<IDbCommandFactory>().Object, DatabaseContextService))
5553
{
5654
if (sync)
@@ -80,7 +78,6 @@ public async Task Should_be_able_to_begin_and_rollback_a_transaction_async()
8078

8179
private async Task Should_be_able_to_begin_and_rollback_a_transaction_async(bool sync)
8280
{
83-
using (DatabaseContextService.BeginScope())
8481
await using (var databaseContext = new DatabaseContext("context", "Microsoft.Data.SqlClient", DbConnectionFactory.Create(DefaultProviderName, DefaultConnectionString), new Mock<IDbCommandFactory>().Object, new DatabaseContextService()))
8582
{
8683
if (sync)
@@ -108,7 +105,6 @@ public async Task Should_be_able_to_call_commit_without_a_transaction_async()
108105

109106
private async Task Should_be_able_to_call_commit_without_a_transaction_async(bool sync)
110107
{
111-
using (DatabaseContextService.BeginScope())
112108
await using (var databaseContext = new DatabaseContext("context", "Microsoft.Data.SqlClient", DbConnectionFactory.Create(DefaultProviderName, DefaultConnectionString), new Mock<IDbCommandFactory>().Object, DatabaseContextService))
113109
{
114110
if (sync)
@@ -125,7 +121,6 @@ private async Task Should_be_able_to_call_commit_without_a_transaction_async(boo
125121
[Test]
126122
public void Should_be_able_to_call_dispose_more_than_once()
127123
{
128-
using (DatabaseContextService.BeginScope())
129124
using (var databaseContext = new DatabaseContext("context", "Microsoft.Data.SqlClient", DbConnectionFactory.Create(DefaultProviderName, DefaultConnectionString), new Mock<IDbCommandFactory>().Object, DatabaseContextService))
130125
{
131126
databaseContext.Dispose();
@@ -143,7 +138,6 @@ public void Should_be_able_to_create_a_command()
143138

144139
dbCommandFactory.Setup(m => m.Create(dbConnection, query.Object)).Returns(dbCommand.Object);
145140

146-
using (DatabaseContextService.BeginScope())
147141
using (var databaseContext = new DatabaseContext("context", "Microsoft.Data.SqlClient", dbConnection, dbCommandFactory.Object, DatabaseContextService))
148142
{
149143
databaseContext.CreateCommand(query.Object);

Shuttle.Core.Data.Tests/DatabaseContextServiceFixture.cs

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,31 +11,28 @@ public void Should_be_able_to_use_different_contexts()
1111
{
1212
var service = new DatabaseContextService();
1313

14-
using (service.BeginScope())
15-
{
16-
var context1 = new DatabaseContext("mock-1", "provider-name", new Mock<DbConnection>().Object, new Mock<IDbCommandFactory>().Object, service);
14+
var context1 = new DatabaseContext("mock-1", "provider-name", new Mock<DbConnection>().Object, new Mock<IDbCommandFactory>().Object, service);
1715

18-
Assert.That(service.Current.Key, Is.EqualTo(context1.Key));
16+
Assert.That(service.Current.Key, Is.EqualTo(context1.Key));
1917

20-
var context2 = new DatabaseContext("mock-2", "provider-name", new Mock<DbConnection>().Object, new Mock<IDbCommandFactory>().Object, service);
18+
var context2 = new DatabaseContext("mock-2", "provider-name", new Mock<DbConnection>().Object, new Mock<IDbCommandFactory>().Object, service);
2119

22-
Assert.That(service.Current.Key, Is.EqualTo(context2.Key));
20+
Assert.That(service.Current.Key, Is.EqualTo(context2.Key));
2321

24-
service.Activate("mock-1");
22+
service.Activate("mock-1");
2523

26-
Assert.That(service.Current.Key, Is.EqualTo(context1.Key));
24+
Assert.That(service.Current.Key, Is.EqualTo(context1.Key));
2725

28-
service.Activate("mock-2");
26+
service.Activate("mock-2");
2927

30-
Assert.That(service.Current.Key, Is.EqualTo(context2.Key));
28+
Assert.That(service.Current.Key, Is.EqualTo(context2.Key));
3129

32-
service.Activate(context1);
30+
service.Activate(context1);
3331

34-
Assert.That(service.Current.Key, Is.EqualTo(context1.Key));
32+
Assert.That(service.Current.Key, Is.EqualTo(context1.Key));
3533

36-
service.Activate(context2);
34+
service.Activate(context2);
3735

38-
Assert.That(service.Current.Key, Is.EqualTo(context2.Key));
39-
}
36+
Assert.That(service.Current.Key, Is.EqualTo(context2.Key));
4037
}
4138
}

Shuttle.Core.Data.Tests/Mapping/DataRowMapperFixture.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ select top 1
4040
BasicMapping
4141
");
4242

43-
using (DatabaseContextService.BeginScope())
4443
await using (DatabaseContextFactory.Create())
4544
{
4645
var item = sync
@@ -101,7 +100,6 @@ Age as TheAge
101100
BasicMapping
102101
");
103102

104-
using (DatabaseContextService.BeginScope())
105103
await using (DatabaseContextFactory.Create())
106104
{
107105
var item = sync
@@ -159,7 +157,6 @@ select top 1
159157
BasicMapping
160158
");
161159

162-
using (DatabaseContextService.BeginScope())
163160
await using (DatabaseContextFactory.Create())
164161
{
165162
var value = sync
@@ -216,7 +213,6 @@ private async Task Should_be_able_to_perform_dynamic_mapping_async(bool sync)
216213
BasicMapping
217214
");
218215

219-
using (DatabaseContextService.BeginScope())
220216
await using (DatabaseContextFactory.Create())
221217
{
222218
var item = sync

Shuttle.Core.Data.Tests/Mapping/MappingFixture.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ public class MappingFixture : Fixture
88
[SetUp]
99
public async Task SetUp()
1010
{
11-
using (DatabaseContextService.BeginScope())
1211
using (DatabaseContextFactory.Create())
1312
{
1413
await DatabaseGateway.ExecuteAsync(new Query(@"

Shuttle.Core.Data.Tests/Mapping/QueryMapperFixture.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ select top 1
4242
BasicMapping
4343
");
4444

45-
using (DatabaseContextService.BeginScope())
4645
await using (DatabaseContextFactory.Create())
4746
{
4847
var item = sync
@@ -103,7 +102,6 @@ Age as TheAge
103102
BasicMapping
104103
");
105104

106-
using (DatabaseContextService.BeginScope())
107105
await using (DatabaseContextFactory.Create())
108106
{
109107
var item = await mapper.MapObjectAsync<BasicMapping>(queryRow);
@@ -150,7 +148,6 @@ select top 1
150148
BasicMapping
151149
");
152150

153-
using (DatabaseContextService.BeginScope())
154151
await using (DatabaseContextFactory.Create())
155152
{
156153
var value = sync
@@ -200,7 +197,6 @@ select top 1
200197
BasicMapping
201198
");
202199

203-
using (DatabaseContextService.BeginScope())
204200
await using (DatabaseContextFactory.Create())
205201
{
206202
var item = sync

Shuttle.Core.Data/.package/package.nuspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<package>
44
<metadata>
55
<id>Shuttle.Core.Data</id>
6-
<version>16.0.1</version>
6+
<version>16.0.2</version>
77
<authors>Eben Roux</authors>
88
<owners>Eben Roux</owners>
99
<license type="expression">BSD-3-Clause</license>

Shuttle.Core.Data/DatabaseContextAsyncLocalValueAssignedEventArgs.cs

Lines changed: 0 additions & 17 deletions
This file was deleted.

Shuttle.Core.Data/DatabaseContextAsyncValueEventArgs.cs

Lines changed: 0 additions & 16 deletions
This file was deleted.

Shuttle.Core.Data/DatabaseContextAmbientData.cs renamed to Shuttle.Core.Data/DatabaseContextCollection.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
namespace Shuttle.Core.Data
55
{
6-
public class DatabaseContextAmbientData
6+
public class DatabaseContextCollection
77
{
88
public IEnumerable<IDatabaseContext> DatabaseContexts => _databaseContexts.AsReadOnly();
99

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System;
2+
using System.Threading;
3+
4+
namespace Shuttle.Core.Data
5+
{
6+
public class DatabaseContextScope : IDisposable
7+
{
8+
private static readonly AsyncLocal<DatabaseContextCollection> AmbientData = new AsyncLocal<DatabaseContextCollection>();
9+
10+
public DatabaseContextScope()
11+
{
12+
if (AmbientData.Value != null)
13+
{
14+
throw new InvalidOperationException(Resources.AmbientScopeException);
15+
}
16+
17+
AmbientData.Value = new DatabaseContextCollection();
18+
}
19+
20+
public static DatabaseContextCollection Current => AmbientData.Value;
21+
22+
public void Dispose()
23+
{
24+
AmbientData.Value = null;
25+
}
26+
}
27+
}

0 commit comments

Comments
 (0)