Prise Plugin Framework

repository·master·Indexed 18 days ago

https://github.com/merken/prise

A .NET (Core) plugin framework for creating decoupled, customizable, and backwards-compatible plugin architectures. Prise enables loading plugins from foreign assemblies and NuGet packages while managing dependency isolation to prevent assembly mismatches. It features a system based on Hosts, Contracts, and Plugins, providing tools like IPluginLoader for discovery, IPluginBootstrapper for service registration, and ReverseProxy for sharing Host services with plugins.

Tokens
4.3K
Snippets
12
Records
17
Agent score
14%

What's inside Prise

  1. What is Prise?

    master

    Prise is a plugin framework for .NET (Core) applications. It is designed to enable the development of decoupled pieces of code with minimal effort while maximizing customizability and backwards compatibility.

    Key capabilities include:

    • Loading plugins from foreign assemblies.
    • Decoupling local and remote dependencies to avoid assembly mismatches.
    • Fully customizable plugin loading processes.
    • Support for loading plugins via Prise Plugin Packages (NuGet).
    • Maintaining backwards compatibility for older plugins.
  2. Overview of Prise plugin framework

    master
    Prise is a plugin framework for .NET (Core) applications designed to enable the development of decoupled pieces of code with minimal effort. Its primary goals are to maximize customizability and backwards compatibility while helping you load plugins from foreign assemblies. Prise is specifically built to decouple local and remote dependencies and strives to avoid common assembly mismatch issues.
  3. Understand the Prise plugin system architecture

    master

    Prise is built around three core components that work together to enable dynamic extensibility:

    • 🎩 Host: The main application (e.g., ASP.NET Core Web app, Console app, Azure Function) that consumes plugins.
    • 📝 Contract: A shared library (typically a .NET Standard class library) containing the interfaces and data models that define the operations the Host can invoke on the Plugin.
    • 🔌 Plugin: A separate assembly that implements the interfaces defined in the Contract.

    This separation allows the Host to remain decoupled from specific plugin implementations, enabling features like hot-swapping plugins without restarting the Host application.

  4. Create a Prise Plugin

    master

    To create a plugin that the Host can discover, follow these requirements:

    1. Project Setup: Create a class library (e.g., netcoreapp3.1 or higher) and add the Prise.Plugin NuGet package and a reference to your shared Contract.

      dotnet add package Prise.Plugin
      dotnet add reference ../Your.Contract
    2. Implement the Contract: Create a class that implements the interface defined in your Contract.

    3. Decorate with [Plugin] attribute: You must annotate your plugin class with the [Plugin] attribute, specifying the PluginType (the interface from your Contract). This makes the plugin discoverable by Prise.

    Note: Implementation of the interface methods is optional as long as the class exposes the correct method signature (name, parameters, and return type) that matches the Contract, as Prise can invoke plugins via reflection.

    [Plugin(PluginType = typeof(IWeatherPlugin))]
    public class OpenWeatherPlugin : IWeatherPlugin
    {
        public async Task<IEnumerable<WeatherForecast>> GetWeatherFor(string location)
        {
            // Implementation logic
        }
    }
  5. Test plugins using Prise.Testing

    master

    To unit test a plugin, use the Prise.Testing package to simulate the Prise environment. The CreateTestPluginInstance<T> method allows you to instantiate a plugin and automatically inject mock or real services into its [PluginService] fields.

    Setup:

    1. Create an MSTest project.
    2. Add Prise.Testing and Moq packages.
    3. Reference your Plugin project.
    4. Use CreateTestPluginInstance<T>(params object[] services) to create the test instance.
    // Example test using Moq and Prise.Testing
    var openWeatherServiceMock = new Mock<IOpenWeatherService>(MockBehavior.Strict);
    var converterService = new ConverterService();
    
    // Mock setup...
    openWeatherServiceMock.Setup(w => w.GetForecastsFor(city)).ReturnsAsync(responseModel);
    
    // Prise.Testing injects the mocks into the plugin fields
    var plugin = Prise.Testing.CreateTestPluginInstance<OpenWeatherPlugin>(openWeatherServiceMock.Object, converterService);
    
    var results = await plugin.GetWeatherFor(city);
    Assert.AreEqual(description, results.First().Summary);
  6. Share Host services with a Plugin

    master

    Plugins often need access to Host-level data or configuration. To share a service from the Host to a Plugin, follow these steps:

    1. Define an interface in the Contract: The interface must be part of the shared contract project.
    2. Implement the service in the Host: Register the implementation in the Host's IServiceCollection.
    3. Configure the Load Context: When calling IPluginLoader.LoadPlugin<T>(), use the configure parameter to add the host service to the plugin's load context using loadContext.AddHostService<T>(instance).
    4. Use a ReverseProxy in the Plugin: To maintain compatibility and allow the plugin to call back into the host, implement the interface in the plugin using a class that inherits from Prise.ReverseProxy.
    // In the Host: Loading the plugin and sharing a service
    var plugin = await this.weatherPluginLoader.LoadPlugin<IWeatherPlugin>(scanResult, configure: (loadContext) =>
    {
        loadContext.AddHostService<IConfigurationService>(this.configurationService);
    });
    
    // In the Plugin: Implementing the proxy
    public class ConfigurationServiceProxy : ReverseProxy, IConfigurationService
    {
        public ConfigurationServiceProxy(object hostService) : base(hostService) { }
    
        public string GetConfigurationValueForKey(string key)
        {
            return this.InvokeOnHostService<string>(key);
        }
    }
  7. Set up a Prise Host in ASP.NET Core

    master

    To use Prise in an ASP.NET Core application, follow these steps:

    1. Install the NuGet package:

      dotnet add package Prise
    2. Add the Contract reference: Add a project reference to your shared Contract library.

      dotnet add reference ../Your.Contract
    3. Register Prise services: In your Startup.cs (or Program.cs for newer templates), call AddPrise() within the ConfigureServices method:

      public void ConfigureServices(IServiceCollection services)
      {
          services.AddControllers();
          services.AddPrise();
      }
    4. Inject IPluginLoader: Inject the IPluginLoader into your controllers or services to find and load plugins at runtime.

    public class WeatherForecastController : ControllerBase
    {
        private readonly IPluginLoader weatherPluginLoader;
    
        public WeatherForecastController(ILogger<WeatherForecastController> logger, IPluginLoader weatherPluginLoader)
        {
            this.weatherPluginLoader = weatherPluginLoader;
        }
    }
  8. Install Prise packages via NuGet

    master

    Prise and its related extensions are available as NuGet packages. Depending on your requirements, you can install the core framework or specific extensions like MVC support, Proxying, or Testing support.

    Core packages:

    • Prise
    • Prise.Plugin

    Extension packages:

    • Prise.Mvc
    • Prise.Proxy
    • Prise.ReverseProxy
    • Prise.Testing
  9. Install Prise via NuGet

    master

    Prise and its related ecosystem are available as NuGet packages. Depending on your use case, you may need one or more of the following:

    • Prise: The core framework.
    • Prise.Plugin: For developing plugins.
    • Prise.Mvc: For integration with ASP.NET Core MVC.
    • Prise.Proxy or Prise.ReverseProxy: For proxy-related plugin functionality.
    • Prise.Testing: For testing hosts and plugins.
    # Example installation of the core framework
    dotnet add package Prise
    
    # Example installation of the plugin development package
    dotnet add package Prise.Plugin
  10. Distribute plugins as NuGet packages

    master

    Prise supports loading plugins directly from .nupkg files.

    Workflow:

    1. Use the Prise Publish Plugin Extension for VS Code to create a .nuspec file for your plugin.
    2. Right-click the .csproj and select "Publish Prise Plugin as NuGet package" to generate the .nupkg.
    3. In the Host, replace services.AddPrise() with services.AddPriseNugetPackages() in Startup.cs.

    The NuGet Assembly Scanner will scan specified locations, decompress packages, and automatically replace existing plugins if the version in the .nupkg is newer.

    // In the Host Startup.cs
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
        services.AddScoped<IConfigurationService, AppSettingsConfigurationService>();
        services.AddPriseNugetPackages(); // Enables NuGet loading
    }
  11. Configure Plugin publishing with prise.plugin.json

    master

    Prise supports a plugin-specific configuration file for managing how plugins are built and published.

    Using the Prise Publish Plugin Extension for VS Code, you can right-click a .csproj file and select Create Prise Plugin File to scaffold a prise.plugin.json file.

    Key configuration:

    • publishDir: A relative or absolute path on the filesystem where the plugin should be published (e.g., "../_dist").

    Once configured, you can use the extension to Publish Prise Plugin, which runs dotnet publish and copies the output to the specified publishDir.

    {
      "publishDir": "../_dist"
    }
  12. Inject Host services into a Bootstrapper using [BootstrapperService]

    master

    If you need a Host service (like IConfigurationService) inside your IPluginBootstrapper to configure other services, use the [BootstrapperService] attribute on a field.

    You must specify the ServiceType and the ProxyType (the ReverseProxy implementation that lives in your plugin project). Prise will inject the host service into the bootstrapper instance.

    [PluginBootstrapper(PluginType = typeof(OpenWeatherPlugin))]
    public class OpenWeatherPluginBootstrapper : IPluginBootstrapper
    {
        [BootstrapperService(
            ServiceType = typeof(IConfigurationService), 
            ProxyType = typeof(ConfigurationServiceProxy))]
        private readonly IConfigurationService configurationService;
    
        public IServiceCollection Bootstrap(IServiceCollection services)
        {
            // configurationService is now available for use here
            return services;
        }
    }