diff options
25 files changed, 2158 insertions, 507 deletions
diff --git a/VNLib.Plugins.Extensions.Data/SQL/DbExtensions.cs b/VNLib.Plugins.Extensions.Data/SQL/DbExtensions.cs new file mode 100644 index 0000000..e6ee6b1 --- /dev/null +++ b/VNLib.Plugins.Extensions.Data/SQL/DbExtensions.cs @@ -0,0 +1,526 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Data +* File: DbExtensions.cs +* +* DbExtensions.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 General Public License as published +* by the Free Software Foundation, either version 2 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 +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/. +*/ + +using System; +using System.Data; +using System.Reflection; +using System.Data.Common; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +using VNLib.Utils; +using VNLib.Utils.Memory.Caching; + +namespace VNLib.Plugins.Extensions.Data.SQL +{ + /// <summary> + /// Provides basic extension methods for ADO.NET abstract classes + /// for rapid development + /// </summary> + public static class DbExtensions + { + /* + * Object rental for propery dictionaries used for custom result objects + */ + private static readonly ObjectRental<Dictionary<string, PropertyInfo>> DictStore; + + static DbExtensions() + { + //Setup dict store + DictStore = ObjectRental.Create<Dictionary<string, PropertyInfo>>(null, static dict => dict.Clear(), 20); + } + + /// <summary> + /// Creates a new <see cref="DbParameter"/> configured for <see cref="ParameterDirection.Input"/> with the specified value + /// and adds it to the command. + /// </summary> + /// <param name="cmd"></param> + /// <param name="name">The parameter name</param> + /// <param name="value">The value of the parameter</param> + /// <param name="type">The <see cref="DbType"/> of the column</param> + /// <param name="nullable">Are null types allowed in the value parameter</param> + /// <returns>The created parameter</returns> + public static DbParameter AddParameter<T>(this DbCommand cmd, string @name, T @value, DbType @type, bool @nullable = false) + { + //Create the new parameter from command + DbParameter param = cmd.CreateParameter(); + //Set parameter variables + param.ParameterName = name; + param.Value = value; + param.DbType = type; + //Force non null mapping + param.SourceColumnNullMapping = nullable; + //Specify input parameter + param.Direction = ParameterDirection.Input; + //Add param to list + cmd.Parameters.Add(param); + return param; + } + /// <summary> + /// Creates a new <see cref="DbParameter"/> configured for <see cref="ParameterDirection.Input"/> with the specified value + /// and adds it to the command. + /// </summary> + /// <param name="cmd"></param> + /// <param name="name">The parameter name</param> + /// <param name="value">The value of the parameter</param> + /// <param name="type">The <see cref="DbType"/> of the column</param> + /// <param name="size">Size of the data value</param> + /// <param name="nullable">Are null types allowed in the value parameter</param> + /// <returns>The created parameter</returns> + public static DbParameter AddParameter<T>(this DbCommand cmd, string @name, T @value, DbType @type, int @size, bool @nullable = false) + { + DbParameter param = AddParameter(cmd, name, value, type, nullable); + //Set size parameter + param.Size = size; + return param; + } + /// <summary> + /// Creates a new <see cref="DbParameter"/> configured for <see cref="ParameterDirection.Output"/> with the specified value + /// and adds it to the command. + /// </summary> + /// <param name="cmd"></param> + /// <param name="name">The parameter name</param> + /// <param name="value">The value of the parameter</param> + /// <param name="type">The <see cref="DbType"/> of the column</param> + /// <param name="nullable">Are null types allowed in the value parameter</param> + /// <returns>The created parameter</returns> + public static DbParameter AddOutParameter<T>(this DbCommand cmd, string @name, T @value, DbType @type, bool @nullable = false) + { + //Create the new parameter from command + DbParameter param = AddParameter(cmd, name, value, type, nullable); + //Specify output parameter + param.Direction = ParameterDirection.Output; + return param; + } + /// <summary> + /// Creates a new <see cref="DbParameter"/> configured for <see cref="ParameterDirection.Output"/> with the specified value + /// and adds it to the command. + /// </summary> + /// <param name="cmd"></param> + /// <param name="name">The parameter name</param> + /// <param name="value">The value of the parameter</param> + /// <param name="type">The <see cref="DbType"/> of the column</param> + /// <param name="size">Size of the data value</param> + /// <param name="nullable">Are null types allowed in the value parameter</param> + /// <returns>The created parameter</returns> + public static DbParameter AddOutParameter<T>(this DbCommand cmd, string @name, T @value, DbType @type, int @size, bool @nullable = false) + { + DbParameter param = AddOutParameter(cmd, name, value, type, nullable); + //Set size parameter + param.Size = size; + return param; + } + + /// <summary> + /// Creates a new <see cref="DbCommand"/> for <see cref="CommandType.Text"/> with the specified command + /// </summary> + /// <param name="db"></param> + /// <param name="cmdText">The command to run against the connection</param> + /// <returns>The initalized <see cref="DbCommand"/></returns> + public static DbCommand CreateTextCommand(this DbConnection db, string cmdText) + { + //Create the new command + DbCommand cmd = db.CreateCommand(); + cmd.CommandText = cmdText; + cmd.CommandType = CommandType.Text; //Specify text command + return cmd; + } + /// <summary> + /// Creates a new <see cref="DbCommand"/> for <see cref="CommandType.StoredProcedure"/> with the specified procedure name + /// </summary> + /// <param name="db"></param> + /// <param name="procedureName">The name of the stored proecedure to execute</param> + /// <returns>The initalized <see cref="DbCommand"/></returns> + public static DbCommand CreateProcedureCommand(this DbConnection db, string procedureName) + { + //Create the new command + DbCommand cmd = db.CreateCommand(); + cmd.CommandText = procedureName; + cmd.CommandType = CommandType.StoredProcedure; //Specify stored procedure + return cmd; + } + + /// <summary> + /// Creates a new <see cref="DbCommand"/> for <see cref="CommandType.Text"/> with the specified command + /// on a given transaction + /// </summary> + /// <param name="db"></param> + /// <param name="cmdText">The command to run against the connection</param> + /// <param name="transaction">The transaction to execute on</param> + /// <returns>The initalized <see cref="DbCommand"/></returns> + public static DbCommand CreateTextCommand(this DbConnection db, string cmdText, DbTransaction transaction) + { + return CreateCommand(db, transaction, CommandType.Text, cmdText); + } + /// <summary> + /// Shortcut to create a command on a transaction with the specifed command type and command + /// </summary> + /// <param name="db"></param> + /// <param name="transaction">The transaction to complete the operation on</param> + /// <param name="type">The command type</param> + /// <param name="command">The command to execute</param> + /// <returns>The intialized db command</returns> + public static DbCommand CreateCommand(this DbConnection db, DbTransaction transaction, CommandType type, string command) + { + //Create the new command + DbCommand cmd = db.CreateCommand(); + cmd.Transaction = transaction; + cmd.CommandText = command; + cmd.CommandType = type; + return cmd; + } + /// <summary> + /// Creates a new <see cref="DbCommand"/> for <see cref="CommandType.StoredProcedure"/> with the specified procedure name + /// </summary> + /// <param name="db"></param> + /// <param name="procedureName">The name of the stored proecedure to execute</param> + /// <param name="transaction">The transaction to execute on</param> + /// <returns>The initalized <see cref="DbCommand"/></returns> + public static DbCommand CreateProcedureCommand(this DbConnection db, string procedureName, DbTransaction transaction) + { + return CreateCommand(db, transaction, CommandType.StoredProcedure, procedureName); + } + + /// <summary> + /// Reads all available rows from the reader, adapts columns to public properties with <see cref="SqlColumnName"/> + /// attributes, and adds them to the collection + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="reader"></param> + /// <param name="container">The container to write created objects to</param> + /// <returns>The number of objects created and written to the collection</returns> + public static int GetAllObjects<T>(this DbDataReader reader, ICollection<T> container) where T : new() + { + //make sure its worth collecting object meta + if (!reader.HasRows) + { + return 0; + } + Type objectType = typeof(T); + //Rent a dict of properties that have the column attribute set so we can load the proper results + Dictionary<string, PropertyInfo> avialbleProps = DictStore.Rent(); + //Itterate through public properties + foreach (PropertyInfo prop in objectType.GetProperties()) + { + //try to get the column name attribute of the propery + SqlColumnName colAtt = prop.GetCustomAttribute<SqlColumnName>(true); + //Attribute is valid and coumn name is not empty + if (!string.IsNullOrWhiteSpace(colAtt?.ColumnName)) + { + //Store the property for later + avialbleProps[colAtt.ColumnName] = prop; + } + } + //Get the column schema + ReadOnlyCollection<DbColumn> columns = reader.GetColumnSchema(); + int count = 0; + //Read + while (reader.Read()) + { + //Create the new object + T ret = new(); + //Iterate through columns + foreach (DbColumn col in columns) + { + //Get the propery if its specified by its column-name attribute + if (avialbleProps.TryGetValue(col.ColumnName, out PropertyInfo prop)) + { + //make sure the column has a value + if (col.ColumnOrdinal.HasValue) + { + //Get the object + object val = reader.GetValue(col.ColumnOrdinal.Value); + //Set check if the row is DB null, if so set it, otherwise set the value + prop.SetValue(ret, Convert.IsDBNull(val) ? null : val); + } + } + } + //Add the object to the collection + container.Add(ret); + //Increment count + count++; + } + //return dict (if an error occurs, just let the dict go and create a new one next time, no stress setting up a try/finally block) + DictStore.Return(avialbleProps); + return count; + } + /// <summary> + /// Reads all available rows from the reader, adapts columns to public properties with <see cref="SqlColumnName"/> + /// attributes, and adds them to the collection + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="reader"></param> + /// <param name="container">The container to write created objects to</param> + /// <returns>The number of objects created and written to the collection</returns> + public static async ValueTask<int> GetAllObjectsAsync<T>(this DbDataReader reader, ICollection<T> container) where T : new() + { + //make sure its worth collecting object meta + if (!reader.HasRows) + { + return 0; + } + Type objectType = typeof(T); + //Rent a dict of properties that have the column attribute set so we can load the proper results + Dictionary<string, PropertyInfo> avialbleProps = DictStore.Rent(); + //Itterate through public properties + foreach (PropertyInfo prop in objectType.GetProperties()) + { + //try to get the column name attribute of the propery + SqlColumnName colAtt = prop.GetCustomAttribute<SqlColumnName>(true); + //Attribute is valid and coumn name is not empty + if (!string.IsNullOrWhiteSpace(colAtt?.ColumnName)) + { + //Store the property for later + avialbleProps[colAtt.ColumnName] = prop; + } + } + //Get the column schema + ReadOnlyCollection<DbColumn> columns = await reader.GetColumnSchemaAsync(); + int count = 0; + //Read + while (await reader.ReadAsync()) + { + //Create the new object + T ret = new(); + //Iterate through columns + foreach (DbColumn col in columns) + { + //Get the propery if its specified by its column-name attribute + if (avialbleProps.TryGetValue(col.ColumnName, out PropertyInfo prop)) + { + //make sure the column has a value + if (col.ColumnOrdinal.HasValue) + { + //Get the object + object val = reader.GetValue(col.ColumnOrdinal.Value); + //Set check if the row is DB null, if so set it, otherwise set the value + prop.SetValue(ret, Convert.IsDBNull(val) ? null : val); + } + } + } + //Add the object to the collection + container.Add(ret); + //Increment count + count++; + } + //return dict (if an error occurs, just let the dict go and create a new one next time, no stress setting up a try/finally block) + DictStore.Return(avialbleProps); + return count; + } + /// <summary> + /// Reads the first available row from the reader, adapts columns to public properties with <see cref="SqlColumnName"/> + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="reader"></param> + /// <returns>The created object, or default if no rows are available</returns> + public static T GetFirstObject<T>(this DbDataReader reader) where T : new() + { + //make sure its worth collecting object meta + if (!reader.HasRows) + { + return default; + } + //Get the object type + Type objectType = typeof(T); + //Get the column schema + ReadOnlyCollection<DbColumn> columns = reader.GetColumnSchema(); + //Read + if (reader.Read()) + { + //Rent a dict of properties that have the column attribute set so we can load the proper results + Dictionary<string, PropertyInfo> availbleProps = DictStore.Rent(); + //Itterate through public properties + foreach (PropertyInfo prop in objectType.GetProperties()) + { + //try to get the column name attribute of the propery + SqlColumnName colAtt = prop.GetCustomAttribute<SqlColumnName>(true); + //Attribute is valid and coumn name is not empty + if (colAtt != null && !string.IsNullOrWhiteSpace(colAtt.ColumnName)) + { + //Store the property for later + availbleProps[colAtt.ColumnName] = prop; + } + } + //Create the new object + T ret = new(); + //Iterate through columns + foreach (DbColumn col in columns) + { + //Get the propery if its specified by its column-name attribute + if (availbleProps.TryGetValue(col.ColumnName, out PropertyInfo prop) && col.ColumnOrdinal.HasValue) + { + //Get the object + object val = reader.GetValue(col.ColumnOrdinal.Value); + //Set check if the row is DB null, if so set it, otherwise set the value + prop.SetValue(ret, Convert.IsDBNull(val) ? null : val); + } + } + //Return dict, no stress if error occurs, the goal is lower overhead + DictStore.Return(availbleProps); + //Return the new object + return ret; + } + return default; + } + /// <summary> + /// Reads the first available row from the reader, adapts columns to public properties with <see cref="SqlColumnName"/> + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="reader"></param> + /// <returns>The created object, or default if no rows are available</returns> + public static async Task<T> GetFirstObjectAsync<T>(this DbDataReader reader) where T : new() + { + //Read + if (await reader.ReadAsync()) + { + //Get the object type + Type objectType = typeof(T); + //Get the column schema + ReadOnlyCollection<DbColumn> columns = await reader.GetColumnSchemaAsync(); + //Rent a dict of properties that have the column attribute set so we can load the proper results + Dictionary<string, PropertyInfo> availbleProps = DictStore.Rent(); + //Itterate through public properties + foreach (PropertyInfo prop in objectType.GetProperties()) + { + //try to get the column name attribute of the propery + SqlColumnName colAtt = prop.GetCustomAttribute<SqlColumnName>(true); + //Attribute is valid and coumn name is not empty + if (colAtt != null && !string.IsNullOrWhiteSpace(colAtt.ColumnName)) + { + //Store the property for later + availbleProps[colAtt.ColumnName] = prop; + } + } + //Create the new object + T ret = new(); + //Iterate through columns + foreach (DbColumn col in columns) + { + //Get the propery if its specified by its column-name attribute + if (availbleProps.TryGetValue(col.ColumnName, out PropertyInfo prop) && col.ColumnOrdinal.HasValue) + { + //Get the object + object val = reader.GetValue(col.ColumnOrdinal.Value); + //Set check if the row is DB null, if so set it, otherwise set the value + prop.SetValue(ret, Convert.IsDBNull(val) ? null : val); + } + } + //Return dict, no stress if error occurs, the goal is lower overhead + DictStore.Return(availbleProps); + //Return the new object + return ret; + } + return default; + } + /// <summary> + /// Executes a nonquery operation with the specified command using the object properties set with the + /// <see cref="SqlVariable"/> attributes + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="cmd"></param> + /// <param name="obj">The object containing the <see cref="SqlVariable"/> properties to write to command variables</param> + /// <returns>The number of rows affected</returns> + /// <exception cref="TypeLoadException"></exception> + /// <exception cref="ArgumentException"></exception> + /// <exception cref="NotSupportedException"></exception> + /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="AmbiguousMatchException"></exception> + /// <exception cref="TargetInvocationException"></exception> + public static ERRNO ExecuteNonQuery<T>(this DbCommand cmd, T obj) where T : notnull + { + if (obj == null) + { + throw new ArgumentNullException(nameof(obj)); + } + //Get the objec type + Type objtype = typeof(T); + //Itterate through public properties + foreach (PropertyInfo prop in objtype.GetProperties()) + { + //try to get the variable attribute of the propery + SqlVariable varprops = prop.GetCustomAttribute<SqlVariable>(true); + //This property is an sql variable, so lets add it + if (varprops == null) + { + continue; + } + //If the command type is text, then make sure the variable is actually in the command, if not, ignore it + if (cmd.CommandType != CommandType.Text || cmd.CommandText.Contains(varprops.VariableName)) + { + //Add the parameter to the command list + cmd.AddParameter(varprops.VariableName, prop.GetValue(obj), varprops.DataType, varprops.Size, varprops.Nullable).Direction = varprops.Direction; + } + } + //Prepare the sql statement + cmd.Prepare(); + //Exect the query and return the results + return cmd.ExecuteNonQuery(); + } + /// <summary> + /// Executes a nonquery operation with the specified command using the object properties set with the + /// <see cref="SqlVariable"/> attributes + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="cmd"></param> + /// <param name="obj">The object containing the <see cref="SqlVariable"/> properties to write to command variables</param> + /// <returns>The number of rows affected</returns> + /// <exception cref="TypeLoadException"></exception> + /// <exception cref="ArgumentException"></exception> + /// <exception cref="NotSupportedException"></exception> + /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="AmbiguousMatchException"></exception> + /// <exception cref="TargetInvocationException"></exception> + public static async Task<ERRNO> ExecuteNonQueryAsync<T>(this DbCommand cmd, T obj) where T : notnull + { + if (obj == null) + { + throw new ArgumentNullException(nameof(obj)); + } + //Get the objec type + Type objtype = typeof(T); + //Itterate through public properties + foreach (PropertyInfo prop in objtype.GetProperties()) + { + //try to get the variable attribute of the propery + SqlVariable varprops = prop.GetCustomAttribute<SqlVariable>(true); + //This property is an sql variable, so lets add it + if (varprops == null) + { + continue; + } + //If the command type is text, then make sure the variable is actually in the command, if not, ignore it + if (cmd.CommandType != CommandType.Text || cmd.CommandText.Contains(varprops.VariableName)) + { + //Add the parameter to the command list + cmd.AddParameter(varprops.VariableName, prop.GetValue(obj), varprops.DataType, varprops.Size, varprops.Nullable).Direction = varprops.Direction; + } + } + //Prepare the sql statement + await cmd.PrepareAsync(); + //Exect the query and return the results + return await cmd.ExecuteNonQueryAsync(); + } + } +}
\ No newline at end of file diff --git a/VNLib.Plugins.Extensions.Data/SQL/EnumerableTable.cs b/VNLib.Plugins.Extensions.Data/SQL/EnumerableTable.cs new file mode 100644 index 0000000..23cd889 --- /dev/null +++ b/VNLib.Plugins.Extensions.Data/SQL/EnumerableTable.cs @@ -0,0 +1,118 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Data +* File: EnumerableTable.cs +* +* EnumerableTable.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 General Public License as published +* by the Free Software Foundation, either version 2 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 +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/. +*/ + +using System; +using System.Data; +using System.Threading; +using System.Data.Common; +using System.Threading.Tasks; +using System.Collections.Generic; + +namespace VNLib.Plugins.Extensions.Data.SQL +{ + /// <summary> + /// A base class for client side async enumerable SQL queries + /// </summary> + /// <typeparam name="T">The entity type</typeparam> + public abstract class EnumerableTable<T> : TableManager, IAsyncEnumerable<T> + { + const string DEFAULT_ENUM_STATMENT = "SELECT *\r\nFROM @table\r\n;"; + + public EnumerableTable(Func<DbConnection> factory, string tableName) : base(factory, tableName) + { + //Build the default select all statment + Enumerate = DEFAULT_ENUM_STATMENT.Replace("@table", tableName); + } + public EnumerableTable(Func<DbConnection> factory) : base(factory) + { } + + /// <summary> + /// The command that will be run against the database to return rows for enumeration + /// </summary> + protected string Enumerate { get; set; } + + /// <summary> + /// The isolation level to use when creating the transaction during enumerations + /// </summary> + protected IsolationLevel TransactionIsolationLevel { get; set; } = IsolationLevel.ReadUncommitted; + + IAsyncEnumerator<T> IAsyncEnumerable<T>.GetAsyncEnumerator(CancellationToken cancellationToken) + { + return GetAsyncEnumerator(cancellationToken: cancellationToken); + } + + /// <summary> + /// Transforms a row from the <paramref name="reader"/> into the item type + /// to be returned when yielded. + /// </summary> + /// <param name="reader">The reader to get the item data from</param> + /// <param name="cancellationToken">A token to cancel the operation</param> + /// <returns>A task that returns the transformed item</returns> + /// <remarks>The <paramref name="reader"/> position is set before this method is invoked</remarks> + protected abstract Task<T> GetItemAsync(DbDataReader reader, CancellationToken cancellationToken); + /// <summary> + /// Invoked when an item is no longer in the enumerator scope, in the enumeration process. + /// </summary> + /// <param name="item">The item to cleanup</param> + /// <param name="cancellationToken">A token to cancel the operation</param> + /// <returns>A ValueTask that represents the cleanup process</returns> + protected abstract ValueTask CleanupItemAsync(T item, CancellationToken cancellationToken); + + /// <summary> + /// Gets an <see cref="IAsyncEnumerator{T}"/> to enumerate items within the backing store. + /// </summary> + /// <param name="closeItems">Cleanup items after each item is enumerated and the enumeration scope has + /// returned to the enumerator</param> + /// <param name="cancellationToken">A token to cancel the enumeration</param> + /// <returns>A <see cref="IAsyncEnumerator{T}"/> to enumerate records within the store</returns> + public virtual async IAsyncEnumerator<T> GetAsyncEnumerator(bool closeItems = true, CancellationToken cancellationToken = default) + { + await using DbConnection db = GetConnection(); + await db.OpenAsync(cancellationToken); + await using DbTransaction transaction = await db.BeginTransactionAsync(cancellationToken); + //Start the enumeration command + await using DbCommand cmd = db.CreateTextCommand(Enumerate, transaction); + await cmd.PrepareAsync(cancellationToken); + await using DbDataReader reader = await cmd.ExecuteReaderAsync(cancellationToken); + //loop through results and transform each element + while (reader.Read()) + { + //get the item + T item = await GetItemAsync(reader, cancellationToken); + try + { + yield return item; + } + finally + { + if (closeItems) + { + //Cleanup the item + await CleanupItemAsync(item, cancellationToken); + } + } + } + } + } +}
\ No newline at end of file diff --git a/VNLib.Plugins.Extensions.Data/SQL/SqlColumnName.cs b/VNLib.Plugins.Extensions.Data/SQL/SqlColumnName.cs new file mode 100644 index 0000000..0039fb5 --- /dev/null +++ b/VNLib.Plugins.Extensions.Data/SQL/SqlColumnName.cs @@ -0,0 +1,65 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Data +* File: SqlColumnName.cs +* +* SqlColumnName.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 General Public License as published +* by the Free Software Foundation, either version 2 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 +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/. +*/ + +using System; + +namespace VNLib.Plugins.Extensions.Data.SQL +{ + /// <summary> + /// Property attribute that specifies the property represents an SQL column in the database + /// </summary> + [AttributeUsage(AttributeTargets.Property)] + public class SqlColumnName : Attribute + { + public bool Nullable { get; } + public bool Unique { get; } + public bool PrimaryKey { get; } + public string ColumnName { get; init; } + /// <summary> + /// Specifies the property is an SQL column name + /// </summary> + /// <param name="columnName">Name of the SQL column</param> + /// <param name="primaryKey"></param> + /// <param name="nullable"></param> + /// <param name="unique"></param> + public SqlColumnName(string columnName, bool primaryKey = false, bool nullable = true, bool unique = false) + { + this.ColumnName = columnName; + this.PrimaryKey = primaryKey; + this.Nullable = nullable; + this.Unique = unique; + } + } + + /// <summary> + /// Allows a type to declare itself as a <see cref="System.Data.DataTable"/> with the specified name + /// </summary> + [AttributeUsage(AttributeTargets.Class, AllowMultiple =false, Inherited = true)] + public class SqlTableName : Attribute + { + public string TableName { get; } + + public SqlTableName(string tableName) => TableName = tableName; + } +}
\ No newline at end of file diff --git a/VNLib.Plugins.Extensions.Data/SQL/SqlVariable.cs b/VNLib.Plugins.Extensions.Data/SQL/SqlVariable.cs new file mode 100644 index 0000000..d33854a --- /dev/null +++ b/VNLib.Plugins.Extensions.Data/SQL/SqlVariable.cs @@ -0,0 +1,58 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Data +* File: SqlVariable.cs +* +* SqlVariable.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 General Public License as published +* by the Free Software Foundation, either version 2 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 +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/. +*/ + +using System; +using System.Data; + +namespace VNLib.Plugins.Extensions.Data.SQL +{ + /// <summary> + /// Property attribute that specifies the property is to be used for a given command variable + /// </summary> + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] + public class SqlVariable : Attribute + { + public string VariableName { get; init; } + public DbType DataType { get; init; } + public ParameterDirection Direction { get; init; } + public int Size { get; init; } + public bool Nullable { get; init; } + /// <summary> + /// Specifies the property to be used as an SQL variable + /// </summary> + /// <param name="variableName">Sql statement variable this property will substitute</param> + /// <param name="dataType">The sql data the property will represent</param> + /// <param name="direction">Data direction during execution</param> + /// <param name="size">Column size</param> + /// <param name="isNullable">Is this property allowed to be null</param> + public SqlVariable(string variableName, DbType dataType, ParameterDirection direction, int size, bool isNullable) + { + this.VariableName = variableName; + this.DataType = dataType; + this.Direction = direction; + this.Size = size; + this.Nullable = isNullable; + } + } +} diff --git a/VNLib.Plugins.Extensions.Data/SQL/TableManager.cs b/VNLib.Plugins.Extensions.Data/SQL/TableManager.cs new file mode 100644 index 0000000..14c4e64 --- /dev/null +++ b/VNLib.Plugins.Extensions.Data/SQL/TableManager.cs @@ -0,0 +1,65 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Data +* File: TableManager.cs +* +* TableManager.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 General Public License as published +* by the Free Software Foundation, either version 2 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 +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/. +*/ + +using System; +using System.Data.Common; + +namespace VNLib.Plugins.Extensions.Data.SQL +{ + /// <summary> + /// A class that contains basic structures for interacting with an SQL driven database + /// </summary> + public abstract class TableManager + { + private readonly Func<DbConnection> Factory; + protected string Insert { get; set; } + protected string Select { get; set; } + protected string Update { get; set; } + protected string Delete { get; set; } + + /// <summary> + /// The name of the table specified during initialized + /// </summary> + protected string TableName { get; } + + public TableManager(Func<DbConnection> factory, string tableName) + { + this.Factory = factory ?? throw new ArgumentNullException(nameof(factory)); + this.TableName = !string.IsNullOrWhiteSpace(tableName) ? tableName : throw new ArgumentNullException(nameof(tableName)); + } + public TableManager(Func<DbConnection> factory) + { + this.Factory = factory ?? throw new ArgumentNullException(nameof(factory)); + this.TableName = ""; + } + /// <summary> + /// Opens a new <see cref="DbConnection"/> by invoking the factory callback method + /// </summary> + /// <returns>The open connection</returns> + protected DbConnection GetConnection() + { + return Factory(); + } + } +} diff --git a/VNLib.Plugins.Extensions.Data/Storage/Blob.cs b/VNLib.Plugins.Extensions.Data/Storage/Blob.cs new file mode 100644 index 0000000..ab18eeb --- /dev/null +++ b/VNLib.Plugins.Extensions.Data/Storage/Blob.cs @@ -0,0 +1,244 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Data +* File: Blob.cs +* +* Blob.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 General Public License as published +* by the Free Software Foundation, either version 2 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 +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/. +*/ + +using System; +using System.IO; +using System.Threading.Tasks; +using System.Runtime.Versioning; + +using VNLib.Utils; +using VNLib.Utils.IO; +using VNLib.Utils.Async; + +namespace VNLib.Plugins.Extensions.Data.Storage +{ + /// <summary> + /// Represents a stream of arbitrary binary data + /// </summary> + public class Blob : BackingStream<FileStream>, IObjectStorage, IAsyncExclusiveResource + { + protected readonly LWStorageDescriptor Descriptor; + + /// <summary> + /// The current blob's unique ID + /// </summary> + public string BlobId => Descriptor.DescriptorID; + /// <summary> + /// A value indicating if the <see cref="Blob"/> has been modified + /// </summary> + public bool Modified { get; protected set; } + /// <summary> + /// A valid indicating if the blob was flagged for deletiong + /// </summary> + public bool Deleted { get; protected set; } + + /// <summary> + /// The name of the file (does not change the actual file system name) + /// </summary> + public string Name + { + get => Descriptor.GetName(); + set => Descriptor.SetName(value); + } + /// <summary> + /// The UTC time the <see cref="Blob"/> was last modified + /// </summary> + public DateTimeOffset LastWriteTimeUtc => Descriptor.LastModified; + /// <summary> + /// The UTC time the <see cref="Blob"/> was created + /// </summary> + public DateTimeOffset CreationTimeUtc => Descriptor.Created; + + internal Blob(LWStorageDescriptor descriptor, in FileStream file) + { + this.Descriptor = descriptor; + base.BaseStream = file; + } + + /// <summary> + /// Prevents other processes from reading from or writing to the <see cref="Blob"/> + /// </summary> + /// <param name="position">The begining position of the range to lock</param> + /// <param name="length">The range to be locked</param> + /// <exception cref="IOException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("macos")] + [UnsupportedOSPlatform("tvos")] + public void Lock(long position, long length) => BaseStream.Lock(position, length); + /// <summary> + /// Prevents other processes from reading from or writing to the <see cref="Blob"/> + /// </summary> + /// <exception cref="IOException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("macos")] + [UnsupportedOSPlatform("tvos")] + public void Lock() => BaseStream.Lock(0, BaseStream.Length); + /// <summary> + /// Allows access by other processes to all or part of the <see cref="Blob"/> that was previously locked + /// </summary> + /// <param name="position">The begining position of the range to unlock</param> + /// <param name="length">The range to be unlocked</param> + /// <exception cref="ArgumentOutOfRangeException"></exception> + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("macos")] + [UnsupportedOSPlatform("tvos")] + public void Unlock(long position, long length) => BaseStream.Unlock(position, length); + /// <summary> + /// Allows access by other processes to the entire <see cref="Blob"/> + /// </summary> + /// <exception cref="ArgumentOutOfRangeException"></exception> + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("macos")] + [UnsupportedOSPlatform("tvos")] + public void Unlock() => BaseStream.Unlock(0, BaseStream.Length); + ///<inheritdoc/> + public override void SetLength(long value) + { + base.SetLength(value); + //Set modified flag + Modified |= true; + } + + /* + * Capture on-write calls to set the modified flag + */ + ///<inheritdoc/> + protected override void OnWrite(int count) => Modified |= true; + + T IObjectStorage.GetObject<T>(string key) => ((IObjectStorage)Descriptor).GetObject<T>(key); + void IObjectStorage.SetObject<T>(string key, T obj) => ((IObjectStorage)Descriptor).SetObject(key, obj); + + public string this[string index] + { + get => Descriptor[index]; + set => Descriptor[index] = value; + } + + + /// <summary> + /// Marks the file for deletion and will be deleted when the <see cref="Blob"/> is disposed + /// </summary> + public void Delete() + { + //Set deleted flag + Deleted |= true; + Descriptor.Delete(); + } + ///<inheritdoc/> + public bool IsReleased => Descriptor.IsReleased; + + + /// <summary> + /// <para> + /// If the <see cref="Blob"/> was opened with writing enabled, + /// and file was modified, changes are flushed to the backing store + /// and the stream is set to readonly. + /// </para> + /// <para> + /// If calls to this method succeed the stream is placed into a read-only mode + /// which will cause any calls to Write to throw a <see cref="NotSupportedException"/> + /// </para> + /// </summary> + /// <returns>A <see cref="ValueTask"/> that may be awaited until the operation completes</returns> + /// <remarks> + /// This method may be called to avoid flushing changes to the backing store + /// when the <see cref="Blob"/> is disposed (i.e. lifetime is manged outside of the desired scope) + /// </remarks> + /// <exception cref="IOException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="InvalidOperationException"></exception> + public async ValueTask FlushChangesAndSetReadonlyAsync() + { + if (Deleted) + { + throw new InvalidOperationException("The blob has been deleted and must be closed!"); + } + if (Modified) + { + //flush the base stream + await BaseStream.FlushAsync(); + //Update the file length in the store + Descriptor.SetLength(BaseStream.Length); + } + //flush changes, this will cause the dispose method to complete synchronously when closing + await Descriptor.WritePendingChangesAsync(); + //Clear modified flag + Modified = false; + //Set to readonly mode + base.ForceReadOnly = true; + } + + + /* + * Override the dispose async to manually dispose the + * base stream and avoid the syncrhonous (OnClose) + * method and allow awaiting the descriptor release + */ + ///<inheritdoc/> + public override async ValueTask DisposeAsync() + { + await ReleaseAsync(); + GC.SuppressFinalize(this); + } + ///<inheritdoc/> + public async ValueTask ReleaseAsync() + { + try + { + //Check for deleted + if (Deleted) + { + //Dispose the base stream explicitly + await BaseStream.DisposeAsync(); + //Try to delete the file + File.Delete(BaseStream.Name); + } + //Check to see if the file was modified + else if (Modified) + { + //Set the file size in bytes + Descriptor.SetLength(BaseStream.Length); + } + } + catch + { + //Set the error flag + Descriptor.IsError(true); + //propagate the exception + throw; + } + finally + { + //Dispose the stream + await BaseStream.DisposeAsync(); + //Release the descriptor + await Descriptor.ReleaseAsync(); + } + } + } +}
\ No newline at end of file diff --git a/VNLib.Plugins.Extensions.Data/Storage/BlobExtensions.cs b/VNLib.Plugins.Extensions.Data/Storage/BlobExtensions.cs new file mode 100644 index 0000000..468a66d --- /dev/null +++ b/VNLib.Plugins.Extensions.Data/Storage/BlobExtensions.cs @@ -0,0 +1,67 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Data +* File: BlobExtensions.cs +* +* BlobExtensions.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 General Public License as published +* by the Free Software Foundation, either version 2 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 +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/. +*/ + +using System; + +using VNLib.Utils; + +namespace VNLib.Plugins.Extensions.Data.Storage +{ + public static class BlobExtensions + { + public const string USER_ID_ENTRY = "__.uid"; + public const string VERSION_ENTRY = "__.vers"; + + private const string FILE_SIZE = "__.size"; + private const string FILE_NAME = "__.name"; + private const string ERROR_FLAG = "__.err"; + + public static string GetUserId(this Blob blob) => blob[USER_ID_ENTRY]; + /// <summary> + /// Gets the <see cref="Version"/> stored in the current <see cref="Blob"/> + /// </summary> + /// <returns>The sored version if previously set, thows otherwise</returns> + /// <exception cref="FormatException"></exception> + public static Version GetVersion(this Blob blob) => Version.Parse(blob[VERSION_ENTRY]); + /// <summary> + /// Sets a <see cref="Version"/> for the current <see cref="Blob"/> + /// </summary> + /// <param name="blob"></param> + /// <param name="version">The <see cref="Version"/> of the <see cref="Blob"/></param> + public static void SetVersion(this Blob blob, Version version) => blob[VERSION_ENTRY] = version.ToString(); + + /// <summary> + /// Gets a value indicating if the last operation left the <see cref="Blob"/> in an undefined state + /// </summary> + /// <returns>True if the <see cref="Blob"/> state is undefined, false otherwise</returns> + public static bool IsError(this Blob blob) => bool.TrueString.Equals(blob[ERROR_FLAG]); + internal static void IsError(this LWStorageDescriptor blob, bool value) => blob[ERROR_FLAG] = value ? bool.TrueString : null; + + internal static long GetLength(this LWStorageDescriptor blob) => (blob as IObjectStorage).GetObject<long>(FILE_SIZE); + internal static void SetLength(this LWStorageDescriptor blob, long length) => (blob as IObjectStorage).SetObject(FILE_SIZE, length); + + internal static string GetName(this LWStorageDescriptor blob) => blob[FILE_NAME]; + internal static string SetName(this LWStorageDescriptor blob, string filename) => blob[FILE_NAME] = filename; + } +}
\ No newline at end of file diff --git a/VNLib.Plugins.Extensions.Data/Storage/BlobStore.cs b/VNLib.Plugins.Extensions.Data/Storage/BlobStore.cs new file mode 100644 index 0000000..6897516 --- /dev/null +++ b/VNLib.Plugins.Extensions.Data/Storage/BlobStore.cs @@ -0,0 +1,162 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Data +* File: BlobStore.cs +* +* BlobStore.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 General Public License as published +* by the Free Software Foundation, either version 2 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 +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/. +*/ + +using System; +using System.IO; +using System.Security.Cryptography; +using System.Threading.Tasks; + +using VNLib.Utils.Extensions; + +namespace VNLib.Plugins.Extensions.Data.Storage +{ + + /// <summary> + /// Stores <see cref="Blob"/>s to the local file system backed with a single table <see cref="LWStorageManager"/> + /// that tracks changes + /// </summary> + public class BlobStore + { + /// <summary> + /// The root directory all blob files are stored + /// </summary> + public DirectoryInfo RootDir { get; } + /// <summary> + /// The backing store for blob meta-data + /// </summary> + protected LWStorageManager BlobTable { get; } + /// <summary> + /// Creates a new <see cref="BlobStore"/> that accesses files + /// within the specified root directory. + /// </summary> + /// <param name="rootDir">The root directory containing the blob file contents</param> + /// <param name="blobStoreMan">The db backing store</param> + public BlobStore(DirectoryInfo rootDir, LWStorageManager blobStoreMan) + { + RootDir = rootDir; + BlobTable = blobStoreMan; + } + + private string GetPath(string fileId) => Path.Combine(RootDir.FullName, fileId); + + /* + * Creates a repeatable unique identifier for the file + * name and allows for lookups + */ + internal static string CreateFileHash(string fileName) + { + throw new NotImplementedException(); + //return ManagedHash.ComputeBase64Hash(fileName, HashAlg.SHA1); + } + + /// <summary> + /// Opens an existing <see cref="Blob"/> from the current store + /// </summary> + /// <param name="fileId">The id of the file being requested</param> + /// <param name="access">Access level of the file</param> + /// <param name="share">The sharing option of the underlying file</param> + /// <param name="bufferSize">The size of the file buffer</param> + /// <returns>If found, the requested <see cref="Blob"/>, null otherwise. Throws exceptions if the file is opened in a non-sharable state</returns> + /// <exception cref="IOException"></exception> + /// <exception cref="NotSupportedException"></exception> + /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="UnauthorizedAccessException"></exception> + /// <exception cref="UndefinedBlobStateException"></exception> + public virtual async Task<Blob> OpenBlobAsync(string fileId, FileAccess access, FileShare share, int bufferSize = 4096) + { + //Get the file's data descriptor + LWStorageDescriptor fileDescriptor = await BlobTable.GetDescriptorFromIDAsync(fileId); + //return null if not found + if (fileDescriptor == null) + { + return null; + } + try + { + string fsSafeName = GetPath(fileDescriptor.DescriptorID); + //try to open the file + FileStream file = new(fsSafeName, FileMode.Open, access, share, bufferSize, FileOptions.Asynchronous); + //Create the new blob + return new Blob(fileDescriptor, file); + } + catch (FileNotFoundException) + { + //If the file was not found but the descriptor was, delete the descriptor from the db + fileDescriptor.Delete(); + //Flush changes + await fileDescriptor.ReleaseAsync(); + //return null since this is a desync issue and the file technically does not exist + return null; + } + catch + { + //Release the descriptor and pass the exception + await fileDescriptor.ReleaseAsync(); + throw; + } + } + + /// <summary> + /// Creates a new <see cref="Blob"/> for the specified file sharing permissions + /// </summary> + /// <param name="name">The name of the original file</param> + /// <param name="share">The blob sharing permissions</param> + /// <param name="bufferSize"></param> + /// <returns>The newly created <see cref="Blob"/></returns> + /// <exception cref="IoExtensions"></exception> + /// <exception cref="NotSupportedException"></exception> + /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="UnauthorizedAccessException"></exception> + public virtual async Task<Blob> CreateBlobAsync(string name, FileShare share = FileShare.None, int bufferSize = 4096) + { + //hash the file name to create a unique id for the file name + LWStorageDescriptor newFile = await BlobTable.CreateDescriptorAsync(CreateFileHash(name)); + //if the descriptor was not created, return null + if (newFile == null) + { + return null; + } + try + { + string fsSafeName = GetPath(newFile.DescriptorID); + //Open/create the new file + FileStream file = new(fsSafeName, FileMode.OpenOrCreate, FileAccess.ReadWrite, share, bufferSize, FileOptions.Asynchronous); + //If the file already exists, make sure its zero'd + file.SetLength(0); + //Save the original name of the file + newFile.SetName(name); + //Create and return the new blob + return new Blob(newFile, file); + } + catch + { + //If an exception occurs, remove the descritor from the db + newFile.Delete(); + await newFile.ReleaseAsync(); + //Pass exception + throw; + } + } + } +}
\ No newline at end of file diff --git a/VNLib.Plugins.Extensions.Data/Storage/LWDecriptorCreationException.cs b/VNLib.Plugins.Extensions.Data/Storage/LWDecriptorCreationException.cs new file mode 100644 index 0000000..db0dbbb --- /dev/null +++ b/VNLib.Plugins.Extensions.Data/Storage/LWDecriptorCreationException.cs @@ -0,0 +1,45 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Data +* File: LWDecriptorCreationException.cs +* +* LWDecriptorCreationException.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 General Public License as published +* by the Free Software Foundation, either version 2 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 +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/. +*/ + +using System; + +namespace VNLib.Plugins.Extensions.Data.Storage +{ + /// <summary> + /// Raised when an operation to create a new <see cref="LWStorageDescriptor"/> + /// fails + /// </summary> + public class LWDescriptorCreationException : Exception + { + ///<inheritdoc/> + public LWDescriptorCreationException() + {} + ///<inheritdoc/> + public LWDescriptorCreationException(string? message) : base(message) + {} + ///<inheritdoc/> + public LWDescriptorCreationException(string? message, Exception? innerException) : base(message, innerException) + {} + } +}
\ No newline at end of file diff --git a/VNLib.Plugins.Extensions.Data/Storage/LWStorageDescriptor.cs b/VNLib.Plugins.Extensions.Data/Storage/LWStorageDescriptor.cs new file mode 100644 index 0000000..3766a97 --- /dev/null +++ b/VNLib.Plugins.Extensions.Data/Storage/LWStorageDescriptor.cs @@ -0,0 +1,204 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Data +* File: LWStorageDescriptor.cs +* +* LWStorageDescriptor.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 General Public License as published +* by the Free Software Foundation, either version 2 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 +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/. +*/ + +using System; +using System.IO; +using System.Text.Json; +using System.Collections; +using System.Threading.Tasks; +using System.Collections.Generic; + +using VNLib.Utils; +using VNLib.Utils.Async; +using VNLib.Utils.Extensions; +using System.Text.Json.Serialization; + +namespace VNLib.Plugins.Extensions.Data.Storage +{ + /// <summary> + /// Represents an open storage object, that when released or disposed, will flush its changes to the underlying table + /// for which this descriptor represents + /// </summary> + public sealed class LWStorageDescriptor : AsyncUpdatableResource, IObjectStorage, IEnumerable<KeyValuePair<string, string>>, IIndexable<string, string> + { + + public static readonly JsonSerializerOptions SerializerOptions = new() + { + DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, + NumberHandling = JsonNumberHandling.Strict, + ReadCommentHandling = JsonCommentHandling.Disallow, + WriteIndented = false, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + IgnoreReadOnlyFields = true, + DefaultBufferSize = Environment.SystemPageSize, + }; + + private Dictionary<string, string> StringStorage; + + /// <summary> + /// The currnt descriptor's identifier string within its backing table. Usually the primary key. + /// </summary> + public string DescriptorID { get; init; } + /// <summary> + /// The identifier of the user for which this descriptor belongs to + /// </summary> + public string UserID { get; init; } + /// <summary> + /// The <see cref="DateTime"/> when the descriptor was created + /// </summary> + public DateTimeOffset Created { get; init; } + /// <summary> + /// The last time this descriptor was updated + /// </summary> + public DateTimeOffset LastModified { get; init; } + + ///<inheritdoc/> + protected override AsyncUpdateCallback UpdateCb { get; } + ///<inheritdoc/> + protected override AsyncDeleteCallback DeleteCb { get; } + ///<inheritdoc/> + protected override JsonSerializerOptions JSO => SerializerOptions; + + internal LWStorageDescriptor(LWStorageManager manager) + { + UpdateCb = manager.UpdateDescriptorAsync; + DeleteCb = manager.RemoveDescriptorAsync; + } + + internal async ValueTask PrepareAsync(Stream data) + { + try + { + //Deserialze async + StringStorage = await VnEncoding.JSONDeserializeFromBinaryAsync<Dictionary<string,string>>(data, SerializerOptions); + } + //Ignore a json exceton, a new store will be generated + catch (JsonException) + { } + StringStorage ??= new(); + } + + /// <inheritdoc/> + /// <exception cref="JsonException"></exception> + /// <exception cref="NotSupportedException"></exception> + /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + public T? GetObject<T>(string key) + { + //De-serialize and return object + return StringStorage.TryGetValue(key, out string? val) ? val.AsJsonObject<T>(SerializerOptions) : default; + } + + /// <inheritdoc/> + /// <exception cref="NotSupportedException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + public void SetObject<T>(string key, T obj) + { + //Remove the object from storage if its null + if (obj == null) + { + SetStringValue(key, null); + } + else + { + //Serialize the object to a string + string value = obj.ToJsonString(SerializerOptions)!; + //Attempt to store string in storage + SetStringValue(key, value); + } + } + + + /// <summary> + /// Gets a string value from string storage matching a given key + /// </summary> + /// <param name="key">Key for storage</param> + /// <returns>Value associaetd with key if exists, <see cref="string.Empty"/> otherwise</returns> + /// <exception cref="ArgumentNullException">If key is null</exception> + /// <exception cref="ObjectDisposedException"></exception> + public string GetStringValue(string key) + { + Check(); + return StringStorage.TryGetValue(key, out string? val) ? val : string.Empty; + } + + /// <summary> + /// Creates, overwrites, or removes a string value identified by key. + /// </summary> + /// <param name="key">Entry key</param> + /// <param name="value">String to store or overwrite, set to null or string.Empty to remove a property</param> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="ArgumentNullException">If key is null</exception> + public void SetStringValue(string key, string? value) + { + if (string.IsNullOrEmpty(key)) + { + throw new ArgumentNullException(nameof(key)); + } + Check(); + //If the value is null, see if the the properties are null + if (string.IsNullOrWhiteSpace(value)) + { + //If the value is null and properies exist, remove the entry + StringStorage.Remove(key); + Modified |= true; + } + else + { + //Set the value + StringStorage[key] = value; + //Set modified flag + Modified |= true; + } + } + + /// <summary> + /// Gets or sets a string value from string storage matching a given key + /// </summary> + /// <param name="key">Key for storage</param> + /// <returns>Value associaetd with key if exists, <seealso cref="string.Empty "/> otherwise</returns> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="ArgumentNullException">If key is null</exception> + public string this[string key] + { + get => GetStringValue(key); + set => SetStringValue(key, value); + } + + /// <summary> + /// Flushes all pending changes to the backing store asynchronously + /// </summary> + /// <exception cref="ObjectDisposedException"></exception> + public ValueTask WritePendingChangesAsync() + { + Check(); + return Modified ? (new(FlushPendingChangesAsync())) : ValueTask.CompletedTask; + } + ///<inheritdoc/> + public IEnumerator<KeyValuePair<string, string>> GetEnumerator() => StringStorage.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + ///<inheritdoc/> + protected override object GetResource() => StringStorage; + } +}
\ No newline at end of file diff --git a/VNLib.Plugins.Extensions.Data/Storage/LWStorageManager.cs b/VNLib.Plugins.Extensions.Data/Storage/LWStorageManager.cs new file mode 100644 index 0000000..63d41af --- /dev/null +++ b/VNLib.Plugins.Extensions.Data/Storage/LWStorageManager.cs @@ -0,0 +1,379 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Data +* File: LWStorageManager.cs +* +* LWStorageManager.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 General Public License as published +* by the Free Software Foundation, either version 2 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 +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/. +*/ + +using System; +using System.IO; +using System.Data; +using System.Threading; +using System.Data.Common; +using System.Threading.Tasks; + +using VNLib.Utils; + +using VNLib.Plugins.Extensions.Data.SQL; + +namespace VNLib.Plugins.Extensions.Data.Storage +{ + + /// <summary> + /// Provides single table database object storage services + /// </summary> + public sealed class LWStorageManager : EnumerableTable<LWStorageDescriptor> + { + const int DTO_SIZE = 7; + const int MAX_DATA_SIZE = 8000; + + //Mssql statments + private const string GET_DESCRIPTOR_STATMENT_ID_MSQQL = "SELECT TOP 1\r\n[Id],\r\n[UserID],\r\n[Data],\r\n[Created],\r\n[LastModified]\r\nFROM @table\r\nWHERE Id=@Id;"; + private const string GET_DESCRIPTOR_STATMENT_UID_MSQL = "SELECT TOP 1\r\n[Id],\r\n[UserID],\r\n[Data],\r\n[Created],\r\n[LastModified]\r\nFROM @table\r\nWHERE UserID=@UserID;"; + + private const string GET_DESCRIPTOR_STATMENT_ID = "SELECT\r\n[Id],\r\n[UserID],\r\n[Data],\r\n[Created],\r\n[LastModified]\r\nFROM @table\r\nWHERE Id=@Id\r\nLIMIT 1;"; + private const string GET_DESCRIPTOR_STATMENT_UID = "SELECT\r\n[Id],\r\n[UserID],\r\n[Data],\r\n[Created],\r\n[LastModified]\r\nFROM @table\r\nWHERE UserID=@UserID\r\nLIMIT 1;"; + + private const string CREATE_DESCRIPTOR_STATMENT = "INSERT INTO @table\r\n(UserID,Id,Created,LastModified)\r\nVALUES (@UserID,@Id,@Created,@LastModified);"; + + private const string UPDATE_DESCRIPTOR_STATMENT = "UPDATE @table\r\nSET [Data]=@Data\r\n,[LastModified]=@LastModified\r\nWHERE Id=@Id;"; + private const string REMOVE_DESCRIPTOR_STATMENT = "DELETE FROM @table\r\nWHERE Id=@Id"; + private const string CLEANUP_STATEMENT = "DELETE FROM @table\r\nWHERE [created]<@timeout;"; + private const string ENUMERATION_STATMENT = "SELECT [Id],\r\n[UserID],\r\n[Data],\r\n[LastModified],\r\n[Created]\r\nFROM @table;"; + + private readonly string GetFromUD; + private readonly string Cleanup; + private readonly int keySize; + + /// <summary> + /// The generator function that is invoked when a new <see cref="LWStorageDescriptor"/> is to + /// be created without an explicit id + /// </summary> + public Func<string> NewDescriptorIdGenerator { get; init; } = static () => Guid.NewGuid().ToString("N"); + + /// <summary> + /// Creates a new <see cref="LWStorageManager"/> with + /// </summary> + /// <param name="factory">A <see cref="DbConnection"/> factory function that will generate and open connections to a database</param> + /// <param name="tableName">The name of the table to operate on</param> + /// <param name="pkCharSize">The maximum number of characters of the DescriptorID and </param> + /// <exception cref="ArgumentException"></exception> + /// <exception cref="ArgumentNullException"></exception> + public LWStorageManager(Func<DbConnection> factory, string tableName, int pkCharSize) : base(factory, tableName) + { + //Compile statments with specified tableid + Insert = CREATE_DESCRIPTOR_STATMENT.Replace("@table", tableName); + + //Test connector type to compile MSSQL statments vs Sqlite/Mysql + using (DbConnection testConnection = GetConnection()) + { + //Determine if MSSql connections are being used + bool isMsSql = testConnection.GetType().FullName!.Contains("SqlConnection", StringComparison.OrdinalIgnoreCase); + + if (isMsSql) + { + GetFromUD = GET_DESCRIPTOR_STATMENT_UID_MSQL.Replace("@table", tableName); + Select = GET_DESCRIPTOR_STATMENT_ID_MSQQL.Replace("@table", tableName); + } + else + { + Select = GET_DESCRIPTOR_STATMENT_ID.Replace("@table", tableName); + GetFromUD = GET_DESCRIPTOR_STATMENT_UID.Replace("@table", tableName); + } + } + + Update = UPDATE_DESCRIPTOR_STATMENT.Replace("@table", tableName); + Delete = REMOVE_DESCRIPTOR_STATMENT.Replace("@table", tableName); + Cleanup = CLEANUP_STATEMENT.Replace("@table", tableName); + //Set key size + keySize = pkCharSize; + //Set default generator + Enumerate = ENUMERATION_STATMENT.Replace("@table", tableName); + } + + /// <summary> + /// Creates a new <see cref="LWStorageDescriptor"/> fror a given user + /// </summary> + /// <param name="userId">Id of user</param> + /// <param name="descriptorIdOverride">An override to specify the new descriptor's id</param> + /// <param name="cancellation">A token to cancel the operation</param> + /// <returns>A new <see cref="LWStorageDescriptor"/> if successfully created, null otherwise</returns> + /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="LWDescriptorCreationException"></exception> + public async Task<LWStorageDescriptor> CreateDescriptorAsync(string userId, string? descriptorIdOverride = null, CancellationToken cancellation = default) + { + if (string.IsNullOrWhiteSpace(userId)) + { + throw new ArgumentNullException(nameof(userId)); + } + //If no override id was specified, generate a new one + descriptorIdOverride ??= NewDescriptorIdGenerator(); + //Set created time + DateTimeOffset now = DateTimeOffset.UtcNow; + //Open a new sql client + await using DbConnection Database = GetConnection(); + await Database.OpenAsync(cancellation); + //Setup transaction with repeatable read iso level + await using DbTransaction transaction = await Database.BeginTransactionAsync(IsolationLevel.Serializable, cancellation); + //Create command for text command + await using DbCommand cmd = Database.CreateTextCommand(Insert, transaction); + //add parameters + _ = cmd.AddParameter("@Id", descriptorIdOverride, DbType.String, keySize); + _ = cmd.AddParameter("@UserID", userId, DbType.String, keySize); + _ = cmd.AddParameter("@Created", now, DbType.DateTimeOffset, DTO_SIZE); + _ = cmd.AddParameter("@LastModified", now, DbType.DateTimeOffset, DTO_SIZE); + //Prepare operation + await cmd.PrepareAsync(cancellation); + //Exec and if successful will return > 0, so we can properly return a descriptor + int result = await cmd.ExecuteNonQueryAsync(cancellation); + //Commit transaction + await transaction.CommitAsync(cancellation); + if (result <= 0) + { + throw new LWDescriptorCreationException("Failed to create the new descriptor because the database retuned an invalid update row count"); + } + //Rent new descriptor + LWStorageDescriptor desciptor = new(this) + { + DescriptorID = descriptorIdOverride, + UserID = userId, + Created = now, + LastModified = now + }; + //Set data to null + await desciptor.PrepareAsync(null); + return desciptor; + } + /// <summary> + /// Attempts to retrieve <see cref="LWStorageDescriptor"/> for a given user-id. The caller is responsible for + /// consitancy state of the descriptor + /// </summary> + /// <param name="userid">User's id</param> + /// <param name="cancellation">A token to cancel the operation</param> + /// <returns>The descriptor belonging to the user, or null if not found or error occurs</returns> + /// <exception cref="ArgumentNullException"></exception> + public async Task<LWStorageDescriptor?> GetDescriptorFromUIDAsync(string userid, CancellationToken cancellation = default) + { + if (string.IsNullOrWhiteSpace(userid)) + { + throw new ArgumentNullException(nameof(userid)); + } + //Open a new sql client + await using DbConnection Database = GetConnection(); + await Database.OpenAsync(cancellation); + //Setup transaction with repeatable read iso level + await using DbTransaction transaction = await Database.BeginTransactionAsync(IsolationLevel.RepeatableRead, cancellation); + //Create a new command based on the command text + await using DbCommand cmd = Database.CreateTextCommand(GetFromUD, transaction); + //Add userid parameter + _ = cmd.AddParameter("@UserID", userid, DbType.String, keySize); + //Prepare operation + await cmd.PrepareAsync(cancellation); + //Get the reader + DbDataReader reader = await cmd.ExecuteReaderAsync(CommandBehavior.SingleRow, cancellation); + try + { + //Make sure the record was found + if (!await reader.ReadAsync(cancellation)) + { + return null; + } + return await GetItemAsync(reader, CancellationToken.None); + } + finally + { + //Close the reader + await reader.CloseAsync(); + //Commit the transaction + await transaction.CommitAsync(cancellation); + } + } + /// <summary> + /// Attempts to retrieve the <see cref="LWStorageDescriptor"/> for the given descriptor id. The caller is responsible for + /// consitancy state of the descriptor + /// </summary> + /// <param name="descriptorId">Unique identifier for the descriptor</param> + /// <returns>The descriptor belonging to the user, or null if not found or error occurs</returns> + /// <exception cref="ArgumentNullException"></exception> + public async Task<LWStorageDescriptor?> GetDescriptorFromIDAsync(string descriptorId, CancellationToken cancellation = default) + { + //Allow null/empty entrys to just return null + if (string.IsNullOrWhiteSpace(descriptorId)) + { + throw new ArgumentNullException(nameof(descriptorId)); + } + //Open a new sql client + await using DbConnection Database = GetConnection(); + await Database.OpenAsync(cancellation); + //Setup transaction with repeatable read iso level + await using DbTransaction transaction = await Database.BeginTransactionAsync(IsolationLevel.RepeatableRead, cancellation); + //We dont have the routine stored + await using DbCommand cmd = Database.CreateTextCommand(Select, transaction); + //Set userid (unicode length) + _ = cmd.AddParameter("@Id", descriptorId, DbType.String, keySize); + //Prepare operation + await cmd.PrepareAsync(cancellation); + //Get the reader + DbDataReader reader = await cmd.ExecuteReaderAsync(CommandBehavior.SingleRow, cancellation); + try + { + if (!await reader.ReadAsync(cancellation)) + { + return null; + } + return await GetItemAsync(reader, CancellationToken.None); + } + finally + { + //Close the reader + await reader.CloseAsync(); + //Commit the transaction + await transaction.CommitAsync(cancellation); + } + } + /// <summary> + /// Cleanup entries before the specified <see cref="TimeSpan"/>. Entires are store in UTC time + /// </summary> + /// <param name="compareTime">Time before <see cref="DateTime.UtcNow"/> to compare against</param> + /// <param name="cancellation">A token to cancel the operation</param> + /// <returns>The number of entires cleaned</returns>S + public Task<ERRNO> CleanupTableAsync(TimeSpan compareTime, CancellationToken cancellation = default) => CleanupTableAsync(DateTime.UtcNow.Subtract(compareTime), cancellation); + /// <summary> + /// Cleanup entries before the specified <see cref="DateTime"/>. Entires are store in UTC time + /// </summary> + /// <param name="compareTime">UTC time to compare entires against</param> + /// <param name="cancellation">A token to cancel the operation</param> + /// <returns>The number of entires cleaned</returns> + public async Task<ERRNO> CleanupTableAsync(DateTime compareTime, CancellationToken cancellation = default) + { + //Open a new sql client + await using DbConnection Database = GetConnection(); + await Database.OpenAsync(cancellation); + //Begin a new transaction + await using DbTransaction transaction = await Database.BeginTransactionAsync(IsolationLevel.Serializable, cancellation); + //Setup the cleanup command for the current database + await using DbCommand cmd = Database.CreateTextCommand(Cleanup, transaction); + //Setup timeout parameter as a datetime + cmd.AddParameter("@timeout", compareTime, DbType.DateTime); + await cmd.PrepareAsync(cancellation); + //Exec and if successful will return > 0, so we can properly return a descriptor + int result = await cmd.ExecuteNonQueryAsync(cancellation); + //Commit transaction + await transaction.CommitAsync(cancellation); + return result; + } + + /// <summary> + /// Updates a descriptor's data field + /// </summary> + /// <param name="descriptorObj">Descriptor to update</param> + /// <param name="data">Data string to store to descriptor record</param> + /// <exception cref="LWStorageUpdateFailedException"></exception> + internal async Task UpdateDescriptorAsync(object descriptorObj, Stream data) + { + LWStorageDescriptor descriptor = (descriptorObj as LWStorageDescriptor)!; + int result = 0; + try + { + //Open a new sql client + await using DbConnection Database = GetConnection(); + await Database.OpenAsync(); + //Setup transaction with repeatable read iso level + await using DbTransaction transaction = await Database.BeginTransactionAsync(IsolationLevel.Serializable); + //Create command for stored procedure + await using DbCommand cmd = Database.CreateTextCommand(Update, transaction); + //Add parameters + _ = cmd.AddParameter("@Id", descriptor.DescriptorID, DbType.String, keySize); + _ = cmd.AddParameter("@Data", data, DbType.Binary, MAX_DATA_SIZE); + _ = cmd.AddParameter("@LastModified", DateTime.UtcNow, DbType.DateTime2, DTO_SIZE); + //Prepare operation + await cmd.PrepareAsync(); + //exec and store result + result = await cmd.ExecuteNonQueryAsync(); + //Commit + await transaction.CommitAsync(); + } + catch (Exception ex) + { + throw new LWStorageUpdateFailedException("", ex); + } + //If the result is 0 then the update failed + if (result <= 0) + { + throw new LWStorageUpdateFailedException($"Descriptor {descriptor.DescriptorID} failed to update", null); + } + } + /// <summary> + /// Function to remove the specified descriptor + /// </summary> + /// <param name="descriptorObj">The active descriptor to remove from the database</param> + /// <exception cref="LWStorageRemoveFailedException"></exception> + internal async Task RemoveDescriptorAsync(object descriptorObj) + { + LWStorageDescriptor descriptor = (descriptorObj as LWStorageDescriptor)!; + try + { + //Open a new sql client + await using DbConnection Database = GetConnection(); + await Database.OpenAsync(); + //Setup transaction with repeatable read iso level + await using DbTransaction transaction = await Database.BeginTransactionAsync(IsolationLevel.Serializable); + //Create sql command + await using DbCommand cmd = Database.CreateTextCommand(Delete, transaction); + //set descriptor id + _ = cmd.AddParameter("@Id", descriptor.DescriptorID, DbType.String, keySize); + //Prepare operation + await cmd.PrepareAsync(); + //Execute (the descriptor my already be removed, as long as the transaction doesnt fail we should be okay) + _ = await cmd.ExecuteNonQueryAsync(); + //Commit + await transaction.CommitAsync(); + } + catch (Exception ex) + { + throw new LWStorageRemoveFailedException("", ex); + } + } + + ///<inheritdoc/> + protected async override Task<LWStorageDescriptor> GetItemAsync(DbDataReader reader, CancellationToken cancellationToken) + { + //Open binary stream for the data column + await using Stream data = reader.GetStream("Data"); + //Create new descriptor + LWStorageDescriptor desciptor = new(this) + { + //Set desctiptor data + DescriptorID = reader.GetString("Id"), + UserID = reader.GetString("UserID"), + Created = reader.GetDateTime("Created"), + LastModified = reader.GetDateTime("LastModified") + }; + //Load the descriptor's data + await desciptor.PrepareAsync(data); + return desciptor; + } + ///<inheritdoc/> + protected override ValueTask CleanupItemAsync(LWStorageDescriptor item, CancellationToken cancellationToken) + { + return item.ReleaseAsync(); + } + } +}
\ No newline at end of file diff --git a/VNLib.Plugins.Extensions.Data/Storage/LWStorageRemoveFailedException.cs b/VNLib.Plugins.Extensions.Data/Storage/LWStorageRemoveFailedException.cs new file mode 100644 index 0000000..8e36d6c --- /dev/null +++ b/VNLib.Plugins.Extensions.Data/Storage/LWStorageRemoveFailedException.cs @@ -0,0 +1,38 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Data +* File: LWStorageRemoveFailedException.cs +* +* LWStorageRemoveFailedException.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 General Public License as published +* by the Free Software Foundation, either version 2 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 +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/. +*/ + +using System; +using VNLib.Utils; + +namespace VNLib.Plugins.Extensions.Data.Storage +{ + /// <summary> + /// The exception raised when an open <see cref="LWStorageDescriptor"/> removal operation fails. The + /// <see cref="Exception.InnerException"/> property may contain any nested exceptions that caused the removal to fail. + /// </summary> + public class LWStorageRemoveFailedException : ResourceDeleteFailedException + { + internal LWStorageRemoveFailedException(string error, Exception inner) : base(error, inner) { } + } +}
\ No newline at end of file diff --git a/VNLib.Plugins.Extensions.Data/Storage/LWStorageUpdateFailedException.cs b/VNLib.Plugins.Extensions.Data/Storage/LWStorageUpdateFailedException.cs new file mode 100644 index 0000000..96ea4eb --- /dev/null +++ b/VNLib.Plugins.Extensions.Data/Storage/LWStorageUpdateFailedException.cs @@ -0,0 +1,38 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Data +* File: LWStorageUpdateFailedException.cs +* +* LWStorageUpdateFailedException.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 General Public License as published +* by the Free Software Foundation, either version 2 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 +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/. +*/ + +using System; +using VNLib.Utils; + +namespace VNLib.Plugins.Extensions.Data.Storage +{ + /// <summary> + /// The exception raised when an open <see cref="LWStorageDescriptor"/> update operation fails. The + /// <see cref="Exception.InnerException"/> property may contain any nested exceptions that caused the update to fail. + /// </summary> + public class LWStorageUpdateFailedException : ResourceUpdateFailedException + { + internal LWStorageUpdateFailedException(string error, Exception inner) : base(error, inner) { } + } +}
\ No newline at end of file diff --git a/VNLib.Plugins.Extensions.Data/Storage/UndefinedBlobStateException.cs b/VNLib.Plugins.Extensions.Data/Storage/UndefinedBlobStateException.cs new file mode 100644 index 0000000..e845372 --- /dev/null +++ b/VNLib.Plugins.Extensions.Data/Storage/UndefinedBlobStateException.cs @@ -0,0 +1,45 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Data +* File: UndefinedBlobStateException.cs +* +* UndefinedBlobStateException.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 General Public License as published +* by the Free Software Foundation, either version 2 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 +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/. +*/ + +using System; +using System.Runtime.Serialization; + +namespace VNLib.Plugins.Extensions.Data.Storage +{ + /// <summary> + /// Raised to signal that the requested <see cref="Blob"/> was left in an undefined state + /// when previously accessed + /// </summary> + public class UndefinedBlobStateException : Exception + { + public UndefinedBlobStateException() + {} + public UndefinedBlobStateException(string message) : base(message) + {} + public UndefinedBlobStateException(string message, Exception innerException) : base(message, innerException) + {} + protected UndefinedBlobStateException(SerializationInfo info, StreamingContext context) : base(info, context) + {} + } +}
\ No newline at end of file diff --git a/VNLib.Plugins.Extensions.Data/VNLib.Plugins.Extensions.Data.csproj b/VNLib.Plugins.Extensions.Data/VNLib.Plugins.Extensions.Data.csproj index b008df4..2a780aa 100644 --- a/VNLib.Plugins.Extensions.Data/VNLib.Plugins.Extensions.Data.csproj +++ b/VNLib.Plugins.Extensions.Data/VNLib.Plugins.Extensions.Data.csproj @@ -3,7 +3,7 @@ <PropertyGroup> <TargetFramework>net6.0</TargetFramework> <RootNamespace>VNLib.Plugins.Extensions.Data</RootNamespace> - <Platforms>AnyCPU;x64</Platforms> + </PropertyGroup> <!-- Resolve nuget dll files and store them in the output dir --> @@ -13,13 +13,23 @@ <Authors>Vaughn Nugent</Authors> <Description>Data extensions for VNLib Plugins</Description> <Copyright>Copyright © 2022 Vaughn Nugent</Copyright> - <PackageProjectUrl>www.vaughnnugent.com/resources</PackageProjectUrl> - <Version>1.0.0.1</Version> - <GeneratePackageOnBuild>false</GeneratePackageOnBuild> + <PackageProjectUrl>https://www.vaughnnugent.com/resources</PackageProjectUrl> + <Version>1.0.1.1</Version> <Nullable>enable</Nullable> </PropertyGroup> - <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> - <DocumentationFile>l</DocumentationFile> + + <PropertyGroup> + <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> + <GenerateDocumentationFile>True</GenerateDocumentationFile> + <AnalysisLevel>latest-all</AnalysisLevel> + </PropertyGroup> + + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> + <Deterministic>False</Deterministic> + </PropertyGroup> + + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> + <Deterministic>False</Deterministic> </PropertyGroup> <ItemGroup> diff --git a/VNLib.Plugins.Extensions.Data/l b/VNLib.Plugins.Extensions.Data/l deleted file mode 100644 index 72817ee..0000000 --- a/VNLib.Plugins.Extensions.Data/l +++ /dev/null @@ -1,473 +0,0 @@ -<?xml version="1.0"?> -<doc> - <assembly> - <name>VNLib.Plugins.Extensions.Data</name> - </assembly> - <members> - <member name="T:VNLib.Plugins.Extensions.Data.Abstractions.IBulkDataStore`1"> - <summary> - An abstraction that defines a Data-Store that supports - bulk data operations - </summary> - <typeparam name="T">The data-model type</typeparam> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.Abstractions.IBulkDataStore`1.DeleteBulkAsync(System.Collections.Generic.ICollection{`0})"> - <summary> - Deletes a collection of records from the store - </summary> - <param name="records">A collection of records to delete</param> - <returns>A task the resolves the number of entires removed from the store</returns> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.Abstractions.IBulkDataStore`1.UpdateBulkAsync(System.Collections.Generic.ICollection{`0})"> - <summary> - Updates a collection of records - </summary> - <param name="records">The collection of records to update</param> - <returns>A task the resolves an error code (should evaluate to false on failure, and true on success)</returns> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.Abstractions.IBulkDataStore`1.CreateBulkAsync(System.Collections.Generic.ICollection{`0})"> - <summary> - Creates a bulk collection of records as entries in the store - </summary> - <param name="records">The collection of records to add</param> - <returns>A task the resolves an error code (should evaluate to false on failure, and true on success)</returns> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.Abstractions.IBulkDataStore`1.AddOrUpdateBulkAsync(System.Collections.Generic.ICollection{`0})"> - <summary> - Creates or updates individual records from a bulk collection of records - </summary> - <param name="records">The collection of records to add</param> - <returns>A task the resolves an error code (should evaluate to false on failure, and true on success)</returns> - </member> - <member name="T:VNLib.Plugins.Extensions.Data.Abstractions.IDataStore`1"> - <summary> - An abstraction that defines a Data-Store and common - operations that retrieve or manipulate records of data - </summary> - <typeparam name="T">The data-model type</typeparam> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.Abstractions.IDataStore`1.GetCountAsync"> - <summary> - Gets the total number of records in the current store - </summary> - <returns>A task that resolves the number of records in the store</returns> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.Abstractions.IDataStore`1.GetCountAsync(System.String)"> - <summary> - Gets the number of records that belong to the specified constraint - </summary> - <param name="specifier">A specifier to constrain the reults</param> - <returns>The number of records that belong to the specifier</returns> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.Abstractions.IDataStore`1.GetSingleAsync(System.String)"> - <summary> - Gets a record from its key - </summary> - <param name="key">The key identifying the unique record</param> - <returns>A promise that resolves the record identified by the specified key</returns> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.Abstractions.IDataStore`1.GetSingleAsync(System.String[])"> - <summary> - Gets a record from its key - </summary> - <param name="specifiers">A variable length specifier arguemnt array for retreiving a single application</param> - <returns></returns> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.Abstractions.IDataStore`1.GetSingleAsync(`0)"> - <summary> - Gets a record from the store with a partial model, intended to complete the model - </summary> - <param name="record">The partial model used to query the store</param> - <returns>A task the resolves the completed data-model</returns> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.Abstractions.IDataStore`1.GetCollectionAsync(System.Collections.Generic.ICollection{`0},System.String,System.Int32)"> - <summary> - Fills a collection with enires retireved from the store using the specifer - </summary> - <param name="collection">The collection to add entires to</param> - <param name="specifier">A specifier argument to constrain results</param> - <param name="limit">The maximum number of elements to retrieve</param> - <returns>A Task the resolves to the number of items added to the collection</returns> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.Abstractions.IDataStore`1.GetCollectionAsync(System.Collections.Generic.ICollection{`0},System.Int32,System.String[])"> - <summary> - Fills a collection with enires retireved from the store using a variable length specifier - parameter - </summary> - <param name="collection">The collection to add entires to</param> - <param name="limit">The maximum number of elements to retrieve</param> - <param name="args"></param> - <returns>A Task the resolves to the number of items added to the collection</returns> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.Abstractions.IDataStore`1.UpdateAsync(`0)"> - <summary> - Updates an entry in the store with the specified record - </summary> - <param name="record">The record to update</param> - <returns>A task the resolves an error code (should evaluate to false on failure, and true on success)</returns> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.Abstractions.IDataStore`1.CreateAsync(`0)"> - <summary> - Creates a new entry in the store representing the specified record - </summary> - <param name="record">The record to add to the store</param> - <returns>A task the resolves an error code (should evaluate to false on failure, and true on success)</returns> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.Abstractions.IDataStore`1.DeleteAsync(`0)"> - <summary> - Deletes one or more entrires from the store matching the specified record - </summary> - <param name="record">The record to remove from the store</param> - <returns>A task the resolves the number of records removed(should evaluate to false on failure, and deleted count on success)</returns> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.Abstractions.IDataStore`1.DeleteAsync(System.String)"> - <summary> - Deletes one or more entires from the store matching the specified unique key - </summary> - <param name="key">The unique key that identifies the record</param> - <returns>A task the resolves the number of records removed(should evaluate to false on failure, and deleted count on success)</returns> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.Abstractions.IDataStore`1.DeleteAsync(System.String[])"> - <summary> - Deletes one or more entires from the store matching the supplied specifiers - </summary> - <param name="specifiers">A variable length array of specifiers used to delete one or more entires</param> - <returns>A task the resolves the number of records removed(should evaluate to false on failure, and deleted count on success)</returns> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.Abstractions.IDataStore`1.AddOrUpdateAsync(`0)"> - <summary> - Updates an entry in the store if it exists, or creates a new entry if one does not already exist - </summary> - <param name="record">The record to add to the store</param> - <returns>A task the resolves the result of the operation</returns> - </member> - <member name="T:VNLib.Plugins.Extensions.Data.Abstractions.IPaginatedDataStore`1"> - <summary> - Defines a Data-Store that can retirieve and manipulate paginated - data - </summary> - <typeparam name="T">The data-model type</typeparam> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.Abstractions.IPaginatedDataStore`1.GetPageAsync(System.Collections.Generic.ICollection{`0},System.Int32,System.Int32)"> - <summary> - Gets a collection of records using a pagination style query, and adds the records to the collecion - </summary> - <param name="collection">The collection to add records to</param> - <param name="page">Pagination page to get records from</param> - <param name="limit">The maximum number of items to retrieve from the store</param> - <returns>A task that resolves the number of items added to the collection</returns> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.Abstractions.IPaginatedDataStore`1.GetPageAsync(System.Collections.Generic.ICollection{`0},System.Int32,System.Int32,System.String[])"> - <summary> - Gets a collection of records using a pagination style query with constraint arguments, and adds the records to the collecion - </summary> - <param name="collection">The collection to add records to</param> - <param name="page">Pagination page to get records from</param> - <param name="limit">The maximum number of items to retrieve from the store</param> - <param name="constraints">A params array of strings to constrain the result set from the db</param> - <returns>A task that resolves the number of items added to the collection</returns> - </member> - <member name="T:VNLib.Plugins.Extensions.Data.Abstractions.IUserEntity"> - <summary> - Defines an entity base that has an owner, identified by its user-id - </summary> - </member> - <member name="P:VNLib.Plugins.Extensions.Data.Abstractions.IUserEntity.UserId"> - <summary> - The user-id of the owner of the entity - </summary> - </member> - <member name="T:VNLib.Plugins.Extensions.Data.DbModelBase"> - <summary> - Provides a base for DBSet Records with a timestamp/version - a unique ID key, and create/modified timestamps - </summary> - </member> - <member name="P:VNLib.Plugins.Extensions.Data.DbModelBase.Id"> - <inheritdoc/> - </member> - <member name="P:VNLib.Plugins.Extensions.Data.DbModelBase.Version"> - <inheritdoc/> - </member> - <member name="P:VNLib.Plugins.Extensions.Data.DbModelBase.Created"> - <inheritdoc/> - </member> - <member name="P:VNLib.Plugins.Extensions.Data.DbModelBase.LastModified"> - <inheritdoc/> - </member> - <member name="T:VNLib.Plugins.Extensions.Data.DbStore`1"> - <summary> - Implements basic data-store functionality with abstract query builders - </summary> - <typeparam name="T">A <see cref="T:VNLib.Plugins.Extensions.Data.DbModelBase"/> implemented type</typeparam> - </member> - <member name="P:VNLib.Plugins.Extensions.Data.DbStore`1.RecordIdBuilder"> - <summary> - Gets a unique ID for a new record being added to the store - </summary> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.NewContext"> - <summary> - Gets a new <see cref="T:VNLib.Plugins.Extensions.Data.TransactionalDbContext"/> ready for use - </summary> - <returns></returns> - </member> - <member name="P:VNLib.Plugins.Extensions.Data.DbStore`1.ListRental"> - <summary> - An object rental for entity collections - </summary> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.AddOrUpdateAsync(`0)"> - <inheritdoc/> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.UpdateAsync(`0)"> - <inheritdoc/> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.CreateAsync(`0)"> - <inheritdoc/> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.AddOrUpdateQueryBuilder(VNLib.Plugins.Extensions.Data.TransactionalDbContext,`0)"> - <summary> - 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 <see cref="P:VNLib.Plugins.Extensions.Data.DbModelBase.Id"/> property - </summary> - <param name="context">The active context to query</param> - <param name="record">The record to search for</param> - <returns>A query that yields a single record if it exists in the store</returns> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.UpdateQueryBuilder(VNLib.Plugins.Extensions.Data.TransactionalDbContext,`0)"> - <summary> - Builds a query that attempts to get a single entry from the - store to update based on the specified record - </summary> - <param name="context">The active context to query</param> - <param name="record">The record to search for</param> - <returns>A query that yields a single record to update if it exists in the store</returns> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.OnRecordUpdate(`0,`0)"> - <summary> - Updates the current record (if found) to the new record before - storing the updates. - </summary> - <param name="newRecord">The new record to capture data from</param> - <param name="currentRecord">The current record to be updated</param> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.DeleteAsync(System.String)"> - <inheritdoc/> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.DeleteAsync(`0)"> - <inheritdoc/> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.DeleteAsync(System.String[])"> - <inheritdoc/> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.DeleteQueryBuilder(VNLib.Plugins.Extensions.Data.TransactionalDbContext,System.String[])"> - <summary> - Builds a query that results in a single entry to delete from the - constraint arguments - </summary> - <param name="context">The active context</param> - <param name="constraints">A variable length parameter array of query constraints</param> - <returns>A query that yields a single record (or no record) to delete</returns> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.GetCollectionAsync(System.Collections.Generic.ICollection{`0},System.String,System.Int32)"> - <inheritdoc/> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.GetCollectionAsync(System.Collections.Generic.ICollection{`0},System.Int32,System.String[])"> - <inheritdoc/> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.GetCollectionQueryBuilder(VNLib.Plugins.Extensions.Data.TransactionalDbContext,System.String)"> - <summary> - Builds a query to get a count of records constrained by the specifier - </summary> - <param name="context">The active context to run the query on</param> - <param name="specifier">The specifier constrain</param> - <returns>A query that can be counted</returns> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.GetCollectionQueryBuilder(VNLib.Plugins.Extensions.Data.TransactionalDbContext,System.String[])"> - <summary> - Builds a query to get a collection of records based on an variable length array of parameters - </summary> - <param name="context">The active context to run the query on</param> - <param name="constraints">An arguments array to constrain the results of the query</param> - <returns>A query that returns a collection of records from the store</returns> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.GetCountAsync"> - <inheritdoc/> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.GetCountAsync(System.String)"> - <inheritdoc/> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.GetCountQueryBuilder(VNLib.Plugins.Extensions.Data.TransactionalDbContext,System.String)"> - <summary> - Builds a query to get a count of records constrained by the specifier - </summary> - <param name="context">The active context to run the query on</param> - <param name="specifier">The specifier constrain</param> - <returns>A query that can be counted</returns> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.GetSingleAsync(System.String)"> - <inheritdoc/> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.GetSingleAsync(`0)"> - <inheritdoc/> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.GetSingleAsync(System.String[])"> - <inheritdoc/> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.GetSingleQueryBuilder(VNLib.Plugins.Extensions.Data.TransactionalDbContext,System.String[])"> - <summary> - Builds a query to get a single record from the variable length parameter arguments - </summary> - <param name="context">The context to execute query against</param> - <param name="constraints">Arguments to constrain the results of the query to a single record</param> - <returns>A query that yields a single record</returns> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.GetSingleQueryBuilder(VNLib.Plugins.Extensions.Data.TransactionalDbContext,`0)"> - <summary> - <para> - Builds a query to get a single record from the specified record. - </para> - <para> - Unless overridden, performs an ID based query for a single entry - </para> - </summary> - <param name="context">The context to execute query against</param> - <param name="record">A record to referrence the lookup</param> - <returns>A query that yields a single record</returns> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.GetPageAsync(System.Collections.Generic.ICollection{`0},System.Int32,System.Int32)"> - <inheritdoc/> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.GetPageAsync(System.Collections.Generic.ICollection{`0},System.Int32,System.Int32,System.String[])"> - <inheritdoc/> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.GetPageQueryBuilder(VNLib.Plugins.Extensions.Data.TransactionalDbContext,System.String[])"> - <summary> - Builds a query to get a collection of records based on an variable length array of parameters - </summary> - <param name="context">The active context to run the query on</param> - <param name="constraints">An arguments array to constrain the results of the query</param> - <returns>A query that returns a paginated collection of records from the store</returns> - </member> - <member name="T:VNLib.Plugins.Extensions.Data.IDbModel"> - <summary> - Represents a basic data model for an EFCore entity - for support in data-stores - </summary> - </member> - <member name="P:VNLib.Plugins.Extensions.Data.IDbModel.Id"> - <summary> - A unique id for the entity - </summary> - </member> - <member name="P:VNLib.Plugins.Extensions.Data.IDbModel.Created"> - <summary> - The <see cref="T:System.DateTime"/> the entity was created in the store - </summary> - </member> - <member name="P:VNLib.Plugins.Extensions.Data.IDbModel.LastModified"> - <summary> - The <see cref="T:System.DateTime"/> the entity was last modified in the store - </summary> - </member> - <member name="P:VNLib.Plugins.Extensions.Data.IDbModel.Version"> - <summary> - Entity concurrency token - </summary> - </member> - <member name="T:VNLib.Plugins.Extensions.Data.ProtectedDbStore`1"> - <summary> - A data store that provides unique identities and protections based on an entity that has an owner <see cref="T:VNLib.Plugins.Extensions.Data.Abstractions.IUserEntity"/> - </summary> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.ProtectedDbStore`1.GetCollectionQueryBuilder(VNLib.Plugins.Extensions.Data.TransactionalDbContext,System.String[])"> - <inheritdoc/> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.ProtectedDbStore`1.GetSingleQueryBuilder(VNLib.Plugins.Extensions.Data.TransactionalDbContext,System.String[])"> - <summary> - Gets a single item contrained by a given user-id and item id - </summary> - <param name="context"></param> - <param name="constraints"></param> - <returns></returns> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.ProtectedDbStore`1.GetSingleQueryBuilder(VNLib.Plugins.Extensions.Data.TransactionalDbContext,`0)"> - <inheritdoc/> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.ProtectedEntityExtensions.UpdateAsync``1(VNLib.Plugins.Extensions.Data.Abstractions.IDataStore{``0},``0,System.String)"> - <summary> - Updates the specified record within the store - </summary> - <param name="store"></param> - <param name="record">The record to update</param> - <param name="userId">The userid of the record owner</param> - <returns>A task that evaluates to the number of records modified</returns> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.ProtectedEntityExtensions.CreateAsync``1(VNLib.Plugins.Extensions.Data.Abstractions.IDataStore{``0},``0,System.String)"> - <summary> - Updates the specified record within the store - </summary> - <param name="store"></param> - <param name="record">The record to update</param> - <param name="userId">The userid of the record owner</param> - <returns>A task that evaluates to the number of records modified</returns> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.ProtectedEntityExtensions.GetSingleAsync``1(VNLib.Plugins.Extensions.Data.Abstractions.IDataStore{``0},System.String,System.String)"> - <summary> - Gets a single entity from its ID and user-id - </summary> - <param name="store"></param> - <param name="key">The unique id of the entity</param> - <param name="userId">The user's id that owns the resource</param> - <returns>A task that resolves the entity or null if not found</returns> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.ProtectedEntityExtensions.DeleteAsync``1(VNLib.Plugins.Extensions.Data.Abstractions.IDataStore{``0},System.String,System.String)"> - <summary> - Deletes a single entiry by its ID only if it belongs to the speicifed user - </summary> - <param name="store"></param> - <param name="key">The unique id of the entity</param> - <param name="userId">The user's id that owns the resource</param> - <returns>A task the resolves the number of eneities deleted (should evaluate to true or false)</returns> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.TransactionalDbContext.#ctor"> - <summary> - <inheritdoc/> - </summary> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.TransactionalDbContext.#ctor(Microsoft.EntityFrameworkCore.DbContextOptions)"> - <summary> - <inheritdoc/> - </summary> - </member> - <member name="P:VNLib.Plugins.Extensions.Data.TransactionalDbContext.Transaction"> - <summary> - The transaction that was opened on the current context - </summary> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.TransactionalDbContext.Dispose"> - <inheritdoc/> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.TransactionalDbContext.OpenTransactionAsync(System.Threading.CancellationToken)"> - <summary> - Opens a single transaction on the current context. If a transaction is already open, - it is disposed and a new transaction is begun. - </summary> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.TransactionalDbContext.CommitTransactionAsync(System.Threading.CancellationToken)"> - <summary> - Invokes the <see cref="M:Microsoft.EntityFrameworkCore.Storage.IDbContextTransaction.Commit"/> on the current context - </summary> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.TransactionalDbContext.RollbackTransctionAsync(System.Threading.CancellationToken)"> - <summary> - Invokes the <see cref="M:Microsoft.EntityFrameworkCore.Storage.IDbContextTransaction.Rollback"/> on the current context - </summary> - </member> - <member name="M:VNLib.Plugins.Extensions.Data.TransactionalDbContext.DisposeAsync"> - <inheritdoc/> - </member> - </members> -</doc> diff --git a/VNLib.Plugins.Extensions.Loading.Sql/VNLib.Plugins.Extensions.Loading.Sql.csproj b/VNLib.Plugins.Extensions.Loading.Sql/VNLib.Plugins.Extensions.Loading.Sql.csproj index 9e551aa..c6ab306 100644 --- a/VNLib.Plugins.Extensions.Loading.Sql/VNLib.Plugins.Extensions.Loading.Sql.csproj +++ b/VNLib.Plugins.Extensions.Loading.Sql/VNLib.Plugins.Extensions.Loading.Sql.csproj @@ -4,13 +4,23 @@ <TargetFramework>net6.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> - <PlatformTarget>x64</PlatformTarget> <Authors>Vaughn Nugent</Authors> <Copyright>Copyright © 2022 Vaughn Nugent</Copyright> <PackageProjectUrl>https://www.vaughnnugent.com/resources</PackageProjectUrl> - <Version>1.0.0.1</Version> + <Version>1.0.1.1</Version> <GenerateDocumentationFile>True</GenerateDocumentationFile> - <Platforms>AnyCPU;x64</Platforms> + </PropertyGroup> + + <PropertyGroup> + <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> + </PropertyGroup> + + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> + <Deterministic>False</Deterministic> + </PropertyGroup> + + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> + <Deterministic>False</Deterministic> </PropertyGroup> <ItemGroup> diff --git a/VNLib.Plugins.Extensions.Loading/ConfigurationExtensions.cs b/VNLib.Plugins.Extensions.Loading/ConfigurationExtensions.cs index 0e6ff57..62b898c 100644 --- a/VNLib.Plugins.Extensions.Loading/ConfigurationExtensions.cs +++ b/VNLib.Plugins.Extensions.Loading/ConfigurationExtensions.cs @@ -37,12 +37,12 @@ namespace VNLib.Plugins.Extensions.Loading /// containing data specific to the type /// </summary> [AttributeUsage(AttributeTargets.Class)] - public class ConfigurationNameAttribute : Attribute + public sealed class ConfigurationNameAttribute : Attribute { /// <summary> /// /// </summary> - public readonly string ConfigVarName; + public string ConfigVarName { get; } /// <summary> /// Initializes a new <see cref="ConfigurationNameAttribute"/> @@ -167,7 +167,7 @@ namespace VNLib.Plugins.Extensions.Loading Type type = typeof(T); ConfigurationNameAttribute? configName = type.GetCustomAttribute<ConfigurationNameAttribute>(); //See if the plugin contains a configuration varables - return configName != null ? plugin.PluginConfig.TryGetProperty(configName.ConfigVarName, out _) : false; + return configName != null && plugin.PluginConfig.TryGetProperty(configName.ConfigVarName, out _); } /// <summary> diff --git a/VNLib.Plugins.Extensions.Loading/Events/AsyncIntervalAttribute.cs b/VNLib.Plugins.Extensions.Loading/Events/AsyncIntervalAttribute.cs index 0540ffa..85b0b6d 100644 --- a/VNLib.Plugins.Extensions.Loading/Events/AsyncIntervalAttribute.cs +++ b/VNLib.Plugins.Extensions.Loading/Events/AsyncIntervalAttribute.cs @@ -31,7 +31,7 @@ namespace VNLib.Plugins.Extensions.Loading.Events /// the plugin is loaded, and stops when unloaded /// </summary> [AttributeUsage(AttributeTargets.Method)] - public class AsyncIntervalAttribute : Attribute + public sealed class AsyncIntervalAttribute : Attribute { internal readonly TimeSpan Interval; diff --git a/VNLib.Plugins.Extensions.Loading/Events/ConfigurableAsyncIntervalAttribute.cs b/VNLib.Plugins.Extensions.Loading/Events/ConfigurableAsyncIntervalAttribute.cs index d141eba..12c5ec4 100644 --- a/VNLib.Plugins.Extensions.Loading/Events/ConfigurableAsyncIntervalAttribute.cs +++ b/VNLib.Plugins.Extensions.Loading/Events/ConfigurableAsyncIntervalAttribute.cs @@ -31,7 +31,7 @@ namespace VNLib.Plugins.Extensions.Loading.Events /// the plugin is loaded, and stops when unloaded /// </summary> [AttributeUsage(AttributeTargets.Method)] - public class ConfigurableAsyncIntervalAttribute : Attribute + public sealed class ConfigurableAsyncIntervalAttribute : Attribute { internal readonly string IntervalPropertyName; internal readonly IntervalResultionType Resolution; diff --git a/VNLib.Plugins.Extensions.Loading/LoadingExtensions.cs b/VNLib.Plugins.Extensions.Loading/LoadingExtensions.cs index 411b9b4..bfe0de1 100644 --- a/VNLib.Plugins.Extensions.Loading/LoadingExtensions.cs +++ b/VNLib.Plugins.Extensions.Loading/LoadingExtensions.cs @@ -26,11 +26,13 @@ using System; using System.IO; using System.Linq; using System.Text.Json; +using System.Threading.Tasks; using System.Collections.Generic; using System.Security.Cryptography; using System.Runtime.CompilerServices; using VNLib.Utils; +using VNLib.Utils.Logging; using VNLib.Utils.Extensions; using VNLib.Plugins.Essentials.Accounts; @@ -44,10 +46,9 @@ namespace VNLib.Plugins.Extensions.Loading public const string DEBUG_CONFIG_KEY = "debug"; public const string SECRETS_CONFIG_KEY = "secrets"; public const string PASSWORD_HASHING_KEY = "passwords"; - - + private static readonly ConditionalWeakTable<PluginBase, Lazy<PasswordHashing>> LazyPasswordTable = new(); - + /// <summary> /// Gets the plugins ambient <see cref="PasswordHashing"/> if loaded, or loads it if required. This class will @@ -176,5 +177,55 @@ namespace VNLib.Plugins.Extensions.Loading throw new ObjectDisposedException("The plugin has been unloaded"); } } + + /// <summary> + /// Schedules an asynchronous callback function to run and its results will be observed + /// when the operation completes, or when the plugin is unloading + /// </summary> + /// <param name="plugin"></param> + /// <param name="asyncTask">The asynchronous operation to observe</param> + /// <param name="delayMs">An optional startup delay for the operation</param> + /// <returns>A task that completes when the deferred task completes </returns> + /// <exception cref="ObjectDisposedException"></exception> + public static async Task DeferTask(this PluginBase plugin, Func<Task> asyncTask, int delayMs = 0) + { + /* + * Motivation: + * Sometimes during plugin loading, a plugin may want to asynchronously load + * data, where the results are not required to be observed during loading, but + * should not be pending after the plugin is unloaded, as the assembly may be + * unloaded and referrences collected by the GC. + * + * So we can use the plugin's unload cancellation token to observe the results + * of a pending async operation + */ + + //Test status + plugin.ThrowIfUnloaded(); + + //Optional delay + await Task.Delay(delayMs); + + //Run on ts + Task deferred = Task.Run(asyncTask); + + //Add task to deferred list + plugin.DeferredTasks.Add(deferred); + try + { + //Await the task results + await deferred; + } + catch(Exception ex) + { + //Log errors + plugin.Log.Error(ex, "Error occured while observing deferred task"); + } + finally + { + //Remove task when complete + plugin.DeferredTasks.Remove(deferred); + } + } } } diff --git a/VNLib.Plugins.Extensions.Loading/UserLoading.cs b/VNLib.Plugins.Extensions.Loading/UserLoading.cs index b67af0d..3457dc3 100644 --- a/VNLib.Plugins.Extensions.Loading/UserLoading.cs +++ b/VNLib.Plugins.Extensions.Loading/UserLoading.cs @@ -81,7 +81,7 @@ namespace VNLib.Plugins.Extensions.Loading.Users //Get the onplugin load method Action<object>? onLoadMethod = runtimeType.GetMethods() - .Where(static p => p.IsPublic && !p.IsAbstract && ONLOAD_METHOD_NAME.Equals(p.Name)) + .Where(static p => p.IsPublic && !p.IsAbstract && ONLOAD_METHOD_NAME.Equals(p.Name, StringComparison.Ordinal)) .Select(p => p.CreateDelegate<Action<object>>(loader.Resource)) .FirstOrDefault(); diff --git a/VNLib.Plugins.Extensions.Loading/VNLib.Plugins.Extensions.Loading.csproj b/VNLib.Plugins.Extensions.Loading/VNLib.Plugins.Extensions.Loading.csproj index 7a25ef1..43eefc2 100644 --- a/VNLib.Plugins.Extensions.Loading/VNLib.Plugins.Extensions.Loading.csproj +++ b/VNLib.Plugins.Extensions.Loading/VNLib.Plugins.Extensions.Loading.csproj @@ -4,18 +4,16 @@ <TargetFramework>net6.0</TargetFramework> <Copyright>Copyright © 2022 Vaughn Nugent</Copyright> <Authors>Vaughn Nugent</Authors> - <Version>1.0.0.1</Version> - <Platforms>AnyCPU;x64</Platforms> + <Version>1.0.1.1</Version> + <GenerateDocumentationFile>True</GenerateDocumentationFile> <Nullable>enable</Nullable> </PropertyGroup> - <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> - <DocumentationFile></DocumentationFile> - </PropertyGroup> - - <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> - <DocumentationFile></DocumentationFile> + <PropertyGroup> + <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> + <PackageProjectUrl>https://www.vaughnnugent.com/resources</PackageProjectUrl> + <AnalysisLevel>latest-all</AnalysisLevel> </PropertyGroup> <ItemGroup> diff --git a/VNLib.Plugins.Extensions.Validation/VNLib.Plugins.Extensions.Validation.csproj b/VNLib.Plugins.Extensions.Validation/VNLib.Plugins.Extensions.Validation.csproj index a1d3e83..ea34a6c 100644 --- a/VNLib.Plugins.Extensions.Validation/VNLib.Plugins.Extensions.Validation.csproj +++ b/VNLib.Plugins.Extensions.Validation/VNLib.Plugins.Extensions.Validation.csproj @@ -1,17 +1,18 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>net6.0</TargetFramework> - <Platforms>AnyCPU;x64</Platforms> + <TargetFramework>net6.0</TargetFramework> <Authors>Vaughn Nugent</Authors> <Copyright>Copyright © 2022 Vaughn Nugent</Copyright> <PackageProjectUrl>https://www.vaughnnugent.com/resources</PackageProjectUrl> - <Version>1.0.0.1</Version> - <GenerateDocumentationFile>True</GenerateDocumentationFile> + <Version>1.0.1.1</Version> </PropertyGroup> - <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> - <DocumentationFile></DocumentationFile> + <PropertyGroup> + <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> + <GenerateDocumentationFile>True</GenerateDocumentationFile> + <AnalysisLevel>latest-all</AnalysisLevel> + <Nullable>enable</Nullable> </PropertyGroup> <ItemGroup> @@ -27,7 +28,7 @@ </ItemGroup> <ItemGroup> - <ProjectReference Include="..\..\..\VNLib\Plugins\VNLib.Plugins.csproj" /> + <ProjectReference Include="..\..\..\VNLib\Plugins\src\VNLib.Plugins.csproj" /> </ItemGroup> </Project> diff --git a/VNLib.Plugins.Extensions.Validation/ValErrWebMessage.cs b/VNLib.Plugins.Extensions.Validation/ValErrWebMessage.cs index efb0529..8e439da 100644 --- a/VNLib.Plugins.Extensions.Validation/ValErrWebMessage.cs +++ b/VNLib.Plugins.Extensions.Validation/ValErrWebMessage.cs @@ -30,7 +30,7 @@ namespace VNLib.Plugins.Extensions.Validation /// <summary> /// Extends the <see cref="WebMessage"/> class with provisions for a collection of validations /// </summary> - public class ValErrWebMessage:WebMessage + public class ValErrWebMessage : WebMessage { /// <summary> /// A collection of error messages to send to clients |