diff options
Diffstat (limited to 'lib')
29 files changed, 1090 insertions, 619 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> diff --git a/lib/VNLib.Plugins.Extensions.Loading.Sql/src/SqlDbConnectionLoader.cs b/lib/VNLib.Plugins.Extensions.Loading.Sql/src/SqlDbConnectionLoader.cs index 803f36e..4897b59 100644 --- a/lib/VNLib.Plugins.Extensions.Loading.Sql/src/SqlDbConnectionLoader.cs +++ b/lib/VNLib.Plugins.Extensions.Loading.Sql/src/SqlDbConnectionLoader.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Extensions.Loading.Sql @@ -25,15 +25,12 @@ using System; using System.Linq; using System.Data; +using System.Text; using System.Data.Common; using System.Threading.Tasks; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using MySqlConnector; - -using Microsoft.Data.Sqlite; -using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore; using VNLib.Utils.Logging; @@ -52,12 +49,10 @@ namespace VNLib.Plugins.Extensions.Loading.Sql { public const string SQL_CONFIG_KEY = "sql"; public const string DB_PASSWORD_KEY = "db_password"; - public const string EXTERN_SQL_LIB_KEY = "custom_assembly"; - - public const string EXTERN_LIB_GET_CONN_FUNC_NAME = "GetDbConnections"; + public const string SQL_PROVIDER_DLL_KEY = "provider"; private const string MAX_LEN_BYPASS_KEY = "MaxLen"; - private const string TIMESTAMP_BYPASS = "TimeStamp"; + private const string TIMESTAMP_BYPASS = "TimeStamp"; /// <summary> @@ -87,90 +82,30 @@ namespace VNLib.Plugins.Extensions.Loading.Sql /// <exception cref="ObjectDisposedException"></exception> public static IAsyncLazy<Func<DbConnection>> GetConnectionFactoryAsync(this PluginBase plugin) { - static IAsyncLazy<Func<DbConnection>> FactoryLoader(PluginBase plugin) - { - return Task.Run(() => GetFactoryLoaderAsync(plugin)).AsLazy(); - } - plugin.ThrowIfUnloaded(); - //Get or load - return LoadingExtensions.GetOrCreateSingleton(plugin, FactoryLoader); + //Get the provider singleton + DbProvider provider = LoadingExtensions.GetOrCreateSingleton(plugin, GetDbPovider); + + return provider.ConnectionFactory.Value.AsLazy(); } - private async static Task<Func<DbConnection>> GetFactoryLoaderAsync(PluginBase plugin) + private static DbProvider GetDbPovider(PluginBase plugin) { + //Get the sql configuration scope IConfigScope sqlConf = plugin.GetConfig(SQL_CONFIG_KEY); - - //See if the user wants to use a custom assembly - if (sqlConf.ContainsKey(EXTERN_SQL_LIB_KEY)) - { - string dllPath = sqlConf.GetRequiredProperty(EXTERN_SQL_LIB_KEY, k => k.GetString()!); - - //Load the library and get instance - object dbProvider = plugin.CreateServiceExternal<object>(dllPath); - - return ManagedLibrary.GetMethod<Func<DbConnection>>(dbProvider, EXTERN_LIB_GET_CONN_FUNC_NAME); - } - - //Get the db-type - string? type = sqlConf.GetPropString("db_type"); - - //Try to get the password and always dispose the secret value - using ISecretResult? password = await plugin.TryGetSecretAsync(DB_PASSWORD_KEY); - - DbConnectionStringBuilder sqlBuilder; - - if ("sqlite".Equals(type, StringComparison.OrdinalIgnoreCase)) - { - //Use connection builder - sqlBuilder = new SqliteConnectionStringBuilder() - { - DataSource = sqlConf["source"].GetString(), - Password = password?.Result.ToString(), - Pooling = true, - Mode = SqliteOpenMode.ReadWriteCreate - }; - - string connectionString = sqlBuilder.ToString(); - return () => new SqliteConnection(connectionString); - } - else if("mysql".Equals(type, StringComparison.OrdinalIgnoreCase)) - { - sqlBuilder = new MySqlConnectionStringBuilder() - { - Server = sqlConf["hostname"].GetString(), - Database = sqlConf["catalog"].GetString(), - UserID = sqlConf["username"].GetString(), - Password = password?.Result.ToString(), - Pooling = true, - LoadBalance = MySqlLoadBalance.LeastConnections, - MinimumPoolSize = sqlConf["min_pool_size"].GetUInt32(), - }; - - string connectionString = sqlBuilder.ToString(); - return () => new MySqlConnection(connectionString); - } - //Default to mssql - else - { - //Use connection builder - sqlBuilder = new SqlConnectionStringBuilder() - { - DataSource = sqlConf["hostname"].GetString(), - UserID = sqlConf["username"].GetString(), - Password = password?.Result.ToString(), - InitialCatalog = sqlConf["catalog"].GetString(), - IntegratedSecurity = sqlConf["ms_security"].GetBoolean(), - Pooling = true, - MinPoolSize = sqlConf["min_pool_size"].GetInt32(), - Replication = true, - TrustServerCertificate = sqlConf["trust_cert"].GetBoolean(), - }; - - string connectionString = sqlBuilder.ToString(); - return () => new SqlConnection(connectionString); - } + + //Get the provider dll path + string dllPath = sqlConf.GetRequiredProperty(SQL_PROVIDER_DLL_KEY, k => k.GetString()!); + + /* + * I am loading a bare object here and dynamically resolbing the required methods + * insead of forcing a shared interface. This allows the external library to be + * more flexible and slimmer. + */ + object instance = plugin.CreateServiceExternal<object>(dllPath); + + return new(instance, sqlConf); } /// <summary> @@ -202,59 +137,12 @@ namespace VNLib.Plugins.Extensions.Loading.Sql /// <remarks>If plugin is in debug mode, writes log data to the default log</remarks> public static IAsyncLazy<DbContextOptions> GetContextOptionsAsync(this PluginBase plugin) { - static IAsyncLazy<DbContextOptions> LoadOptions(PluginBase plugin) - { - //Wrap in a lazy options - return GetDbOptionsAsync(plugin).AsLazy(); - } - plugin.ThrowIfUnloaded(); - return LoadingExtensions.GetOrCreateSingleton(plugin, LoadOptions); - } - private async static Task<DbContextOptions> GetDbOptionsAsync(PluginBase plugin) - { - try - { - //Get a db connection object, we must wait synchronously tho - await using DbConnection connection = (await plugin.GetConnectionFactoryAsync()).Invoke(); - - DbContextOptionsBuilder builder = new(); + //Get the provider singleton + DbProvider provider = LoadingExtensions.GetOrCreateSingleton(plugin, GetDbPovider); - //Determine connection type - if (connection is SqlConnection sql) - { - //Use sql server from connection - builder.UseSqlServer(sql.ConnectionString); - } - else if (connection is SqliteConnection slc) - { - builder.UseSqlite(slc.ConnectionString); - } - else if (connection is MySqlConnection msconn) - { - //Detect version - ServerVersion version = ServerVersion.AutoDetect(msconn); - - builder.UseMySql(msconn.ConnectionString, version); - } - - //Enable logging - if (plugin.IsDebug()) - { - builder.LogTo(plugin.Log.Debug); - } - - //Get context and freez it before returning - DbContextOptions options = builder.Options; - options.Freeze(); - return options; - } - catch(Exception ex) - { - plugin.Log.Error(ex, "DBContext options load error"); - throw; - } + return provider.OptionsFactory.Value.AsLazy(); } /// <summary> @@ -282,20 +170,23 @@ namespace VNLib.Plugins.Extensions.Loading.Sql /// <returns>A task that resolves when the tables have been created</returns> public static async Task EnsureDbCreatedAsync<T>(this PluginBase plugin, T dbCreator, object? state) where T : IDbTableDefinition { + ArgumentNullException.ThrowIfNull(plugin); + ArgumentNullException.ThrowIfNull(dbCreator); + DbBuilder builder = new(); //Invoke ontbCreating to setup the dbBuilder dbCreator.OnDatabaseCreating(builder, state); + //Get the abstract database from the connection type + IDBCommandGenerator cb = GetCmdGenerator(plugin); + //Wait for the connection factory to load Func<DbConnection> dbConFactory = await GetConnectionFactoryAsync(plugin); //Create a new db connection await using DbConnection connection = dbConFactory(); - //Get the abstract database from the connection type - IDBCommandGenerator cb = connection.GetCmGenerator(); - //Compile the db command as a text Sql command string[] createComamnds = builder.BuildCreateCommand(cb); @@ -452,22 +343,28 @@ namespace VNLib.Plugins.Extensions.Loading.Sql #endregion - private static IDBCommandGenerator GetCmGenerator(this IDbConnection connection) + private static IDBCommandGenerator GetCmdGenerator(PluginBase plugin) { - //Determine connection type - if (connection is SqlConnection) + //Get the provider singleton + DbProvider provider = LoadingExtensions.GetOrCreateSingleton(plugin, GetDbPovider); + + //See if the provider has a command builder function, otherwise try to use known defaults + if (provider.HasCommandBuilder) { - //Return the abstract db from the db command type - return new MsSqlDb(); + return provider.CommandGenerator; } - else if (connection is SqliteConnection) + else if (string.Equals(provider.ProviderName, "sqlserver", StringComparison.OrdinalIgnoreCase)) { - return new SqlLiteDb(); + return new MsSqlDb(); } - else if (connection is MySqlConnection) + else if (string.Equals(provider.ProviderName, "mysql", StringComparison.OrdinalIgnoreCase)) { return new MySqlDb(); } + else if (string.Equals(provider.ProviderName, "sqlite", StringComparison.OrdinalIgnoreCase)) + { + return new SqlLiteDb(); + } else { throw new NotSupportedException("This library does not support the abstract databse backend"); @@ -516,5 +413,107 @@ namespace VNLib.Plugins.Extensions.Loading.Sql //Update the table primary keys now that this col has been added col.Table.PrimaryKey = cols.Distinct().ToArray(); } + + internal sealed class DbProvider(object instance, IConfigScope sqlConfig) + { + public delegate Task<Func<DbConnection>> AsynConBuilderDelegate(IConfigScope sqlConf); + public delegate Func<DbConnection> SyncConBuilderDelegate(IConfigScope sqlConf); + public delegate DbContextOptions SyncOptBuilderDelegate(IConfigScope sqlConf); + public delegate Task<DbContextOptions> AsynOptBuilderDelegate(IConfigScope sqlConf); + public delegate void BuildTableStringDelegate(StringBuilder builder, DataTable table); + public delegate string ProviderNameDelegate(); + + + public object Provider { get; } = instance; + + public IConfigScope SqlConfig { get; } = sqlConfig; + + /// <summary> + /// A lazy async connection factory. When called, may cause invocation in the external library, + /// but only once. + /// </summary> + public readonly Lazy<Task<Func<DbConnection>>> ConnectionFactory = new(() => GetConnections(instance, sqlConfig)); + + /// <summary> + /// A lazy async options factory. When called, may cause invocation in the external library, + /// but only once. + /// </summary> + public readonly Lazy<Task<DbContextOptions>> OptionsFactory = new(() => GetOptions(instance, sqlConfig)); + + /// <summary> + /// Gets the extern command generator for the external library + /// </summary> + public readonly IDBCommandGenerator CommandGenerator = new ExternCommandGenerator(instance); + + /// <summary> + /// Gets the provider name from the external library + /// </summary> + public readonly ProviderNameDelegate ProviderNameFunc = ManagedLibrary.GetMethod<ProviderNameDelegate>(instance, "GetProviderName"); + + /// <summary> + /// Gets a value indicating if the external library has a command builder + /// </summary> + public bool HasCommandBuilder => (CommandGenerator as ExternCommandGenerator)!.BuildTableString is not null; + + /// <summary> + /// Gets the provider name from the external library + /// </summary> + public string ProviderName => ProviderNameFunc.Invoke(); + + /* + * Methods below are designed to be called within a lazy/defered context and possible awaited + * by mutliple threads. This causes data to be only loaded once, and then cached for future calls. + */ + + private static Task<Func<DbConnection>> GetConnections(object instance, IConfigScope sqlConfig) + { + //Connection builder functions + SyncConBuilderDelegate? SyncBuilder = ManagedLibrary.TryGetMethod<SyncConBuilderDelegate>(instance, "GetDbConnection"); + + //try sync first + if (SyncBuilder is not null) + { + return Task.FromResult(SyncBuilder.Invoke(sqlConfig)); + } + + //If no sync function force call async, but try to schedule it on a new thread + AsynConBuilderDelegate? AsynConnectionBuilder = ManagedLibrary.GetMethod<AsynConBuilderDelegate>(instance, "GetDbConnectionAsync"); + return Task.Run(() => AsynConnectionBuilder.Invoke(sqlConfig)); + } + + private static Task<DbContextOptions> GetOptions(object instance, IConfigScope sqlConfig) + { + //Options builder functions + SyncOptBuilderDelegate? SyncBuilder = ManagedLibrary.TryGetMethod<SyncOptBuilderDelegate>(instance, "GetDbOptions"); + + //try sync first + if (SyncBuilder is not null) + { + return Task.FromResult(SyncBuilder.Invoke(sqlConfig)); + } + + //If no sync function force call async, but try to schedule it on a new thread + AsynOptBuilderDelegate? AsynOptionsBuilder = ManagedLibrary.GetMethod<AsynOptBuilderDelegate>(instance, "GetDbOptionsAsync"); + return Task.Run(() => AsynOptionsBuilder.Invoke(sqlConfig)); + } + + private sealed class ExternCommandGenerator(object instance) : IDBCommandGenerator + { + public BuildTableStringDelegate? BuildTableString = ManagedLibrary.TryGetMethod<BuildTableStringDelegate>(instance, "BuildCreateStatment"); + + + public void BuildCreateStatment(StringBuilder builder, DataTable table) + { + if(BuildTableString is not null) + { + BuildTableString.Invoke(builder, table); + } + else + { + throw new NotSupportedException("The external library does not support table creation"); + } + } + } + } } } diff --git a/lib/VNLib.Plugins.Extensions.Loading.Sql/src/VNLib.Plugins.Extensions.Loading.Sql.csproj b/lib/VNLib.Plugins.Extensions.Loading.Sql/src/VNLib.Plugins.Extensions.Loading.Sql.csproj index d923527..f505e4c 100644 --- a/lib/VNLib.Plugins.Extensions.Loading.Sql/src/VNLib.Plugins.Extensions.Loading.Sql.csproj +++ b/lib/VNLib.Plugins.Extensions.Loading.Sql/src/VNLib.Plugins.Extensions.Loading.Sql.csproj @@ -38,10 +38,7 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.0" /> - <PackageReference Include="MySqlConnector" Version="2.3.3" /> - <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="7.0.0" /> + <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.1" /> </ItemGroup> <ItemGroup> diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs b/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs index bbd6c10..0337fbd 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Extensions.Loading @@ -198,12 +198,12 @@ namespace VNLib.Plugins.Extensions.Loading public static T GetRequiredProperty<T>(this IConfigScope config, string property, Func<JsonElement, T> getter) { //Check null - _ = config ?? throw new ArgumentNullException(nameof(config)); - _ = property ?? throw new ArgumentNullException(nameof(property)); - _ = getter ?? throw new ArgumentNullException(nameof(getter)); + ArgumentNullException.ThrowIfNull(config); + ArgumentNullException.ThrowIfNull(property); + ArgumentNullException.ThrowIfNull(getter); //Get the property - if(!config.TryGetValue(property, out JsonElement el)) + if (!config.TryGetValue(property, out JsonElement el)) { throw new KeyNotFoundException($"Missing required configuration property '{property}' in config {config.ScopeName}"); } @@ -223,12 +223,13 @@ namespace VNLib.Plugins.Extensions.Loading /// <param name="getter">The function used to set the desired value from the config element</param> /// <param name="value">The output value to set</param> /// <returns>A value that indicates if the property was found</returns> + /// <exception cref="ArgumentNullException"></exception> public static bool TryGetProperty<T>(this IConfigScope config, string property, Func<JsonElement, T> getter, out T? value) { //Check null - ArgumentNullException.ThrowIfNull(config, nameof(config)); - ArgumentNullException.ThrowIfNull(property, nameof(property)); - ArgumentNullException.ThrowIfNull(getter, nameof(getter)); + ArgumentNullException.ThrowIfNull(config); + ArgumentNullException.ThrowIfNull(property); + ArgumentNullException.ThrowIfNull(getter); //Get the property if (config.TryGetValue(property, out JsonElement el)) @@ -242,6 +243,24 @@ namespace VNLib.Plugins.Extensions.Loading } /// <summary> + /// Gets a configuration property from the specified configuration scope + /// and invokes your callback function on the element if found to transform the + /// output value, or returns the default value if the property is not found. + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="config"></param> + /// <param name="property">The name of the configuration element to get</param> + /// <param name="getter">The function used to set the desired value from the config element</param> + /// <param name="defaultValue">The default value to return</param> + /// <returns>The property value returned from your getter callback, or the default value if not found</returns> + /// <exception cref="ArgumentNullException"></exception> + [return:NotNullIfNotNull(nameof(defaultValue))] + public static T? GetValueOrDefault<T>(this IConfigScope config, string property, Func<JsonElement, T> getter, T defaultValue) + { + return TryGetProperty(config, property, getter, out T? value) ? value : defaultValue; + } + + /// <summary> /// Gets the configuration property name for the type /// </summary> /// <param name="type">The type to get the configuration name for</param> diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs b/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs index 470d954..3538337 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Extensions.Loading @@ -30,7 +30,6 @@ using System.Reflection; using System.Runtime.Loader; using System.Threading.Tasks; using System.Collections.Generic; -using System.ComponentModel.Design; using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; @@ -74,19 +73,9 @@ namespace VNLib.Plugins.Extensions.Loading /// <returns>The cached or newly created singleton</returns> public static object GetOrCreateSingleton(PluginBase plugin, Type serviceType, Func<PluginBase, object> serviceFactory) { - Lazy<object>? service; //Get local cache PluginLocalCache pc = _localCache.GetValue(plugin, PluginLocalCache.Create); - //Hold lock while get/set the singleton - lock (pc.SyncRoot) - { - //Check if service already exists - service = pc.GetService(serviceType); - //publish the service if it isnt loaded yet - service ??= pc.AddService(serviceType, serviceFactory); - } - //Deferred load of the service - return service.Value; + return pc.GetOrCreateService(serviceType, serviceFactory); } /// <summary> @@ -113,7 +102,7 @@ namespace VNLib.Plugins.Extensions.Loading public static string? GetAssetFilePath(this PluginBase plugin, string assemblyName, SearchOption searchOption) { plugin.ThrowIfUnloaded(); - _ = assemblyName ?? throw new ArgumentNullException(nameof(assemblyName)); + ArgumentNullException.ThrowIfNull(assemblyName); /* * Allow an assets directory to limit the scope of the search for the desired @@ -127,7 +116,7 @@ namespace VNLib.Plugins.Extensions.Loading * This should never happen since this method can only be called from a * plugin context, which means this path was used to load the current plugin */ - _ = assetDir ?? throw new ArgumentNullException(ConfigurationExtensions.PLUGIN_ASSET_KEY, "No plugin path is defined for the current host configuration, this is likely a bug"); + ArgumentNullException.ThrowIfNull(assetDir, "No plugin asset directory is defined for the current host configuration, this is likely a bug"); //Get the first file that matches the search file return Directory.EnumerateFiles(assetDir, assemblyName, searchOption).FirstOrDefault(); @@ -274,10 +263,7 @@ namespace VNLib.Plugins.Extensions.Loading public static void ThrowIfUnloaded(this PluginBase plugin) { //See if the plugin was unlaoded - if (plugin.UnloadToken.IsCancellationRequested) - { - throw new ObjectDisposedException("The plugin has been unloaded"); - } + ObjectDisposedException.ThrowIf(plugin.UnloadToken.IsCancellationRequested, plugin); } /// <summary> @@ -356,7 +342,7 @@ namespace VNLib.Plugins.Extensions.Loading { //Test status plugin.ThrowIfUnloaded(); - _ = callback ?? throw new ArgumentNullException(nameof(callback)); + ArgumentNullException.ThrowIfNull(callback); //Wait method static async Task WaitForUnload(PluginBase pb, Action callback) @@ -637,8 +623,8 @@ namespace VNLib.Plugins.Extensions.Loading /// <exception cref="ConcreteTypeAmbiguousMatchException"></exception> public static object CreateService(this PluginBase plugin, Type serviceType, IConfigScope? config) { - _ = plugin ?? throw new ArgumentNullException(nameof(plugin)); - _ = serviceType ?? throw new ArgumentNullException(nameof(serviceType)); + ArgumentNullException.ThrowIfNull(plugin); + ArgumentNullException.ThrowIfNull(serviceType); plugin.ThrowIfUnloaded(); @@ -752,11 +738,8 @@ namespace VNLib.Plugins.Extensions.Loading private sealed class PluginLocalCache { private readonly PluginBase _plugin; - private readonly Dictionary<Type, Lazy<object>> _store; - public object SyncRoot { get; } = new(); - private PluginLocalCache(PluginBase plugin) { _plugin = plugin; @@ -767,23 +750,34 @@ namespace VNLib.Plugins.Extensions.Loading public static PluginLocalCache Create(PluginBase plugin) => new(plugin); + /* + * Service code should not be executed in multiple threads, so no need to lock + * + * However if a service is added because it does not exist, the second call to + * get service, will invoke the creation callback. Which may be "recursive" + * as child dependencies required more services. + */ - public Lazy<object>? GetService(Type serviceType) + public object GetOrCreateService(Type serviceType, Func<PluginBase, object> ctor) { - Lazy<object>? t = _store.Where(t => t.Key.IsAssignableTo(serviceType)) - .Select(static tk => tk.Value) - .FirstOrDefault(); - return t; - } + Lazy<object>? lazyService; - public Lazy<object> AddService(Type serviceType, Func<PluginBase, object> factory) - { - //Get lazy loader to invoke factory outside of cache lock - Lazy<object> lazyFactory = new(() => factory(_plugin), true); - //Store lazy factory - _store.Add(serviceType, lazyFactory); - //Pass the lazy factory back - return lazyFactory; + lock (_store) + { + lazyService = _store.Where(t => t.Key.IsAssignableTo(serviceType)) + .Select(static tk => tk.Value) + .FirstOrDefault(); + + if (lazyService is null) + { + lazyService = new Lazy<object>(() => ctor(_plugin)); + //add to pool + _store.Add(serviceType, lazyService); + } + } + + //Return the service instance + return lazyService.Value; } } } diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Users/UserManager.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Users/UserManager.cs index 3e9dbde..6374b18 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/Users/UserManager.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Users/UserManager.cs @@ -99,9 +99,9 @@ namespace VNLib.Plugins.Extensions.Loading.Users } ///<inheritdoc/> - public Task<IUser?> GetUserFromEmailAsync(string emailAddress, CancellationToken cancellationToken = default) + public Task<IUser?> GetUserFromUsernameAsync(string emailAddress, CancellationToken cancellationToken = default) { - return _dynamicLoader.GetUserFromEmailAsync(emailAddress, cancellationToken); + return _dynamicLoader.GetUserFromUsernameAsync(emailAddress, cancellationToken); } ///<inheritdoc/> diff --git a/lib/sql-providers/mysql/VNLib.Plugins.Extensions.Loading.Sql.MySql/README.md b/lib/sql-providers/mysql/VNLib.Plugins.Extensions.Loading.Sql.MySql/README.md new file mode 100644 index 0000000..2823281 --- /dev/null +++ b/lib/sql-providers/mysql/VNLib.Plugins.Extensions.Loading.Sql.MySql/README.md @@ -0,0 +1,17 @@ +# VNLib.Plugins.Extensions.Sql.MySql +*A runtime asset library that provides access to MySql database features for plugins that are configured to load the provider* + +**This library contains 3rd-party dependencies** + +## Builds +Debug build w/ symbols & xml docs, release builds, NuGet packages, and individually packaged source code are available on my website (link below). + +## Docs and Guides +Documentation, specifications, and setup guides are available on my website. + +[Docs and Articles](https://www.vaughnnugent.com/resources/software/articles?tags=docs,_vnlib.plugins.Extensions.Sql.MySql) +[Builds and Source](https://www.vaughnnugent.com/resources/software/modules/VNLib.Plugins.Extensions) +[Nuget Feeds](https://www.vaughnnugent.com/resources/software/modules) + +## License +Source files in for this project are licensed to you under the GNU Affero General Public License (or any later version). See the LICENSE files for more information.
\ No newline at end of file diff --git a/lib/sql-providers/mysql/VNLib.Plugins.Extensions.Loading.Sql.MySql/build.readme.md b/lib/sql-providers/mysql/VNLib.Plugins.Extensions.Loading.Sql.MySql/build.readme.md new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/lib/sql-providers/mysql/VNLib.Plugins.Extensions.Loading.Sql.MySql/build.readme.md diff --git a/lib/sql-providers/mysql/VNLib.Plugins.Extensions.Loading.Sql.MySql/src/MYSqlExport.cs b/lib/sql-providers/mysql/VNLib.Plugins.Extensions.Loading.Sql.MySql/src/MYSqlExport.cs new file mode 100644 index 0000000..26fd3a3 --- /dev/null +++ b/lib/sql-providers/mysql/VNLib.Plugins.Extensions.Loading.Sql.MySql/src/MYSqlExport.cs @@ -0,0 +1,138 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Loading.Sql.Mysql +* File: MySqlExport.cs +* +* MySqlExport.cs is part of VNLib.Plugins.Extensions.Loading.Sql which +* is part of the larger VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Extensions.Loading.Sql 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.Loading.Sql 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.Text.Json; +using System.Data.Common; +using System.Threading.Tasks; + +using Microsoft.EntityFrameworkCore; + +using MySql.Data.MySqlClient; + +using VNLib.Utils.Logging; +using VNLib.Plugins.Extensions.Loading; + +namespace VNLib.Plugins.Extensions.Sql +{ + + [ServiceExport] + [ConfigurationName("sql", Required = true)] + public sealed class MySqlExport(PluginBase plugin, IConfigScope config) + { + private async Task<string> BuildConnStringAsync() + { + //See if the user suggested a raw connection string + if (config.TryGetProperty("connection_string", ps => ps.GetString(), out string? conString)) + { + return conString!; + } + else if (config.TryGetValue("json", out JsonElement value)) + { + JsonSerializerOptions opt = new(JsonSerializerDefaults.General) + { + AllowTrailingCommas = true, + IgnoreReadOnlyFields = true, + DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower, + }; + + MySqlConnectionStringBuilder b = value.Deserialize<MySqlConnectionStringBuilder>(opt)!; + + //Get the password from the secret manager + using ISecretResult? secret = await plugin.TryGetSecretAsync("db_password"); + + b.Password = secret?.Result.ToString(); + return b.ConnectionString; + } + else + { + //Get the password from the secret manager + using ISecretResult? secret = await plugin.TryGetSecretAsync("db_password"); + + // Build connection strin + return new MySqlConnectionStringBuilder() + { + Server = config["hostname"].GetString(), + Database = config["catalog"].GetString(), + UserID = config["username"].GetString(), + Pooling = true, + MinimumPoolSize = config.GetValueOrDefault("min_pool_size", p => p.GetUInt32(), 10u), + MaximumPoolSize = config.GetValueOrDefault("max_pool_size", p => p.GetUInt32(), 50u), + AllowBatch = config.GetValueOrDefault("allow_batch", p => p.GetBoolean(), true), + ConnectionLifeTime = config.GetValueOrDefault("connection_lifetime", p => p.GetUInt32(), 0u), + ConnectionTimeout = config.GetValueOrDefault("connection_timeout", p => p.GetUInt32(), 15u), + Port = config.GetValueOrDefault("port", p => p.GetUInt32(), 3306u), + PipeName = config.GetValueOrDefault("pipe_name", p => p.GetString(), null), + AllowLoadLocalInfile = config.GetValueOrDefault("allow_load_local_infile", p => p.GetBoolean(), false), + AllowLoadLocalInfileInPath = config.GetValueOrDefault("allow_load_local_infile_in_path", p => p.GetString(), null), + + Password = secret?.Result.ToString(), + } + .ConnectionString; + } + } + + /* + * NOTICE: + * Function names must be public and must match the SqlConnectionLoader delegate names. + * + * GetDbConnection - A sync or async function that takes a configuration scope and + * returns a DbConnection factory + * + * GetDbOptions - A sync or async function that takes a configuration scope and + * returns a DbConnectionOptions instance + * + * GetProviderName - Returns a string that is the provider name for the connection + */ + + public string GetProviderName() => "mysql"; + + public async Task<Func<DbConnection>> GetDbConnectionAsync(IConfigScope sqlConfig) + { + //Store local copy of the connection string, probably not the best idea because of the password, but best for now + string connString = await BuildConnStringAsync(); + + return () => new MySqlConnection(connString); + } + + public async Task<DbContextOptions> GetDbOptionsAsync(IConfigScope sqlConfig) + { + //Get the connection string from the configuration + string connString = await BuildConnStringAsync(); + + //Build the options using the mysql extension method + DbContextOptionsBuilder b = new(); + b.UseMySQL(connString); + + //Write debug loggin to the debug log if the user has it enabled or the plugin is in debug mode + if (sqlConfig.GetValueOrDefault("debug", p => p.GetBoolean(), false) || plugin.IsDebug()) + { + //Write the SQL to the debug log + b.LogTo((v) => plugin.Log.Debug("MySql: {v}", v)); + } + + return b.Options; + } + } +} diff --git a/lib/sql-providers/mysql/VNLib.Plugins.Extensions.Loading.Sql.MySql/src/VNLib.Plugins.Extensions.Loading.Sql.MYSql.csproj b/lib/sql-providers/mysql/VNLib.Plugins.Extensions.Loading.Sql.MySql/src/VNLib.Plugins.Extensions.Loading.Sql.MYSql.csproj new file mode 100644 index 0000000..dcb4a5c --- /dev/null +++ b/lib/sql-providers/mysql/VNLib.Plugins.Extensions.Loading.Sql.MySql/src/VNLib.Plugins.Extensions.Loading.Sql.MYSql.csproj @@ -0,0 +1,59 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net8.0</TargetFramework> + <AssemblyName>VNLib.Plugins.Extensions.Sql.MySql</AssemblyName> + <RootNamespace>VNLib.Plugins.Extensions.Sql</RootNamespace> + <Nullable>enable</Nullable> + <ImplicitUsings>disable</ImplicitUsings> + <GenerateDocumentationFile>True</GenerateDocumentationFile> + <!--Enable dynamic loading--> + <EnableDynamicLoading>true</EnableDynamicLoading> + </PropertyGroup> + + <PropertyGroup> + <PackageId>VNLib.Plugins.Extensions.Sql.MySql</PackageId> + <Authors>Vaughn Nugent</Authors> + <Company>Vaughn Nugent</Company> + <Product>VNLib.Plugins.Extensions.Sql.MySql</Product> + <Copyright>Copyright © 2024 Vaughn Nugent</Copyright> + <PackageProjectUrl>https://www.vaughnnugent.com/resources/software/modules/VNLib.Plugins.Extensions</PackageProjectUrl> + <RepositoryUrl>https://github.com/VnUgE/VNLib.Plugins.Extensions/tree/master/lib/sql-providers/VNLib.Plugins.Extensions.Sql.MySql</RepositoryUrl> + <Description>A runtime asset library that provides MySql interfaces for ADO and EFCore SQL server clients</Description> + <PackageReadmeFile>README.md</PackageReadmeFile> + <PackageLicenseFile>LICENSE</PackageLicenseFile> + </PropertyGroup> + + <ItemGroup> + <None Include="../../../../../LICENSE"> + <Pack>True</Pack> + <PackagePath>\</PackagePath> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Include="..\README.md"> + <Pack>True</Pack> + <PackagePath>\</PackagePath> + </None> + </ItemGroup> + + <ItemGroup> + <PackageReference Include="ErrorProne.NET.CoreAnalyzers" Version="0.1.2"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="ErrorProne.NET.Structs" Version="0.1.2"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="MySql.EntityFrameworkCore" Version="8.0.0" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\..\..\..\VNLib.Plugins.Extensions.Loading\src\VNLib.Plugins.Extensions.Loading.csproj" /> + </ItemGroup> + + <Target Condition="'$(BuildingInsideVisualStudio)' == true" Name="PostBuild" AfterTargets="PostBuildEvent"> + <Exec Command="start xcopy "$(TargetDir)" "$(SolutionDir)devplugins\RuntimeAssets\$(TargetName)" /E /Y /R" /> + </Target> + +</Project> diff --git a/lib/sql-providers/sqlite/VNLib.Plugins.Extensions.Loading.Sql.SQLite/README.md b/lib/sql-providers/sqlite/VNLib.Plugins.Extensions.Loading.Sql.SQLite/README.md new file mode 100644 index 0000000..abf236f --- /dev/null +++ b/lib/sql-providers/sqlite/VNLib.Plugins.Extensions.Loading.Sql.SQLite/README.md @@ -0,0 +1,17 @@ +# VNLib.Plugins.Extensions.Sql.SQLite +*A runtime asset library that provides access to SQLite database features for plugins that are configured to load the provider* + +**This library contains 3rd-party dependencies** + +## Builds +Debug build w/ symbols & xml docs, release builds, NuGet packages, and individually packaged source code are available on my website (link below). + +## Docs and Guides +Documentation, specifications, and setup guides are available on my website. + +[Docs and Articles](https://www.vaughnnugent.com/resources/software/articles?tags=docs,_vnlib.plugins.Extensions.Sql.SQLite) +[Builds and Source](https://www.vaughnnugent.com/resources/software/modules/VNLib.Plugins.Extensions) +[Nuget Feeds](https://www.vaughnnugent.com/resources/software/modules) + +## License +Source files in for this project are licensed to you under the GNU Affero General Public License (or any later version). See the LICENSE files for more information.
\ No newline at end of file diff --git a/lib/sql-providers/sqlite/VNLib.Plugins.Extensions.Loading.Sql.SQLite/build.readme.md b/lib/sql-providers/sqlite/VNLib.Plugins.Extensions.Loading.Sql.SQLite/build.readme.md new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/lib/sql-providers/sqlite/VNLib.Plugins.Extensions.Loading.Sql.SQLite/build.readme.md diff --git a/lib/sql-providers/sqlite/VNLib.Plugins.Extensions.Loading.Sql.SQLite/src/SQLiteExport.cs b/lib/sql-providers/sqlite/VNLib.Plugins.Extensions.Loading.Sql.SQLite/src/SQLiteExport.cs new file mode 100644 index 0000000..6f9455a --- /dev/null +++ b/lib/sql-providers/sqlite/VNLib.Plugins.Extensions.Loading.Sql.SQLite/src/SQLiteExport.cs @@ -0,0 +1,131 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Loading.Sql.SQLite +* File: SQLiteExport.cs +* +* SQLiteExport.cs is part of VNLib.Plugins.Extensions.Loading.Sql.SQLite which +* is part of the larger VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Extensions.Loading.Sql 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.Loading.Sql 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.Text.Json; +using System.Data.Common; +using System.Threading.Tasks; + +using Microsoft.EntityFrameworkCore; + +using Microsoft.Data.Sqlite; + +using VNLib.Utils.Logging; +using VNLib.Plugins.Extensions.Loading; + +namespace VNLib.Plugins.Extensions.Sql +{ + + [ServiceExport] + [ConfigurationName("sql", Required = true)] + public sealed class SQLiteExport(PluginBase plugin, IConfigScope config) + { + + private async Task<string> BuildConnStringAsync() + { + //See if the user suggested a raw connection string + if (config.TryGetProperty("connection_string", ps => ps.GetString(), out string? conString)) + { + return conString!; + } + else if (config.TryGetValue("json", out JsonElement value)) + { + JsonSerializerOptions opt = new(JsonSerializerDefaults.General) + { + AllowTrailingCommas = true, + IgnoreReadOnlyFields = true, + DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower, + }; + + SqliteConnectionStringBuilder b = value.Deserialize<SqliteConnectionStringBuilder>(opt)!; + + //Get the password from the secret manager + using ISecretResult? secret = await plugin.TryGetSecretAsync("db_password"); + + b.Password = secret?.Result.ToString(); + return b.ConnectionString; + } + else + { + //Get the password from the secret manager + using ISecretResult? secret = await plugin.TryGetSecretAsync("db_password"); + + // Build connection strin + return new SqliteConnectionStringBuilder() + { + DataSource = config["source"].GetString(), + Pooling = true, + Cache = SqliteCacheMode.Default, + RecursiveTriggers = config.GetValueOrDefault("recursive_triggers", p => p.GetBoolean(), false), + DefaultTimeout = config.GetValueOrDefault("timeout", p => p.GetInt32(), 30), + Mode = config.GetValueOrDefault("mode", p => (SqliteOpenMode)p.GetInt32(), SqliteOpenMode.ReadWriteCreate), + + Password = secret?.Result.ToString(), + } + .ConnectionString; + } + } + + /* + * NOTICE: + * Function names must be public and must match the SqlConnectionLoader delegate names. + * + * GetDbConnection - A sync or async function that takes a configuration scope and + * returns a DbConnection factory + * + * GetDbOptions - A sync or async function that takes a configuration scope and + * returns a DbConnectionOptions instance + * + * GetProviderName - Returns a string that is the provider name for the connection + */ + + public string GetProviderName() => "sqlite"; //Use default handler for sqlite db creation + + public async Task<Func<DbConnection>> GetDbConnectionAsync(IConfigScope sqlConfig) + { + //Store local copy of the connection string, probably not the best idea because of the password, but best for now + string connString = await BuildConnStringAsync(); + + return () => new SqliteConnection(connString); + } + + public async Task<DbContextOptions> GetDbOptionsAsync(IConfigScope sqlConfig) + { + //Get the connection string from the configuration + string connString = await BuildConnStringAsync(); + + DbContextOptionsBuilder b = new(); + b.UseSqlite(connString); + + //Write debug loggin to the debug log if the user has it enabled or the plugin is in debug mode + if (sqlConfig.GetValueOrDefault("debug", p => p.GetBoolean(), false) || plugin.IsDebug()) + { + //Write the SQL to the debug log + b.LogTo((v) => plugin.Log.Debug("SQLite: {v}", v)); + } + + return b.Options; + } + } +} diff --git a/lib/sql-providers/sqlite/VNLib.Plugins.Extensions.Loading.Sql.SQLite/src/VNLib.Plugins.Extensions.Loading.Sql.SQLite.csproj b/lib/sql-providers/sqlite/VNLib.Plugins.Extensions.Loading.Sql.SQLite/src/VNLib.Plugins.Extensions.Loading.Sql.SQLite.csproj new file mode 100644 index 0000000..b61f523 --- /dev/null +++ b/lib/sql-providers/sqlite/VNLib.Plugins.Extensions.Loading.Sql.SQLite/src/VNLib.Plugins.Extensions.Loading.Sql.SQLite.csproj @@ -0,0 +1,59 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net8.0</TargetFramework> + <AssemblyName>VNLib.Plugins.Extensions.Sql.SQLite</AssemblyName> + <RootNamespace>VNLib.Plugins.Extensions.Sql</RootNamespace> + <Nullable>enable</Nullable> + <ImplicitUsings>disable</ImplicitUsings> + <GenerateDocumentationFile>True</GenerateDocumentationFile> + <!--Enable dynamic loading--> + <EnableDynamicLoading>true</EnableDynamicLoading> + </PropertyGroup> + + <PropertyGroup> + <PackageId>VNLib.Plugins.Extensions.Sql.SQLite</PackageId> + <Authors>Vaughn Nugent</Authors> + <Company>Vaughn Nugent</Company> + <Product>VNLib.Plugins.Extensions.Sql.SQLite</Product> + <Copyright>Copyright © 2024 Vaughn Nugent</Copyright> + <PackageProjectUrl>https://www.vaughnnugent.com/resources/software/modules/VNLib.Plugins.Extensions</PackageProjectUrl> + <RepositoryUrl>https://github.com/VnUgE/VNLib.Plugins.Extensions/tree/master/lib/sql-providers/VNLib.Plugins.Extensions.Sql.SQLite</RepositoryUrl> + <Description>A runtime asset library that provides SQLite interfaces for ADO and EFCore SQL server clients</Description> + <PackageReadmeFile>README.md</PackageReadmeFile> + <PackageLicenseFile>LICENSE</PackageLicenseFile> + </PropertyGroup> + + <ItemGroup> + <None Include="../../../../../LICENSE"> + <Pack>True</Pack> + <PackagePath>\</PackagePath> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Include="..\README.md"> + <Pack>True</Pack> + <PackagePath>\</PackagePath> + </None> + </ItemGroup> + + <ItemGroup> + <PackageReference Include="ErrorProne.NET.CoreAnalyzers" Version="0.1.2"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="ErrorProne.NET.Structs" Version="0.1.2"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.1" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\..\..\..\VNLib.Plugins.Extensions.Loading\src\VNLib.Plugins.Extensions.Loading.csproj" /> + </ItemGroup> + + <Target Condition="'$(BuildingInsideVisualStudio)' == true" Name="PostBuild" AfterTargets="PostBuildEvent"> + <Exec Command="start xcopy "$(TargetDir)" "$(SolutionDir)devplugins\RuntimeAssets\$(TargetName)" /E /Y /R" /> + </Target> + +</Project> diff --git a/lib/sql-providers/sqlserver/VNLib.Plugins.Extensions.Loading.Sql.SQLServer/README.md b/lib/sql-providers/sqlserver/VNLib.Plugins.Extensions.Loading.Sql.SQLServer/README.md new file mode 100644 index 0000000..89662e7 --- /dev/null +++ b/lib/sql-providers/sqlserver/VNLib.Plugins.Extensions.Loading.Sql.SQLServer/README.md @@ -0,0 +1,17 @@ +# VNLib.Plugins.Extensions.Sql.SqlServer +*A runtime asset library that provides access to SqlServer database features for plugins that are configured to load the provider* + +**This library contains 3rd-party dependencies** + +## Builds +Debug build w/ symbols & xml docs, release builds, NuGet packages, and individually packaged source code are available on my website (link below). + +## Docs and Guides +Documentation, specifications, and setup guides are available on my website. + +[Docs and Articles](https://www.vaughnnugent.com/resources/software/articles?tags=docs,_vnlib.plugins.Extensions.Sql.SqlServer) +[Builds and Source](https://www.vaughnnugent.com/resources/software/modules/VNLib.Plugins.Extensions) +[Nuget Feeds](https://www.vaughnnugent.com/resources/software/modules) + +## License +Source files in for this project are licensed to you under the GNU Affero General Public License (or any later version). See the LICENSE files for more information.
\ No newline at end of file diff --git a/lib/sql-providers/sqlserver/VNLib.Plugins.Extensions.Loading.Sql.SQLServer/build.readme.md b/lib/sql-providers/sqlserver/VNLib.Plugins.Extensions.Loading.Sql.SQLServer/build.readme.md new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/lib/sql-providers/sqlserver/VNLib.Plugins.Extensions.Loading.Sql.SQLServer/build.readme.md diff --git a/lib/sql-providers/sqlserver/VNLib.Plugins.Extensions.Loading.Sql.SQLServer/src/SqlServerExport.cs b/lib/sql-providers/sqlserver/VNLib.Plugins.Extensions.Loading.Sql.SQLServer/src/SqlServerExport.cs new file mode 100644 index 0000000..71f16bf --- /dev/null +++ b/lib/sql-providers/sqlserver/VNLib.Plugins.Extensions.Loading.Sql.SQLServer/src/SqlServerExport.cs @@ -0,0 +1,149 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Loading.Sql.SQLServer +* File: SQLServerExport.cs +* +* SQLServerExport.cs is part of VNLib.Plugins.Extensions.Loading.Sql.SQLServer which +* is part of the larger VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Extensions.Loading.Sql 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.Loading.Sql 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.Text.Json; +using System.Data.Common; +using System.Threading.Tasks; + +using Microsoft.Data.SqlClient; + +using Microsoft.EntityFrameworkCore; + +using VNLib.Utils.Logging; +using VNLib.Plugins.Extensions.Loading; + +namespace VNLib.Plugins.Extensions.Sql +{ + + [ServiceExport] + [ConfigurationName("sql", Required = true)] + public sealed class SqlServerExport(PluginBase plugin, IConfigScope config) + { + private async Task<string> BuildConnStringAsync() + { + //See if the user suggested a raw connection string + if (config.TryGetProperty("connection_string", ps => ps.GetString(), out string? conString)) + { + return conString!; + } + else if (config.TryGetValue("json", out JsonElement value)) + { + JsonSerializerOptions opt = new(JsonSerializerDefaults.General) + { + AllowTrailingCommas = true, + IgnoreReadOnlyFields = true, + DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower, + }; + + SqlConnectionStringBuilder b = value.Deserialize<SqlConnectionStringBuilder>(opt)!; + + //Get the password from the secret manager + using ISecretResult? secret = await plugin.TryGetSecretAsync("db_password"); + + b.Password = secret?.Result.ToString(); + return b.ConnectionString; + } + else + { + //Get the password from the secret manager + using ISecretResult? secret = await plugin.TryGetSecretAsync("db_password"); + + // Build connection string + return new SqlConnectionStringBuilder() + { + DataSource = config["hostname"].GetString(), + InitialCatalog = config["catalog"].GetString(), + UserID = config["username"].GetString(), + Pooling = true, + + + ApplicationName = config.GetValueOrDefault("application_name", p => p.GetString(), string.Empty), + HostNameInCertificate = config.GetValueOrDefault("hostname_in_certificate", p => p.GetString(), string.Empty), + PacketSize = config.GetValueOrDefault("packet_size", p => p.GetInt32(), 8000), + Encrypt = config.GetValueOrDefault("encrypted", p => p.GetBoolean(), false), + IntegratedSecurity = config.GetValueOrDefault("integrated_security", p => p.GetBoolean(), false), + MultipleActiveResultSets = config.GetValueOrDefault("multiple_active_result_sets", p => p.GetBoolean(), false), + ConnectTimeout = config.GetValueOrDefault("connect_timeout", p => p.GetInt32(), 15), + LoadBalanceTimeout = config.GetValueOrDefault("load_balance_timeout", p => p.GetInt32(), 0), + MaxPoolSize = config.GetValueOrDefault("max_pool_size", p => p.GetInt32(), 100), + MinPoolSize = config.GetValueOrDefault("min_pool_size", p => p.GetInt32(), 0), + TransactionBinding = config.GetValueOrDefault("transaction_binding", p => p.GetString(), "Implicit Unbind"), + TypeSystemVersion = config.GetValueOrDefault("type_system_version", p => p.GetString(), "Latest"), + WorkstationID = config.GetValueOrDefault("workstation_id", p => p.GetString(), string.Empty), + CurrentLanguage = config.GetValueOrDefault("current_language", p => p.GetString(), "us_english"), + PersistSecurityInfo = config.GetValueOrDefault("persist_security_info", p => p.GetBoolean(), false), + Replication = config.GetValueOrDefault("replication", p => p.GetBoolean(), false), + TrustServerCertificate = config.GetValueOrDefault("trust_server_certificate", p => p.GetBoolean(), false), + UserInstance = config.GetValueOrDefault("user_instance", p => p.GetBoolean(), false), + + Password = secret?.Result.ToString(), + } + .ConnectionString; + } + } + + /* + * NOTICE: + * Function names must be public and must match the SqlConnectionLoader delegate names. + * + * GetDbConnection - A sync or async function that takes a configuration scope and + * returns a DbConnection factory + * + * GetDbOptions - A sync or async function that takes a configuration scope and + * returns a DbConnectionOptions instance + * + * GetProviderName - Returns a string that is the provider name for the connection + */ + + public string GetProviderName() => "sqlserver"; + + public async Task<Func<DbConnection>> GetDbConnectionAsync(IConfigScope sqlConfig) + { + //Store local copy of the connection string, probably not the best idea because of the password, but best for now + string connString = await BuildConnStringAsync(); + + return () => new SqlConnection(connString); + } + + public async Task<DbContextOptions> GetDbOptionsAsync(IConfigScope sqlConfig) + { + //Get the connection string from the configuration + string connString = await BuildConnStringAsync(); + + //Build the options using the mysql extension method + DbContextOptionsBuilder b = new(); + b.UseSqlServer(connString); + + //Write debug loggin to the debug log if the user has it enabled or the plugin is in debug mode + if (sqlConfig.GetValueOrDefault("debug", p => p.GetBoolean(), false) || plugin.IsDebug()) + { + //Write the SQL to the debug log + b.LogTo((v) => plugin.Log.Debug("SqlServer: {v}", v)); + } + + return b.Options; + } + } +} diff --git a/lib/sql-providers/sqlserver/VNLib.Plugins.Extensions.Loading.Sql.SQLServer/src/VNLib.Plugins.Extensions.Loading.Sql.SQLServer.csproj b/lib/sql-providers/sqlserver/VNLib.Plugins.Extensions.Loading.Sql.SQLServer/src/VNLib.Plugins.Extensions.Loading.Sql.SQLServer.csproj new file mode 100644 index 0000000..fea8eee --- /dev/null +++ b/lib/sql-providers/sqlserver/VNLib.Plugins.Extensions.Loading.Sql.SQLServer/src/VNLib.Plugins.Extensions.Loading.Sql.SQLServer.csproj @@ -0,0 +1,59 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net8.0</TargetFramework> + <AssemblyName>VNLib.Plugins.Extensions.Sql.SqlServer</AssemblyName> + <RootNamespace>VNLib.Plugins.Extensions.Sql</RootNamespace> + <Nullable>enable</Nullable> + <ImplicitUsings>disable</ImplicitUsings> + <GenerateDocumentationFile>True</GenerateDocumentationFile> + <!--Enable dynamic loading--> + <EnableDynamicLoading>true</EnableDynamicLoading> + </PropertyGroup> + + <PropertyGroup> + <PackageId>VNLib.Plugins.Extensions.Sql.SqlServer</PackageId> + <Authors>Vaughn Nugent</Authors> + <Company>Vaughn Nugent</Company> + <Product>VNLib.Plugins.Extensions.Sql.SqlServer</Product> + <Copyright>Copyright © 2024 Vaughn Nugent</Copyright> + <PackageProjectUrl>https://www.vaughnnugent.com/resources/software/modules/VNLib.Plugins.Extensions</PackageProjectUrl> + <RepositoryUrl>https://github.com/VnUgE/VNLib.Plugins.Extensions/tree/master/lib/sql-providers/VNLib.Plugins.Extensions.Sql.SqlServer</RepositoryUrl> + <Description>A runtime asset library that provides SqlServer interfaces for ADO and EFCore SQL server clients</Description> + <PackageReadmeFile>README.md</PackageReadmeFile> + <PackageLicenseFile>LICENSE</PackageLicenseFile> + </PropertyGroup> + + <ItemGroup> + <None Include="../../../../../LICENSE"> + <Pack>True</Pack> + <PackagePath>\</PackagePath> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Include="..\README.md"> + <Pack>True</Pack> + <PackagePath>\</PackagePath> + </None> + </ItemGroup> + + <ItemGroup> + <PackageReference Include="ErrorProne.NET.CoreAnalyzers" Version="0.1.2"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="ErrorProne.NET.Structs" Version="0.1.2"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.1" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\..\..\..\VNLib.Plugins.Extensions.Loading\src\VNLib.Plugins.Extensions.Loading.csproj" /> + </ItemGroup> + + <Target Condition="'$(BuildingInsideVisualStudio)' == true" Name="PostBuild" AfterTargets="PostBuildEvent"> + <Exec Command="start xcopy "$(TargetDir)" "$(SolutionDir)devplugins\RuntimeAssets\$(TargetName)" /E /Y /R" /> + </Target> + +</Project> |