/* * Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Extensions.Data * File: DbStore.cs * * DbStore.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger * VNLib collection of libraries and utilities. * * VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ using System; using System.Linq; using System.Threading; using System.Transactions; using System.Threading.Tasks; using System.Collections.Generic; using Microsoft.EntityFrameworkCore; using VNLib.Utils; using VNLib.Plugins.Extensions.Data.Abstractions; namespace VNLib.Plugins.Extensions.Data.Extensions { /// /// Extension methods for to add additional functionality /// public static class DbStoreExtensions { /// /// 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 public static async Task AddOrUpdateAsync(this IDataStore store, T record, CancellationToken cancellation = default) where T : class, IDbModel { //Open new db context await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.ReadCommitted, cancellation); IQueryable query; if (string.IsNullOrWhiteSpace(record.Id)) { //Get the application query = store.QueryTable.AddOrUpdateQueryBuilder(ctx, record); } else { //Get the application query = (from et in ctx.Set() where et.Id == record.Id select et); } //Using single T? entry = await query.SingleOrDefaultAsync(cancellation); //Check if creted if (entry == null) { //Create a new template id record.Id = store.GetNewRecordId(); //Set the created/lm times record.Created = record.LastModified = DateTime.UtcNow; //Add the new template to the ctx ctx.Add(record); } else { store.OnRecordUpdate(record, entry); } return await ctx.SaveAndCloseAsync(true, cancellation); } /// /// 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) public static async Task UpdateAsync(this IDataStore store, T record, CancellationToken cancellation = default) where T : class, IDbModel { //Open new db context await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.Serializable, cancellation); //Get the application IQueryable query = store.QueryTable.UpdateQueryBuilder(ctx, record); //Using single to make sure only one app is in the db (should never be an issue) T? oldEntry = await query.SingleOrDefaultAsync(cancellation); if (oldEntry == null) { return false; } //Update the template meta-data store.OnRecordUpdate(record, oldEntry); return await ctx.SaveAndCloseAsync(true, cancellation); } /// /// 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) public static async Task CreateAsync(this IDataStore store, T record, CancellationToken cancellation = default) where T : class, IDbModel { //Open new db context await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.ReadUncommitted, cancellation); //Create a new template id record.Id = store.GetNewRecordId(); //Update the created/last modified time of the record record.Created = record.LastModified = DateTime.UtcNow; //Add the new template ctx.Add(record); return await ctx.SaveAndCloseAsync(true, cancellation); } /// /// 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 public static async Task GetCountAsync(this IDataStore store, CancellationToken cancellation = default) where T : class, IDbModel { //Open db connection await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.ReadUncommitted, cancellation); //Async get the number of records of the given entity type long count = await ctx.Set().LongCountAsync(cancellation); //close db and transaction await ctx.SaveAndCloseAsync(true, cancellation); return count; } /// /// 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 public static async Task GetCountAsync(this IDataStore store, string specifier, CancellationToken cancellation = default) where T : class, IDbModel { await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.ReadUncommitted, cancellation); //Async get the number of records of the given entity type long count = await store.QueryTable.GetCountQueryBuilder(ctx, specifier).LongCountAsync(cancellation); //close db and transaction await ctx.SaveAndCloseAsync(true, cancellation); return count; } /// /// 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 public static async Task GetSingleAsync(this IDataStore store, string key, CancellationToken cancellation = default) where T : class, IDbModel { //Open db connection await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.ReadUncommitted, cancellation); //Get the single template by its id T? record = await (from entry in ctx.Set() where entry.Id == key select entry) .AsNoTracking() .SingleOrDefaultAsync(cancellation); //close db and transaction await ctx.SaveAndCloseAsync(true, cancellation); return record; } /// /// Gets a record identified by it's id /// /// /// A variable length specifier arguemnt array for retreiving a single application /// A task that resolves the entity if it exists public static async Task GetSingleAsync(this IDataStore store, params string[] specifiers) where T : class, IDbModel { //Open db connection await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.ReadUncommitted); //Get the single item by specifiers T? record = await store.QueryTable.GetSingleQueryBuilder(ctx, specifiers).SingleOrDefaultAsync(); //close db and transaction await ctx.SaveAndCloseAsync(true); return record; } /// /// 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 public static async Task GetSingleAsync(this IDataStore store, T record, CancellationToken cancellation = default) where T : class, IDbModel { //Open db connection await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.ReadUncommitted, cancellation); //Get the single template by its id T? entry = await store.QueryTable.GetSingleQueryBuilder(ctx, record).SingleOrDefaultAsync(cancellation); //close db and transaction await ctx.SaveAndCloseAsync(true, cancellation); return record; } /// /// 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 public static async Task GetCollectionAsync(this IDataStore store, ICollection collection, string specifier, int limit, CancellationToken cancellation = default) where T : class, IDbModel { int previous = collection.Count; //Open new db context await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.ReadUncommitted, cancellation); //Get the single template by its id await store.QueryTable.GetCollectionQueryBuilder(ctx, specifier) .Take(limit) .Select(static e => e) .AsNoTracking() .ForEachAsync(collection.Add, cancellation); //close db and transaction _ = await ctx.SaveAndCloseAsync(true, cancellation); //Return the number of elements add to the collection return collection.Count - previous; } /// /// Fills a collection with enires retireved from the store using a variable length specifier /// parameter /// /// /// The collection to add entires to /// The maximum number of elements to retrieve /// /// A Task the resolves to the number of items added to the collection public static async Task GetCollectionAsync(this IDataStore store, ICollection collection, int limit, params string[] args) where T : class, IDbModel { int previous = collection.Count; //Open new db context await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.ReadUncommitted); //Get the single template by its id await store.QueryTable.GetCollectionQueryBuilder(ctx, args) .Take(limit) .Select(static e => e) .AsNoTracking() .ForEachAsync(collection.Add); //close db and transaction _ = await ctx.SaveAndCloseAsync(true); //Return the number of elements add to the collection return collection.Count - previous; } /// /// 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) public static async Task DeleteAsync(this IDataStore store, T record, CancellationToken cancellation = default) where T : class, IDbModel { //Open new db context await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.RepeatableRead, cancellation); //Get a query for a a single item IQueryable query = store.QueryTable.GetSingleQueryBuilder(ctx, record); //Get the entry if it exists T? entry = await query.SingleOrDefaultAsync(cancellation); if (entry == null) { await ctx.SaveAndCloseAsync(false, cancellation); return false; } else { //Remove the entry ctx.Remove(entry); return await ctx.SaveAndCloseAsync(true, cancellation); } } /// /// 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) public static async Task DeleteAsync(this IDataStore store, string key, CancellationToken cancellation = default) where T : class, IDbModel { //Open new db context await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.RepeatableRead, cancellation); //Get a query for a a single item IQueryable query = store.QueryTable.GetSingleQueryBuilder(ctx, key); //Get the entry if it exists T? entry = await query.SingleOrDefaultAsync(cancellation); if (entry == null) { await ctx.SaveAndCloseAsync(false, cancellation); return false; } else { //Remove the entry ctx.Remove(entry); return await ctx.SaveAndCloseAsync(true, cancellation); } } /// /// Deletes one or more entires from the store matching the supplied specifiers /// /// /// A variable length array of specifiers used to delete one or more entires /// A task the resolves the number of records removed(should evaluate to false on failure, and deleted count on success) public static async Task DeleteAsync(this IDataStore store, params string[] specifiers) where T : class, IDbModel { //Open new db context await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.RepeatableRead); //Get the template by its id IQueryable query = store.QueryTable.DeleteQueryBuilder(ctx, specifiers); T? entry = await query.SingleOrDefaultAsync(); if (entry == null) { return false; } //Add the new application ctx.Remove(entry); return await ctx.SaveAndCloseAsync(true); } /// /// Gets a collection of records using a pagination style query, and adds the records to the collecion /// /// /// 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 public static async Task GetPageAsync(this IDataStore store, ICollection collection, int page, int limit, CancellationToken cancellation = default) where T : class, IDbModel { //Store preivous count int previous = collection.Count; //Open db connection await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.ReadUncommitted, cancellation); //Get a page offset and a limit for the await ctx.Set() .Skip(page * limit) .Take(limit) .Select(static p => p) .AsNoTracking() .ForEachAsync(collection.Add, cancellation); //close db and transaction await ctx.SaveAndCloseAsync(true, cancellation); //Return the number of records added return collection.Count - previous; } /// /// Gets a collection of records using a pagination style query with constraint arguments, and adds the records to the collecion /// /// /// The collection to add records to /// Pagination page to get records from /// The maximum number of items to retrieve from the store /// A params array of strings to constrain the result set from the db /// A task that resolves the number of items added to the collection public static async Task GetPageAsync(this IDataStore store, ICollection collection, int page, int limit, params string[] constraints) where T : class, IDbModel { //Store preivous count int previous = collection.Count; //Open new db context await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.ReadUncommitted); //Get a page of records constrained by the given arguments await store.QueryTable.GetPageQueryBuilder(ctx, constraints) .Skip(page * limit) .Take(limit) .Select(static e => e) .AsNoTracking() .ForEachAsync(collection.Add); //close db and transaction await ctx.SaveAndCloseAsync(true); //Return the number of records added return collection.Count - previous; } public static Task AddBulkAsync(this IDataStore store, IEnumerable records, string userId, bool overwriteTime = true, CancellationToken cancellation = default) where T : class, IDbModel, IUserEntity { //Assign user-id when numerated IEnumerable withUserId = records.Select(p => { p.UserId = userId; return p; }); return store.AddBulkAsync(withUserId, overwriteTime, cancellation); } public static async Task AddBulkAsync(this IDataStore store, IEnumerable records, bool overwriteTime = true, CancellationToken cancellation = default) where T : class, IDbModel { DateTime now = DateTime.UtcNow; //Open context and transaction await using IDbContextHandle database = await store.OpenAsync(IsolationLevel.ReadCommitted, cancellation); //Get the entity set IQueryable set = database.Set(); //Generate random ids for the feeds and set user-id foreach (T entity in records) { entity.Id = store.GetNewRecordId(); //If the entity has the default created time, update it, otherwise leave it as is if (overwriteTime || entity.Created == default) { entity.Created = now; } //Update last-modified time entity.LastModified = now; } //Add bulk items to database database.AddRange(records); return await database.SaveAndCloseAsync(true, cancellation); } } }