aboutsummaryrefslogtreecommitdiff
path: root/lib/VNLib.Plugins.Extensions.Data/src
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2023-08-01 18:39:13 -0400
committerLibravatar vnugent <public@vaughnnugent.com>2023-08-01 18:39:13 -0400
commit5f76434c98ab0e45447e947c4489ec644f93439a (patch)
tree2cfab192627e4e61f573e3f0425d36763af2ee9d /lib/VNLib.Plugins.Extensions.Data/src
parent3fbd6e9c483503419cdfe76544090d89397e35de (diff)
Latest updates, build configurations, and native compression
Diffstat (limited to 'lib/VNLib.Plugins.Extensions.Data/src')
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDataStore.cs34
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IPaginatedDataStore.cs8
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/DbStore.cs382
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/DbStoreHelperExtensions.cs82
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/DbStoreQueries.cs153
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/IConcurrentDbContext.cs44
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/ProtectedEntityExtensions.cs43
7 files changed, 485 insertions, 261 deletions
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);
+ }
}
}
}