From 3d0ad4f9ac444c8e274c67930d721998ddf0cb40 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 6 Jun 2026 21:37:46 +0000 Subject: [PATCH 1/3] Remove ambient context stack from EF persistence layer The EntityFramework persistence layer used an AsyncLocal-based context stack (ContextStack) so that shared singleton repositories could discover the current context, its DbContext and its GameConfiguration. Since every repository operation already originates from an EntityFrameworkContextBase (which holds its own DbContext and current GameConfiguration) and the stack never actually nested distinct contexts, the ambient mechanism was unnecessary. This replaces the stack by threading the originating context explicitly: - Remove ContextStack / IContextStack / IContextStackProvider. - Add an internal IContextAwareRepository (and context-aware GetRepository overloads on IContextAwareRepositoryProvider) so repositories receive the originating context as an explicit parameter. - Thread the originating context through GenericRepositoryBase, the configuration/account/letter/game-server repositories and CachedRepository, while keeping the shared per-GameConfiguration caches intact. - Move the cache/non-cache repository selection (edit TypedContext check) to read the explicitly passed context instead of the ambient stack. Public API (IPersistenceContextProvider, IRepositoryProvider, ICacheAwareRepositoryProvider) and the InMemory implementation are unchanged. https://claude.ai/code/session_015sGcQwyYMwppSjbP1uD66j --- .../EntityFramework/AccountRepository.cs | 64 +++++---- .../CacheAwareRepositoryProvider.cs | 64 ++++++--- .../EntityFramework/CachedRepository{T}.cs | 48 ++++++- .../CachingGameConfigurationRepository.cs | 10 +- .../CachingGenericRepository.cs | 8 +- .../CachingRepositoryProvider.cs | 9 +- .../ConfigurationTypeRepository.cs | 86 ++++++++---- .../EntityFramework/ContextStack.cs | 62 --------- .../EntityFrameworkContextBase.cs | 27 +--- .../GameConfigurationRepository.cs | 8 +- .../GameServerDefinitionRepository.cs | 24 ++-- .../EntityFramework/GenericRepository.cs | 8 +- .../EntityFramework/GenericRepositoryBase.cs | 128 ++++++++++++------ .../IConfigurationTypeRepository.cs | 5 +- .../IContextAwareRepository.cs | 38 ++++++ .../IContextAwareRepositoryProvider.cs | 43 +++++- .../EntityFramework/IContextStack.cs | 25 ---- .../EntityFramework/IContextStackProvider.cs | 16 --- .../EntityFramework/ILoadByProperty.cs | 3 +- .../EntityFramework/LetterBodyRepository.cs | 10 +- .../NonCachingRepositoryProvider.cs | 5 +- .../PersistenceContextProvider.cs | 4 +- .../EntityFramework/PlayerContext.cs | 38 ++---- .../EntityFramework/RepositoryProvider.cs | 36 +++-- 24 files changed, 441 insertions(+), 328 deletions(-) delete mode 100644 src/Persistence/EntityFramework/ContextStack.cs create mode 100644 src/Persistence/EntityFramework/IContextAwareRepository.cs delete mode 100644 src/Persistence/EntityFramework/IContextStack.cs delete mode 100644 src/Persistence/EntityFramework/IContextStackProvider.cs diff --git a/src/Persistence/EntityFramework/AccountRepository.cs b/src/Persistence/EntityFramework/AccountRepository.cs index f5f226025..4de5a1321 100644 --- a/src/Persistence/EntityFramework/AccountRepository.cs +++ b/src/Persistence/EntityFramework/AccountRepository.cs @@ -28,30 +28,32 @@ public AccountRepository(IContextAwareRepositoryProvider repositoryProvider, ILo } /// - public override async ValueTask GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + public override async ValueTask GetByIdAsync(Guid id, EntityFrameworkContextBase? context, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); - (this.RepositoryProvider as ICacheAwareRepositoryProvider)?.EnsureCachesForCurrentGameConfiguration(); + using var ownedContext = context is null ? this.GetContext(null) : null; + var origin = context ?? ownedContext!; - using var context = this.GetContext(); - await context.Context.Database.OpenConnectionAsync(cancellationToken).ConfigureAwait(false); + this.RepositoryProvider.EnsureCachesForCurrentGameConfiguration(origin); + + await origin.Context.Database.OpenConnectionAsync(cancellationToken).ConfigureAwait(false); try { - var accountEntry = context.Context.ChangeTracker.Entries().FirstOrDefault(a => a.Entity.Id == id); + var accountEntry = origin.Context.ChangeTracker.Entries().FirstOrDefault(a => a.Entity.Id == id); var account = accountEntry?.Entity; if (account is null || accountEntry?.References.Any(reference => !reference.IsLoaded) is true) { if (account is not null) { - context.Detach(account); + origin.Detach(account); } var objectLoader = new AccountJsonObjectLoader(); - account = await objectLoader.LoadObjectAsync(id, context.Context, cancellationToken).ConfigureAwait(false); - if (account != null && !(context.Context.Entry(account) is { } entry && entry.State != EntityState.Detached)) + account = await objectLoader.LoadObjectAsync(id, origin.Context, cancellationToken).ConfigureAwait(false); + if (account != null && !(origin.Context.Entry(account) is { } entry && entry.State != EntityState.Detached)) { - context.Context.Attach(account); + origin.Context.Attach(account); } } @@ -59,7 +61,7 @@ public AccountRepository(IContextAwareRepositoryProvider repositoryProvider, ILo } finally { - await context.Context.Database.CloseConnectionAsync().ConfigureAwait(false); + await origin.Context.Database.CloseConnectionAsync().ConfigureAwait(false); } } @@ -67,23 +69,25 @@ public AccountRepository(IContextAwareRepositoryProvider repositoryProvider, ILo /// Gets the account by character name. /// /// The character name. + /// The originating context. /// The cancellation token. /// /// The account; otherwise, null. /// - internal async ValueTask GetAccountByCharacterNameAsync(string characterName, CancellationToken cancellationToken = default) + internal async ValueTask GetAccountByCharacterNameAsync(string characterName, EntityFrameworkContextBase? context, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); - using var context = this.GetContext(); - var accountInfo = await context.Context.Set() + using var ownedContext = context is null ? this.GetContext(null) : null; + var origin = context ?? ownedContext!; + var accountInfo = await origin.Context.Set() .AsNoTracking() .FirstOrDefaultAsync(a => a.RawCharacters.Any(c => c.Name == characterName), cancellationToken) .ConfigureAwait(false); if (accountInfo != null) { - return await this.GetByIdAsync(accountInfo.Id, cancellationToken).ConfigureAwait(false); + return await this.GetByIdAsync(accountInfo.Id, origin, cancellationToken).ConfigureAwait(false); } return null; @@ -94,14 +98,16 @@ public AccountRepository(IContextAwareRepositoryProvider repositoryProvider, ILo /// /// The login name. /// The password. + /// The originating context. /// The cancellation token. /// /// The account, if the password is correct. Otherwise, null. /// - internal async ValueTask GetAccountByLoginNameAsync(string loginName, string password, CancellationToken cancellationToken = default) + internal async ValueTask GetAccountByLoginNameAsync(string loginName, string password, EntityFrameworkContextBase? context, CancellationToken cancellationToken = default) { - using var context = this.GetContext(); - return await this.LoadAccountByLoginNameByJsonQueryAsync(loginName, password, context, cancellationToken).ConfigureAwait(false); + using var ownedContext = context is null ? this.GetContext(null) : null; + var origin = context ?? ownedContext!; + return await this.LoadAccountByLoginNameByJsonQueryAsync(loginName, password, origin, cancellationToken).ConfigureAwait(false); } /// @@ -109,14 +115,16 @@ public AccountRepository(IContextAwareRepositoryProvider repositoryProvider, ILo /// /// The login name. /// The password. + /// The originating context. /// The cancellation token. /// The if credentials are valid; otherwise, null. - internal async ValueTask AuthenticateAsync(string loginName, string password, CancellationToken cancellationToken = default) + internal async ValueTask AuthenticateAsync(string loginName, string password, EntityFrameworkContextBase? context, CancellationToken cancellationToken = default) { - using var context = this.GetContext(); + using var ownedContext = context is null ? this.GetContext(null) : null; + var origin = context ?? ownedContext!; cancellationToken.ThrowIfCancellationRequested(); - var accountInfo = await context.Context.Set() + var accountInfo = await origin.Context.Set() .Where(a => a.LoginName == loginName) .Select(a => new { a.PasswordHash, a.State }) .AsNoTracking() @@ -134,39 +142,41 @@ public AccountRepository(IContextAwareRepositoryProvider repositoryProvider, ILo /// Gets the account by login name. /// /// The login name. + /// The originating context. /// The cancellation token. /// /// The account, if exists. Otherwise, null. /// - internal async ValueTask GetAccountByLoginNameAsync(string loginName, CancellationToken cancellationToken = default) + internal async ValueTask GetAccountByLoginNameAsync(string loginName, EntityFrameworkContextBase? context, CancellationToken cancellationToken = default) { - using var context = this.GetContext(); + using var ownedContext = context is null ? this.GetContext(null) : null; + var origin = context ?? ownedContext!; - var accountInfo = await context.Context.Set() + var accountInfo = await origin.Context.Set() .Select(a => new { a.Id, a.LoginName }) .AsNoTracking() .FirstOrDefaultAsync(a => a.LoginName == loginName, cancellationToken).ConfigureAwait(false); if (accountInfo != null) { - return await this.GetByIdAsync(accountInfo.Id, cancellationToken).ConfigureAwait(false); + return await this.GetByIdAsync(accountInfo.Id, origin, cancellationToken).ConfigureAwait(false); } return null; } - private async ValueTask LoadAccountByLoginNameByJsonQueryAsync(string loginName, string password, EntityFrameworkContextBase context, CancellationToken cancellationToken) + private async ValueTask LoadAccountByLoginNameByJsonQueryAsync(string loginName, string password, EntityFrameworkContextBase origin, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - var accountInfo = await context.Context.Set() + var accountInfo = await origin.Context.Set() .Select(a => new { a.Id, a.LoginName, a.PasswordHash }) .AsNoTracking() .FirstOrDefaultAsync(a => a.LoginName == loginName, cancellationToken).ConfigureAwait(false); if (accountInfo != null && BCrypt.Verify(password, accountInfo.PasswordHash)) { - return await this.GetByIdAsync(accountInfo.Id, cancellationToken).ConfigureAwait(false); + return await this.GetByIdAsync(accountInfo.Id, origin, cancellationToken).ConfigureAwait(false); } return null; diff --git a/src/Persistence/EntityFramework/CacheAwareRepositoryProvider.cs b/src/Persistence/EntityFramework/CacheAwareRepositoryProvider.cs index 64cb0aa6d..e64857699 100644 --- a/src/Persistence/EntityFramework/CacheAwareRepositoryProvider.cs +++ b/src/Persistence/EntityFramework/CacheAwareRepositoryProvider.cs @@ -1,4 +1,4 @@ -// +// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // @@ -21,7 +21,7 @@ internal class CacheAwareRepositoryProvider : ICacheAwareRepositoryProvider, ICo { private readonly ILoggerFactory _loggerFactory; - private readonly IRepositoryProvider _nonCachingRepositoryProvider; + private readonly IContextAwareRepositoryProvider _nonCachingRepositoryProvider; private CachingRepositoryProvider _cachingRepositoryProvider; @@ -29,63 +29,89 @@ internal class CacheAwareRepositoryProvider : ICacheAwareRepositoryProvider, ICo /// Initializes a new instance of the class. /// /// The logger factory. - /// The configuration change publisher. + /// The configuration change listener. public CacheAwareRepositoryProvider(ILoggerFactory loggerFactory, IConfigurationChangeListener? configurationChangeListener) { this._loggerFactory = loggerFactory; this._cachingRepositoryProvider = new CachingRepositoryProvider(loggerFactory, this); - this._nonCachingRepositoryProvider = new NonCachingRepositoryProvider(loggerFactory, this, configurationChangeListener, this.ContextStack); + this._nonCachingRepositoryProvider = new NonCachingRepositoryProvider(loggerFactory, this, configurationChangeListener); } /// - public IContextStack ContextStack { get; } = new ContextStack(); + public IRepository? GetRepository(Type objectType) + { + return this._cachingRepositoryProvider.GetRepository(objectType) + ?? this._nonCachingRepositoryProvider.GetRepository(objectType); + } /// - public IRepository? GetRepository(Type objectType) + public IRepository? GetRepository() + where T : class + { + return this._cachingRepositoryProvider.GetRepository() + ?? this._nonCachingRepositoryProvider.GetRepository(); + } + + /// + public TRepository? GetRepository() + where T : class + where TRepository : IRepository { - if (this.ContextStack.GetCurrentContext() is EntityFrameworkContextBase { Context: ITypedContext editContext } + return this._cachingRepositoryProvider.GetRepository() + ?? this._nonCachingRepositoryProvider.GetRepository(); + } + + /// + public IRepository? GetRepository(Type objectType, EntityFrameworkContextBase? context) + { + if (context is { Context: ITypedContext editContext } && editContext.IsIncluded(objectType)) { return this._nonCachingRepositoryProvider.GetRepository(objectType); } - return this._cachingRepositoryProvider.GetRepository(objectType) - ?? this._nonCachingRepositoryProvider.GetRepository(objectType); + return this.GetRepository(objectType); } /// - public IRepository? GetRepository() + public IRepository? GetRepository(EntityFrameworkContextBase? context) where T : class { - if (this.ContextStack.GetCurrentContext() is EntityFrameworkContextBase { Context: ITypedContext editContext } + if (context is { Context: ITypedContext editContext } && (editContext.IsIncluded(typeof(T)) || editContext.IsIncluded(typeof(T).BaseType!))) { return this._nonCachingRepositoryProvider.GetRepository(); } - return this._cachingRepositoryProvider.GetRepository() - ?? this._nonCachingRepositoryProvider.GetRepository(); + return this.GetRepository(); } /// - public TRepository? GetRepository() + public TRepository? GetRepository(EntityFrameworkContextBase? context) where T : class where TRepository : IRepository { - if (this.ContextStack.GetCurrentContext() is EntityFrameworkContextBase { Context: ITypedContext editContext } + if (context is { Context: ITypedContext editContext } && (editContext.IsIncluded(typeof(T)) || editContext.IsIncluded(typeof(T).BaseType!))) { return this._nonCachingRepositoryProvider.GetRepository(); } - return this._cachingRepositoryProvider.GetRepository() - ?? this._nonCachingRepositoryProvider.GetRepository(); + return this.GetRepository(); + } + + /// + public void EnsureCachesForCurrentGameConfiguration(EntityFrameworkContextBase context) + { + this._cachingRepositoryProvider.EnsureCachesForCurrentGameConfiguration(context); } /// public void EnsureCachesForCurrentGameConfiguration() { - this._cachingRepositoryProvider.EnsureCachesForCurrentGameConfiguration(); + // The caches are ensured per originating context (see the overload taking a context) + // and lazily on access. Without an ambient context, there is no "current" configuration + // to ensure here. } /// @@ -117,4 +143,4 @@ public async ValueTask UpdateCachedInstanceAsync(object changedInstance) } } } -} \ No newline at end of file +} diff --git a/src/Persistence/EntityFramework/CachedRepository{T}.cs b/src/Persistence/EntityFramework/CachedRepository{T}.cs index 9c2d938ac..c93dc49b0 100644 --- a/src/Persistence/EntityFramework/CachedRepository{T}.cs +++ b/src/Persistence/EntityFramework/CachedRepository{T}.cs @@ -5,13 +5,14 @@ namespace MUnique.OpenMU.Persistence.EntityFramework; using System.Collections; +using System.Linq; using System.Threading; /// /// A repository which caches all of its data in memory. /// /// The type of the business object. -public class CachedRepository : IRepository +public class CachedRepository : IRepository, IContextAwareRepository where T : class, IIdentifiable { private readonly IDictionary _cache; @@ -42,7 +43,18 @@ async ValueTask IRepository.GetAllAsync(CancellationToken cancellat } /// - public async ValueTask> GetAllAsync(CancellationToken cancellationToken = default) + public ValueTask> GetAllAsync(CancellationToken cancellationToken = default) + { + return this.GetAllAsync(null, cancellationToken); + } + + /// + /// Gets all objects, using the given originating context to load them from the base repository. + /// + /// The originating context, or null. + /// The cancellation token. + /// All objects of the repository. + public async ValueTask> GetAllAsync(EntityFrameworkContextBase? context, CancellationToken cancellationToken = default) { if (this._allLoaded) { @@ -62,7 +74,9 @@ public async ValueTask> GetAllAsync(CancellationToken cancellatio this._loading = true; try { - IEnumerable values = await this.BaseRepository.GetAllAsync(cancellationToken).ConfigureAwait(false); + IEnumerable values = this.BaseRepository is IContextAwareRepository contextAware + ? (await contextAware.GetAllAsync(context, cancellationToken).ConfigureAwait(false)).Cast() + : await this.BaseRepository.GetAllAsync(cancellationToken).ConfigureAwait(false); foreach (var obj in values) { if (!this._cache.ContainsKey(obj.Id)) @@ -82,9 +96,21 @@ public async ValueTask> GetAllAsync(CancellationToken cancellatio } /// - public async ValueTask GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + public ValueTask GetByIdAsync(Guid id, CancellationToken cancellationToken = default) { - await this.GetAllAsync(cancellationToken).ConfigureAwait(false); + return this.GetByIdAsync(id, null, cancellationToken); + } + + /// + /// Gets an object by identifier, using the given originating context to load the data. + /// + /// The identifier. + /// The originating context, or null. + /// The cancellation token. + /// The object with the identifier. + public async ValueTask GetByIdAsync(Guid id, EntityFrameworkContextBase? context, CancellationToken cancellationToken = default) + { + await this.GetAllAsync(context, cancellationToken).ConfigureAwait(false); this._cache.TryGetValue(id, out var result); return result; } @@ -95,6 +121,18 @@ public async ValueTask> GetAllAsync(CancellationToken cancellatio return await this.GetByIdAsync(id, cancellationToken).ConfigureAwait(false); } + /// + async ValueTask IContextAwareRepository.GetAllAsync(EntityFrameworkContextBase? context, CancellationToken cancellationToken) + { + return await this.GetAllAsync(context, cancellationToken).ConfigureAwait(false); + } + + /// + async ValueTask IContextAwareRepository.GetByIdAsync(Guid id, EntityFrameworkContextBase? context, CancellationToken cancellationToken) + { + return await this.GetByIdAsync(id, context, cancellationToken).ConfigureAwait(false); + } + /// public async ValueTask DeleteAsync(object obj) { diff --git a/src/Persistence/EntityFramework/CachingGameConfigurationRepository.cs b/src/Persistence/EntityFramework/CachingGameConfigurationRepository.cs index 0fcbd75c2..a05841ea1 100644 --- a/src/Persistence/EntityFramework/CachingGameConfigurationRepository.cs +++ b/src/Persistence/EntityFramework/CachingGameConfigurationRepository.cs @@ -31,11 +31,11 @@ public CachingGameConfigurationRepository(IContextAwareRepositoryProvider reposi } /// - public override async ValueTask GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + public override async ValueTask GetByIdAsync(Guid id, EntityFrameworkContextBase? context, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); - if (this.RepositoryProvider.ContextStack.GetCurrentContext() is not EntityFrameworkContextBase currentContext) + if (context is not { } currentContext) { throw new InvalidOperationException("There is no current context set."); } @@ -53,9 +53,9 @@ public CachingGameConfigurationRepository(IContextAwareRepositoryProvider reposi } /// - public override async ValueTask> GetAllAsync(CancellationToken cancellationToken = default) + public override async ValueTask> GetAllAsync(EntityFrameworkContextBase? context, CancellationToken cancellationToken = default) { - if (this.RepositoryProvider.ContextStack.GetCurrentContext() is not EntityFrameworkContextBase currentContext) + if (context is not { } currentContext) { throw new InvalidOperationException("There is no current context set."); } @@ -72,7 +72,7 @@ public override async ValueTask> GetAllAsync(Canc configs.ForEach(config => { ((EntityDataContext)currentContext.Context).CurrentGameConfiguration = config; - (this.RepositoryProvider as ICacheAwareRepositoryProvider)?.EnsureCachesForCurrentGameConfiguration(); + this.RepositoryProvider.EnsureCachesForCurrentGameConfiguration(currentContext); }); } finally diff --git a/src/Persistence/EntityFramework/CachingGenericRepository.cs b/src/Persistence/EntityFramework/CachingGenericRepository.cs index cb6bccdf9..6d46d1b5e 100644 --- a/src/Persistence/EntityFramework/CachingGenericRepository.cs +++ b/src/Persistence/EntityFramework/CachingGenericRepository.cs @@ -31,13 +31,13 @@ public CachingGenericRepository(IContextAwareRepositoryProvider repositoryProvid } /// - /// Gets a context to work with. If no context is currently registered at the repository provider, a new one is getting created. + /// Gets a context to work with. If no originating context is given, a new temporary one is getting created. /// + /// The originating context, or null to create a temporary context. /// The context. - protected override EntityFrameworkContextBase GetContext() + protected override EntityFrameworkContextBase GetContext(EntityFrameworkContextBase? origin) { - var context = this.RepositoryProvider.ContextStack.GetCurrentContext() as EntityFrameworkContextBase; - return new CachingEntityFrameworkContext(context?.Context ?? new EntityDataContext(), this.RepositoryProvider, context is null, null, this._loggerFactory.CreateLogger()); + return new CachingEntityFrameworkContext(origin?.Context ?? new EntityDataContext(), this.RepositoryProvider, origin is null, null, this._loggerFactory.CreateLogger()); } /// diff --git a/src/Persistence/EntityFramework/CachingRepositoryProvider.cs b/src/Persistence/EntityFramework/CachingRepositoryProvider.cs index 038830074..e9812fa4d 100644 --- a/src/Persistence/EntityFramework/CachingRepositoryProvider.cs +++ b/src/Persistence/EntityFramework/CachingRepositoryProvider.cs @@ -26,20 +26,21 @@ internal class CachingRepositoryProvider : RepositoryProvider /// The logger factory. /// The parent context aware repository provider. public CachingRepositoryProvider(ILoggerFactory loggerFactory, IContextAwareRepositoryProvider parent) - : base(loggerFactory, null, parent.ContextStack) + : base(loggerFactory, null) { this._parent = parent; } /// - /// Ensures the caches for current game configuration. + /// Ensures the caches for the game configuration of the given originating context. /// It's meant to fill the caches also in . /// - public void EnsureCachesForCurrentGameConfiguration() + /// The originating context which holds the current game configuration. + public override void EnsureCachesForCurrentGameConfiguration(EntityFrameworkContextBase context) { foreach (var repository in this.Repositories.Values.OfType()) { - repository.EnsureCacheForCurrentConfiguration(); + repository.EnsureCacheForCurrentConfiguration(context); } } diff --git a/src/Persistence/EntityFramework/ConfigurationTypeRepository.cs b/src/Persistence/EntityFramework/ConfigurationTypeRepository.cs index d97f96f1b..ad6cdb242 100644 --- a/src/Persistence/EntityFramework/ConfigurationTypeRepository.cs +++ b/src/Persistence/EntityFramework/ConfigurationTypeRepository.cs @@ -1,4 +1,4 @@ -// +// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // @@ -19,7 +19,7 @@ namespace MUnique.OpenMU.Persistence.EntityFramework; /// A repository which gets its data from the , without additionally touching the database. /// /// The data object type. -internal class ConfigurationTypeRepository : IRepository, IConfigurationTypeRepository +internal class ConfigurationTypeRepository : IRepository, IConfigurationTypeRepository, IContextAwareRepository where T : class { private readonly IContextAwareRepositoryProvider _repositoryProvider; @@ -51,26 +51,21 @@ public ConfigurationTypeRepository(IContextAwareRepositoryProvider repositoryPro /// /// Gets all objects by using the to the current . /// + /// The originating context which holds the current game configuration. + /// The cancellation token. /// All objects of the repository. - public ValueTask> GetAllAsync(CancellationToken cancellationToken = default) - { - return ValueTask.FromResult>(this._collectionSelector(this.GetCurrentGameConfiguration())); - } - - /// - async ValueTask IRepository.GetAllAsync(CancellationToken cancellationToken = default) + public ValueTask> GetAllAsync(EntityFrameworkContextBase? context, CancellationToken cancellationToken = default) { - cancellationToken.ThrowIfCancellationRequested(); - return await this.GetAllAsync(cancellationToken).ConfigureAwait(false); + return ValueTask.FromResult>(this._collectionSelector(this.GetCurrentGameConfiguration(context))); } /// - public ValueTask GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + public ValueTask GetByIdAsync(Guid id, EntityFrameworkContextBase? context, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); - this.EnsureCacheForCurrentConfiguration(); + this.EnsureCacheForCurrentConfiguration(context); - var dictionary = this._cache[this.GetCurrentGameConfiguration()]; + var dictionary = this._cache[this.GetCurrentGameConfiguration(context)]; if (dictionary.TryGetValue(id, out var result)) { return ValueTask.FromResult(result); @@ -79,6 +74,44 @@ async ValueTask IRepository.GetAllAsync(CancellationToken cancellat return ValueTask.FromResult(null); } + /// + public ValueTask> GetAllAsync(CancellationToken cancellationToken = default) + { + return this.GetAllAsync((EntityFrameworkContextBase?)null, cancellationToken); + } + + /// + public ValueTask GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + { + return this.GetByIdAsync(id, (EntityFrameworkContextBase?)null, cancellationToken); + } + + /// + async ValueTask IRepository.GetAllAsync(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + return await this.GetAllAsync((EntityFrameworkContextBase?)null, cancellationToken).ConfigureAwait(false); + } + + /// + async ValueTask IRepository.GetByIdAsync(Guid id, CancellationToken cancellationToken) + { + return await this.GetByIdAsync(id, (EntityFrameworkContextBase?)null, cancellationToken).ConfigureAwait(false); + } + + /// + async ValueTask IContextAwareRepository.GetAllAsync(EntityFrameworkContextBase? context, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + return await this.GetAllAsync(context, cancellationToken).ConfigureAwait(false); + } + + /// + async ValueTask IContextAwareRepository.GetByIdAsync(Guid id, EntityFrameworkContextBase? context, CancellationToken cancellationToken) + { + return await this.GetByIdAsync(id, context, cancellationToken).ConfigureAwait(false); + } + /// public async ValueTask DeleteAsync(object obj) { @@ -87,7 +120,7 @@ public async ValueTask DeleteAsync(object obj) return false; } - var gameConfiguration = this.GetCurrentGameConfiguration(); + var gameConfiguration = this.GetCurrentGameConfiguration(null); var collection = this._collectionSelector(gameConfiguration); return collection.Remove(item); } @@ -103,19 +136,14 @@ public async ValueTask DeleteAsync(Guid id) return false; } - /// - async ValueTask IRepository.GetByIdAsync(Guid id, CancellationToken cancellationToken = default) - { - return await this.GetByIdAsync(id, cancellationToken).ConfigureAwait(false); - } - /// - /// Ensures the cache for the current configuration. + /// Ensures the cache for the configuration of the given originating context. /// TODO: Call this at a better place and time - so that we can remove this check before every GetById /// - public void EnsureCacheForCurrentConfiguration() + /// The originating context which holds the current game configuration. + public void EnsureCacheForCurrentConfiguration(EntityFrameworkContextBase? context) { - var configuration = this.GetCurrentGameConfiguration(); + var configuration = this.GetCurrentGameConfiguration(context); if (this._cache.ContainsKey(configuration)) { return; @@ -162,14 +190,14 @@ public void UpdateCachedInstances(object changedInstance) } } - private GameConfiguration GetCurrentGameConfiguration() + private GameConfiguration GetCurrentGameConfiguration(EntityFrameworkContextBase? context) { - var context = (this._repositoryProvider.ContextStack.GetCurrentContext() as CachingEntityFrameworkContext)?.Context as EntityDataContext; - if (context is null) + var dbContext = (context as CachingEntityFrameworkContext)?.Context as EntityDataContext; + if (dbContext is null) { throw new InvalidOperationException("This repository can only be used within an account context."); } - return context.CurrentGameConfiguration ?? throw new InvalidOperationException("There is no current configuration."); + return dbContext.CurrentGameConfiguration ?? throw new InvalidOperationException("There is no current configuration."); } -} \ No newline at end of file +} diff --git a/src/Persistence/EntityFramework/ContextStack.cs b/src/Persistence/EntityFramework/ContextStack.cs deleted file mode 100644 index a65348f29..000000000 --- a/src/Persistence/EntityFramework/ContextStack.cs +++ /dev/null @@ -1,62 +0,0 @@ -// -// Licensed under the MIT License. See LICENSE file in the project root for full license information. -// - -namespace MUnique.OpenMU.Persistence.EntityFramework; - -using System.Threading; - -/// -/// A stack for persistence contexts. -/// -internal sealed class ContextStack : IContextStack -{ - private readonly AsyncLocal> _localStack = new(); - - /// - /// Puts this context on the context stack of the current thread to be used for the upcoming repository actions. - /// If no context is on the context stack of the current thread, a new temporary context will be used for the action. - /// - /// The context. - /// The disposable to end the usage. - public IDisposable UseContext(IContext context) - { - var contextsOfCurrentThread = this._localStack.Value ??= new Stack(); - contextsOfCurrentThread.Push(context); - return new ContextPop(contextsOfCurrentThread); - } - - /// - /// Gets the current context of the current thread. - /// - /// The current context. - public IContext? GetCurrentContext() - { - var contextsOfCurrentThread = this._localStack.Value ??= new Stack(); - if (contextsOfCurrentThread is { Count: > 0 }) - { - return contextsOfCurrentThread.Peek(); - } - - return null; - } - - private sealed class ContextPop : IDisposable - { - private Stack? _stack; - - public ContextPop(Stack stack) - { - this._stack = stack; - } - - public void Dispose() - { - if (this._stack != null) - { - this._stack.Pop(); - this._stack = null; - } - } - } -} \ No newline at end of file diff --git a/src/Persistence/EntityFramework/EntityFrameworkContextBase.cs b/src/Persistence/EntityFramework/EntityFrameworkContextBase.cs index c11a2160b..f55d3d102 100644 --- a/src/Persistence/EntityFramework/EntityFrameworkContextBase.cs +++ b/src/Persistence/EntityFramework/EntityFrameworkContextBase.cs @@ -199,16 +199,14 @@ public async ValueTask DeleteAsync(T obj) where T : class { using var l = await this._lock.LockAsync(cancellationToken); - using var context = this.RepositoryProvider.ContextStack.UseContext(this); - return await this.GetRepository().GetByIdAsync(id, cancellationToken).ConfigureAwait(false); + return (T?)await this.GetRepository(typeof(T)).GetByIdAsync(id, this, cancellationToken).ConfigureAwait(false); } /// public async Task GetByIdAsync(Guid id, Type type, CancellationToken cancellationToken) { using var l = await this._lock.LockAsync(cancellationToken).ConfigureAwait(false); - using var context = this.RepositoryProvider.ContextStack.UseContext(this); - return await this.GetRepository(type).GetByIdAsync(id, cancellationToken).ConfigureAwait(false); + return await this.GetRepository(type).GetByIdAsync(id, this, cancellationToken).ConfigureAwait(false); } /// @@ -216,16 +214,14 @@ public async ValueTask> GetAsync(CancellationToken cancellatio where T : class { using var l = await this._lock.LockAsync(cancellationToken).ConfigureAwait(false); - using var context = this.RepositoryProvider.ContextStack.UseContext(this); - return await this.GetRepository().GetAllAsync(cancellationToken).ConfigureAwait(false); + return (IEnumerable)await this.GetRepository(typeof(T)).GetAllAsync(this, cancellationToken).ConfigureAwait(false); } /// public async ValueTask GetAsync(Type type, CancellationToken cancellationToken) { using var l = await this._lock.LockAsync(cancellationToken).ConfigureAwait(false); - using var context = this.RepositoryProvider.ContextStack.UseContext(this); - return await this.GetRepository(type).GetAllAsync(cancellationToken).ConfigureAwait(false); + return await this.GetRepository(type).GetAllAsync(this, cancellationToken).ConfigureAwait(false); } /// @@ -282,20 +278,9 @@ protected virtual void Dispose(bool dispose) this.Context.Dispose(); } - private IRepository GetRepository() - where T : class - { - if (this.RepositoryProvider.GetRepository() is { } repository) - { - return repository; - } - - throw new RepositoryNotFoundException(typeof(T)); - } - - private IRepository GetRepository(Type type) + private IContextAwareRepository GetRepository(Type type) { - if (this.RepositoryProvider.GetRepository(type) is { } repository) + if (this.RepositoryProvider.GetRepository(type, this) is IContextAwareRepository repository) { return repository; } diff --git a/src/Persistence/EntityFramework/GameConfigurationRepository.cs b/src/Persistence/EntityFramework/GameConfigurationRepository.cs index c07cbe7f8..c963c7f5b 100644 --- a/src/Persistence/EntityFramework/GameConfigurationRepository.cs +++ b/src/Persistence/EntityFramework/GameConfigurationRepository.cs @@ -31,9 +31,9 @@ public GameConfigurationRepository(IContextAwareRepositoryProvider repositoryPro } /// - public override async ValueTask GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + public override async ValueTask GetByIdAsync(Guid id, EntityFrameworkContextBase? context, CancellationToken cancellationToken = default) { - var currentContext = this.RepositoryProvider.ContextStack.GetCurrentContext() as EntityFrameworkContextBase; + var currentContext = context; if (currentContext is null) { throw new InvalidOperationException("There is no current context set."); @@ -58,9 +58,9 @@ public GameConfigurationRepository(IContextAwareRepositoryProvider repositoryPro } /// - public override async ValueTask> GetAllAsync(CancellationToken cancellationToken = default) + public override async ValueTask> GetAllAsync(EntityFrameworkContextBase? context, CancellationToken cancellationToken = default) { - var currentContext = this.RepositoryProvider.ContextStack.GetCurrentContext() as EntityFrameworkContextBase; + var currentContext = context; if (currentContext is null) { throw new InvalidOperationException("There is no current context set."); diff --git a/src/Persistence/EntityFramework/GameServerDefinitionRepository.cs b/src/Persistence/EntityFramework/GameServerDefinitionRepository.cs index 93a73c642..d77f7a0a9 100644 --- a/src/Persistence/EntityFramework/GameServerDefinitionRepository.cs +++ b/src/Persistence/EntityFramework/GameServerDefinitionRepository.cs @@ -28,35 +28,39 @@ public GameServerDefinitionRepository(IContextAwareRepositoryProvider repository } /// - protected override async ValueTask LoadDependentDataAsync(object obj, DbContext currentContext, CancellationToken cancellationToken) + protected override async ValueTask LoadDependentDataAsync(object obj, EntityFrameworkContextBase origin, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); if (obj is GameServerDefinition definition) { - var entityEntry = currentContext.Entry(obj); + var dbContext = origin.Context; + var entityEntry = dbContext.Entry(obj); foreach (var collection in entityEntry.Collections.Where(c => !c.IsLoaded && c.Metadata is INavigation)) { - await this.LoadCollectionAsync(entityEntry, (INavigation)collection.Metadata, currentContext, cancellationToken).ConfigureAwait(false); + await this.LoadCollectionAsync(entityEntry, (INavigation)collection.Metadata, origin, cancellationToken).ConfigureAwait(false); collection.IsLoaded = true; } if (definition.GameConfigurationId.HasValue) { - definition.RawGameConfiguration = - await this.RepositoryProvider.GetRepository()! - .GetByIdAsync(definition.GameConfigurationId.Value, cancellationToken).ConfigureAwait(false); + if (this.RepositoryProvider.GetRepository(typeof(GameConfiguration), origin) is IContextAwareRepository configRepository) + { + definition.RawGameConfiguration = + (GameConfiguration?)await configRepository.GetByIdAsync(definition.GameConfigurationId.Value, origin, cancellationToken).ConfigureAwait(false); + } - if (currentContext is EntityDataContext context) + if (dbContext is EntityDataContext context) { context.CurrentGameConfiguration = definition.RawGameConfiguration; } } - if (definition.ServerConfigurationId.HasValue) + if (definition.ServerConfigurationId.HasValue + && this.RepositoryProvider.GetRepository(typeof(GameServerConfiguration), origin) is IContextAwareRepository serverConfigRepository) { - definition.ServerConfiguration = await this.RepositoryProvider.GetRepository()! - .GetByIdAsync(definition.ServerConfigurationId.Value, cancellationToken).ConfigureAwait(false); + definition.ServerConfiguration = + (GameServerConfiguration?)await serverConfigRepository.GetByIdAsync(definition.ServerConfigurationId.Value, origin, cancellationToken).ConfigureAwait(false); } } } diff --git a/src/Persistence/EntityFramework/GenericRepository.cs b/src/Persistence/EntityFramework/GenericRepository.cs index 7ff82842a..00d711a0b 100644 --- a/src/Persistence/EntityFramework/GenericRepository.cs +++ b/src/Persistence/EntityFramework/GenericRepository.cs @@ -32,12 +32,12 @@ public GenericRepository(IContextAwareRepositoryProvider repositoryProvider, ILo } /// - /// Gets a context to work with. If no context is currently registered at the repository provider, a new one is getting created. + /// Gets a context to work with. If no originating context is given, a new temporary one is getting created. /// + /// The originating context, or null to create a temporary context. /// The context. - protected override EntityFrameworkContextBase GetContext() + protected override EntityFrameworkContextBase GetContext(EntityFrameworkContextBase? origin) { - var context = this.RepositoryProvider.ContextStack.GetCurrentContext() as EntityFrameworkContextBase; - return new EntityFrameworkContext(context?.Context ?? new TypedContext(typeof(T)), this._loggerFactory, this.RepositoryProvider, context is null, this._changeListener); + return new EntityFrameworkContext(origin?.Context ?? new TypedContext(typeof(T)), this._loggerFactory, this.RepositoryProvider, origin is null, this._changeListener); } } \ No newline at end of file diff --git a/src/Persistence/EntityFramework/GenericRepositoryBase.cs b/src/Persistence/EntityFramework/GenericRepositoryBase.cs index f4b61d6b8..2c1cfdccf 100644 --- a/src/Persistence/EntityFramework/GenericRepositoryBase.cs +++ b/src/Persistence/EntityFramework/GenericRepositoryBase.cs @@ -1,4 +1,4 @@ -// +// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // @@ -16,7 +16,7 @@ namespace MUnique.OpenMU.Persistence.EntityFramework; /// Entities are getting eagerly (=completely) loaded automatically. /// /// The type which this repository should manage. -internal abstract class GenericRepositoryBase : IRepository, ILoadByProperty +internal abstract class GenericRepositoryBase : IRepository, ILoadByProperty, IContextAwareRepository where T : class { private readonly ILogger _logger; @@ -58,59 +58,99 @@ public async ValueTask DeleteAsync(Guid id) /// public async ValueTask DeleteAsync(object obj) { - using var context = this.GetContext(); + using var context = this.GetContext(null); return context.Context.Remove(obj) is not null; } /// - async ValueTask IRepository.GetAllAsync(CancellationToken cancellationToken = default) + public ValueTask> GetAllAsync(CancellationToken cancellationToken = default) { - return await this.GetAllAsync(cancellationToken).ConfigureAwait(false); + return this.GetAllAsync((EntityFrameworkContextBase?)null, cancellationToken); } /// - public virtual async ValueTask> GetAllAsync(CancellationToken cancellationToken = default) + public ValueTask GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + { + return this.GetByIdAsync(id, (EntityFrameworkContextBase?)null, cancellationToken); + } + + /// + async ValueTask IRepository.GetAllAsync(CancellationToken cancellationToken) + { + return await this.GetAllAsync((EntityFrameworkContextBase?)null, cancellationToken).ConfigureAwait(false); + } + + /// + async ValueTask IRepository.GetByIdAsync(Guid id, CancellationToken cancellationToken) + { + return await this.GetByIdAsync(id, (EntityFrameworkContextBase?)null, cancellationToken).ConfigureAwait(false); + } + + /// + async ValueTask IContextAwareRepository.GetAllAsync(EntityFrameworkContextBase? context, CancellationToken cancellationToken) + { + return await this.GetAllAsync(context, cancellationToken).ConfigureAwait(false); + } + + /// + async ValueTask IContextAwareRepository.GetByIdAsync(Guid id, EntityFrameworkContextBase? context, CancellationToken cancellationToken) + { + return await this.GetByIdAsync(id, context, cancellationToken).ConfigureAwait(false); + } + + /// + /// Gets all objects, using the given originating context. + /// + /// The originating context, or null to use a temporary context. + /// The cancellation token. + /// All objects of the repository. + public virtual async ValueTask> GetAllAsync(EntityFrameworkContextBase? context, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); - using var context = this.GetContext(); - var result = await context.Context.Set().ToListAsync(cancellationToken).ConfigureAwait(false); - await this.LoadDependentDataAsync(result, context.Context, cancellationToken).ConfigureAwait(false); - var newItems = context.Context.ChangeTracker.Entries().Where(e => e.State == EntityState.Added).Select(e => e.Entity); + using var ownedContext = context is null ? this.GetContext(null) : null; + var origin = context ?? ownedContext!; + + var result = await origin.Context.Set().ToListAsync(cancellationToken).ConfigureAwait(false); + await this.LoadDependentDataAsync(result, origin, cancellationToken).ConfigureAwait(false); + var newItems = origin.Context.ChangeTracker.Entries().Where(e => e.State == EntityState.Added).Select(e => e.Entity); result.AddRange(newItems); return result; } - /// - public virtual async ValueTask GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + /// + /// Gets an object by identifier, using the given originating context. + /// + /// The identifier. + /// The originating context, or null to use a temporary context. + /// The cancellation token. + /// The object with the identifier. + public virtual async ValueTask GetByIdAsync(Guid id, EntityFrameworkContextBase? context, CancellationToken cancellationToken = default) { - using var context = this.GetContext(); + using var ownedContext = context is null ? this.GetContext(null) : null; + var origin = context ?? ownedContext!; - var result = await context.Context.Set().FindAsync(id, cancellationToken).ConfigureAwait(false); + var result = await origin.Context.Set().FindAsync(id, cancellationToken).ConfigureAwait(false); if (result is null) { this._logger.LogDebug("Object with id {Id} could not be found.", id); } else { - await this.LoadDependentDataAsync(result, context.Context, cancellationToken).ConfigureAwait(false); + await this.LoadDependentDataAsync(result, origin, cancellationToken).ConfigureAwait(false); } return result; } /// - async ValueTask IRepository.GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + public async ValueTask LoadByPropertyAsync(IProperty property, object propertyValue, EntityFrameworkContextBase? context, CancellationToken cancellationToken = default) { - return await this.GetByIdAsync(id, cancellationToken).ConfigureAwait(false); - } + using var ownedContext = context is null ? this.GetContext(null) : null; + var origin = context ?? ownedContext!; - /// - public async ValueTask LoadByPropertyAsync(IProperty property, object propertyValue, CancellationToken cancellationToken = default) - { - using var context = this.GetContext(); - var result = (await this.LoadByPropertyInternalAsync(property, propertyValue, context.Context, cancellationToken).ConfigureAwait(false)).OfType().ToList(); - await this.LoadDependentDataAsync(result, context.Context, cancellationToken).ConfigureAwait(false); + var result = (await this.LoadByPropertyInternalAsync(property, propertyValue, origin.Context, cancellationToken).ConfigureAwait(false)).OfType().ToList(); + await this.LoadDependentDataAsync(result, origin, cancellationToken).ConfigureAwait(false); return result; } @@ -134,10 +174,11 @@ protected virtual IEnumerable GetNavigations(EntityEntry entity /// Loads the dependent data of the object from the corresponding repositories. /// /// The object. - /// The current context with which the object got loaded. It is necessary to retrieve the foreign key ids. + /// The originating context with which the object got loaded. It is necessary to retrieve the foreign key ids. /// The cancellation token. - protected virtual async ValueTask LoadDependentDataAsync(object obj, DbContext currentContext, CancellationToken cancellationToken) + protected virtual async ValueTask LoadDependentDataAsync(object obj, EntityFrameworkContextBase origin, CancellationToken cancellationToken) { + var currentContext = origin.Context; var entityEntry = currentContext.Entry(obj); foreach (var navigation in this.GetNavigations(entityEntry).OfType()) @@ -161,7 +202,7 @@ protected virtual async ValueTask LoadDependentDataAsync(object obj, DbContext c } else { - await this.LoadNavigationPropertyAsync(entityEntry, navigation, cancellationToken).ConfigureAwait(false); + await this.LoadNavigationPropertyAsync(entityEntry, navigation, origin, cancellationToken).ConfigureAwait(false); navigation.SetIsLoadedWhenNoTracking(obj); } } @@ -179,7 +220,7 @@ protected virtual async ValueTask LoadDependentDataAsync(object obj, DbContext c continue; } - await this.LoadCollectionAsync(entityEntry, metadata, currentContext, cancellationToken).ConfigureAwait(false); + await this.LoadCollectionAsync(entityEntry, metadata, origin, cancellationToken).ConfigureAwait(false); collection.IsLoaded = true; } } @@ -189,14 +230,14 @@ protected virtual async ValueTask LoadDependentDataAsync(object obj, DbContext c /// Loads the dependent data of the objects from the corresponding repositories. /// /// The loaded objects. - /// The current context with which the objects got loaded. It is necessary to retrieve the foreign key ids. + /// The originating context with which the objects got loaded. It is necessary to retrieve the foreign key ids. /// The cancellation token. /// - protected virtual async ValueTask LoadDependentDataAsync(IEnumerable loadedObjects, DbContext currentContext, CancellationToken cancellationToken) + protected virtual async ValueTask LoadDependentDataAsync(IEnumerable loadedObjects, EntityFrameworkContextBase origin, CancellationToken cancellationToken) { foreach (var obj in loadedObjects) { - await this.LoadDependentDataAsync(obj, currentContext, cancellationToken).ConfigureAwait(false); + await this.LoadDependentDataAsync(obj, origin, cancellationToken).ConfigureAwait(false); } } @@ -205,9 +246,9 @@ protected virtual async ValueTask LoadDependentDataAsync(IEnumerable loadedObjec /// /// The entity entry. /// The navigation. - /// The context. + /// The originating context. /// The cancellation token. - protected virtual async ValueTask LoadCollectionAsync(EntityEntry entityEntry, INavigation navigation, DbContext context, CancellationToken cancellationToken) + protected virtual async ValueTask LoadCollectionAsync(EntityEntry entityEntry, INavigation navigation, EntityFrameworkContextBase origin, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -232,12 +273,12 @@ protected virtual async ValueTask LoadCollectionAsync(EntityEntry entityEntry, I loadStatusAware.LoadingStatus = LoadingStatus.Loading; - if (this.RepositoryProvider.GetRepository(foreignKeyProperty.DeclaringType.ClrType) is ILoadByProperty repository) + if (this.RepositoryProvider.GetRepository(foreignKeyProperty.DeclaringType.ClrType, origin) is ILoadByProperty repository) { var foreignKeyValue = entityEntry.Property(navigation.ForeignKey.PrincipalKey.Properties[0].Name).CurrentValue; if (foreignKeyValue is { }) { - var items = await repository.LoadByPropertyAsync(foreignKeyProperty, foreignKeyValue, cancellationToken).ConfigureAwait(false); + var items = await repository.LoadByPropertyAsync(foreignKeyProperty, foreignKeyValue, origin, cancellationToken).ConfigureAwait(false); foreach (var obj in items) { if (!loadStatusAware.Contains(obj)) @@ -261,8 +302,9 @@ protected virtual async ValueTask LoadCollectionAsync(EntityEntry entityEntry, I /// /// The entity entry from the context. /// The navigation property. + /// The originating context. /// The cancellation token. - protected virtual async ValueTask LoadNavigationPropertyAsync(EntityEntry entityEntry, IReadOnlyNavigation navigation, CancellationToken cancellationToken) + protected virtual async ValueTask LoadNavigationPropertyAsync(EntityEntry entityEntry, IReadOnlyNavigation navigation, EntityFrameworkContextBase origin, CancellationToken cancellationToken) { if (navigation.ForeignKey.DeclaringEntityType != navigation.DeclaringEntityType) { @@ -286,7 +328,7 @@ protected virtual async ValueTask LoadNavigationPropertyAsync(EntityEntry entity IRepository? repository = null; try { - repository = this.RepositoryProvider.GetRepository(navigation.TargetEntityType.ClrType); + repository = this.RepositoryProvider.GetRepository(navigation.TargetEntityType.ClrType, origin); } catch (RepositoryNotFoundException ex) { @@ -295,7 +337,10 @@ protected virtual async ValueTask LoadNavigationPropertyAsync(EntityEntry entity if (repository != null) { - if (!navigation.TrySetClrValue(entityEntry.Entity, await repository.GetByIdAsync(id, cancellationToken).ConfigureAwait(false))) + var loaded = repository is IContextAwareRepository contextAware + ? await contextAware.GetByIdAsync(id, origin, cancellationToken).ConfigureAwait(false) + : await repository.GetByIdAsync(id, cancellationToken).ConfigureAwait(false); + if (!navigation.TrySetClrValue(entityEntry.Entity, loaded)) { this._logger.LogError("Could not find setter for navigation {Navigation}", navigation); } @@ -308,10 +353,11 @@ protected virtual async ValueTask LoadNavigationPropertyAsync(EntityEntry entity } /// - /// Gets a context to work with. If no context is currently registered at the repository provider, a new one is getting created. + /// Gets a context to work with. If no originating context is given, a new temporary one is getting created. /// + /// The originating context, or null to create a temporary context. /// The context. - protected abstract EntityFrameworkContextBase GetContext(); + protected abstract EntityFrameworkContextBase GetContext(EntityFrameworkContextBase? origin); private async ValueTask LoadByPropertyInternalAsync(IProperty property, object propertyValue, DbContext context, CancellationToken cancellationToken) { @@ -327,4 +373,4 @@ private async ValueTask LoadByPropertyInternalAsync(IProperty prope return Enumerable.Empty(); } -} \ No newline at end of file +} diff --git a/src/Persistence/EntityFramework/IConfigurationTypeRepository.cs b/src/Persistence/EntityFramework/IConfigurationTypeRepository.cs index 91bdc57c8..830c757b4 100644 --- a/src/Persistence/EntityFramework/IConfigurationTypeRepository.cs +++ b/src/Persistence/EntityFramework/IConfigurationTypeRepository.cs @@ -10,9 +10,10 @@ namespace MUnique.OpenMU.Persistence.EntityFramework; internal interface IConfigurationTypeRepository { /// - /// Ensures the cache for the current configuration. + /// Ensures the cache for the configuration of the given originating context. /// - void EnsureCacheForCurrentConfiguration(); + /// The originating context which holds the current game configuration. + void EnsureCacheForCurrentConfiguration(EntityFrameworkContextBase? context); /// /// Updates the cached instance. diff --git a/src/Persistence/EntityFramework/IContextAwareRepository.cs b/src/Persistence/EntityFramework/IContextAwareRepository.cs new file mode 100644 index 000000000..e52da933a --- /dev/null +++ b/src/Persistence/EntityFramework/IContextAwareRepository.cs @@ -0,0 +1,38 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Persistence.EntityFramework; + +using System.Collections; +using System.Threading; + +/// +/// Interface for a repository which can operate within an explicitly given originating context, +/// instead of relying on an ambient context. +/// +/// +/// The originating context is the which started the +/// current operation. It provides the to work +/// with and, for configuration data, the current game configuration. When null is passed, +/// a new temporary context is used for the action. +/// +internal interface IContextAwareRepository : IRepository +{ + /// + /// Gets the object by an identifier, using the given originating context. + /// + /// The identifier. + /// The originating context, or null to use a temporary context. + /// The cancellation token. + /// The loaded object, or null if not found. + ValueTask GetByIdAsync(Guid id, EntityFrameworkContextBase? context, CancellationToken cancellationToken = default); + + /// + /// Gets all objects, using the given originating context. + /// + /// The originating context, or null to use a temporary context. + /// The cancellation token. + /// All objects of the repository. + ValueTask GetAllAsync(EntityFrameworkContextBase? context, CancellationToken cancellationToken = default); +} diff --git a/src/Persistence/EntityFramework/IContextAwareRepositoryProvider.cs b/src/Persistence/EntityFramework/IContextAwareRepositoryProvider.cs index dfac9c67a..332ed9fa0 100644 --- a/src/Persistence/EntityFramework/IContextAwareRepositoryProvider.cs +++ b/src/Persistence/EntityFramework/IContextAwareRepositoryProvider.cs @@ -1,13 +1,46 @@ -// +// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // namespace MUnique.OpenMU.Persistence.EntityFramework; /// -/// A which is aware of a current context by -/// implementing . +/// A which can resolve repositories for an explicitly given +/// originating context, instead of relying on an ambient context. /// -internal interface IContextAwareRepositoryProvider : IRepositoryProvider, IContextStackProvider +internal interface IContextAwareRepositoryProvider : IRepositoryProvider { -} \ No newline at end of file + /// + /// Gets the repository of the specified type for the given originating context. + /// + /// Type of the object. + /// The originating context, or null. + /// The repository of the specified type. + IRepository? GetRepository(Type objectType, EntityFrameworkContextBase? context); + + /// + /// Gets the repository of the specified generic type for the given originating context. + /// + /// The generic type. + /// The originating context, or null. + /// The repository of the specified generic type. + IRepository? GetRepository(EntityFrameworkContextBase? context) + where T : class; + + /// + /// Gets the repository of the specified generic type for the given originating context. + /// + /// The generic type. + /// The type of the repository. + /// The originating context, or null. + /// The repository of the specified generic type. + TRepository? GetRepository(EntityFrameworkContextBase? context) + where T : class + where TRepository : IRepository; + + /// + /// Ensures the caches for the game configuration of the given originating context. + /// + /// The originating context which holds the current game configuration. + void EnsureCachesForCurrentGameConfiguration(EntityFrameworkContextBase context); +} diff --git a/src/Persistence/EntityFramework/IContextStack.cs b/src/Persistence/EntityFramework/IContextStack.cs deleted file mode 100644 index 99a927f54..000000000 --- a/src/Persistence/EntityFramework/IContextStack.cs +++ /dev/null @@ -1,25 +0,0 @@ -// -// Licensed under the MIT License. See LICENSE file in the project root for full license information. -// - -namespace MUnique.OpenMU.Persistence.EntityFramework; - -/// -/// Interface for a stack of persistence contexts. -/// -internal interface IContextStack -{ - /// - /// Puts this context on the context stack of the current thread to be used for the upcoming repository actions. - /// If no context is on the context stack of the current thread, a new temporary context will be used for the action. - /// - /// The context. - /// The disposable to end the usage. - IDisposable UseContext(IContext context); - - /// - /// Gets the current context of the current thread. - /// - /// The current context. - IContext? GetCurrentContext(); -} \ No newline at end of file diff --git a/src/Persistence/EntityFramework/IContextStackProvider.cs b/src/Persistence/EntityFramework/IContextStackProvider.cs deleted file mode 100644 index 9d21d551f..000000000 --- a/src/Persistence/EntityFramework/IContextStackProvider.cs +++ /dev/null @@ -1,16 +0,0 @@ -// -// Licensed under the MIT License. See LICENSE file in the project root for full license information. -// - -namespace MUnique.OpenMU.Persistence.EntityFramework; - -/// -/// Interface for a class which provides a . -/// -internal interface IContextStackProvider -{ - /// - /// Gets the context stack. - /// - IContextStack ContextStack { get; } -} \ No newline at end of file diff --git a/src/Persistence/EntityFramework/ILoadByProperty.cs b/src/Persistence/EntityFramework/ILoadByProperty.cs index eceb318a6..3d55be49d 100644 --- a/src/Persistence/EntityFramework/ILoadByProperty.cs +++ b/src/Persistence/EntityFramework/ILoadByProperty.cs @@ -18,9 +18,10 @@ internal interface ILoadByProperty /// /// The property of the object which should be compared. /// The value of the property. + /// The originating context, or null to use a temporary context. /// The cancellation token. /// /// The enumeration of the loaded objects. /// - ValueTask LoadByPropertyAsync(IProperty property, object propertyValue, CancellationToken cancellationToken = default); + ValueTask LoadByPropertyAsync(IProperty property, object propertyValue, EntityFrameworkContextBase? context, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/Persistence/EntityFramework/LetterBodyRepository.cs b/src/Persistence/EntityFramework/LetterBodyRepository.cs index 1f69800bb..ac5c34d04 100644 --- a/src/Persistence/EntityFramework/LetterBodyRepository.cs +++ b/src/Persistence/EntityFramework/LetterBodyRepository.cs @@ -28,17 +28,19 @@ public LetterBodyRepository(IContextAwareRepositoryProvider repositoryProvider, /// Gets the letter body by the id of its header. /// /// The id of its header. + /// The originating context. /// The cancellation token. /// /// The body of the header. /// - public async ValueTask GetBodyByHeaderIdAsync(Guid headerId, CancellationToken cancellationToken = default) + public async ValueTask GetBodyByHeaderIdAsync(Guid headerId, EntityFrameworkContextBase? context, CancellationToken cancellationToken = default) { - using var context = this.GetContext(); - var letterBody = await context.Context.Set().FirstOrDefaultAsync(body => body.HeaderId == headerId, cancellationToken).ConfigureAwait(false); + using var ownedContext = context is null ? this.GetContext(null) : null; + var origin = context ?? ownedContext!; + var letterBody = await origin.Context.Set().FirstOrDefaultAsync(body => body.HeaderId == headerId, cancellationToken).ConfigureAwait(false); if (letterBody is not null) { - await this.LoadDependentDataAsync(letterBody, context.Context, cancellationToken).ConfigureAwait(false); + await this.LoadDependentDataAsync(letterBody, origin, cancellationToken).ConfigureAwait(false); } return letterBody; diff --git a/src/Persistence/EntityFramework/NonCachingRepositoryProvider.cs b/src/Persistence/EntityFramework/NonCachingRepositoryProvider.cs index 082b79d63..e2dbece90 100644 --- a/src/Persistence/EntityFramework/NonCachingRepositoryProvider.cs +++ b/src/Persistence/EntityFramework/NonCachingRepositoryProvider.cs @@ -20,9 +20,8 @@ internal class NonCachingRepositoryProvider : RepositoryProvider /// The logger factory. /// The parent. /// The change publisher. - /// The context stack. - public NonCachingRepositoryProvider(ILoggerFactory loggerFactory, IContextAwareRepositoryProvider? parent, IConfigurationChangeListener? changeListener, IContextStack contextStack) - : base(loggerFactory, changeListener, contextStack) + public NonCachingRepositoryProvider(ILoggerFactory loggerFactory, IContextAwareRepositoryProvider? parent, IConfigurationChangeListener? changeListener) + : base(loggerFactory, changeListener) { this._parent = parent; } diff --git a/src/Persistence/EntityFramework/PersistenceContextProvider.cs b/src/Persistence/EntityFramework/PersistenceContextProvider.cs index ff0b37958..59b60bfe3 100644 --- a/src/Persistence/EntityFramework/PersistenceContextProvider.cs +++ b/src/Persistence/EntityFramework/PersistenceContextProvider.cs @@ -196,7 +196,7 @@ public void ResetCache() /// public IContext CreateNewContext() { - var repositoryProvider = new NonCachingRepositoryProvider(this._loggerFactory, null, this._changeListener, this.RepositoryProvider.ContextStack); + var repositoryProvider = new NonCachingRepositoryProvider(this._loggerFactory, null, this._changeListener); return new EntityFrameworkContext(new EntityDataContext(), this._loggerFactory, repositoryProvider, true, this._changeListener); } @@ -258,7 +258,7 @@ public IContext CreateNewTypedContext(Type editType, bool useCache, DataModel.Co return new CachingEntityFrameworkContext(dbContext, this.RepositoryProvider, this._changeListener, this._loggerFactory.CreateLogger()); } - var repositoryProvider = new NonCachingRepositoryProvider(this._loggerFactory, null, this._changeListener, this.RepositoryProvider.ContextStack); + var repositoryProvider = new NonCachingRepositoryProvider(this._loggerFactory, null, this._changeListener); return new EntityFrameworkContext(dbContext, this._loggerFactory, repositoryProvider, true, this._changeListener); } } \ No newline at end of file diff --git a/src/Persistence/EntityFramework/PlayerContext.cs b/src/Persistence/EntityFramework/PlayerContext.cs index 034d74e8b..52e5669e9 100644 --- a/src/Persistence/EntityFramework/PlayerContext.cs +++ b/src/Persistence/EntityFramework/PlayerContext.cs @@ -28,10 +28,9 @@ public PlayerContext(DbContext context, IContextAwareRepositoryProvider reposito /// public async ValueTask GetLetterBodyByHeaderIdAsync(Guid headerId, CancellationToken cancellationToken = default) { - using var context = this.RepositoryProvider.ContextStack.UseContext(this); - if (this.RepositoryProvider.GetRepository() is { } repository) + if (this.RepositoryProvider.GetRepository(this) is { } repository) { - return await repository.GetBodyByHeaderIdAsync(headerId, cancellationToken).ConfigureAwait(false); + return await repository.GetBodyByHeaderIdAsync(headerId, this, cancellationToken).ConfigureAwait(false); } return null; @@ -52,12 +51,9 @@ public async ValueTask CanSaveLetterAsync(Interfaces.LetterHeader letterHe /// public async ValueTask AuthenticateAsync(string loginName, string password, CancellationToken cancellationToken = default) { - using (this.RepositoryProvider.ContextStack.UseContext(this)) + if (this.RepositoryProvider.GetRepository(this) is { } accountRepository) { - if (this.RepositoryProvider.GetRepository() is { } accountRepository) - { - return await accountRepository.AuthenticateAsync(loginName, password, cancellationToken).ConfigureAwait(false); - } + return await accountRepository.AuthenticateAsync(loginName, password, this, cancellationToken).ConfigureAwait(false); } return null; @@ -66,12 +62,9 @@ public async ValueTask CanSaveLetterAsync(Interfaces.LetterHeader letterHe /// public async ValueTask GetAccountByLoginNameAsync(string loginName, string password, CancellationToken cancellationToken = default) { - using (this.RepositoryProvider.ContextStack.UseContext(this)) + if (this.RepositoryProvider.GetRepository(this) is { } accountRepository) { - if (this.RepositoryProvider.GetRepository() is { } accountRepository) - { - return await accountRepository.GetAccountByLoginNameAsync(loginName, password, cancellationToken).ConfigureAwait(false); - } + return await accountRepository.GetAccountByLoginNameAsync(loginName, password, this, cancellationToken).ConfigureAwait(false); } return null; @@ -80,12 +73,9 @@ public async ValueTask CanSaveLetterAsync(Interfaces.LetterHeader letterHe /// public async ValueTask GetAccountByLoginNameAsync(string loginName, CancellationToken cancellationToken = default) { - using (this.RepositoryProvider.ContextStack.UseContext(this)) + if (this.RepositoryProvider.GetRepository(this) is { } accountRepository) { - if (this.RepositoryProvider.GetRepository() is { } accountRepository) - { - return await accountRepository.GetAccountByLoginNameAsync(loginName, cancellationToken).ConfigureAwait(false); - } + return await accountRepository.GetAccountByLoginNameAsync(loginName, this, cancellationToken).ConfigureAwait(false); } return null; @@ -94,21 +84,15 @@ public async ValueTask CanSaveLetterAsync(Interfaces.LetterHeader letterHe /// public async ValueTask> GetAccountsOrderedByLoginNameAsync(int skip, int count, CancellationToken cancellationToken = default) { - using (this.RepositoryProvider.ContextStack.UseContext(this)) - { - return await this.Context.Set().AsNoTracking().OrderBy(a => a.LoginName).Skip(skip).Take(count).ToListAsync(cancellationToken).ConfigureAwait(false); - } + return await this.Context.Set().AsNoTracking().OrderBy(a => a.LoginName).Skip(skip).Take(count).ToListAsync(cancellationToken).ConfigureAwait(false); } /// public async ValueTask GetAccountByCharacterNameAsync(string characterName, CancellationToken cancellationToken = default) { - using (this.RepositoryProvider.ContextStack.UseContext(this)) + if (this.RepositoryProvider.GetRepository(this) is { } accountRepository) { - if (this.RepositoryProvider.GetRepository() is { } accountRepository) - { - return await accountRepository.GetAccountByCharacterNameAsync(characterName, cancellationToken).ConfigureAwait(false); - } + return await accountRepository.GetAccountByCharacterNameAsync(characterName, this, cancellationToken).ConfigureAwait(false); } return null; diff --git a/src/Persistence/EntityFramework/RepositoryProvider.cs b/src/Persistence/EntityFramework/RepositoryProvider.cs index a65a6d556..53b0058c3 100644 --- a/src/Persistence/EntityFramework/RepositoryProvider.cs +++ b/src/Persistence/EntityFramework/RepositoryProvider.cs @@ -17,19 +17,12 @@ internal class RepositoryProvider : BaseRepositoryProvider, IContextAwareReposit /// /// The logger factory. /// The change publisher. - /// The context stack. - public RepositoryProvider(ILoggerFactory loggerFactory, IConfigurationChangeListener? changeListener, IContextStack contextStack) + public RepositoryProvider(ILoggerFactory loggerFactory, IConfigurationChangeListener? changeListener) { this.LoggerFactory = loggerFactory; this.ChangeListener = changeListener; - this.ContextStack = contextStack; } - /// - /// Gets the context stack. When loading an object, the current context should be pushed onto the stack. - /// - public IContextStack ContextStack { get; } - /// /// Gets the logger factory. /// @@ -40,6 +33,33 @@ public RepositoryProvider(ILoggerFactory loggerFactory, IConfigurationChangeList /// protected IConfigurationChangeListener? ChangeListener { get; } + /// + public virtual IRepository? GetRepository(Type objectType, EntityFrameworkContextBase? context) + { + return this.GetRepository(objectType); + } + + /// + public virtual IRepository? GetRepository(EntityFrameworkContextBase? context) + where T : class + { + return this.GetRepository(); + } + + /// + public virtual TRepository? GetRepository(EntityFrameworkContextBase? context) + where T : class + where TRepository : IRepository + { + return this.GetRepository(); + } + + /// + public virtual void EnsureCachesForCurrentGameConfiguration(EntityFrameworkContextBase context) + { + // No caches at this level. Caching providers override this. + } + /// /// Creates the generic repository for the specified type. /// From c6d24c6b85618784e79d01495f63353e9b5b9ce8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 7 Jun 2026 08:15:12 +0000 Subject: [PATCH 2/3] Fix accessibility error in CachedRepository: context-aware overloads must be internal CachedRepository is a public class, so its public methods cannot expose the internal type EntityFrameworkContextBase. The two context-aware overloads added during the ambient-stack removal are changed from public to internal; they are called only within this assembly. https://claude.ai/code/session_015sGcQwyYMwppSjbP1uD66j --- src/Persistence/EntityFramework/CachedRepository{T}.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Persistence/EntityFramework/CachedRepository{T}.cs b/src/Persistence/EntityFramework/CachedRepository{T}.cs index c93dc49b0..50a845404 100644 --- a/src/Persistence/EntityFramework/CachedRepository{T}.cs +++ b/src/Persistence/EntityFramework/CachedRepository{T}.cs @@ -54,7 +54,7 @@ public ValueTask> GetAllAsync(CancellationToken cancellationToken /// The originating context, or null. /// The cancellation token. /// All objects of the repository. - public async ValueTask> GetAllAsync(EntityFrameworkContextBase? context, CancellationToken cancellationToken = default) + internal async ValueTask> GetAllAsync(EntityFrameworkContextBase? context, CancellationToken cancellationToken = default) { if (this._allLoaded) { @@ -108,7 +108,7 @@ public async ValueTask> GetAllAsync(EntityFrameworkContextBase? c /// The originating context, or null. /// The cancellation token. /// The object with the identifier. - public async ValueTask GetByIdAsync(Guid id, EntityFrameworkContextBase? context, CancellationToken cancellationToken = default) + internal async ValueTask GetByIdAsync(Guid id, EntityFrameworkContextBase? context, CancellationToken cancellationToken = default) { await this.GetAllAsync(context, cancellationToken).ConfigureAwait(false); this._cache.TryGetValue(id, out var result); From fb1e4fddcf115ce6ceae9fa47a1b9dd02b3df2e0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 7 Jun 2026 21:18:04 +0000 Subject: [PATCH 3/3] Address Gemini review: fix null-context crashes in 4 repositories - AccountRepository: guard EnsureCachesForCurrentGameConfiguration behind a CurrentGameConfiguration null-check so temporary contexts don't crash. - ConfigurationTypeRepository.DeleteAsync(object): throw NotSupportedException instead of calling GetCurrentGameConfiguration(null) which always throws. - CachingGameConfigurationRepository.GetByIdAsync/GetAllAsync: use the GetContext(null) fallback rather than throwing when context is null, so the parameterless IRepository contract is honoured. - GameConfigurationRepository.GetByIdAsync/GetAllAsync: same fallback fix. https://claude.ai/code/session_015sGcQwyYMwppSjbP1uD66j --- .../EntityFramework/AccountRepository.cs | 5 ++++- .../CachingGameConfigurationRepository.cs | 12 ++++-------- .../EntityFramework/ConfigurationTypeRepository.cs | 11 ++--------- .../EntityFramework/GameConfigurationRepository.cs | 14 ++++---------- 4 files changed, 14 insertions(+), 28 deletions(-) diff --git a/src/Persistence/EntityFramework/AccountRepository.cs b/src/Persistence/EntityFramework/AccountRepository.cs index 4de5a1321..2b72dc494 100644 --- a/src/Persistence/EntityFramework/AccountRepository.cs +++ b/src/Persistence/EntityFramework/AccountRepository.cs @@ -35,7 +35,10 @@ public AccountRepository(IContextAwareRepositoryProvider repositoryProvider, ILo using var ownedContext = context is null ? this.GetContext(null) : null; var origin = context ?? ownedContext!; - this.RepositoryProvider.EnsureCachesForCurrentGameConfiguration(origin); + if (origin.Context is EntityDataContext { CurrentGameConfiguration: not null }) + { + this.RepositoryProvider.EnsureCachesForCurrentGameConfiguration(origin); + } await origin.Context.Database.OpenConnectionAsync(cancellationToken).ConfigureAwait(false); try diff --git a/src/Persistence/EntityFramework/CachingGameConfigurationRepository.cs b/src/Persistence/EntityFramework/CachingGameConfigurationRepository.cs index a05841ea1..6cf7e2f63 100644 --- a/src/Persistence/EntityFramework/CachingGameConfigurationRepository.cs +++ b/src/Persistence/EntityFramework/CachingGameConfigurationRepository.cs @@ -35,10 +35,8 @@ public CachingGameConfigurationRepository(IContextAwareRepositoryProvider reposi { cancellationToken.ThrowIfCancellationRequested(); - if (context is not { } currentContext) - { - throw new InvalidOperationException("There is no current context set."); - } + using var ownedContext = context is null ? this.GetContext(null) : null; + var currentContext = context ?? ownedContext!; var database = currentContext.Context.Database; await database.OpenConnectionAsync(cancellationToken).ConfigureAwait(false); @@ -55,10 +53,8 @@ public CachingGameConfigurationRepository(IContextAwareRepositoryProvider reposi /// public override async ValueTask> GetAllAsync(EntityFrameworkContextBase? context, CancellationToken cancellationToken = default) { - if (context is not { } currentContext) - { - throw new InvalidOperationException("There is no current context set."); - } + using var ownedContext = context is null ? this.GetContext(null) : null; + var currentContext = context ?? ownedContext!; var database = currentContext.Context.Database; await database.OpenConnectionAsync(cancellationToken).ConfigureAwait(false); diff --git a/src/Persistence/EntityFramework/ConfigurationTypeRepository.cs b/src/Persistence/EntityFramework/ConfigurationTypeRepository.cs index ad6cdb242..4ac4b2a3e 100644 --- a/src/Persistence/EntityFramework/ConfigurationTypeRepository.cs +++ b/src/Persistence/EntityFramework/ConfigurationTypeRepository.cs @@ -113,16 +113,9 @@ async ValueTask IContextAwareRepository.GetAllAsync(EntityFramework } /// - public async ValueTask DeleteAsync(object obj) + public ValueTask DeleteAsync(object obj) { - if (obj is not T item) - { - return false; - } - - var gameConfiguration = this.GetCurrentGameConfiguration(null); - var collection = this._collectionSelector(gameConfiguration); - return collection.Remove(item); + throw new NotSupportedException("Deleting configuration types directly through the repository is not supported. Delete via the owning IContext instead."); } /// diff --git a/src/Persistence/EntityFramework/GameConfigurationRepository.cs b/src/Persistence/EntityFramework/GameConfigurationRepository.cs index c963c7f5b..41371a6ad 100644 --- a/src/Persistence/EntityFramework/GameConfigurationRepository.cs +++ b/src/Persistence/EntityFramework/GameConfigurationRepository.cs @@ -33,11 +33,8 @@ public GameConfigurationRepository(IContextAwareRepositoryProvider repositoryPro /// public override async ValueTask GetByIdAsync(Guid id, EntityFrameworkContextBase? context, CancellationToken cancellationToken = default) { - var currentContext = context; - if (currentContext is null) - { - throw new InvalidOperationException("There is no current context set."); - } + using var ownedContext = context is null ? this.GetContext(null) : null; + var currentContext = context ?? ownedContext!; var database = currentContext.Context.Database; await database.OpenConnectionAsync(cancellationToken).ConfigureAwait(false); @@ -60,11 +57,8 @@ public GameConfigurationRepository(IContextAwareRepositoryProvider repositoryPro /// public override async ValueTask> GetAllAsync(EntityFrameworkContextBase? context, CancellationToken cancellationToken = default) { - var currentContext = context; - if (currentContext is null) - { - throw new InvalidOperationException("There is no current context set."); - } + using var ownedContext = context is null ? this.GetContext(null) : null; + var currentContext = context ?? ownedContext!; var database = currentContext.Context.Database; await database.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);