/* * Copyright (c) 2022 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.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using VNLib.Utils; using VNLib.Utils.Memory.Caching; 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 { /// /// Gets a unique ID for a new record being added to the store /// public abstract string RecordIdBuilder { get; } /// /// Gets a new ready for use /// /// public abstract TransactionalDbContext NewContext(); /// /// An object rental for entity collections /// public ObjectRental> ListRental { get; } = ObjectRental.Create>(null, static ret => ret.Clear()); #region Add Or Update /// public virtual async Task AddOrUpdateAsync(T record) { //Open new db context await using TransactionalDbContext ctx = NewContext(); //Open transaction await ctx.OpenTransactionAsync(); IQueryable query; if (string.IsNullOrWhiteSpace(record.Id)) { //Get the application query = 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(); //Check if creted if (entry == null) { //Create a new template id record.Id = RecordIdBuilder; //Set the created/lm times record.Created = record.LastModified = DateTime.UtcNow; //Add the new template to the ctx ctx.Add(record); } else { OnRecordUpdate(record, entry); } //Save changes ERRNO result = await ctx.SaveChangesAsync(); if (result) { //commit transaction if update was successful await ctx.CommitTransactionAsync(); } return result; } /// public virtual async Task UpdateAsync(T record) { //Open new db context await using TransactionalDbContext ctx = NewContext(); //Open transaction await ctx.OpenTransactionAsync(); //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(); 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(); return true; } //Save changes ERRNO result = await ctx.SaveChangesAsync(); if (result) { //commit transaction if update was successful await ctx.CommitTransactionAsync(); } return result; } /// public virtual async Task CreateAsync(T record) { //Open new db context await using TransactionalDbContext ctx = NewContext(); //Open transaction await ctx.OpenTransactionAsync(); //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; } /// /// 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) { //Open new db context await using TransactionalDbContext ctx = NewContext(); //Open transaction await ctx.OpenTransactionAsync(); //Get the template by its id IQueryable query = (from temp in ctx.Set() where temp.Id == key select temp); T? record = await query.SingleOrDefaultAsync(); 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; } /// public virtual async Task DeleteAsync(T record) { //Open new db context await using TransactionalDbContext ctx = NewContext(); //Open transaction await ctx.OpenTransactionAsync(); //Get a query for a a single item IQueryable query = GetSingleQueryBuilder(ctx, record); //Get the entry 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; } /// public virtual async Task DeleteAsync(params string[] specifiers) { //Open new db context await using TransactionalDbContext ctx = NewContext(); //Open transaction await ctx.OpenTransactionAsync(); //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); } #endregion #region Get Collection /// public virtual async Task GetCollectionAsync(ICollection collection, string specifier, int limit) { 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, specifier).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; } /// 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); //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() { //Open db connection await using TransactionalDbContext ctx = NewContext(); //Open transaction await ctx.OpenTransactionAsync(); //Async get the number of records of the given entity type long count = await ctx.Set().LongCountAsync(); //close db and transaction await ctx.CommitTransactionAsync(); return count; } /// public virtual async Task GetCountAsync(string specifier) { await using TransactionalDbContext ctx = NewContext(); //Open transaction await ctx.OpenTransactionAsync(); //Async get the number of records of the given entity type long count = await GetCountQueryBuilder(ctx, specifier).LongCountAsync(); //close db and transaction await ctx.CommitTransactionAsync(); 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) { //Open db connection await using TransactionalDbContext ctx = NewContext(); //Open transaction await ctx.OpenTransactionAsync(); //Get the single template by its id T? record = await (from entry in ctx.Set() where entry.Id == key select entry) .SingleOrDefaultAsync(); //close db and transaction await ctx.CommitTransactionAsync(); return record; } /// public virtual async Task GetSingleAsync(T record) { //Open db connection await using TransactionalDbContext ctx = NewContext(); //Open transaction await ctx.OpenTransactionAsync(); //Get the single template by its id T? entry = await GetSingleQueryBuilder(ctx, record).SingleOrDefaultAsync(); //close db and transaction await ctx.CommitTransactionAsync(); return record; } /// public virtual async Task GetSingleAsync(params string[] specifiers) { //Open db connection await using TransactionalDbContext ctx = NewContext(); //Open transaction await ctx.OpenTransactionAsync(); //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) { //Store preivous count int previous = collection.Count; //Open db connection await using TransactionalDbContext ctx = NewContext(); //Open transaction await ctx.OpenTransactionAsync(); //Get a page offset and a limit for the await ctx.Set() .Skip(page * limit) .Take(limit) .Select(static p => p) .ForEachAsync(collection.Add); //close db and transaction await ctx.CommitTransactionAsync(); //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 GetPageQueryBuilder(ctx, constraints) .Skip(page * limit) .Take(limit) .Select(static e => e) .ForEachAsync(collection.Add); //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 } }