Skip to content

Commit

Permalink
Merge pull request #129 from zejji/develop
Browse files Browse the repository at this point in the history
Develop (update for .NET 8 GA release)
  • Loading branch information
zejji authored Nov 20, 2023
2 parents f52062b + a89d7ac commit 27f3283
Show file tree
Hide file tree
Showing 24 changed files with 265 additions and 192 deletions.
2 changes: 1 addition & 1 deletion .config/dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"csharpier": {
"version": "0.25.0",
"version": "0.26.2",
"commands": [
"dotnet-csharpier"
]
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ jobs:
uses: actions/setup-dotnet@v3
with:
dotnet-version: '8.0.x'
dotnet-quality: 'preview'
dotnet-quality: 'ga'

# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
uses: actions/setup-dotnet@v3
with:
dotnet-version: '8.0.x'
dotnet-quality: 'preview'
dotnet-quality: 'ga'
- name: Restore dependencies
run: dotnet restore
- name: Build
Expand All @@ -36,7 +36,7 @@ jobs:
uses: actions/setup-dotnet@v3
with:
dotnet-version: '8.0.x'
dotnet-quality: 'preview'
dotnet-quality: 'ga'
- name: Restore dependencies
run: dotnet restore
- name: Build NuGet
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,5 @@ data/
msbuild.log
msbuild.err
msbuild.wrn

.idea/
8 changes: 4 additions & 4 deletions DbContextScope.Tests/DbContextScope.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="7.0.0-alpha.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0-rc.2.23480.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0-preview-23503-02" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0-preview-23531-01" />
<PackageReference Include="System.Dynamic.Runtime" Version="4.3.0" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3">
<PackageReference Include="xunit" Version="2.6.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
Expand Down
79 changes: 44 additions & 35 deletions DbContextScope.Tests/DbContextScopeTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using DbContextScope.Exceptions;
using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using System;
Expand Down Expand Up @@ -59,11 +60,24 @@ public void Nested_scopes_should_use_same_DbContext_by_default()
}
}

[Fact]
public void Calling_GetRequired_on_an_AmbientDbContextLocator_outside_of_an_ambient_scope_should_throw()
{
var ex = Record.Exception(() =>
{
var contextLocator = new AmbientDbContextLocator();
contextLocator.GetRequired<TestDbContext>();
});

ex.Should().NotBeNull();
ex.Should().BeOfType<NoAmbientDbContextScopeException>();
}

[Fact]
public void Calling_SaveChanges_on_a_nested_scope_has_no_effect()
{
var originalName = "Test User";
var newName = "New name";
const string originalName = "Test User";
const string newName = "New name";

// Arrange - add one user to the database
using (var dbContext = _dbContextFactory.CreateDbContext<TestDbContext>())
Expand Down Expand Up @@ -95,8 +109,8 @@ public void Calling_SaveChanges_on_a_nested_scope_has_no_effect()
[Fact]
public void Calling_SaveChanges_on_the_outer_scope_saves_changes()
{
var originalName = "Test User";
var newName = "New name";
const string originalName = "Test User";
const string newName = "New name";

// Arrange - add one user to the database
using (var dbContext = _dbContextFactory.CreateDbContext<TestDbContext>())
Expand Down Expand Up @@ -134,7 +148,10 @@ public void Changes_can_only_be_saved_once_on_a_DbContextScope()
dbContextScope.SaveChanges();

// Act - call SaveChanges again
var ex = Record.Exception(() => dbContextScope.SaveChanges());
var ex = Record.Exception(() =>
{
dbContextScope.SaveChanges();
});

// Assert - an InvalidOperationException should have been thrown
ex.Should().NotBeNull();
Expand All @@ -145,9 +162,9 @@ public void Changes_can_only_be_saved_once_on_a_DbContextScope()
[Fact]
public void SaveChanges_can_be_called_again_after_a_DbUpdateConcurrencyException()
{
var originalName = "Test User";
var newName1 = "New name 1";
var newName2 = "New name 2";
const string originalName = "Test User";
const string newName1 = "New name 1";
const string newName2 = "New name 2";

// Arrange - add one user to the database
using (var dbContext = _dbContextFactory.CreateDbContext<TestDbContext>())
Expand Down Expand Up @@ -228,9 +245,7 @@ public void IDbContextReadOnlyScope_should_not_have_SaveChanges_method()
var publicMethods = type.GetMethods(
BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly
);
var saveChangesMethod = publicMethods
.Where(m => m.Name == "SaveChanges")
.SingleOrDefault();
var saveChangesMethod = publicMethods.SingleOrDefault(m => m.Name == "SaveChanges");
saveChangesMethod.Should().BeNull();
}
}
Expand Down Expand Up @@ -276,20 +291,17 @@ public void ForceCreateNew_option_should_create_new_DbContext_in_nested_scope()
[Fact]
public void RefreshEntitiesInParentScope_should_reload_changed_data_from_database()
{
var originalName1 = "Test User 1";
var originalName2 = "Test User 2";
var newName1 = "New name 1";
var newName2 = "New name 2";
const string originalName1 = "Test User 1";
const string originalName2 = "Test User 2";
const string newName1 = "New name 1";
const string newName2 = "New name 2";

// Arrange - add two users to the database
using (var dbContext = _dbContextFactory.CreateDbContext<TestDbContext>())
{
dbContext.Users.AddRange(
new User[]
{
new User { Name = originalName1 },
new User { Name = originalName2 }
}
new User { Name = originalName1 },
new User { Name = originalName2 }
);
dbContext.SaveChanges();
}
Expand Down Expand Up @@ -340,24 +352,21 @@ public void RefreshEntitiesInParentScope_should_refresh_entities_with_composite_
using (var dbContext = _dbContextFactory.CreateDbContext<TestDbContext>())
{
dbContext.Users.AddRange(
new User[]
new()
{
new User
Name = "Test User 1",
CoursesUsers = new CourseUser[]
{
Name = "Test User 1",
CoursesUsers = new CourseUser[]
{
new CourseUser { Course = course1, Grade = "A" },
new CourseUser { Course = course2, Grade = "C" }
}
},
new User
new() { Course = course1, Grade = "A" },
new() { Course = course2, Grade = "C" }
}
},
new()
{
Name = "Test User 2",
CoursesUsers = new CourseUser[]
{
Name = "Test User 2",
CoursesUsers = new CourseUser[]
{
new CourseUser { Course = course1, Grade = "F" }
}
new() { Course = course1, Grade = "F" }
}
}
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,18 @@

namespace Zejji.Tests.Helpers
{
internal class SqliteMemoryDatabaseLifetimeManager : IDisposable
/// <summary>
/// This class is responsible for keeping an in-memory SQLite database
/// alive for the lifetime of an instance. This is required because SQLite
/// in-memory databases cease to exist as soon as the last database
/// connection is closed.
///
/// See <a href="https://www.sqlite.org/inmemorydb.html">this link</a> for more information.
///
/// An instance of this class instantiates a SQLite database upon creation
/// and disposes of it when it is itself disposed.
/// </summary>
internal sealed class SqliteMemoryDatabaseLifetimeManager : IDisposable
{
public readonly string ConnectionString =
$"DataSource={Guid.NewGuid()};mode=memory;cache=shared";
Expand All @@ -23,13 +34,13 @@ public void Dispose() // see https://rules.sonarsource.com/csharp/RSPEC-3881
GC.SuppressFinalize(this);
}

protected virtual void Dispose(bool disposing)
private void Dispose(bool disposing)
{
if (_keepAliveConnection != null)
{
_keepAliveConnection.Dispose();
_keepAliveConnection = null;
}
if (_keepAliveConnection == null)
return;

_keepAliveConnection.Dispose();
_keepAliveConnection = null;
}
}
}
1 change: 1 addition & 0 deletions DbContextScope.sln
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution items", "Solution
.gitattributes = .gitattributes
.gitignore = .gitignore
Directory.Build.props = Directory.Build.props
.config\dotnet-tools.json = .config\dotnet-tools.json
global.json = global.json
README.md = README.md
stylecop.json = stylecop.json
Expand Down
8 changes: 4 additions & 4 deletions DbContextScope/DbContextScope.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<PackageId>Zejji.DbContextScope.EFCore</PackageId>
<Version>8.0.100-rc.2</Version>
<Version>8.0.100.1</Version>
<Authors>Mehdi El Gueddari;Gerard Howell</Authors>
<PackageTags>DbContextScope;DbContext;EF;EFCore;EntityFramework;EntityFrameworkCore;UnitOfWork;AmbientDbContext;AmbientContext;RepositoryPattern</PackageTags>
<Description>A library for managing the lifetime of Entity Framework Core DbContext instances. This package is based on the original DbContextScope repository by Mehdi El Gueddari, updated for .NET 6+ and EF Core, with a number of additional improvements and bug fixes.</Description>
Expand All @@ -22,9 +22,9 @@
<ItemGroup>
<None Include="LICENSE.txt" Pack="true" PackagePath="" />
<None Include="..\README.md" Pack="true" PackagePath="\" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0-rc.2.23480.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.0-rc.2.23480.1" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.0" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
</ItemGroup>

</Project>
20 changes: 11 additions & 9 deletions DbContextScope/Enums/DbContextScopeOption.cs
Original file line number Diff line number Diff line change
@@ -1,34 +1,36 @@
namespace Zejji.Entity
using Microsoft.EntityFrameworkCore;

namespace Zejji.Entity
{
/// <summary>
/// Indicates whether or not a new DbContextScope will join the ambient scope.
/// Indicates whether or not a new <see cref="DbContextScope"/> will join the ambient scope.
/// </summary>
public enum DbContextScopeOption
{
/// <summary>
/// Join the ambient DbContextScope if one exists. Creates a new
/// Join the ambient <see cref="DbContextScope"/> if one exists. Creates a new
/// one otherwise.
///
/// This is what you want in most cases. Joining the existing ambient scope
/// ensures that all code within a business transaction uses the same DbContext
/// ensures that all code within a business transaction uses the same <see cref="DbContext"/>
/// instance and that all changes made by service methods called within that
/// business transaction are either committed or rolled back atomically when the top-level
/// scope completes (i.e. it ensures that there are no partial commits).
/// </summary>
JoinExisting,

/// <summary>
/// Ignore the ambient DbContextScope (if any) and force the creation of
/// a new DbContextScope.
/// Ignore the ambient <see cref="DbContextScope"/> (if any) and force the creation of
/// a new <see cref="DbContextScope"/>.
///
/// This is an advanced feature that should be used with great care.
///
/// When forcing the creation of a new scope, new DbContext instances will be
/// created within that inner scope instead of re-using the DbContext instances that
/// When forcing the creation of a new scope, new <see cref="DbContext"/> instances will be
/// created within that inner scope instead of re-using the <see cref="DbContext"/> instances that
/// the parent scope (if any) is using.
///
/// Any changes made to entities within that inner scope will therefore get persisted
/// to the database when SaveChanges() is called in the inner scope regardless of whether
/// to the database when <see cref="DbContextScope.SaveChanges()"/> is called in the inner scope regardless of whether
/// or not the parent scope is successful.
///
/// You would typically do this to ensure that the changes made within the inner scope
Expand Down
18 changes: 18 additions & 0 deletions DbContextScope/Exceptions/NoAmbientDbContextScopeException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace DbContextScope.Exceptions;

using System;

/// <summary>
/// A custom exception that is thrown when an operation which is required to be run in the presence
/// of an ambient <see cref="DbContextScope"/> is run with no such scope present.
/// </summary>
public class NoAmbientDbContextScopeException : Exception
{
public NoAmbientDbContextScopeException() { }

public NoAmbientDbContextScopeException(string message)
: base(message) { }

public NoAmbientDbContextScopeException(string message, Exception inner)
: base(message, inner) { }
}
15 changes: 14 additions & 1 deletion DbContextScope/Implementations/AmbientDbContextLocator.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.EntityFrameworkCore;
using DbContextScope.Exceptions;
using Microsoft.EntityFrameworkCore;

namespace Zejji.Entity
{
Expand All @@ -10,5 +11,17 @@ public class AmbientDbContextLocator : IAmbientDbContextLocator
var ambientDbContextScope = DbContextScope.GetAmbientScope();
return ambientDbContextScope?.DbContexts.Get<TDbContext>();
}

public TDbContext GetRequired<TDbContext>()
where TDbContext : DbContext
{
var ambientDbContextScope =
DbContextScope.GetAmbientScope()
?? throw new NoAmbientDbContextScopeException(
$"No ambient {nameof(DbContext)} of type {typeof(TDbContext).Name} found. This usually means that this repository method has been called outside of the scope of a {nameof(DbContextScope)}. A repository must only be accessed within the scope of a {nameof(DbContextScope)}, which takes care of creating the {nameof(DbContext)} instances that the repositories need and making them available as ambient contexts. This is what ensures that, for any given {nameof(DbContext)}-derived type, the same instance is used throughout the duration of a business transaction. To fix this issue, use {nameof(IDbContextScopeFactory)} in your top-level business logic service method to create a {nameof(DbContextScope)} that wraps the entire business transaction that your service method implements. Then access this repository within that scope. Refer to the comments in the {nameof(IDbContextScope)}.cs file for more details."
);

return ambientDbContextScope.DbContexts.Get<TDbContext>();
}
}
}
Loading

0 comments on commit 27f3283

Please sign in to comment.