aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2024-02-14 14:35:03 -0500
committerLibravatar vnugent <public@vaughnnugent.com>2024-02-14 14:35:03 -0500
commit54760bfabb36c96f666ca7f77028d0d6a9c812fc (patch)
treecd0d010ef461ecd8a7259df0ade979471a6e527e
parent107f9774df2ad484f3edc5f401c738f647f00f01 (diff)
Squashed commit of the following:
commit d72bd53e20770be4ced0d627567ecf567d1ce9f4 Author: vnugent <public@vaughnnugent.com> Date: Mon Feb 12 18:34:52 2024 -0500 refactor: #1 convert sql libraries to assets for better code splitting commit 736b873e32447254b3aadbb5c6252818c25e8fd4 Author: vnugent <public@vaughnnugent.com> Date: Sun Feb 4 01:30:25 2024 -0500 submit pending changes
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IConcurrentDbContext.cs44
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDataStore.cs4
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDbContextHandle.cs2
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/Abstractions/ITransactionalDbContext.cs58
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/DBContextBase.cs85
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/DbStore.cs4
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/Extensions/DbStoreExtensions.cs186
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/Extensions/DbStoreHelperExtensions.cs89
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageContext.cs2
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageManager.cs37
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/TransactionalDbContext.cs139
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/VNLib.Plugins.Extensions.Data.csproj2
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading.Sql/src/SqlDbConnectionLoader.cs295
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading.Sql/src/VNLib.Plugins.Extensions.Loading.Sql.csproj5
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs35
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs72
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/Users/UserManager.cs4
-rw-r--r--lib/sql-providers/mysql/VNLib.Plugins.Extensions.Loading.Sql.MySql/README.md17
-rw-r--r--lib/sql-providers/mysql/VNLib.Plugins.Extensions.Loading.Sql.MySql/build.readme.md0
-rw-r--r--lib/sql-providers/mysql/VNLib.Plugins.Extensions.Loading.Sql.MySql/src/MYSqlExport.cs138
-rw-r--r--lib/sql-providers/mysql/VNLib.Plugins.Extensions.Loading.Sql.MySql/src/VNLib.Plugins.Extensions.Loading.Sql.MYSql.csproj59
-rw-r--r--lib/sql-providers/sqlite/VNLib.Plugins.Extensions.Loading.Sql.SQLite/README.md17
-rw-r--r--lib/sql-providers/sqlite/VNLib.Plugins.Extensions.Loading.Sql.SQLite/build.readme.md0
-rw-r--r--lib/sql-providers/sqlite/VNLib.Plugins.Extensions.Loading.Sql.SQLite/src/SQLiteExport.cs131
-rw-r--r--lib/sql-providers/sqlite/VNLib.Plugins.Extensions.Loading.Sql.SQLite/src/VNLib.Plugins.Extensions.Loading.Sql.SQLite.csproj59
-rw-r--r--lib/sql-providers/sqlserver/VNLib.Plugins.Extensions.Loading.Sql.SQLServer/README.md17
-rw-r--r--lib/sql-providers/sqlserver/VNLib.Plugins.Extensions.Loading.Sql.SQLServer/build.readme.md0
-rw-r--r--lib/sql-providers/sqlserver/VNLib.Plugins.Extensions.Loading.Sql.SQLServer/src/SqlServerExport.cs149
-rw-r--r--lib/sql-providers/sqlserver/VNLib.Plugins.Extensions.Loading.Sql.SQLServer/src/VNLib.Plugins.Extensions.Loading.Sql.SQLServer.csproj59
-rw-r--r--vnlib.plugins.extensions.build.sln25
30 files changed, 1115 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 &quot;$(TargetDir)&quot; &quot;$(SolutionDir)devplugins\RuntimeAssets\$(TargetName)&quot; /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 &quot;$(TargetDir)&quot; &quot;$(SolutionDir)devplugins\RuntimeAssets\$(TargetName)&quot; /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 &quot;$(TargetDir)&quot; &quot;$(SolutionDir)devplugins\RuntimeAssets\$(TargetName)&quot; /E /Y /R" />
+ </Target>
+
+</Project>
diff --git a/vnlib.plugins.extensions.build.sln b/vnlib.plugins.extensions.build.sln
index f1aaf46..308e2af 100644
--- a/vnlib.plugins.extensions.build.sln
+++ b/vnlib.plugins.extensions.build.sln
@@ -24,6 +24,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
Taskfile.yaml = Taskfile.yaml
EndProjectSection
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sql-providers", "sql-providers", "{A03C3E08-6EAA-4D4A-A434-E9455E9B3868}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VNLib.Plugins.Extensions.Loading.Sql.MYSql", "lib\sql-providers\mysql\VNLib.Plugins.Extensions.Loading.Sql.MySql\src\VNLib.Plugins.Extensions.Loading.Sql.MYSql.csproj", "{50B5D5B8-2944-43E3-9CB0-F19D46F42984}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VNLib.Plugins.Extensions.Loading.Sql.SQLite", "lib\sql-providers\sqlite\VNLib.Plugins.Extensions.Loading.Sql.SQLite\src\VNLib.Plugins.Extensions.Loading.Sql.SQLite.csproj", "{F6520193-1C5B-4A7C-8E5A-05D910AC9139}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VNLib.Plugins.Extensions.Loading.Sql.SQLServer", "lib\sql-providers\sqlserver\VNLib.Plugins.Extensions.Loading.Sql.SQLServer\src\VNLib.Plugins.Extensions.Loading.Sql.SQLServer.csproj", "{2766FAA1-F7D0-45B9-A77B-9CE1F966FB3F}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -46,10 +54,27 @@ Global
{857DA423-DD18-40B4-AABB-0C12DB5E389D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{857DA423-DD18-40B4-AABB-0C12DB5E389D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{857DA423-DD18-40B4-AABB-0C12DB5E389D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {50B5D5B8-2944-43E3-9CB0-F19D46F42984}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {50B5D5B8-2944-43E3-9CB0-F19D46F42984}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {50B5D5B8-2944-43E3-9CB0-F19D46F42984}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {50B5D5B8-2944-43E3-9CB0-F19D46F42984}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F6520193-1C5B-4A7C-8E5A-05D910AC9139}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F6520193-1C5B-4A7C-8E5A-05D910AC9139}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F6520193-1C5B-4A7C-8E5A-05D910AC9139}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F6520193-1C5B-4A7C-8E5A-05D910AC9139}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2766FAA1-F7D0-45B9-A77B-9CE1F966FB3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2766FAA1-F7D0-45B9-A77B-9CE1F966FB3F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2766FAA1-F7D0-45B9-A77B-9CE1F966FB3F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2766FAA1-F7D0-45B9-A77B-9CE1F966FB3F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {50B5D5B8-2944-43E3-9CB0-F19D46F42984} = {A03C3E08-6EAA-4D4A-A434-E9455E9B3868}
+ {F6520193-1C5B-4A7C-8E5A-05D910AC9139} = {A03C3E08-6EAA-4D4A-A434-E9455E9B3868}
+ {2766FAA1-F7D0-45B9-A77B-9CE1F966FB3F} = {A03C3E08-6EAA-4D4A-A434-E9455E9B3868}
+ EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BC28944A-A5ED-4433-AAC7-82840C94387A}
EndGlobalSection