//
// 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);
}
}