diff options
Diffstat (limited to 'lib/VNLib.Plugins.Extensions.Data')
12 files changed, 234 insertions, 418 deletions
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IConcurrentDbContext.cs b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IConcurrentDbContext.cs deleted file mode 100644 index f3308c5..0000000 --- a/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IConcurrentDbContext.cs +++ /dev/null @@ -1,44 +0,0 @@ -/* -* Copyright (c) 2023 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Extensions.Data -* File: IConcurrentDbContext.cs -* -* IConcurrentDbContext.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify -* it under the terms of the GNU Affero General Public License as -* published by the Free Software Foundation, either version 3 of the -* License, or (at your option) any later version. -* -* VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU Affero General Public License for more details. -* -* You should have received a copy of the GNU Affero General Public License -* along with this program. If not, see https://www.gnu.org/licenses/. -*/ - -using System.Threading; -using System.Threading.Tasks; -using System.Transactions; - -namespace VNLib.Plugins.Extensions.Data.Abstractions -{ - /// <summary> - /// Represents a database context that can manage concurrency via transactions - /// </summary> - public interface IConcurrentDbContext : ITransactionalDbContext - { - /// <summary> - /// Opens a single transaction on the current context. If a transaction is already open, - /// it is disposed and a new transaction is begun. - /// </summary> - /// <param name="isolationLevel">The isolation level of the transaction</param> - /// <param name="cancellationToken">A token to cancel the operations</param> - Task OpenTransactionAsync(IsolationLevel isolationLevel, CancellationToken cancellationToken = default); - } -}
\ No newline at end of file diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDataStore.cs b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDataStore.cs index bdd1b6c..e966176 100644 --- a/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDataStore.cs +++ b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDataStore.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Extensions.Data @@ -37,7 +37,7 @@ namespace VNLib.Plugins.Extensions.Data.Abstractions string GetNewRecordId(); /// <summary> - /// Gets a new <see cref="TransactionalDbContext"/> ready for use + /// Gets a new <see cref="IDbContextHandle"/> ready for use /// </summary> /// <returns></returns> IDbContextHandle GetNewContext(); diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDbContextHandle.cs b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDbContextHandle.cs index 73734b5..0d06467 100644 --- a/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDbContextHandle.cs +++ b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDbContextHandle.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Extensions.Data diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/ITransactionalDbContext.cs b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/ITransactionalDbContext.cs deleted file mode 100644 index dd906d1..0000000 --- a/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/ITransactionalDbContext.cs +++ /dev/null @@ -1,58 +0,0 @@ -/* -* Copyright (c) 2023 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Extensions.Data -* File: TransactionalDbContext.cs -* -* TransactionalDbContext.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify -* it under the terms of the GNU Affero General Public License as -* published by the Free Software Foundation, either version 3 of the -* License, or (at your option) any later version. -* -* VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU Affero General Public License for more details. -* -* You should have received a copy of the GNU Affero General Public License -* along with this program. If not, see https://www.gnu.org/licenses/. -*/ - -using System.Threading; -using System.Threading.Tasks; - -using Microsoft.EntityFrameworkCore.Storage; - -namespace VNLib.Plugins.Extensions.Data.Abstractions -{ - /// <summary> - /// Represents a database context that can manage concurrency via transactions - /// </summary> - public interface ITransactionalDbContext - { - /// <summary> - /// The transaction that was opened on the current context - /// </summary> - IDbContextTransaction? Transaction { get; set; } - - /// <summary> - /// Invokes the <see cref="IDbContextTransaction.Commit"/> on the current context - /// </summary> - Task CommitTransactionAsync(CancellationToken token = default); - - /// <summary> - /// Opens a single transaction on the current context. If a transaction is already open, - /// it is disposed and a new transaction is begun. - /// </summary> - Task OpenTransactionAsync(CancellationToken cancellationToken = default); - - /// <summary> - /// Invokes the <see cref="IDbContextTransaction.Rollback"/> on the current context - /// </summary> - Task RollbackTransctionAsync(CancellationToken token = default); - } -}
\ No newline at end of file diff --git a/lib/VNLib.Plugins.Extensions.Data/src/DBContextBase.cs b/lib/VNLib.Plugins.Extensions.Data/src/DBContextBase.cs new file mode 100644 index 0000000..e933452 --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.Data/src/DBContextBase.cs @@ -0,0 +1,85 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Data +* File: DBContextBase.cs +* +* DBContextBase.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; + +using Microsoft.EntityFrameworkCore; + +using VNLib.Utils; +using VNLib.Plugins.Extensions.Data.Abstractions; + + +namespace VNLib.Plugins.Extensions.Data +{ + /// <summary> + /// Provides abstract implementation of a database context that can manage concurrency via transactions + /// </summary> + public abstract class DBContextBase : DbContext, IAsyncDisposable, IDbContextHandle + { + /// <summary> + /// <inheritdoc/> + /// </summary> + protected DBContextBase() + { } + + /// <summary> + /// <inheritdoc/> + /// </summary> + protected DBContextBase(DbContextOptions options) : base(options) + { } + + ///<inheritdoc/> + IQueryable<T> IDbContextHandle.Set<T>() => base.Set<T>(); + + ///<inheritdoc/> + void IDbContextHandle.Add<T>(T entity) => base.Add(entity); + + ///<inheritdoc/> + void IDbContextHandle.Remove<T>(T entity) => base.Remove<T>(entity); + + ///<inheritdoc/> + public virtual void AddRange<T>(IEnumerable<T> entities) where T : class + { + DbSet<T> set = base.Set<T>(); + set.AddRange(entities); + } + + ///<inheritdoc/> + public virtual async Task<ERRNO> SaveAndCloseAsync(bool commit, CancellationToken cancellation = default) + { + return await base.SaveChangesAsync(cancellation); + } + + ///<inheritdoc/> + public virtual void RemoveRange<T>(IEnumerable<T> entities) where T : class + { + DbSet<T> set = base.Set<T>(); + set.RemoveRange(entities); + } + } +}
\ No newline at end of file diff --git a/lib/VNLib.Plugins.Extensions.Data/src/DbStore.cs b/lib/VNLib.Plugins.Extensions.Data/src/DbStore.cs index b5f327e..5593507 100644 --- a/lib/VNLib.Plugins.Extensions.Data/src/DbStore.cs +++ b/lib/VNLib.Plugins.Extensions.Data/src/DbStore.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Extensions.Data @@ -38,7 +38,7 @@ namespace VNLib.Plugins.Extensions.Data { /// <summary> - /// Gets a new <see cref="TransactionalDbContext"/> ready for use + /// Gets a new <see cref="IDbContextHandle"/> ready for use /// </summary> /// <returns></returns> public abstract IDbContextHandle GetNewContext(); diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Extensions/DbStoreExtensions.cs b/lib/VNLib.Plugins.Extensions.Data/src/Extensions/DbStoreExtensions.cs index e053900..e3d4f6b 100644 --- a/lib/VNLib.Plugins.Extensions.Data/src/Extensions/DbStoreExtensions.cs +++ b/lib/VNLib.Plugins.Extensions.Data/src/Extensions/DbStoreExtensions.cs @@ -1,11 +1,11 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Extensions.Data -* File: DbStore.cs +* File: DbStoreExtensions.cs * -* DbStore.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger +* DbStoreExtensions.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger * VNLib collection of libraries and utilities. * * VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify @@ -23,9 +23,9 @@ */ using System; +using System.Data; using System.Linq; using System.Threading; -using System.Transactions; using System.Threading.Tasks; using System.Collections.Generic; @@ -51,8 +51,10 @@ namespace VNLib.Plugins.Extensions.Data.Extensions public static async Task<ERRNO> AddOrUpdateAsync<T>(this IDataStore<T> store, T record, CancellationToken cancellation = default) where T : class, IDbModel { + ArgumentNullException.ThrowIfNull(store); + //Open new db context - await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.ReadCommitted, cancellation); + await using IDbContextHandle ctx = store.GetNewContext(); IQueryable<T> query; @@ -73,7 +75,7 @@ namespace VNLib.Plugins.Extensions.Data.Extensions T? entry = await query.SingleOrDefaultAsync(cancellation); //Check if creted - if (entry == null) + if (entry is null) { //Create a new template id record.Id = store.GetNewRecordId(); @@ -100,8 +102,10 @@ namespace VNLib.Plugins.Extensions.Data.Extensions public static async Task<ERRNO> UpdateAsync<T>(this IDataStore<T> store, T record, CancellationToken cancellation = default) where T : class, IDbModel { + ArgumentNullException.ThrowIfNull(store); + //Open new db context - await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.Serializable, cancellation); + await using IDbContextHandle ctx = store.GetNewContext(); //Get the application IQueryable<T> query = store.QueryTable.UpdateQueryBuilder(ctx, record); @@ -130,8 +134,10 @@ namespace VNLib.Plugins.Extensions.Data.Extensions public static async Task<ERRNO> CreateAsync<T>(this IDataStore<T> store, T record, CancellationToken cancellation = default) where T : class, IDbModel { + ArgumentNullException.ThrowIfNull(store); + //Open new db context - await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.ReadUncommitted, cancellation); + await using IDbContextHandle ctx = store.GetNewContext(); //Create a new template id record.Id = store.GetNewRecordId(); @@ -155,8 +161,10 @@ namespace VNLib.Plugins.Extensions.Data.Extensions public static async Task<long> GetCountAsync<T>(this IDataStore<T> store, CancellationToken cancellation = default) where T : class, IDbModel { - //Open db connection - await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.ReadUncommitted, cancellation); + ArgumentNullException.ThrowIfNull(store); + + //Open new db context + await using IDbContextHandle ctx = store.GetNewContext(); //Async get the number of records of the given entity type long count = await ctx.Set<T>().LongCountAsync(cancellation); @@ -177,7 +185,10 @@ namespace VNLib.Plugins.Extensions.Data.Extensions public static async Task<long> GetCountAsync<T>(this IDataStore<T> store, string specifier, CancellationToken cancellation = default) where T : class, IDbModel { - await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.ReadUncommitted, cancellation); + ArgumentNullException.ThrowIfNull(store); + + //Open new db context + await using IDbContextHandle ctx = store.GetNewContext(); //Async get the number of records of the given entity type long count = await store.QueryTable.GetCountQueryBuilder(ctx, specifier).LongCountAsync(cancellation); @@ -199,8 +210,10 @@ namespace VNLib.Plugins.Extensions.Data.Extensions public static async Task<T?> GetSingleAsync<T>(this IDataStore<T> store, string key, CancellationToken cancellation = default) where T : class, IDbModel { - //Open db connection - await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.ReadUncommitted, cancellation); + ArgumentNullException.ThrowIfNull(store); + + //Open new db context + await using IDbContextHandle ctx = store.GetNewContext(); //Get the single template by its id T? record = await (from entry in ctx.Set<T>() @@ -223,8 +236,10 @@ namespace VNLib.Plugins.Extensions.Data.Extensions public static async Task<T?> GetSingleAsync<T>(this IDataStore<T> store, params string[] specifiers) where T : class, IDbModel { - //Open db connection - await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.ReadUncommitted); + ArgumentNullException.ThrowIfNull(store); + + //Open new db context + await using IDbContextHandle ctx = store.GetNewContext(); //Get the single item by specifiers T? record = await store.QueryTable.GetSingleQueryBuilder(ctx, specifiers).SingleOrDefaultAsync(); @@ -245,8 +260,10 @@ namespace VNLib.Plugins.Extensions.Data.Extensions public static async Task<T?> GetSingleAsync<T>(this IDataStore<T> store, T record, CancellationToken cancellation = default) where T : class, IDbModel { - //Open db connection - await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.ReadUncommitted, cancellation); + ArgumentNullException.ThrowIfNull(store); + + //Open new db context + await using IDbContextHandle ctx = store.GetNewContext(); //Get the single template by its id T? entry = await store.QueryTable.GetSingleQueryBuilder(ctx, record).SingleOrDefaultAsync(cancellation); @@ -272,8 +289,10 @@ namespace VNLib.Plugins.Extensions.Data.Extensions { int previous = collection.Count; + ArgumentNullException.ThrowIfNull(store); + //Open new db context - await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.ReadUncommitted, cancellation); + await using IDbContextHandle ctx = store.GetNewContext(); //Get the single template by its id await store.QueryTable.GetCollectionQueryBuilder(ctx, specifier) @@ -303,8 +322,10 @@ namespace VNLib.Plugins.Extensions.Data.Extensions { int previous = collection.Count; + ArgumentNullException.ThrowIfNull(store); + //Open new db context - await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.ReadUncommitted); + await using IDbContextHandle ctx = store.GetNewContext(); //Get the single template by its id await store.QueryTable.GetCollectionQueryBuilder(ctx, args) @@ -331,8 +352,10 @@ namespace VNLib.Plugins.Extensions.Data.Extensions public static async Task<ERRNO> DeleteAsync<T>(this IDataStore<T> store, T record, CancellationToken cancellation = default) where T : class, IDbModel { + ArgumentNullException.ThrowIfNull(store); + //Open new db context - await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.RepeatableRead, cancellation); + await using IDbContextHandle ctx = store.GetNewContext(); //Get a query for a a single item IQueryable<T> query = store.QueryTable.GetSingleQueryBuilder(ctx, record); @@ -340,7 +363,7 @@ namespace VNLib.Plugins.Extensions.Data.Extensions //Get the entry if it exists T? entry = await query.SingleOrDefaultAsync(cancellation); - if (entry == null) + if (entry is null) { await ctx.SaveAndCloseAsync(false, cancellation); return false; @@ -364,7 +387,7 @@ namespace VNLib.Plugins.Extensions.Data.Extensions where T : class, IDbModel { //Open new db context - await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.RepeatableRead, cancellation); + await using IDbContextHandle ctx = store.GetNewContext(); //Get a query for a a single item IQueryable<T> query = store.QueryTable.GetSingleQueryBuilder(ctx, key); @@ -372,14 +395,14 @@ namespace VNLib.Plugins.Extensions.Data.Extensions //Get the entry if it exists T? entry = await query.SingleOrDefaultAsync(cancellation); - if (entry == null) + if (entry is null) { await ctx.SaveAndCloseAsync(false, cancellation); return false; } else { - //Remove the entry + //Remove the entry then exit ctx.Remove(entry); return await ctx.SaveAndCloseAsync(true, cancellation); } @@ -394,15 +417,17 @@ namespace VNLib.Plugins.Extensions.Data.Extensions public static async Task<ERRNO> DeleteAsync<T>(this IDataStore<T> store, params string[] specifiers) where T : class, IDbModel { + ArgumentNullException.ThrowIfNull(store); + //Open new db context - await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.RepeatableRead); + await using IDbContextHandle ctx = store.GetNewContext(); //Get the template by its id IQueryable<T> query = store.QueryTable.DeleteQueryBuilder(ctx, specifiers); T? entry = await query.SingleOrDefaultAsync(); - if (entry == null) + if (entry is null) { return false; } @@ -429,8 +454,10 @@ namespace VNLib.Plugins.Extensions.Data.Extensions //Store preivous count int previous = collection.Count; - //Open db connection - await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.ReadUncommitted, cancellation); + ArgumentNullException.ThrowIfNull(store); + + //Open new db context + await using IDbContextHandle ctx = store.GetNewContext(); //Get a page offset and a limit for the await ctx.Set<T>() @@ -459,11 +486,14 @@ namespace VNLib.Plugins.Extensions.Data.Extensions public static async Task<int> GetPageAsync<T>(this IDataStore<T> store, ICollection<T> collection, int page, int limit, params string[] constraints) where T : class, IDbModel { + ArgumentNullException.ThrowIfNull(store); + ArgumentNullException.ThrowIfNull(collection); + //Store preivous count int previous = collection.Count; //Open new db context - await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.ReadUncommitted); + await using IDbContextHandle ctx = store.GetNewContext(); //Get a page of records constrained by the given arguments await store.QueryTable.GetPageQueryBuilder(ctx, constraints) @@ -480,50 +510,102 @@ namespace VNLib.Plugins.Extensions.Data.Extensions return collection.Count - previous; } - - public static Task<ERRNO> AddBulkAsync<T>(this IDataStore<T> store, IEnumerable<T> records, string userId, bool overwriteTime = true, CancellationToken cancellation = default) - where T : class, IDbModel, IUserEntity + /// <summary> + /// Specifies the user-id for each record in collection of user entities + /// during enumeration. + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="current"></param> + /// <param name="userId">The user-id to specify</param> + /// <returns>A new enumeration instance that will add the new user-id to each record upon enumerating</returns> + public static IEnumerable<T> WithUserId<T>(this IEnumerable<T> current, string? userId) + where T : IUserEntity { - //Assign user-id when numerated - IEnumerable<T> withUserId = records.Select(p => + return current.Select(p => { p.UserId = userId; return p; }); + } + + /// <summary> + /// Updates the created and last modified times for each record in the collection + /// with the current time in UTC. + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="current"></param> + /// <param name="overwriteTime">A value that indicates if the record's created time should be overwritten or ignored</param> + /// <returns>A new enumeration that will updated record times upon enumerating</returns> + public static IEnumerable<T> WithCurrentUtcTime<T>(this IEnumerable<T> current, bool overwriteTime) where T : IDbModel + { + return WithTime(current, DateTime.UtcNow, overwriteTime); + } - return store.AddBulkAsync(withUserId, overwriteTime, cancellation); + /// <summary> + /// Updates the created and last modified times for each record in the collection + /// with the current system local time. + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="current"></param> + /// <param name="overwriteTime">A value that indicates if the record's created time should be overwritten or ignored</param> + /// <returns>A new enumeration that will updated record times upon enumerating</returns> + public static IEnumerable<T> WithCurrentLocalTime<T>(this IEnumerable<T> current, bool overwriteTime) where T : IDbModel + { + return WithTime(current, DateTime.Now, overwriteTime); } - public static async Task<ERRNO> AddBulkAsync<T>(this IDataStore<T> store, IEnumerable<T> records, bool overwriteTime = true, CancellationToken cancellation = default) - where T : class, IDbModel + /// <summary> + /// Updates the created and last modified times for each record in the collection + /// with the specified time. + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="current"></param> + /// <param name="now">A structure to the time to set seach record's last modifed, and optionally created time</param> + /// <param name="overwriteTime">A value that indicates if the record's created time should be overwritten or ignored</param> + /// <returns>A new enumeration that will updated record times upon enumerating</returns> + public static IEnumerable<T> WithTime<T>(this IEnumerable<T> current, DateTime now, bool overwriteTime) + where T : IDbModel { - DateTime now = DateTime.UtcNow; + return current.Select(p => + { + //Only overwrite the created time if it is the default value or the overwrite flag is set + if (overwriteTime || p.Created == default) + { + p.Created = now; + } - //Open context and transaction - await using IDbContextHandle database = await store.OpenAsync(IsolationLevel.ReadCommitted, cancellation); + //Always update the last modified time + p.LastModified = now; + return p; + }); + } - //Get the entity set - IQueryable<T> set = database.Set<T>(); + /// <summary> + /// Adds a collection of records directly to the store without any additional processing + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="store"></param> + /// <param name="records">The collection of records to add</param> + /// <param name="cancellation">A token to cancel the operation</param> + /// <returns>A task the complets with the number of records update</returns> + public static async Task<ERRNO> AddBulkAsync<T>(this IDataStore<T> store, IEnumerable<T> records, CancellationToken cancellation = default) + where T : class, IDbModel + { + ArgumentNullException.ThrowIfNull(store); + ArgumentNullException.ThrowIfNull(records); + + //Open new db context + await using IDbContextHandle database = store.GetNewContext(); //Generate random ids for the feeds and set user-id foreach (T entity in records) { entity.Id = store.GetNewRecordId(); - - //If the entity has the default created time, update it, otherwise leave it as is - if (overwriteTime || entity.Created == default) - { - entity.Created = now; - } - - //Update last-modified time - entity.LastModified = now; } //Add bulk items to database database.AddRange(records); return await database.SaveAndCloseAsync(true, cancellation); } - } } diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Extensions/DbStoreHelperExtensions.cs b/lib/VNLib.Plugins.Extensions.Data/src/Extensions/DbStoreHelperExtensions.cs deleted file mode 100644 index 55230cf..0000000 --- a/lib/VNLib.Plugins.Extensions.Data/src/Extensions/DbStoreHelperExtensions.cs +++ /dev/null @@ -1,89 +0,0 @@ -/* -* Copyright (c) 2023 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Extensions.Data -* File: DbStoreHelperExtensions.cs -* -* DbStoreHelperExtensions.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify -* it under the terms of the GNU Affero General Public License as -* published by the Free Software Foundation, either version 3 of the -* License, or (at your option) any later version. -* -* VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU Affero General Public License for more details. -* -* You should have received a copy of the GNU Affero General Public License -* along with this program. If not, see https://www.gnu.org/licenses/. -*/ - -using System.Threading; -using System.Transactions; -using System.Threading.Tasks; - -using VNLib.Plugins.Extensions.Data.Abstractions; - -namespace VNLib.Plugins.Extensions.Data.Extensions -{ - internal static class DbStoreHelperExtensions - { - /// <summary> - /// If the current context instance inherits the <see cref="IConcurrentDbContext"/> interface, - /// attempts to open a transaction with the specified isolation level. - /// </summary> - /// <param name="tdb"></param> - /// <param name="isolationLevel">The transaction isolation level</param> - /// <param name="cancellationToken">A token to cancel the operation</param> - /// <returns></returns> - internal static Task OpenTransactionAsync(this ITransactionalDbContext tdb, IsolationLevel isolationLevel, CancellationToken cancellationToken = default) - { - if (tdb is IConcurrentDbContext ccdb) - { - return ccdb.OpenTransactionAsync(isolationLevel, cancellationToken); - } - else - { - //Just ignore the isolation level - return tdb.OpenTransactionAsync(cancellationToken); - } - } - - /// <summary> - /// Opens a new database connection. If the context supports transactions, it will - /// open a transaction with the specified isolation level. - /// </summary> - /// <typeparam name="T"></typeparam> - /// <param name="store"></param> - /// <param name="level">The transaction isolation level</param> - /// <param name="cancellation">A token to cancel the transaction operation</param> - /// <returns>A task that resolves the new open <see cref="IDbContextHandle"/> </returns> - public static async Task<IDbContextHandle> OpenAsync<T>(this IDataStore<T> store, IsolationLevel level, CancellationToken cancellation = default) - where T : class, IDbModel - { - //Open new db context - IDbContextHandle ctx = store.GetNewContext(); - - //Support transactions and start them if the context supports it - if(ctx is ITransactionalDbContext tdb) - { - try - { - //Open transaction - await tdb.OpenTransactionAsync(level, cancellation); - } - catch - { - await ctx.DisposeAsync(); - throw; - } - } - - return ctx; - } - } -} diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageContext.cs b/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageContext.cs index fabd54e..032d380 100644 --- a/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageContext.cs +++ b/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageContext.cs @@ -29,7 +29,7 @@ using Microsoft.EntityFrameworkCore; namespace VNLib.Plugins.Extensions.Data.Storage { #nullable disable - internal sealed class LWStorageContext : TransactionalDbContext + internal sealed class LWStorageContext : DBContextBase { private readonly string TableName; public DbSet<LWStorageEntry> Descriptors { get; set; } diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageManager.cs b/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageManager.cs index bc506c7..f098def 100644 --- a/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageManager.cs +++ b/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageManager.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Extensions.Data @@ -32,8 +32,6 @@ using Microsoft.EntityFrameworkCore; using VNLib.Utils; using VNLib.Utils.Async; -using VNLib.Plugins.Extensions.Data.Extensions; - namespace VNLib.Plugins.Extensions.Data.Storage { @@ -78,10 +76,7 @@ namespace VNLib.Plugins.Extensions.Data.Storage /// <exception cref="LWDescriptorCreationException"></exception> public async Task<LWStorageDescriptor> CreateDescriptorAsync(string userId, string? descriptorIdOverride = null, CancellationToken cancellation = default) { - if (string.IsNullOrWhiteSpace(userId)) - { - throw new ArgumentNullException(nameof(userId)); - } + ArgumentException.ThrowIfNullOrWhiteSpace(userId); //If no override id was specified, generate a new one descriptorIdOverride ??= NewDescriptorIdGenerator(); @@ -89,7 +84,6 @@ namespace VNLib.Plugins.Extensions.Data.Storage DateTime createdOrModifedTime = DateTime.UtcNow; await using LWStorageContext ctx = GetContext(); - await ctx.OpenTransactionAsync(cancellation); //Make sure the descriptor doesnt exist only by its descriptor id if (await ctx.Descriptors.AnyAsync(d => d.Id == descriptorIdOverride, cancellation)) @@ -129,16 +123,11 @@ namespace VNLib.Plugins.Extensions.Data.Storage /// <exception cref="ArgumentNullException"></exception> public async Task<LWStorageDescriptor?> GetDescriptorFromUIDAsync(string userid, CancellationToken cancellation = default) { - //Allow null/empty entrys to just return null - if (string.IsNullOrWhiteSpace(userid)) - { - throw new ArgumentNullException(nameof(userid)); - } + ArgumentException.ThrowIfNullOrWhiteSpace(userid); //Init db - await using LWStorageContext db = GetContext(); - //Begin transaction - await db.OpenTransactionAsync(cancellation); + await using LWStorageContext db = GetContext(); + //Get entry LWStorageEntry? entry = await (from s in db.Descriptors where s.UserId == userid @@ -161,16 +150,11 @@ namespace VNLib.Plugins.Extensions.Data.Storage /// <exception cref="ArgumentNullException"></exception> public async Task<LWStorageDescriptor?> GetDescriptorFromIDAsync(string descriptorId, CancellationToken cancellation = default) { - //Allow null/empty entrys to just return null - if (string.IsNullOrWhiteSpace(descriptorId)) - { - throw new ArgumentNullException(nameof(descriptorId)); - } + ArgumentException.ThrowIfNullOrWhiteSpace(descriptorId); //Init db - await using LWStorageContext db = GetContext(); - //Begin transaction - await db.OpenTransactionAsync(cancellation); + await using LWStorageContext db = GetContext(); + //Get entry LWStorageEntry? entry = await (from s in db.Descriptors where s.Id == descriptorId @@ -201,8 +185,6 @@ namespace VNLib.Plugins.Extensions.Data.Storage { //Init db await using LWStorageContext db = GetContext(); - //Begin transaction - await db.OpenTransactionAsync(cancellation); //Get all expired entires LWStorageEntry[] expired = await (from s in db.Descriptors @@ -224,7 +206,6 @@ namespace VNLib.Plugins.Extensions.Data.Storage try { await using LWStorageContext ctx = GetContext(); - await ctx.OpenTransactionAsync(System.Transactions.IsolationLevel.RepeatableRead, cancellation); //Begin tracking ctx.Descriptors.Attach(entry); @@ -254,8 +235,6 @@ namespace VNLib.Plugins.Extensions.Data.Storage { //Init db await using LWStorageContext db = GetContext(); - //Begin transaction - await db.OpenTransactionAsync(cancellation); //Delete the user from the database db.Descriptors.Remove(descriptor); diff --git a/lib/VNLib.Plugins.Extensions.Data/src/TransactionalDbContext.cs b/lib/VNLib.Plugins.Extensions.Data/src/TransactionalDbContext.cs deleted file mode 100644 index cbb9799..0000000 --- a/lib/VNLib.Plugins.Extensions.Data/src/TransactionalDbContext.cs +++ /dev/null @@ -1,139 +0,0 @@ -/* -* Copyright (c) 2023 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Extensions.Data -* File: TransactionalDbContext.cs -* -* TransactionalDbContext.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify -* it under the terms of the GNU Affero General Public License as -* published by the Free Software Foundation, either version 3 of the -* License, or (at your option) any later version. -* -* VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU Affero General Public License for more details. -* -* You should have received a copy of the GNU Affero General Public License -* along with this program. If not, see https://www.gnu.org/licenses/. -*/ - -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using System.Collections.Generic; - -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Storage; - -using VNLib.Utils; -using VNLib.Plugins.Extensions.Data.Abstractions; - - -namespace VNLib.Plugins.Extensions.Data -{ - /// <summary> - /// Abstract implementation of <see cref="ITransactionalDbContext"/> that provides a transactional context for database operations - /// </summary> - public abstract class TransactionalDbContext : DbContext, IAsyncDisposable, ITransactionalDbContext, IDbContextHandle - { - /// <summary> - /// <inheritdoc/> - /// </summary> - protected TransactionalDbContext() - { } - - /// <summary> - /// <inheritdoc/> - /// </summary> - protected TransactionalDbContext(DbContextOptions options) : base(options) - { } - - ///<inheritdoc/> - public IDbContextTransaction? Transaction { get; set; } - - ///<inheritdoc/> - public async Task OpenTransactionAsync(CancellationToken cancellationToken = default) - { - //open a new transaction on the current database - this.Transaction = await base.Database.BeginTransactionAsync(cancellationToken); - } - - ///<inheritdoc/> - public Task CommitTransactionAsync(CancellationToken token = default) - { - return Transaction != null ? Transaction.CommitAsync(token) : Task.CompletedTask; - } - - ///<inheritdoc/> - public Task RollbackTransctionAsync(CancellationToken token = default) - { - return Transaction != null ? Transaction.RollbackAsync(token) : Task.CompletedTask; - } - - ///<inheritdoc/> - IQueryable<T> IDbContextHandle.Set<T>() => base.Set<T>(); - - ///<inheritdoc/> - void IDbContextHandle.Add<T>(T entity) => base.Add(entity); - - ///<inheritdoc/> - void IDbContextHandle.Remove<T>(T entity) => base.Remove<T>(entity); - - ///<inheritdoc/> - public virtual void AddRange<T>(IEnumerable<T> entities) where T : class - { - DbSet<T> set = base.Set<T>(); - set.AddRange(entities); - } - - ///<inheritdoc/> - public virtual async Task<ERRNO> SaveAndCloseAsync(bool commit, CancellationToken cancellation = default) - { - //Save db changes - ERRNO result = await base.SaveChangesAsync(cancellation); - if (commit) - { - await CommitTransactionAsync(cancellation); - } - else - { - await RollbackTransctionAsync(cancellation); - } - return result; - } - - ///<inheritdoc/> - public virtual void RemoveRange<T>(IEnumerable<T> entities) where T : class - { - DbSet<T> set = base.Set<T>(); - set.RemoveRange(entities); - } - -#pragma warning disable CA1816 // Dispose methods should call SuppressFinalize, ignore because base.Dispose() is called - ///<inheritdoc/> - public sealed override void Dispose() - { - //dispose the transaction - Transaction?.Dispose(); - base.Dispose(); - } - - ///<inheritdoc/> - public override async ValueTask DisposeAsync() - { - //If transaction has been created, dispose the transaction - if (Transaction != null) - { - await Transaction.DisposeAsync(); - } - await base.DisposeAsync(); - } -#pragma warning restore CA1816 // Dispose methods should call SuppressFinalize - } -}
\ No newline at end of file diff --git a/lib/VNLib.Plugins.Extensions.Data/src/VNLib.Plugins.Extensions.Data.csproj b/lib/VNLib.Plugins.Extensions.Data/src/VNLib.Plugins.Extensions.Data.csproj index 7e9136f..cc181e1 100644 --- a/lib/VNLib.Plugins.Extensions.Data/src/VNLib.Plugins.Extensions.Data.csproj +++ b/lib/VNLib.Plugins.Extensions.Data/src/VNLib.Plugins.Extensions.Data.csproj @@ -47,7 +47,7 @@ <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> - <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" /> + <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.1" /> </ItemGroup> <ItemGroup> |