/* * 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.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 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 /// /// 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, CancellationToken cancellation = default) { //Open new db context await using TransactionalDbContext ctx = await this.OpenAsync(IsolationLevel.ReadCommitted, cancellation); 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(cancellation); //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); } return await ctx.SaveAndCloseAsync(cancellation); } /// public virtual async Task UpdateAsync(T record, CancellationToken cancellation = default) { //Open new db context 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(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(cancellation); return true; } return await ctx.SaveAndCloseAsync(cancellation); } /// public virtual async Task CreateAsync(T record, CancellationToken cancellation = default) { //Open new db context 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); return await ctx.SaveAndCloseAsync(cancellation); } #endregion #region Delete /// public virtual async Task DeleteAsync(string key, CancellationToken cancellation = default) { //Open new db context 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(cancellation); if (record == null) { return false; } //Add the new application ctx.Remove(record); return await ctx.SaveAndCloseAsync(cancellation); } /// public virtual async Task DeleteAsync(T record, CancellationToken cancellation = default) { //Open new db context 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(cancellation); if (entry == null) { return false; } //Add the new application ctx.Remove(entry); return await ctx.SaveAndCloseAsync(cancellation); } /// public virtual async Task DeleteAsync(params string[] specifiers) { //Open new db context 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); return await ctx.SaveAndCloseAsync(); } #endregion #region Get Collection /// 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 = 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, cancellation); //close db and transaction 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 = 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; } #endregion #region Get Count /// public virtual async Task GetCountAsync(CancellationToken cancellation = default) { //Open db connection 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(cancellation); //close db and transaction await ctx.CommitTransactionAsync(cancellation); return count; } /// public virtual async Task GetCountAsync(string specifier, CancellationToken cancellation) { 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(cancellation); //close db and transaction await ctx.CommitTransactionAsync(cancellation); return count; } #endregion #region Get Single /// public virtual async Task GetSingleAsync(string key, CancellationToken cancellation = default) { //Open db connection 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(cancellation); //close db and transaction await ctx.CommitTransactionAsync(cancellation); return record; } /// public virtual async Task GetSingleAsync(T record, CancellationToken cancellation = default) { //Open db connection await using TransactionalDbContext ctx = await this.OpenAsync(IsolationLevel.ReadUncommitted, cancellation); //Get the single template by its id T? entry = await GetSingleQueryBuilder(ctx, record).SingleOrDefaultAsync(cancellation); //close db and transaction await ctx.CommitTransactionAsync(cancellation); return record; } /// public virtual async Task GetSingleAsync(params string[] specifiers) { //Open db connection 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; } #endregion #region Get Page /// 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 = 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, cancellation); //close db and transaction 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 = await this.OpenAsync(IsolationLevel.ReadUncommitted); //Get a page of records constrained by the given arguments 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; } #endregion } }