diff --git a/src/Persistence/EntityFramework/AccountRepository.cs b/src/Persistence/EntityFramework/AccountRepository.cs index f5f226025..2b72dc494 100644 --- a/src/Persistence/EntityFramework/AccountRepository.cs +++ b/src/Persistence/EntityFramework/AccountRepository.cs @@ -28,30 +28,35 @@ 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); + if (origin.Context is EntityDataContext { CurrentGameConfiguration: not null }) + { + 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 +64,7 @@ public AccountRepository(IContextAwareRepositoryProvider repositoryProvider, ILo } finally { - await context.Context.Database.CloseConnectionAsync().ConfigureAwait(false); + await origin.Context.Database.CloseConnectionAsync().ConfigureAwait(false); } } @@ -67,23 +72,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 +101,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 +118,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 +145,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..50a845404 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. + internal 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. + 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); 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..6cf7e2f63 100644 --- a/src/Persistence/EntityFramework/CachingGameConfigurationRepository.cs +++ b/src/Persistence/EntityFramework/CachingGameConfigurationRepository.cs @@ -31,14 +31,12 @@ 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) - { - 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); @@ -53,12 +51,10 @@ 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) - { - 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); @@ -72,7 +68,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..4ac4b2a3e 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); @@ -80,16 +75,47 @@ async ValueTask IRepository.GetAllAsync(CancellationToken cancellat } /// - public async ValueTask DeleteAsync(object obj) + public ValueTask> GetAllAsync(CancellationToken cancellationToken = default) { - if (obj is not T item) - { - return false; - } + 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); + } - var gameConfiguration = this.GetCurrentGameConfiguration(); - var collection = this._collectionSelector(gameConfiguration); - return collection.Remove(item); + /// + async ValueTask IContextAwareRepository.GetByIdAsync(Guid id, EntityFrameworkContextBase? context, CancellationToken cancellationToken) + { + return await this.GetByIdAsync(id, context, cancellationToken).ConfigureAwait(false); + } + + /// + public ValueTask DeleteAsync(object obj) + { + throw new NotSupportedException("Deleting configuration types directly through the repository is not supported. Delete via the owning IContext instead."); } /// @@ -103,19 +129,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 +183,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..41371a6ad 100644 --- a/src/Persistence/EntityFramework/GameConfigurationRepository.cs +++ b/src/Persistence/EntityFramework/GameConfigurationRepository.cs @@ -31,13 +31,10 @@ 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; - 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); @@ -58,13 +55,10 @@ 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; - 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); 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. ///