// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // namespace MUnique.OpenMU.Persistence.EntityFramework; using System.Diagnostics; using System.Threading; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using MUnique.OpenMU.Persistence.EntityFramework.Model; using Nito.Disposables; using Npgsql; /// /// The persistence context provider for the persistence implemented with entity framework core. /// public class PersistenceContextProvider : IMigratableDatabaseContextProvider { private readonly ILoggerFactory _loggerFactory; private IConfigurationChangeListener? _changeListener; /// /// Initializes a new instance of the class. /// /// The logger factory. /// The change publisher. public PersistenceContextProvider(ILoggerFactory loggerFactory, IConfigurationChangeListener? changeListener) { this._loggerFactory = loggerFactory; this._changeListener = changeListener; this.RepositoryProvider = new CacheAwareRepositoryProvider(loggerFactory, changeListener); } /// IRepositoryProvider IPersistenceContextProvider.RepositoryProvider => this.RepositoryProvider; /// /// Gets the repository provider. /// /// /// The repository provider. /// internal CacheAwareRepositoryProvider RepositoryProvider { get; private set; } /// public async Task IsDatabaseUpToDateAsync(CancellationToken cancellationToken = default) { try { await using var installationContext = new EntityDataContext(); return !(await installationContext.Database.GetPendingMigrationsAsync(cancellationToken).ConfigureAwait(false)).Any(); } catch { return false; } } /// /// Applies all pending updates to the database schema. /// public async Task ApplyAllPendingUpdatesAsync() { await using var installationContext = new EntityDataContext(); await installationContext.Database.MigrateAsync().ConfigureAwait(false); } /// /// Waits until all database updates are applied. /// /// The cancellation token. public async Task WaitForUpdatedDatabaseAsync(CancellationToken cancellationToken = default) { while (!await this.DatabaseExistsAsync(cancellationToken).ConfigureAwait(false) || !await this.IsDatabaseUpToDateAsync(cancellationToken).ConfigureAwait(false)) { await Task.Delay(3000, cancellationToken).ConfigureAwait(false); } while (!await this.ConfigurationExistsAsync(cancellationToken).ConfigureAwait(false)) { await Task.Delay(3000, cancellationToken).ConfigureAwait(false); } await Task.Delay(5000, cancellationToken).ConfigureAwait(false); } /// /// Determines if a exists on the database. /// /// The cancellation token. /// True, if a exists; Otherwise, false. public async Task ConfigurationExistsAsync(CancellationToken cancellationToken = default) { try { await using var installationContext = new EntityDataContext(); return await installationContext.Set().AnyAsync(cancellationToken).ConfigureAwait(false); } catch { return false; } } /// public async Task DatabaseExistsAsync(CancellationToken cancellationToken = default) { try { await using var installationContext = new EntityDataContext(); return (await installationContext.Database.GetAppliedMigrationsAsync(cancellationToken).ConfigureAwait(false)).Any(); } catch { return false; } } /// public async Task CanConnectToDatabaseAsync(CancellationToken cancellationToken = default) { try { await using var installationContext = new EntityDataContext(); return await installationContext.Database.CanConnectAsync(cancellationToken).ConfigureAwait(false); } catch { return false; } } /// public async Task ShouldDoAutoSchemaUpdateAsync(CancellationToken cancellationToken = default) { try { await using var installationContext = new EntityDataContext(); return await installationContext.Database.SqlQueryRaw( """ SELECT "AutoUpdateSchema" as "Value" FROM config."SystemConfiguration" """).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); } catch (Exception) { return false; } } /// /// Recreates the database by deleting and creating it again. /// /// /// If (the default), the database is dropped and created again from scratch. /// If , the existing database is kept and only its schema is built via /// migrations — required when the database is provisioned externally and the connecting role is /// not permitted to create or drop databases. /// /// The disposable that should be disposed of when the data creation process is finished. public async Task ReCreateDatabaseAsync(bool dropExistingDatabase = true) { var changePublisher = this._changeListener; this._changeListener = null; try { if (dropExistingDatabase) { try { await using var installationContext = new EntityDataContext(); await installationContext.Database.EnsureDeletedAsync().ConfigureAwait(false); } catch (NpgsqlException) { // That's expected for a fresh database } } await this.ApplyAllPendingUpdatesAsync().ConfigureAwait(false); // We create a new repository provider so that the previously loaded data is not effective anymore. this.ResetCache(); } catch { this._changeListener = changePublisher; } return new Disposable(() => { this._changeListener = changePublisher; }); } /// /// Resets the cache of this instance. /// public void ResetCache() { this.RepositoryProvider = new CacheAwareRepositoryProvider(this._loggerFactory, this._changeListener); } /// public IContext CreateNewContext() { var repositoryProvider = new NonCachingRepositoryProvider(this._loggerFactory, null, this._changeListener, this.RepositoryProvider.ContextStack); return new EntityFrameworkContext(new EntityDataContext(), this._loggerFactory, repositoryProvider, true, this._changeListener); } /// public IContext CreateNewContext(DataModel.Configuration.GameConfiguration gameConfiguration) { return new CachingEntityFrameworkContext( new EntityDataContext { CurrentGameConfiguration = gameConfiguration as GameConfiguration }, this.RepositoryProvider, this._changeListener, this._loggerFactory.CreateLogger()); } /// public IPlayerContext CreateNewPlayerContext(DataModel.Configuration.GameConfiguration gameConfiguration) { return new PlayerContext(new AccountContext { CurrentGameConfiguration = gameConfiguration as GameConfiguration }, this.RepositoryProvider, this._loggerFactory.CreateLogger()); } /// public IConfigurationContext CreateNewConfigurationContext() { return new GameConfigurationContext(this.RepositoryProvider, this._loggerFactory.CreateLogger()); } /// public IContext CreateNewTradeContext() { return new CachingEntityFrameworkContext(new TradeContext(), this.RepositoryProvider, null, this._loggerFactory.CreateLogger()); } /// public IFriendServerContext CreateNewFriendServerContext() { return new FriendServerContext(new FriendContext(), this.RepositoryProvider, this._loggerFactory.CreateLogger()); } /// public IGuildServerContext CreateNewGuildContext() { return new GuildServerContext(new GuildContext(), this.RepositoryProvider, this._loggerFactory.CreateLogger()); } /// public IContext CreateNewTypedContext(Type editType, bool useCache, DataModel.Configuration.GameConfiguration? gameConfiguration = null) { if (!editType.IsConfigurationType() && gameConfiguration is null) { Debug.WriteLine($"Non-configuration type {editType} without game configuration"); } if (useCache && gameConfiguration is null) { throw new ArgumentNullException(nameof(gameConfiguration), "When cache should be used, the game configuration must be provided."); } var dbContext = new TypedContext(editType) { CurrentGameConfiguration = gameConfiguration as GameConfiguration }; if (useCache) { return new CachingEntityFrameworkContext(dbContext, this.RepositoryProvider, this._changeListener, this._loggerFactory.CreateLogger()); } var repositoryProvider = new NonCachingRepositoryProvider(this._loggerFactory, null, this._changeListener, this.RepositoryProvider.ContextStack); return new EntityFrameworkContext(dbContext, this._loggerFactory, repositoryProvider, true, this._changeListener); } }