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