diff options
Diffstat (limited to 'lib/VNLib.Plugins.Extensions.Data/src/DbStore.cs')
-rw-r--r-- | lib/VNLib.Plugins.Extensions.Data/src/DbStore.cs | 382 |
1 files changed, 143 insertions, 239 deletions
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 } } |