/*
* 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
}
}