aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading.Sql/src/SqlDbConnectionLoader.cs153
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/AssemblyLoader.cs20
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs40
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/IAsyncLazy.cs148
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs175
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/ManagedPasswordHashing.cs44
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/Secrets/ISecretResult.cs39
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/Secrets/PrivateKey.cs (renamed from lib/VNLib.Plugins.Extensions.Loading/src/PrivateKey.cs)4
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/Secrets/SecretResult.cs (renamed from lib/VNLib.Plugins.Extensions.Loading/src/SecretResult.cs)9
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/Secrets/VaultSecrets.cs (renamed from lib/VNLib.Plugins.Extensions.Loading/src/VaultSecrets.cs)140
10 files changed, 619 insertions, 153 deletions
diff --git a/lib/VNLib.Plugins.Extensions.Loading.Sql/src/SqlDbConnectionLoader.cs b/lib/VNLib.Plugins.Extensions.Loading.Sql/src/SqlDbConnectionLoader.cs
index f6985f8..8775d54 100644
--- a/lib/VNLib.Plugins.Extensions.Loading.Sql/src/SqlDbConnectionLoader.cs
+++ b/lib/VNLib.Plugins.Extensions.Loading.Sql/src/SqlDbConnectionLoader.cs
@@ -56,7 +56,8 @@ namespace VNLib.Plugins.Extensions.Loading.Sql
private const string TIMESTAMP_BYPASS = "TimeStamp";
/// <summary>
- /// Gets (or loads) the ambient sql connection factory for the current plugin
+ /// Gets (or loads) the ambient sql connection factory for the current plugin
+ /// and synchronously blocks the current thread until the connection is ready.
/// </summary>
/// <param name="plugin"></param>
/// <returns>The ambient <see cref="DbConnection"/> factory</returns>
@@ -64,24 +65,50 @@ namespace VNLib.Plugins.Extensions.Loading.Sql
/// <exception cref="ObjectDisposedException"></exception>
public static Func<DbConnection> GetConnectionFactory(this PluginBase plugin)
{
+ //Get the async factory
+ IAsyncLazy<Func<DbConnection>> async = plugin.GetConnectionFactoryAsync();
+
+ //Block the current thread until the connection is ready
+ return async.GetAwaiter().GetResult();
+ }
+
+ /// <summary>
+ /// Gets (or loads) the ambient sql connection factory for the current plugin
+ /// asynchronously
+ /// </summary>
+ /// <param name="plugin"></param>
+ /// <returns>The ambient <see cref="DbConnection"/> factory</returns>
+ /// <exception cref="KeyNotFoundException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static IAsyncLazy<Func<DbConnection>> GetConnectionFactoryAsync(this PluginBase plugin)
+ {
+ static IAsyncLazy<Func<DbConnection>> FactoryLoader(PluginBase plugin)
+ {
+ return GetFactoryLoaderAsync(plugin).AsLazy();
+ }
+
plugin.ThrowIfUnloaded();
+
//Get or load
return LoadingExtensions.GetOrCreateSingleton(plugin, FactoryLoader);
}
- private static Func<DbConnection> FactoryLoader(PluginBase plugin)
+ private async static Task<Func<DbConnection>> GetFactoryLoaderAsync(PluginBase plugin)
{
IConfigScope sqlConf = plugin.GetConfig(SQL_CONFIG_KEY);
//Get the db-type
- string? type = sqlConf.GetPropString("db_type");
+ string? type = sqlConf.GetPropString("db_type");
+
+ //Try to get the password and always dispose the secret value
+ using ISecretResult? password = await plugin.TryGetSecretAsync(DB_PASSWORD_KEY);
+
+ DbConnectionStringBuilder sqlBuilder;
if ("sqlite".Equals(type, StringComparison.OrdinalIgnoreCase))
{
- using SecretResult? password = plugin.TryGetSecretAsync(DB_PASSWORD_KEY).GetAwaiter().GetResult();
-
//Use connection builder
- DbConnectionStringBuilder sqlBuilder = new SqliteConnectionStringBuilder()
+ sqlBuilder = new SqliteConnectionStringBuilder()
{
DataSource = sqlConf["source"].GetString(),
Password = password?.Result.ToString(),
@@ -90,14 +117,11 @@ namespace VNLib.Plugins.Extensions.Loading.Sql
};
string connectionString = sqlBuilder.ToString();
- DbConnection DbFactory() => new SqliteConnection(connectionString);
- return DbFactory;
+ return () => new SqliteConnection(connectionString);
}
else if("mysql".Equals(type, StringComparison.OrdinalIgnoreCase))
{
- using SecretResult? password = plugin.TryGetSecretAsync(DB_PASSWORD_KEY).GetAwaiter().GetResult();
-
- DbConnectionStringBuilder sqlBuilder = new MySqlConnectionStringBuilder()
+ sqlBuilder = new MySqlConnectionStringBuilder()
{
Server = sqlConf["hostname"].GetString(),
Database = sqlConf["database"].GetString(),
@@ -107,18 +131,15 @@ namespace VNLib.Plugins.Extensions.Loading.Sql
LoadBalance = MySqlLoadBalance.LeastConnections,
MinimumPoolSize = sqlConf["min_pool_size"].GetUInt32(),
};
-
+
string connectionString = sqlBuilder.ToString();
- DbConnection DbFactory() => new MySqlConnection(connectionString);
- return DbFactory;
+ return () => new MySqlConnection(connectionString);
}
//Default to mssql
else
{
- using SecretResult? password = plugin.TryGetSecretAsync(DB_PASSWORD_KEY).GetAwaiter().GetResult();
-
//Use connection builder
- DbConnectionStringBuilder sqlBuilder = new SqlConnectionStringBuilder()
+ sqlBuilder = new SqlConnectionStringBuilder()
{
DataSource = sqlConf["hostname"].GetString(),
UserID = sqlConf["username"].GetString(),
@@ -131,14 +152,13 @@ namespace VNLib.Plugins.Extensions.Loading.Sql
};
string connectionString = sqlBuilder.ToString();
- DbConnection DbFactory() => new SqlConnection(connectionString);
- return DbFactory;
- }
+ return () => new SqlConnection(connectionString);
+ }
}
/// <summary>
/// Gets (or loads) the ambient <see cref="DbContextOptions"/> configured from
- /// the ambient sql factory
+ /// the ambient sql factory and blocks the current thread until the options are ready
/// </summary>
/// <param name="plugin"></param>
/// <returns>The ambient <see cref="DbContextOptions"/> for the current plugin</returns>
@@ -147,44 +167,77 @@ namespace VNLib.Plugins.Extensions.Loading.Sql
/// <remarks>If plugin is in debug mode, writes log data to the default log</remarks>
public static DbContextOptions GetContextOptions(this PluginBase plugin)
{
- plugin.ThrowIfUnloaded();
- return LoadingExtensions.GetOrCreateSingleton(plugin, GetDbOptionsLoader);
+ //Get the async factory
+ IAsyncLazy<DbContextOptions> async = plugin.GetContextOptionsAsync();
+
+ //Block the current thread until the connection is ready
+ return async.GetAwaiter().GetResult();
}
- private static DbContextOptions GetDbOptionsLoader(PluginBase plugin)
+ /// <summary>
+ /// Gets (or loads) the ambient <see cref="DbContextOptions"/> configured from
+ /// the ambient sql factory
+ /// </summary>
+ /// <param name="plugin"></param>
+ /// <returns>The ambient <see cref="DbContextOptions"/> for the current plugin</returns>
+ /// <exception cref="KeyNotFoundException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <remarks>If plugin is in debug mode, writes log data to the default log</remarks>
+ public static IAsyncLazy<DbContextOptions> GetContextOptionsAsync(this PluginBase plugin)
{
- //Get a db connection object
- using DbConnection connection = plugin.GetConnectionFactory().Invoke();
- DbContextOptionsBuilder builder = new();
-
- //Determine connection type
- if(connection is SqlConnection sql)
+ static IAsyncLazy<DbContextOptions> LoadOptions(PluginBase plugin)
{
- //Use sql server from connection
- builder.UseSqlServer(sql.ConnectionString);
+ //Wrap in a lazy options
+ return GetDbOptionsAsync(plugin).AsLazy();
}
- else if(connection is SqliteConnection slc)
- {
- builder.UseSqlite(slc.ConnectionString);
- }
- else if(connection is MySqlConnection msconn)
+
+ plugin.ThrowIfUnloaded();
+ return LoadingExtensions.GetOrCreateSingleton(plugin, LoadOptions);
+ }
+
+ private async static Task<DbContextOptions> GetDbOptionsAsync(PluginBase plugin)
+ {
+ try
{
- //Detect version
- ServerVersion version = ServerVersion.AutoDetect(msconn);
+ //Get a db connection object, we must wait synchronously tho
+ await using DbConnection connection = (await plugin.GetConnectionFactoryAsync()).Invoke();
+
+ DbContextOptionsBuilder builder = new();
- builder.UseMySql(msconn.ConnectionString, version);
+ //Determine connection type
+ if (connection is SqlConnection sql)
+ {
+ //Use sql server from connection
+ builder.UseSqlServer(sql.ConnectionString);
+ }
+ else if (connection is SqliteConnection slc)
+ {
+ builder.UseSqlite(slc.ConnectionString);
+ }
+ else if (connection is MySqlConnection msconn)
+ {
+ //Detect version
+ ServerVersion version = ServerVersion.AutoDetect(msconn);
+
+ builder.UseMySql(msconn.ConnectionString, version);
+ }
+
+ //Enable logging
+ if (plugin.IsDebug())
+ {
+ builder.LogTo(plugin.Log.Debug);
+ }
+
+ //Get context and freez it before returning
+ DbContextOptions options = builder.Options;
+ options.Freeze();
+ return options;
}
-
- //Enable logging
- if(plugin.IsDebug())
+ catch(Exception ex)
{
- builder.LogTo(plugin.Log.Debug);
+ plugin.Log.Error(ex, "DBContext options load error");
+ throw;
}
-
- //Get context and freez it before returning
- DbContextOptions options = builder.Options;
- options.Freeze();
- return options;
}
/// <summary>
@@ -218,7 +271,7 @@ namespace VNLib.Plugins.Extensions.Loading.Sql
dbCreator.OnDatabaseCreating(builder, state);
//Create a new db connection
- await using DbConnection connection = plugin.GetConnectionFactory()();
+ await using DbConnection connection = (await plugin.GetConnectionFactoryAsync()).Invoke();
//Get the abstract database from the connection type
IDBCommandGenerator cb = connection.GetCmGenerator();
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/AssemblyLoader.cs b/lib/VNLib.Plugins.Extensions.Loading/src/AssemblyLoader.cs
index 5de6103..e01b32d 100644
--- a/lib/VNLib.Plugins.Extensions.Loading/src/AssemblyLoader.cs
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/AssemblyLoader.cs
@@ -159,29 +159,19 @@ namespace VNLib.Plugins.Extensions.Loading
/// </summary>
/// <param name="assemblyName">The name of the assmbly within the current plugin directory</param>
/// <param name="unloadToken">The plugin unload token</param>
- /// <param name="explicitContext">Explicitly set an assembly load context to load the requested assembly into</param>
+ /// <param name="loadContext">The assembly load context to load the assmbly into</param>
/// <exception cref="FileNotFoundException"></exception>
- internal static AssemblyLoader<T> Load(string assemblyName, AssemblyLoadContext? explicitContext, CancellationToken unloadToken)
+ internal static AssemblyLoader<T> Load(string assemblyName, AssemblyLoadContext loadContext, CancellationToken unloadToken)
{
+ _ = loadContext ?? throw new ArgumentNullException(nameof(loadContext));
+
//Make sure the file exists
if (!FileOperations.FileExists(assemblyName))
{
throw new FileNotFoundException($"The desired assembly {assemblyName} could not be found at the file path");
}
-
- if(explicitContext == null)
- {
- /*
- * Dynamic assemblies are loaded directly to the exe assembly context.
- * This should always be the plugin isolated context.
- */
-
- Assembly executingAsm = Assembly.GetExecutingAssembly();
- explicitContext = AssemblyLoadContext.GetLoadContext(executingAsm) ?? throw new InvalidOperationException("Could not get default assembly load context");
- }
-
- return new(assemblyName, explicitContext, unloadToken);
+ return new(assemblyName, loadContext, unloadToken);
}
}
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs b/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs
index 190c153..fbe8d48 100644
--- a/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs
@@ -23,11 +23,14 @@
*/
using System;
+using System.IO;
using System.Text.Json;
using System.Reflection;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
+using VNLib.Utils.Extensions;
+
namespace VNLib.Plugins.Extensions.Loading
{
/// <summary>
@@ -66,6 +69,9 @@ namespace VNLib.Plugins.Extensions.Loading
{
public const string S3_CONFIG = "s3_config";
public const string S3_SECRET_KEY = "s3_secret";
+ public const string PLUGIN_ASSET_KEY = "assets";
+ public const string PLUGINS_HOST_KEY = "plugins";
+ public const string PLUGIN_PATH_KEY = "path";
/// <summary>
/// Retrieves a top level configuration dictionary of elements for the specified type.
@@ -356,5 +362,39 @@ namespace VNLib.Plugins.Extensions.Loading
IConfigScope? s3conf = plugin.TryGetConfig(S3_CONFIG);
return s3conf?.Deserialze<S3Config>();
}
+
+ /// <summary>
+ /// Trys to get the optional assets directory from the plugin configuration
+ /// </summary>
+ /// <param name="plugin"></param>
+ /// <returns>The absolute path to the assets directory if defined, null otherwise</returns>
+ public static string? GetAssetsPath(this PluginBase plugin)
+ {
+ //Get global plugin config element
+ IConfigScope config = plugin.GetConfig(PLUGINS_HOST_KEY);
+
+ //Try to get the assets path if its defined
+ string? assetsPath = config.GetPropString(PLUGIN_ASSET_KEY);
+
+ //Try to get the full path for the assets if we can
+ return assetsPath != null ? Path.GetFullPath(assetsPath) : null;
+ }
+
+ /// <summary>
+ /// Gets the absolute path to the plugins directory as defined in the host configuration
+ /// </summary>
+ /// <param name="plugin"></param>
+ /// <returns>The absolute path to the directory containing all plugins</returns>
+ public static string GetPluginsPath(this PluginBase plugin)
+ {
+ //Get global plugin config element
+ IConfigScope config = plugin.GetConfig(PLUGINS_HOST_KEY);
+
+ //Get the plugins path or throw because it should ALWAYS be defined if this method is called
+ string pluginsPath = config[PLUGIN_PATH_KEY].GetString()!;
+
+ //Get absolute path
+ return Path.GetFullPath(pluginsPath);
+ }
}
}
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/IAsyncLazy.cs b/lib/VNLib.Plugins.Extensions.Loading/src/IAsyncLazy.cs
new file mode 100644
index 0000000..98e0ebe
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/IAsyncLazy.cs
@@ -0,0 +1,148 @@
+/*
+* Copyright (c) 2023 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Loading
+* File: IAsyncLazy.cs
+*
+* IAsyncLazy.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify
+* it under the terms of the GNU Affero General Public License as
+* published by the Free Software Foundation, either version 3 of the
+* License, or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU Affero General Public License for more details.
+*
+* You should have received a copy of the GNU Affero General Public License
+* along with this program. If not, see https://www.gnu.org/licenses/.
+*/
+
+
+using System;
+using System.Threading.Tasks;
+using System.Runtime.CompilerServices;
+
+namespace VNLib.Plugins.Extensions.Loading
+{
+ /// <summary>
+ /// Represents an asynchronous lazy operation. with non-blocking access to the target value.
+ /// </summary>
+ /// <typeparam name="T">The result type</typeparam>
+ public interface IAsyncLazy<T>
+ {
+ /// <summary>
+ /// Gets a value indicating whether the asynchronous operation has completed.
+ /// </summary>
+ bool Completed { get; }
+
+ /// <summary>
+ /// Gets a task that represents the asynchronous operation.
+ /// </summary>
+ /// <returns></returns>
+ TaskAwaiter<T> GetAwaiter();
+
+ /// <summary>
+ /// Gets the target value of the asynchronous operation without blocking.
+ /// If the operation failed, throws an exception that caused the failure.
+ /// If the operation has not completed, throws an exception.
+ /// </summary>
+ T Value { get; }
+ }
+
+ /// <summary>
+ /// Extension methods for <see cref="IAsyncLazy{T}"/>
+ /// </summary>
+ public static class AsyncLazyExtensions
+ {
+ /// <summary>
+ /// Gets an <see cref="IAsyncLazy{T}"/> wrapper for the specified <see cref="Task{T}"/>
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="task"></param>
+ /// <returns>The async operation task wrapper</returns>
+ public static IAsyncLazy<T> AsLazy<T>(this Task<T> task) => new AsyncLazy<T>(task);
+
+ /// <summary>
+ /// Tranforms one lazy operation into another using the specified handler
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <typeparam name="TResult">The resultant type</typeparam>
+ /// <param name="lazy"></param>
+ /// <param name="handler">The function that will peform the transformation of the lazy result</param>
+ /// <returns>A new <see cref="IAsyncLazy{T}"/> that returns the transformed type</returns>
+ public static IAsyncLazy<TResult> Transform<T, TResult>(this IAsyncLazy<T> lazy, Func<T, TResult> handler)
+ {
+ _ = lazy ?? throw new ArgumentNullException(nameof(lazy));
+ _ = handler ?? throw new ArgumentNullException(nameof(handler));
+
+ //Await the lazy task, then pass the result to the handler
+ static async Task<TResult> OnResult(IAsyncLazy<T> lazy, Func<T, TResult> cb)
+ {
+ T result = await lazy;
+ return cb(result);
+ }
+
+ return OnResult(lazy, handler).AsLazy();
+ }
+
+#nullable disable
+
+ private sealed class AsyncLazy<T> : IAsyncLazy<T>
+ {
+ private readonly Task<T> _task;
+
+ private T _result;
+
+ public AsyncLazy(Task<T> task)
+ {
+ _task = task ?? throw new ArgumentNullException(nameof(task));
+ _ = task.ContinueWith(SetResult, TaskScheduler.Default);
+ }
+
+ ///<inheritdoc/>
+ public bool Completed => _task.IsCompleted;
+
+ ///<inheritdoc/>
+ public T Value
+ {
+ get
+ {
+ if (_task.IsCompletedSuccessfully)
+ {
+ return _result;
+ }
+ else if(_task.IsFaulted)
+ {
+ //Compress and raise exception from result
+ return _task.GetAwaiter().GetResult();
+ }
+ else
+ {
+ throw new InvalidOperationException("The asynchronous operation has not completed.");
+ }
+ }
+ }
+
+ /*
+ * Only set the result if the task completed successfully.
+ */
+ private void SetResult(Task<T> task)
+ {
+ if (task.IsCompletedSuccessfully)
+ {
+ _result = task.Result;
+ }
+ }
+
+ ///<inheritdoc/>
+ public TaskAwaiter<T> GetAwaiter() => _task.GetAwaiter();
+ }
+#nullable enable
+
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs b/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs
index 5511398..8f7dee8 100644
--- a/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs
@@ -37,6 +37,50 @@ using VNLib.Utils.Extensions;
namespace VNLib.Plugins.Extensions.Loading
{
+ /// <summary>
+ /// Base class for concrete type loading exceptions. Raised when searching
+ /// for a concrete type fails.
+ /// </summary>
+ public class ConcreteTypeException : TypeLoadException
+ {
+ public ConcreteTypeException() : base()
+ { }
+
+ public ConcreteTypeException(string? message) : base(message)
+ { }
+
+ public ConcreteTypeException(string? message, Exception? innerException) : base(message, innerException)
+ { }
+ }
+
+ /// <summary>
+ /// Raised when a concrete type is found but is ambiguous because more than one
+ /// type implements the desired abstract type.
+ /// </summary>
+ public sealed class ConcreteTypeAmbiguousMatchException : ConcreteTypeException
+ {
+ public ConcreteTypeAmbiguousMatchException(string message) : base(message)
+ { }
+
+ public ConcreteTypeAmbiguousMatchException(string message, Exception innerException) : base(message, innerException)
+ { }
+
+ public ConcreteTypeAmbiguousMatchException()
+ { }
+ }
+
+ /// <summary>
+ /// The requested concrete type was not found in the assembly
+ /// </summary>
+ public sealed class ConcreteTypeNotFoundException : ConcreteTypeException
+ {
+ public ConcreteTypeNotFoundException(string message) : base(message)
+ { }
+ public ConcreteTypeNotFoundException(string message, Exception innerException) : base(message, innerException)
+ { }
+ public ConcreteTypeNotFoundException()
+ { }
+ }
/// <summary>
/// Provides common loading (and unloading when required) extensions for plugins
@@ -47,7 +91,7 @@ namespace VNLib.Plugins.Extensions.Loading
/// A key in the 'plugins' configuration object that specifies
/// an asset search directory
/// </summary>
- public const string PLUGIN_ASSET_KEY = "assets";
+
public const string DEBUG_CONFIG_KEY = "debug";
public const string SECRETS_CONFIG_KEY = "secrets";
public const string PASSWORD_HASHING_KEY = "passwords";
@@ -121,31 +165,79 @@ namespace VNLib.Plugins.Extensions.Loading
{
plugin.ThrowIfUnloaded();
_ = assemblyName ?? throw new ArgumentNullException(nameof(assemblyName));
-
- //get plugin directory from config
- IConfigScope config = plugin.GetConfig("plugins");
+
/*
* Allow an assets directory to limit the scope of the search for the desired
* assembly, otherwise search all plugins directories
*/
-
- string? assetDir = config.GetPropString(PLUGIN_ASSET_KEY);
- assetDir ??= config["path"].GetString();
+
+ string? assetDir = plugin.GetAssetsPath();
+ assetDir ??= plugin.GetPluginsPath();
/*
* This should never happen since this method can only be called from a
* plugin context, which means this path was used to load the current plugin
*/
- _ = assetDir ?? throw new ArgumentNullException(PLUGIN_ASSET_KEY, "No plugin path is defined for the current host configuration, this is likely a bug");
+ _ = assetDir ?? throw new ArgumentNullException(ConfigurationExtensions.PLUGIN_ASSET_KEY, "No plugin path is defined for the current host configuration, this is likely a bug");
//Get the first file that matches the search file
string? asmFile = Directory.EnumerateFiles(assetDir, assemblyName, dirSearchOption).FirstOrDefault();
_ = asmFile ?? throw new FileNotFoundException($"Failed to load custom assembly {assemblyName} from plugin directory");
-
+
+ //Get the plugin's load context if not explicitly supplied
+ explictAlc ??= GetPluginLoadContext();
+
//Load the assembly
return AssemblyLoader<T>.Load(asmFile, explictAlc, plugin.UnloadToken);
- }
+ }
+
+ /// <summary>
+ /// Gets the current plugin's <see cref="AssemblyLoadContext"/>.
+ /// </summary>
+ /// <returns></returns>
+ /// <exception cref="InvalidOperationException"></exception>
+ public static AssemblyLoadContext GetPluginLoadContext()
+ {
+ /*
+ * Since this library should only be used in a plugin context, the executing assembly
+ * will be loaded into the plugin's isolated load context. So we can get the load
+ * context for the executing assembly and use that as the plugin's load context.
+ */
+
+ Assembly executingAsm = Assembly.GetExecutingAssembly();
+ return AssemblyLoadContext.GetLoadContext(executingAsm) ?? throw new InvalidOperationException("Could not get plugin's assembly load context");
+ }
+
+ /// <summary>
+ /// Gets a single type implemenation of the abstract type from the current assembly. If multiple
+ /// concrete types are found, an exception is raised, if no concrete types are found, an exception
+ /// is raised.
+ /// </summary>
+ /// <param name="abstractType">The abstract type to get the concrete type from</param>
+ /// <returns>The concrete type if found</returns>
+ /// <exception cref="ConcreteTypeNotFoundException"></exception>
+ /// <exception cref="ConcreteTypeAmbiguousMatchException"></exception>
+ public static Type GetTypeImplFromCurrentAssembly(Type abstractType)
+ {
+ //Get all types from the current assembly that implement the abstract type
+ Assembly executingAsm = Assembly.GetExecutingAssembly();
+ Type[] concreteTypes = executingAsm.GetTypes().Where(t => !t.IsAbstract && abstractType.IsAssignableFrom(t)).ToArray();
+
+ if(concreteTypes.Length == 0)
+ {
+ throw new ConcreteTypeNotFoundException($"Failed to load implemenation of abstract type {abstractType} because no concrete implementations were found in this assembly");
+ }
+
+ if(concreteTypes.Length > 1)
+ {
+ throw new ConcreteTypeAmbiguousMatchException(
+ $"Failed to load implemenation of abstract type {abstractType} because multiple concrete implementations were found in this assembly");
+ }
+
+ //Get the only concrete type
+ return concreteTypes[0];
+ }
/// <summary>
/// Determintes if the current plugin config has a debug propety set
@@ -285,11 +377,13 @@ namespace VNLib.Plugins.Extensions.Loading
/// <exception cref="KeyNotFoundException"></exception>
/// <exception cref="ObjectDisposedException"></exception>
/// <exception cref="EntryPointNotFoundException"></exception>
+ /// <exception cref="ConcreteTypeNotFoundException"></exception>
+ /// <exception cref="ConcreteTypeAmbiguousMatchException"></exception>
public static T GetOrCreateSingleton<T>(this PluginBase plugin)
{
//Add service to service continer
return GetOrCreateSingleton(plugin, CreateService<T>);
- }
+ }
/// <summary>
/// <para>
@@ -311,6 +405,8 @@ namespace VNLib.Plugins.Extensions.Loading
/// <exception cref="KeyNotFoundException"></exception>
/// <exception cref="ObjectDisposedException"></exception>
/// <exception cref="EntryPointNotFoundException"></exception>
+ /// <exception cref="ConcreteTypeNotFoundException"></exception>
+ /// <exception cref="ConcreteTypeAmbiguousMatchException"></exception>
public static T GetOrCreateSingleton<T>(this PluginBase plugin, string configName)
{
//Add service to service continer
@@ -357,6 +453,8 @@ namespace VNLib.Plugins.Extensions.Loading
/// <exception cref="KeyNotFoundException"></exception>
/// <exception cref="ObjectDisposedException"></exception>
/// <exception cref="EntryPointNotFoundException"></exception>
+ /// <exception cref="ConcreteTypeNotFoundException"></exception>
+ /// <exception cref="ConcreteTypeAmbiguousMatchException"></exception>
public static T CreateService<T>(this PluginBase plugin)
{
if (plugin.HasConfigForType<T>())
@@ -390,6 +488,8 @@ namespace VNLib.Plugins.Extensions.Loading
/// <exception cref="KeyNotFoundException"></exception>
/// <exception cref="ObjectDisposedException"></exception>
/// <exception cref="EntryPointNotFoundException"></exception>
+ /// <exception cref="ConcreteTypeNotFoundException"></exception>
+ /// <exception cref="ConcreteTypeAmbiguousMatchException"></exception>
public static T CreateService<T>(this PluginBase plugin, string configName)
{
IConfigScope config = plugin.GetConfig(configName);
@@ -416,30 +516,67 @@ namespace VNLib.Plugins.Extensions.Loading
/// <exception cref="KeyNotFoundException"></exception>
/// <exception cref="ObjectDisposedException"></exception>
/// <exception cref="EntryPointNotFoundException"></exception>
+ /// <exception cref="ConcreteTypeNotFoundException"></exception>
+ /// <exception cref="ConcreteTypeAmbiguousMatchException"></exception>
public static T CreateService<T>(this PluginBase plugin, IConfigScope? config)
{
+ return (T)CreateService(plugin, typeof(T), config);
+ }
+
+ /// <summary>
+ /// <para>
+ /// Creates and configures a new instance of the desired type, with the specified configuration scope
+ /// </para>
+ /// <para>
+ /// If the type derrives <see cref="IAsyncConfigurable"/> the <see cref="IAsyncConfigurable.ConfigureServiceAsync"/>
+ /// method is called once when the instance is loaded, and observed on the plugin scheduler.
+ /// </para>
+ /// <para>
+ /// If the type derrives <see cref="IAsyncBackgroundWork"/> the <see cref="IAsyncBackgroundWork.DoWorkAsync(ILogProvider, System.Threading.CancellationToken)"/>
+ /// method is called once when the instance is loaded, and observed on the plugin scheduler.
+ /// </para>
+ /// </summary>
+ /// <param name="plugin"></param>
+ /// <param name="serviceType">The service type to instantiate</param>
+ /// <param name="config">The configuration scope to pass directly to the new instance</param>
+ /// <returns>The a new instance configured service</returns>
+ /// <exception cref="KeyNotFoundException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="EntryPointNotFoundException"></exception>
+ /// <exception cref="ConcreteTypeNotFoundException"></exception>
+ /// <exception cref="ConcreteTypeAmbiguousMatchException"></exception>
+ public static object CreateService(this PluginBase plugin, Type serviceType, IConfigScope? config)
+ {
+ _ = plugin ?? throw new ArgumentNullException(nameof(plugin));
+ _ = serviceType ?? throw new ArgumentNullException(nameof(serviceType));
+
plugin.ThrowIfUnloaded();
- Type serviceType = typeof(T);
+ //The requested sesrvice is not a class, so see if we can find a default implementation in assembly
+ if (serviceType.IsAbstract || serviceType.IsInterface)
+ {
+ //Overwrite the service type with the default implementation
+ serviceType = GetTypeImplFromCurrentAssembly(serviceType);
+ }
- T service;
+ object service;
//Determin configuration requirments
if (ConfigurationExtensions.ConfigurationRequired(serviceType) || config != null)
{
- if(config == null)
+ if (config == null)
{
ConfigurationExtensions.ThrowConfigNotFoundForType(serviceType);
}
//Get the constructor for required or available config
- ConstructorInfo? constructor = serviceType.GetConstructor(new Type[] { typeof(PluginBase), typeof(IConfigScope) });
+ ConstructorInfo? constructor = serviceType.GetConstructor(new Type[] { typeof(PluginBase), typeof(IConfigScope) });
//Make sure the constructor exists
_ = constructor ?? throw new EntryPointNotFoundException($"No constructor found for {serviceType.Name}");
//Call constructore
- service = (T)constructor.Invoke(new object[2] { plugin, config });
+ service = constructor.Invoke(new object[2] { plugin, config });
}
else
{
@@ -450,8 +587,8 @@ namespace VNLib.Plugins.Extensions.Loading
_ = constructor ?? throw new EntryPointNotFoundException($"No constructor found for {serviceType.Name}");
//Call constructore
- service = (T)constructor.Invoke(new object[1] { plugin });
- }
+ service = constructor.Invoke(new object[1] { plugin });
+ }
Task? loading = null;
@@ -475,7 +612,7 @@ namespace VNLib.Plugins.Extensions.Loading
#pragma warning restore CA5394 // Do not use insecure randomness
//If the instances supports async loading, dont start work until its loaded
- if(loading != null)
+ if (loading != null)
{
_ = loading.ContinueWith(t => ObserveWork(plugin, bw, randomDelay), TaskScheduler.Default);
}
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/ManagedPasswordHashing.cs b/lib/VNLib.Plugins.Extensions.Loading/src/ManagedPasswordHashing.cs
index 522bfae..3489789 100644
--- a/lib/VNLib.Plugins.Extensions.Loading/src/ManagedPasswordHashing.cs
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/ManagedPasswordHashing.cs
@@ -25,7 +25,6 @@
using System;
using System.Linq;
using System.Text.Json;
-using System.Threading.Tasks;
using System.Collections.Generic;
using VNLib.Utils;
@@ -121,10 +120,9 @@ namespace VNLib.Plugins.Extensions.Loading
public bool Verify(ReadOnlySpan<byte> passHash, ReadOnlySpan<byte> password) => _loader.Resource.Verify(passHash, password);
}
- private sealed class SecretProvider : VnDisposeable, ISecretProvider, IAsyncConfigurable
+ private sealed class SecretProvider : VnDisposeable, ISecretProvider
{
- private byte[]? _pepper;
- private Exception? _error;
+ private readonly IAsyncLazy<byte[]> _pepper;
public SecretProvider(PluginBase plugin, IConfigScope config)
{
@@ -146,11 +144,19 @@ namespace VNLib.Plugins.Extensions.Loading
{
Passwords = new(this);
}
+
+ //Get the pepper from secret storage
+ _pepper = plugin.GetSecretAsync(LoadingExtensions.PASSWORD_HASHING_KEY)
+ .ToLazy(static sr => sr.GetFromBase64());
}
public SecretProvider(PluginBase plugin)
{
Passwords = new(this);
+
+ //Get the pepper from secret storage
+ _pepper = plugin.GetSecretAsync(LoadingExtensions.PASSWORD_HASHING_KEY)
+ .ToLazy(static sr => sr.GetFromBase64());
}
@@ -162,7 +168,7 @@ namespace VNLib.Plugins.Extensions.Loading
get
{
Check();
- return _pepper!.Length;
+ return _pepper.Value.Length;
}
}
@@ -170,41 +176,21 @@ namespace VNLib.Plugins.Extensions.Loading
{
Check();
//Coppy pepper to buffer
- _pepper.CopyTo(buffer);
+ _pepper.Value.CopyTo(buffer);
//Return pepper length
- return _pepper!.Length;
+ return _pepper.Value.Length;
}
protected override void Check()
{
base.Check();
- if (_error != null)
- {
- throw _error;
- }
+ _ = _pepper.Value;
}
protected override void Free()
{
//Clear the pepper if set
- MemoryUtil.InitializeBlock(_pepper.AsSpan());
- }
-
- public async Task ConfigureServiceAsync(PluginBase plugin)
- {
- try
- {
- //Get the pepper from secret storage
- _pepper = await plugin.TryGetSecretAsync(LoadingExtensions.PASSWORD_HASHING_KEY).ToBase64Bytes();
- }
- catch (Exception ex)
- {
- //Store exception for re-propagation
- _error = ex;
-
- //Propagate exception to system
- throw;
- }
+ MemoryUtil.InitializeBlock(_pepper.Value.AsSpan());
}
}
}
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/ISecretResult.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/ISecretResult.cs
new file mode 100644
index 0000000..b3c8737
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/ISecretResult.cs
@@ -0,0 +1,39 @@
+/*
+* Copyright (c) 2023 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Loading
+* File: ISecretResult.cs
+*
+* ISecretResult.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify
+* it under the terms of the GNU Affero General Public License as
+* published by the Free Software Foundation, either version 3 of the
+* License, or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU Affero General Public License for more details.
+*
+* You should have received a copy of the GNU Affero General Public License
+* along with this program. If not, see https://www.gnu.org/licenses/.
+*/
+
+using System;
+
+namespace VNLib.Plugins.Extensions.Loading
+{
+ /// <summary>
+ /// The result of a secret fetch operation
+ /// </summary>
+ public interface ISecretResult : IDisposable
+ {
+ /// <summary>
+ /// The protected raw result value
+ /// </summary>
+ ReadOnlySpan<char> Result { get; }
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/PrivateKey.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/PrivateKey.cs
index 2e5fb7f..08637cd 100644
--- a/lib/VNLib.Plugins.Extensions.Loading/src/PrivateKey.cs
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/PrivateKey.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2022 Vaughn Nugent
+* Copyright (c) 2023 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Extensions.Loading
@@ -90,7 +90,7 @@ namespace VNLib.Plugins.Extensions.Loading
return alg;
}
- internal PrivateKey(SecretResult secret)
+ internal PrivateKey(ISecretResult secret)
{
//Alloc and get utf8
byte[] buffer = new byte[secret.Result.Length];
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/SecretResult.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/SecretResult.cs
index 6c1c5f8..f2cbd28 100644
--- a/lib/VNLib.Plugins.Extensions.Loading/src/SecretResult.cs
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/SecretResult.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2022 Vaughn Nugent
+* Copyright (c) 2023 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Extensions.Loading
@@ -30,16 +30,15 @@ using VNLib.Utils.Memory;
namespace VNLib.Plugins.Extensions.Loading
{
+
/// <summary>
/// The result of a secret fetch operation
/// </summary>
- public sealed class SecretResult : VnDisposeable
+ public sealed class SecretResult : VnDisposeable, ISecretResult
{
private readonly char[] _secretChars;
- /// <summary>
- /// The protected raw result value
- /// </summary>
+ ///<inheritdoc/>
public ReadOnlySpan<char> Result => _secretChars;
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/VaultSecrets.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/VaultSecrets.cs
index 9e3c222..711ae50 100644
--- a/lib/VNLib.Plugins.Extensions.Loading/src/VaultSecrets.cs
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/VaultSecrets.cs
@@ -61,7 +61,27 @@ namespace VNLib.Plugins.Extensions.Loading
public const string VAULT_URL_KEY = "url";
public const string VAULT_URL_SCHEME = "vault://";
-
+
+
+ /// <summary>
+ /// <para>
+ /// Gets a secret from the "secrets" element.
+ /// </para>
+ /// <para>
+ /// Secrets elements are merged from the host config and plugin local config 'secrets' element.
+ /// before searching. The plugin config takes precedence over the host config.
+ /// </para>
+ /// </summary>
+ /// <param name="plugin"></param>
+ /// <param name="secretName">The name of the secret propery to get</param>
+ /// <returns>The element from the configuration file with the given name, or null if the configuration or property does not exist</returns>
+ /// <exception cref="KeyNotFoundException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static async Task<ISecretResult> GetSecretAsync(this PluginBase plugin, string secretName)
+ {
+ ISecretResult? res = await TryGetSecretAsync(plugin, secretName).ConfigureAwait(false);
+ return res ?? throw new KeyNotFoundException($"Missing required secret {secretName}");
+ }
/// <summary>
/// <para>
@@ -77,21 +97,24 @@ namespace VNLib.Plugins.Extensions.Loading
/// <returns>The element from the configuration file with the given name, or null if the configuration or property does not exist</returns>
/// <exception cref="KeyNotFoundException"></exception>
/// <exception cref="ObjectDisposedException"></exception>
- public static Task<SecretResult?> TryGetSecretAsync(this PluginBase plugin, string secretName)
+ public static Task<ISecretResult?> TryGetSecretAsync(this PluginBase plugin, string secretName)
{
plugin.ThrowIfUnloaded();
+
//Get the secret from the config file raw
string? rawSecret = TryGetSecretInternal(plugin, secretName);
+
if (rawSecret == null)
{
- return Task.FromResult<SecretResult?>(null);
+ return Task.FromResult<ISecretResult?>(null);
}
//Secret is a vault path, or return the raw value
if (!rawSecret.StartsWith(VAULT_URL_SCHEME, StringComparison.OrdinalIgnoreCase))
{
- return Task.FromResult<SecretResult?>(new(rawSecret.AsSpan()));
+ return Task.FromResult<ISecretResult?>(new SecretResult(rawSecret.AsSpan()));
}
+
return GetSecretFromVaultAsync(plugin, rawSecret);
}
@@ -104,7 +127,7 @@ namespace VNLib.Plugins.Extensions.Loading
/// <exception cref="UriFormatException"></exception>
/// <exception cref="KeyNotFoundException"></exception>
/// <exception cref="ObjectDisposedException"></exception>
- public static Task<SecretResult?> GetSecretFromVaultAsync(this PluginBase plugin, ReadOnlySpan<char> vaultPath)
+ public static Task<ISecretResult?> GetSecretFromVaultAsync(this PluginBase plugin, ReadOnlySpan<char> vaultPath)
{
//print the path for debug
if (plugin.IsDebug())
@@ -130,7 +153,7 @@ namespace VNLib.Plugins.Extensions.Loading
string mount = path[..lastSep].ToString();
string secret = path[(lastSep + 1)..].ToString();
- async Task<SecretResult?> execute()
+ async Task<ISecretResult?> execute()
{
//Try load client
IVaultClient? client = plugin.GetVault();
@@ -332,7 +355,7 @@ namespace VNLib.Plugins.Extensions.Loading
/// <returns>The base64 decoded secret as a byte[]</returns>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="InternalBufferTooSmallException"></exception>
- public static byte[] GetFromBase64(this SecretResult secret)
+ public static byte[] GetFromBase64(this ISecretResult secret)
{
_ = secret ?? throw new ArgumentNullException(nameof(secret));
@@ -355,28 +378,12 @@ namespace VNLib.Plugins.Extensions.Loading
}
/// <summary>
- /// Converts the secret recovery task to
- /// </summary>
- /// <param name="secret"></param>
- /// <returns>A task whos result the base64 decoded secret as a byte[]</returns>
- /// <exception cref="ArgumentNullException"></exception>
- /// <exception cref="InternalBufferTooSmallException"></exception>
- public static async Task<byte[]?> ToBase64Bytes(this Task<SecretResult?> secret)
- {
- _ = secret ?? throw new ArgumentNullException(nameof(secret));
-
- using SecretResult? sec = await secret.ConfigureAwait(false);
-
- return sec?.GetFromBase64();
- }
-
- /// <summary>
/// Recovers a certificate from a PEM encoded secret
/// </summary>
/// <param name="secret"></param>
/// <returns>The <see cref="X509Certificate2"/> parsed from the PEM encoded data</returns>
/// <exception cref="ArgumentNullException"></exception>
- public static X509Certificate2 GetCertificate(this SecretResult secret)
+ public static X509Certificate2 GetCertificate(this ISecretResult secret)
{
_ = secret ?? throw new ArgumentNullException(nameof(secret));
return X509Certificate2.CreateFromPem(secret.Result);
@@ -387,7 +394,7 @@ namespace VNLib.Plugins.Extensions.Loading
/// </summary>
/// <param name="secret"></param>
/// <returns>The document parsed from the secret value</returns>
- public static JsonDocument GetJsonDocument(this SecretResult secret)
+ public static JsonDocument GetJsonDocument(this ISecretResult secret)
{
_ = secret ?? throw new ArgumentNullException(nameof(secret));
@@ -409,7 +416,7 @@ namespace VNLib.Plugins.Extensions.Loading
/// <param name="secret"></param>
/// <returns>The <see cref="PublicKey"/> parsed from the SPKI public key</returns>
/// <exception cref="ArgumentNullException"></exception>
- public static PublicKey GetPublicKey(this SecretResult secret)
+ public static PublicKey GetPublicKey(this ISecretResult secret)
{
_ = secret ?? throw new ArgumentNullException(nameof(secret));
@@ -431,7 +438,7 @@ namespace VNLib.Plugins.Extensions.Loading
/// <returns>The <see cref="PrivateKey"/> from the secret value</returns>
/// <exception cref="FormatException"></exception>
/// <exception cref="ArgumentNullException"></exception>
- public static PrivateKey GetPrivateKey(this SecretResult secret)
+ public static PrivateKey GetPrivateKey(this ISecretResult secret)
{
_ = secret ?? throw new ArgumentNullException(nameof(secret));
return new PrivateKey(secret);
@@ -445,7 +452,7 @@ namespace VNLib.Plugins.Extensions.Loading
/// <exception cref="JsonException"></exception>
/// <exception cref="ArgumentException"></exception>
/// <exception cref="ArgumentNullException"></exception>
- public static ReadOnlyJsonWebKey GetJsonWebKey(this SecretResult secret)
+ public static ReadOnlyJsonWebKey GetJsonWebKey(this ISecretResult secret)
{
_ = secret ?? throw new ArgumentNullException(nameof(secret));
@@ -458,6 +465,24 @@ namespace VNLib.Plugins.Extensions.Loading
return new ReadOnlyJsonWebKey(buffer.Span[..count]);
}
+#nullable disable
+
+ /// <summary>
+ /// Converts the secret recovery task to return the base64 decoded secret as a byte[]
+ /// </summary>
+ /// <param name="secret"></param>
+ /// <returns>A task whos result the base64 decoded secret as a byte[]</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="InternalBufferTooSmallException"></exception>
+ public static async Task<byte[]> ToBase64Bytes(this Task<ISecretResult> secret)
+ {
+ _ = secret ?? throw new ArgumentNullException(nameof(secret));
+
+ using ISecretResult sec = await secret.ConfigureAwait(false);
+
+ return sec?.GetFromBase64();
+ }
+
/// <summary>
/// Gets a task that resolves a <see cref="ReadOnlyJsonWebKey"/>
/// from a <see cref="SecretResult"/> task
@@ -465,11 +490,11 @@ namespace VNLib.Plugins.Extensions.Loading
/// <param name="secret"></param>
/// <returns>The <see cref="ReadOnlyJsonWebKey"/> from the secret, or null if the secret was not found</returns>
/// <exception cref="ArgumentNullException"></exception>
- public static async Task<ReadOnlyJsonWebKey?> ToJsonWebKey(this Task<SecretResult?> secret)
+ public static async Task<ReadOnlyJsonWebKey> ToJsonWebKey(this Task<ISecretResult> secret)
{
_ = secret ?? throw new ArgumentNullException(nameof(secret));
- using SecretResult? sec = await secret.ConfigureAwait(false);
+ using ISecretResult sec = await secret.ConfigureAwait(false);
return sec?.GetJsonWebKey();
}
@@ -483,16 +508,65 @@ namespace VNLib.Plugins.Extensions.Loading
/// A value that inidcates that a value is required from the result,
/// or a <see cref="KeyNotFoundException"/> is raised
/// </param>
- /// <returns>The <see cref="ReadOnlyJsonWebKey"/> from the secret, or null if the secret was not found</returns>
+ /// <returns>The <see cref="ReadOnlyJsonWebKey"/> from the secret, or throws <see cref="KeyNotFoundException"/> if the key was not found</returns>
/// <exception cref="ArgumentNullException"></exception>
- public static async Task<ReadOnlyJsonWebKey> ToJsonWebKey(this Task<SecretResult?> secret, bool required)
+ /// <exception cref="KeyNotFoundException"></exception>
+ public static async Task<ReadOnlyJsonWebKey> ToJsonWebKey(this Task<ISecretResult> secret, bool required)
{
_ = secret ?? throw new ArgumentNullException(nameof(secret));
- using SecretResult? sec = await secret.ConfigureAwait(false);
+ using ISecretResult sec = await secret.ConfigureAwait(false);
//If required is true and result is null, raise an exception
return required && sec == null ? throw new KeyNotFoundException("A required secret was missing") : (sec?.GetJsonWebKey()!);
}
+
+ /// <summary>
+ /// Converts a <see cref="SecretResult"/> async operation to a lazy result that can be awaited, that transforms the result
+ /// to your desired type. If the result is null, the default value of <typeparamref name="TResult"/> is returned
+ /// </summary>
+ /// <typeparam name="TResult"></typeparam>
+ /// <param name="result"></param>
+ /// <param name="transformer">Your function to transform the secret to its output form</param>
+ /// <returns>A <see cref="IAsyncLazy{T}"/> </returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ public static IAsyncLazy<TResult> ToLazy<TResult>(this Task<ISecretResult> result, Func<ISecretResult, TResult> transformer)
+ {
+ _ = result ?? throw new ArgumentNullException(nameof(result));
+ _ = transformer ?? throw new ArgumentNullException(nameof(transformer));
+
+ //standard secret transformer
+ static async Task<TResult> Run(Task<ISecretResult> tr, Func<ISecretResult, TResult> transformer)
+ {
+ using ISecretResult res = await tr.ConfigureAwait(false);
+ return res == null ? default : transformer(res);
+ }
+
+ return Run(result, transformer).AsLazy();
+ }
+
+ /// <summary>
+ /// Converts a <see cref="SecretResult"/> async operation to a lazy result that can be awaited, that transforms the result
+ /// to your desired type. If the result is null, the default value of <typeparamref name="TResult"/> is returned
+ /// </summary>
+ /// <typeparam name="TResult"></typeparam>
+ /// <param name="result"></param>
+ /// <param name="transformer">Your function to transform the secret to its output form</param>
+ /// <returns>A <see cref="IAsyncLazy{T}"/> </returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ public static IAsyncLazy<TResult> ToLazy<TResult>(this Task<ISecretResult> result, Func<ISecretResult, Task<TResult>> transformer)
+ {
+ _ = result ?? throw new ArgumentNullException(nameof(result));
+ _ = transformer ?? throw new ArgumentNullException(nameof(transformer));
+
+ //Transform with task transformer
+ static async Task<TResult> Run(Task<ISecretResult?> tr, Func<ISecretResult, Task<TResult>> transformer)
+ {
+ using ISecretResult res = await tr.ConfigureAwait(false);
+ return res == null ? default : await transformer(res).ConfigureAwait(false);
+ }
+
+ return Run(result, transformer).AsLazy();
+ }
}
}