// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // namespace MUnique.OpenMU.Persistence; using System.Collections; using System.Diagnostics; using System.Threading; using Microsoft.Extensions.Logging; using Nito.AsyncEx; /// /// Provider which provides the latest and it's containing /// child objects. /// /// The type of the owner. /// /// Approach: One context for each composition root type. When child data is going to be edited, the whole type /// should be loaded. /// public abstract class DataSourceBase : IDataSource where TOwner : class { private readonly ILogger> _logger; private readonly AsyncLock _loadLock = new(); private IContext? _context; private TOwner? _owner; private IDictionary? _subObjects; /// /// Initializes a new instance of the class. /// /// The logger. /// The persistence context provider. protected DataSourceBase(ILogger> logger, IPersistenceContextProvider persistenceContextProvider) { this._logger = logger; this.ContextProvider = persistenceContextProvider; } /// /// Gets the mapping of a to their of the . /// protected abstract IReadOnlyDictionary> TypeToEnumerables { get; } /// /// Gets the which can be used to create new contexts. /// protected IPersistenceContextProvider ContextProvider { get; } private TOwner Owner => this._owner ?? throw new InvalidOperationException("owner is not loaded."); private IDictionary SubObjects => this._subObjects ??= this.BuildDictionary(); /// public bool IsSupporting(Type type) { return this.TypeToEnumerables.ContainsKey(type); } /// async ValueTask IDataSource.GetOwnerAsync(Guid ownerId, CancellationToken cancellationToken) { return (TOwner)(await this.GetOwnerAsync(ownerId, cancellationToken).ConfigureAwait(false)); } /// public async ValueTask GetContextAsync(CancellationToken cancellationToken) { return this._context ??= await this.CreateNewContextAsync().ConfigureAwait(false); } /// public async ValueTask GetOwnerAsync(Guid ownerId = default, CancellationToken cancellationToken = default) { using var l = await this._loadLock.LockAsync(cancellationToken).ConfigureAwait(false); if (this._owner is { } owner && (ownerId == Guid.Empty || owner.GetId() == ownerId)) { return owner; } var context = await this.GetContextAsync(cancellationToken).ConfigureAwait(false); this._logger.LogDebug("Loading owner ..."); var stopwatch = new Stopwatch(); stopwatch.Start(); if (ownerId == Guid.Empty) { owner = (await context.GetAsync().ConfigureAwait(false)).FirstOrDefault(); } else { owner = await context.GetByIdAsync(ownerId, cancellationToken).ConfigureAwait(false); } this._owner = owner; this._subObjects = null; stopwatch.Stop(); this._logger.LogDebug("Loaded owner in {duration}", stopwatch.Elapsed); return owner ?? throw new InvalidOperationException("Owner not found"); } /// public async ValueTask DiscardChangesAsync() { using var l = await this._loadLock.LockAsync().ConfigureAwait(false); if (this._context?.HasChanges is true) { // next time, we have to load again. // if we would be able to clone objects, that wouldn't be necessary. this.Reset(); } } /// public async ValueTask ForceDiscardChangesAsync() { using var l = await this._loadLock.LockAsync().ConfigureAwait(false); this.Reset(); } /// public IEnumerable GetAll() { return this.GetAll(typeof(T)).OfType(); } /// public IEnumerable GetAll(Type type) { var gameConfiguration = this._owner ?? throw new InvalidOperationException("config is not loaded."); if (this.TypeToEnumerables.TryGetValue(type, out var getter)) { return getter(gameConfiguration); } throw new ArgumentOutOfRangeException($"The type {type} is not registered as child of the {nameof(this.Owner)}", nameof(type)); } /// public IIdentifiable? Get(Guid id) { using var l = this._loadLock.Lock(); if (this.SubObjects.TryGetValue(id, out var obj)) { return obj; } return null; } /// public void Dispose() { this.Reset(); } /// /// Creates a new . /// /// The created . protected virtual ValueTask CreateNewContextAsync() { return ValueTask.FromResult(this.ContextProvider.CreateNewContext()); } private IDictionary BuildDictionary() { var owner = this.Owner; var result = new Dictionary(); foreach (var (type, objects) in this.TypeToEnumerables) { foreach (var obj in objects(owner).OfType()) { if (!result.TryAdd(obj.Id, obj)) { this._logger.LogDebug($"Duplicate key {obj.Id}, type {type}."); } } } return result; } private void Reset() { this._owner = null; this._context?.Dispose(); this._context = null; this._subObjects = null; } }