baseline: OpenMU upstream b5a0961 (fresh source)
This commit is contained in:
32
src/Persistence/EntityFramework/AccountContext.cs
Normal file
32
src/Persistence/EntityFramework/AccountContext.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
// <copyright file="AccountContext.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 Microsoft.EntityFrameworkCore;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework.Model;
|
||||
|
||||
/// <summary>
|
||||
/// A context which is used by each account. This context does not track and save configuration data.
|
||||
/// </summary>
|
||||
internal class AccountContext : EntityDataContext
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
var types = modelBuilder.Model.GetEntityTypes().ToList();
|
||||
var configTypes = types.Where(t => t.ClrType.BaseType?.Namespace?.Contains("Configuration") ?? (t.ClrType.BaseType?.Namespace?.Contains("AttributeSystem") ?? false)).ToList();
|
||||
foreach (var type in configTypes)
|
||||
{
|
||||
modelBuilder.Ignore(type.ClrType);
|
||||
}
|
||||
|
||||
modelBuilder.Ignore(typeof(AttributeDefinition));
|
||||
|
||||
modelBuilder.Ignore<Guild>();
|
||||
modelBuilder.Ignore<GuildMember>();
|
||||
modelBuilder.Entity<Item>(b => b.Ignore(e => e.RawItemStorage));
|
||||
}
|
||||
}
|
||||
174
src/Persistence/EntityFramework/AccountRepository.cs
Normal file
174
src/Persistence/EntityFramework/AccountRepository.cs
Normal file
@@ -0,0 +1,174 @@
|
||||
// <copyright file="AccountRepository.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.Linq;
|
||||
using System.Threading;
|
||||
using BCrypt.Net;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework.Json;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Repository for accounts.
|
||||
/// </summary>
|
||||
internal class AccountRepository : CachingGenericRepository<Account>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AccountRepository" /> class.
|
||||
/// </summary>
|
||||
/// <param name="repositoryProvider">The repository provider.</param>
|
||||
/// <param name="loggerFactory">The logger factory.</param>
|
||||
public AccountRepository(IContextAwareRepositoryProvider repositoryProvider, ILoggerFactory loggerFactory)
|
||||
: base(repositoryProvider, loggerFactory)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask<Account?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
(this.RepositoryProvider as ICacheAwareRepositoryProvider)?.EnsureCachesForCurrentGameConfiguration();
|
||||
|
||||
using var context = this.GetContext();
|
||||
await context.Context.Database.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var accountEntry = context.Context.ChangeTracker.Entries<Account>().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);
|
||||
}
|
||||
|
||||
var objectLoader = new AccountJsonObjectLoader();
|
||||
account = await objectLoader.LoadObjectAsync<Account>(id, context.Context, cancellationToken).ConfigureAwait(false);
|
||||
if (account != null && !(context.Context.Entry(account) is { } entry && entry.State != EntityState.Detached))
|
||||
{
|
||||
context.Context.Attach(account);
|
||||
}
|
||||
}
|
||||
|
||||
return account;
|
||||
}
|
||||
finally
|
||||
{
|
||||
await context.Context.Database.CloseConnectionAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the account by character name.
|
||||
/// </summary>
|
||||
/// <param name="characterName">The character name.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>
|
||||
/// The account; otherwise, null.
|
||||
/// </returns>
|
||||
internal async ValueTask<DataModel.Entities.Account?> GetAccountByCharacterNameAsync(string characterName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
using var context = this.GetContext();
|
||||
var accountInfo = await context.Context.Set<Account>()
|
||||
.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 null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the account by login name if the password is correct.
|
||||
/// </summary>
|
||||
/// <param name="loginName">The login name.</param>
|
||||
/// <param name="password">The password.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>
|
||||
/// The account, if the password is correct. Otherwise, null.
|
||||
/// </returns>
|
||||
internal async ValueTask<DataModel.Entities.Account?> GetAccountByLoginNameAsync(string loginName, string password, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var context = this.GetContext();
|
||||
return await this.LoadAccountByLoginNameByJsonQueryAsync(loginName, password, context, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Authenticates the account by login name and password, returning minimal state data without loading the full account.
|
||||
/// </summary>
|
||||
/// <param name="loginName">The login name.</param>
|
||||
/// <param name="password">The password.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The <see cref="DataModel.Entities.AccountState"/> if credentials are valid; otherwise, null.</returns>
|
||||
internal async ValueTask<DataModel.Entities.AccountState?> AuthenticateAsync(string loginName, string password, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var context = this.GetContext();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var accountInfo = await context.Context.Set<Account>()
|
||||
.Where(a => a.LoginName == loginName)
|
||||
.Select(a => new { a.PasswordHash, a.State })
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (accountInfo is not null && BCrypt.Verify(password, accountInfo.PasswordHash))
|
||||
{
|
||||
return accountInfo.State;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the account by login name.
|
||||
/// </summary>
|
||||
/// <param name="loginName">The login name.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>
|
||||
/// The account, if exists. Otherwise, null.
|
||||
/// </returns>
|
||||
internal async ValueTask<DataModel.Entities.Account?> GetAccountByLoginNameAsync(string loginName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var context = this.GetContext();
|
||||
|
||||
var accountInfo = await context.Context.Set<Account>()
|
||||
.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 null;
|
||||
}
|
||||
|
||||
private async ValueTask<Account?> LoadAccountByLoginNameByJsonQueryAsync(string loginName, string password, EntityFrameworkContextBase context, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var accountInfo = await context.Context.Set<Account>()
|
||||
.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 null;
|
||||
}
|
||||
}
|
||||
119
src/Persistence/EntityFramework/CacheAwareRepositoryProvider.cs
Normal file
119
src/Persistence/EntityFramework/CacheAwareRepositoryProvider.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
// <copyright file="CacheAwareRepositoryProvider.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 Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.DataModel;
|
||||
using MUnique.OpenMU.DataModel.Configuration;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// This provider holds two other repository providers:
|
||||
/// One which provides repositories which actually load the data from the database,
|
||||
/// and another one which returns repositories with cached data, based on the
|
||||
/// loaded GameConfiguration. The CacheAwareRepositoryProvider first tries to retrieve
|
||||
/// a repository for the cached data. If none is found, it takes the other.
|
||||
/// </summary>
|
||||
internal class CacheAwareRepositoryProvider : ICacheAwareRepositoryProvider, IContextAwareRepositoryProvider
|
||||
{
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
|
||||
private readonly IRepositoryProvider _nonCachingRepositoryProvider;
|
||||
|
||||
private CachingRepositoryProvider _cachingRepositoryProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CacheAwareRepositoryProvider"/> class.
|
||||
/// </summary>
|
||||
/// <param name="loggerFactory">The logger factory.</param>
|
||||
/// <param name="configurationChangeListener">The configuration change listener.</param>
|
||||
public CacheAwareRepositoryProvider(ILoggerFactory loggerFactory, IConfigurationChangeListener? configurationChangeListener)
|
||||
{
|
||||
this._loggerFactory = loggerFactory;
|
||||
this._cachingRepositoryProvider = new CachingRepositoryProvider(loggerFactory, this);
|
||||
this._nonCachingRepositoryProvider = new NonCachingRepositoryProvider(loggerFactory, this, configurationChangeListener, this.ContextStack);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IContextStack ContextStack { get; } = new ContextStack();
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRepository? GetRepository(Type objectType)
|
||||
{
|
||||
if (this.ContextStack.GetCurrentContext() is EntityFrameworkContextBase { Context: ITypedContext editContext }
|
||||
&& editContext.IsIncluded(objectType))
|
||||
{
|
||||
return this._nonCachingRepositoryProvider.GetRepository(objectType);
|
||||
}
|
||||
|
||||
return this._cachingRepositoryProvider.GetRepository(objectType)
|
||||
?? this._nonCachingRepositoryProvider.GetRepository(objectType);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRepository<T>? GetRepository<T>()
|
||||
where T : class
|
||||
{
|
||||
if (this.ContextStack.GetCurrentContext() is EntityFrameworkContextBase { Context: ITypedContext editContext }
|
||||
&& (editContext.IsIncluded(typeof(T)) || editContext.IsIncluded(typeof(T).BaseType!)))
|
||||
{
|
||||
return this._nonCachingRepositoryProvider.GetRepository<T>();
|
||||
}
|
||||
|
||||
return this._cachingRepositoryProvider.GetRepository<T>()
|
||||
?? this._nonCachingRepositoryProvider.GetRepository<T>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public TRepository? GetRepository<T, TRepository>()
|
||||
where T : class
|
||||
where TRepository : IRepository
|
||||
{
|
||||
if (this.ContextStack.GetCurrentContext() is EntityFrameworkContextBase { Context: ITypedContext editContext }
|
||||
&& (editContext.IsIncluded(typeof(T)) || editContext.IsIncluded(typeof(T).BaseType!)))
|
||||
{
|
||||
return this._nonCachingRepositoryProvider.GetRepository<T, TRepository>();
|
||||
}
|
||||
|
||||
return this._cachingRepositoryProvider.GetRepository<T, TRepository>()
|
||||
?? this._nonCachingRepositoryProvider.GetRepository<T, TRepository>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void EnsureCachesForCurrentGameConfiguration()
|
||||
{
|
||||
this._cachingRepositoryProvider.EnsureCachesForCurrentGameConfiguration();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ResetCache()
|
||||
{
|
||||
this._cachingRepositoryProvider = new CachingRepositoryProvider(this._loggerFactory, this);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask UpdateCachedInstanceAsync(object changedInstance)
|
||||
{
|
||||
if (this._cachingRepositoryProvider.GetRepository(changedInstance.GetType().BaseType ?? changedInstance.GetType()) is IConfigurationTypeRepository repository)
|
||||
{
|
||||
repository.UpdateCachedInstances(changedInstance);
|
||||
}
|
||||
else
|
||||
{
|
||||
// not all types have an own repository.
|
||||
var gameConfigRepo = this._cachingRepositoryProvider.GetRepository<GameConfiguration>();
|
||||
if (gameConfigRepo is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var config in await gameConfigRepo.GetAllAsync().ConfigureAwait(false))
|
||||
{
|
||||
var obj = config.GetObjectOfConfig(changedInstance) as IAssignable;
|
||||
obj?.AssignValuesOf(changedInstance, config);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
149
src/Persistence/EntityFramework/CachedRepository{T}.cs
Normal file
149
src/Persistence/EntityFramework/CachedRepository{T}.cs
Normal file
@@ -0,0 +1,149 @@
|
||||
// <copyright file="CachedRepository{T}.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.Collections;
|
||||
using System.Threading;
|
||||
|
||||
/// <summary>
|
||||
/// A repository which caches all of its data in memory.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the business object.</typeparam>
|
||||
public class CachedRepository<T> : IRepository<T>
|
||||
where T : class, IIdentifiable
|
||||
{
|
||||
private readonly IDictionary<Guid, T> _cache;
|
||||
|
||||
private bool _allLoaded;
|
||||
private bool _loading;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CachedRepository{T}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="baseRepository">The base repository.</param>
|
||||
public CachedRepository(IRepository<T> baseRepository)
|
||||
{
|
||||
this.BaseRepository = baseRepository;
|
||||
|
||||
this._cache = new Dictionary<Guid, T>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the underlying base repository.
|
||||
/// </summary>
|
||||
protected IRepository<T> BaseRepository { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
async ValueTask<IEnumerable> IRepository.GetAllAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await this.GetAllAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<IEnumerable<T>> GetAllAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (this._allLoaded)
|
||||
{
|
||||
return this._cache.Values;
|
||||
}
|
||||
|
||||
if (this._loading)
|
||||
{
|
||||
while (this._loading)
|
||||
{
|
||||
await Task.Delay(10, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return this._cache.Values;
|
||||
}
|
||||
|
||||
this._loading = true;
|
||||
try
|
||||
{
|
||||
IEnumerable<T> values = await this.BaseRepository.GetAllAsync(cancellationToken).ConfigureAwait(false);
|
||||
foreach (var obj in values)
|
||||
{
|
||||
if (!this._cache.ContainsKey(obj.Id))
|
||||
{
|
||||
this.AddToCache(obj.Id, obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
this._loading = false;
|
||||
}
|
||||
|
||||
this._allLoaded = true;
|
||||
|
||||
return this._cache.Values;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<T?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await this.GetAllAsync(cancellationToken).ConfigureAwait(false);
|
||||
this._cache.TryGetValue(id, out var result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
async ValueTask<object?> IRepository.GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await this.GetByIdAsync(id, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<bool> DeleteAsync(object obj)
|
||||
{
|
||||
if (obj is not IIdentifiable identifiable)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return await this.DeleteAsync(identifiable.Id).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<bool> DeleteAsync(Guid id)
|
||||
{
|
||||
if (!await this.BaseRepository.DeleteAsync(id).ConfigureAwait(false))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
this.RemoveFromCache(id);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the object to the cache.
|
||||
/// </summary>
|
||||
/// <param name="id">The identifier.</param>
|
||||
/// <param name="obj">The object.</param>
|
||||
protected virtual void AddToCache(Guid id, T obj)
|
||||
{
|
||||
if (this._cache.TryGetValue(id, out var value))
|
||||
{
|
||||
if (Equals(value, obj))
|
||||
{
|
||||
throw new ArgumentException("Other object with same id is already in cache.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this._cache.Add(id, obj);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the object from cache.
|
||||
/// </summary>
|
||||
/// <param name="id">The identifier.</param>
|
||||
protected virtual void RemoveFromCache(Guid id)
|
||||
{
|
||||
this._cache.Remove(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// <copyright file="CachingEntityFrameworkContext.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 Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of <see cref="IContext"/> for the entity framework <see cref="PersistenceContextProvider"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// TODO: Check if this class can be removed. It doesn't seem to have any additional logic to <see cref="EntityFrameworkContext"/>.
|
||||
/// </remarks>
|
||||
internal class CachingEntityFrameworkContext : EntityFrameworkContextBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CachingEntityFrameworkContext" /> class.
|
||||
/// </summary>
|
||||
/// <param name="context">The context.</param>
|
||||
/// <param name="repositoryProvider">The repository provider.</param>
|
||||
/// <param name="changeListener">The change listener.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public CachingEntityFrameworkContext(DbContext context, IContextAwareRepositoryProvider repositoryProvider, IConfigurationChangeListener? changeListener, ILogger<CachingEntityFrameworkContext> logger)
|
||||
: base(context, repositoryProvider, true, changeListener, logger)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CachingEntityFrameworkContext" /> class.
|
||||
/// </summary>
|
||||
/// <param name="context">The context.</param>
|
||||
/// <param name="repositoryProvider">The repository provider.</param>
|
||||
/// <param name="isOwner">if set to <c>true</c> this instance owns the <paramref name="context" />.</param>
|
||||
/// <param name="changeListener">The change listener.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public CachingEntityFrameworkContext(DbContext context, IContextAwareRepositoryProvider repositoryProvider, bool isOwner, IConfigurationChangeListener? changeListener, ILogger<CachingEntityFrameworkContext> logger)
|
||||
: base(context, repositoryProvider, isOwner, changeListener, logger)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// <copyright file="CachingGameConfigurationRepository.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.Threading;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework.Json;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework.Model;
|
||||
|
||||
/// <summary>
|
||||
/// The game configuration repository, which loads the configuration by using the
|
||||
/// <see cref="JsonObjectLoader"/>, to speed up loading the whole object graph.
|
||||
/// </summary>
|
||||
internal class CachingGameConfigurationRepository : CachingGenericRepository<GameConfiguration>
|
||||
{
|
||||
private readonly JsonObjectLoader _objectLoader;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CachingGameConfigurationRepository" /> class.
|
||||
/// </summary>
|
||||
/// <param name="repositoryProvider">The repository provider.</param>
|
||||
/// <param name="loggerFactory">The logger factory.</param>
|
||||
public CachingGameConfigurationRepository(IContextAwareRepositoryProvider repositoryProvider, ILoggerFactory loggerFactory)
|
||||
: base(repositoryProvider, loggerFactory)
|
||||
{
|
||||
this._objectLoader = new GameConfigurationJsonObjectLoader();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask<GameConfiguration?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (this.RepositoryProvider.ContextStack.GetCurrentContext() is not EntityFrameworkContextBase currentContext)
|
||||
{
|
||||
throw new InvalidOperationException("There is no current context set.");
|
||||
}
|
||||
|
||||
var database = currentContext.Context.Database;
|
||||
await database.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
return await this._objectLoader.LoadObjectAsync<GameConfiguration>(id, currentContext.Context, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await database.CloseConnectionAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask<IEnumerable<GameConfiguration>> GetAllAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (this.RepositoryProvider.ContextStack.GetCurrentContext() is not EntityFrameworkContextBase currentContext)
|
||||
{
|
||||
throw new InvalidOperationException("There is no current context set.");
|
||||
}
|
||||
|
||||
var database = currentContext.Context.Database;
|
||||
await database.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var configs = (await this._objectLoader.LoadAllObjectsAsync<GameConfiguration>(currentContext.Context, cancellationToken).ConfigureAwait(false)).ToList();
|
||||
|
||||
var oldConfig = ((EntityDataContext)currentContext.Context).CurrentGameConfiguration;
|
||||
try
|
||||
{
|
||||
configs.ForEach(config =>
|
||||
{
|
||||
((EntityDataContext)currentContext.Context).CurrentGameConfiguration = config;
|
||||
(this.RepositoryProvider as ICacheAwareRepositoryProvider)?.EnsureCachesForCurrentGameConfiguration();
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
((EntityDataContext)currentContext.Context).CurrentGameConfiguration = oldConfig;
|
||||
}
|
||||
|
||||
return configs;
|
||||
}
|
||||
finally
|
||||
{
|
||||
await database.CloseConnectionAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
48
src/Persistence/EntityFramework/CachingGenericRepository.cs
Normal file
48
src/Persistence/EntityFramework/CachingGenericRepository.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
// <copyright file="CachingGenericRepository.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.Reflection;
|
||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// A generic repository which wraps the access to the dbset of the <see cref="EntityDataContext"/>.
|
||||
/// Entities are getting eagerly (=completely) loaded automatically.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type which this repository should manage.</typeparam>
|
||||
internal class CachingGenericRepository<T> : GenericRepositoryBase<T>
|
||||
where T : class
|
||||
{
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CachingGenericRepository{T}" /> class.
|
||||
/// </summary>
|
||||
/// <param name="repositoryProvider">The repository provider.</param>
|
||||
/// <param name="loggerFactory">The logger factory.</param>
|
||||
public CachingGenericRepository(IContextAwareRepositoryProvider repositoryProvider, ILoggerFactory loggerFactory)
|
||||
: base(repositoryProvider, loggerFactory.CreateLogger(MethodBase.GetCurrentMethod()?.DeclaringType ?? typeof(CachingGenericRepository<T>)))
|
||||
{
|
||||
this._loggerFactory = loggerFactory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a context to work with. If no context is currently registered at the repository provider, a new one is getting created.
|
||||
/// </summary>
|
||||
/// <returns>The context.</returns>
|
||||
protected override EntityFrameworkContextBase GetContext()
|
||||
{
|
||||
var context = this.RepositoryProvider.ContextStack.GetCurrentContext() as EntityFrameworkContextBase;
|
||||
return new CachingEntityFrameworkContext(context?.Context ?? new EntityDataContext(), this.RepositoryProvider, context is null, null, this._loggerFactory.CreateLogger<CachingEntityFrameworkContext>());
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override IEnumerable<INavigation> GetNavigations(EntityEntry entityEntry)
|
||||
{
|
||||
return this.FullEntityType.GetNavigations();
|
||||
}
|
||||
}
|
||||
94
src/Persistence/EntityFramework/CachingRepositoryProvider.cs
Normal file
94
src/Persistence/EntityFramework/CachingRepositoryProvider.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
// <copyright file="CachingRepositoryProvider.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 Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework.Json;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework.Model;
|
||||
|
||||
/// <summary>
|
||||
/// A repository provider which utilizes repositories which use the entity framework core to do data access.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// We create the most repositories by the following convention:
|
||||
/// - Configuration data repositories: We create repositories which retrieve data from the current GameConfiguration to save memory and less database queries.
|
||||
/// - Entity data (Accounts, etc.): We create repositories which retrieves every object at every access from the database.
|
||||
/// </remarks>
|
||||
internal class CachingRepositoryProvider : RepositoryProvider
|
||||
{
|
||||
private readonly IContextAwareRepositoryProvider _parent;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CachingRepositoryProvider" /> class.
|
||||
/// </summary>
|
||||
/// <param name="loggerFactory">The logger factory.</param>
|
||||
/// <param name="parent">The parent context aware repository provider.</param>
|
||||
public CachingRepositoryProvider(ILoggerFactory loggerFactory, IContextAwareRepositoryProvider parent)
|
||||
: base(loggerFactory, null, parent.ContextStack)
|
||||
{
|
||||
this._parent = parent;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures the caches for current game configuration.
|
||||
/// It's meant to fill the caches also in <see cref="ConfigurationIdReferenceResolver"/>.
|
||||
/// </summary>
|
||||
public void EnsureCachesForCurrentGameConfiguration()
|
||||
{
|
||||
foreach (var repository in this.Repositories.Values.OfType<IConfigurationTypeRepository>())
|
||||
{
|
||||
repository.EnsureCacheForCurrentConfiguration();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers the repositories.
|
||||
/// </summary>
|
||||
protected override void Initialize()
|
||||
{
|
||||
this.RegisterRepository(new CachedRepository<GameConfiguration>(new CachingGameConfigurationRepository(this._parent, this.LoggerFactory)));
|
||||
this.RegisterRepository(new CachingGenericRepository<GameServerConfiguration>(this._parent, this.LoggerFactory));
|
||||
this.RegisterRepository(new CachingGenericRepository<GameClientDefinition>(this._parent, this.LoggerFactory));
|
||||
this.RegisterRepository(new CachingGenericRepository<ConnectServerDefinition>(this._parent, this.LoggerFactory));
|
||||
this.RegisterRepository(new CachingGenericRepository<ChatServerDefinition>(this._parent, this.LoggerFactory));
|
||||
this.RegisterRepository(new CachingGenericRepository<ChatServerEndpoint>(this._parent, this.LoggerFactory));
|
||||
this.RegisterRepository(new CachingGenericRepository<GameServerEndpoint>(this._parent, this.LoggerFactory));
|
||||
this.RegisterRepository(new GameServerDefinitionRepository(this._parent, this.LoggerFactory));
|
||||
|
||||
this.RegisterRepository(new ConfigurationTypeRepository<ItemOptionDefinition>(this._parent, this.LoggerFactory, config => config.RawItemOptions));
|
||||
this.RegisterRepository(new ConfigurationTypeRepository<IncreasableItemOption>(
|
||||
this._parent,
|
||||
this.LoggerFactory,
|
||||
config => config.RawItemOptions.SelectMany(o => o.RawPossibleOptions).Distinct().ToList()));
|
||||
this.RegisterRepository(new ConfigurationTypeRepository<AttributeDefinition>(this._parent, this.LoggerFactory, config => config.RawAttributes));
|
||||
this.RegisterRepository(new ConfigurationTypeRepository<AttributeRelationship>(this._parent, this.LoggerFactory, config => config.RawGlobalAttributeCombinations));
|
||||
this.RegisterRepository(new ConfigurationTypeRepository<ConstValueAttribute>(this._parent, this.LoggerFactory, config => config.RawGlobalBaseAttributeValues));
|
||||
this.RegisterRepository(new ConfigurationTypeRepository<DropItemGroup>(this._parent, this.LoggerFactory, config => config.RawDropItemGroups));
|
||||
this.RegisterRepository(new ConfigurationTypeRepository<CharacterClass>(this._parent, this.LoggerFactory, config => config.RawCharacterClasses));
|
||||
this.RegisterRepository(new ConfigurationTypeRepository<ItemOptionType>(this._parent, this.LoggerFactory, config => config.RawItemOptionTypes));
|
||||
this.RegisterRepository(new ConfigurationTypeRepository<ItemSetGroup>(this._parent, this.LoggerFactory, config => config.RawItemSetGroups));
|
||||
this.RegisterRepository(new ConfigurationTypeRepository<ItemOfItemSet>(this._parent, this.LoggerFactory, config => config.RawItemSetGroups.SelectMany(g => g.RawItems).ToList()));
|
||||
this.RegisterRepository(new ConfigurationTypeRepository<ItemSlotType>(this._parent, this.LoggerFactory, config => config.RawItemSlotTypes));
|
||||
this.RegisterRepository(new ConfigurationTypeRepository<ItemDefinition>(this._parent, this.LoggerFactory, config => config.RawItems));
|
||||
this.RegisterRepository(new ConfigurationTypeRepository<JewelMix>(this._parent, this.LoggerFactory, config => config.RawJewelMixes));
|
||||
this.RegisterRepository(new ConfigurationTypeRepository<MagicEffectDefinition>(this._parent, this.LoggerFactory, config => config.RawMagicEffects));
|
||||
this.RegisterRepository(new ConfigurationTypeRepository<GameMapDefinition>(this._parent, this.LoggerFactory, config => config.RawMaps));
|
||||
this.RegisterRepository(new ConfigurationTypeRepository<MasterSkillRoot>(this._parent, this.LoggerFactory, config => config.RawMasterSkillRoots));
|
||||
this.RegisterRepository(new ConfigurationTypeRepository<MonsterDefinition>(this._parent, this.LoggerFactory, config => config.RawMonsters));
|
||||
this.RegisterRepository(new ConfigurationTypeRepository<Skill>(this._parent, this.LoggerFactory, config => config.RawSkills));
|
||||
this.RegisterRepository(new ConfigurationTypeRepository<PlugInConfiguration>(this._parent, this.LoggerFactory, config => config.RawPlugInConfigurations));
|
||||
this.RegisterRepository(new ConfigurationTypeRepository<QuestDefinition>(this._parent, this.LoggerFactory, config => config.RawMonsters.SelectMany(m => m.RawQuests).ToList()));
|
||||
this.RegisterRepository(new ConfigurationTypeRepository<Item>(this._parent, this.LoggerFactory, config => config.RawMonsters.SelectMany(m => m.RawMerchantStore?.RawItems ?? Enumerable.Empty<Item>()).ToList()));
|
||||
|
||||
base.Initialize();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override IRepository CreateGenericRepository(Type entityType, IContextAwareRepositoryProvider repositoryProvider)
|
||||
{
|
||||
var repositoryType = typeof(CachingGenericRepository<>).MakeGenericType(entityType);
|
||||
return (IRepository)Activator.CreateInstance(repositoryType, this._parent, this.LoggerFactory)!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
// <copyright file="ConfigFileDatabaseConnectionStringProvider.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.IO;
|
||||
using System.Threading;
|
||||
using System.Xml;
|
||||
using System.Xml.Serialization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of <see cref="IDatabaseConnectionSettingProvider"/> which takes the connection strings out of
|
||||
/// a configuration file, usually <c>ConnectionSettings.xml</c>.
|
||||
/// The settings can be influenced by the environment variables <c>DB_HOST</c>, <c>DB_ADMIN_USER</c> and <c>DB_ADMIN_PW</c>.
|
||||
/// </summary>
|
||||
public class ConfigFileDatabaseConnectionStringProvider : IDatabaseConnectionSettingProvider
|
||||
{
|
||||
private const string DbHostVariableName = "DB_HOST";
|
||||
private const string DbAdminUserVariableName = "DB_ADMIN_USER";
|
||||
private const string DbAdminPasswordVariableName = "DB_ADMIN_PW";
|
||||
|
||||
private readonly string _fileName;
|
||||
|
||||
private IDictionary<Type, ConnectionSetting>? _settings;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConfigFileDatabaseConnectionStringProvider"/> class.
|
||||
/// </summary>
|
||||
public ConfigFileDatabaseConnectionStringProvider()
|
||||
: this("ConnectionSettings.xml")
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConfigFileDatabaseConnectionStringProvider"/> class.
|
||||
/// </summary>
|
||||
/// <param name="fileName">Name of the file.</param>
|
||||
public ConfigFileDatabaseConnectionStringProvider(string fileName)
|
||||
{
|
||||
this._fileName = fileName;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task? Initialization { get; private set; }
|
||||
|
||||
private IDictionary<Type, ConnectionSetting> Settings => this._settings ??= this.LoadSettings();
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task InitializeAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
this.Initialization = Task.Run(() => this._settings = this.LoadSettings(), cancellationToken);
|
||||
await this.Initialization.ConfigureAwait(false);
|
||||
ConnectionConfigurator.Initialize(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ConnectionSetting GetConnectionSetting<TContextType>()
|
||||
where TContextType : DbContext
|
||||
{
|
||||
return this.GetConnectionSetting(typeof(TContextType));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ConnectionSetting GetConnectionSetting(Type contextType)
|
||||
{
|
||||
if (this.Settings.TryGetValue(contextType, out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
throw new ArgumentException("DB Configuration not found for type {0}", contextType.FullName);
|
||||
}
|
||||
|
||||
private IDictionary<Type, ConnectionSetting> LoadSettings()
|
||||
{
|
||||
var settings = new XmlReaderSettings
|
||||
{
|
||||
IgnoreComments = true,
|
||||
IgnoreProcessingInstructions = true,
|
||||
IgnoreWhitespace = true,
|
||||
DtdProcessing = DtdProcessing.Ignore,
|
||||
CloseInput = true,
|
||||
XmlResolver = null,
|
||||
};
|
||||
var result = new Dictionary<Type, ConnectionSetting>();
|
||||
|
||||
var configurationFilePath = Path.Combine(Path.GetDirectoryName(new Uri(typeof(ConnectionConfigurator).Assembly.Location!).LocalPath)!, this._fileName);
|
||||
using var xmlReader = XmlReader.Create(File.OpenRead(configurationFilePath), settings);
|
||||
var serializer = new XmlSerializer(typeof(ConnectionSettings));
|
||||
if (serializer.CanDeserialize(xmlReader))
|
||||
{
|
||||
if (serializer.Deserialize(xmlReader) is ConnectionSettings xmlSettings)
|
||||
{
|
||||
foreach (var setting in xmlSettings.Connections)
|
||||
{
|
||||
if (setting.ContextTypeName is null)
|
||||
{
|
||||
throw new InvalidDataException("ContextTypeName is null.");
|
||||
}
|
||||
|
||||
if (setting.ConnectionString is null)
|
||||
{
|
||||
throw new InvalidDataException("ConnectionString is null.");
|
||||
}
|
||||
|
||||
if (Type.GetType(setting.ContextTypeName, false, true) is { } contextType)
|
||||
{
|
||||
this.ApplyEnvironmentVariables(setting);
|
||||
result.Add(contextType, setting);
|
||||
}
|
||||
else if (setting.ContextTypeName.EndsWith($".{nameof(TypedContext)}") || setting.ContextTypeName.EndsWith($".{nameof(TypedContext)}^1"))
|
||||
{
|
||||
this.ApplyEnvironmentVariables(setting);
|
||||
result.Add(typeof(TypedContext), setting);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidDataException($"Unknown context type: {setting.ContextTypeName}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void ApplyEnvironmentVariables(ConnectionSetting setting)
|
||||
{
|
||||
if (Environment.GetEnvironmentVariable(DbHostVariableName) is { } dbHost
|
||||
&& !string.IsNullOrEmpty(dbHost))
|
||||
{
|
||||
setting.ConnectionString = setting.ConnectionString!.Replace("Server=localhost;", $"Server={dbHost};");
|
||||
}
|
||||
|
||||
if (setting.ConnectionString!.Contains("User Id=postgres;"))
|
||||
{
|
||||
if (Environment.GetEnvironmentVariable(DbAdminUserVariableName) is { } dbAdminUser
|
||||
&& !string.IsNullOrEmpty(dbAdminUser))
|
||||
{
|
||||
setting.ConnectionString = setting.ConnectionString.Replace("User Id=postgres;", $"User Id={dbAdminUser};");
|
||||
}
|
||||
|
||||
if (Environment.GetEnvironmentVariable(DbAdminPasswordVariableName) is { } dbAdminPassword
|
||||
&& !string.IsNullOrEmpty(dbAdminPassword))
|
||||
{
|
||||
setting.ConnectionString = setting.ConnectionString.Replace("Password=admin;", $"Password={dbAdminPassword};");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// <copyright file="ConfigurationChangeListener.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.IO;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using MUnique.OpenMU.DataModel;
|
||||
using MUnique.OpenMU.DataModel.Configuration;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Class which listens to changes within the <see cref="GameConfiguration"/>,
|
||||
/// updates the cached instances and publishes them to the <see cref="IConfigurationChangePublisher"/>.
|
||||
/// </summary>
|
||||
public class ConfigurationChangeListener : IConfigurationChangeListener
|
||||
{
|
||||
private readonly Lazy<IPersistenceContextProvider> _contextProvider;
|
||||
private readonly IConfigurationChangePublisher _changePublisher;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConfigurationChangeListener"/> class.
|
||||
/// </summary>
|
||||
/// <param name="contextProvider">The context provider.</param>
|
||||
/// <param name="changePublisher">The change publisher.</param>
|
||||
public ConfigurationChangeListener(Lazy<IPersistenceContextProvider> contextProvider, IConfigurationChangePublisher changePublisher)
|
||||
{
|
||||
this._contextProvider = contextProvider;
|
||||
this._changePublisher = changePublisher;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask ConfigurationChangedAsync(Type type, Guid id, object configuration, object? parent)
|
||||
{
|
||||
var repositoryProvider = this._contextProvider.Value.RepositoryProvider;
|
||||
if (repositoryProvider is ICacheAwareRepositoryProvider cacheAwareRepositoryProvider)
|
||||
{
|
||||
await cacheAwareRepositoryProvider.UpdateCachedInstanceAsync(configuration).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await this._changePublisher.ConfigurationChangedAsync(type, id, configuration).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask ConfigurationAddedAsync(Type type, Guid id, object configuration, object? parent, INavigationBase? parentCollectionNavigation)
|
||||
{
|
||||
if (parentCollectionNavigation?.GetCollectionAccessor() is { } collectionAccessor
|
||||
&& (parent?.GetId() ?? parent as Guid?) is { } parentId)
|
||||
{
|
||||
using var configContext = this._contextProvider.Value.CreateNewConfigurationContext();
|
||||
var gameConfiguration = (await configContext.GetAsync<GameConfiguration>().ConfigureAwait(false)).FirstOrDefault();
|
||||
if (gameConfiguration is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var context = this._contextProvider.Value.CreateNewContext(gameConfiguration);
|
||||
var cachedParent = await context.GetByIdAsync(parentId, parentCollectionNavigation.DeclaringEntityType.ClrType).ConfigureAwait(false);
|
||||
|
||||
if (cachedParent is not null)
|
||||
{
|
||||
var cachedConfiguration = configContext.CreateNew(type);
|
||||
if (cachedConfiguration is IAssignable assignable)
|
||||
{
|
||||
assignable.AssignValuesOf(configuration, gameConfiguration);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"Configuration type {type} is not assignable.");
|
||||
}
|
||||
|
||||
collectionAccessor.Add(cachedParent, cachedConfiguration, false);
|
||||
}
|
||||
}
|
||||
|
||||
await this._changePublisher.ConfigurationAddedAsync(type, id, configuration).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask ConfigurationRemovedAsync(Type type, Guid id, object? parent, INavigationBase? parentCollectionNavigation)
|
||||
{
|
||||
using var configContext = this._contextProvider.Value.CreateNewConfigurationContext();
|
||||
var gameConfiguration = (await configContext.GetAsync<GameConfiguration>().ConfigureAwait(false)).First();
|
||||
using var context = this._contextProvider.Value.CreateNewContext(gameConfiguration);
|
||||
|
||||
if (parentCollectionNavigation?.GetCollectionAccessor() is { } collectionAccessor
|
||||
&& (parent?.GetId() ?? parent as Guid?) is { } parentId
|
||||
&& await context.GetByIdAsync(parentId, parentCollectionNavigation.DeclaringEntityType.ClrType).ConfigureAwait(false) is { } cachedParent
|
||||
&& await context.GetByIdAsync(id, type).ConfigureAwait(false) is { } cachedEntity)
|
||||
{
|
||||
collectionAccessor.Remove(cachedParent, cachedEntity);
|
||||
}
|
||||
|
||||
await this._changePublisher.ConfigurationRemovedAsync(type, id).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
21
src/Persistence/EntityFramework/ConfigurationContext.cs
Normal file
21
src/Persistence/EntityFramework/ConfigurationContext.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
// <copyright file="ConfigurationContext.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 Microsoft.EntityFrameworkCore;
|
||||
|
||||
/// <summary>
|
||||
/// Context to access the game configuration (read-only).
|
||||
/// </summary>
|
||||
public class ConfigurationContext : EntityDataContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConfigurationContext"/> class.
|
||||
/// </summary>
|
||||
public ConfigurationContext()
|
||||
{
|
||||
this.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
||||
}
|
||||
}
|
||||
176
src/Persistence/EntityFramework/ConfigurationTypeRepository.cs
Normal file
176
src/Persistence/EntityFramework/ConfigurationTypeRepository.cs
Normal file
@@ -0,0 +1,176 @@
|
||||
// <copyright file="ConfigurationTypeRepository.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.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using MUnique.OpenMU.DataModel;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework.Json;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework.Model;
|
||||
|
||||
/// <summary>
|
||||
/// A repository which gets its data from the <see cref="EntityDataContext.CurrentGameConfiguration"/>, without additionally touching the database.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The data object type.</typeparam>
|
||||
internal class ConfigurationTypeRepository<T> : IRepository<T>, IConfigurationTypeRepository
|
||||
where T : class
|
||||
{
|
||||
private readonly IContextAwareRepositoryProvider _repositoryProvider;
|
||||
|
||||
private readonly Func<GameConfiguration, ICollection<T>> _collectionSelector;
|
||||
|
||||
/// <summary>
|
||||
/// A cache which holds each <typeparamref name="T"/> in a dictionary to be able to access it by faster by id.
|
||||
/// There is one cache for each <see cref="GameConfiguration"/>, because it could be possible that more than one
|
||||
/// <see cref="GameConfiguration"/> could be hosted by one server.
|
||||
/// </summary>
|
||||
private readonly IDictionary<GameConfiguration, IDictionary<Guid, T>> _cache = new ConcurrentDictionary<GameConfiguration, IDictionary<Guid, T>>();
|
||||
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConfigurationTypeRepository{T}" /> class.
|
||||
/// </summary>
|
||||
/// <param name="repositoryProvider">The repository provider.</param>
|
||||
/// <param name="loggerFactory">The logger factory.</param>
|
||||
/// <param name="collectionSelector">The collection selector which returns the collection of <typeparamref name="T" /> of a <see cref="GameConfiguration" />.</param>
|
||||
public ConfigurationTypeRepository(IContextAwareRepositoryProvider repositoryProvider, ILoggerFactory loggerFactory, Func<GameConfiguration, ICollection<T>> collectionSelector)
|
||||
{
|
||||
this._repositoryProvider = repositoryProvider;
|
||||
this._collectionSelector = collectionSelector;
|
||||
this._logger = loggerFactory.CreateLogger(this.GetType());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all objects by using the <see cref="_collectionSelector"/> to the current <see cref="GameConfiguration"/>.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>All objects of the repository.</returns>
|
||||
public ValueTask<IEnumerable<T>> GetAllAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return ValueTask.FromResult<IEnumerable<T>>(this._collectionSelector(this.GetCurrentGameConfiguration()));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
async ValueTask<IEnumerable> IRepository.GetAllAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return await this.GetAllAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask<T?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
this.EnsureCacheForCurrentConfiguration();
|
||||
|
||||
var dictionary = this._cache[this.GetCurrentGameConfiguration()];
|
||||
if (dictionary.TryGetValue(id, out var result))
|
||||
{
|
||||
return ValueTask.FromResult<T?>(result);
|
||||
}
|
||||
|
||||
return ValueTask.FromResult<T?>(null);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<bool> DeleteAsync(object obj)
|
||||
{
|
||||
if (obj is not T item)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var gameConfiguration = this.GetCurrentGameConfiguration();
|
||||
var collection = this._collectionSelector(gameConfiguration);
|
||||
return collection.Remove(item);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<bool> DeleteAsync(Guid id)
|
||||
{
|
||||
if (await this.GetByIdAsync(id).ConfigureAwait(false) is { } obj)
|
||||
{
|
||||
return await this.DeleteAsync(obj).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
async ValueTask<object?> IRepository.GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await this.GetByIdAsync(id, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures the cache for the current configuration.
|
||||
/// TODO: Call this at a better place and time - so that we can remove this check before every GetById.
|
||||
/// </summary>
|
||||
public void EnsureCacheForCurrentConfiguration()
|
||||
{
|
||||
var configuration = this.GetCurrentGameConfiguration();
|
||||
if (this._cache.ContainsKey(configuration))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (this._cache)
|
||||
{
|
||||
if (this._cache.ContainsKey(configuration))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var dictionary = this._collectionSelector(configuration)
|
||||
.Where(item => item is IIdentifiable)
|
||||
.ToDictionary(item => ((IIdentifiable)item).Id, item => item);
|
||||
this._cache.Add(configuration, dictionary);
|
||||
foreach (var item in dictionary.Values)
|
||||
{
|
||||
ConfigurationIdReferenceResolver.Instance.AddReference((IIdentifiable)item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void UpdateCachedInstances(object changedInstance)
|
||||
{
|
||||
foreach (var (gameConfiguration, cache) in this._cache)
|
||||
{
|
||||
if (!cache.TryGetValue(changedInstance.GetId(), out var cachedInstance))
|
||||
{
|
||||
this._logger.LogDebug("Cached instance '{cachedInstance}' couldn't be updated because it wasn't found.", cachedInstance);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cachedInstance is not IAssignable<T> assignable)
|
||||
{
|
||||
// todo: implement this for all types
|
||||
this._logger.LogWarning("Cached instance '{cachedInstance}' couldn't be updated because it doesn't implement {IAssignable}.", cachedInstance, typeof(IAssignable<T>));
|
||||
return;
|
||||
}
|
||||
|
||||
assignable.AssignValuesOf((T)changedInstance, gameConfiguration);
|
||||
this._logger.LogInformation("Updated cached instance '{cachedInstance}'.", cachedInstance);
|
||||
}
|
||||
}
|
||||
|
||||
private GameConfiguration GetCurrentGameConfiguration()
|
||||
{
|
||||
var context = (this._repositoryProvider.ContextStack.GetCurrentContext() as CachingEntityFrameworkContext)?.Context as EntityDataContext;
|
||||
if (context 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.");
|
||||
}
|
||||
}
|
||||
145
src/Persistence/EntityFramework/ConnectionConfigurator.cs
Normal file
145
src/Persistence/EntityFramework/ConnectionConfigurator.cs
Normal file
@@ -0,0 +1,145 @@
|
||||
// <copyright file="ConnectionConfigurator.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.Text.RegularExpressions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Nito.AsyncEx.Synchronous;
|
||||
|
||||
/// <summary>
|
||||
/// The database roles.
|
||||
/// </summary>
|
||||
public enum DatabaseRole
|
||||
{
|
||||
/// <summary>
|
||||
/// The admin role which can create databases and other roles.
|
||||
/// </summary>
|
||||
Admin,
|
||||
|
||||
/// <summary>
|
||||
/// The account role which can load and save account data.
|
||||
/// </summary>
|
||||
Account,
|
||||
|
||||
/// <summary>
|
||||
/// The role which can load configuration data.
|
||||
/// </summary>
|
||||
Configuration,
|
||||
|
||||
/// <summary>
|
||||
/// The role which can load guild data.
|
||||
/// </summary>
|
||||
Guild,
|
||||
|
||||
/// <summary>
|
||||
/// The role which can load friend data.
|
||||
/// </summary>
|
||||
Friend,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The database connection configurator which loads the configuration from a file.
|
||||
/// TODO: Make class non-static.
|
||||
/// </summary>
|
||||
public static class ConnectionConfigurator
|
||||
{
|
||||
private static IDatabaseConnectionSettingProvider? _provider;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance is initialized.
|
||||
/// </summary>
|
||||
public static bool IsInitialized => _provider is not null;
|
||||
|
||||
private static IDatabaseConnectionSettingProvider Provider => _provider ?? throw new InvalidOperationException("Call Initialize before.");
|
||||
|
||||
/// <summary>
|
||||
/// Initializes this instance.
|
||||
/// </summary>
|
||||
/// <param name="provider">The <see cref="IDatabaseConnectionSettingProvider"/> which provides the required connection settings.</param>
|
||||
public static void Initialize(IDatabaseConnectionSettingProvider provider)
|
||||
{
|
||||
if (_provider is not null)
|
||||
{
|
||||
throw new InvalidOperationException("provider is initialized already");
|
||||
}
|
||||
|
||||
_provider = provider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the role from the configured connection string.
|
||||
/// </summary>
|
||||
/// <param name="role">The role.</param>
|
||||
/// <returns>The name of the role from the configured connection string.</returns>
|
||||
public static string GetRoleName(DatabaseRole role)
|
||||
{
|
||||
Provider.Initialization?.WaitWithoutException();
|
||||
var settings = Provider.GetConnectionSetting(GetContextTypeOfRole(role));
|
||||
return Regex.Match(settings.ConnectionString!, "User Id=([^;]+?);").Groups[1].Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the password password of the role from the configured connection string.
|
||||
/// </summary>
|
||||
/// <param name="role">The role.</param>
|
||||
/// <returns>The password password of the role from the configured connection string.</returns>
|
||||
public static string GetRolePassword(DatabaseRole role)
|
||||
{
|
||||
Provider.Initialization?.WaitWithoutException();
|
||||
var settings = Provider.GetConnectionSetting(GetContextTypeOfRole(role));
|
||||
return Regex.Match(settings.ConnectionString!, "Password=([^;]+?);").Groups[1].Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures the specified options builder.
|
||||
/// </summary>
|
||||
/// <param name="context">The context.</param>
|
||||
/// <param name="optionsBuilder">The options builder.</param>
|
||||
/// <exception cref="NotImplementedException">At the moment only Npgsql engine (PostgreSQL) is implemented.</exception>
|
||||
internal static void Configure(this DbContext context, DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
// see https://github.com/dotnet/efcore/issues/34431
|
||||
optionsBuilder.ConfigureWarnings(a => a.Ignore(RelationalEventId.PendingModelChangesWarning));
|
||||
|
||||
var type = context.GetType();
|
||||
if (type.IsGenericType)
|
||||
{
|
||||
type = type.GetGenericTypeDefinition();
|
||||
}
|
||||
|
||||
Provider.Initialization?.WaitWithoutException();
|
||||
if (Provider.GetConnectionSetting(type) is { } setting)
|
||||
{
|
||||
switch (setting.DatabaseEngine)
|
||||
{
|
||||
case DatabaseEngine.Npgsql:
|
||||
optionsBuilder.UseNpgsql(setting.ConnectionString!);
|
||||
optionsBuilder.ReplaceService<IMigrationsSqlGenerator, MyNpgsqlMigrationsSqlGenerator>();
|
||||
optionsBuilder.ReplaceService<IModelCacheKeyFactory, MyModelCacheKeyFactory>();
|
||||
return;
|
||||
default:
|
||||
throw new NotImplementedException("At the moment only Npgsql engine (PostgreSQL) is implemented.");
|
||||
}
|
||||
}
|
||||
|
||||
throw new ArgumentException($"No configuration found for context type {context.GetType()}", nameof(context));
|
||||
}
|
||||
|
||||
private static Type GetContextTypeOfRole(DatabaseRole role)
|
||||
{
|
||||
return role switch
|
||||
{
|
||||
DatabaseRole.Account => typeof(AccountContext),
|
||||
DatabaseRole.Admin => typeof(EntityDataContext),
|
||||
DatabaseRole.Configuration => typeof(ConfigurationContext),
|
||||
DatabaseRole.Guild => typeof(GuildContext),
|
||||
DatabaseRole.Friend => typeof(FriendContext),
|
||||
_ => throw new ArgumentException($"Role {role} unknown."),
|
||||
};
|
||||
}
|
||||
}
|
||||
56
src/Persistence/EntityFramework/ConnectionSetting.cs
Normal file
56
src/Persistence/EntityFramework/ConnectionSetting.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
// <copyright file="ConnectionSetting.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.ComponentModel;
|
||||
using System.Xml.Serialization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
/// <summary>
|
||||
/// The configured database engine.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
[XmlType(Namespace = "http://www.munique.net/ConnectionSettings")]
|
||||
public enum DatabaseEngine
|
||||
{
|
||||
/// <summary>
|
||||
/// The NPGSQL engine (PostgreSQL).
|
||||
/// </summary>
|
||||
Npgsql,
|
||||
|
||||
/// <summary>
|
||||
/// The MSSQL server engine.
|
||||
/// </summary>
|
||||
SqlServer,
|
||||
|
||||
/// <summary>
|
||||
/// The in memory engine (could be used for testing).
|
||||
/// </summary>
|
||||
InMemory,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A database connection setting for the specified role.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
[XmlType(Namespace = "http://www.munique.net/ConnectionSettings")]
|
||||
public class ConnectionSetting
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the type of the <see cref="DbContext"/>.
|
||||
/// </summary>
|
||||
public string? ContextTypeName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the connection string which should be used for the DbContext.
|
||||
/// </summary>
|
||||
public string? ConnectionString { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the database engine.
|
||||
/// </summary>
|
||||
[DefaultValue(DatabaseEngine.Npgsql)]
|
||||
public DatabaseEngine DatabaseEngine { get; set; }
|
||||
}
|
||||
41
src/Persistence/EntityFramework/ConnectionSettings.cs
Normal file
41
src/Persistence/EntityFramework/ConnectionSettings.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
// <copyright file="ConnectionSettings.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.Runtime.Serialization;
|
||||
using System.Xml.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// The database connection settings xml serialization class.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
[XmlType(AnonymousType = true, Namespace = "http://www.munique.net/ConnectionSettings")]
|
||||
[XmlRoot(Namespace = "http://www.munique.net/ConnectionSettings", IsNullable = false)]
|
||||
public class ConnectionSettings : IDeserializationCallback
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConnectionSettings"/> class.
|
||||
/// </summary>
|
||||
public ConnectionSettings()
|
||||
{
|
||||
this.Connections = new ConnectionSetting[0];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the database connections.
|
||||
/// </summary>
|
||||
[XmlArray(IsNullable = false)]
|
||||
[XmlArrayItem("Connection", IsNullable = false)]
|
||||
public ConnectionSetting[] Connections { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Runs when the entire object graph has been deserialized.
|
||||
/// </summary>
|
||||
/// <param name="sender">The object that initiated the callback. The functionality for this parameter is not currently implemented.</param>
|
||||
public void OnDeserialization(object? sender)
|
||||
{
|
||||
this.Connections ??= new ConnectionSetting[0];
|
||||
}
|
||||
}
|
||||
47
src/Persistence/EntityFramework/ConnectionSettings.xml
Normal file
47
src/Persistence/EntityFramework/ConnectionSettings.xml
Normal file
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ConnectionSettings xmlns="http://www.munique.net/ConnectionSettings">
|
||||
<Connections>
|
||||
<Connection>
|
||||
<!-- The user role of this context should have rights to create/update a database and user roles. -->
|
||||
<ContextTypeName>MUnique.OpenMU.Persistence.EntityFramework.EntityDataContext</ContextTypeName>
|
||||
<ConnectionString>Server=localhost;Port=5432;User Id=postgres;Password=admin;Database=openmu;Command Timeout=120;</ConnectionString>
|
||||
<DatabaseEngine>Npgsql</DatabaseEngine>
|
||||
</Connection>
|
||||
<Connection>
|
||||
<!-- The user role of this context should have rights to select/insert/update/delete data in all schemas. -->
|
||||
<ContextTypeName>MUnique.OpenMU.Persistence.EntityFramework.TypedContext</ContextTypeName>
|
||||
<ConnectionString>Server=localhost;Port=5432;User Id=postgres;Password=admin;Database=openmu;Command Timeout=120;</ConnectionString>
|
||||
<DatabaseEngine>Npgsql</DatabaseEngine>
|
||||
</Connection>
|
||||
<Connection>
|
||||
<!-- The user role of this context should have rights to select the config schema -->
|
||||
<ContextTypeName>MUnique.OpenMU.Persistence.EntityFramework.ConfigurationContext</ContextTypeName>
|
||||
<ConnectionString>Server=localhost;Port=5432;User Id=config;Password=config;Database=openmu;Command Timeout=120;</ConnectionString>
|
||||
<DatabaseEngine>Npgsql</DatabaseEngine>
|
||||
</Connection>
|
||||
<Connection>
|
||||
<!-- The account context should connect with a user with less privileges. It should not be allowed to edit configuration tables, for example. -->
|
||||
<ContextTypeName>MUnique.OpenMU.Persistence.EntityFramework.AccountContext</ContextTypeName>
|
||||
<ConnectionString>Server=localhost;Port=5432;User Id=account;Password=account;Database=openmu;Command Timeout=120;</ConnectionString>
|
||||
<DatabaseEngine>Npgsql</DatabaseEngine>
|
||||
</Connection>
|
||||
<Connection>
|
||||
<!-- The trade context should connect with a user with less privileges. It should just be allowed to edit the item table. -->
|
||||
<ContextTypeName>MUnique.OpenMU.Persistence.EntityFramework.TradeContext</ContextTypeName>
|
||||
<ConnectionString>Server=localhost;Port=5432;User Id=account;Password=account;Database=openmu;Command Timeout=120;</ConnectionString>
|
||||
<DatabaseEngine>Npgsql</DatabaseEngine>
|
||||
</Connection>
|
||||
<Connection>
|
||||
<!-- The friend context should connect with a user with less privileges. It should not be allowed to edit configuration tables, for example. -->
|
||||
<ContextTypeName>MUnique.OpenMU.Persistence.EntityFramework.FriendContext</ContextTypeName>
|
||||
<ConnectionString>Server=localhost;Port=5432;User Id=friend;Password=friend;Database=openmu;Command Timeout=120;</ConnectionString>
|
||||
<DatabaseEngine>Npgsql</DatabaseEngine>
|
||||
</Connection>
|
||||
<Connection>
|
||||
<!-- The guild context should connect with a user with less privileges. It should not be allowed to edit Guild and GuildMember and to read the Character table. -->
|
||||
<ContextTypeName>MUnique.OpenMU.Persistence.EntityFramework.GuildContext</ContextTypeName>
|
||||
<ConnectionString>Server=localhost;Port=5432;User Id=guild;Password=guild;Database=openmu;Command Timeout=120;</ConnectionString>
|
||||
<DatabaseEngine>Npgsql</DatabaseEngine>
|
||||
</Connection>
|
||||
</Connections>
|
||||
</ConnectionSettings>
|
||||
62
src/Persistence/EntityFramework/ContextStack.cs
Normal file
62
src/Persistence/EntityFramework/ContextStack.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
// <copyright file="ContextStack.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.Threading;
|
||||
|
||||
/// <summary>
|
||||
/// A stack for persistence contexts.
|
||||
/// </summary>
|
||||
internal sealed class ContextStack : IContextStack
|
||||
{
|
||||
private readonly AsyncLocal<Stack<IContext>> _localStack = new();
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="context">The context.</param>
|
||||
/// <returns>The disposable to end the usage.</returns>
|
||||
public IDisposable UseContext(IContext context)
|
||||
{
|
||||
var contextsOfCurrentThread = this._localStack.Value ??= new Stack<IContext>();
|
||||
contextsOfCurrentThread.Push(context);
|
||||
return new ContextPop(contextsOfCurrentThread);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current context of the current thread.
|
||||
/// </summary>
|
||||
/// <returns>The current context.</returns>
|
||||
public IContext? GetCurrentContext()
|
||||
{
|
||||
var contextsOfCurrentThread = this._localStack.Value ??= new Stack<IContext>();
|
||||
if (contextsOfCurrentThread is { Count: > 0 })
|
||||
{
|
||||
return contextsOfCurrentThread.Peek();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private sealed class ContextPop : IDisposable
|
||||
{
|
||||
private Stack<IContext>? _stack;
|
||||
|
||||
public ContextPop(Stack<IContext> stack)
|
||||
{
|
||||
this._stack = stack;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (this._stack != null)
|
||||
{
|
||||
this._stack.Pop();
|
||||
this._stack = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
101
src/Persistence/EntityFramework/EntityDataContext.cs
Normal file
101
src/Persistence/EntityFramework/EntityDataContext.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
// <copyright file="EntityDataContext.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 Microsoft.EntityFrameworkCore;
|
||||
using MUnique.OpenMU.AttributeSystem;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework.Extensions;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework.Extensions.ModelBuilder;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Context for all types of the data model.
|
||||
/// </summary>
|
||||
public class EntityDataContext : ExtendedTypeContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the current game configuration.
|
||||
/// This is used by the <see cref="ConfigurationTypeRepository{T}"/> which gets its data from the current game configuration.
|
||||
/// </summary>
|
||||
internal GameConfiguration? CurrentGameConfiguration { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
if (!ConnectionConfigurator.IsInitialized)
|
||||
{
|
||||
ConnectionConfigurator.Initialize(new ConfigFileDatabaseConnectionStringProvider());
|
||||
}
|
||||
|
||||
base.OnConfiguring(optionsBuilder);
|
||||
this.Configure(optionsBuilder);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
modelBuilder.Entity<AppearanceData>(o => o.Ignore(p => p.CharacterStatus)); // todo
|
||||
modelBuilder.Ignore<ConstantElement>();
|
||||
modelBuilder.Ignore<SimpleElement>();
|
||||
modelBuilder.Entity<Model.AttributeDefinition>();
|
||||
modelBuilder.Entity<ConnectServerDefinition>();
|
||||
modelBuilder.Entity<ChatServerDefinition>();
|
||||
modelBuilder.Entity<MiniGameRankingEntry>();
|
||||
modelBuilder.Entity<GameServerDefinition>(entity =>
|
||||
{
|
||||
entity.Property(e => e.PvpEnabled).HasDefaultValue(true);
|
||||
});
|
||||
modelBuilder.Entity<ConfigurationUpdate>().Apply();
|
||||
modelBuilder.Entity<ConfigurationUpdateState>();
|
||||
modelBuilder.Entity<SystemConfiguration>();
|
||||
|
||||
modelBuilder.Entity<PowerUpDefinitionValue>().Apply();
|
||||
modelBuilder.Entity<Model.ConstValueAttribute>().Apply();
|
||||
modelBuilder.Entity<Account>().Apply();
|
||||
modelBuilder.Entity<Character>().Apply();
|
||||
modelBuilder.Entity<CharacterClass>().Apply();
|
||||
modelBuilder.Entity<DropItemGroup>().Apply();
|
||||
modelBuilder.Entity<ExitGate>().Apply();
|
||||
modelBuilder.Entity<GameConfiguration>().Apply();
|
||||
modelBuilder.Entity<GameMapDefinition>().Apply();
|
||||
modelBuilder.Entity<ItemCrafting>().Apply();
|
||||
modelBuilder.Entity<ItemDefinition>().Apply();
|
||||
modelBuilder.Entity<ItemLevelBonusTable>().Apply();
|
||||
modelBuilder.Entity<ItemDropItemGroup>().Apply();
|
||||
modelBuilder.Entity<ItemOptionCombinationBonus>().Apply();
|
||||
modelBuilder.Entity<ItemOptionDefinition>().Apply();
|
||||
modelBuilder.Entity<ItemOptionType>().Apply();
|
||||
modelBuilder.Entity<ItemSetGroup>().Apply();
|
||||
modelBuilder.Entity<ItemSlotType>().Apply();
|
||||
modelBuilder.Entity<ItemStorage>().Apply();
|
||||
modelBuilder.Entity<ItemBasePowerUpDefinition>().Apply();
|
||||
modelBuilder.Entity<LevelBonus>().Apply();
|
||||
modelBuilder.Entity<MagicEffectDefinition>().Apply();
|
||||
modelBuilder.Entity<MasterSkillRoot>().Apply();
|
||||
modelBuilder.Entity<MiniGameChangeEvent>().Apply();
|
||||
modelBuilder.Entity<MiniGameDefinition>().Apply();
|
||||
modelBuilder.Entity<MiniGameSpawnWave>().Apply();
|
||||
modelBuilder.Entity<MonsterDefinition>().Apply();
|
||||
modelBuilder.Entity<MonsterSpawnArea>().Apply();
|
||||
modelBuilder.Entity<Skill>().Apply();
|
||||
modelBuilder.Entity<SkillComboDefinition>().Apply();
|
||||
modelBuilder.Entity<SkillEntry>().Apply();
|
||||
modelBuilder.Entity<MasterSkillDefinition>().Apply();
|
||||
modelBuilder.Entity<LetterBody>().Apply();
|
||||
modelBuilder.Entity<LetterHeader>().Apply();
|
||||
modelBuilder.Entity<QuestDefinition>().Apply();
|
||||
modelBuilder.Entity<WarpInfo>().Apply();
|
||||
|
||||
// join entity keys:
|
||||
this.AddJoinDefinitions(modelBuilder);
|
||||
|
||||
modelBuilder.UseGuidV7Ids();
|
||||
|
||||
GuildContext.ConfigureModel(modelBuilder);
|
||||
FriendContext.ConfigureModel(modelBuilder);
|
||||
}
|
||||
}
|
||||
24
src/Persistence/EntityFramework/EntityDataContextFactory.cs
Normal file
24
src/Persistence/EntityFramework/EntityDataContextFactory.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
// <copyright file="EntityDataContextFactory.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 Microsoft.EntityFrameworkCore.Design;
|
||||
|
||||
/// <summary>
|
||||
/// Design-time factory for <see cref="EntityDataContext"/>.
|
||||
/// </summary>
|
||||
public class EntityDataContextFactory : IDesignTimeDbContextFactory<EntityDataContext>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public EntityDataContext CreateDbContext(string[] args)
|
||||
{
|
||||
if (!ConnectionConfigurator.IsInitialized)
|
||||
{
|
||||
ConnectionConfigurator.Initialize(new ConfigFileDatabaseConnectionStringProvider());
|
||||
}
|
||||
|
||||
return new EntityDataContext();
|
||||
}
|
||||
}
|
||||
28
src/Persistence/EntityFramework/EntityFrameworkContext.cs
Normal file
28
src/Persistence/EntityFramework/EntityFrameworkContext.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
// <copyright file="EntityFrameworkContext.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 Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// A implementation of <see cref="EntityFrameworkContextBase"/> which doesn't cache and always asks the database for objects.
|
||||
/// </summary>
|
||||
internal class EntityFrameworkContext : EntityFrameworkContextBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="EntityFrameworkContext" /> class.
|
||||
/// </summary>
|
||||
/// <param name="context">The db context.</param>
|
||||
/// <param name="loggerFactory">The logger factory.</param>
|
||||
/// <param name="repositoryProvider">The repository provider.</param>
|
||||
/// <param name="isOwner">If set to <c>true</c>, this instance owns the <see cref="EntityFrameworkContextBase.Context" />. That means it will be disposed when this instance will be disposed.</param>
|
||||
/// <param name="changeListener">The change listener.</param>
|
||||
public EntityFrameworkContext(DbContext context, ILoggerFactory loggerFactory, IContextAwareRepositoryProvider repositoryProvider, bool isOwner, IConfigurationChangeListener? changeListener)
|
||||
: base(context, repositoryProvider, isOwner, changeListener, loggerFactory.CreateLogger<EntityFrameworkContext>())
|
||||
{
|
||||
}
|
||||
}
|
||||
416
src/Persistence/EntityFramework/EntityFrameworkContextBase.cs
Normal file
416
src/Persistence/EntityFramework/EntityFrameworkContextBase.cs
Normal file
@@ -0,0 +1,416 @@
|
||||
// <copyright file="EntityFrameworkContextBase.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.Collections;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.DataModel.Composition;
|
||||
using MUnique.OpenMU.DataModel.Configuration;
|
||||
using Nito.AsyncEx;
|
||||
using Nito.Disposables;
|
||||
|
||||
/// <summary>
|
||||
/// Abstract base class for an <see cref="IContext"/> which uses an <see cref="DbContext"/>.
|
||||
/// </summary>
|
||||
internal class EntityFrameworkContextBase : IContext
|
||||
{
|
||||
private readonly bool _isOwner;
|
||||
private readonly IConfigurationChangeListener? _changeListener;
|
||||
private readonly AsyncLock _lock = new();
|
||||
private readonly ILogger _logger;
|
||||
private bool _isDisposed;
|
||||
private int _notificationSuspensions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="EntityFrameworkContextBase" /> class.
|
||||
/// </summary>
|
||||
/// <param name="context">The db context.</param>
|
||||
/// <param name="repositoryProvider">The repository provider.</param>
|
||||
/// <param name="isOwner">If set to <c>true</c>, this instance owns the <see cref="Context" />. That means it will be disposed when this instance will be disposed.</param>
|
||||
/// <param name="changeListener">The change listener.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
protected EntityFrameworkContextBase(DbContext context, IContextAwareRepositoryProvider repositoryProvider, bool isOwner, IConfigurationChangeListener? changeListener, ILogger logger)
|
||||
{
|
||||
this.Context = context;
|
||||
this.RepositoryProvider = repositoryProvider;
|
||||
this._isOwner = isOwner;
|
||||
this._changeListener = changeListener;
|
||||
this._logger = logger;
|
||||
|
||||
// Ensure that the model is created.
|
||||
_ = context.Model;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finalizes an instance of the <see cref="EntityFrameworkContextBase"/> class.
|
||||
/// </summary>
|
||||
~EntityFrameworkContextBase() => this.Dispose(false);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool HasChanges => this.Context.ChangeTracker.HasChanges();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the entity framework context.
|
||||
/// </summary>
|
||||
internal DbContext Context { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the repository provider.
|
||||
/// </summary>
|
||||
protected IContextAwareRepositoryProvider RepositoryProvider { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<bool> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var l = await this._lock.LockAsync();
|
||||
|
||||
// when we have a change publisher attached, we want to get the changed entries before accepting them.
|
||||
// Otherwise, we can accept them.
|
||||
var acceptChanges = true;
|
||||
|
||||
object? sender = null;
|
||||
SavedChangesEventArgs? args = null;
|
||||
if (this._changeListener is { })
|
||||
{
|
||||
this.Context.SavedChanges += OnSavedChanges;
|
||||
acceptChanges = false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await this.Context.SaveChangesAsync(acceptChanges, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (args is not null)
|
||||
{
|
||||
await this.OnSavedChangesAsync(sender, args).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.Context.SavedChanges -= OnSavedChanges;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
void OnSavedChanges(object? s, SavedChangesEventArgs e)
|
||||
{
|
||||
sender = s;
|
||||
args = e;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IDisposable SuspendChangeNotifications()
|
||||
{
|
||||
Interlocked.Increment(ref this._notificationSuspensions);
|
||||
return new Disposable(() => Interlocked.Decrement(ref this._notificationSuspensions));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Detach(object item)
|
||||
{
|
||||
using var l = this._lock.Lock();
|
||||
return this.DetachInternal(item);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Attach(object item)
|
||||
{
|
||||
using var l = this._lock.Lock();
|
||||
this.Context.Attach(item);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public T CreateNew<T>(params object?[] args)
|
||||
where T : class
|
||||
{
|
||||
using var l = this._lock.Lock();
|
||||
var instance = typeof(CachingEntityFrameworkContext).Assembly.CreateNew<T>(args);
|
||||
this.Context.Add(instance);
|
||||
return instance;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public object CreateNew(Type type, params object?[] args)
|
||||
{
|
||||
using var l = this._lock.Lock();
|
||||
var instance = typeof(CachingEntityFrameworkContext).Assembly.CreateNew(type, args);
|
||||
this.Context.Add(instance);
|
||||
return instance;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<bool> DeleteAsync<T>(T obj)
|
||||
where T : class
|
||||
{
|
||||
using var l = await this._lock.LockAsync();
|
||||
|
||||
var result = false;
|
||||
var entry = this.Context.Entry(obj);
|
||||
if (entry.State == EntityState.Detached)
|
||||
{
|
||||
this.Context.Attach(obj);
|
||||
entry = this.Context.Entry(obj);
|
||||
}
|
||||
|
||||
switch (entry.State)
|
||||
{
|
||||
case EntityState.Detached:
|
||||
return true;
|
||||
case EntityState.Added:
|
||||
this.DetachInternal(obj);
|
||||
break;
|
||||
default:
|
||||
this.Context.Remove(obj);
|
||||
this.ForEachAggregate(obj, a => this.Context.Remove(a));
|
||||
break;
|
||||
}
|
||||
|
||||
result = true;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<T?> GetByIdAsync<T>(Guid id, CancellationToken cancellationToken)
|
||||
where T : class
|
||||
{
|
||||
using var l = await this._lock.LockAsync(cancellationToken);
|
||||
using var context = this.RepositoryProvider.ContextStack.UseContext(this);
|
||||
return await this.GetRepository<T>().GetByIdAsync(id, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<object?> 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<IEnumerable<T>> GetAsync<T>(CancellationToken cancellationToken)
|
||||
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<T>().GetAllAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<IEnumerable> 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsSupporting(Type type)
|
||||
{
|
||||
var currentSearchType = type;
|
||||
do
|
||||
{
|
||||
if (currentSearchType is null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (this.Context.Model.FindLeastDerivedEntityTypes(currentSearchType).FirstOrDefault() is not null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (currentSearchType.Name != currentSearchType.BaseType?.Name)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
currentSearchType = currentSearchType.BaseType;
|
||||
}
|
||||
while (currentSearchType != typeof(object));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
if (!this._isDisposed)
|
||||
{
|
||||
this.Dispose(true);
|
||||
}
|
||||
|
||||
this._isDisposed = true;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
/// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
protected virtual void Dispose(bool dispose)
|
||||
{
|
||||
if (!dispose || !this._isOwner)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.Context.Dispose();
|
||||
}
|
||||
|
||||
private bool DetachInternal(object item)
|
||||
{
|
||||
var entry = this.Context.Entry(item);
|
||||
if (entry is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var previousState = entry.State;
|
||||
entry.State = EntityState.Detached;
|
||||
this.ForEachAggregate(item, obj => this.DetachInternal(obj));
|
||||
|
||||
return previousState != EntityState.Added;
|
||||
}
|
||||
|
||||
private IRepository<T> GetRepository<T>()
|
||||
where T : class
|
||||
{
|
||||
if (this.RepositoryProvider.GetRepository<T>() is { } repository)
|
||||
{
|
||||
return repository;
|
||||
}
|
||||
|
||||
throw new RepositoryNotFoundException(typeof(T));
|
||||
}
|
||||
|
||||
private IRepository GetRepository(Type type)
|
||||
{
|
||||
if (this.RepositoryProvider.GetRepository(type) is { } repository)
|
||||
{
|
||||
return repository;
|
||||
}
|
||||
|
||||
throw new RepositoryNotFoundException(type);
|
||||
}
|
||||
|
||||
private void ForEachAggregate(object obj, Action<object> action)
|
||||
{
|
||||
var aggregateProperties = obj.GetType()
|
||||
.GetProperties(BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(p => p.GetCustomAttribute<MemberOfAggregateAttribute>() is { }
|
||||
|| p.Name.StartsWith("Joined"));
|
||||
foreach (var propertyInfo in aggregateProperties)
|
||||
{
|
||||
var propertyValue = propertyInfo.GetMethod?.Invoke(obj, []);
|
||||
if (propertyValue is IEnumerable enumerable)
|
||||
{
|
||||
foreach (var value in enumerable)
|
||||
{
|
||||
action(value);
|
||||
}
|
||||
}
|
||||
else if (propertyValue is { })
|
||||
{
|
||||
action(propertyValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "Catching all Exceptions.")]
|
||||
private async ValueTask OnSavedChangesAsync(object? sender, SavedChangesEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (this._changeListener is null || this._notificationSuspensions > 0)
|
||||
{
|
||||
// should never be the case
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.EntitiesSavedCount == 0)
|
||||
{
|
||||
// why are we getting this event then anyway?
|
||||
return;
|
||||
}
|
||||
|
||||
var changedEntries = this.Context.ChangeTracker.Entries()
|
||||
.Where(entity => entity.State != EntityState.Unchanged).ToList();
|
||||
foreach (var entry in changedEntries)
|
||||
{
|
||||
var (parent, parentCollectionNavigation) = this.GetParentInformation(entry);
|
||||
|
||||
switch (entry.State)
|
||||
{
|
||||
case EntityState.Added:
|
||||
await this._changeListener.ConfigurationAddedAsync(entry.Metadata.ClrType, entry.Entity.GetId(), entry.Entity, parent, parentCollectionNavigation).ConfigureAwait(false);
|
||||
break;
|
||||
case EntityState.Deleted:
|
||||
await this._changeListener.ConfigurationRemovedAsync(entry.Metadata.ClrType, entry.Entity.GetId(), parent, parentCollectionNavigation).ConfigureAwait(false);
|
||||
break;
|
||||
case EntityState.Modified:
|
||||
await this._changeListener.ConfigurationChangedAsync(entry.Metadata.ClrType, entry.Entity.GetId(), entry.Entity, parent).ConfigureAwait(false);
|
||||
break;
|
||||
default:
|
||||
// no change publishing required.
|
||||
break;
|
||||
}
|
||||
|
||||
if (parent is not null && parent is not GameConfiguration && parent is not Guid)
|
||||
{
|
||||
await this._changeListener.ConfigurationChangedAsync(parent.GetType(), parent.GetId(), parent, null).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Unexpected error publishing changes.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
this.Context.ChangeTracker.AcceptAllChanges();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Unexpected error when accepting all saved changes.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private (object? Parent, INavigationBase? ParentCollectionNavigation) GetParentInformation(EntityEntry entry)
|
||||
{
|
||||
var propertyToParent = entry.Properties
|
||||
.FirstOrDefault(p => p.Metadata.IsForeignKey()
|
||||
&& (p.Metadata.IsShadowProperty() || p.Metadata.PropertyInfo?.GetCustomAttribute<IsLinkToParentAttribute>() is not null));
|
||||
|
||||
var parentId = (Guid?)(propertyToParent?.CurrentValue ?? propertyToParent?.OriginalValue);
|
||||
if (parentId is null)
|
||||
{
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
object? parent = null;
|
||||
INavigationBase? parentCollectionNavigation = null;
|
||||
var parentEntry = this.Context.ChangeTracker.Entries().FirstOrDefault(e => e.Entity.GetId() == parentId);
|
||||
if (parentEntry is not null && propertyToParent is not null)
|
||||
{
|
||||
parent = parentEntry.Entity;
|
||||
var parentCollection = parentEntry.Collections
|
||||
.FirstOrDefault(c => c.Metadata.IsCollection
|
||||
&& (c.Metadata as INavigation)?.ForeignKey == propertyToParent.Metadata.GetContainingForeignKeys().FirstOrDefault());
|
||||
parentCollectionNavigation = parentCollection?.Metadata;
|
||||
}
|
||||
|
||||
return (parent ?? parentId, parentCollectionNavigation);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// <copyright file="LocalizedStringConverter.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.Extensions;
|
||||
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// A value converter for <see cref="LocalizedString" /> which stores only the underlying string value.
|
||||
/// </summary>
|
||||
internal class LocalizedStringConverter : ValueConverter<LocalizedString, string>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LocalizedStringConverter" /> class.
|
||||
/// </summary>
|
||||
private LocalizedStringConverter()
|
||||
: base(value => value.Value ?? string.Empty, value => new LocalizedString(value))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the singleton instance of the <see cref="LocalizedStringConverter"/>.
|
||||
/// </summary>
|
||||
public static LocalizedStringConverter Instance { get; } = new();
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// <copyright file="AccountExtensions.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.Extensions.ModelBuilder;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for the <see cref="EntityTypeBuilder{Account}"/>.
|
||||
/// </summary>
|
||||
internal static class AccountExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies the settings for the <see cref="Account"/> entity.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder.</param>
|
||||
public static void Apply(this EntityTypeBuilder<Account> builder)
|
||||
{
|
||||
builder.Property(account => account.LoginName).HasMaxLength(10).IsRequired();
|
||||
builder.HasIndex(account => account.LoginName).IsUnique();
|
||||
|
||||
builder.Property(account => account.LanguageIsoCode).HasMaxLength(3).IsRequired().HasDefaultValue("en");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// <copyright file="AttributeExtensions.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.Extensions.ModelBuilder;
|
||||
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for the <see cref="Attribute"/>-related <see cref="EntityTypeBuilder"/>s.
|
||||
/// </summary>
|
||||
internal static class AttributeExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies the settings for the <see cref="ConstValueAttribute"/> entity.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder.</param>
|
||||
public static void Apply(this EntityTypeBuilder<ConstValueAttribute> builder)
|
||||
{
|
||||
builder.Ignore(c => c.AggregateType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the settings for the <see cref="PowerUpDefinitionValue"/> entity.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder.</param>
|
||||
public static void Apply(this EntityTypeBuilder<PowerUpDefinitionValue> builder)
|
||||
{
|
||||
builder.Ignore(p => p.ConstantValue);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// <copyright file="CharacterExtensions.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.Extensions.ModelBuilder;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for the <see cref="EntityTypeBuilder{Account}"/>.
|
||||
/// </summary>
|
||||
internal static class CharacterExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies the settings for the <see cref="Character"/> entity.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder.</param>
|
||||
public static void Apply(this EntityTypeBuilder<Character> builder)
|
||||
{
|
||||
builder.Property(character => character.Name).HasMaxLength(10).IsRequired();
|
||||
builder.HasIndex(character => character.Name).IsUnique();
|
||||
|
||||
if (builder.Metadata.FindNavigation(nameof(Character.RawCharacterClass)) is { } navigation)
|
||||
{
|
||||
navigation.ForeignKey.IsRequired = true;
|
||||
}
|
||||
|
||||
builder.Property(character => character.CharacterSlot).IsRequired();
|
||||
builder.HasMany(character => character.RawLetters).WithOne(letter => letter.Receiver!).OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the settings for the <see cref="CharacterClass"/> entity.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder.</param>
|
||||
public static void Apply(this EntityTypeBuilder<CharacterClass> builder)
|
||||
{
|
||||
builder.Property(p => p.Name).HasConversion(LocalizedStringConverter.Instance);
|
||||
builder.HasMany(c => c.RawBaseAttributeValues)
|
||||
.WithOne(c => c.CharacterClass);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// <copyright file="ExitGateExtensions.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.Extensions.ModelBuilder;
|
||||
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for the <see cref="EntityTypeBuilder{ExitGate}"/>.
|
||||
/// </summary>
|
||||
internal static class ExitGateExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies the settings for the <see cref="ExitGate"/> entity.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder.</param>
|
||||
public static void Apply(this EntityTypeBuilder<ExitGate> builder)
|
||||
{
|
||||
builder.HasOne(gate => gate.RawMap);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// <copyright file="GameConfigurationExtensions.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.Extensions.ModelBuilder;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for the <see cref="EntityTypeBuilder{GameConfiguration}"/>.
|
||||
/// </summary>
|
||||
internal static class GameConfigurationExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies the settings for the <see cref="GameConfiguration"/> entity.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder.</param>
|
||||
public static void Apply(this EntityTypeBuilder<GameConfiguration> builder)
|
||||
{
|
||||
builder.Property(c => c.ItemDropDuration).HasDefaultValue(TimeSpan.FromSeconds(60));
|
||||
builder.Property(c => c.ExcellentItemDropLevelDelta).HasDefaultValue((byte)25);
|
||||
builder.Property(c => c.ExperienceFormula).HasDefaultValue("if(level == 0, 0, if(level < 256, 10 * (level + 8) * (level - 1) * (level - 1), (10 * (level + 8) * (level - 1) * (level - 1)) + (1000 * (level - 247) * (level - 256) * (level - 256))))");
|
||||
builder.Property(c => c.MasterExperienceFormula).HasDefaultValue("(505 * level * level * level) + (35278500 * level) + (228045 * level * level)");
|
||||
builder.HasMany(c => c.RawGlobalBaseAttributeValues).WithOne(c => c.GameConfiguration);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the settings for the <see cref="ConfigurationUpdate"/> entity.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder.</param>
|
||||
public static void Apply(this EntityTypeBuilder<ConfigurationUpdate> builder)
|
||||
{
|
||||
builder.Property(p => p.Name).HasConversion(LocalizedStringConverter.Instance);
|
||||
builder.Property(p => p.Description).HasConversion(LocalizedStringConverter.Instance);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// <copyright file="GameMapDefinitionExtensions.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.Extensions.ModelBuilder;
|
||||
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for the <see cref="EntityTypeBuilder{GameMapDefinition}"/>.
|
||||
/// </summary>
|
||||
internal static class GameMapDefinitionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies the settings for the <see cref="GameMapDefinition"/> entity.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder.</param>
|
||||
public static void Apply(this EntityTypeBuilder<GameMapDefinition> builder)
|
||||
{
|
||||
builder.Property(p => p.Name).HasConversion(LocalizedStringConverter.Instance);
|
||||
builder.HasMany(map => map.RawEnterGates);
|
||||
builder.HasMany(map => map.RawExitGates).WithOne(g => g.RawMap);
|
||||
builder.HasOne(map => map.RawSafezoneMap);
|
||||
builder.HasMany(map => map.RawMonsterSpawns);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the settings for the <see cref="WarpInfo"/> entity.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder.</param>
|
||||
public static void Apply(this EntityTypeBuilder<WarpInfo> builder)
|
||||
{
|
||||
builder.Property(p => p.Name).HasConversion(LocalizedStringConverter.Instance);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
// <copyright file="ItemExtensions.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.Extensions.ModelBuilder;
|
||||
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for the item-related <see cref="EntityTypeBuilder"/>.
|
||||
/// </summary>
|
||||
internal static class ItemExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies the settings for the <see cref="ItemDefinition"/> entity.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder.</param>
|
||||
public static void Apply(this EntityTypeBuilder<ItemDefinition> builder)
|
||||
{
|
||||
builder.Property(p => p.Name).HasConversion(LocalizedStringConverter.Instance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the settings for the <see cref="ItemStorage"/> entity.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder.</param>
|
||||
public static void Apply(this EntityTypeBuilder<ItemStorage> builder)
|
||||
{
|
||||
builder.HasMany(storage => storage.RawItems).WithOne(item => item.RawItemStorage!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the settings for the <see cref="ItemSetGroup"/> entity.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder.</param>
|
||||
public static void Apply(this EntityTypeBuilder<ItemSetGroup> builder)
|
||||
{
|
||||
builder.Property(p => p.Name).HasConversion(LocalizedStringConverter.Instance);
|
||||
builder.HasMany(isg => isg.RawItems).WithOne(item => item.RawItemSetGroup!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the settings for the <see cref="ItemBasePowerUpDefinition"/> entity.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder.</param>
|
||||
public static void Apply(this EntityTypeBuilder<ItemBasePowerUpDefinition> builder)
|
||||
{
|
||||
builder.Ignore(d => d.BaseValueElement);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the settings for the <see cref="ItemLevelBonusTable"/> entity.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder.</param>
|
||||
public static void Apply(this EntityTypeBuilder<ItemLevelBonusTable> builder)
|
||||
{
|
||||
builder.Property(p => p.Name).HasConversion(LocalizedStringConverter.Instance);
|
||||
builder.Property(p => p.Description).HasConversion(LocalizedStringConverter.Instance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the settings for the <see cref="ItemDropItemGroup"/> entity.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder.</param>
|
||||
public static void Apply(this EntityTypeBuilder<ItemDropItemGroup> builder)
|
||||
{
|
||||
builder.Property(p => p.Description).HasConversion(LocalizedStringConverter.Instance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the settings for the <see cref="LevelBonus"/> entity.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder.</param>
|
||||
public static void Apply(this EntityTypeBuilder<LevelBonus> builder)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the settings for the <see cref="ItemSlotType"/> entity.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder.</param>
|
||||
public static void Apply(this EntityTypeBuilder<ItemSlotType> builder)
|
||||
{
|
||||
builder.Property(p => p.Description).HasConversion(LocalizedStringConverter.Instance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the settings for the <see cref="ItemOptionCombinationBonus"/> entity.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder.</param>
|
||||
public static void Apply(this EntityTypeBuilder<ItemOptionCombinationBonus> builder)
|
||||
{
|
||||
builder.Property(p => p.Description).HasConversion(LocalizedStringConverter.Instance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the settings for the <see cref="ItemOptionDefinition"/> entity.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder.</param>
|
||||
public static void Apply(this EntityTypeBuilder<ItemOptionDefinition> builder)
|
||||
{
|
||||
builder.Property(p => p.Name).HasConversion(LocalizedStringConverter.Instance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the settings for the <see cref="ItemOptionType"/> entity.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder.</param>
|
||||
public static void Apply(this EntityTypeBuilder<ItemOptionType> builder)
|
||||
{
|
||||
builder.Property(p => p.Name).HasConversion(LocalizedStringConverter.Instance);
|
||||
builder.Property(p => p.Description).HasConversion(LocalizedStringConverter.Instance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the settings for the <see cref="ItemCrafting"/> entity.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder.</param>
|
||||
public static void Apply(this EntityTypeBuilder<ItemCrafting> builder)
|
||||
{
|
||||
builder.Property(p => p.Name).HasConversion(LocalizedStringConverter.Instance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the settings for the <see cref="DropItemGroup"/> entity.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder.</param>
|
||||
public static void Apply(this EntityTypeBuilder<DropItemGroup> builder)
|
||||
{
|
||||
builder.Property(p => p.Description).HasConversion(LocalizedStringConverter.Instance);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// <copyright file="LetterExtensions.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.Extensions.ModelBuilder;
|
||||
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for the <see cref="EntityTypeBuilder{LetterBody}"/> and <see cref="EntityTypeBuilder{LetterHeader}"/>.
|
||||
/// </summary>
|
||||
internal static class LetterExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies the settings for the <see cref="LetterBody"/> entity.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder.</param>
|
||||
public static void Apply(this EntityTypeBuilder<LetterBody> builder)
|
||||
{
|
||||
builder.HasOne(body => body.RawHeader);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the settings for the <see cref="LetterHeader"/> entity.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder.</param>
|
||||
public static void Apply(this EntityTypeBuilder<LetterHeader> builder)
|
||||
{
|
||||
builder.Ignore(header => header.ReceiverName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// <copyright file="MiniGameExtensions.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.Extensions.ModelBuilder;
|
||||
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for the <see cref="MiniGameDefinition"/>-related <see cref="EntityTypeBuilder"/>.
|
||||
/// </summary>
|
||||
internal static class MiniGameExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies the settings for the <see cref="MiniGameChangeEvent"/> entity.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder.</param>
|
||||
public static void Apply(this EntityTypeBuilder<MiniGameChangeEvent> builder)
|
||||
{
|
||||
builder.Property(p => p.Description).HasConversion(LocalizedStringConverter.Instance);
|
||||
builder.Property(p => p.Message).HasConversion(LocalizedStringConverter.Instance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the settings for the <see cref="MiniGameSpawnWave"/> entity.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder.</param>
|
||||
public static void Apply(this EntityTypeBuilder<MiniGameSpawnWave> builder)
|
||||
{
|
||||
builder.Property(p => p.Description).HasConversion(LocalizedStringConverter.Instance);
|
||||
builder.Property(p => p.Message).HasConversion(LocalizedStringConverter.Instance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the settings for the <see cref="MiniGameDefinition"/> entity.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder.</param>
|
||||
public static void Apply(this EntityTypeBuilder<MiniGameDefinition> builder)
|
||||
{
|
||||
builder.Property(p => p.Name).HasConversion(LocalizedStringConverter.Instance);
|
||||
builder.Property(p => p.Description).HasConversion(LocalizedStringConverter.Instance);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// <copyright file="MonsterExtensions.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.Extensions.ModelBuilder;
|
||||
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for the <see cref="MonsterDefinition"/>-related <see cref="EntityTypeBuilder"/>s.
|
||||
/// </summary>
|
||||
internal static class MonsterExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies the settings for the <see cref="MonsterDefinition"/> entity.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder.</param>
|
||||
public static void Apply(this EntityTypeBuilder<MonsterDefinition> builder)
|
||||
{
|
||||
builder.HasMany<QuestDefinition>().WithOne(q => q.RawQuestGiver);
|
||||
builder.Property(m => m.Designation).HasConversion(LocalizedStringConverter.Instance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the settings for the <see cref="MonsterSpawnArea"/> entity.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder.</param>
|
||||
public static void Apply(this EntityTypeBuilder<MonsterSpawnArea> builder)
|
||||
{
|
||||
builder.HasOne(spawn => spawn.RawMonsterDefinition);
|
||||
builder.HasOne(spawn => spawn.RawGameMap).WithMany(map => map.RawMonsterSpawns);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// <copyright file="QuestExtensions.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.Extensions.ModelBuilder;
|
||||
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for the item-related <see cref="EntityTypeBuilder"/>.
|
||||
/// </summary>
|
||||
internal static class QuestExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies the settings for the <see cref="QuestDefinition"/> entity.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder.</param>
|
||||
public static void Apply(this EntityTypeBuilder<QuestDefinition> builder)
|
||||
{
|
||||
builder.Property(p => p.Name).HasConversion(LocalizedStringConverter.Instance);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// <copyright file="SkillExtensions.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.Extensions.ModelBuilder;
|
||||
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for the <see cref="Skill"/>-related <see cref="EntityTypeBuilder"/>.
|
||||
/// </summary>
|
||||
internal static class SkillExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies the settings for the <see cref="Skill"/> entity.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder.</param>
|
||||
public static void Apply(this EntityTypeBuilder<Skill> builder)
|
||||
{
|
||||
builder.Property(p => p.Name).HasConversion(LocalizedStringConverter.Instance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the settings for the <see cref="Skill"/> entity.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder.</param>
|
||||
public static void Apply(this EntityTypeBuilder<SkillComboDefinition> builder)
|
||||
{
|
||||
builder.Property(p => p.Name).HasConversion(LocalizedStringConverter.Instance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the settings for the <see cref="SkillEntry"/> entity.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder.</param>
|
||||
public static void Apply(this EntityTypeBuilder<SkillEntry> builder)
|
||||
{
|
||||
builder.Ignore(s => s.PowerUps);
|
||||
builder.Ignore(s => s.PowerUpsPvp);
|
||||
builder.Ignore(s => s.PowerUpDuration);
|
||||
builder.Ignore(s => s.PowerUpDurationPvp);
|
||||
builder.Ignore(s => s.PowerUpChance);
|
||||
builder.Ignore(s => s.PowerUpChancePvp);
|
||||
builder.Ignore(s => s.Attributes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the settings for the <see cref="MasterSkillRoot"/> entity.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder.</param>
|
||||
public static void Apply(this EntityTypeBuilder<MasterSkillRoot> builder)
|
||||
{
|
||||
builder.Property(p => p.Name).HasConversion(LocalizedStringConverter.Instance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the settings for the <see cref="MasterSkillDefinition"/> entity.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder.</param>
|
||||
public static void Apply(this EntityTypeBuilder<MasterSkillDefinition> builder)
|
||||
{
|
||||
builder.HasOne(s => s.RawRoot);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the settings for the <see cref="MagicEffectDefinition"/> entity.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder.</param>
|
||||
public static void Apply(this EntityTypeBuilder<MagicEffectDefinition> builder)
|
||||
{
|
||||
builder.Property(p => p.Name).HasConversion(LocalizedStringConverter.Instance);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// <copyright file="ModelBuilderExtensions.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.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for <see cref="Microsoft.EntityFrameworkCore.ModelBuilder"/>.
|
||||
/// </summary>
|
||||
internal static class ModelBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Configures the model builder to use UUID V7 as primary keys.
|
||||
/// </summary>
|
||||
/// <param name="modelBuilder">The model builder to configure.</param>
|
||||
/// <returns>The configured model builder with UUID V7 key generation applied.</returns>
|
||||
public static Microsoft.EntityFrameworkCore.ModelBuilder UseGuidV7Ids(this Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder)
|
||||
{
|
||||
var types = modelBuilder.Model.GetEntityTypes();
|
||||
foreach (var t in types)
|
||||
{
|
||||
var entity = modelBuilder.Entity(t.ClrType);
|
||||
var key = entity.Metadata.FindProperty("Id");
|
||||
if (key != null)
|
||||
{
|
||||
key.ValueGenerated = Microsoft.EntityFrameworkCore.Metadata.ValueGenerated.OnAdd;
|
||||
key.SetValueGeneratorFactory((_, _) => new GuidV7ValueGenerator());
|
||||
}
|
||||
}
|
||||
|
||||
return modelBuilder;
|
||||
}
|
||||
}
|
||||
43
src/Persistence/EntityFramework/FriendContext.cs
Normal file
43
src/Persistence/EntityFramework/FriendContext.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
// <copyright file="FriendContext.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 Microsoft.EntityFrameworkCore;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework.Extensions;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Context to load instances of <see cref="Friend"/>s.
|
||||
/// </summary>
|
||||
public class FriendContext : DbContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Configures the model.
|
||||
/// </summary>
|
||||
/// <param name="modelBuilder">The model builder.</param>
|
||||
internal static void ConfigureModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<Friend>().ToTable("Friend", SchemaNames.Friend);
|
||||
modelBuilder.Entity<Friend>(e =>
|
||||
{
|
||||
e.HasAlternateKey(f => new { f.CharacterId, f.FriendId });
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
base.OnConfiguring(optionsBuilder);
|
||||
this.Configure(optionsBuilder);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
ConfigureModel(modelBuilder);
|
||||
modelBuilder.Entity<CharacterName>().HasKey(f => f.Id);
|
||||
modelBuilder.UseGuidV7Ids();
|
||||
}
|
||||
}
|
||||
95
src/Persistence/EntityFramework/FriendServerContext.cs
Normal file
95
src/Persistence/EntityFramework/FriendServerContext.cs
Normal file
@@ -0,0 +1,95 @@
|
||||
// <copyright file="FriendServerContext.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 Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework.Model;
|
||||
|
||||
/// <summary>
|
||||
/// A context which is used by the <see cref="IFriendServer"/>.
|
||||
/// </summary>
|
||||
internal class FriendServerContext : CachingEntityFrameworkContext, IFriendServerContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FriendServerContext" /> class.
|
||||
/// </summary>
|
||||
/// <param name="context">The context.</param>
|
||||
/// <param name="repositoryProvider">The repositoryManager.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public FriendServerContext(FriendContext context, IContextAwareRepositoryProvider repositoryProvider, ILogger<FriendServerContext> logger)
|
||||
: base(context, repositoryProvider, null, logger)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<Interfaces.Friend> CreateNewFriendAsync(string characterName, string friendName)
|
||||
{
|
||||
var item = this.CreateNew<Model.Friend>();
|
||||
item.CharacterId = await this.GetCharacterIdByNameAsync(characterName).ConfigureAwait(false) ?? Guid.Empty;
|
||||
item.FriendId = await this.GetCharacterIdByNameAsync(friendName).ConfigureAwait(false) ?? Guid.Empty;
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask DeleteAsync(string characterName, string friendName)
|
||||
{
|
||||
this.Context.RemoveRange(await this.FindItems(characterName, friendName).ToListAsync().ConfigureAwait(false));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<Interfaces.Friend?> GetFriendByNamesAsync(string characterName, string friendName)
|
||||
{
|
||||
return await this.FindItems(characterName, friendName).FirstOrDefaultAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<IEnumerable<FriendViewItem>> GetFriendsAsync(Guid characterId)
|
||||
{
|
||||
return await (from friend in this.Context.Set<Model.Friend>()
|
||||
join friendCharacter in this.Context.Set<CharacterName>() on friend.FriendId equals friendCharacter.Id
|
||||
join character in this.Context.Set<CharacterName>() on friend.CharacterId equals character.Id
|
||||
select new FriendViewItem(character.Name, friendCharacter.Name)
|
||||
{
|
||||
Id = friend.Id,
|
||||
CharacterId = friend.CharacterId,
|
||||
FriendId = friend.FriendId,
|
||||
Accepted = friend.Accepted,
|
||||
RequestOpen = friend.RequestOpen,
|
||||
}).ToListAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<IEnumerable<string>> GetFriendNamesAsync(Guid characterId)
|
||||
{
|
||||
return await (from friend in this.Context.Set<Model.Friend>()
|
||||
where friend.CharacterId == characterId
|
||||
join friendCharacter in this.Context.Set<CharacterName>() on friend.FriendId equals friendCharacter.Id
|
||||
select friendCharacter.Name).ToListAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<IEnumerable<string>> GetOpenFriendRequesterNamesAsync(Guid characterId)
|
||||
{
|
||||
return await (from friend in this.Context.Set<Model.Friend>()
|
||||
where friend.RequestOpen == true && friend.FriendId == characterId
|
||||
join requester in this.Context.Set<CharacterName>() on friend.CharacterId equals requester.Id
|
||||
select requester.Name).ToListAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private IQueryable<Model.Friend> FindItems(string characterName, string friendName)
|
||||
{
|
||||
return from friend in this.Context.Set<Model.Friend>()
|
||||
join friendCharacter in this.Context.Set<CharacterName>() on friend.FriendId equals friendCharacter.Id
|
||||
join character in this.Context.Set<CharacterName>() on friend.CharacterId equals character.Id
|
||||
where friendCharacter.Name == friendName && character.Name == characterName
|
||||
select friend;
|
||||
}
|
||||
|
||||
private async ValueTask<Guid?> GetCharacterIdByNameAsync(string name) => await this.Context.Set<CharacterName>().Where(character => character.Name == name).Select(character => character.Id).FirstOrDefaultAsync().ConfigureAwait(false);
|
||||
}
|
||||
32
src/Persistence/EntityFramework/GameConfigurationContext.cs
Normal file
32
src/Persistence/EntityFramework/GameConfigurationContext.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
// <copyright file="GameConfigurationContext.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.Threading;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Persistence context which is used to access the <see cref="GameConfiguration"/>.
|
||||
/// </summary>
|
||||
internal class GameConfigurationContext : CachingEntityFrameworkContext, IConfigurationContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GameConfigurationContext"/> class.
|
||||
/// </summary>
|
||||
/// <param name="repositoryProvider">The repository provider.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public GameConfigurationContext(IContextAwareRepositoryProvider repositoryProvider, ILogger<GameConfigurationContext> logger)
|
||||
: base(new ConfigurationContext(), repositoryProvider, null, logger)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<Guid?> GetDefaultGameConfigurationIdAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return await this.Context.Set<GameConfiguration>().Select(g => g.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// <copyright file="GameConfigurationRepository.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.Threading;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework.Json;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework.Model;
|
||||
|
||||
/// <summary>
|
||||
/// The game configuration repository, which loads the configuration by using the
|
||||
/// <see cref="JsonObjectLoader"/>, to speed up loading the whole object graph.
|
||||
/// </summary>
|
||||
internal class GameConfigurationRepository : GenericRepository<GameConfiguration>
|
||||
{
|
||||
private readonly JsonObjectLoader _objectLoader;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GameConfigurationRepository" /> class.
|
||||
/// </summary>
|
||||
/// <param name="repositoryProvider">The repository provider.</param>
|
||||
/// <param name="loggerFactory">The logger factory.</param>
|
||||
/// <param name="changeListener">The change publisher.</param>
|
||||
public GameConfigurationRepository(IContextAwareRepositoryProvider repositoryProvider, ILoggerFactory loggerFactory, IConfigurationChangeListener? changeListener)
|
||||
: base(repositoryProvider, loggerFactory, changeListener)
|
||||
{
|
||||
this._objectLoader = new GameConfigurationJsonObjectLoader();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask<GameConfiguration?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var currentContext = this.RepositoryProvider.ContextStack.GetCurrentContext() as EntityFrameworkContextBase;
|
||||
if (currentContext is null)
|
||||
{
|
||||
throw new InvalidOperationException("There is no current context set.");
|
||||
}
|
||||
|
||||
var database = currentContext.Context.Database;
|
||||
await database.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (await this._objectLoader.LoadObjectAsync<GameConfiguration>(id, currentContext.Context, cancellationToken).ConfigureAwait(false) is { } config)
|
||||
{
|
||||
currentContext.Context.Attach(config);
|
||||
return config;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
await database.CloseConnectionAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask<IEnumerable<GameConfiguration>> GetAllAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var currentContext = this.RepositoryProvider.ContextStack.GetCurrentContext() as EntityFrameworkContextBase;
|
||||
if (currentContext is null)
|
||||
{
|
||||
throw new InvalidOperationException("There is no current context set.");
|
||||
}
|
||||
|
||||
var database = currentContext.Context.Database;
|
||||
await database.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var configs = (await this._objectLoader.LoadAllObjectsAsync<GameConfiguration>(currentContext.Context, cancellationToken).ConfigureAwait(false)).ToList();
|
||||
configs.ForEach(c => currentContext.Context.Attach(c));
|
||||
return configs;
|
||||
}
|
||||
finally
|
||||
{
|
||||
await database.CloseConnectionAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// <copyright file="GameServerDefinitionRepository.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.Threading;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Repository for the <see cref="GameServerDefinition"/>.
|
||||
/// It sets the <see cref="EntityDataContext.CurrentGameConfiguration"/> before loading other dependent data which tries to load the configured maps of a server.
|
||||
/// </summary>
|
||||
internal class GameServerDefinitionRepository : CachingGenericRepository<GameServerDefinition>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GameServerDefinitionRepository" /> class.
|
||||
/// </summary>
|
||||
/// <param name="repositoryProvider">The repository provider.</param>
|
||||
/// <param name="loggerFactory">The logger factory.</param>
|
||||
public GameServerDefinitionRepository(IContextAwareRepositoryProvider repositoryProvider, ILoggerFactory loggerFactory)
|
||||
: base(repositoryProvider, loggerFactory)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask LoadDependentDataAsync(object obj, DbContext currentContext, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (obj is GameServerDefinition definition)
|
||||
{
|
||||
var entityEntry = currentContext.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);
|
||||
collection.IsLoaded = true;
|
||||
}
|
||||
|
||||
if (definition.GameConfigurationId.HasValue)
|
||||
{
|
||||
definition.RawGameConfiguration =
|
||||
await this.RepositoryProvider.GetRepository<GameConfiguration>()!
|
||||
.GetByIdAsync(definition.GameConfigurationId.Value, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (currentContext is EntityDataContext context)
|
||||
{
|
||||
context.CurrentGameConfiguration = definition.RawGameConfiguration;
|
||||
}
|
||||
}
|
||||
|
||||
if (definition.ServerConfigurationId.HasValue)
|
||||
{
|
||||
definition.ServerConfiguration = await this.RepositoryProvider.GetRepository<GameServerConfiguration>()!
|
||||
.GetByIdAsync(definition.ServerConfigurationId.Value, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
43
src/Persistence/EntityFramework/GenericRepository.cs
Normal file
43
src/Persistence/EntityFramework/GenericRepository.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
// <copyright file="GenericRepository.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.Reflection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// A generic repository which wraps the access to the DBSet of the <see cref="EntityDataContext"/>.
|
||||
/// Entities are getting eagerly (=completely) loaded automatically.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type which this repository should manage.</typeparam>
|
||||
internal class GenericRepository<T> : GenericRepositoryBase<T>
|
||||
where T : class
|
||||
{
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly IConfigurationChangeListener? _changeListener;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GenericRepository{T}" /> class.
|
||||
/// </summary>
|
||||
/// <param name="repositoryProvider">The repository provider.</param>
|
||||
/// <param name="loggerFactory">The logger factory.</param>
|
||||
/// <param name="changeListener">The change publisher.</param>
|
||||
public GenericRepository(IContextAwareRepositoryProvider repositoryProvider, ILoggerFactory loggerFactory, IConfigurationChangeListener? changeListener)
|
||||
: base(repositoryProvider, loggerFactory.CreateLogger(MethodBase.GetCurrentMethod()?.DeclaringType ?? typeof(GenericRepository<T>)))
|
||||
{
|
||||
this._loggerFactory = loggerFactory;
|
||||
this._changeListener = changeListener;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a context to work with. If no context is currently registered at the repository provider, a new one is getting created.
|
||||
/// </summary>
|
||||
/// <returns>The context.</returns>
|
||||
protected override EntityFrameworkContextBase GetContext()
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
329
src/Persistence/EntityFramework/GenericRepositoryBase.cs
Normal file
329
src/Persistence/EntityFramework/GenericRepositoryBase.cs
Normal file
@@ -0,0 +1,329 @@
|
||||
// <copyright file="GenericRepositoryBase.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.Collections;
|
||||
using System.Threading;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for a generic repository which wraps the access to the DBSet of the <see cref="EntityDataContext"/>.
|
||||
/// Entities are getting eagerly (=completely) loaded automatically.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type which this repository should manage.</typeparam>
|
||||
internal abstract class GenericRepositoryBase<T> : IRepository<T>, ILoadByProperty
|
||||
where T : class
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GenericRepositoryBase{T}" /> class.
|
||||
/// </summary>
|
||||
/// <param name="repositoryProvider">The repository provider.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
protected GenericRepositoryBase(IContextAwareRepositoryProvider repositoryProvider, ILogger logger)
|
||||
{
|
||||
this._logger = logger;
|
||||
this.RepositoryProvider = repositoryProvider;
|
||||
using var completeContext = new EntityDataContext();
|
||||
this.FullEntityType = completeContext.Model.FindEntityType(typeof(T)) ?? throw new InvalidOperationException($"{typeof(T)} is not included in the model");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the repository provider.
|
||||
/// </summary>
|
||||
protected IContextAwareRepositoryProvider RepositoryProvider { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the complete meta model of the entity type in <see cref="EntityDataContext"/>.
|
||||
/// </summary>
|
||||
protected IEntityType FullEntityType { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<bool> DeleteAsync(Guid id)
|
||||
{
|
||||
if (await this.GetByIdAsync(id).ConfigureAwait(false) is { } item)
|
||||
{
|
||||
return await this.DeleteAsync(item).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<bool> DeleteAsync(object obj)
|
||||
{
|
||||
using var context = this.GetContext();
|
||||
return context.Context.Remove(obj) is not null;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
async ValueTask<IEnumerable> IRepository.GetAllAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await this.GetAllAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual async ValueTask<IEnumerable<T>> GetAllAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
using var context = this.GetContext();
|
||||
var result = await context.Context.Set<T>().ToListAsync(cancellationToken).ConfigureAwait(false);
|
||||
await this.LoadDependentDataAsync(result, context.Context, cancellationToken).ConfigureAwait(false);
|
||||
var newItems = context.Context.ChangeTracker.Entries<T>().Where(e => e.State == EntityState.Added).Select(e => e.Entity);
|
||||
result.AddRange(newItems);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual async ValueTask<T?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var context = this.GetContext();
|
||||
|
||||
var result = await context.Context.Set<T>().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);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
async ValueTask<object?> IRepository.GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await this.GetByIdAsync(id, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<IEnumerable> 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<T>().ToList();
|
||||
await this.LoadDependentDataAsync(result, context.Context, cancellationToken).ConfigureAwait(false);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the navigations which should be considered when loading the data.
|
||||
/// By default, we just load what the meta model of the current context contains.
|
||||
/// </summary>
|
||||
/// <param name="entityEntry">The entity entry.</param>
|
||||
/// <returns>The navigations which should be considered when loading the data.</returns>
|
||||
protected virtual IEnumerable<INavigationBase> GetNavigations(EntityEntry entityEntry)
|
||||
{
|
||||
if (entityEntry.Context is ITypedContext)
|
||||
{
|
||||
return entityEntry.Metadata.GetNavigations();
|
||||
}
|
||||
|
||||
return this.FullEntityType.GetNavigations();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the dependent data of the object from the corresponding repositories.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object.</param>
|
||||
/// <param name="currentContext">The current context with which the object got loaded. It is necessary to retrieve the foreign key ids.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
protected virtual async ValueTask LoadDependentDataAsync(object obj, DbContext currentContext, CancellationToken cancellationToken)
|
||||
{
|
||||
var entityEntry = currentContext.Entry(obj);
|
||||
|
||||
foreach (var navigation in this.GetNavigations(entityEntry).OfType<INavigation>())
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (!navigation.IsCollection && navigation.GetClrValue(obj) is null)
|
||||
{
|
||||
if (currentContext is ITypedContext editContext
|
||||
&& editContext.RootType != navigation.DeclaringEntityType
|
||||
&& editContext.IsBackReference(entityEntry.Metadata.ClrType))
|
||||
{
|
||||
// prevents endless loop.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.FullEntityType.FindPrimaryKey()?.Properties[0] == navigation.ForeignKey.Properties[0])
|
||||
{
|
||||
// The entity type is a many-to-many join entity and the property of the navigation is the "owner" of it.
|
||||
// Therefore, we don't need to load it. It would be nice to set the object reference somehow, though.
|
||||
}
|
||||
else
|
||||
{
|
||||
await this.LoadNavigationPropertyAsync(entityEntry, navigation, cancellationToken).ConfigureAwait(false);
|
||||
navigation.SetIsLoadedWhenNoTracking(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var collection in entityEntry.Collections.Where(c => !c.IsLoaded))
|
||||
{
|
||||
if (collection.Metadata is INavigation metadata)
|
||||
{
|
||||
if (currentContext is ITypedContext editContext
|
||||
&& editContext.RootType != metadata.DeclaringEntityType
|
||||
&& editContext.IsBackReference(entityEntry.Metadata.ClrType))
|
||||
{
|
||||
// prevents endless loop.
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.LoadCollectionAsync(entityEntry, metadata, currentContext, cancellationToken).ConfigureAwait(false);
|
||||
collection.IsLoaded = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the dependent data of the objects from the corresponding repositories.
|
||||
/// </summary>
|
||||
/// <param name="loadedObjects">The loaded objects.</param>
|
||||
/// <param name="currentContext">The current context with which the objects got loaded. It is necessary to retrieve the foreign key ids.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
protected virtual async ValueTask LoadDependentDataAsync(IEnumerable loadedObjects, DbContext currentContext, CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var obj in loadedObjects)
|
||||
{
|
||||
await this.LoadDependentDataAsync(obj, currentContext, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the navigation collection.
|
||||
/// </summary>
|
||||
/// <param name="entityEntry">The entity entry.</param>
|
||||
/// <param name="navigation">The navigation.</param>
|
||||
/// <param name="context">The context.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
protected virtual async ValueTask LoadCollectionAsync(EntityEntry entityEntry, INavigation navigation, DbContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var foreignKeyProperty = navigation.ForeignKey.Properties[0];
|
||||
var loadStatusAware = navigation.GetClrValue<ILoadingStatusAwareList>(entityEntry.Entity);
|
||||
if (loadStatusAware?.LoadingStatus == LoadingStatus.Loaded || loadStatusAware?.LoadingStatus == LoadingStatus.Loading)
|
||||
{
|
||||
// already loaded or loading
|
||||
return;
|
||||
}
|
||||
|
||||
if (loadStatusAware is null && navigation.GetClrValue(entityEntry.Entity) != null)
|
||||
{
|
||||
// already loaded or loading
|
||||
return;
|
||||
}
|
||||
|
||||
if (loadStatusAware is null)
|
||||
{
|
||||
throw new InvalidOperationException($"The collection is not implementing {nameof(ILoadingStatusAware)}");
|
||||
}
|
||||
|
||||
loadStatusAware.LoadingStatus = LoadingStatus.Loading;
|
||||
|
||||
if (this.RepositoryProvider.GetRepository(foreignKeyProperty.DeclaringType.ClrType) 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);
|
||||
foreach (var obj in items)
|
||||
{
|
||||
if (!loadStatusAware.Contains(obj))
|
||||
{
|
||||
loadStatusAware.Add(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadStatusAware.LoadingStatus = LoadingStatus.Loaded;
|
||||
}
|
||||
else
|
||||
{
|
||||
this._logger.LogWarning("No repository found which supports loading by foreign key for type {ClrType}.", foreignKeyProperty.DeclaringType.ClrType);
|
||||
loadStatusAware.LoadingStatus = LoadingStatus.Failed;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the data navigation property and sets it in the entity.
|
||||
/// </summary>
|
||||
/// <param name="entityEntry">The entity entry from the context.</param>
|
||||
/// <param name="navigation">The navigation property.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
protected virtual async ValueTask LoadNavigationPropertyAsync(EntityEntry entityEntry, IReadOnlyNavigation navigation, CancellationToken cancellationToken)
|
||||
{
|
||||
if (navigation.ForeignKey.DeclaringEntityType != navigation.DeclaringEntityType)
|
||||
{
|
||||
// inverse property
|
||||
return;
|
||||
}
|
||||
|
||||
var keyProperty = navigation.ForeignKey.Properties[0];
|
||||
var idValue = entityEntry.Property(keyProperty.Name).CurrentValue;
|
||||
|
||||
Guid id = (idValue as Guid?) ?? Guid.Empty;
|
||||
if (id != Guid.Empty)
|
||||
{
|
||||
var currentValue = navigation.GetClrValue(entityEntry.Entity);
|
||||
if (currentValue is IIdentifiable identifiable && identifiable.Id == id)
|
||||
{
|
||||
// loaded already
|
||||
return;
|
||||
}
|
||||
|
||||
IRepository? repository = null;
|
||||
try
|
||||
{
|
||||
repository = this.RepositoryProvider.GetRepository(navigation.TargetEntityType.ClrType);
|
||||
}
|
||||
catch (RepositoryNotFoundException ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Repository not found: {Message}", ex.Message);
|
||||
}
|
||||
|
||||
if (repository != null)
|
||||
{
|
||||
if (!navigation.TrySetClrValue(entityEntry.Entity, await repository.GetByIdAsync(id, cancellationToken).ConfigureAwait(false)))
|
||||
{
|
||||
this._logger.LogError("Could not find setter for navigation {Navigation}", navigation);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this._logger.LogError("Repository not found for navigation target type {TargetEntityType}.", navigation.TargetEntityType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a context to work with. If no context is currently registered at the repository provider, a new one is getting created.
|
||||
/// </summary>
|
||||
/// <returns>The context.</returns>
|
||||
protected abstract EntityFrameworkContextBase GetContext();
|
||||
|
||||
private async ValueTask<IEnumerable> LoadByPropertyInternalAsync(IProperty property, object propertyValue, DbContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
if (property.ClrType == typeof(Guid))
|
||||
{
|
||||
return await context.Set<T>().Where(o => EF.Property<Guid>(o, property.Name) == (Guid)propertyValue).ToListAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (property.ClrType == typeof(Guid?))
|
||||
{
|
||||
return await context.Set<T>().Where(o => EF.Property<Guid?>(o, property.Name) == (Guid?)propertyValue).ToListAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return Enumerable.Empty<object>();
|
||||
}
|
||||
}
|
||||
23
src/Persistence/EntityFramework/GuidV7ValueGenerator.cs
Normal file
23
src/Persistence/EntityFramework/GuidV7ValueGenerator.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
// <copyright file="GuidV7ValueGenerator.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 Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||
using Microsoft.EntityFrameworkCore.ValueGeneration;
|
||||
|
||||
/// <summary>
|
||||
/// A value generator for UUID V7 used as primary key.
|
||||
/// </summary>
|
||||
public class GuidV7ValueGenerator : ValueGenerator<Guid>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override bool GeneratesTemporaryValues => false;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Guid Next(EntityEntry entry)
|
||||
{
|
||||
return GuidV7.NewGuid();
|
||||
}
|
||||
}
|
||||
53
src/Persistence/EntityFramework/GuildContext.cs
Normal file
53
src/Persistence/EntityFramework/GuildContext.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
// <copyright file="GuildContext.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 Microsoft.EntityFrameworkCore;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework.Extensions;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Context for the guild server which just uses <see cref="Guild"/>, <see cref="GuildMember"/> and <see cref="Character"/>.
|
||||
/// </summary>
|
||||
public class GuildContext : DbContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Configures the model, especially defines that <see cref="Guild"/> and <see cref="GuildMember"/> are created in a separate "guild" schema.
|
||||
/// </summary>
|
||||
/// <param name="modelBuilder">The model builder.</param>
|
||||
internal static void ConfigureModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<GuildMember>(member =>
|
||||
{
|
||||
member.Property(m => m.Id).ValueGeneratedNever();
|
||||
member.ToTable(nameof(GuildMember), SchemaNames.Guild);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Guild>(e =>
|
||||
{
|
||||
e.Property(guild => guild.Name).HasMaxLength(8).IsRequired();
|
||||
e.HasIndex(guild => guild.Name).IsUnique();
|
||||
e.ToTable(nameof(Guild), SchemaNames.Guild);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
base.OnConfiguring(optionsBuilder);
|
||||
this.Configure(optionsBuilder);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Ignore<Character>();
|
||||
ConfigureModel(modelBuilder);
|
||||
modelBuilder.Entity<GuildMember>().Ignore(m => m.Character);
|
||||
modelBuilder.Entity<CharacterName>().HasKey(f => f.Id);
|
||||
|
||||
modelBuilder.UseGuidV7Ids();
|
||||
}
|
||||
}
|
||||
51
src/Persistence/EntityFramework/GuildServerContext.cs
Normal file
51
src/Persistence/EntityFramework/GuildServerContext.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
// <copyright file="GuildServerContext.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 Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework.Model;
|
||||
|
||||
/// <summary>
|
||||
/// The EF Core implementation of a context which is used by the guild server.
|
||||
/// </summary>
|
||||
internal class GuildServerContext : CachingEntityFrameworkContext, IGuildServerContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GuildServerContext" /> class.
|
||||
/// </summary>
|
||||
/// <param name="guildContext">The guild context.</param>
|
||||
/// <param name="repositoryProvider">The repository provider.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public GuildServerContext(GuildContext guildContext, IContextAwareRepositoryProvider repositoryProvider, ILogger<GuildServerContext> logger)
|
||||
: base(guildContext, repositoryProvider, null, logger)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<bool> GuildWithNameExistsAsync(string name)
|
||||
{
|
||||
return await this.Context.Set<Guild>().AnyAsync(guild => guild.Name == name).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<IReadOnlyDictionary<Guid, string>> GetMemberNamesAsync(Guid guildId)
|
||||
{
|
||||
return await (from member in this.Context.Set<GuildMember>()
|
||||
join character in this.Context.Set<CharacterName>() on member.Id equals character.Id
|
||||
where member.GuildId == guildId
|
||||
select new { character.Id, character.Name })
|
||||
.ToDictionaryAsync(member => member.Id, member => member.Name).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<IReadOnlyList<DataModel.Entities.Guild>> GetAlliancesAsync(Guid allianceMasterId)
|
||||
{
|
||||
return await this.Context.Set<Guild>()
|
||||
.Where(g => g.AllianceGuildId == allianceMasterId)
|
||||
.Include(g => g.RawMembers)
|
||||
.ToListAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// <copyright file="IConfigurationChangeListener.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 Microsoft.EntityFrameworkCore.Metadata;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for an object which listens to configuration changes.
|
||||
/// </summary>
|
||||
public interface IConfigurationChangeListener
|
||||
{
|
||||
/// <summary>
|
||||
/// A configuration has changed.
|
||||
/// </summary>
|
||||
/// <param name="type">The type.</param>
|
||||
/// <param name="id">The identifier.</param>
|
||||
/// <param name="configuration">The changed configuration.</param>
|
||||
/// <param name="parent">The parent object, if available.</param>
|
||||
ValueTask ConfigurationChangedAsync(Type type, Guid id, object configuration, object? parent);
|
||||
|
||||
/// <summary>
|
||||
/// A configuration has been added.
|
||||
/// </summary>
|
||||
/// <param name="type">The type.</param>
|
||||
/// <param name="id">The identifier.</param>
|
||||
/// <param name="configuration">The added configuration.</param>
|
||||
/// <param name="parent">The parent object, if available.</param>
|
||||
/// <param name="parentCollectionNavigation">The parent collection navigation, if available.</param>
|
||||
ValueTask ConfigurationAddedAsync(Type type, Guid id, object configuration, object? parent, INavigationBase? parentCollectionNavigation);
|
||||
|
||||
/// <summary>
|
||||
/// A configuration has been removed.
|
||||
/// </summary>
|
||||
/// <param name="type">The type.</param>
|
||||
/// <param name="id">The identifier of the removed configuration object.</param>
|
||||
/// <param name="parent">The parent, if available.</param>
|
||||
/// <param name="parentCollectionNavigation">The parent collection navigation, if available.</param>
|
||||
ValueTask ConfigurationRemovedAsync(Type type, Guid id, object? parent, INavigationBase? parentCollectionNavigation);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// <copyright file="IConfigurationTypeRepository.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;
|
||||
|
||||
/// <summary>
|
||||
/// Non-generic interface for <see cref="ConfigurationTypeRepository{T}"/>.
|
||||
/// </summary>
|
||||
internal interface IConfigurationTypeRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Ensures the cache for the current configuration.
|
||||
/// </summary>
|
||||
void EnsureCacheForCurrentConfiguration();
|
||||
|
||||
/// <summary>
|
||||
/// Updates the cached instance.
|
||||
/// </summary>
|
||||
/// <param name="changedInstance">The changed instance.</param>
|
||||
void UpdateCachedInstances(object changedInstance);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// <copyright file="IContextAwareRepositoryProvider.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;
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="IRepositoryProvider"/> which is aware of a current context by
|
||||
/// implementing <see cref="IContextStackProvider"/>.
|
||||
/// </summary>
|
||||
internal interface IContextAwareRepositoryProvider : IRepositoryProvider, IContextStackProvider
|
||||
{
|
||||
}
|
||||
25
src/Persistence/EntityFramework/IContextStack.cs
Normal file
25
src/Persistence/EntityFramework/IContextStack.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
// <copyright file="IContextStack.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;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for a stack of persistence contexts.
|
||||
/// </summary>
|
||||
internal interface IContextStack
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="context">The context.</param>
|
||||
/// <returns>The disposable to end the usage.</returns>
|
||||
IDisposable UseContext(IContext context);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current context of the current thread.
|
||||
/// </summary>
|
||||
/// <returns>The current context.</returns>
|
||||
IContext? GetCurrentContext();
|
||||
}
|
||||
16
src/Persistence/EntityFramework/IContextStackProvider.cs
Normal file
16
src/Persistence/EntityFramework/IContextStackProvider.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
// <copyright file="IContextStackProvider.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;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for a class which provides a <see cref="IContextStack"/>.
|
||||
/// </summary>
|
||||
internal interface IContextStackProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the context stack.
|
||||
/// </summary>
|
||||
IContextStack ContextStack { get; }
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// <copyright file="IDatabaseConnectionSettingProvider.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.Threading;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for a provider of <see cref="ConnectionSetting"/>s for specified context types.
|
||||
/// </summary>
|
||||
public interface IDatabaseConnectionSettingProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the initialization task.
|
||||
/// </summary>
|
||||
Task? Initialization { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the settings provider.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
Task InitializeAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the connection setting for the specified context type.
|
||||
/// </summary>
|
||||
/// <typeparam name="TContextType">The type of the context type.</typeparam>
|
||||
/// <returns>The connection settings.</returns>
|
||||
ConnectionSetting GetConnectionSetting<TContextType>()
|
||||
where TContextType : DbContext;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the connection setting for the specified context type.
|
||||
/// </summary>
|
||||
/// <param name="contextType">Type of the context.</param>
|
||||
/// <returns>The connection settings.</returns>
|
||||
ConnectionSetting GetConnectionSetting(Type contextType);
|
||||
}
|
||||
26
src/Persistence/EntityFramework/ILoadByProperty.cs
Normal file
26
src/Persistence/EntityFramework/ILoadByProperty.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
// <copyright file="ILoadByProperty.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.Collections;
|
||||
using System.Threading;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
|
||||
/// <summary>
|
||||
/// Interface to load data by property.
|
||||
/// </summary>
|
||||
internal interface ILoadByProperty
|
||||
{
|
||||
/// <summary>
|
||||
/// Loads objects by property.
|
||||
/// </summary>
|
||||
/// <param name="property">The property of the object which should be compared.</param>
|
||||
/// <param name="propertyValue">The value of the property.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>
|
||||
/// The enumeration of the loaded objects.
|
||||
/// </returns>
|
||||
ValueTask<IEnumerable> LoadByPropertyAsync(IProperty property, object propertyValue, CancellationToken cancellationToken = default);
|
||||
}
|
||||
16
src/Persistence/EntityFramework/ILoadingStatusAware.cs
Normal file
16
src/Persistence/EntityFramework/ILoadingStatusAware.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
// <copyright file="ILoadingStatusAware.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;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for a loading status aware class.
|
||||
/// </summary>
|
||||
internal interface ILoadingStatusAware
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the loading status.
|
||||
/// </summary>
|
||||
LoadingStatus LoadingStatus { get; set; }
|
||||
}
|
||||
16
src/Persistence/EntityFramework/ILoadingStatusAwareList.cs
Normal file
16
src/Persistence/EntityFramework/ILoadingStatusAwareList.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
// <copyright file="ILoadingStatusAwareList.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.Collections;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for a loading status aware list.
|
||||
/// </summary>
|
||||
/// <seealso cref="MUnique.OpenMU.Persistence.EntityFramework.ILoadingStatusAware" />
|
||||
/// <seealso cref="System.Collections.IList" />
|
||||
internal interface ILoadingStatusAwareList : ILoadingStatusAware, IList
|
||||
{
|
||||
}
|
||||
35
src/Persistence/EntityFramework/ITypedContext.cs
Normal file
35
src/Persistence/EntityFramework/ITypedContext.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
// <copyright file="ITypedContext.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 Microsoft.EntityFrameworkCore.Metadata;
|
||||
using MUnique.OpenMU.DataModel.Composition;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for a context which is used to edit instances of the <see cref="RootType"/>
|
||||
/// and their navigations marked with the <see cref="MemberOfAggregateAttribute"/>.
|
||||
/// </summary>
|
||||
internal interface ITypedContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the root entity type.
|
||||
/// </summary>
|
||||
IEntityType RootType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the entity type of the specified clr type is included in the context.
|
||||
/// </summary>
|
||||
/// <param name="clrType">The clr type.</param>
|
||||
bool IsIncluded(Type clrType);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the entity type contains a back reference to the <see cref="RootType"/> or one of it's included types.
|
||||
/// </summary>
|
||||
/// <param name="clrType">Type of the color.</param>
|
||||
/// <remarks>
|
||||
/// These types are included in the context, but their navigations are not resolved.
|
||||
/// </remarks>
|
||||
bool IsBackReference(Type clrType);
|
||||
}
|
||||
13
src/Persistence/EntityFramework/IsLinkToParentAttribute.cs
Normal file
13
src/Persistence/EntityFramework/IsLinkToParentAttribute.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
// <copyright file="IsLinkToParentAttribute.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;
|
||||
|
||||
/// <summary>
|
||||
/// Attribute to mark properties which are a link to the parent object.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public sealed class IsLinkToParentAttribute : Attribute
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// <copyright file="AccountJsonObjectLoader.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.Json;
|
||||
|
||||
using MUnique.OpenMU.Persistence.EntityFramework.Model;
|
||||
|
||||
/// <summary>
|
||||
/// A json object loader for <see cref="Account"/>s.
|
||||
/// </summary>
|
||||
public class AccountJsonObjectLoader : JsonObjectLoader
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AccountJsonObjectLoader"/> class.
|
||||
/// </summary>
|
||||
public AccountJsonObjectLoader()
|
||||
: base(new JsonQueryBuilder(), new JsonObjectDeserializer(), new CachingReferenceHandler())
|
||||
{
|
||||
}
|
||||
}
|
||||
100
src/Persistence/EntityFramework/Json/BinaryAsHexJsonConverter.cs
Normal file
100
src/Persistence/EntityFramework/Json/BinaryAsHexJsonConverter.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
// <copyright file="BinaryAsHexJsonConverter.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.Json;
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// This converter converts a hex string to a byte array.
|
||||
/// Newtonsoft.Json expects a base64 string, but postgres delivers us a hex string,
|
||||
/// so this converter is needed.
|
||||
/// </summary>
|
||||
public class BinaryAsHexJsonConverter : JsonConverter<byte[]>
|
||||
{
|
||||
/// <summary>
|
||||
/// The prefix of a byte array string, provided by postgres.
|
||||
/// </summary>
|
||||
private const string ByteArrayPrefix = @"\x";
|
||||
|
||||
/// <summary>
|
||||
/// A character to value mapping table for faster hex string parsing. Each character (0-9a-fA-F)
|
||||
/// has it's corresponding value of 0-15. All other characters are initialized with <see cref="int.MaxValue"/>.
|
||||
/// </summary>
|
||||
private readonly int[] _characterToValue;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BinaryAsHexJsonConverter"/> class.
|
||||
/// </summary>
|
||||
public BinaryAsHexJsonConverter()
|
||||
{
|
||||
this._characterToValue = new int[byte.MaxValue + 1];
|
||||
for (int i = 0; i < this._characterToValue.Length; i++)
|
||||
{
|
||||
this._characterToValue[i] = int.MaxValue;
|
||||
}
|
||||
|
||||
for (byte i = 0; i < 10; i++)
|
||||
{
|
||||
this._characterToValue['0' + i] = i;
|
||||
}
|
||||
|
||||
for (byte i = 0xA; i < 16; i++)
|
||||
{
|
||||
this._characterToValue['a' + i - 0xA] = i;
|
||||
this._characterToValue['A' + i - 0xA] = i;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, byte[] value, JsonSerializerOptions options)
|
||||
{
|
||||
var arrayString = ByteArrayPrefix + BitConverter.ToString(value)
|
||||
.Replace("-", string.Empty, StringComparison.InvariantCulture)
|
||||
.ToLowerInvariant();
|
||||
writer.WriteStringValue(arrayString);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override byte[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.Null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (reader.TokenType == JsonTokenType.String)
|
||||
{
|
||||
var prefixSize = ByteArrayPrefix.Length + 1; // +1 for escaping
|
||||
var hexData = reader.ValueSpan.Slice(prefixSize);
|
||||
var data = new byte[(hexData.Length - prefixSize) / 2];
|
||||
for (var i = 0; i < data.Length; i++)
|
||||
{
|
||||
var index = i * 2;
|
||||
int highNibble = this.ParseCharacter(hexData[index]);
|
||||
int lowNibble = this.ParseCharacter(hexData[index + 1]);
|
||||
data[i] = (byte)((highNibble << 4) | lowNibble);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
throw new Exception($"Unexpected token parsing binary. Expected String, got {reader.TokenType}.");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type objectType) => objectType == typeof(byte[]);
|
||||
|
||||
private int ParseCharacter(byte character)
|
||||
{
|
||||
var value = this._characterToValue[character];
|
||||
if (value == int.MaxValue)
|
||||
{
|
||||
throw new ArgumentException($"invalid character: {character}", nameof(character));
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// <copyright file="CachingReferenceHandler.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.Json;
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using MUnique.OpenMU.Persistence.Json;
|
||||
|
||||
/// <summary>
|
||||
/// A reference handler which considers cached configuration objects.
|
||||
/// This is basically needed when loading account data. They can have references
|
||||
/// to configuration.
|
||||
/// </summary>
|
||||
public class CachingReferenceHandler : ReferenceHandler, IIdReferenceHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the currently used resolver.
|
||||
/// </summary>
|
||||
public ReferenceResolver? Current { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override ReferenceResolver CreateResolver()
|
||||
{
|
||||
return this.Current ??= new MultipleSourceReferenceResolver(new IdReferenceResolver(), ConfigurationIdReferenceResolver.Instance);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// <copyright file="ConfigurationIdReferenceResolver.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.Json;
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework.Model;
|
||||
|
||||
/// <summary>
|
||||
/// A reference resolver which resolves them by looking at the objects occurring in the <see cref="GameConfiguration" />.
|
||||
/// The cache is maintained by instances of the <see cref="ConfigurationTypeRepository{T}" />.
|
||||
/// </summary>
|
||||
/// <remarks>TODO: I don't like it as a singleton, but I keep it until I find a cleaner solution.</remarks>
|
||||
internal class ConfigurationIdReferenceResolver : ReferenceResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// The singleton instance.
|
||||
/// </summary>
|
||||
private static readonly ConfigurationIdReferenceResolver InstanceValue = new();
|
||||
|
||||
private readonly IDictionary<Guid, IIdentifiable> _cache = new Dictionary<Guid, IIdentifiable>();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConfigurationIdReferenceResolver"/> class.
|
||||
/// </summary>
|
||||
protected ConfigurationIdReferenceResolver()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the singleton instance.
|
||||
/// </summary>
|
||||
public static ConfigurationIdReferenceResolver Instance => InstanceValue;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object ResolveReference(string referenceId)
|
||||
{
|
||||
var id = new Guid(referenceId);
|
||||
if (id == Guid.Empty)
|
||||
{
|
||||
return null!;
|
||||
}
|
||||
|
||||
var success = this._cache.TryGetValue(id, out var obj);
|
||||
if (!success)
|
||||
{
|
||||
return null!;
|
||||
}
|
||||
|
||||
return obj!;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string GetReference(object value, out bool alreadyExists)
|
||||
{
|
||||
var p = (IIdentifiable)value;
|
||||
alreadyExists = this._cache.ContainsKey(p.Id);
|
||||
return p.Id.ToString();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void AddReference(string referenceId, object value)
|
||||
{
|
||||
var id = new Guid(referenceId);
|
||||
this._cache[id] = (IIdentifiable)value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the reference to the cache of the resolver.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
public void AddReference(IIdentifiable value)
|
||||
{
|
||||
this._cache[value.Id] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the reference from the cache of the resolver.
|
||||
/// </summary>
|
||||
/// <param name="key">The key.</param>
|
||||
public void RemoveReference(Guid key)
|
||||
{
|
||||
this._cache.Remove(key);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// <copyright file="GameConfigurationJsonObjectLoader.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.Json;
|
||||
|
||||
using MUnique.OpenMU.Persistence.EntityFramework.Model;
|
||||
using MUnique.OpenMU.Persistence.Json;
|
||||
|
||||
/// <summary>
|
||||
/// Object loader for <see cref="GameConfiguration"/> objects.
|
||||
/// </summary>
|
||||
public class GameConfigurationJsonObjectLoader : JsonObjectLoader
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GameConfigurationJsonObjectLoader"/> class.
|
||||
/// </summary>
|
||||
public GameConfigurationJsonObjectLoader()
|
||||
: base(
|
||||
new GameConfigurationJsonQueryBuilder(),
|
||||
new JsonObjectDeserializer(),
|
||||
new IdReferenceHandler())
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// <copyright file="GameConfigurationJsonQueryBuilder.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.Json;
|
||||
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework.Model;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="JsonQueryBuilder"/> for <see cref="GameConfiguration"/>.
|
||||
/// </summary>
|
||||
public class GameConfigurationJsonQueryBuilder : JsonQueryBuilder
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
protected override IEnumerable<INavigation> GetNavigations(IEntityType entityType)
|
||||
{
|
||||
if (entityType.ClrType != typeof(GameConfiguration))
|
||||
{
|
||||
return base.GetNavigations(entityType);
|
||||
}
|
||||
|
||||
var navigations = base.GetNavigations(entityType).ToList();
|
||||
|
||||
// We move the maps with their spawn points to the end because they depend on all other data
|
||||
var mapsProperty = navigations.First(nav => nav.Name == "RawMaps");
|
||||
navigations.Remove(mapsProperty);
|
||||
navigations.Add(mapsProperty);
|
||||
return navigations;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// <copyright file="JsonObjectDeserializer.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.Json;
|
||||
|
||||
using System.Text.Json;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
|
||||
/// <summary>
|
||||
/// A deserializer which parses the json retrieved from the postgres database by using a query built by the <see cref="JsonQueryBuilder"/>.
|
||||
/// We need to register a special binary converter, because postgres provides binary data in a non-standard format.
|
||||
/// </summary>
|
||||
public class JsonObjectDeserializer : MUnique.OpenMU.Persistence.Json.JsonObjectDeserializer
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
protected override void BeforeDeserialize(JsonSerializerOptions options)
|
||||
{
|
||||
base.BeforeDeserialize(options);
|
||||
foreach (var converter in JsonConverterRegistry.Converters)
|
||||
{
|
||||
options.Converters.Add(converter);
|
||||
}
|
||||
}
|
||||
}
|
||||
109
src/Persistence/EntityFramework/Json/JsonObjectLoader.cs
Normal file
109
src/Persistence/EntityFramework/Json/JsonObjectLoader.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
// <copyright file="JsonObjectLoader.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.Json;
|
||||
|
||||
using System.Data;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
|
||||
/// <summary>
|
||||
/// A class which allows to load objects from the database by using a query provided by the <see cref="JsonQueryBuilder"/>
|
||||
/// which retrieves a whole object graph by using json functions of postgres.
|
||||
/// </summary>
|
||||
public class JsonObjectLoader
|
||||
{
|
||||
private readonly JsonQueryBuilder _queryBuilder;
|
||||
private readonly JsonObjectDeserializer _deserializer;
|
||||
private readonly ReferenceHandler _referenceHandler;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JsonObjectLoader" /> class.
|
||||
/// </summary>
|
||||
/// <param name="queryBuilder">The query builder.</param>
|
||||
/// <param name="deserializer">The deserializer.</param>
|
||||
/// <param name="referenceHandler">The reference handler.</param>
|
||||
public JsonObjectLoader(JsonQueryBuilder queryBuilder, JsonObjectDeserializer deserializer, ReferenceHandler referenceHandler)
|
||||
{
|
||||
this._queryBuilder = queryBuilder;
|
||||
this._deserializer = deserializer;
|
||||
this._referenceHandler = referenceHandler;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads all objects of the specified type <typeparamref name="T"/>.
|
||||
/// Please note, that it's expected that the database connection is already established when calling this method.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the <see cref="IIdentifiable"/> object.</typeparam>
|
||||
/// <param name="context">The context.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>All objects of <typeparamref name="T"/>.</returns>
|
||||
public async ValueTask<IEnumerable<T>> LoadAllObjectsAsync<T>(DbContext context, CancellationToken cancellationToken = default)
|
||||
where T : class, IIdentifiable
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var result = new List<T>();
|
||||
var type = context.Model.FindEntityType(typeof(T)) ?? throw new ArgumentException($"{typeof(T)} is not included in the model of the context.");
|
||||
var queryString = this._queryBuilder.BuildJsonQueryForEntity(type);
|
||||
await using var command = context.Database.GetDbConnection().CreateCommand();
|
||||
command.CommandText = queryString;
|
||||
await using var reader = await command.ExecuteReaderAsync(CommandBehavior.SequentialAccess, cancellationToken).ConfigureAwait(false);
|
||||
if (reader.HasRows)
|
||||
{
|
||||
while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
await using var textReader = reader.GetStream(2);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (this._deserializer.Deserialize<T>(textReader, this._referenceHandler) is { } item)
|
||||
{
|
||||
result.Add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the object with the specified id from the context.
|
||||
/// Please note, that it's expected that the database connection is already established when calling this method.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the <see cref="IIdentifiable"/> object.</typeparam>
|
||||
/// <param name="id">The identifier of the object which should be loaded.</param>
|
||||
/// <param name="context">The context.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The loaded object, if available; Otherwise, null.</returns>
|
||||
public async ValueTask<T?> LoadObjectAsync<T>(Guid id, DbContext context, CancellationToken cancellationToken = default)
|
||||
where T : class, IIdentifiable
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
IEntityType type;
|
||||
await using (var completeContext = new EntityDataContext())
|
||||
{
|
||||
type = completeContext.Model.FindEntityType(typeof(T)) ?? throw new ArgumentException($"{typeof(T)} is not included in the model of the context.");
|
||||
}
|
||||
|
||||
var queryString = this._queryBuilder.BuildJsonQueryForEntity(type);
|
||||
queryString += " where result.\"Id\" = @id;";
|
||||
await using var command = context.Database.GetDbConnection().CreateCommand();
|
||||
command.CommandText = queryString;
|
||||
var idParameter = command.CreateParameter();
|
||||
idParameter.ParameterName = "id";
|
||||
idParameter.Value = id;
|
||||
|
||||
command.Parameters.Add(idParameter);
|
||||
await using var reader = await command.ExecuteReaderAsync(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult, cancellationToken).ConfigureAwait(false);
|
||||
if (reader.HasRows && await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
await using var textReader = reader.GetStream(2);
|
||||
return this._deserializer.Deserialize<T>(textReader, this._referenceHandler);
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
222
src/Persistence/EntityFramework/Json/JsonQueryBuilder.cs
Normal file
222
src/Persistence/EntityFramework/Json/JsonQueryBuilder.cs
Normal file
@@ -0,0 +1,222 @@
|
||||
// <copyright file="JsonQueryBuilder.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.Json;
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// A query builder which creates a query to return a whole object graph as json by using postgres json functions.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// TODO: Make the resulting query more readable by adding indenting for subqueries.
|
||||
/// </remarks>
|
||||
public class JsonQueryBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds the json query for the given entity type.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// It creates a query like this (e.g. for GameConfiguration):
|
||||
/// select result."Id" "$id", result."Id", row_to_json(result) as GameConfiguration
|
||||
/// from (
|
||||
/// select a."Id", a.*, (
|
||||
/// -- additional subqueries which return json
|
||||
/// ...)
|
||||
/// from config."GameConfiguration" a
|
||||
/// ) result;.
|
||||
/// </remarks>
|
||||
/// <param name="entityType">Type of the entity.</param>
|
||||
/// <returns>The query which returns the objects of the given type as json string.</returns>
|
||||
public string BuildJsonQueryForEntity(IEntityType entityType)
|
||||
{
|
||||
// Debug.WriteLine($"Building the json query for {entityType.Name}.");
|
||||
var stringBuilder = new StringBuilder();
|
||||
stringBuilder.Append("select result.\"Id\" \"$id\", result.\"Id\" id, row_to_json(result) as ").AppendLine(entityType.GetTableName())
|
||||
.AppendLine("from (");
|
||||
this.AddTypeToQuery(entityType, stringBuilder, "a");
|
||||
stringBuilder.AppendLine(") result");
|
||||
var result = stringBuilder.ToString();
|
||||
|
||||
// Debug.WriteLine("Finished building the json query for {0}. Result: {1}", entityType.Name, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the navigations of the entity type.
|
||||
/// </summary>
|
||||
/// <param name="entityType">Type of the entity.</param>
|
||||
/// <returns>The navigations of the entity type.</returns>
|
||||
/// <remarks>
|
||||
/// Can be overwritten to apply sorting.
|
||||
/// TODO: Can sorting based on dependencies be done automatically?.
|
||||
/// </remarks>
|
||||
protected virtual IEnumerable<INavigation> GetNavigations(IEntityType entityType)
|
||||
{
|
||||
return entityType.GetNavigations();
|
||||
}
|
||||
|
||||
private void AddTypeToQuery(IEntityType entityType, StringBuilder stringBuilder, string alias)
|
||||
{
|
||||
stringBuilder.Append("select ").Append(alias).Append(".\"Id\" as \"$id\", ").Append(alias).Append(".*");
|
||||
this.AddNavigationsToQuery(entityType, stringBuilder, alias);
|
||||
stringBuilder.Append(" from ").Append(entityType.GetSchema()).Append('.').Append('"').Append(entityType.GetTableName()).Append("\" ").AppendLine(alias);
|
||||
}
|
||||
|
||||
private void AddNavigationsToQuery(IEntityType entityType, StringBuilder stringBuilder, string parentAlias)
|
||||
{
|
||||
var navigationAlias = this.GetNextAlias(parentAlias);
|
||||
if (navigationAlias == "i")
|
||||
{
|
||||
// stopping circular reference
|
||||
// Debug.Fail($"Stopping circular reference at entity type {entityType.Name}");
|
||||
return;
|
||||
}
|
||||
|
||||
var navigations = this.GetNavigations(entityType);
|
||||
|
||||
foreach (var navigation in navigations)
|
||||
{
|
||||
if (navigation.IsCollection)
|
||||
{
|
||||
this.AddCollection(navigation, entityType, stringBuilder, parentAlias);
|
||||
}
|
||||
else //// it's a foreign key
|
||||
{
|
||||
this.AddNavigation(navigation, stringBuilder, parentAlias);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string GetNextAlias(string parentAlias)
|
||||
{
|
||||
return ((char)(parentAlias[0] + 1)).ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private void AddNavigation(INavigation navigation, StringBuilder stringBuilder, string parentAlias)
|
||||
{
|
||||
if (navigation.ForeignKey.DeclaringEntityType != navigation.DeclaringEntityType)
|
||||
{
|
||||
// inverse property, no data required
|
||||
Debug.WriteLine("Inverse property {0}", navigation.Name);
|
||||
return;
|
||||
}
|
||||
|
||||
var navigationAlias = this.GetNextAlias(parentAlias);
|
||||
var targetType = navigation.TargetEntityType;
|
||||
var foreignKey = navigation.ForeignKey.Properties[0];
|
||||
if (foreignKey.IsShadowProperty())
|
||||
{
|
||||
// We assume that every important foreign key is mapped to a "real" property
|
||||
Debug.WriteLine("Shadow property {0}", navigation.Name);
|
||||
return;
|
||||
}
|
||||
|
||||
var isBackReference = navigation.ForeignKey.PrincipalToDependent?.IsCollection ?? false;
|
||||
if (isBackReference)
|
||||
{
|
||||
// It's a back reference of a collection - we just have to create a reference json object
|
||||
Debug.WriteLine("Back Reference property {0}", navigation.Name);
|
||||
}
|
||||
|
||||
stringBuilder.Append(", (");
|
||||
if (!navigation.IsMemberOfAggregate() || isBackReference)
|
||||
{
|
||||
stringBuilder
|
||||
.Append($"case when {parentAlias}.\"{foreignKey.Name}\" is null then null else ")
|
||||
.Append("json_build_object('$ref', ").Append(parentAlias).Append(".\"").Append(foreignKey.Name).Append("\") end");
|
||||
}
|
||||
else
|
||||
{
|
||||
stringBuilder.Append("select row_to_json(").Append(navigationAlias).Append(") from (");
|
||||
this.AddTypeToQuery(targetType, stringBuilder, navigationAlias);
|
||||
stringBuilder.Append(") ").Append(navigationAlias).Append(" where ").Append(navigationAlias).Append(".\"").Append(targetType.GetPrimaryKeyColumnName()).Append("\" = ").Append(parentAlias).Append(".\"").Append(foreignKey.Name).AppendLine("\"");
|
||||
}
|
||||
|
||||
stringBuilder.Append(") as \"").Append(navigation.Name).AppendLine("\"");
|
||||
}
|
||||
|
||||
private void AddCollection(INavigation navigation, IEntityType entityType, StringBuilder stringBuilder, string parentAlias)
|
||||
{
|
||||
var keyProperty = navigation.ForeignKey.Properties[0];
|
||||
var navigationType = keyProperty.DeclaringEntityType;
|
||||
#pragma warning disable EF1001 // Internal EF Core API usage.
|
||||
if (navigationType.FindDeclaredPrimaryKey() is not { } primaryKey)
|
||||
{
|
||||
return;
|
||||
}
|
||||
#pragma warning restore EF1001 // Internal EF Core API usage.
|
||||
|
||||
if (primaryKey.Properties.Count > 1)
|
||||
{
|
||||
this.AddManyToManyCollection(navigationType, entityType, stringBuilder, parentAlias, keyProperty);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.AddOneToManyCollection(navigation, navigationType, stringBuilder, parentAlias, keyProperty);
|
||||
}
|
||||
|
||||
stringBuilder.Append(") as \"").Append(navigation.Name.Replace("Joined", string.Empty)).AppendLine("\"");
|
||||
}
|
||||
|
||||
private void AddOneToManyCollection(INavigation navigation, IEntityType navigationType, StringBuilder stringBuilder, string parentAlias, IProperty keyProperty)
|
||||
{
|
||||
var primaryKeyName = navigationType.GetDeclaredPrimaryKeyColumnName();
|
||||
|
||||
var navigationAlias = this.GetNextAlias(parentAlias);
|
||||
|
||||
stringBuilder.AppendLine(", (")
|
||||
.Append("select array_to_json(array_agg(row_to_json(").Append(navigationAlias).AppendLine("))) from (");
|
||||
|
||||
if (navigation.IsMemberOfAggregate())
|
||||
{
|
||||
this.AddTypeToQuery(navigationType, stringBuilder, navigationAlias);
|
||||
stringBuilder.Append(") ").AppendLine(navigationAlias)
|
||||
.Append("where ").Append(navigationAlias).Append(".\"").Append(keyProperty.Name).Append("\" = ").Append(parentAlias).Append(".\"").Append(primaryKeyName).AppendLine("\"");
|
||||
}
|
||||
else
|
||||
{
|
||||
var primaryKeyColumnName = navigationType.GetPrimaryKeyColumnName(); // It's always one property, usually called "Id"
|
||||
stringBuilder.Append("select \"").Append(primaryKeyColumnName).AppendLine("\" as \"$ref\"");
|
||||
stringBuilder.Append("from ").Append(navigationType.GetSchema()).Append(".\"").Append(navigationType.GetTableName()).AppendLine("\" ")
|
||||
.Append("where \"").Append(keyProperty.Name).Append("\" = ").Append(parentAlias).Append(".\"").Append(primaryKeyName).AppendLine("\"")
|
||||
.Append(") as ").AppendLine(navigationAlias);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the many to many collection to the query.
|
||||
/// We assume that every many to many join entity only consists of the both foreign keys.
|
||||
/// Additionally we assume that every target entity is referencable.
|
||||
/// Therefore we just select the target entities as reference.
|
||||
/// When adding an object of the target type, our Ef-Core entity classes would automatically create join entities
|
||||
/// with the right keys, because they use <see cref="ManyToManyCollectionAdapter{T,TJoin}"/>s.
|
||||
/// </summary>
|
||||
/// <param name="navigationType">Type of the navigation, the join entity type.</param>
|
||||
/// <param name="entityType">Type of the entity which is currently getting processed. It holds the collection which is about to be added.</param>
|
||||
/// <param name="stringBuilder">The string builder which is used to create the query string.</param>
|
||||
/// <param name="parentAlias">The parent alias, required to reference the primary key of the <paramref name="entityType"/>.</param>
|
||||
/// <param name="keyProperty">The key property.</param>
|
||||
private void AddManyToManyCollection(IEntityType navigationType, IEntityType entityType, StringBuilder stringBuilder, string parentAlias, IProperty keyProperty)
|
||||
{
|
||||
var navigationAlias = this.GetNextAlias(parentAlias);
|
||||
var entityTypePrimaryKeyName = entityType.GetPrimaryKeyColumnName(); // usually "Id"
|
||||
var otherEntityTypeForeignKey = navigationType.GetForeignKeys().FirstOrDefault(fk => fk.PrincipalEntityType != entityType);
|
||||
var otherEntityTypeKey = navigationType.GetKeys().FirstOrDefault(fk => fk.DeclaringEntityType != entityType);
|
||||
var referenceColumnToOtherEntity = otherEntityTypeForeignKey?.GetColumnName() ?? otherEntityTypeKey?.GetColumnName()
|
||||
?? throw new InvalidOperationException("No reference column available.");
|
||||
|
||||
stringBuilder.AppendLine(", (")
|
||||
.Append("select array_to_json(array_agg(row_to_json(").Append(navigationAlias).AppendLine("))) from (");
|
||||
|
||||
stringBuilder.Append("select \"").Append(referenceColumnToOtherEntity).AppendLine("\" as \"$ref\"")
|
||||
.Append("from ").Append(navigationType.GetSchema()).Append(".\"").Append(navigationType.GetTableName()).AppendLine("\" ")
|
||||
.Append("where ").Append('"').Append(keyProperty.Name).Append("\" = ").Append(parentAlias).Append(".\"").Append(entityTypePrimaryKeyName).AppendLine("\"")
|
||||
.Append(") as ").AppendLine(navigationAlias);
|
||||
}
|
||||
}
|
||||
46
src/Persistence/EntityFramework/LetterBodyRepository.cs
Normal file
46
src/Persistence/EntityFramework/LetterBodyRepository.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
// <copyright file="LetterBodyRepository.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.Threading;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Repository which is able to load <see cref="LetterBody"/>s for a specific letter header.
|
||||
/// </summary>
|
||||
internal class LetterBodyRepository : CachingGenericRepository<LetterBody>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LetterBodyRepository" /> class.
|
||||
/// </summary>
|
||||
/// <param name="repositoryProvider">The repository provider.</param>
|
||||
/// <param name="loggerFactory">The logger factory.</param>
|
||||
public LetterBodyRepository(IContextAwareRepositoryProvider repositoryProvider, ILoggerFactory loggerFactory)
|
||||
: base(repositoryProvider, loggerFactory)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the letter body by the id of its header.
|
||||
/// </summary>
|
||||
/// <param name="headerId">The id of its header.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>
|
||||
/// The body of the header.
|
||||
/// </returns>
|
||||
public async ValueTask<LetterBody?> GetBodyByHeaderIdAsync(Guid headerId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var context = this.GetContext();
|
||||
var letterBody = await context.Context.Set<LetterBody>().FirstOrDefaultAsync(body => body.HeaderId == headerId, cancellationToken).ConfigureAwait(false);
|
||||
if (letterBody is not null)
|
||||
{
|
||||
await this.LoadDependentDataAsync(letterBody, context.Context, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return letterBody;
|
||||
}
|
||||
}
|
||||
51
src/Persistence/EntityFramework/List.cs
Normal file
51
src/Persistence/EntityFramework/List.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
// <copyright file="List.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;
|
||||
|
||||
/// <summary>
|
||||
/// A generic list which implements additional required interfaces for the usage as list and collection in the object model.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the items of the list.</typeparam>
|
||||
/// <seealso cref="System.Collections.Generic.List{T}" />
|
||||
internal class List<T> : System.Collections.Generic.List<T>, ILoadingStatusAwareList
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="List{T}"/> class that
|
||||
/// is empty and has the default initial capacity.
|
||||
/// </summary>
|
||||
public List()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="List{T}"/> class that
|
||||
/// is empty and has the specified initial capacity.
|
||||
/// </summary>
|
||||
/// <param name="capacity">The number of elements that the new list can initially store.</param>
|
||||
public List(int capacity)
|
||||
: base(capacity)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="List{T}"/> class that
|
||||
/// contains elements copied from the specified collection and has sufficient capacity
|
||||
/// to accommodate the number of elements copied.
|
||||
/// </summary>
|
||||
/// <param name="collection">The collection whose elements are copied to the new list.</param>
|
||||
public List(IEnumerable<T> collection)
|
||||
: base(collection)
|
||||
{
|
||||
this.LoadingStatus = LoadingStatus.Loaded;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the loading status.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The loading status.
|
||||
/// </value>
|
||||
public LoadingStatus LoadingStatus { get; set; } = LoadingStatus.Unloaded;
|
||||
}
|
||||
36
src/Persistence/EntityFramework/LoadingStatus.cs
Normal file
36
src/Persistence/EntityFramework/LoadingStatus.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
// <copyright file="LoadingStatus.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;
|
||||
|
||||
/// <summary>
|
||||
/// The loading status of a <see cref="ILoadingStatusAware"/>.
|
||||
/// </summary>
|
||||
internal enum LoadingStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// The undefined state.
|
||||
/// </summary>
|
||||
Undefined,
|
||||
|
||||
/// <summary>
|
||||
/// The data is not loaded yet.
|
||||
/// </summary>
|
||||
Unloaded,
|
||||
|
||||
/// <summary>
|
||||
/// The data is loaded.
|
||||
/// </summary>
|
||||
Loaded,
|
||||
|
||||
/// <summary>
|
||||
/// The data is loading at the moment.
|
||||
/// </summary>
|
||||
Loading,
|
||||
|
||||
/// <summary>
|
||||
/// The loading of the data failed.
|
||||
/// </summary>
|
||||
Failed,
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<WarningsAsErrors>nullable;CS4014;VSTHRD110;VSTHRD100</WarningsAsErrors>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<!--<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>-->
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<OutputPath>..\..\..\bin\Debug\</OutputPath>
|
||||
<DocumentationFile>..\..\..\bin\Debug\MUnique.OpenMU.Persistence.EntityFramework.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<OutputPath>..\..\..\bin\Release\</OutputPath>
|
||||
<DocumentationFile>..\..\..\bin\Release\MUnique.OpenMU.Persistence.EntityFramework.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BCrypt.Net-Next" />
|
||||
<PackageReference Include="Mapster" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\AttributeSystem\MUnique.OpenMU.AttributeSystem.csproj" />
|
||||
<ProjectReference Include="..\..\DataModel\MUnique.OpenMU.DataModel.csproj" />
|
||||
<ProjectReference Include="..\..\Interfaces\MUnique.OpenMU.Interfaces.csproj" />
|
||||
<ProjectReference Include="..\..\PlugIns\MUnique.OpenMU.PlugIns.csproj" />
|
||||
<ProjectReference Include="..\MUnique.OpenMU.Persistence.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="ConnectionSettings.xml">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Service Include="{508349b6-6b84-4df5-91f0-309beebad82d}" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PreBuild" BeforeTargets="PreBuildEvent" Condition="'$(ci)'!='true'">
|
||||
<Exec Command="dotnet run --project ../SourceGenerator/MUnique.OpenMU.Persistence.SourceGenerator.csproj -c:$(ConfigurationName) MUnique.OpenMU.Persistence.EntityFramework "$(ProjectDir)Model" --no-build" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
105
src/Persistence/EntityFramework/ManyToManyCollectionAdapter.cs
Normal file
105
src/Persistence/EntityFramework/ManyToManyCollectionAdapter.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
// <copyright file="ManyToManyCollectionAdapter.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.Collections;
|
||||
|
||||
/// <summary>
|
||||
/// A many-to-many collection adapter which adapts beween <typeparamref name="T"/> and <typeparamref name="TJoin"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Usually in our object model we don't define collections of join entities and their types.
|
||||
/// This is done automatically by our T4 templates.
|
||||
/// </remarks>
|
||||
/// <typeparam name="T">The type which is in a many to many relationship.</typeparam>
|
||||
/// <typeparam name="TJoin">The type of the join entity. It contains a property of <typeparamref name="T"/>.</typeparam>
|
||||
/// <seealso cref="System.Collections.Generic.ICollection{T}" />
|
||||
internal class ManyToManyCollectionAdapter<T, TJoin> : ICollection<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// The raw collection, which is usually the collection which is mapped by entity framework.
|
||||
/// </summary>
|
||||
private readonly ICollection<TJoin> _rawCollection;
|
||||
|
||||
/// <summary>
|
||||
/// The function to create a new join entity.
|
||||
/// </summary>
|
||||
private readonly Func<T, TJoin> _createJoinEntityFunction;
|
||||
|
||||
/// <summary>
|
||||
/// The function to extract the instance of <typeparamref name="T"/> out of <typeparamref name="TJoin"/>.
|
||||
/// </summary>
|
||||
private readonly Func<TJoin, T> _extractFunction;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ManyToManyCollectionAdapter{T, TJoin}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="rawCollection">The raw collection, which is usually the collection which is mapped by entity framework.</param>
|
||||
/// <param name="extractFunction">The function to extract the instance of <typeparamref name="T"/> out of <typeparamref name="TJoin"/>.</param>
|
||||
/// <param name="createJoinEntityFunction">The function to create a new join entity.</param>
|
||||
public ManyToManyCollectionAdapter(ICollection<TJoin> rawCollection, Func<TJoin, T> extractFunction, Func<T, TJoin> createJoinEntityFunction)
|
||||
{
|
||||
this._rawCollection = rawCollection;
|
||||
this._createJoinEntityFunction = createJoinEntityFunction;
|
||||
this._extractFunction = extractFunction;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Count => this._rawCollection.Count;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsReadOnly => this._rawCollection.IsReadOnly;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerator<T> GetEnumerator()
|
||||
{
|
||||
return this._rawCollection.Select(this._extractFunction).GetEnumerator();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return this.GetEnumerator();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Add(T item)
|
||||
{
|
||||
if (item != null)
|
||||
{
|
||||
this._rawCollection.Add(this._createJoinEntityFunction(item));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Clear()
|
||||
{
|
||||
this._rawCollection.Clear();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Contains(T item)
|
||||
{
|
||||
return this._rawCollection.Any(i => object.Equals(this._extractFunction(i), item));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void CopyTo(T[] array, int arrayIndex)
|
||||
{
|
||||
this._rawCollection.Select(this._extractFunction).ToList().CopyTo(array, arrayIndex);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Remove(T item)
|
||||
{
|
||||
var joinItem = this._rawCollection.FirstOrDefault(i => object.Equals(this._extractFunction(i), item));
|
||||
if (joinItem != null)
|
||||
{
|
||||
return this._rawCollection.Remove(joinItem);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
4199
src/Persistence/EntityFramework/Migrations/00000000000000_Initial.cs
Normal file
4199
src/Persistence/EntityFramework/Migrations/00000000000000_Initial.cs
Normal file
File diff suppressed because it is too large
Load Diff
4519
src/Persistence/EntityFramework/Migrations/00000000000000_Initial.designer.cs
generated
Normal file
4519
src/Persistence/EntityFramework/Migrations/00000000000000_Initial.designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
4525
src/Persistence/EntityFramework/Migrations/20221008183306_PetLevels.Designer.cs
generated
Normal file
4525
src/Persistence/EntityFramework/Migrations/20221008183306_PetLevels.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,49 @@
|
||||
// <copyright file="20221008183306_PetLevels.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations
|
||||
{
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
/// <summary>
|
||||
/// Adds pet experience columns to Item and ItemDefinition.
|
||||
/// </summary>
|
||||
public partial class PetLevels : Migration
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "PetExperienceFormula",
|
||||
schema: "config",
|
||||
table: "ItemDefinition",
|
||||
type: "text",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "PetExperience",
|
||||
schema: "data",
|
||||
table: "Item",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PetExperienceFormula",
|
||||
schema: "config",
|
||||
table: "ItemDefinition");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PetExperience",
|
||||
schema: "data",
|
||||
table: "Item");
|
||||
}
|
||||
}
|
||||
}
|
||||
4599
src/Persistence/EntityFramework/Migrations/20221018194652_Combo.Designer.cs
generated
Normal file
4599
src/Persistence/EntityFramework/Migrations/20221018194652_Combo.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,124 @@
|
||||
// <copyright file="20221018194652_Combo.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations
|
||||
{
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
/// <summary>
|
||||
/// Adds skill combo definitions and steps.
|
||||
/// </summary>
|
||||
public partial class Combo : Migration
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "ComboDefinitionId",
|
||||
schema: "config",
|
||||
table: "CharacterClass",
|
||||
type: "uuid",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "SkillComboDefinition",
|
||||
schema: "config",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Name = table.Column<string>(type: "text", nullable: false),
|
||||
MaximumCompletionTime = table.Column<TimeSpan>(type: "interval", nullable: false),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_SkillComboDefinition", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "SkillComboStep",
|
||||
schema: "config",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
SkillId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
SkillComboDefinitionId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
Order = table.Column<int>(type: "integer", nullable: false),
|
||||
IsFinalStep = table.Column<bool>(type: "boolean", nullable: false),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_SkillComboStep", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_SkillComboStep_Skill_SkillId",
|
||||
column: x => x.SkillId,
|
||||
principalSchema: "config",
|
||||
principalTable: "Skill",
|
||||
principalColumn: "Id");
|
||||
table.ForeignKey(
|
||||
name: "FK_SkillComboStep_SkillComboDefinition_SkillComboDefinitionId",
|
||||
column: x => x.SkillComboDefinitionId,
|
||||
principalSchema: "config",
|
||||
principalTable: "SkillComboDefinition",
|
||||
principalColumn: "Id");
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CharacterClass_ComboDefinitionId",
|
||||
schema: "config",
|
||||
table: "CharacterClass",
|
||||
column: "ComboDefinitionId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SkillComboStep_SkillComboDefinitionId",
|
||||
schema: "config",
|
||||
table: "SkillComboStep",
|
||||
column: "SkillComboDefinitionId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SkillComboStep_SkillId",
|
||||
schema: "config",
|
||||
table: "SkillComboStep",
|
||||
column: "SkillId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_CharacterClass_SkillComboDefinition_ComboDefinitionId",
|
||||
schema: "config",
|
||||
table: "CharacterClass",
|
||||
column: "ComboDefinitionId",
|
||||
principalSchema: "config",
|
||||
principalTable: "SkillComboDefinition",
|
||||
principalColumn: "Id");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_CharacterClass_SkillComboDefinition_ComboDefinitionId",
|
||||
schema: "config",
|
||||
table: "CharacterClass");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "SkillComboStep",
|
||||
schema: "config");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "SkillComboDefinition",
|
||||
schema: "config");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_CharacterClass_ComboDefinitionId",
|
||||
schema: "config",
|
||||
table: "CharacterClass");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ComboDefinitionId",
|
||||
schema: "config",
|
||||
table: "CharacterClass");
|
||||
}
|
||||
}
|
||||
}
|
||||
4751
src/Persistence/EntityFramework/Migrations/20221121161552_DeleteCascade.Designer.cs
generated
Normal file
4751
src/Persistence/EntityFramework/Migrations/20221121161552_DeleteCascade.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
4762
src/Persistence/EntityFramework/Migrations/20221121163220_AttributeRelationship_OperandAttribute.Designer.cs
generated
Normal file
4762
src/Persistence/EntityFramework/Migrations/20221121163220_AttributeRelationship_OperandAttribute.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,60 @@
|
||||
// <copyright file="20221121163220_AttributeRelationship_OperandAttribute.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations
|
||||
{
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AttributeRelationshipOperandAttribute : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "OperandAttributeId",
|
||||
schema: "config",
|
||||
table: "AttributeRelationship",
|
||||
type: "uuid",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AttributeRelationship_OperandAttributeId",
|
||||
schema: "config",
|
||||
table: "AttributeRelationship",
|
||||
column: "OperandAttributeId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_AttributeRelationship_AttributeDefinition_OperandAttributeId",
|
||||
schema: "config",
|
||||
table: "AttributeRelationship",
|
||||
column: "OperandAttributeId",
|
||||
principalSchema: "config",
|
||||
principalTable: "AttributeDefinition",
|
||||
principalColumn: "Id");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_AttributeRelationship_AttributeDefinition_OperandAttributeId",
|
||||
schema: "config",
|
||||
table: "AttributeRelationship");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_AttributeRelationship_OperandAttributeId",
|
||||
schema: "config",
|
||||
table: "AttributeRelationship");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "OperandAttributeId",
|
||||
schema: "config",
|
||||
table: "AttributeRelationship");
|
||||
}
|
||||
}
|
||||
}
|
||||
4759
src/Persistence/EntityFramework/Migrations/20221128203249_RemoveConsumeHandler.Designer.cs
generated
Normal file
4759
src/Persistence/EntityFramework/Migrations/20221128203249_RemoveConsumeHandler.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,34 @@
|
||||
// <copyright file="20221128203249_RemoveConsumeHandler.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations
|
||||
{
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class RemoveConsumeHandler : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ConsumeHandlerClass",
|
||||
schema: "config",
|
||||
table: "ItemDefinition");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ConsumeHandlerClass",
|
||||
schema: "config",
|
||||
table: "ItemDefinition",
|
||||
type: "text",
|
||||
nullable: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
4764
src/Persistence/EntityFramework/Migrations/20221128204928_AddItemDropDuration.Designer.cs
generated
Normal file
4764
src/Persistence/EntityFramework/Migrations/20221128204928_AddItemDropDuration.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,36 @@
|
||||
// <copyright file="20221128204928_AddItemDropDuration.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations
|
||||
{
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddItemDropDuration : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<TimeSpan>(
|
||||
name: "ItemDropDuration",
|
||||
schema: "config",
|
||||
table: "GameConfiguration",
|
||||
type: "interval",
|
||||
nullable: false,
|
||||
defaultValue: new TimeSpan(0, 0, 1, 0, 0));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ItemDropDuration",
|
||||
schema: "config",
|
||||
table: "GameConfiguration");
|
||||
}
|
||||
}
|
||||
}
|
||||
4767
src/Persistence/EntityFramework/Migrations/20221221182248_MuHelper.Designer.cs
generated
Normal file
4767
src/Persistence/EntityFramework/Migrations/20221221182248_MuHelper.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,34 @@
|
||||
// <copyright file="20221221182248_MuHelper.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations
|
||||
{
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class MuHelper : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<byte[]>(
|
||||
name: "MuHelperConfiguration",
|
||||
schema: "data",
|
||||
table: "Character",
|
||||
type: "bytea",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MuHelperConfiguration",
|
||||
schema: "data",
|
||||
table: "Character");
|
||||
}
|
||||
}
|
||||
}
|
||||
4810
src/Persistence/EntityFramework/Migrations/20230303185735_ConfigurationUpdate.Designer.cs
generated
Normal file
4810
src/Persistence/EntityFramework/Migrations/20230303185735_ConfigurationUpdate.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,62 @@
|
||||
// <copyright file="20230303185735_ConfigurationUpdate.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations
|
||||
{
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class ConfigurationUpdate : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ConfigurationUpdate",
|
||||
schema: "config",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Version = table.Column<int>(type: "integer", nullable: false),
|
||||
Name = table.Column<string>(type: "text", nullable: true),
|
||||
Description = table.Column<string>(type: "text", nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||
InstalledAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ConfigurationUpdate", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ConfigurationUpdateState",
|
||||
schema: "config",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
InitializationKey = table.Column<string>(type: "text", nullable: true),
|
||||
CurrentInstalledVersion = table.Column<int>(type: "integer", nullable: false),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ConfigurationUpdateState", x => x.Id);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "ConfigurationUpdate",
|
||||
schema: "config");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ConfigurationUpdateState",
|
||||
schema: "config");
|
||||
}
|
||||
}
|
||||
}
|
||||
4822
src/Persistence/EntityFramework/Migrations/20230306185635_MiniGameExt.Designer.cs
generated
Normal file
4822
src/Persistence/EntityFramework/Migrations/20230306185635_MiniGameExt.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,95 @@
|
||||
// <copyright file="20230306185635_MiniGameExt.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations
|
||||
{
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class MiniGameExt : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsClientUpdateRequired",
|
||||
schema: "config",
|
||||
table: "MiniGameTerrainChange",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AlterColumn<DateTime>(
|
||||
name: "Timestamp",
|
||||
schema: "data",
|
||||
table: "MiniGameRankingEntry",
|
||||
type: "timestamp with time zone",
|
||||
nullable: true,
|
||||
oldClrType: typeof(DateTime),
|
||||
oldType: "timestamp with time zone");
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "AllowParty",
|
||||
schema: "config",
|
||||
table: "MiniGameDefinition",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "ArePlayerKillersAllowedToEnter",
|
||||
schema: "config",
|
||||
table: "MiniGameDefinition",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "EntranceFee",
|
||||
schema: "config",
|
||||
table: "MiniGameDefinition",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsClientUpdateRequired",
|
||||
schema: "config",
|
||||
table: "MiniGameTerrainChange");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AllowParty",
|
||||
schema: "config",
|
||||
table: "MiniGameDefinition");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ArePlayerKillersAllowedToEnter",
|
||||
schema: "config",
|
||||
table: "MiniGameDefinition");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "EntranceFee",
|
||||
schema: "config",
|
||||
table: "MiniGameDefinition");
|
||||
|
||||
migrationBuilder.AlterColumn<DateTime>(
|
||||
name: "Timestamp",
|
||||
schema: "data",
|
||||
table: "MiniGameRankingEntry",
|
||||
type: "timestamp with time zone",
|
||||
nullable: false,
|
||||
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
oldClrType: typeof(DateTime),
|
||||
oldType: "timestamp with time zone",
|
||||
oldNullable: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
4849
src/Persistence/EntityFramework/Migrations/20230324205415_AddSystemConfiguration.Designer.cs
generated
Normal file
4849
src/Persistence/EntityFramework/Migrations/20230324205415_AddSystemConfiguration.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,73 @@
|
||||
// <copyright file="20230324205415_AddSystemConfiguration.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations
|
||||
{
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddSystemConfiguration : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_QuestMonsterKillRequirementState_CharacterQuestState_Charac~",
|
||||
schema: "data",
|
||||
table: "QuestMonsterKillRequirementState");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "SystemConfiguration",
|
||||
schema: "config",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
IpResolver = table.Column<int>(type: "integer", nullable: false),
|
||||
IpResolverParameter = table.Column<string>(type: "text", nullable: true),
|
||||
AutoStart = table.Column<bool>(type: "boolean", nullable: false),
|
||||
AutoUpdateSchema = table.Column<bool>(type: "boolean", nullable: false),
|
||||
ReadConsoleInput = table.Column<bool>(type: "boolean", nullable: false),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_SystemConfiguration", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_QuestMonsterKillRequirementState_CharacterQuestState_Charac~",
|
||||
schema: "data",
|
||||
table: "QuestMonsterKillRequirementState",
|
||||
column: "CharacterQuestStateId",
|
||||
principalSchema: "data",
|
||||
principalTable: "CharacterQuestState",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_QuestMonsterKillRequirementState_CharacterQuestState_Charac~",
|
||||
schema: "data",
|
||||
table: "QuestMonsterKillRequirementState");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "SystemConfiguration",
|
||||
schema: "config");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_QuestMonsterKillRequirementState_CharacterQuestState_Charac~",
|
||||
schema: "data",
|
||||
table: "QuestMonsterKillRequirementState",
|
||||
column: "CharacterQuestStateId",
|
||||
principalSchema: "data",
|
||||
principalTable: "CharacterQuestState",
|
||||
principalColumn: "Id");
|
||||
}
|
||||
}
|
||||
}
|
||||
4852
src/Persistence/EntityFramework/Migrations/20230504172021_StorageLimitPerCharacter.Designer.cs
generated
Normal file
4852
src/Persistence/EntityFramework/Migrations/20230504172021_StorageLimitPerCharacter.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,35 @@
|
||||
// <copyright file="20230504172021_StorageLimitPerCharacter.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations
|
||||
{
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class StorageLimitPerCharacter : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "StorageLimitPerCharacter",
|
||||
schema: "config",
|
||||
table: "ItemDefinition",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "StorageLimitPerCharacter",
|
||||
schema: "config",
|
||||
table: "ItemDefinition");
|
||||
}
|
||||
}
|
||||
}
|
||||
4770
src/Persistence/EntityFramework/Migrations/20230701010432_AddChatBanUntil.Designer.cs
generated
Normal file
4770
src/Persistence/EntityFramework/Migrations/20230701010432_AddChatBanUntil.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,34 @@
|
||||
// <copyright file="20230701010432_AddChatBanUntil.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations
|
||||
{
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddChatBanUntil : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<DateTime?>(
|
||||
name: "ChatBanUntil",
|
||||
schema: "data",
|
||||
table: "Account",
|
||||
type: "timestamp with time zone",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ChatBanUntil",
|
||||
schema: "data",
|
||||
table: "Account");
|
||||
}
|
||||
}
|
||||
}
|
||||
4879
src/Persistence/EntityFramework/Migrations/20230724154747_AccountAndMapAttributes.Designer.cs
generated
Normal file
4879
src/Persistence/EntityFramework/Migrations/20230724154747_AccountAndMapAttributes.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,99 @@
|
||||
// <copyright file="20230724154747_AccountAndMapAttributes.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations
|
||||
{
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AccountAndMapAttributes : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "AccountId",
|
||||
schema: "data",
|
||||
table: "StatAttribute",
|
||||
type: "uuid",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "GameMapDefinitionId",
|
||||
schema: "config",
|
||||
table: "PowerUpDefinition",
|
||||
type: "uuid",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_StatAttribute_AccountId",
|
||||
schema: "data",
|
||||
table: "StatAttribute",
|
||||
column: "AccountId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PowerUpDefinition_GameMapDefinitionId",
|
||||
schema: "config",
|
||||
table: "PowerUpDefinition",
|
||||
column: "GameMapDefinitionId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_PowerUpDefinition_GameMapDefinition_GameMapDefinitionId",
|
||||
schema: "config",
|
||||
table: "PowerUpDefinition",
|
||||
column: "GameMapDefinitionId",
|
||||
principalSchema: "config",
|
||||
principalTable: "GameMapDefinition",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_StatAttribute_Account_AccountId",
|
||||
schema: "data",
|
||||
table: "StatAttribute",
|
||||
column: "AccountId",
|
||||
principalSchema: "data",
|
||||
principalTable: "Account",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_PowerUpDefinition_GameMapDefinition_GameMapDefinitionId",
|
||||
schema: "config",
|
||||
table: "PowerUpDefinition");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_StatAttribute_Account_AccountId",
|
||||
schema: "data",
|
||||
table: "StatAttribute");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_StatAttribute_AccountId",
|
||||
schema: "data",
|
||||
table: "StatAttribute");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_PowerUpDefinition_GameMapDefinitionId",
|
||||
schema: "config",
|
||||
table: "PowerUpDefinition");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AccountId",
|
||||
schema: "data",
|
||||
table: "StatAttribute");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "GameMapDefinitionId",
|
||||
schema: "config",
|
||||
table: "PowerUpDefinition");
|
||||
}
|
||||
}
|
||||
}
|
||||
4878
src/Persistence/EntityFramework/Migrations/20240602060055_FixItemOptions.Designer.cs
generated
Normal file
4878
src/Persistence/EntityFramework/Migrations/20240602060055_FixItemOptions.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,110 @@
|
||||
// <copyright file="20240602060055_FixItemOptions.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations
|
||||
{
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class FixItemOptions : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_IncreasableItemOption_ItemSetGroup_ItemSetGroupId",
|
||||
schema: "config",
|
||||
table: "IncreasableItemOption");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_IncreasableItemOption_ItemSetGroup_ItemSetGroupId",
|
||||
schema: "config",
|
||||
table: "IncreasableItemOption",
|
||||
column: "ItemSetGroupId",
|
||||
principalSchema: "config",
|
||||
principalTable: "ItemSetGroup",
|
||||
principalColumn: "Id");
|
||||
|
||||
// First, we need to create the new item option definitions for the ancient sets
|
||||
migrationBuilder.Sql(
|
||||
"""
|
||||
INSERT INTO config."ItemOptionDefinition" ("Id", "GameConfigurationId", "Name", "AddsRandomly", "AddChance", "MaximumOptionsPerItem")
|
||||
SELECT UUID(REPLACE(sets.setId, '00000092-','00000083-')), '00000001-0001-0000-0000-000000000000', sets.setName || ' (Ancient Set)', false, 0, 0
|
||||
FROM
|
||||
(
|
||||
SELECT COUNT(o."Id") c, text(o."ItemSetGroupId") setId, g."Name" setName
|
||||
FROM config."IncreasableItemOption" o, config."ItemSetGroup" g
|
||||
WHERE "ItemSetGroupId" is not null
|
||||
AND "ItemOptionDefinitionId" is null
|
||||
AND g."Id" = o."ItemSetGroupId"
|
||||
GROUP BY o."ItemSetGroupId", g."Name"
|
||||
ORDER BY c
|
||||
) sets
|
||||
WHERE sets.c > 1
|
||||
""");
|
||||
|
||||
// Then we need to update the item option definitions of the ancient sets,
|
||||
// so that they belong to the previously created item option defintions
|
||||
migrationBuilder.Sql(
|
||||
"""
|
||||
UPDATE config."IncreasableItemOption" o
|
||||
SET "ItemOptionDefinitionId" = UUID(REPLACE(TEXT(o."ItemSetGroupId"), '00000092-','00000083-'))
|
||||
WHERE o."ItemSetGroupId" is not null
|
||||
AND o."ItemOptionDefinitionId" is null
|
||||
AND o."OptionTypeId" is not null
|
||||
""");
|
||||
|
||||
// Then what's left are the options for set completion of normal sets.
|
||||
migrationBuilder.Sql(
|
||||
"""
|
||||
INSERT INTO config."ItemOptionDefinition" ("Id", "GameConfigurationId", "Name", "AddsRandomly", "AddChance", "MaximumOptionsPerItem")
|
||||
SELECT UUID(REPLACE(sets.optionId, '00000088-','00000083-')),
|
||||
'00000001-0001-0000-0000-000000000000',
|
||||
CASE WHEN sets.setLevel=0 THEN 'Complete Set Bonus (any level)'
|
||||
ELSE 'Complete Set Bonus (Level ' || sets.setLevel || ')'
|
||||
END,
|
||||
false, 0, 0
|
||||
FROM
|
||||
(
|
||||
SELECT text(o."Id") optionId, text(o."ItemSetGroupId") setId, g."Name" setName, g."SetLevel" setLevel
|
||||
FROM config."IncreasableItemOption" o, config."ItemSetGroup" g
|
||||
WHERE "ItemSetGroupId" is not null
|
||||
AND "ItemOptionDefinitionId" is null
|
||||
AND g."Id" = o."ItemSetGroupId"
|
||||
ORDER BY setLevel
|
||||
) sets
|
||||
""");
|
||||
|
||||
// Then we need to update the item option definitions of the normal sets
|
||||
// so that they belong to the previously created item option defintions
|
||||
migrationBuilder.Sql(
|
||||
"""
|
||||
UPDATE config."IncreasableItemOption" o
|
||||
SET "ItemOptionDefinitionId" = UUID(REPLACE(TEXT(o."Id"), '00000088-','00000083-'))
|
||||
WHERE o."ItemOptionDefinitionId" is null
|
||||
""");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_IncreasableItemOption_ItemSetGroup_ItemSetGroupId",
|
||||
schema: "config",
|
||||
table: "IncreasableItemOption");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_IncreasableItemOption_ItemSetGroup_ItemSetGroupId",
|
||||
schema: "config",
|
||||
table: "IncreasableItemOption",
|
||||
column: "ItemSetGroupId",
|
||||
principalSchema: "config",
|
||||
principalTable: "ItemSetGroup",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user