aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IBulkDataStore.cs65
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IConcurrentDbContext.cs (renamed from lib/VNLib.Plugins.Extensions.Data/src/IConcurrentDbContext.cs)2
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDataStore.cs107
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDbContextHandle.cs84
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDbModel.cs (renamed from lib/VNLib.Plugins.Extensions.Data/src/IDbModel.cs)7
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDbQueryLookup.cs (renamed from lib/VNLib.Plugins.Extensions.Data/src/DbStoreQueries.cs)45
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IPaginatedDataStore.cs58
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/Abstractions/ITransactionalDbContext.cs (renamed from lib/VNLib.Plugins.Extensions.Data/src/ITransactionalDbContext.cs)4
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/DbModelBase.cs4
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/DbStore.cs370
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/DbStoreHelperExtensions.cs82
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/Extensions.cs81
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/Extensions/BulkExtensions.cs44
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/Extensions/DbStoreExtensions.cs529
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/Extensions/DbStoreHelperExtensions.cs89
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/Extensions/ProtectedEntityExtensions.cs (renamed from lib/VNLib.Plugins.Extensions.Data/src/ProtectedEntityExtensions.cs)33
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/ProtectedDbStore.cs70
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/Storage/Exceptions/LWDecriptorCreationException.cs (renamed from lib/VNLib.Plugins.Extensions.Data/src/Storage/LWDecriptorCreationException.cs)0
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/Storage/Exceptions/LWStorageRemoveFailedException.cs (renamed from lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageRemoveFailedException.cs)0
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/Storage/Exceptions/LWStorageUpdateFailedException.cs (renamed from lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageUpdateFailedException.cs)0
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/Storage/Exceptions/UndefinedBlobStateException.cs (renamed from lib/VNLib.Plugins.Extensions.Data/src/Storage/UndefinedBlobStateException.cs)0
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/Storage/FsExtensions.cs72
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/Storage/ISimpleFilesystem.cs72
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/TransactionalDbContext.cs84
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/VNLib.Plugins.Extensions.Data.csproj10
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading.Sql/src/SqlDbConnectionLoader.cs2
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading.Sql/src/VNLib.Plugins.Extensions.Loading.Sql.csproj4
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs46
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/Events/EventManagment.cs2
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/Secrets/SecretResult.cs11
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/Secrets/VaultSecrets.cs28
-rw-r--r--lib/VNLib.Plugins.Extensions.Validation/src/VNLib.Plugins.Extensions.Validation.csproj2
32 files changed, 1105 insertions, 902 deletions
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IBulkDataStore.cs b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IBulkDataStore.cs
deleted file mode 100644
index 0a2e4a8..0000000
--- a/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IBulkDataStore.cs
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
-* Copyright (c) 2022 Vaughn Nugent
-*
-* Library: VNLib
-* Package: VNLib.Plugins.Extensions.Data
-* File: IBulkDataStore.cs
-*
-* IBulkDataStore.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.Collections.Generic;
-using System.Threading.Tasks;
-
-using VNLib.Utils;
-
-namespace VNLib.Plugins.Extensions.Data.Abstractions
-{
- /// <summary>
- /// An abstraction that defines a Data-Store that supports
- /// bulk data operations
- /// </summary>
- /// <typeparam name="T">The data-model type</typeparam>
- public interface IBulkDataStore<T>
- {
- /// <summary>
- /// Deletes a collection of records from the store
- /// </summary>
- /// <param name="records">A collection of records to delete</param>
- ///<returns>A task the resolves the number of entires removed from the store</returns>
- Task<ERRNO> DeleteBulkAsync(ICollection<T> records);
- /// <summary>
- /// Updates a collection of records
- /// </summary>
- /// <param name="records">The collection of records to update</param>
- /// <returns>A task the resolves an error code (should evaluate to false on failure, and true on success)</returns>
- Task<ERRNO> UpdateBulkAsync(ICollection<T> records);
- /// <summary>
- /// Creates a bulk collection of records as entries in the store
- /// </summary>
- /// <param name="records">The collection of records to add</param>
- /// <returns>A task the resolves an error code (should evaluate to false on failure, and true on success)</returns>
- Task<ERRNO> CreateBulkAsync(ICollection<T> records);
- /// <summary>
- /// Creates or updates individual records from a bulk collection of records
- /// </summary>
- /// <param name="records">The collection of records to add</param>
- /// <returns>A task the resolves an error code (should evaluate to false on failure, and true on success)</returns>
- Task<ERRNO> AddOrUpdateBulkAsync(ICollection<T> records);
- }
-
-}
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/IConcurrentDbContext.cs b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IConcurrentDbContext.cs
index 330b05a..f3308c5 100644
--- a/lib/VNLib.Plugins.Extensions.Data/src/IConcurrentDbContext.cs
+++ b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IConcurrentDbContext.cs
@@ -26,7 +26,7 @@ using System.Threading;
using System.Threading.Tasks;
using System.Transactions;
-namespace VNLib.Plugins.Extensions.Data
+namespace VNLib.Plugins.Extensions.Data.Abstractions
{
/// <summary>
/// Represents a database context that can manage concurrency via transactions
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDataStore.cs b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDataStore.cs
index 1c8174c..bdd1b6c 100644
--- a/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDataStore.cs
+++ b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDataStore.cs
@@ -22,14 +22,6 @@
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
-using System;
-using System.Threading;
-using System.Threading.Tasks;
-using System.Collections.Generic;
-
-using VNLib.Utils;
-
-
namespace VNLib.Plugins.Extensions.Data.Abstractions
{
/// <summary>
@@ -37,99 +29,30 @@ namespace VNLib.Plugins.Extensions.Data.Abstractions
/// operations that retrieve or manipulate records of data
/// </summary>
/// <typeparam name="T">The data-model type</typeparam>
- public interface IDataStore<T>
+ public interface IDataStore<T> where T: class, IDbModel
{
/// <summary>
- /// Gets the total number of records in the current store
- /// </summary>
- /// <param name="cancellation">A cancellation token to cancel the operation</param>
- /// <returns>A task that resolves the number of records in the store</returns>
- Task<long> GetCountAsync(CancellationToken cancellation = default);
- /// <summary>
- /// Gets the number of records that belong to the specified constraint
- /// </summary>
- /// <param name="specifier">A specifier to constrain the reults</param>
- /// <param name="cancellation">A cancellation token to cancel the operation</param>
- /// <returns>The number of records that belong to the specifier</returns>
- Task<long> GetCountAsync(string specifier, CancellationToken cancellation = default);
- /// <summary>
- /// Gets a record from its key
+ /// Gets a unique ID for a new record being added to the store
/// </summary>
- /// <param name="key">The key identifying the unique record</param>
- /// <param name="cancellation">A cancellation token to cancel the operation</param>
- /// <returns>A promise that resolves the record identified by the specified key</returns>
- Task<T?> GetSingleAsync(string key, CancellationToken cancellation = default);
+ string GetNewRecordId();
+
/// <summary>
- /// Gets a record from its key
+ /// Gets a new <see cref="TransactionalDbContext"/> ready for use
/// </summary>
- /// <param name="specifiers">A variable length specifier arguemnt array for retreiving a single application</param>
/// <returns></returns>
- Task<T?> GetSingleAsync(params string[] specifiers);
- /// <summary>
- /// Gets a record from the store with a partial model, intended to complete the model
- /// </summary>
- /// <param name="record">The partial model used to query the store</param>
- /// <param name="cancellation">A cancellation token to cancel the operation</param>
- /// <returns>A task the resolves the completed data-model</returns>
- Task<T?> GetSingleAsync(T record, CancellationToken cancellation = default);
- /// <summary>
- /// Fills a collection with enires retireved from the store using the specifer
- /// </summary>
- /// <param name="collection">The collection to add entires to</param>
- /// <param name="specifier">A specifier argument to constrain results</param>
- /// <param name="limit">The maximum number of elements to retrieve</param>
- /// <param name="cancellation">A cancellation token to cancel the operation</param>
- /// <returns>A Task the resolves to the number of items added to the collection</returns>
- Task<ERRNO> GetCollectionAsync(ICollection<T> collection, string specifier, int limit, CancellationToken cancellation = default);
- /// <summary>
- /// Fills a collection with enires retireved from the store using a variable length specifier
- /// parameter
- /// </summary>
- /// <param name="collection">The collection to add entires to</param>
- /// <param name="limit">The maximum number of elements to retrieve</param>
- /// <param name="args"></param>
- /// <returns>A Task the resolves to the number of items added to the collection</returns>
- Task<ERRNO> GetCollectionAsync(ICollection<T> collection, int limit, params string[] args);
- /// <summary>
- /// Updates an entry in the store with the specified record
- /// </summary>
- /// <param name="record">The record to update</param>
- /// <param name="cancellation">A cancellation token to cancel the operation</param>
- /// <returns>A task the resolves an error code (should evaluate to false on failure, and true on success)</returns>
- Task<ERRNO> UpdateAsync(T record, CancellationToken cancellation = default);
- /// <summary>
- /// Creates a new entry in the store representing the specified record
- /// </summary>
- /// <param name="record">The record to add to the store</param>
- /// <param name="cancellation">A cancellation token to cancel the operation</param>
- /// <returns>A task the resolves an error code (should evaluate to false on failure, and true on success)</returns>
- Task<ERRNO> CreateAsync(T record, CancellationToken cancellation = default);
- /// <summary>
- /// Deletes one or more entrires from the store matching the specified record
- /// </summary>
- /// <param name="record">The record to remove from the store</param>
- /// <param name="cancellation">A cancellation token to cancel the operation</param>
- /// <returns>A task the resolves the number of records removed(should evaluate to false on failure, and deleted count on success)</returns>
- Task<ERRNO> DeleteAsync(T record, CancellationToken cancellation = default);
- /// <summary>
- /// Deletes one or more entires from the store matching the specified unique key
- /// </summary>
- /// <param name="key">The unique key that identifies the record</param>
- /// <param name="cancellation">A cancellation token to cancel the operation</param>
- /// <returns>A task the resolves the number of records removed(should evaluate to false on failure, and deleted count on success)</returns>
- Task<ERRNO> DeleteAsync(string key, CancellationToken cancellation = default);
+ IDbContextHandle GetNewContext();
+
/// <summary>
- /// Deletes one or more entires from the store matching the supplied specifiers
+ /// Represents a table of ef queryies that can be used to execute operations against a a database
/// </summary>
- /// <param name="specifiers">A variable length array of specifiers used to delete one or more entires</param>
- /// <returns>A task the resolves the number of records removed(should evaluate to false on failure, and deleted count on success)</returns>
- Task<ERRNO> DeleteAsync(params string[] specifiers);
+ IDbQueryLookup<T> QueryTable { get; }
+
/// <summary>
- /// Updates an entry in the store if it exists, or creates a new entry if one does not already exist
+ /// Updates the current record (if found) to the new record before
+ /// storing the updates.
/// </summary>
- /// <param name="record">The record to add to the store</param>
- /// <param name="cancellation">A cancellation token to cancel the operation</param>
- /// <returns>A task the resolves the result of the operation</returns>
- Task<ERRNO> AddOrUpdateAsync(T record, CancellationToken cancellation = default);
+ /// <param name="newRecord">The new record to capture data from</param>
+ /// <param name="existing">The current record to be updated</param>
+ void OnRecordUpdate(T newRecord, T existing);
}
}
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDbContextHandle.cs b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDbContextHandle.cs
new file mode 100644
index 0000000..73734b5
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDbContextHandle.cs
@@ -0,0 +1,84 @@
+/*
+* Copyright (c) 2023 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Data
+* File: IDbContextHandle.cs
+*
+* IDbContextHandle.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 VNLib.Utils;
+
+namespace VNLib.Plugins.Extensions.Data.Abstractions
+{
+ /// <summary>
+ /// Represents an open database connection and interfaces with the database,
+ /// allows queries, and modifications of the set
+ /// </summary>
+ public interface IDbContextHandle : IAsyncDisposable
+ {
+ /// <summary>
+ /// Gets a supported set of the desired entity type within the context
+ /// </summary>
+ /// <typeparam name="T">The entity model type</typeparam>
+ /// <returns>A querriable instance to execute queries on</returns>
+ IQueryable<T> Set<T>() where T : class;
+
+ /// <summary>
+ /// Adds a new entity to the set
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="entity">The entity instance to add to the set</param>
+ void Add<T>(T entity) where T : class;
+
+ /// <summary>
+ /// Adds a range of entities to the set of the given type
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="entities">The range of entitites to add to the set</param>
+ void AddRange<T>(IEnumerable<T> entities) where T : class;
+
+ /// <summary>
+ /// Removes an entity of a given type from the set
+ /// </summary>
+ /// <typeparam name="T">The entity type to remove</typeparam>
+ /// <param name="entity">The entity instance containing required information to remove</param>
+ void Remove<T>(T entity) where T : class;
+
+ /// <summary>
+ /// Removes a range of entities of a given type from the set
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="entities">The range of entities to remove</param>
+ void RemoveRange<T>(IEnumerable<T> entities) where T : class;
+
+ /// <summary>
+ /// Commits saves changes on the context and optionally commits changes to the database
+ /// </summary>
+ /// <param name="commit">A value that indicates whether the changes should be commited to the database</param>
+ /// <param name="cancellation">A token to cancel the operation</param>
+ /// <returns>The result of the database commit</returns>
+ Task<ERRNO> SaveAndCloseAsync(bool commit, CancellationToken cancellation = default);
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/IDbModel.cs b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDbModel.cs
index f60dc27..2826c5a 100644
--- a/lib/VNLib.Plugins.Extensions.Data/src/IDbModel.cs
+++ b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDbModel.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2022 Vaughn Nugent
+* Copyright (c) 2023 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Extensions.Data
@@ -24,7 +24,7 @@
using System;
-namespace VNLib.Plugins.Extensions.Data
+namespace VNLib.Plugins.Extensions.Data.Abstractions
{
/// <summary>
/// Represents a basic data model for an EFCore entity
@@ -36,14 +36,17 @@ namespace VNLib.Plugins.Extensions.Data
/// A unique id for the entity
/// </summary>
string Id { get; set; }
+
/// <summary>
/// The <see cref="DateTime"/> the entity was created in the store
/// </summary>
DateTime Created { get; set; }
+
/// <summary>
/// The <see cref="DateTime"/> the entity was last modified in the store
/// </summary>
DateTime LastModified { get; set; }
+
/// <summary>
/// Entity concurrency token
/// </summary>
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/DbStoreQueries.cs b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDbQueryLookup.cs
index ff0319e..86c8bff 100644
--- a/lib/VNLib.Plugins.Extensions.Data/src/DbStoreQueries.cs
+++ b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDbQueryLookup.cs
@@ -3,10 +3,10 @@
*
* Library: VNLib
* Package: VNLib.Plugins.Extensions.Data
-* File: DbStoreQueries.cs
+* File: IDbQueryLookup.cs
*
-* DbStoreQueries.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger
-* VNLib collection of libraries and utilities.
+* IDbQueryLookup.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
@@ -22,15 +22,16 @@
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
-using System;
using System.Linq;
-namespace VNLib.Plugins.Extensions.Data
+namespace VNLib.Plugins.Extensions.Data.Abstractions
{
-
- public partial class DbStore<T>
+ /// <summary>
+ /// Represents a collection of queries that can be used to execute operations against a a database
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ public interface IDbQueryLookup<T> where T : class, IDbModel
{
-
/// <summary>
/// Builds a query that attempts to get a single entry from the
/// store based on the specified record if it does not have a
@@ -39,7 +40,7 @@ namespace VNLib.Plugins.Extensions.Data
/// <param name="context">The active context to query</param>
/// <param name="record">The record to search for</param>
/// <returns>A query that yields a single record if it exists in the store</returns>
- protected virtual IQueryable<T> AddOrUpdateQueryBuilder(TransactionalDbContext context, T record)
+ virtual IQueryable<T> AddOrUpdateQueryBuilder(IDbContextHandle context, T record)
{
//default to get single of the specific record
return GetSingleQueryBuilder(context, record);
@@ -52,13 +53,12 @@ namespace VNLib.Plugins.Extensions.Data
/// <param name="context">The active context to query</param>
/// <param name="record">The record to search for</param>
/// <returns>A query that yields a single record to update if it exists in the store</returns>
- protected virtual IQueryable<T> UpdateQueryBuilder(TransactionalDbContext context, T record)
+ virtual IQueryable<T> UpdateQueryBuilder(IDbContextHandle context, T record)
{
//default to get single of the specific record
return GetSingleQueryBuilder(context, record);
}
-
/// <summary>
/// Builds a query that results in a single entry to delete from the
/// constraint arguments
@@ -66,7 +66,7 @@ namespace VNLib.Plugins.Extensions.Data
/// <param name="context">The active context</param>
/// <param name="constraints">A variable length parameter array of query constraints</param>
/// <returns>A query that yields a single record (or no record) to delete</returns>
- protected virtual IQueryable<T> DeleteQueryBuilder(TransactionalDbContext context, params string[] constraints)
+ virtual IQueryable<T> DeleteQueryBuilder(IDbContextHandle context, params string[] constraints)
{
//default use the get-single method, as the implementation is usually identical
return GetSingleQueryBuilder(context, constraints);
@@ -78,19 +78,18 @@ namespace VNLib.Plugins.Extensions.Data
/// <param name="context">The active context to run the query on</param>
/// <param name="specifier">The specifier constrain</param>
/// <returns>A query that can be counted</returns>
- protected virtual IQueryable<T> GetCollectionQueryBuilder(TransactionalDbContext context, string specifier)
+ virtual IQueryable<T> GetCollectionQueryBuilder(IDbContextHandle context, string specifier)
{
return GetCollectionQueryBuilder(context, new string[] { specifier });
}
-
/// <summary>
/// Builds a query to get a count of records constrained by the specifier
/// </summary>
/// <param name="context">The active context to run the query on</param>
/// <param name="specifier">The specifier constrain</param>
/// <returns>A query that can be counted</returns>
- protected virtual IQueryable<T> GetCountQueryBuilder(TransactionalDbContext context, string specifier)
+ virtual IQueryable<T> GetCountQueryBuilder(IDbContextHandle context, string specifier)
{
//Default use the get collection and just call the count method
return GetCollectionQueryBuilder(context, specifier);
@@ -107,7 +106,7 @@ namespace VNLib.Plugins.Extensions.Data
/// <param name="context">The context to execute query against</param>
/// <param name="record">A record to referrence the lookup</param>
/// <returns>A query that yields a single record</returns>
- protected virtual IQueryable<T> GetSingleQueryBuilder(TransactionalDbContext context, T record)
+ virtual IQueryable<T> GetSingleQueryBuilder(IDbContextHandle context, T record)
{
return from entry in context.Set<T>()
where entry.Id == record.Id
@@ -120,27 +119,19 @@ namespace VNLib.Plugins.Extensions.Data
/// <param name="context">The active context to run the query on</param>
/// <param name="constraints">An arguments array to constrain the results of the query</param>
/// <returns>A query that returns a paginated collection of records from the store</returns>
- protected virtual IQueryable<T> GetPageQueryBuilder(TransactionalDbContext context, params string[] constraints)
+ virtual IQueryable<T> GetPageQueryBuilder(IDbContextHandle context, params string[] constraints)
{
//Default to getting the entire collection and just selecting a single page
return GetCollectionQueryBuilder(context, constraints);
}
/// <summary>
- /// Updates the current record (if found) to the new record before
- /// storing the updates.
- /// </summary>
- /// <param name="newRecord">The new record to capture data from</param>
- /// <param name="currentRecord">The current record to be updated</param>
- protected abstract void OnRecordUpdate(T newRecord, T currentRecord);
-
- /// <summary>
/// Builds a query to get a single record from the variable length parameter arguments
/// </summary>
/// <param name="context">The context to execute query against</param>
/// <param name="constraints">Arguments to constrain the results of the query to a single record</param>
/// <returns>A query that yields a single record</returns>
- protected abstract IQueryable<T> GetSingleQueryBuilder(TransactionalDbContext context, params string[] constraints);
+ IQueryable<T> GetSingleQueryBuilder(IDbContextHandle context, params string[] constraints);
/// <summary>
/// Builds a query to get a collection of records based on an variable length array of parameters
@@ -148,6 +139,6 @@ namespace VNLib.Plugins.Extensions.Data
/// <param name="context">The active context to run the query on</param>
/// <param name="constraints">An arguments array to constrain the results of the query</param>
/// <returns>A query that returns a collection of records from the store</returns>
- protected abstract IQueryable<T> GetCollectionQueryBuilder(TransactionalDbContext context, params string[] constraints);
+ IQueryable<T> GetCollectionQueryBuilder(IDbContextHandle context, params string[] constraints);
}
}
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IPaginatedDataStore.cs b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IPaginatedDataStore.cs
deleted file mode 100644
index 7c271eb..0000000
--- a/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IPaginatedDataStore.cs
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
-* Copyright (c) 2023 Vaughn Nugent
-*
-* Library: VNLib
-* Package: VNLib.Plugins.Extensions.Data
-* File: IPaginatedDataStore.cs
-*
-* IPaginatedDataStore.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.Collections.Generic;
-
-namespace VNLib.Plugins.Extensions.Data.Abstractions
-{
- /// <summary>
- /// Defines a Data-Store that can retirieve and manipulate paginated
- /// data
- /// </summary>
- /// <typeparam name="T">The data-model type</typeparam>
- public interface IPaginatedDataStore<T>
- {
- /// <summary>
- /// Gets a collection of records using a pagination style query, and adds the records to the collecion
- /// </summary>
- /// <param name="collection">The collection to add records to</param>
- /// <param name="page">Pagination page to get records from</param>
- /// <param name="limit">The maximum number of items to retrieve from the store</param>
- /// <param name="cancellation">A cancellation token to cancel the operation</param>
- /// <returns>A task that resolves the number of items added to the collection</returns>
- Task<int> GetPageAsync(ICollection<T> collection, int page, int limit, CancellationToken cancellation = default);
- /// <summary>
- /// Gets a collection of records using a pagination style query with constraint arguments, and adds the records to the collecion
- /// </summary>
- /// <param name="collection">The collection to add records to</param>
- /// <param name="page">Pagination page to get records from</param>
- /// <param name="limit">The maximum number of items to retrieve from the store</param>
- /// <param name="constraints">A params array of strings to constrain the result set from the db</param>
- /// <returns>A task that resolves the number of items added to the collection</returns>
- Task<int> GetPageAsync(ICollection<T> collection, int page, int limit, params string[] constraints);
- }
-
-}
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/ITransactionalDbContext.cs b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/ITransactionalDbContext.cs
index a699140..dd906d1 100644
--- a/lib/VNLib.Plugins.Extensions.Data/src/ITransactionalDbContext.cs
+++ b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/ITransactionalDbContext.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2022 Vaughn Nugent
+* Copyright (c) 2023 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Extensions.Data
@@ -27,7 +27,7 @@ using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore.Storage;
-namespace VNLib.Plugins.Extensions.Data
+namespace VNLib.Plugins.Extensions.Data.Abstractions
{
/// <summary>
/// Represents a database context that can manage concurrency via transactions
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/DbModelBase.cs b/lib/VNLib.Plugins.Extensions.Data/src/DbModelBase.cs
index 5104e02..cdf763c 100644
--- a/lib/VNLib.Plugins.Extensions.Data/src/DbModelBase.cs
+++ b/lib/VNLib.Plugins.Extensions.Data/src/DbModelBase.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2022 Vaughn Nugent
+* Copyright (c) 2023 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Extensions.Data
@@ -26,6 +26,8 @@ using System;
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
+using VNLib.Plugins.Extensions.Data.Abstractions;
+
namespace VNLib.Plugins.Extensions.Data
{
/// <summary>
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/DbStore.cs b/lib/VNLib.Plugins.Extensions.Data/src/DbStore.cs
index 761d78f..b5f327e 100644
--- a/lib/VNLib.Plugins.Extensions.Data/src/DbStore.cs
+++ b/lib/VNLib.Plugins.Extensions.Data/src/DbStore.cs
@@ -22,16 +22,8 @@
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
-using System;
-using System.Linq;
-using System.Threading;
-using System.Transactions;
-using System.Threading.Tasks;
using System.Collections.Generic;
-using Microsoft.EntityFrameworkCore;
-
-using VNLib.Utils;
using VNLib.Utils.Memory.Caching;
using VNLib.Plugins.Extensions.Data.Abstractions;
@@ -42,370 +34,28 @@ namespace VNLib.Plugins.Extensions.Data
/// Implements basic data-store functionality with abstract query builders
/// </summary>
/// <typeparam name="T">A <see cref="DbModelBase"/> implemented type</typeparam>
- public abstract partial class DbStore<T> : IDataStore<T>, IPaginatedDataStore<T> where T: class, IDbModel
+ public abstract class DbStore<T> : IDataStore<T> where T: class, IDbModel
{
- /// <summary>
- /// Gets a unique ID for a new record being added to the store
- /// </summary>
- public abstract string RecordIdBuilder { get; }
/// <summary>
/// Gets a new <see cref="TransactionalDbContext"/> ready for use
/// </summary>
/// <returns></returns>
- public abstract TransactionalDbContext NewContext();
-
- /// <summary>
- /// An object rental for entity collections
- /// </summary>
- public ObjectRental<List<T>> ListRental { get; } = ObjectRental.Create<List<T>>(null, static ret => ret.Clear());
-
- #region Add Or Update
- ///<inheritdoc/>
- public virtual async Task<ERRNO> AddOrUpdateAsync(T record, CancellationToken cancellation = default)
- {
- //Open new db context
- await using TransactionalDbContext ctx = await this.OpenAsync(IsolationLevel.ReadCommitted, cancellation);
-
- IQueryable<T> query;
-
- if (string.IsNullOrWhiteSpace(record.Id))
- {
- //Get the application
- query = AddOrUpdateQueryBuilder(ctx, record);
- }
- else
- {
- //Get the application
- query = (from et in ctx.Set<T>()
- where et.Id == record.Id
- select et);
- }
-
- //Using single
- T? entry = await query.SingleOrDefaultAsync(cancellation);
-
- //Check if creted
- if (entry == null)
- {
- //Create a new template id
- record.Id = RecordIdBuilder;
- //Set the created/lm times
- record.Created = record.LastModified = DateTime.UtcNow;
- //Add the new template to the ctx
- ctx.Add(record);
- }
- else
- {
- OnRecordUpdate(record, entry);
- }
-
- return await ctx.SaveAndCloseAsync(cancellation);
- }
-
- ///<inheritdoc/>
- public virtual async Task<ERRNO> UpdateAsync(T record, CancellationToken cancellation = default)
- {
- //Open new db context
- await using TransactionalDbContext ctx = await this.OpenAsync(IsolationLevel.Serializable, cancellation);
-
- //Get the application
- IQueryable<T> query = UpdateQueryBuilder(ctx, record);
-
- //Using single to make sure only one app is in the db (should never be an issue)
- T? oldEntry = await query.SingleOrDefaultAsync(cancellation);
-
- if (oldEntry == null)
- {
- return false;
- }
-
- //Update the template meta-data
- OnRecordUpdate(record, oldEntry);
-
- //Only publish update if changes happened
- if (!ctx.ChangeTracker.HasChanges())
- {
- //commit transaction if no changes need to be made
- await ctx.CommitTransactionAsync(cancellation);
- return true;
- }
-
- return await ctx.SaveAndCloseAsync(cancellation);
- }
-
- ///<inheritdoc/>
- public virtual async Task<ERRNO> CreateAsync(T record, CancellationToken cancellation = default)
- {
- //Open new db context
- await using TransactionalDbContext ctx = await this.OpenAsync(IsolationLevel.ReadUncommitted, cancellation);
-
- //Create a new template id
- record.Id = RecordIdBuilder;
-
- //Update the created/last modified time of the record
- record.Created = record.LastModified = DateTime.UtcNow;
-
- //Add the new template
- ctx.Add(record);
-
- return await ctx.SaveAndCloseAsync(cancellation);
- }
-
- #endregion
-
- #region Delete
+ public abstract IDbContextHandle GetNewContext();
///<inheritdoc/>
- public virtual async Task<ERRNO> DeleteAsync(string key, CancellationToken cancellation = default)
- {
- //Open new db context
- await using TransactionalDbContext ctx = await this.OpenAsync(IsolationLevel.RepeatableRead, cancellation);
-
- //Get the template by its id
- IQueryable<T> query = (from temp in ctx.Set<T>()
- where temp.Id == key
- select temp);
-
- T? record = await query.SingleOrDefaultAsync(cancellation);
-
- if (record == null)
- {
- return false;
- }
-
- //Add the new application
- ctx.Remove(record);
-
- return await ctx.SaveAndCloseAsync(cancellation);
- }
+ public abstract string GetNewRecordId();
///<inheritdoc/>
- public virtual async Task<ERRNO> DeleteAsync(T record, CancellationToken cancellation = default)
- {
- //Open new db context
- await using TransactionalDbContext ctx = await this.OpenAsync(IsolationLevel.RepeatableRead, cancellation);
-
- //Get a query for a a single item
- IQueryable<T> query = GetSingleQueryBuilder(ctx, record);
-
- //Get the entry
- T? entry = await query.SingleOrDefaultAsync(cancellation);
-
- if (entry == null)
- {
- return false;
- }
-
- //Add the new application
- ctx.Remove(entry);
-
- return await ctx.SaveAndCloseAsync(cancellation);
- }
+ public abstract void OnRecordUpdate(T newRecord, T existing);
///<inheritdoc/>
- public virtual async Task<ERRNO> DeleteAsync(params string[] specifiers)
- {
- //Open new db context
- await using TransactionalDbContext ctx = await this.OpenAsync(IsolationLevel.RepeatableRead);
-
- //Get the template by its id
- IQueryable<T> query = DeleteQueryBuilder(ctx, specifiers);
-
- T? entry = await query.SingleOrDefaultAsync();
-
- if (entry == null)
- {
- return false;
- }
-
- //Add the new application
- ctx.Remove(entry);
-
- return await ctx.SaveAndCloseAsync();
- }
-
- #endregion
-
- #region Get Collection
-
- ///<inheritdoc/>
- public virtual async Task<ERRNO> GetCollectionAsync(ICollection<T> collection, string specifier, int limit, CancellationToken cancellation = default)
- {
- int previous = collection.Count;
-
- //Open new db context
- await using TransactionalDbContext ctx = await this.OpenAsync(IsolationLevel.ReadUncommitted, cancellation);
-
- //Get the single template by its id
- await GetCollectionQueryBuilder(ctx, specifier)
- .Take(limit)
- .Select(static e => e)
- .ForEachAsync(collection.Add, cancellation);
-
- //close db and transaction
- await ctx.CommitTransactionAsync(cancellation);
+ public abstract IDbQueryLookup<T> QueryTable { get; }
- //Return the number of elements add to the collection
- return collection.Count - previous;
- }
-
- ///<inheritdoc/>
- public virtual async Task<ERRNO> GetCollectionAsync(ICollection<T> collection, int limit, params string[] args)
- {
- int previous = collection.Count;
-
- //Open new db context
- await using TransactionalDbContext ctx = await this.OpenAsync(IsolationLevel.ReadUncommitted);
-
- //Get the single template by the supplied user arguments
- await GetCollectionQueryBuilder(ctx, args)
- .Take(limit)
- .Select(static e => e)
- .ForEachAsync(collection.Add);
-
- //close db and transaction
- await ctx.CommitTransactionAsync();
-
- //Return the number of elements add to the collection
- return collection.Count - previous;
- }
-
- #endregion
-
- #region Get Count
-
- ///<inheritdoc/>
- public virtual async Task<long> GetCountAsync(CancellationToken cancellation = default)
- {
- //Open db connection
- await using TransactionalDbContext ctx = await this.OpenAsync(IsolationLevel.ReadUncommitted, cancellation);
-
- //Async get the number of records of the given entity type
- long count = await ctx.Set<T>().LongCountAsync(cancellation);
-
- //close db and transaction
- await ctx.CommitTransactionAsync(cancellation);
-
- return count;
- }
-
- ///<inheritdoc/>
- public virtual async Task<long> GetCountAsync(string specifier, CancellationToken cancellation)
- {
- await using TransactionalDbContext ctx = await this.OpenAsync(IsolationLevel.ReadUncommitted, cancellation);
-
- //Async get the number of records of the given entity type
- long count = await GetCountQueryBuilder(ctx, specifier).LongCountAsync(cancellation);
-
- //close db and transaction
- await ctx.CommitTransactionAsync(cancellation);
-
- return count;
- }
-
-
- #endregion
-
- #region Get Single
-
- ///<inheritdoc/>
- public virtual async Task<T?> GetSingleAsync(string key, CancellationToken cancellation = default)
- {
- //Open db connection
- await using TransactionalDbContext ctx = await this.OpenAsync(IsolationLevel.ReadUncommitted, cancellation);
-
- //Get the single template by its id
- T? record = await (from entry in ctx.Set<T>()
- where entry.Id == key
- select entry)
- .SingleOrDefaultAsync(cancellation);
-
- //close db and transaction
- await ctx.CommitTransactionAsync(cancellation);
- return record;
- }
-
- ///<inheritdoc/>
- public virtual async Task<T?> GetSingleAsync(T record, CancellationToken cancellation = default)
- {
- //Open db connection
- await using TransactionalDbContext ctx = await this.OpenAsync(IsolationLevel.ReadUncommitted, cancellation);
-
- //Get the single template by its id
- T? entry = await GetSingleQueryBuilder(ctx, record).SingleOrDefaultAsync(cancellation);
-
- //close db and transaction
- await ctx.CommitTransactionAsync(cancellation);
-
- return record;
- }
-
- ///<inheritdoc/>
- public virtual async Task<T?> GetSingleAsync(params string[] specifiers)
- {
- //Open db connection
- await using TransactionalDbContext ctx = await this.OpenAsync(IsolationLevel.ReadUncommitted);
-
- //Get the single template by its id
- T? record = await GetSingleQueryBuilder(ctx, specifiers).SingleOrDefaultAsync();
-
- //close db and transaction
- await ctx.CommitTransactionAsync();
-
- return record;
- }
-
- #endregion
-
- #region Get Page
-
- ///<inheritdoc/>
- public virtual async Task<int> GetPageAsync(ICollection<T> collection, int page, int limit, CancellationToken cancellation = default)
- {
- //Store preivous count
- int previous = collection.Count;
-
- //Open db connection
- await using TransactionalDbContext ctx = await this.OpenAsync(IsolationLevel.ReadUncommitted, cancellation);
-
- //Get a page offset and a limit for the
- await ctx.Set<T>()
- .Skip(page * limit)
- .Take(limit)
- .Select(static p => p)
- .ForEachAsync(collection.Add, cancellation);
-
- //close db and transaction
- await ctx.CommitTransactionAsync(cancellation);
-
- //Return the number of records added
- return collection.Count - previous;
- }
-
- ///<inheritdoc/>
- public virtual async Task<int> GetPageAsync(ICollection<T> collection, int page, int limit, params string[] constraints)
- {
- //Store preivous count
- int previous = collection.Count;
-
- //Open new db context
- await using TransactionalDbContext ctx = await this.OpenAsync(IsolationLevel.ReadUncommitted);
-
- //Get a page of records constrained by the given arguments
- await GetPageQueryBuilder(ctx, constraints)
- .Skip(page * limit)
- .Take(limit)
- .Select(static e => e)
- .ForEachAsync(collection.Add);
-
- //close db and transaction
- await ctx.CommitTransactionAsync();
-
- //Return the number of records added
- return collection.Count - previous;
- }
-
- #endregion
+ /// <summary>
+ /// An object rental for entity collections
+ /// </summary>
+ public ObjectRental<List<T>> ListRental { get; } = ObjectRental.Create<List<T>>(null, static ret => ret.Clear());
+
}
}
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/DbStoreHelperExtensions.cs b/lib/VNLib.Plugins.Extensions.Data/src/DbStoreHelperExtensions.cs
deleted file mode 100644
index 5971307..0000000
--- a/lib/VNLib.Plugins.Extensions.Data/src/DbStoreHelperExtensions.cs
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
-* Copyright (c) 2023 Vaughn Nugent
-*
-* Library: VNLib
-* Package: VNLib.Plugins.Extensions.Data
-* File: DbStore.cs
-*
-* DbStore.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.Utils;
-
-namespace VNLib.Plugins.Extensions.Data
-{
- internal static class DbStoreHelperExtensions
- {
- /// <summary>
- /// Commits saves changes on the context and commits the transaction if the result
- /// of the operation was successful
- /// </summary>
- /// <param name="ctx"></param>
- /// <param name="cancellation">A token to cancel the operation</param>
- /// <returns>A task that resolves the result of the operation</returns>
- public static async Task<ERRNO> SaveAndCloseAsync(this TransactionalDbContext ctx, CancellationToken cancellation = default)
- {
- //Save changes
- ERRNO result = await ctx.SaveChangesAsync(cancellation);
-
- if (result)
- {
- //commit transaction if update was successful
- await ctx.CommitTransactionAsync(cancellation);
- }
-
- return result;
- }
-
- /// <summary>
- /// Opens a new database connection and begins 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></returns>
- public static async Task<TransactionalDbContext> OpenAsync<T>(this DbStore<T> store, IsolationLevel level, CancellationToken cancellation = default)
- where T : class, IDbModel
- {
- //Open new db context
- TransactionalDbContext ctx = store.NewContext();
- try
- {
- //Open transaction
- await ctx.OpenTransactionAsync(level, cancellation);
- return ctx;
- }
- catch
- {
- await ctx.DisposeAsync();
- throw;
- }
- }
- }
-}
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Extensions.cs b/lib/VNLib.Plugins.Extensions.Data/src/Extensions.cs
deleted file mode 100644
index 0a956ed..0000000
--- a/lib/VNLib.Plugins.Extensions.Data/src/Extensions.cs
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
-* Copyright (c) 2022 Vaughn Nugent
-*
-* Library: VNLib
-* Package: VNLib.Plugins.Extensions.Data
-* File: Extensions.cs
-*
-* Extensions.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.Tasks;
-using System.Collections.Generic;
-
-using Microsoft.EntityFrameworkCore;
-
-using VNLib.Utils;
-using VNLib.Plugins.Extensions.Data.Abstractions;
-
-namespace VNLib.Plugins.Extensions.Data
-{
- public static class Extensions
- {
-
- public static int GetPageOrDefault(this IReadOnlyDictionary<string, string> queryArgs, int @default, int minClamp = 0, int maxClamp = int.MaxValue)
- {
- return queryArgs.TryGetValue("page", out string? pageStr) && int.TryParse(pageStr, out int page) ? Math.Clamp(page, minClamp, maxClamp) : @default;
- }
-
- public static int GetLimitOrDefault(this IReadOnlyDictionary<string, string> queryArgs, int @default, int minClamp = 0, int maxClamp = int.MaxValue)
- {
- return queryArgs.TryGetValue("limit", out string? limitStr) && int.TryParse(limitStr, out int limit) ? Math.Clamp(limit, minClamp, maxClamp) : @default;
- }
-
- public static async Task<ERRNO> AddBulkAsync<TEntity>(this DbStore<TEntity> store, IEnumerable<TEntity> records, string userId, bool overwriteTime = true)
- where TEntity : class, IDbModel, IUserEntity
- {
- //Open context and transaction
- await using TransactionalDbContext database = store.NewContext();
- await database.OpenTransactionAsync();
- //Get the entity set
- DbSet<TEntity> set = database.Set<TEntity>();
- //Generate random ids for the feeds and set user-id
- foreach (TEntity entity in records)
- {
- entity.Id = store.RecordIdBuilder;
- //Explicitly assign the user-id
- entity.UserId = userId;
- //If the entity has the default created time, update it, otherwise leave it as is
- if (overwriteTime || entity.Created == default)
- {
- entity.Created = DateTime.UtcNow;
- }
- //Update last-modified time
- entity.LastModified = DateTime.UtcNow;
- }
- //Add feeds to database
- set.AddRange(records);
- //Commit changes
- ERRNO count = await database.SaveChangesAsync();
- //Commit transaction and exit
- await database.CommitTransactionAsync();
- return count;
- }
- }
-}
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Extensions/BulkExtensions.cs b/lib/VNLib.Plugins.Extensions.Data/src/Extensions/BulkExtensions.cs
new file mode 100644
index 0000000..f14fdc6
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Data/src/Extensions/BulkExtensions.cs
@@ -0,0 +1,44 @@
+/*
+* Copyright (c) 2023 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Data
+* File: Extensions.cs
+*
+* Extensions.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.Collections.Generic;
+
+
+namespace VNLib.Plugins.Extensions.Data.Extensions
+{
+ public static class BulkExtensions
+ {
+
+ public static int GetPageOrDefault(this IReadOnlyDictionary<string, string> queryArgs, int @default, int minClamp = 0, int maxClamp = int.MaxValue)
+ {
+ return queryArgs.TryGetValue("page", out string? pageStr) && int.TryParse(pageStr, out int page) ? Math.Clamp(page, minClamp, maxClamp) : @default;
+ }
+
+ public static int GetLimitOrDefault(this IReadOnlyDictionary<string, string> queryArgs, int @default, int minClamp = 0, int maxClamp = int.MaxValue)
+ {
+ return queryArgs.TryGetValue("limit", out string? limitStr) && int.TryParse(limitStr, out int limit) ? Math.Clamp(limit, minClamp, maxClamp) : @default;
+ }
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Extensions/DbStoreExtensions.cs b/lib/VNLib.Plugins.Extensions.Data/src/Extensions/DbStoreExtensions.cs
new file mode 100644
index 0000000..e053900
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Data/src/Extensions/DbStoreExtensions.cs
@@ -0,0 +1,529 @@
+/*
+* Copyright (c) 2023 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Data
+* File: DbStore.cs
+*
+* DbStore.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.Transactions;
+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.Extensions
+{
+ /// <summary>
+ /// Extension methods for <see cref="IDataStore{T}"/> to add additional functionality
+ /// </summary>
+ public static class DbStoreExtensions
+ {
+ /// <summary>
+ /// Updates an entry in the store if it exists, or creates a new entry if one does not already exist
+ /// </summary>
+ /// <param name="store"></param>
+ /// <param name="record">The record to add to the store</param>
+ /// <param name="cancellation">A cancellation token to cancel the operation</param>
+ /// <returns>A task the resolves the result of the operation</returns>
+ public static async Task<ERRNO> AddOrUpdateAsync<T>(this IDataStore<T> store, T record, CancellationToken cancellation = default)
+ where T : class, IDbModel
+ {
+ //Open new db context
+ await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.ReadCommitted, cancellation);
+
+ IQueryable<T> query;
+
+ if (string.IsNullOrWhiteSpace(record.Id))
+ {
+ //Get the application
+ query = store.QueryTable.AddOrUpdateQueryBuilder(ctx, record);
+ }
+ else
+ {
+ //Get the application
+ query = (from et in ctx.Set<T>()
+ where et.Id == record.Id
+ select et);
+ }
+
+ //Using single
+ T? entry = await query.SingleOrDefaultAsync(cancellation);
+
+ //Check if creted
+ if (entry == null)
+ {
+ //Create a new template id
+ record.Id = store.GetNewRecordId();
+ //Set the created/lm times
+ record.Created = record.LastModified = DateTime.UtcNow;
+ //Add the new template to the ctx
+ ctx.Add(record);
+ }
+ else
+ {
+ store.OnRecordUpdate(record, entry);
+ }
+
+ return await ctx.SaveAndCloseAsync(true, cancellation);
+ }
+
+ /// <summary>
+ /// Updates an entry in the store with the specified record
+ /// </summary>
+ /// <param name="store"></param>
+ /// <param name="record">The record to update</param>
+ /// <param name="cancellation">A cancellation token to cancel the operation</param>
+ /// <returns>A task the resolves an error code (should evaluate to false on failure, and true on success)</returns>
+ public static async Task<ERRNO> UpdateAsync<T>(this IDataStore<T> store, T record, CancellationToken cancellation = default)
+ where T : class, IDbModel
+ {
+ //Open new db context
+ await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.Serializable, cancellation);
+
+ //Get the application
+ IQueryable<T> query = store.QueryTable.UpdateQueryBuilder(ctx, record);
+
+ //Using single to make sure only one app is in the db (should never be an issue)
+ T? oldEntry = await query.SingleOrDefaultAsync(cancellation);
+
+ if (oldEntry == null)
+ {
+ return false;
+ }
+
+ //Update the template meta-data
+ store.OnRecordUpdate(record, oldEntry);
+
+ return await ctx.SaveAndCloseAsync(true, cancellation);
+ }
+
+ /// <summary>
+ /// Creates a new entry in the store representing the specified record
+ /// </summary>
+ /// <param name="store"></param>
+ /// <param name="record">The record to add to the store</param>
+ /// <param name="cancellation">A cancellation token to cancel the operation</param>
+ /// <returns>A task the resolves an error code (should evaluate to false on failure, and true on success)</returns>
+ public static async Task<ERRNO> CreateAsync<T>(this IDataStore<T> store, T record, CancellationToken cancellation = default)
+ where T : class, IDbModel
+ {
+ //Open new db context
+ await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.ReadUncommitted, cancellation);
+
+ //Create a new template id
+ record.Id = store.GetNewRecordId();
+
+ //Update the created/last modified time of the record
+ record.Created = record.LastModified = DateTime.UtcNow;
+
+ //Add the new template
+ ctx.Add(record);
+
+ return await ctx.SaveAndCloseAsync(true, cancellation);
+ }
+
+
+ /// <summary>
+ /// Gets the total number of records in the current store
+ /// </summary>
+ /// <param name="store"></param>
+ /// <param name="cancellation">A cancellation token to cancel the operation</param>
+ /// <returns>A task that resolves the number of records in the store</returns>
+ 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);
+
+ //Async get the number of records of the given entity type
+ long count = await ctx.Set<T>().LongCountAsync(cancellation);
+
+ //close db and transaction
+ await ctx.SaveAndCloseAsync(true, cancellation);
+
+ return count;
+ }
+
+ /// <summary>
+ /// Gets the number of records that belong to the specified constraint
+ /// </summary>
+ /// <param name="store"></param>
+ /// <param name="specifier">A specifier to constrain the reults</param>
+ /// <param name="cancellation">A cancellation token to cancel the operation</param>
+ /// <returns>The number of records that belong to the specifier</returns>
+ 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);
+
+ //Async get the number of records of the given entity type
+ long count = await store.QueryTable.GetCountQueryBuilder(ctx, specifier).LongCountAsync(cancellation);
+
+ //close db and transaction
+ await ctx.SaveAndCloseAsync(true, cancellation);
+
+ return count;
+ }
+
+
+ /// <summary>
+ /// Gets a record from its key
+ /// </summary>
+ /// <param name="store"></param>
+ /// <param name="key">The key identifying the unique record</param>
+ /// <param name="cancellation">A cancellation token to cancel the operation</param>
+ /// <returns>A promise that resolves the record identified by the specified key</returns>
+ 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);
+
+ //Get the single template by its id
+ T? record = await (from entry in ctx.Set<T>()
+ where entry.Id == key
+ select entry)
+ .AsNoTracking()
+ .SingleOrDefaultAsync(cancellation);
+
+ //close db and transaction
+ await ctx.SaveAndCloseAsync(true, cancellation);
+ return record;
+ }
+
+ /// <summary>
+ /// Gets a record identified by it's id
+ /// </summary>
+ /// <param name="store"></param>
+ /// <param name="specifiers">A variable length specifier arguemnt array for retreiving a single application</param>
+ /// <returns>A task that resolves the entity if it exists</returns>
+ 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);
+
+ //Get the single item by specifiers
+ T? record = await store.QueryTable.GetSingleQueryBuilder(ctx, specifiers).SingleOrDefaultAsync();
+
+ //close db and transaction
+ await ctx.SaveAndCloseAsync(true);
+
+ return record;
+ }
+
+ /// <summary>
+ /// Gets a record from the store with a partial model, intended to complete the model
+ /// </summary>
+ /// <param name="store"></param>
+ /// <param name="record">The partial model used to query the store</param>
+ /// <param name="cancellation">A cancellation token to cancel the operation</param>
+ /// <returns>A task the resolves the completed data-model</returns>
+ 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);
+
+ //Get the single template by its id
+ T? entry = await store.QueryTable.GetSingleQueryBuilder(ctx, record).SingleOrDefaultAsync(cancellation);
+
+ //close db and transaction
+ await ctx.SaveAndCloseAsync(true, cancellation);
+
+ return record;
+ }
+
+
+ /// <summary>
+ /// Fills a collection with enires retireved from the store using the specifer
+ /// </summary>
+ /// <param name="store"></param>
+ /// <param name="collection">The collection to add entires to</param>
+ /// <param name="specifier">A specifier argument to constrain results</param>
+ /// <param name="limit">The maximum number of elements to retrieve</param>
+ /// <param name="cancellation">A cancellation token to cancel the operation</param>
+ /// <returns>A Task the resolves to the number of items added to the collection</returns>
+ public static async Task<ERRNO> GetCollectionAsync<T>(this IDataStore<T> store, ICollection<T> collection, string specifier, int limit, CancellationToken cancellation = default)
+ where T : class, IDbModel
+ {
+ int previous = collection.Count;
+
+ //Open new db context
+ await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.ReadUncommitted, cancellation);
+
+ //Get the single template by its id
+ await store.QueryTable.GetCollectionQueryBuilder(ctx, specifier)
+ .Take(limit)
+ .Select(static e => e)
+ .AsNoTracking()
+ .ForEachAsync(collection.Add, cancellation);
+
+ //close db and transaction
+ _ = await ctx.SaveAndCloseAsync(true, cancellation);
+
+ //Return the number of elements add to the collection
+ return collection.Count - previous;
+ }
+
+ /// <summary>
+ /// Fills a collection with enires retireved from the store using a variable length specifier
+ /// parameter
+ /// </summary>
+ /// <param name="store"></param>
+ /// <param name="collection">The collection to add entires to</param>
+ /// <param name="limit">The maximum number of elements to retrieve</param>
+ /// <param name="args"></param>
+ /// <returns>A Task the resolves to the number of items added to the collection</returns>
+ public static async Task<ERRNO> GetCollectionAsync<T>(this IDataStore<T> store, ICollection<T> collection, int limit, params string[] args)
+ where T : class, IDbModel
+ {
+ int previous = collection.Count;
+
+ //Open new db context
+ await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.ReadUncommitted);
+
+ //Get the single template by its id
+ await store.QueryTable.GetCollectionQueryBuilder(ctx, args)
+ .Take(limit)
+ .Select(static e => e)
+ .AsNoTracking()
+ .ForEachAsync(collection.Add);
+
+ //close db and transaction
+ _ = await ctx.SaveAndCloseAsync(true);
+
+ //Return the number of elements add to the collection
+ return collection.Count - previous;
+ }
+
+
+ /// <summary>
+ /// Deletes one or more entrires from the store matching the specified record
+ /// </summary>
+ /// <param name="store"></param>
+ /// <param name="record">The record to remove from the store</param>
+ /// <param name="cancellation">A cancellation token to cancel the operation</param>
+ /// <returns>A task the resolves the number of records removed(should evaluate to false on failure, and deleted count on success)</returns>
+ public static async Task<ERRNO> DeleteAsync<T>(this IDataStore<T> store, T record, CancellationToken cancellation = default)
+ where T : class, IDbModel
+ {
+ //Open new db context
+ await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.RepeatableRead, cancellation);
+
+ //Get a query for a a single item
+ IQueryable<T> query = store.QueryTable.GetSingleQueryBuilder(ctx, record);
+
+ //Get the entry if it exists
+ T? entry = await query.SingleOrDefaultAsync(cancellation);
+
+ if (entry == null)
+ {
+ await ctx.SaveAndCloseAsync(false, cancellation);
+ return false;
+ }
+ else
+ {
+ //Remove the entry
+ ctx.Remove(entry);
+ return await ctx.SaveAndCloseAsync(true, cancellation);
+ }
+ }
+
+ /// <summary>
+ /// Deletes one or more entires from the store matching the specified unique key
+ /// </summary>
+ /// <param name="store"></param>
+ /// <param name="key">The unique key that identifies the record</param>
+ /// <param name="cancellation">A cancellation token to cancel the operation</param>
+ /// <returns>A task the resolves the number of records removed(should evaluate to false on failure, and deleted count on success)</returns>
+ public static async Task<ERRNO> DeleteAsync<T>(this IDataStore<T> store, string key, CancellationToken cancellation = default)
+ where T : class, IDbModel
+ {
+ //Open new db context
+ await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.RepeatableRead, cancellation);
+
+ //Get a query for a a single item
+ IQueryable<T> query = store.QueryTable.GetSingleQueryBuilder(ctx, key);
+
+ //Get the entry if it exists
+ T? entry = await query.SingleOrDefaultAsync(cancellation);
+
+ if (entry == null)
+ {
+ await ctx.SaveAndCloseAsync(false, cancellation);
+ return false;
+ }
+ else
+ {
+ //Remove the entry
+ ctx.Remove(entry);
+ return await ctx.SaveAndCloseAsync(true, cancellation);
+ }
+ }
+
+ /// <summary>
+ /// Deletes one or more entires from the store matching the supplied specifiers
+ /// </summary>
+ /// <param name="store"></param>
+ /// <param name="specifiers">A variable length array of specifiers used to delete one or more entires</param>
+ /// <returns>A task the resolves the number of records removed(should evaluate to false on failure, and deleted count on success)</returns>
+ public static async Task<ERRNO> DeleteAsync<T>(this IDataStore<T> store, params string[] specifiers)
+ where T : class, IDbModel
+ {
+ //Open new db context
+ await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.RepeatableRead);
+
+ //Get the template by its id
+ IQueryable<T> query = store.QueryTable.DeleteQueryBuilder(ctx, specifiers);
+
+ T? entry = await query.SingleOrDefaultAsync();
+
+ if (entry == null)
+ {
+ return false;
+ }
+
+ //Add the new application
+ ctx.Remove(entry);
+
+ return await ctx.SaveAndCloseAsync(true);
+ }
+
+
+ /// <summary>
+ /// Gets a collection of records using a pagination style query, and adds the records to the collecion
+ /// </summary>
+ /// <param name="store"></param>
+ /// <param name="collection">The collection to add records to</param>
+ /// <param name="page">Pagination page to get records from</param>
+ /// <param name="limit">The maximum number of items to retrieve from the store</param>
+ /// <param name="cancellation">A cancellation token to cancel the operation</param>
+ /// <returns>A task that resolves the number of items added to the collection</returns>
+ public static async Task<int> GetPageAsync<T>(this IDataStore<T> store, ICollection<T> collection, int page, int limit, CancellationToken cancellation = default)
+ where T : class, IDbModel
+ {
+ //Store preivous count
+ int previous = collection.Count;
+
+ //Open db connection
+ await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.ReadUncommitted, cancellation);
+
+ //Get a page offset and a limit for the
+ await ctx.Set<T>()
+ .Skip(page * limit)
+ .Take(limit)
+ .Select(static p => p)
+ .AsNoTracking()
+ .ForEachAsync(collection.Add, cancellation);
+
+ //close db and transaction
+ await ctx.SaveAndCloseAsync(true, cancellation);
+
+ //Return the number of records added
+ return collection.Count - previous;
+ }
+
+ /// <summary>
+ /// Gets a collection of records using a pagination style query with constraint arguments, and adds the records to the collecion
+ /// </summary>
+ /// <param name="store"></param>
+ /// <param name="collection">The collection to add records to</param>
+ /// <param name="page">Pagination page to get records from</param>
+ /// <param name="limit">The maximum number of items to retrieve from the store</param>
+ /// <param name="constraints">A params array of strings to constrain the result set from the db</param>
+ /// <returns>A task that resolves the number of items added to the collection</returns>
+ 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
+ {
+ //Store preivous count
+ int previous = collection.Count;
+
+ //Open new db context
+ await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.ReadUncommitted);
+
+ //Get a page of records constrained by the given arguments
+ await store.QueryTable.GetPageQueryBuilder(ctx, constraints)
+ .Skip(page * limit)
+ .Take(limit)
+ .Select(static e => e)
+ .AsNoTracking()
+ .ForEachAsync(collection.Add);
+
+ //close db and transaction
+ await ctx.SaveAndCloseAsync(true);
+
+ //Return the number of records added
+ 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
+ {
+ //Assign user-id when numerated
+ IEnumerable<T> withUserId = records.Select(p =>
+ {
+ p.UserId = userId;
+ return p;
+ });
+
+ return store.AddBulkAsync(withUserId, overwriteTime, cancellation);
+ }
+
+ public static async Task<ERRNO> AddBulkAsync<T>(this IDataStore<T> store, IEnumerable<T> records, bool overwriteTime = true, CancellationToken cancellation = default)
+ where T : class, IDbModel
+ {
+ DateTime now = DateTime.UtcNow;
+
+ //Open context and transaction
+ await using IDbContextHandle database = await store.OpenAsync(IsolationLevel.ReadCommitted, cancellation);
+
+ //Get the entity set
+ IQueryable<T> set = database.Set<T>();
+
+ //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
new file mode 100644
index 0000000..55230cf
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Data/src/Extensions/DbStoreHelperExtensions.cs
@@ -0,0 +1,89 @@
+/*
+* 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/ProtectedEntityExtensions.cs b/lib/VNLib.Plugins.Extensions.Data/src/Extensions/ProtectedEntityExtensions.cs
index ec7b4f5..ebb71cf 100644
--- a/lib/VNLib.Plugins.Extensions.Data/src/ProtectedEntityExtensions.cs
+++ b/lib/VNLib.Plugins.Extensions.Data/src/Extensions/ProtectedEntityExtensions.cs
@@ -22,17 +22,18 @@
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
-using System.Linq;
using System.Threading;
-using System.Transactions;
using System.Threading.Tasks;
using System.Collections.Generic;
using VNLib.Utils;
using VNLib.Plugins.Extensions.Data.Abstractions;
-namespace VNLib.Plugins.Extensions.Data
+namespace VNLib.Plugins.Extensions.Data.Extensions
{
+ /// <summary>
+ /// Extension methods for <see cref="IDataStore{TEntity}"/> implementations that support user-protected entities
+ /// </summary>
public static class ProtectedEntityExtensions
{
/// <summary>
@@ -43,7 +44,7 @@ namespace VNLib.Plugins.Extensions.Data
/// <param name="userId">The userid of the record owner</param>
/// <param name="cancellation">A token to cancel the operation</param>
/// <returns>A task that evaluates to the number of records modified</returns>
- public static Task<ERRNO> UpdateUserRecordAsync<TEntity>(this IDataStore<TEntity> store, TEntity record, string userId, CancellationToken cancellation = default)
+ public static Task<ERRNO> UpdateUserRecordAsync<TEntity>(this IDataStore<TEntity> store, TEntity record, string userId, CancellationToken cancellation = default)
where TEntity : class, IDbModel, IUserEntity
{
record.UserId = userId;
@@ -58,7 +59,7 @@ namespace VNLib.Plugins.Extensions.Data
/// <param name="userId">The userid of the record owner</param>
/// <param name="cancellation">A token to cancel the operation</param>
/// <returns>A task that evaluates to the number of records modified</returns>
- public static Task<ERRNO> CreateUserRecordAsync<TEntity>(this IDataStore<TEntity> store, TEntity record, string userId, CancellationToken cancellation = default)
+ public static Task<ERRNO> CreateUserRecordAsync<TEntity>(this IDataStore<TEntity> store, TEntity record, string userId, CancellationToken cancellation = default)
where TEntity : class, IDbModel, IUserEntity
{
record.UserId = userId;
@@ -88,7 +89,7 @@ namespace VNLib.Plugins.Extensions.Data
/// <param name="page">The page offset</param>
/// <param name="limit">The record limit for the page</param>
/// <returns>A task that resolves the number of entities added to the collection</returns>
- public static Task<int> GetUserPageAsync<TEntity>(this IPaginatedDataStore<TEntity> store, ICollection<TEntity> collection, string userId, int page, int limit)
+ public static Task<int> GetUserPageAsync<TEntity>(this IDataStore<TEntity> store, ICollection<TEntity> collection, string userId, int page, int limit)
where TEntity : class, IDbModel, IUserEntity
{
return store.GetPageAsync(collection, page, limit, userId);
@@ -120,25 +121,5 @@ namespace VNLib.Plugins.Extensions.Data
return store.GetCountAsync(userId, cancellation);
}
- /// <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);
- }
- }
}
}
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/ProtectedDbStore.cs b/lib/VNLib.Plugins.Extensions.Data/src/ProtectedDbStore.cs
deleted file mode 100644
index 8e85cbb..0000000
--- a/lib/VNLib.Plugins.Extensions.Data/src/ProtectedDbStore.cs
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
-* Copyright (c) 2022 Vaughn Nugent
-*
-* Library: VNLib
-* Package: VNLib.Plugins.Extensions.Data
-* File: ProtectedDbStore.cs
-*
-* ProtectedDbStore.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 VNLib.Plugins.Extensions.Data.Abstractions;
-
-namespace VNLib.Plugins.Extensions.Data
-{
-#nullable enable
- /// <summary>
- /// A data store that provides unique identities and protections based on an entity that has an owner <see cref="IUserEntity"/>
- /// </summary>
- public abstract class ProtectedDbStore<TEntity> : DbStore<TEntity> where TEntity : class, IDbModel, IUserEntity
- {
- ///<inheritdoc/>
- protected override IQueryable<TEntity> GetCollectionQueryBuilder(TransactionalDbContext context, params string[] constraints)
- {
- string userId = constraints[0];
- //Query items for the user and its id
- return from item in context.Set<TEntity>()
- where item.UserId == userId
- orderby item.Created descending
- select item;
- }
-
- /// <summary>
- /// Gets a single item contrained by a given user-id and item id
- /// </summary>
- /// <param name="context"></param>
- /// <param name="constraints"></param>
- /// <returns></returns>
- protected override IQueryable<TEntity> GetSingleQueryBuilder(TransactionalDbContext context, params string[] constraints)
- {
- string key = constraints[0];
- string userId = constraints[1];
- //Query items for the user and its id
- return from item in context.Set<TEntity>()
- where item.Id == key && item.UserId == userId
- select item;
- }
- ///<inheritdoc/>
- protected override IQueryable<TEntity> GetSingleQueryBuilder(TransactionalDbContext context, TEntity record)
- {
- return this.GetSingleQueryBuilder(context, record.Id, record.UserId);
- }
- }
-}
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWDecriptorCreationException.cs b/lib/VNLib.Plugins.Extensions.Data/src/Storage/Exceptions/LWDecriptorCreationException.cs
index 7ea50d0..7ea50d0 100644
--- a/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWDecriptorCreationException.cs
+++ b/lib/VNLib.Plugins.Extensions.Data/src/Storage/Exceptions/LWDecriptorCreationException.cs
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageRemoveFailedException.cs b/lib/VNLib.Plugins.Extensions.Data/src/Storage/Exceptions/LWStorageRemoveFailedException.cs
index d91019c..d91019c 100644
--- a/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageRemoveFailedException.cs
+++ b/lib/VNLib.Plugins.Extensions.Data/src/Storage/Exceptions/LWStorageRemoveFailedException.cs
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageUpdateFailedException.cs b/lib/VNLib.Plugins.Extensions.Data/src/Storage/Exceptions/LWStorageUpdateFailedException.cs
index f13792d..f13792d 100644
--- a/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageUpdateFailedException.cs
+++ b/lib/VNLib.Plugins.Extensions.Data/src/Storage/Exceptions/LWStorageUpdateFailedException.cs
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Storage/UndefinedBlobStateException.cs b/lib/VNLib.Plugins.Extensions.Data/src/Storage/Exceptions/UndefinedBlobStateException.cs
index 8b4d81b..8b4d81b 100644
--- a/lib/VNLib.Plugins.Extensions.Data/src/Storage/UndefinedBlobStateException.cs
+++ b/lib/VNLib.Plugins.Extensions.Data/src/Storage/Exceptions/UndefinedBlobStateException.cs
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Storage/FsExtensions.cs b/lib/VNLib.Plugins.Extensions.Data/src/Storage/FsExtensions.cs
new file mode 100644
index 0000000..8f5c099
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Data/src/Storage/FsExtensions.cs
@@ -0,0 +1,72 @@
+/*
+* Copyright (c) 2023 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Data
+* File: FsExtensions.cs
+*
+* FsExtensions.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.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace VNLib.Plugins.Extensions.Data.Storage
+{
+ /// <summary>
+ /// Contains filesystem extension methods
+ /// </summary>
+ public static class FsExtensions
+ {
+ /// <summary>
+ /// Creates a new scope for the given filesystem. All operations will be offset by the given path
+ /// within the parent filesystem.
+ /// </summary>
+ /// <param name="fs"></param>
+ /// <param name="offsetPath">The base path to prepend to all requests</param>
+ /// <returns>A new <see cref="ISimpleFilesystem"/> with a new filesystem directory scope</returns>
+ public static ISimpleFilesystem CreateNewScope(this ISimpleFilesystem fs, string offsetPath) => new FsScope(fs, offsetPath);
+
+ private sealed record class FsScope(ISimpleFilesystem Parent, string OffsetPath) : ISimpleFilesystem
+ {
+ public Task DeleteFileAsync(string filePath, CancellationToken cancellation)
+ {
+ string path = Path.Combine(OffsetPath, filePath);
+ return Parent.DeleteFileAsync(path, cancellation);
+ }
+
+ public string GetExternalFilePath(string filePath)
+ {
+ string path = Path.Combine(OffsetPath, filePath);
+ return Parent.GetExternalFilePath(path);
+ }
+
+ public Task<long> ReadFileAsync(string filePath, Stream output, CancellationToken cancellation)
+ {
+ string path = Path.Combine(OffsetPath, filePath);
+ return Parent.ReadFileAsync(path, output, cancellation);
+ }
+
+ public Task SetFileAsync(string filePath, Stream data, string contentType, CancellationToken cancellation)
+ {
+ string path = Path.Combine(OffsetPath, filePath);
+ return Parent.SetFileAsync(path, data, contentType, cancellation);
+ }
+ }
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Storage/ISimpleFilesystem.cs b/lib/VNLib.Plugins.Extensions.Data/src/Storage/ISimpleFilesystem.cs
new file mode 100644
index 0000000..d3dc431
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Data/src/Storage/ISimpleFilesystem.cs
@@ -0,0 +1,72 @@
+/*
+* Copyright (c) 2023 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Data
+* File: ISimpleFilesystem.cs
+*
+* ISimpleFilesystem.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.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace VNLib.Plugins.Extensions.Data.Storage
+{
+
+ /// <summary>
+ /// Represents an opaque storage interface that abstracts simple storage operations
+ /// ignorant of the underlying storage system.
+ /// </summary>
+ public interface ISimpleFilesystem
+ {
+ /// <summary>
+ /// Gets the full public file path for the given relative file path
+ /// </summary>
+ /// <param name="filePath">The relative file path of the item to get the full path for</param>
+ /// <returns>The full relative file path</returns>
+ string GetExternalFilePath(string filePath);
+
+ /// <summary>
+ /// Deletes a file from the storage system asynchronously
+ /// </summary>
+ /// <param name="filePath">The path to the file to delete</param>
+ /// <param name="cancellation">A token to cancel the operation</param>
+ /// <returns>A task that represents and asynchronous work</returns>
+ Task DeleteFileAsync(string filePath, CancellationToken cancellation);
+
+ /// <summary>
+ /// Writes a file from the stream to the given file location
+ /// </summary>
+ /// <param name="filePath">The path to the file to write to</param>
+ /// <param name="data">The file data to stream</param>
+ /// <param name="contentType">The content type of the file to write</param>
+ /// <param name="cancellation">A token to cancel the operation</param>
+ /// <returns>A task that represents and asynchronous work</returns>
+ Task SetFileAsync(string filePath, Stream data, string contentType, CancellationToken cancellation);
+
+ /// <summary>
+ /// Reads a file from the storage system at the given path asynchronously
+ /// </summary>
+ /// <param name="filePath">The file to read</param>
+ /// <param name="output">The stream to write the file output to</param>
+ /// <param name="cancellation">A token to cancel the operation</param>
+ /// <returns>The number of bytes read, -1 if the operation failed</returns>
+ Task<long> ReadFileAsync(string filePath, Stream output, CancellationToken cancellation);
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/TransactionalDbContext.cs b/lib/VNLib.Plugins.Extensions.Data/src/TransactionalDbContext.cs
index e06ac43..cbb9799 100644
--- a/lib/VNLib.Plugins.Extensions.Data/src/TransactionalDbContext.cs
+++ b/lib/VNLib.Plugins.Extensions.Data/src/TransactionalDbContext.cs
@@ -23,24 +23,31 @@
*/
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
+ public abstract class TransactionalDbContext : DbContext, IAsyncDisposable, ITransactionalDbContext, IDbContextHandle
{
/// <summary>
/// <inheritdoc/>
/// </summary>
protected TransactionalDbContext()
{ }
+
/// <summary>
/// <inheritdoc/>
/// </summary>
@@ -50,46 +57,83 @@ namespace VNLib.Plugins.Extensions.Data
///<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);
+ }
-#pragma warning disable CA1816 // Dispose methods should call SuppressFinalize, ignore because base.Dispose() is called
///<inheritdoc/>
- public sealed override void Dispose()
+ public Task CommitTransactionAsync(CancellationToken token = default)
{
- //dispose the transaction
- Transaction?.Dispose();
- base.Dispose();
+ return Transaction != null ? Transaction.CommitAsync(token) : Task.CompletedTask;
}
///<inheritdoc/>
- public override async ValueTask DisposeAsync()
+ public Task RollbackTransctionAsync(CancellationToken token = default)
{
- //If transaction has been created, dispose the transaction
- if (Transaction != null)
+ 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 Transaction.DisposeAsync();
+ await CommitTransactionAsync(cancellation);
}
- await base.DisposeAsync();
+ else
+ {
+ await RollbackTransctionAsync(cancellation);
+ }
+ return result;
}
-#pragma warning restore CA1816 // Dispose methods should call SuppressFinalize
///<inheritdoc/>
- public async Task OpenTransactionAsync(CancellationToken cancellationToken = default)
+ public virtual void RemoveRange<T>(IEnumerable<T> entities) where T : class
{
- //open a new transaction on the current database
- this.Transaction = await base.Database.BeginTransactionAsync(cancellationToken);
+ 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 Task CommitTransactionAsync(CancellationToken token = default)
+ public sealed override void Dispose()
{
- return Transaction != null ? Transaction.CommitAsync(token) : Task.CompletedTask;
+ //dispose the transaction
+ Transaction?.Dispose();
+ base.Dispose();
}
///<inheritdoc/>
- public Task RollbackTransctionAsync(CancellationToken token = default)
+ public override async ValueTask DisposeAsync()
{
- return Transaction != null ? Transaction.RollbackAsync(token) : Task.CompletedTask;
+ //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 05a6df2..f89bf00 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
@@ -23,14 +23,6 @@
<PackageProjectUrl>https://www.vaughnnugent.com/resources/software/modules/VNLib.Plugins.Extensions</PackageProjectUrl>
<RepositoryUrl>https://github.com/VnUgE/VNLib.Plugins.Extensions/tree/master/lib/VNLib.Plugins.Extensions.Data</RepositoryUrl>
</PropertyGroup>
-
- <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
- <Deterministic>False</Deterministic>
- </PropertyGroup>
-
- <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
- <Deterministic>False</Deterministic>
- </PropertyGroup>
<ItemGroup>
<PackageReference Include="ErrorProne.NET.CoreAnalyzers" Version="0.1.2">
@@ -41,7 +33,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.20" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.21" />
</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 8775d54..0ea60d6 100644
--- a/lib/VNLib.Plugins.Extensions.Loading.Sql/src/SqlDbConnectionLoader.cs
+++ b/lib/VNLib.Plugins.Extensions.Loading.Sql/src/SqlDbConnectionLoader.cs
@@ -124,7 +124,7 @@ namespace VNLib.Plugins.Extensions.Loading.Sql
sqlBuilder = new MySqlConnectionStringBuilder()
{
Server = sqlConf["hostname"].GetString(),
- Database = sqlConf["database"].GetString(),
+ Database = sqlConf["catalog"].GetString(),
UserID = sqlConf["username"].GetString(),
Password = password?.Result.ToString(),
Pooling = true,
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 abfbfb6..d8ab72e 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
@@ -33,8 +33,8 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.20" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.20" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.21" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.21" />
<PackageReference Include="MySqlConnector" Version="2.2.7" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="6.0.2" />
</ItemGroup>
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs b/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs
index adfd997..ccb2341 100644
--- a/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs
@@ -167,6 +167,52 @@ namespace VNLib.Plugins.Extensions.Loading
}
/// <summary>
+ /// Gets a required configuration property from the specified configuration scope
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="config"></param>
+ /// <param name="property">The name of the property to get</param>
+ /// <param name="getter">A function to get the value from the json type</param>
+ /// <returns>The property value</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ public static T? GetProperty<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));
+
+ return !config.TryGetValue(property, out JsonElement el) ? default : getter(el);
+ }
+
+ /// <summary>
+ /// Gets a required configuration property from the specified configuration scope
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="config"></param>
+ /// <param name="property">The name of the property to get</param>
+ /// <param name="getter">A function to get the value from the json type</param>
+ /// <returns>The property value</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="KeyNotFoundException"></exception>
+ 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));
+
+ //Get the property
+ if(!config.TryGetValue(property, out JsonElement el))
+ {
+ throw new KeyNotFoundException($"Missing required configuration property '{property}'");
+ }
+
+ //Even if the getter returns null, throw
+ return getter(el) ?? throw new KeyNotFoundException($"Missing required configuration property '{property}'");
+ }
+
+ /// <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/Events/EventManagment.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Events/EventManagment.cs
index 57e4a9c..40a52fc 100644
--- a/lib/VNLib.Plugins.Extensions.Loading/src/Events/EventManagment.cs
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/Events/EventManagment.cs
@@ -57,7 +57,7 @@ namespace VNLib.Plugins.Extensions.Loading.Events
{
plugin.ThrowIfUnloaded();
- plugin.Log.Verbose("Interval for {t} scheduled", interval);
+ plugin.Log.Verbose("Interval for {t} scheduled on type {rr}", interval, asyncCallback.Target);
//Run interval on plugins bg scheduler
_ = plugin.ObserveWork(() => RunIntervalOnPluginScheduler(plugin, asyncCallback, interval, immediate));
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/SecretResult.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/SecretResult.cs
index f2cbd28..c1e6b3d 100644
--- a/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/SecretResult.cs
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/SecretResult.cs
@@ -42,7 +42,14 @@ namespace VNLib.Plugins.Extensions.Loading
public ReadOnlySpan<char> Result => _secretChars;
- internal SecretResult(ReadOnlySpan<char> value) => _secretChars = value.ToArray();
+ internal SecretResult(ReadOnlySpan<char> value) : this(value.ToArray())
+ { }
+
+ private SecretResult(char[] secretChars)
+ {
+ _secretChars = secretChars;
+ }
+
///<inheritdoc/>
protected override void Free()
@@ -56,5 +63,7 @@ namespace VNLib.Plugins.Extensions.Loading
MemoryUtil.UnsafeZeroMemory<char>(result);
return res;
}
+
+ internal static SecretResult ToSecret(char[] result) => new(result);
}
}
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/VaultSecrets.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/VaultSecrets.cs
index 08af485..c2d830f 100644
--- a/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/VaultSecrets.cs
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/VaultSecrets.cs
@@ -23,9 +23,11 @@
*/
using System;
+using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
+using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Security.Cryptography.X509Certificates;
@@ -62,6 +64,7 @@ namespace VNLib.Plugins.Extensions.Loading
public const string VAULT_URL_SCHEME = "vault://";
public const string ENV_URL_SCHEME = "env://";
+ public const string FILE_URL_SCHEME = "file://";
/// <summary>
@@ -125,11 +128,36 @@ namespace VNLib.Plugins.Extensions.Loading
return Task.FromResult<ISecretResult?>(envVal == null ? null : new SecretResult(envVal));
}
+
+ //See if the secret is a file path
+ if (rawSecret.StartsWith(FILE_URL_SCHEME, StringComparison.OrdinalIgnoreCase))
+ {
+ string filePath = rawSecret[FILE_URL_SCHEME.Length..];
+ return GetSecretFromFileAsync(filePath, plugin.UnloadToken);
+ }
//Finally, return the raw value
return Task.FromResult<ISecretResult?>(new SecretResult(rawSecret.AsSpan()));
}
+ private static async Task<ISecretResult?> GetSecretFromFileAsync(string filePath, CancellationToken ct)
+ {
+ //read the file data
+ byte[] secretFileData = await File.ReadAllBytesAsync(filePath, ct);
+
+ //recover the character data from the file data
+ int chars = Encoding.UTF8.GetCharCount(secretFileData);
+ char[] secretFileChars = new char[chars];
+ Encoding.UTF8.GetChars(secretFileData, secretFileChars);
+
+ //Create secret from the file data
+ SecretResult sr = SecretResult.ToSecret(secretFileChars);
+
+ //Clear file data buffer
+ MemoryUtil.InitializeBlock(secretFileData.AsSpan());
+ return sr;
+ }
+
/// <summary>
/// Gets a secret at the given vault url (in the form of "vault://[mount-name]/[secret-path]?secret=[secret_name]")
/// </summary>
diff --git a/lib/VNLib.Plugins.Extensions.Validation/src/VNLib.Plugins.Extensions.Validation.csproj b/lib/VNLib.Plugins.Extensions.Validation/src/VNLib.Plugins.Extensions.Validation.csproj
index 1747a7d..8b9b6a1 100644
--- a/lib/VNLib.Plugins.Extensions.Validation/src/VNLib.Plugins.Extensions.Validation.csproj
+++ b/lib/VNLib.Plugins.Extensions.Validation/src/VNLib.Plugins.Extensions.Validation.csproj
@@ -34,7 +34,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="FluentValidation" Version="11.6.0" />
+ <PackageReference Include="FluentValidation" Version="11.7.1" />
</ItemGroup>
<ItemGroup>