Support externally-provisioned databases (opt-in, no drop/create) (cherry picked from commit 2e117c26764483fb4ee3de9540c9bf7a647d8067)
274 lines
10 KiB
C#
274 lines
10 KiB
C#
// <copyright file="PersistenceContextProvider.cs" company="MUnique">
|
|
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
|
// </copyright>
|
|
|
|
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;
|
|
|
|
/// <summary>
|
|
/// The persistence context provider for the persistence implemented with entity framework core.
|
|
/// </summary>
|
|
public class PersistenceContextProvider : IMigratableDatabaseContextProvider
|
|
{
|
|
private readonly ILoggerFactory _loggerFactory;
|
|
private IConfigurationChangeListener? _changeListener;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="PersistenceContextProvider" /> class.
|
|
/// </summary>
|
|
/// <param name="loggerFactory">The logger factory.</param>
|
|
/// <param name="changeListener">The change publisher.</param>
|
|
public PersistenceContextProvider(ILoggerFactory loggerFactory, IConfigurationChangeListener? changeListener)
|
|
{
|
|
this._loggerFactory = loggerFactory;
|
|
this._changeListener = changeListener;
|
|
this.RepositoryProvider = new CacheAwareRepositoryProvider(loggerFactory, changeListener);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
IRepositoryProvider IPersistenceContextProvider.RepositoryProvider => this.RepositoryProvider;
|
|
|
|
/// <summary>
|
|
/// Gets the repository provider.
|
|
/// </summary>
|
|
/// <value>
|
|
/// The repository provider.
|
|
/// </value>
|
|
internal CacheAwareRepositoryProvider RepositoryProvider { get; private set; }
|
|
|
|
/// <inheritdoc />
|
|
public async Task<bool> IsDatabaseUpToDateAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
await using var installationContext = new EntityDataContext();
|
|
return !(await installationContext.Database.GetPendingMigrationsAsync(cancellationToken).ConfigureAwait(false)).Any();
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies all pending updates to the database schema.
|
|
/// </summary>
|
|
public async Task ApplyAllPendingUpdatesAsync()
|
|
{
|
|
await using var installationContext = new EntityDataContext();
|
|
await installationContext.Database.MigrateAsync().ConfigureAwait(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Waits until all database updates are applied.
|
|
/// </summary>
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines if a <see cref="GameConfiguration"/> exists on the database.
|
|
/// </summary>
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
/// <returns><c>True</c>, if a <see cref="GameConfiguration"/> exists; Otherwise, <c>false</c>.</returns>
|
|
public async Task<bool> ConfigurationExistsAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
await using var installationContext = new EntityDataContext();
|
|
return await installationContext.Set<GameConfiguration>().AnyAsync(cancellationToken).ConfigureAwait(false);
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<bool> DatabaseExistsAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
await using var installationContext = new EntityDataContext();
|
|
return (await installationContext.Database.GetAppliedMigrationsAsync(cancellationToken).ConfigureAwait(false)).Any();
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<bool> CanConnectToDatabaseAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
await using var installationContext = new EntityDataContext();
|
|
return await installationContext.Database.CanConnectAsync(cancellationToken).ConfigureAwait(false);
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<bool> ShouldDoAutoSchemaUpdateAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
await using var installationContext = new EntityDataContext();
|
|
return await installationContext.Database.SqlQueryRaw<bool>(
|
|
"""
|
|
SELECT "AutoUpdateSchema" as "Value" FROM config."SystemConfiguration"
|
|
""").FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Recreates the database by deleting and creating it again.
|
|
/// </summary>
|
|
/// <param name="dropExistingDatabase">
|
|
/// If <see langword="true"/> (the default), the database is dropped and created again from scratch.
|
|
/// If <see langword="false"/>, 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.
|
|
/// </param>
|
|
/// <returns>The disposable that should be disposed of when the data creation process is finished.</returns>
|
|
public async Task<IDisposable> 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;
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resets the cache of this instance.
|
|
/// </summary>
|
|
public void ResetCache()
|
|
{
|
|
this.RepositoryProvider = new CacheAwareRepositoryProvider(this._loggerFactory, this._changeListener);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public IContext CreateNewContext(DataModel.Configuration.GameConfiguration gameConfiguration)
|
|
{
|
|
return new CachingEntityFrameworkContext(
|
|
new EntityDataContext { CurrentGameConfiguration = gameConfiguration as GameConfiguration },
|
|
this.RepositoryProvider,
|
|
this._changeListener,
|
|
this._loggerFactory.CreateLogger<CachingEntityFrameworkContext>());
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public IPlayerContext CreateNewPlayerContext(DataModel.Configuration.GameConfiguration gameConfiguration)
|
|
{
|
|
return new PlayerContext(new AccountContext { CurrentGameConfiguration = gameConfiguration as GameConfiguration }, this.RepositoryProvider, this._loggerFactory.CreateLogger<PlayerContext>());
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public IConfigurationContext CreateNewConfigurationContext()
|
|
{
|
|
return new GameConfigurationContext(this.RepositoryProvider, this._loggerFactory.CreateLogger<GameConfigurationContext>());
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public IContext CreateNewTradeContext()
|
|
{
|
|
return new CachingEntityFrameworkContext(new TradeContext(), this.RepositoryProvider, null, this._loggerFactory.CreateLogger<CachingEntityFrameworkContext>());
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public IFriendServerContext CreateNewFriendServerContext()
|
|
{
|
|
return new FriendServerContext(new FriendContext(), this.RepositoryProvider, this._loggerFactory.CreateLogger<FriendServerContext>());
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public IGuildServerContext CreateNewGuildContext()
|
|
{
|
|
return new GuildServerContext(new GuildContext(), this.RepositoryProvider, this._loggerFactory.CreateLogger<GuildServerContext>());
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
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<CachingEntityFrameworkContext>());
|
|
}
|
|
|
|
var repositoryProvider = new NonCachingRepositoryProvider(this._loggerFactory, null, this._changeListener, this.RepositoryProvider.ContextStack);
|
|
return new EntityFrameworkContext(dbContext, this._loggerFactory, repositoryProvider, true, this._changeListener);
|
|
}
|
|
} |