Table of Contents

Acuit Pinpoint Plug-ins

Acuit Pinpoint plug-ins are .NET assemblies that provide custom behavior to Acuit Pinpoint. Plug-ins are supported by these hosts:

Acuit Pinpoint Server and Acuit Pinpoint Workstation use .NET 10. Plug-ins must target .NET 10 or a compatible framework. Examples in this documentation use C#, but any supported .NET language may be used.

Plug-in Assemblies

A plug-in assembly can be any .NET assembly that contains types that will interact with its Acuit Pinpoint host to perform custom behavior.

The plug-in assembly must have an accompanying .deps.json file in the same directory that describes the assembly's dependencies. This file is produced during the build process.

Plug-in Assembly Isolation

Within a particular plug-in host, plug-ins are loaded via isolated assembly load contexts, which means plug-in dependencies that are not provided by their host will be private and will not conflict with other plug-ins.

Any plug-ins located in assemblies in the same directory will be loaded into the same assembly load context. This allows plug-ins, even across separate assemblies, to share types or instances.

Package References

References to NuGet packages that are included with the Acuit Pinpoint Server or Acuit Pinpoint Workstation host should normally be excluded from the plug-in's output to ensure that the types in those packages are loaded from the host and not from the plug-in's isolated assembly context. If they are included in the plug-in's output, those types will be loaded from the plug-in's isolated assembly and will be incompatible with those same types in the host itself, which can cause the plug-in to not work properly whenever those types must be shared between the host and the plug-in.

To exclude a package reference from the plug-in's output, set the ExcludeAssets property to runtime in the PackageReference.

For each Acuit Pinpoint host that supports plug-ins, there is a primary NuGet package that references all of the other NuGet packages that are provided by the host and that contain types that a plug-in might use. Typically, this should be the only package reference requiring the ExcludeAssets property to be set to runtime. However, if the plug-in project references other packages that transitively reference packages that are included with the Acuit Pinpoint host, this can result in those packages being included in the plug-in's output, which can cause the plug-in to not work properly whenever those types must be shared between the host and the plug-in. In that case, those transitive package references must also be explicitly referenced with the ExcludeAssets property set to runtime. See the README file for the primary NuGet package for each Acuit Pinpoint host for a list of packages that should never be included in the plug-in's output.

Example Project

To start developing a plug-in, first create a new .NET class library project targeting .NET 10.0.

Then set the EnableDynamicLoading property to true, which prepares the project so that it can be used as a plug-in. Among other things, this will copy all of its dependencies to the output of the project.

Acuit Pinpoint Server Plug-in

For an Acuit Pinpoint Server plug-in, add a reference to the Acuit.Pinpoint.Server.PlugIns NuGet package, which references all of the package dependencies used to develop plug-ins for Acuit Pinpoint Server. Because these dependencies are provided by Acuit Pinpoint Server, they should be excluded from the plug-in's output by setting the ExcludeAssets property to runtime in the PackageReference.

The resulting project file might look like this:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <EnableDynamicLoading>true</EnableDynamicLoading>
  </PropertyGroup>

  <ItemGroup>
    <!-- The package reference needed to develop a plug-in for Acuit Pinpoint Server: -->
    <PackageReference Include="Acuit.Pinpoint.Server.PlugIns" Version="8.0.0" ExcludeAssets="runtime" />

    <!-- References to other packages used internally by the plug-in (which may or may not be included with Acuit Pinpoint Server): -->
    <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
  </ItemGroup>

</Project>

Acuit Pinpoint Workstation Plug-in

For an Acuit Pinpoint Workstation plug-in, TargetFramework should be changed to net9.0-windows and UseWpf should be set to true. Add a reference to the Acuit.Pinpoint.Workstation.PlugIns NuGet package, which references all of the package dependencies used to develop plug-ins for Acuit Pinpoint Workstation. Because these dependencies are provided by Acuit Pinpoint Workstation, they should be excluded from the plug-in's output by setting the ExcludeAssets property to runtime in the PackageReference.

To prevent runtime files for other platforms to be copied to the output folder, set the PlatformTarget property to x64 and the RuntimeIdentifier property to win-x64.

The resulting project file might look like this:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net9.0-windows</TargetFramework>
    <PlatformTarget>x64</PlatformTarget>
    <RuntimeIdentifier>win-x64</RuntimeIdentifier>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <UseWpf>true</UseWpf>
    <EnableDynamicLoading>true</EnableDynamicLoading>
  </PropertyGroup>

  <ItemGroup>
    <!-- The package reference needed to develop a plug-in for Acuit Pinpoint Workstation: -->
    <PackageReference Include="Acuit.Pinpoint.Workstation.PlugIns" Version="8.0.0" ExcludeAssets="runtime" />

    <!-- References to other packages used internally by the plug-in (which may or may not be included with Acuit Pinpoint Workstation): -->
    <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
  </ItemGroup>

</Project>

Plug-in Modules

Plug-in modules are types located within plug-in assemblies that implement IPlugInModule to register services and/or perform initialization for one or more plug-ins.

A directory containing one or more plug-ins is loaded and initialized all at once by the Acuit Pinpoint host. When this happens and what triggers it to occur depend on the host.

Plug-in modules located within a particular assembly will always be loaded and initialized together, with their own private, shared service provider to which, during initialization, plug-in modules can add services for shared use within the modules and by other types within these plug-in assemblies.

The plug-in module loading process happens like this:

  1. All plug-in assemblies are found by looking for files in the directory (not including any subdirectories) with a .dll extension that have an accompanying file with a .deps.json extension.
  2. Each plug-in assembly is loaded, its types are searched for any that implement IPlugInModule, and for each found, the plug-in module is instantiated with constructor arguments provided from the service provider appropriate for the module's context.
  3. A new service collection is created that will be specific to these plug-in modules.
  4. For each plug-in module, its ConfigureServices method is invoked to allow it to add its own services to the service collection.
  5. The host will try to add its provided services to the service collection (i.e., allowing modules to provide their own implementations first, if desired). Typically, these will be the same services as provided by the service provider appropriate for the module's context above.
  6. The host will build the shared service provider from the service collection, resulting in the shared service provider that will be used by all plug-ins in this directory.
  7. For each plug-in module, its Initialize method is invoked, providing the service provider built in the previous step.
  8. All IInitializePlugIn services are obtained from the shared service provider, and then each instance's Initialize method is invoked. Since these services are obtained from the service provider built in the previous step, they can inject any dependencies available via the shared service provider. Note that these will all be obtained (i.e., created) in the order in which those services were added to the service collection, and then have their Initialize method invoked in that same order.

If any of the above steps fail, the entire process will be aborted and no plug-ins from that directory will be loaded. Any partially-initialized plug-in modules will be disposed if they implement IDisposable, as well as any services added that implement IDisposable.

Notes:

  1. A plug-in assembly is not required to include any plug-in modules (i.e., classes that implement IPlugInModule) if the Acuit Pinpoint host loads specific plug-in types by name. For example, Acuit Pinpoint Server loads line plug-ins by type name, so plug-in modules are optional in those plug-in assemblies; however, plug-in modules are required in plug-in assemblies for Acuit Pinpoint Workstation so that the plug-in can integrate its custom behavior with Acuit Pinpoint Workstation.
  2. See the Dependencies section for important considerations about implementing other dependencies used by plug-ins.
Warning

A plug-in module must take care to never reference the service provider provided to its Initialize method after the plug-in module has been disposed, as the service provider instance will be disposed immediately after the plug-in modules are disposed.

For example, consider an Acuit Pinpoint Workstation plug-in module that registers a view with a reference to the service provider like this:

public class PlugInModule(IRegionManager regionManager) : IPlugInModule
{
    public void ConfigureServices(IServiceCollection services, PlugInHostContext hostContext)
    {
        // Add services provided by this plug-in...
    }

    public void Initialize(IServiceProvider serviceProvider, PlugInHostContext hostContext)
    {
        regionManager.RegisterViewWithRegion<MyCustonView>(RegionNames.MainPlugInRegion, serviceProvider);
        // Note that the IDisposable returned above is ignored
    }
}

This can cause an unexpected ObjectDisposedException ("Cannot access a disposed object. Object name: 'IServiceProvider'") if the plug-in is uninitialized for any reason, since the view will remain registered, referencing the disposed service provider. To avoid this, be sure to remove the view registration when the plug-in module is disposed:

public class PlugInModule(IRegionManager regionManager) : IPlugInModule, IDisposable
{
    private IDisposable? _viewRegistration;

    public void Dispose()
    {
        _viewRegistration.Dispose();
    }

    public void ConfigureServices(IServiceCollection services, PlugInHostContext hostContext)
    {
        // Add services provided by this plug-in...
    }

    public void Initialize(IServiceProvider serviceProvider, PlugInHostContext hostContext)
    {
        _viewRegistration = regionManager.RegisterViewWithRegion<MyCustonView>(RegionNames.MainPlugInRegion, serviceProvider);
    }
}

Services

Plug-ins integrate with their Acuit Pinpoint host via services that they obtain via dependency injection, via arguments to a plug-in type's constructor, or via the IServiceProvider argument to a plug-in module's Initialize method.

Service providers and the services they provide vary depending on the host and on the context:

  • The plug-in host application will typically have a core set of service that are shared across the host application and all plug-ins. See the documentation for each Acuit Pinpoint host for the list of services provided by that host.
  • The plug-in modules within a particular directory share a service provider that includes services added by the plug-in modules themselves, along with the above host-wide services.
  • Host-specific plug-ins (e.g., Acuit Pinpoint Server line plug-ins) will typically have additional services specific to each plug-in context. Note that this means that the service provide made available to a plug-in module will likely be a different instance and provide different services than the service provider made available to a specific plug-in within the same assembly.

Note that Acuit Pinpoint hosts do not support asynchronous disposal of services. Any services registered by plug-in modules that require disposal should implement IDisposable rather than IAsyncDisposable.

Dependencies

There are special considerations for dependencies that are designed to operate within an Acuit Pinpoint host, but are not distributed as plug-ins themselves (i.e., they are published as NuGet packages that Acuit Pinpoint plug-ins reference).

First, these dependencies must be aware of its own references to NuGet packages that are included with the Acuit Pinpoint host, ensuring that runtime assets are not included, just as with plug-in assemblies. The project should set the ExcludeAssets property to runtime in the PackageReference to these packages. This resulting NuGet package will still include the package in its list of dependencies, but with runtime assets excluded, so when the plug-in that references this dependency is built, the runtime assets will not be included in the plug-in's output. It is not recommended to reference the primary Acuit Pinpoint host package (e.g., Acuit.Pinpoint.Server.PlugIns or Acuit.Pinpoint.Workstation.PlugIns) from such a dependency; instead, it should reference only the specific packages that it needs to maximize the number of contexts within which it can be used.

Second, because these NuGet packages will not include .deps.json files, they will not be recognized by the Acuit Pinpoint host as plug-in modules, so they must not rely on being initialized via a plug-in module implementing IPlugInModule. A plug-in module that references that dependency is reponsible for ensuring any of its dependencies are properly initialized. Dependencies should normally provide service collection extension methods that referencing plug-in modules should call while configuring services that will add the services provided by the dependency if they are not already added. For example:

public static AddMyDependency(this IServiceCollection services)
{
    services.TryAddSingleton<IMyDependencyService, MyDependencyService>();
    return services;
}

Any special initialization required by the dependency should be performed by registering one or more IInitializePlugIn services in the extension method described above that will perform the initialization.

Options

It is recommended to use the .NET options pattern for plug-in options that can be configured via Acuit Pinpoint's key/value configuration settings. Dependency injection services should typically be used to configure options when configuring services for a plug-in module. An exception is options specific to a line plug-in, since the line plug-in is created after the plug-in module service collection has been initialized. See Acuit Pinpoint Server Plug-ins for details about how to handle such options.

Options Change Notifications

Normally, IOptionsMonitor<TOptions> should be used to access options values so that changes to options values while the Acuit Pinpoint application is running will take effect. However, note that change notifications raised via OnChange will not be raised when options are invalid due to configuration binding or validation errors. To ensure that these errors are reported properly, health checks should always attempt to get the latest options values (e.g., via CurrentValue), which will throw an exception if the options are invalid due to configuration binding or validation errors.

See .NET runtime issue #44381: There's no way to handle validation errors with IOptionsMonitor.OnChange for discussion about this.

Options Helper Extension Methods

Helper extension methods are available in the Acuit.Pinpoint.Common package via OptionsHelperExtensions that simplify getting options values that could fail configuration binding or validation. For example:

void UseOptions(IOptionsMonitor<PlugInOptions> optionsMonitor)
{
    if (optionsMonitor.TryGet(out PlugInOptions? options, out Exception? error))
    {
        // Use options here, knowing there were no binding or validation errors
    }
    else
    {
        // Handle invalid options.
        // Can use error.MessagesHierarchyAsSingleLine() to get an error message for display.
    }
}