Skip to content

Commit

Permalink
[ODS-6120] External connection configuration via plugin (#1119)
Browse files Browse the repository at this point in the history
  • Loading branch information
mjaksn authored Aug 20, 2024
1 parent fc54ba0 commit 0d82779
Show file tree
Hide file tree
Showing 7 changed files with 168 additions and 161 deletions.
189 changes: 115 additions & 74 deletions Application/EdFi.Ods.Api/Helpers/AssemblyLoaderHelper.cs

Large diffs are not rendered by default.

18 changes: 9 additions & 9 deletions Application/EdFi.Ods.Api/Helpers/TypeHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public static IEnumerable<Type> GetModuleTypes()
return allModules
.OrderBy(m =>
// Plugin modules should be invoked last
m.type.IsImplementationOf<IPluginModule>() ? 2
m.type.IsImplementationOf<ICustomModule>() ? 2
// ... after modules using "Override" prefix naming convention
: m.type.Name.StartsWithIgnoreCase("Override") ? 1
// ... after all others
Expand All @@ -50,22 +50,22 @@ public static IEnumerable<Type> GetModuleTypes()
return allModules;
}

private static Type[] _pluginTypes;
private static Type[] _foundTypes;

public static IEnumerable<Type> GetPluginTypes()
public static IEnumerable<Type> GetAssemblyTypes<T>()
{
if (_pluginTypes != null)
if (_foundTypes != null)
{
return _pluginTypes;
return _foundTypes;
}

var allPlugins = GetImplementationsOf<IPlugin>().ToArray();
var allFoundAssemblies = GetImplementationsOf<T>().ToArray();

_logger.Debug($"Assemblies with plugins: {string.Join(", ", allPlugins.Select(m => m.assembly.GetName().Name))}");
_logger.Debug($"Assemblies with type: {string.Join(", ", allFoundAssemblies.Select(m => m.assembly.GetName().Name))}");

_pluginTypes ??= allPlugins.Select(t => t.type).ToArray();
_foundTypes ??= allFoundAssemblies.Select(t => t.type).ToArray();

return _pluginTypes;
return _foundTypes;
}
}
}
72 changes: 18 additions & 54 deletions Application/EdFi.Ods.Api/Startup/OdsStartupBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
Expand Down Expand Up @@ -138,7 +137,7 @@ public void ConfigureServices(IServiceCollection services)
return factory.GetUrlHelper(actionContext);
});

AssemblyLoaderHelper.LoadAssembliesFromExecutingFolder();
AssemblyLoaderHelper.LoadAssembliesFromFolder();

var pluginInfos = LoadPlugins(pluginSettings);

Expand Down Expand Up @@ -247,18 +246,18 @@ void ConfigurePluginsServices()
{
_logger.Debug("Configuring services in plugins:");

foreach (var type in TypeHelper.GetPluginTypes())
foreach (var servicesConfigurationActivityType in TypeHelper.GetAssemblyTypes<IServicesConfigurationActivity>())
{
_logger.Debug($"Plugin {type.Name}");
_logger.Debug($"Executing services configuration activity '{servicesConfigurationActivityType.Name}'...");

try
{
var plugin = (IPlugin) Activator.CreateInstance(type);
plugin?.ConfigureServices(Configuration, services, _apiSettings);
var servicesConfigurationActivity = (IServicesConfigurationActivity) Activator.CreateInstance(servicesConfigurationActivityType);
servicesConfigurationActivity?.ConfigureServices(Configuration, services, _apiSettings);
}
catch (Exception ex)
{
_logger.Error($"Error configuring services using plugin '{type.Name}'.", ex);
_logger.Error($"Error occured during service configuration activity '{servicesConfigurationActivityType.Name}': ", ex);
}
}
}
Expand Down Expand Up @@ -294,13 +293,20 @@ void RegisterModulesDynamically()
{
_logger.Debug($"Module {type.Name}");

if (type.IsSubclassOf(typeof(ConditionalModule)))
try
{
builder.RegisterModule((IModule) Activator.CreateInstance(type, _apiSettings));
if (type.IsSubclassOf(typeof(ConditionalModule)))
{
builder.RegisterModule((IModule)Activator.CreateInstance(type, _apiSettings));
}
else
{
builder.RegisterModule((IModule)Activator.CreateInstance(type));
}
}
else
catch (Exception ex)
{
builder.RegisterModule((IModule) Activator.CreateInstance(type));
_logger.Error($"Error registering module '{type.Name}'.", ex);
}
}
}
Expand Down Expand Up @@ -430,52 +436,10 @@ void SetStaticResolvers()
NHibernate.Cfg.Environment.ObjectsFactory = new NHibernateAutofacObjectsFactory(Container);
}
}

private string GetPluginFolder(Plugin pluginSettings)
{
if (string.IsNullOrWhiteSpace(pluginSettings.Folder))
{
return string.Empty;
}

if (Path.IsPathRooted(pluginSettings.Folder))
{
return pluginSettings.Folder;
}

// in a developer environment the plugin folder is relative to the WebApi project
// "Ed-Fi-ODS-Implementation/Application/EdFi.Ods.WebApi/bin/Debug/net8.0/../../../" => "Ed-Fi-ODS-Implementation/Application/EdFi.Ods.WebApi"
var projectDirectory = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory!, "../../../"));
var relativeToProject = Path.GetFullPath(Path.Combine(projectDirectory, pluginSettings.Folder));

if (Directory.Exists(relativeToProject))
{
return relativeToProject;
}

// in a deployment environment the plugin folder is relative to the executable
// "C:/inetpub/Ed-Fi/WebApi"
var relativeToExecutable = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory!, pluginSettings.Folder));

if (Directory.Exists(relativeToExecutable))
{
return relativeToExecutable;
}

// last attempt to get directory relative to the working directory
var relativeToWorkingDirectory = Path.GetFullPath(pluginSettings.Folder);

if (Directory.Exists(relativeToWorkingDirectory))
{
return relativeToWorkingDirectory;
}

return pluginSettings.Folder;
}

private PluginInfo[] LoadPlugins(Plugin pluginSettings)
{
var pluginFolder = GetPluginFolder(pluginSettings);
var pluginFolder = AssemblyLoaderHelper.GetPluginFolder(pluginSettings.Folder);
var pluginFolderSettingsName = $"{nameof(Plugin)}:{nameof(Plugin.Folder)}";

if (string.IsNullOrWhiteSpace(pluginFolder))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
namespace EdFi.Ods.Common;

/// <summary>
/// Marks a module as a "plugin" module so that it is invoked after all other types of modules so that
/// Marks a module as a "custom" module so that it is invoked after all other types of modules so that
/// services registered this way override services registered by other modules.
/// </summary>
public interface IPluginModule : IModule { }
public interface ICustomModule : IModule { }
20 changes: 20 additions & 0 deletions Application/EdFi.Ods.Common/IHostConfigurationActivity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: Apache-2.0
// Licensed to the Ed-Fi Alliance under one or more agreements.
// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
// See the LICENSE and NOTICES files in the project root for more information.

using Microsoft.Extensions.Hosting;

namespace EdFi.Ods.Common;

/// <summary>
/// Provides a custom assembly author a way of configuring the host before it is built.
/// </summary>
public interface IHostConfigurationActivity
{
/// <summary>
/// Configures the host prior to being finalized and built.
/// </summary>
/// <param name="hostBuilder">The <see cref="IHostBuilder" /> on which to perform configuration activities.</param>
void ConfigureHost(IHostBuilder hostBuilder);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,15 @@
using EdFi.Ods.Common.Configuration;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace EdFi.Ods.Common;

/// <summary>
/// Provides a custom assembly author a way of configuring the host before it is built, and then to configure
/// services direction with the <see cref="IServiceCollection" /> during ASP.NET configuration process.
/// Provides a custom assembly author a way to configure services registration with
/// the <see cref="IServiceCollection" /> during ASP.NET configuration process.
/// </summary>
/// <seealso cref="IPluginModule"/>
public interface IPlugin
/// <seealso cref="ICustomModule"/>
public interface IServicesConfigurationActivity
{
/// <summary>
/// Configures services during ASP.NET startup/configuration process.
Expand All @@ -24,10 +23,4 @@ public interface IPlugin
/// <param name="services">The service collection being configured.</param>
/// <param name="apiSettings">The <see cref="ApiSettings" /> configuration object that has already been bound from the configuration.</param>
void ConfigureServices(IConfigurationRoot configuration, IServiceCollection services, ApiSettings apiSettings);

/// <summary>
/// Configures the host prior to being finalized and built.
/// </summary>
/// <param name="hostBuilder">The <see cref="IHostBuilder" /> on which to perform configuration activities.</param>
void ConfigureHost(IHostBuilder hostBuilder);
}
Original file line number Diff line number Diff line change
Expand Up @@ -251,17 +251,6 @@ public void Should_return_empty_list_when_plugin_folder_name_is_empty()
result.ShouldBeEmpty();
}

[Test]
public void Should_throw_exception_when_an_invalid_plugin_assembly_is_in_plugin_folder()
{
// Arrange

// Act & Assert
Should.Throw<Exception>(() => AssemblyLoaderHelper.FindPluginAssemblies(_unitTestInvalidPluginFolder).ToList())
.Message
.ShouldBe("No plugin artifacts were found in assembly 'InvalidPluginAssembly.dll'. Expected an IPluginMarker implementation and assembly metadata embedded resource 'assemblyMetadata.json' (for Profiles or Extensions plugins), or implementations of IPlugin and/or IPlugModule (for custom application plugins).");
}

// This method uses parts of the example code found at the following link
// https://learn.microsoft.com/en-us/dotnet/standard/io/how-to-copy-directories
private static bool FileExistsInDirectory(string fileName, string directory, bool recursive)
Expand Down

0 comments on commit 0d82779

Please sign in to comment.