diff options
-rw-r--r-- | .gitignore | 9 | ||||
-rw-r--r-- | Module.Taskfile.yaml | 68 | ||||
-rw-r--r-- | Taskfile.yaml | 75 | ||||
-rw-r--r-- | lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDataStore.cs | 34 | ||||
-rw-r--r-- | lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IPaginatedDataStore.cs | 8 | ||||
-rw-r--r-- | lib/VNLib.Plugins.Extensions.Data/src/DbStore.cs | 382 | ||||
-rw-r--r-- | lib/VNLib.Plugins.Extensions.Data/src/DbStoreHelperExtensions.cs | 82 | ||||
-rw-r--r-- | lib/VNLib.Plugins.Extensions.Data/src/DbStoreQueries.cs | 153 | ||||
-rw-r--r-- | lib/VNLib.Plugins.Extensions.Data/src/IConcurrentDbContext.cs | 44 | ||||
-rw-r--r-- | lib/VNLib.Plugins.Extensions.Data/src/ProtectedEntityExtensions.cs | 43 | ||||
-rw-r--r-- | vnlib.plugins.extensions.build.sln | 56 |
11 files changed, 685 insertions, 269 deletions
@@ -362,11 +362,4 @@ MigrationBackup/ # Fody - auto-generated XML schema FodyWeavers.xsd -*.licenseheader -/lib/.task/checksum - -*[Tt]askfile.* - -*.sln - -*.ps1 +*.licenseheader
\ No newline at end of file diff --git a/Module.Taskfile.yaml b/Module.Taskfile.yaml new file mode 100644 index 0000000..d729ea3 --- /dev/null +++ b/Module.Taskfile.yaml @@ -0,0 +1,68 @@ +# https://taskfile.dev + +#Called by the vnbuild system to produce builds for my website +#https://www.vaughnnugent.com/resources/software + +#This taskfile performs the build operations for a module, it handles +#git code updates, msbuild on solutions, and sleet NuGet feed pushes. + +#this file must be in the same directory as the solution file + +version: '3' + +vars: + INT_DIR: '{{.SCRATCH_DIR}}/obj/{{.MODULE_NAME}}/' + MS_ARGS: '/p:RunAnalyzersDuringBuild=false /p:IntermediateOutputPath="{{.INT_DIR}}" /p:UseCommonOutputDirectory=true /p:BuildInParallel=true /p:MultiProcessorCompilation=true' + PACK_OUT: '{{.OUTPUT_DIR}}/{{.HEAD_SHA}}/pkg' + +tasks: + +#called by build pipeline to sync repo + update: + cmds: + - git remote update + - git reset --hard + - git pull origin {{.BRANCH_NAME}} --verify-signatures + +#called by build pipeline to build module + build: + cmds: + - echo "building module {{.MODULE_NAME}}" + + #re-write semver after hard reset, before build + - dotnet-gitversion.exe /updateprojectfiles + + #build debug mode first + - task: build_debug + - task: build_release + + postbuild_success: + cmds: + #push packages to the sleet feed (feed path is vnbuild global) + - sleet push "{{.PACK_OUT}}/debug/" --source debug --config "{{.SLEET_CONFIG_PATH}}" --force + - sleet push "{{.PACK_OUT}}/release/" --source release --config "{{.SLEET_CONFIG_PATH}}" --force + + postbuild_failed: + cmds: + - echo "postbuild failed {{.MODULE_NAME}}" + +#called by build pipeline to clean module + clean: + cmds: + #clean solution + - dotnet clean /p:BuildInParallel=true /p:MultiProcessorCompilation=true + + +#Internal tasks + build_debug: + internal: true + cmds: + - dotnet publish -c debug {{.MS_ARGS}} + - dotnet pack -c debug {{.MS_ARGS}} -o "{{.PACK_OUT}}/debug/" + + build_release: + internal: true + cmds: + - dotnet publish -c release {{.MS_ARGS}} + - dotnet pack -c release {{.MS_ARGS}} -o "{{.PACK_OUT}}/release/" +
\ No newline at end of file diff --git a/Taskfile.yaml b/Taskfile.yaml new file mode 100644 index 0000000..b16b082 --- /dev/null +++ b/Taskfile.yaml @@ -0,0 +1,75 @@ +# https://taskfile.dev + +#Called by the vnbuild system to produce builds for my website +#https://www.vaughnnugent.com/resources/software + +#This taskfile is called from the root of a project that is being built +#and the purpose of this taskfile is to package up the output of a build +#from the solution file, and package it up into a tgz files for distribution + +version: '3' + +vars: + TARGET: '{{.USER_WORKING_DIR}}/bin' + RELEASE_DIR: "./bin/release/{{.TARGET_FRAMEWORK}}/publish" + SOURCE_OUT: "{{.USER_WORKING_DIR}}/bin/source" + +tasks: + + #when build succeeds, archive the output into a tgz + postbuild_success: + dir: '{{.USER_WORKING_DIR}}' + cmds: + #pack up source code + - task: packsource + + #run post in debug mode + - task: postbuild + vars: { BUILD_MODE: debug } + + #remove uncessary files from the release dir + - powershell -Command "Get-ChildItem -Recurse '{{.RELEASE_DIR}}/' -Include *.pdb,*.xml | Remove-Item" + + #run post in release mode + - task: postbuild + vars: { BUILD_MODE: release } + + + postbuild_failed: + dir: '{{.USER_WORKING_DIR}}' + cmds: + - echo "postbuild failed {{.PROJECT_NAME}}" + + + postbuild: + dir: '{{.USER_WORKING_DIR}}' + internal: true + vars: + #the build output directory + BUILD_OUT: "{{.USER_WORKING_DIR}}/bin/{{.BUILD_MODE}}/{{.TARGET_FRAMEWORK}}/publish" + + cmds: + + #copy license and readme to target + - cd .. && powershell -Command "Copy-Item -Path ./LICENSE.txt -Destination '{{.BUILD_OUT}}/license.txt'" + - cd .. && powershell -Command "Copy-Item -Path ./build.readme.md -Destination '{{.BUILD_OUT}}/readme.md'" + + #tar outputs + - cd "{{.BUILD_OUT}}" && tar -czf "{{.TARGET}}/{{.BUILD_MODE}}.tgz" . + + packsource: + dir: '{{.USER_WORKING_DIR}}' + internal: true + cmds: + #copy source code to target + - powershell -Command "Get-ChildItem -Include *.cs,*.csproj -Recurse | Where { \$_.FullName -notlike '*\obj\*' -and \$_.FullName -notlike '*\bin\*' } | Resolve-Path -Relative | tar --files-from - -czf '{{.TARGET}}/src.tgz'" + + +#Remove the output dirs on clean + clean: + dir: '{{.USER_WORKING_DIR}}' + cmds: + - cmd: powershell Remove-Item -Recurse './bin' + ignore_error: true + - cmd: powershell Remove-Item -Recurse './obj' + ignore_error: true diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDataStore.cs b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDataStore.cs index 4e2d682..1c8174c 100644 --- a/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDataStore.cs +++ b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDataStore.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Extensions.Data @@ -23,11 +23,13 @@ */ using System; +using System.Threading; using System.Threading.Tasks; using System.Collections.Generic; using VNLib.Utils; + namespace VNLib.Plugins.Extensions.Data.Abstractions { /// <summary> @@ -40,20 +42,23 @@ namespace VNLib.Plugins.Extensions.Data.Abstractions /// <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(); + 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); + Task<long> GetCountAsync(string specifier, CancellationToken cancellation = default); /// <summary> /// Gets a record from its key /// </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); + Task<T?> GetSingleAsync(string key, CancellationToken cancellation = default); /// <summary> /// Gets a record from its key /// </summary> @@ -64,16 +69,18 @@ namespace VNLib.Plugins.Extensions.Data.Abstractions /// 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); + 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); + 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 @@ -87,26 +94,30 @@ namespace VNLib.Plugins.Extensions.Data.Abstractions /// 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); + 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); + 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); + 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); + Task<ERRNO> DeleteAsync(string key, CancellationToken cancellation = default); /// <summary> /// Deletes one or more entires from the store matching the supplied specifiers /// </summary> @@ -117,7 +128,8 @@ namespace VNLib.Plugins.Extensions.Data.Abstractions /// Updates an entry in the store if it exists, or creates a new entry if one does not already exist /// </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); + Task<ERRNO> AddOrUpdateAsync(T record, CancellationToken cancellation = default); } } diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IPaginatedDataStore.cs b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IPaginatedDataStore.cs index 2ccb5ab..7c271eb 100644 --- a/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IPaginatedDataStore.cs +++ b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IPaginatedDataStore.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Extensions.Data @@ -22,8 +22,9 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; +using System.Collections.Generic; namespace VNLib.Plugins.Extensions.Data.Abstractions { @@ -40,8 +41,9 @@ namespace VNLib.Plugins.Extensions.Data.Abstractions /// <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); + 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> diff --git a/lib/VNLib.Plugins.Extensions.Data/src/DbStore.cs b/lib/VNLib.Plugins.Extensions.Data/src/DbStore.cs index 7beda55..761d78f 100644 --- a/lib/VNLib.Plugins.Extensions.Data/src/DbStore.cs +++ b/lib/VNLib.Plugins.Extensions.Data/src/DbStore.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Extensions.Data @@ -23,9 +23,11 @@ */ using System; -using System.Collections.Generic; using System.Linq; +using System.Threading; +using System.Transactions; using System.Threading.Tasks; +using System.Collections.Generic; using Microsoft.EntityFrameworkCore; @@ -35,16 +37,18 @@ using VNLib.Plugins.Extensions.Data.Abstractions; namespace VNLib.Plugins.Extensions.Data { + /// <summary> /// Implements basic data-store functionality with abstract query builders /// </summary> /// <typeparam name="T">A <see cref="DbModelBase"/> implemented type</typeparam> - public abstract class DbStore<T> : IDataStore<T>, IPaginatedDataStore<T> where T: class, IDbModel + public abstract partial class DbStore<T> : IDataStore<T>, IPaginatedDataStore<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> @@ -58,13 +62,13 @@ namespace VNLib.Plugins.Extensions.Data #region Add Or Update ///<inheritdoc/> - public virtual async Task<ERRNO> AddOrUpdateAsync(T record) + public virtual async Task<ERRNO> AddOrUpdateAsync(T record, CancellationToken cancellation = default) { //Open new db context - await using TransactionalDbContext ctx = NewContext(); - //Open transaction - await ctx.OpenTransactionAsync(); + await using TransactionalDbContext ctx = await this.OpenAsync(IsolationLevel.ReadCommitted, cancellation); + IQueryable<T> query; + if (string.IsNullOrWhiteSpace(record.Id)) { //Get the application @@ -77,8 +81,10 @@ namespace VNLib.Plugins.Extensions.Data where et.Id == record.Id select et); } + //Using single - T? entry = await query.SingleOrDefaultAsync(); + T? entry = await query.SingleOrDefaultAsync(cancellation); + //Check if creted if (entry == null) { @@ -92,394 +98,301 @@ namespace VNLib.Plugins.Extensions.Data else { OnRecordUpdate(record, entry); - } - //Save changes - ERRNO result = await ctx.SaveChangesAsync(); - if (result) - { - //commit transaction if update was successful - await ctx.CommitTransactionAsync(); - } - return result; + } + + return await ctx.SaveAndCloseAsync(cancellation); } + ///<inheritdoc/> - public virtual async Task<ERRNO> UpdateAsync(T record) + public virtual async Task<ERRNO> UpdateAsync(T record, CancellationToken cancellation = default) { //Open new db context - await using TransactionalDbContext ctx = NewContext(); - //Open transaction - await ctx.OpenTransactionAsync(); + 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(); + 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(); + await ctx.CommitTransactionAsync(cancellation); return true; } - //Save changes - ERRNO result = await ctx.SaveChangesAsync(); - if (result) - { - //commit transaction if update was successful - await ctx.CommitTransactionAsync(); - } - return result; + + return await ctx.SaveAndCloseAsync(cancellation); } + ///<inheritdoc/> - public virtual async Task<ERRNO> CreateAsync(T record) + public virtual async Task<ERRNO> CreateAsync(T record, CancellationToken cancellation = default) { //Open new db context - await using TransactionalDbContext ctx = NewContext(); - //Open transaction - await ctx.OpenTransactionAsync(); + 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); - //save changes - ERRNO result = await ctx.SaveChangesAsync(); - if (result) - { - //Commit transaction - await ctx.CommitTransactionAsync(); - } - return result; + + return await ctx.SaveAndCloseAsync(cancellation); } - - /// <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 - /// valid <see cref="DbModelBase.Id"/> property - /// </summary> - /// <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) - { - //default to get single of the specific record - return GetSingleQueryBuilder(context, record); - } - /// <summary> - /// Builds a query that attempts to get a single entry from the - /// store to update based on the specified record - /// </summary> - /// <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) - { - //default to get single of the specific record - return GetSingleQueryBuilder(context, record); - } - /// <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); + #endregion #region Delete + ///<inheritdoc/> - public virtual async Task<ERRNO> DeleteAsync(string key) + public virtual async Task<ERRNO> DeleteAsync(string key, CancellationToken cancellation = default) { //Open new db context - await using TransactionalDbContext ctx = NewContext(); - //Open transaction - await ctx.OpenTransactionAsync(); + 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(); + + T? record = await query.SingleOrDefaultAsync(cancellation); + if (record == null) { return false; } + //Add the new application ctx.Remove(record); - //Save changes - ERRNO result = await ctx.SaveChangesAsync(); - if (result) - { - //Commit transaction - await ctx.CommitTransactionAsync(); - } - return result; + + return await ctx.SaveAndCloseAsync(cancellation); } + ///<inheritdoc/> - public virtual async Task<ERRNO> DeleteAsync(T record) + public virtual async Task<ERRNO> DeleteAsync(T record, CancellationToken cancellation = default) { //Open new db context - await using TransactionalDbContext ctx = NewContext(); - //Open transaction - await ctx.OpenTransactionAsync(); + 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(); + T? entry = await query.SingleOrDefaultAsync(cancellation); + if (entry == null) { return false; } + //Add the new application ctx.Remove(entry); - //Save changes - ERRNO result = await ctx.SaveChangesAsync(); - if (result) - { - //Commit transaction - await ctx.CommitTransactionAsync(); - } - return result; + + return await ctx.SaveAndCloseAsync(cancellation); } + ///<inheritdoc/> public virtual async Task<ERRNO> DeleteAsync(params string[] specifiers) { //Open new db context - await using TransactionalDbContext ctx = NewContext(); - //Open transaction - await ctx.OpenTransactionAsync(); + 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); - //Save changes - ERRNO result = await ctx.SaveChangesAsync(); - if (result) - { - //Commit transaction - await ctx.CommitTransactionAsync(); - } - return result; - } - - /// <summary> - /// Builds a query that results in a single entry to delete from the - /// constraint arguments - /// </summary> - /// <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) - { - //default use the get-single method, as the implementation is usually identical - return GetSingleQueryBuilder(context, constraints); + + return await ctx.SaveAndCloseAsync(); } + #endregion #region Get Collection + ///<inheritdoc/> - public virtual async Task<ERRNO> GetCollectionAsync(ICollection<T> collection, string specifier, int limit) + 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 = NewContext(); - //Open transaction - await ctx.OpenTransactionAsync(); + 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); + await GetCollectionQueryBuilder(ctx, specifier) + .Take(limit) + .Select(static e => e) + .ForEachAsync(collection.Add, cancellation); + //close db and transaction - await ctx.CommitTransactionAsync(); + await ctx.CommitTransactionAsync(cancellation); + //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 = NewContext(); - //Open transaction - await ctx.OpenTransactionAsync(); - //Get the single template by its id - await GetCollectionQueryBuilder(ctx, args).Take(limit).Select(static e => e).ForEachAsync(collection.Add); + 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; } - /// <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> GetCollectionQueryBuilder(TransactionalDbContext context, string specifier) - { - return GetCollectionQueryBuilder(context, new string[] { specifier }); - } - - /// <summary> - /// Builds a query to get a collection of records based on an variable length array of parameters - /// </summary> - /// <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); - #endregion #region Get Count + ///<inheritdoc/> - public virtual async Task<long> GetCountAsync() + public virtual async Task<long> GetCountAsync(CancellationToken cancellation = default) { //Open db connection - await using TransactionalDbContext ctx = NewContext(); - //Open transaction - await ctx.OpenTransactionAsync(); + 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(); + long count = await ctx.Set<T>().LongCountAsync(cancellation); + //close db and transaction - await ctx.CommitTransactionAsync(); + await ctx.CommitTransactionAsync(cancellation); + return count; } + ///<inheritdoc/> - public virtual async Task<long> GetCountAsync(string specifier) + public virtual async Task<long> GetCountAsync(string specifier, CancellationToken cancellation) { - await using TransactionalDbContext ctx = NewContext(); - //Open transaction - await ctx.OpenTransactionAsync(); + 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(); + long count = await GetCountQueryBuilder(ctx, specifier).LongCountAsync(cancellation); + //close db and transaction - await ctx.CommitTransactionAsync(); + await ctx.CommitTransactionAsync(cancellation); + return count; } - /// <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) - { - //Default use the get collection and just call the count method - return GetCollectionQueryBuilder(context, specifier); - } + #endregion #region Get Single + ///<inheritdoc/> - public virtual async Task<T?> GetSingleAsync(string key) + public virtual async Task<T?> GetSingleAsync(string key, CancellationToken cancellation = default) { //Open db connection - await using TransactionalDbContext ctx = NewContext(); - //Open transaction - await ctx.OpenTransactionAsync(); + 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(); + .SingleOrDefaultAsync(cancellation); + //close db and transaction - await ctx.CommitTransactionAsync(); + await ctx.CommitTransactionAsync(cancellation); return record; } + ///<inheritdoc/> - public virtual async Task<T?> GetSingleAsync(T record) + public virtual async Task<T?> GetSingleAsync(T record, CancellationToken cancellation = default) { //Open db connection - await using TransactionalDbContext ctx = NewContext(); - //Open transaction - await ctx.OpenTransactionAsync(); + await using TransactionalDbContext ctx = await this.OpenAsync(IsolationLevel.ReadUncommitted, cancellation); + //Get the single template by its id - T? entry = await GetSingleQueryBuilder(ctx, record).SingleOrDefaultAsync(); + T? entry = await GetSingleQueryBuilder(ctx, record).SingleOrDefaultAsync(cancellation); + //close db and transaction - await ctx.CommitTransactionAsync(); + await ctx.CommitTransactionAsync(cancellation); + return record; } + ///<inheritdoc/> public virtual async Task<T?> GetSingleAsync(params string[] specifiers) { //Open db connection - await using TransactionalDbContext ctx = NewContext(); - //Open transaction - await ctx.OpenTransactionAsync(); + 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; } - /// <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); - /// <summary> - /// <para> - /// Builds a query to get a single record from the specified record. - /// </para> - /// <para> - /// Unless overridden, performs an ID based query for a single entry - /// </para> - /// </summary> - /// <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) - { - return from entry in context.Set<T>() - where entry.Id == record.Id - select entry; - } + #endregion #region Get Page + ///<inheritdoc/> - public virtual async Task<int> GetPageAsync(ICollection<T> collection, int page, int limit) + 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 = NewContext(); - //Open transaction - await ctx.OpenTransactionAsync(); + 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); + .ForEachAsync(collection.Add, cancellation); //close db and transaction - await ctx.CommitTransactionAsync(); + 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 = NewContext(); - //Open transaction - await ctx.OpenTransactionAsync(); - //Get the single template by its id + 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) @@ -488,20 +401,11 @@ namespace VNLib.Plugins.Extensions.Data //close db and transaction await ctx.CommitTransactionAsync(); + //Return the number of records added return collection.Count - previous; } - /// <summary> - /// Builds a query to get a collection of records based on an variable length array of parameters - /// </summary> - /// <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) - { - //Default to getting the entire collection and just selecting a single page - return GetCollectionQueryBuilder(context, constraints); - } + #endregion } } diff --git a/lib/VNLib.Plugins.Extensions.Data/src/DbStoreHelperExtensions.cs b/lib/VNLib.Plugins.Extensions.Data/src/DbStoreHelperExtensions.cs new file mode 100644 index 0000000..5971307 --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.Data/src/DbStoreHelperExtensions.cs @@ -0,0 +1,82 @@ +/* +* 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/DbStoreQueries.cs b/lib/VNLib.Plugins.Extensions.Data/src/DbStoreQueries.cs new file mode 100644 index 0000000..ff0319e --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.Data/src/DbStoreQueries.cs @@ -0,0 +1,153 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Data +* File: DbStoreQueries.cs +* +* DbStoreQueries.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; + +namespace VNLib.Plugins.Extensions.Data +{ + + public partial class DbStore<T> + { + + /// <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 + /// valid <see cref="DbModelBase.Id"/> property + /// </summary> + /// <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) + { + //default to get single of the specific record + return GetSingleQueryBuilder(context, record); + } + + /// <summary> + /// Builds a query that attempts to get a single entry from the + /// store to update based on the specified record + /// </summary> + /// <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) + { + //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 + /// </summary> + /// <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) + { + //default use the get-single method, as the implementation is usually identical + return GetSingleQueryBuilder(context, constraints); + } + + /// <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> GetCollectionQueryBuilder(TransactionalDbContext 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) + { + //Default use the get collection and just call the count method + return GetCollectionQueryBuilder(context, specifier); + } + + /// <summary> + /// <para> + /// Builds a query to get a single record from the specified record. + /// </para> + /// <para> + /// Unless overridden, performs an ID based query for a single entry + /// </para> + /// </summary> + /// <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) + { + return from entry in context.Set<T>() + where entry.Id == record.Id + select entry; + } + + /// <summary> + /// Builds a query to get a collection of records based on an variable length array of parameters + /// </summary> + /// <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) + { + //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); + + /// <summary> + /// Builds a query to get a collection of records based on an variable length array of parameters + /// </summary> + /// <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); + } +} diff --git a/lib/VNLib.Plugins.Extensions.Data/src/IConcurrentDbContext.cs b/lib/VNLib.Plugins.Extensions.Data/src/IConcurrentDbContext.cs new file mode 100644 index 0000000..330b05a --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.Data/src/IConcurrentDbContext.cs @@ -0,0 +1,44 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Data +* File: IConcurrentDbContext.cs +* +* IConcurrentDbContext.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System.Threading; +using System.Threading.Tasks; +using System.Transactions; + +namespace VNLib.Plugins.Extensions.Data +{ + /// <summary> + /// Represents a database context that can manage concurrency via transactions + /// </summary> + public interface IConcurrentDbContext : ITransactionalDbContext + { + /// <summary> + /// Opens a single transaction on the current context. If a transaction is already open, + /// it is disposed and a new transaction is begun. + /// </summary> + /// <param name="isolationLevel">The isolation level of the transaction</param> + /// <param name="cancellationToken">A token to cancel the operations</param> + Task OpenTransactionAsync(IsolationLevel isolationLevel, CancellationToken cancellationToken = default); + } +}
\ No newline at end of file diff --git a/lib/VNLib.Plugins.Extensions.Data/src/ProtectedEntityExtensions.cs b/lib/VNLib.Plugins.Extensions.Data/src/ProtectedEntityExtensions.cs index ea8d8cb..ec7b4f5 100644 --- a/lib/VNLib.Plugins.Extensions.Data/src/ProtectedEntityExtensions.cs +++ b/lib/VNLib.Plugins.Extensions.Data/src/ProtectedEntityExtensions.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Extensions.Data @@ -24,13 +24,13 @@ 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 { public static class ProtectedEntityExtensions @@ -41,11 +41,13 @@ namespace VNLib.Plugins.Extensions.Data /// <param name="store"></param> /// <param name="record">The record to update</param> /// <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) where TEntity : class, IDbModel, IUserEntity + 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; - return store.UpdateAsync(record); + return store.UpdateAsync(record, cancellation); } /// <summary> @@ -54,11 +56,13 @@ namespace VNLib.Plugins.Extensions.Data /// <param name="store"></param> /// <param name="record">The record to update</param> /// <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) where TEntity : class, IDbModel, IUserEntity + 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; - return store.CreateAsync(record); + return store.CreateAsync(record, cancellation); } /// <summary> @@ -108,10 +112,33 @@ namespace VNLib.Plugins.Extensions.Data /// <typeparam name="TEntity"></typeparam> /// <param name="store"></param> /// <param name="userId">The unique id of the user to query record count</param> + /// <param name="cancellation">A token to cancel the operation</param> /// <returns>A task that resolves the number of records belonging to the specified user</returns> - public static Task<long> GetUserRecordCountAsync<TEntity>(this IDataStore<TEntity> store, string userId) where TEntity : class, IDbModel, IUserEntity + public static Task<long> GetUserRecordCountAsync<TEntity>(this IDataStore<TEntity> store, string userId, CancellationToken cancellation = default) + where TEntity : class, IDbModel, IUserEntity + { + 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) { - return store.GetCountAsync(userId); + if(tdb is IConcurrentDbContext ccdb) + { + return ccdb.OpenTransactionAsync(isolationLevel, cancellationToken); + } + else + { + //Just ignore the isolation level + return tdb.OpenTransactionAsync(cancellationToken); + } } } } diff --git a/vnlib.plugins.extensions.build.sln b/vnlib.plugins.extensions.build.sln new file mode 100644 index 0000000..efb8de8 --- /dev/null +++ b/vnlib.plugins.extensions.build.sln @@ -0,0 +1,56 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VNLib.Plugins.Extensions.Data", "lib\VNLib.Plugins.Extensions.Data\src\VNLib.Plugins.Extensions.Data.csproj", "{6A3D4938-0064-443A-A87C-5DB8F8811110}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VNLib.Plugins.Extensions.Loading", "lib\VNLib.Plugins.Extensions.Loading\src\VNLib.Plugins.Extensions.Loading.csproj", "{B8D6ED0A-92C5-437F-A33E-2D8C3FC2EA30}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VNLib.Plugins.Extensions.Loading.Sql", "lib\VNLib.Plugins.Extensions.Loading.Sql\src\VNLib.Plugins.Extensions.Loading.Sql.csproj", "{A9942495-EB73-4A54-B64D-F2F7CA4C3F46}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VNLib.Plugins.Extensions.Validation", "lib\VNLib.Plugins.Extensions.Validation\src\VNLib.Plugins.Extensions.Validation.csproj", "{857DA423-DD18-40B4-AABB-0C12DB5E389D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{4EA8C491-99BE-49FA-AE9E-FCF822BC4937}" + ProjectSection(SolutionItems) = preProject + .gitattributes = .gitattributes + .gitignore = .gitignore + .onedev-buildspec.yml = .onedev-buildspec.yml + Extensions.sln.licenseheader = Extensions.sln.licenseheader + GitVersion.yml = GitVersion.yml + LICENSE.txt = LICENSE.txt + Module.Taskfile.yaml = Module.Taskfile.yaml + README.md = README.md + Taskfile.yaml = Taskfile.yaml + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6A3D4938-0064-443A-A87C-5DB8F8811110}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6A3D4938-0064-443A-A87C-5DB8F8811110}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6A3D4938-0064-443A-A87C-5DB8F8811110}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6A3D4938-0064-443A-A87C-5DB8F8811110}.Release|Any CPU.Build.0 = Release|Any CPU + {B8D6ED0A-92C5-437F-A33E-2D8C3FC2EA30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B8D6ED0A-92C5-437F-A33E-2D8C3FC2EA30}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B8D6ED0A-92C5-437F-A33E-2D8C3FC2EA30}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B8D6ED0A-92C5-437F-A33E-2D8C3FC2EA30}.Release|Any CPU.Build.0 = Release|Any CPU + {A9942495-EB73-4A54-B64D-F2F7CA4C3F46}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A9942495-EB73-4A54-B64D-F2F7CA4C3F46}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A9942495-EB73-4A54-B64D-F2F7CA4C3F46}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A9942495-EB73-4A54-B64D-F2F7CA4C3F46}.Release|Any CPU.Build.0 = Release|Any CPU + {857DA423-DD18-40B4-AABB-0C12DB5E389D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {857DA423-DD18-40B4-AABB-0C12DB5E389D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {857DA423-DD18-40B4-AABB-0C12DB5E389D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {857DA423-DD18-40B4-AABB-0C12DB5E389D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {BC28944A-A5ED-4433-AAC7-82840C94387A} + EndGlobalSection +EndGlobal |