aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLibravatar vman <public@vaughnnugent.com>2022-11-30 14:59:09 -0500
committerLibravatar vman <public@vaughnnugent.com>2022-11-30 14:59:09 -0500
commitc9d9e6d23ad7b6fdf25f30de9b4a84be23885e16 (patch)
tree6f8336e55da2b06bfac2204510bf661dfa1a1476
parente8a846c83ca9922761db56373bc93fe4ea3f4021 (diff)
Project cleanup + analyzer updates
-rw-r--r--VNLib.Plugins.Extensions.Data/SQL/DbExtensions.cs526
-rw-r--r--VNLib.Plugins.Extensions.Data/SQL/EnumerableTable.cs118
-rw-r--r--VNLib.Plugins.Extensions.Data/SQL/SqlColumnName.cs65
-rw-r--r--VNLib.Plugins.Extensions.Data/SQL/SqlVariable.cs58
-rw-r--r--VNLib.Plugins.Extensions.Data/SQL/TableManager.cs65
-rw-r--r--VNLib.Plugins.Extensions.Data/Storage/Blob.cs244
-rw-r--r--VNLib.Plugins.Extensions.Data/Storage/BlobExtensions.cs67
-rw-r--r--VNLib.Plugins.Extensions.Data/Storage/BlobStore.cs162
-rw-r--r--VNLib.Plugins.Extensions.Data/Storage/LWDecriptorCreationException.cs45
-rw-r--r--VNLib.Plugins.Extensions.Data/Storage/LWStorageDescriptor.cs204
-rw-r--r--VNLib.Plugins.Extensions.Data/Storage/LWStorageManager.cs379
-rw-r--r--VNLib.Plugins.Extensions.Data/Storage/LWStorageRemoveFailedException.cs38
-rw-r--r--VNLib.Plugins.Extensions.Data/Storage/LWStorageUpdateFailedException.cs38
-rw-r--r--VNLib.Plugins.Extensions.Data/Storage/UndefinedBlobStateException.cs45
-rw-r--r--VNLib.Plugins.Extensions.Data/VNLib.Plugins.Extensions.Data.csproj22
-rw-r--r--VNLib.Plugins.Extensions.Data/l473
-rw-r--r--VNLib.Plugins.Extensions.Loading.Sql/VNLib.Plugins.Extensions.Loading.Sql.csproj16
-rw-r--r--VNLib.Plugins.Extensions.Loading/ConfigurationExtensions.cs6
-rw-r--r--VNLib.Plugins.Extensions.Loading/Events/AsyncIntervalAttribute.cs2
-rw-r--r--VNLib.Plugins.Extensions.Loading/Events/ConfigurableAsyncIntervalAttribute.cs2
-rw-r--r--VNLib.Plugins.Extensions.Loading/LoadingExtensions.cs57
-rw-r--r--VNLib.Plugins.Extensions.Loading/UserLoading.cs2
-rw-r--r--VNLib.Plugins.Extensions.Loading/VNLib.Plugins.Extensions.Loading.csproj14
-rw-r--r--VNLib.Plugins.Extensions.Validation/VNLib.Plugins.Extensions.Validation.csproj15
-rw-r--r--VNLib.Plugins.Extensions.Validation/ValErrWebMessage.cs2
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