From 5f76434c98ab0e45447e947c4489ec644f93439a Mon Sep 17 00:00:00 2001 From: vnugent Date: Tue, 1 Aug 2023 18:39:13 -0400 Subject: Latest updates, build configurations, and native compression --- .../src/Abstractions/IDataStore.cs | 34 +- .../src/Abstractions/IPaginatedDataStore.cs | 8 +- lib/VNLib.Plugins.Extensions.Data/src/DbStore.cs | 382 ++++++++------------- .../src/DbStoreHelperExtensions.cs | 82 +++++ .../src/DbStoreQueries.cs | 153 +++++++++ .../src/IConcurrentDbContext.cs | 44 +++ .../src/ProtectedEntityExtensions.cs | 43 ++- 7 files changed, 485 insertions(+), 261 deletions(-) create mode 100644 lib/VNLib.Plugins.Extensions.Data/src/DbStoreHelperExtensions.cs create mode 100644 lib/VNLib.Plugins.Extensions.Data/src/DbStoreQueries.cs create mode 100644 lib/VNLib.Plugins.Extensions.Data/src/IConcurrentDbContext.cs (limited to 'lib/VNLib.Plugins.Extensions.Data/src') 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 { /// @@ -40,20 +42,23 @@ namespace VNLib.Plugins.Extensions.Data.Abstractions /// /// Gets the total number of records in the current store /// + /// A cancellation token to cancel the operation /// A task that resolves the number of records in the store - Task GetCountAsync(); + Task GetCountAsync(CancellationToken cancellation = default); /// /// Gets the number of records that belong to the specified constraint /// /// A specifier to constrain the reults + /// A cancellation token to cancel the operation /// The number of records that belong to the specifier - Task GetCountAsync(string specifier); + Task GetCountAsync(string specifier, CancellationToken cancellation = default); /// /// Gets a record from its key /// /// The key identifying the unique record + /// A cancellation token to cancel the operation /// A promise that resolves the record identified by the specified key - Task GetSingleAsync(string key); + Task GetSingleAsync(string key, CancellationToken cancellation = default); /// /// Gets a record from its key /// @@ -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 /// /// The partial model used to query the store + /// A cancellation token to cancel the operation /// A task the resolves the completed data-model - Task GetSingleAsync(T record); + Task GetSingleAsync(T record, CancellationToken cancellation = default); /// /// Fills a collection with enires retireved from the store using the specifer /// /// The collection to add entires to /// A specifier argument to constrain results /// The maximum number of elements to retrieve + /// A cancellation token to cancel the operation /// A Task the resolves to the number of items added to the collection - Task GetCollectionAsync(ICollection collection, string specifier, int limit); + Task GetCollectionAsync(ICollection collection, string specifier, int limit, CancellationToken cancellation = default); /// /// 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 /// /// The record to update + /// A cancellation token to cancel the operation /// A task the resolves an error code (should evaluate to false on failure, and true on success) - Task UpdateAsync(T record); + Task UpdateAsync(T record, CancellationToken cancellation = default); /// /// Creates a new entry in the store representing the specified record /// /// The record to add to the store + /// A cancellation token to cancel the operation /// A task the resolves an error code (should evaluate to false on failure, and true on success) - Task CreateAsync(T record); + Task CreateAsync(T record, CancellationToken cancellation = default); /// /// Deletes one or more entrires from the store matching the specified record /// /// The record to remove from the store + /// A cancellation token to cancel the operation /// A task the resolves the number of records removed(should evaluate to false on failure, and deleted count on success) - Task DeleteAsync(T record); + Task DeleteAsync(T record, CancellationToken cancellation = default); /// /// Deletes one or more entires from the store matching the specified unique key /// /// The unique key that identifies the record + /// A cancellation token to cancel the operation /// A task the resolves the number of records removed(should evaluate to false on failure, and deleted count on success) - Task DeleteAsync(string key); + Task DeleteAsync(string key, CancellationToken cancellation = default); /// /// Deletes one or more entires from the store matching the supplied specifiers /// @@ -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 /// /// The record to add to the store + /// A cancellation token to cancel the operation /// A task the resolves the result of the operation - Task AddOrUpdateAsync(T record); + Task 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 /// The collection to add records to /// Pagination page to get records from /// The maximum number of items to retrieve from the store + /// A cancellation token to cancel the operation /// A task that resolves the number of items added to the collection - Task GetPageAsync(ICollection collection, int page, int limit); + Task GetPageAsync(ICollection collection, int page, int limit, CancellationToken cancellation = default); /// /// Gets a collection of records using a pagination style query with constraint arguments, and adds the records to the collecion /// 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 { + /// /// Implements basic data-store functionality with abstract query builders /// /// A implemented type - public abstract class DbStore : IDataStore, IPaginatedDataStore where T: class, IDbModel + public abstract partial class DbStore : IDataStore, IPaginatedDataStore where T: class, IDbModel { /// /// Gets a unique ID for a new record being added to the store /// public abstract string RecordIdBuilder { get; } + /// /// Gets a new ready for use /// @@ -58,13 +62,13 @@ namespace VNLib.Plugins.Extensions.Data #region Add Or Update /// - public virtual async Task AddOrUpdateAsync(T record) + public virtual async Task 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 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); } + /// - public virtual async Task UpdateAsync(T record) + public virtual async Task 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 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); } + /// - public virtual async Task CreateAsync(T record) + public virtual async Task 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); } - - /// - /// 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 property - /// - /// The active context to query - /// The record to search for - /// A query that yields a single record if it exists in the store - protected virtual IQueryable AddOrUpdateQueryBuilder(TransactionalDbContext context, T record) - { - //default to get single of the specific record - return GetSingleQueryBuilder(context, record); - } - /// - /// Builds a query that attempts to get a single entry from the - /// store to update based on the specified record - /// - /// The active context to query - /// The record to search for - /// A query that yields a single record to update if it exists in the store - protected virtual IQueryable UpdateQueryBuilder(TransactionalDbContext context, T record) - { - //default to get single of the specific record - return GetSingleQueryBuilder(context, record); - } - /// - /// Updates the current record (if found) to the new record before - /// storing the updates. - /// - /// The new record to capture data from - /// The current record to be updated - protected abstract void OnRecordUpdate(T newRecord, T currentRecord); + #endregion #region Delete + /// - public virtual async Task DeleteAsync(string key) + public virtual async Task 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 query = (from temp in ctx.Set() 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); } + /// - public virtual async Task DeleteAsync(T record) + public virtual async Task 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 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); } + /// public virtual async Task 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 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; - } - - /// - /// Builds a query that results in a single entry to delete from the - /// constraint arguments - /// - /// The active context - /// A variable length parameter array of query constraints - /// A query that yields a single record (or no record) to delete - protected virtual IQueryable 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 + /// - public virtual async Task GetCollectionAsync(ICollection collection, string specifier, int limit) + public virtual async Task GetCollectionAsync(ICollection 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; } + /// public virtual async Task GetCollectionAsync(ICollection 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; } - /// - /// Builds a query to get a count of records constrained by the specifier - /// - /// The active context to run the query on - /// The specifier constrain - /// A query that can be counted - protected virtual IQueryable GetCollectionQueryBuilder(TransactionalDbContext context, string specifier) - { - return GetCollectionQueryBuilder(context, new string[] { specifier }); - } - - /// - /// Builds a query to get a collection of records based on an variable length array of parameters - /// - /// The active context to run the query on - /// An arguments array to constrain the results of the query - /// A query that returns a collection of records from the store - protected abstract IQueryable GetCollectionQueryBuilder(TransactionalDbContext context, params string[] constraints); - #endregion #region Get Count + /// - public virtual async Task GetCountAsync() + public virtual async Task 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().LongCountAsync(); + long count = await ctx.Set().LongCountAsync(cancellation); + //close db and transaction - await ctx.CommitTransactionAsync(); + await ctx.CommitTransactionAsync(cancellation); + return count; } + /// - public virtual async Task GetCountAsync(string specifier) + public virtual async Task 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; } - /// - /// Builds a query to get a count of records constrained by the specifier - /// - /// The active context to run the query on - /// The specifier constrain - /// A query that can be counted - protected virtual IQueryable GetCountQueryBuilder(TransactionalDbContext context, string specifier) - { - //Default use the get collection and just call the count method - return GetCollectionQueryBuilder(context, specifier); - } + #endregion #region Get Single + /// - public virtual async Task GetSingleAsync(string key) + public virtual async Task 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() where entry.Id == key select entry) - .SingleOrDefaultAsync(); + .SingleOrDefaultAsync(cancellation); + //close db and transaction - await ctx.CommitTransactionAsync(); + await ctx.CommitTransactionAsync(cancellation); return record; } + /// - public virtual async Task GetSingleAsync(T record) + public virtual async Task 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; } + /// public virtual async Task 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; } - /// - /// Builds a query to get a single record from the variable length parameter arguments - /// - /// The context to execute query against - /// Arguments to constrain the results of the query to a single record - /// A query that yields a single record - protected abstract IQueryable GetSingleQueryBuilder(TransactionalDbContext context, params string[] constraints); - /// - /// - /// Builds a query to get a single record from the specified record. - /// - /// - /// Unless overridden, performs an ID based query for a single entry - /// - /// - /// The context to execute query against - /// A record to referrence the lookup - /// A query that yields a single record - protected virtual IQueryable GetSingleQueryBuilder(TransactionalDbContext context, T record) - { - return from entry in context.Set() - where entry.Id == record.Id - select entry; - } + #endregion #region Get Page + /// - public virtual async Task GetPageAsync(ICollection collection, int page, int limit) + public virtual async Task GetPageAsync(ICollection 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() .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; } + /// public virtual async Task GetPageAsync(ICollection 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; } - /// - /// Builds a query to get a collection of records based on an variable length array of parameters - /// - /// The active context to run the query on - /// An arguments array to constrain the results of the query - /// A query that returns a paginated collection of records from the store - protected virtual IQueryable 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 + { + /// + /// Commits saves changes on the context and commits the transaction if the result + /// of the operation was successful + /// + /// + /// A token to cancel the operation + /// A task that resolves the result of the operation + public static async Task 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; + } + + /// + /// Opens a new database connection and begins a transaction with the specified isolation level + /// + /// + /// + /// The transaction isolation level + /// A token to cancel the transaction operation + /// + public static async Task OpenAsync(this DbStore 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 + { + + /// + /// 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 property + /// + /// The active context to query + /// The record to search for + /// A query that yields a single record if it exists in the store + protected virtual IQueryable AddOrUpdateQueryBuilder(TransactionalDbContext context, T record) + { + //default to get single of the specific record + return GetSingleQueryBuilder(context, record); + } + + /// + /// Builds a query that attempts to get a single entry from the + /// store to update based on the specified record + /// + /// The active context to query + /// The record to search for + /// A query that yields a single record to update if it exists in the store + protected virtual IQueryable UpdateQueryBuilder(TransactionalDbContext context, T record) + { + //default to get single of the specific record + return GetSingleQueryBuilder(context, record); + } + + + /// + /// Builds a query that results in a single entry to delete from the + /// constraint arguments + /// + /// The active context + /// A variable length parameter array of query constraints + /// A query that yields a single record (or no record) to delete + protected virtual IQueryable DeleteQueryBuilder(TransactionalDbContext context, params string[] constraints) + { + //default use the get-single method, as the implementation is usually identical + return GetSingleQueryBuilder(context, constraints); + } + + /// + /// Builds a query to get a count of records constrained by the specifier + /// + /// The active context to run the query on + /// The specifier constrain + /// A query that can be counted + protected virtual IQueryable GetCollectionQueryBuilder(TransactionalDbContext context, string specifier) + { + return GetCollectionQueryBuilder(context, new string[] { specifier }); + } + + + /// + /// Builds a query to get a count of records constrained by the specifier + /// + /// The active context to run the query on + /// The specifier constrain + /// A query that can be counted + protected virtual IQueryable GetCountQueryBuilder(TransactionalDbContext context, string specifier) + { + //Default use the get collection and just call the count method + return GetCollectionQueryBuilder(context, specifier); + } + + /// + /// + /// Builds a query to get a single record from the specified record. + /// + /// + /// Unless overridden, performs an ID based query for a single entry + /// + /// + /// The context to execute query against + /// A record to referrence the lookup + /// A query that yields a single record + protected virtual IQueryable GetSingleQueryBuilder(TransactionalDbContext context, T record) + { + return from entry in context.Set() + where entry.Id == record.Id + select entry; + } + + /// + /// Builds a query to get a collection of records based on an variable length array of parameters + /// + /// The active context to run the query on + /// An arguments array to constrain the results of the query + /// A query that returns a paginated collection of records from the store + protected virtual IQueryable GetPageQueryBuilder(TransactionalDbContext context, params string[] constraints) + { + //Default to getting the entire collection and just selecting a single page + return GetCollectionQueryBuilder(context, constraints); + } + + /// + /// Updates the current record (if found) to the new record before + /// storing the updates. + /// + /// The new record to capture data from + /// The current record to be updated + protected abstract void OnRecordUpdate(T newRecord, T currentRecord); + + /// + /// Builds a query to get a single record from the variable length parameter arguments + /// + /// The context to execute query against + /// Arguments to constrain the results of the query to a single record + /// A query that yields a single record + protected abstract IQueryable GetSingleQueryBuilder(TransactionalDbContext context, params string[] constraints); + + /// + /// Builds a query to get a collection of records based on an variable length array of parameters + /// + /// The active context to run the query on + /// An arguments array to constrain the results of the query + /// A query that returns a collection of records from the store + protected abstract IQueryable 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 +{ + /// + /// Represents a database context that can manage concurrency via transactions + /// + public interface IConcurrentDbContext : ITransactionalDbContext + { + /// + /// Opens a single transaction on the current context. If a transaction is already open, + /// it is disposed and a new transaction is begun. + /// + /// The isolation level of the transaction + /// A token to cancel the operations + 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 /// /// The record to update /// The userid of the record owner + /// A token to cancel the operation /// A task that evaluates to the number of records modified - public static Task UpdateUserRecordAsync(this IDataStore store, TEntity record, string userId) where TEntity : class, IDbModel, IUserEntity + public static Task UpdateUserRecordAsync(this IDataStore 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); } /// @@ -54,11 +56,13 @@ namespace VNLib.Plugins.Extensions.Data /// /// The record to update /// The userid of the record owner + /// A token to cancel the operation /// A task that evaluates to the number of records modified - public static Task CreateUserRecordAsync(this IDataStore store, TEntity record, string userId) where TEntity : class, IDbModel, IUserEntity + public static Task CreateUserRecordAsync(this IDataStore 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); } /// @@ -108,10 +112,33 @@ namespace VNLib.Plugins.Extensions.Data /// /// /// The unique id of the user to query record count + /// A token to cancel the operation /// A task that resolves the number of records belonging to the specified user - public static Task GetUserRecordCountAsync(this IDataStore store, string userId) where TEntity : class, IDbModel, IUserEntity + public static Task GetUserRecordCountAsync(this IDataStore store, string userId, CancellationToken cancellation = default) + where TEntity : class, IDbModel, IUserEntity + { + return store.GetCountAsync(userId, cancellation); + } + + /// + /// If the current context instance inherits the interface, + /// attempts to open a transaction with the specified isolation level. + /// + /// + /// The transaction isolation level + /// A token to cancel the operation + /// + 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); + } } } } -- cgit