Skip to content

Latest commit

 

History

History
596 lines (447 loc) · 24 KB

addin-development.md

File metadata and controls

596 lines (447 loc) · 24 KB

Addin development

This page uses the a sample addin called BasicFodyAddin to describe building an addin.

Lib/Reference project

BasicFodyAddin.csproj

  • Contain all classes to control the addin behavior at compile time or provide intellisense to consumers. Often this is in the form of Attributes.
  • Generally any usage and reference to this project is removed at compile time so it is not needed as part of application deployment.
  • The target frameworks depends on what targets the weaver can support (see Supported Runtimes And Ide).

This project is also used to produce the NuGet package. To achieve this the project consumes two NuGets:

  • Fody with PrivateAssets="None". This results in producing NuGet package having a dependency on Fody with all include="All" in the nuspec. Note that while this project consumes the Fody NuGet, weaving is not performed on this project. This is due to the FodyPackaging NuGet (see below) including <DisableFody>true</DisableFody> in the MSBuild pipeline.
  • FodyPackaging with PrivateAssets="All". This results in a NuGet package being produced by this project, but no dependency on FodyPackaging in the resulting NuGet package.

The produced NuGet package will:

  • Be named with .Fody suffix. This project should also contain all appropriate NuGet metadata properties. Many of these properties have defaults in FodyPackaging, but can be overridden.
  • Target, and hence support from a consumer perspective, the same frameworks that this project targets.
  • Be created in a directory named nugets at the root of the solution.

<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFrameworks>net452;netstandard2.0;netstandard2.1</TargetFrameworks>
    <SignAssembly>true</SignAssembly>
    <AssemblyOriginatorKeyFile>key.snk</AssemblyOriginatorKeyFile>
    <Authors>Simon Cropp</Authors>
    <Copyright>Copyright $([System.DateTime]::UtcNow.ToString(yyyy)).</Copyright>
    <Description>Injects a new type that writes "Hello World".</Description>
    <PackageLicenseExpression>MIT</PackageLicenseExpression>
    <!-- PackageTags are optional. Defaults to 'ILWeaving, Fody, Cecil, AOP' -->
    <PackageTags>Hello World, ILWeaving, Fody, Cecil, AOP</PackageTags>
    <PackageOutputPath>$(SolutionDir)../nugets</PackageOutputPath>
    <PackageProjectUrl>https://github.com/Fody/Home/tree/master/BasicFodyAddin</PackageProjectUrl>
    <PackageIconUrl>https://raw.githubusercontent.com/Fody/Home/master/BasicFodyAddin/package_icon.png</PackageIconUrl>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Fody" Version="6.6.4" PrivateAssets="none" />
    <PackageReference Include="FodyPackaging" Version="6.6.4" PrivateAssets="All" />
  </ItemGroup>
</Project>

snippet source | anchor

Build Order

The Lib/Reference project must contain a Project Dependency on the Weaver-Project to ensure it is built after the Weaver Project produces its output.

project dependencies

If a weaver file cannot be found, the build will fail with one of the following:

FodyPackaging: No weaver found at [PATH]. BasicFodyAddin should have a Project Dependency on BasicFodyAddin.Fody.

Weaver Project

BasicFodyAddin.Fody.csproj

This project contains the weaving code.

  • Has a NuGet dependency on FodyHelpers.
  • Should not have any runtime dependencies (excluding Mono Cecil); runtime dependencies should be combined using e.g. ILMerge and the /Internalize flag.
  • The assembly must contain a public class named 'ModuleWeaver'. The namespace does not matter.

<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="FodyHelpers" Version="6.6.4" />
  </ItemGroup>
</Project>

snippet source | anchor

Target Frameworks

This project must target netstandard2.0.

Output of the project

It outputs a file named BasicFodyAddin.Fody. The '.Fody' suffix is necessary to be picked up by Fody at compile time.

ModuleWeaver

ModuleWeaver.cs is where the target assembly is modified. Fody will pick up this type during its processing. Note that the class must be named as ModuleWeaver.

ModuleWeaver must use the base class of BaseModuleWeaver which exists in the FodyHelpers NuGet.

  • Inherit from BaseModuleWeaver.
  • The class must be public, non static, and not abstract.
  • Have an empty constructor.

public class ModuleWeaver :
    BaseModuleWeaver
{

    public override void Execute()
    {
        var ns = GetNamespace();
        var type = new TypeDefinition(ns, "Hello", TypeAttributes.Public, TypeSystem.ObjectReference);

        AddConstructor(type);

        AddHelloWorld(type);

        ModuleDefinition.Types.Add(type);
        WriteInfo("Added type 'Hello' with method 'World'.");
    }

    public override IEnumerable<string> GetAssembliesForScanning()
    {
        yield return "netstandard";
        yield return "mscorlib";
    }

    string GetNamespace()
    {
        var namespaceFromConfig = GetNamespaceFromConfig();
        var namespaceFromAttribute = GetNamespaceFromAttribute();
        if (namespaceFromConfig != null && namespaceFromAttribute != null)
        {
            throw new WeavingException("Configuring namespace from both Config and Attribute is not supported.");
        }

        if (namespaceFromAttribute != null)
        {
            return namespaceFromAttribute;
        }

        return namespaceFromConfig;
    }

    string GetNamespaceFromConfig()
    {
        var attribute = Config?.Attribute("Namespace");
        if (attribute == null)
        {
            return null;
        }

        var value = attribute.Value;
        ValidateNamespace(value);
        return value;
    }

    string GetNamespaceFromAttribute()
    {
        var attributes = ModuleDefinition.Assembly.CustomAttributes;
        var namespaceAttribute = attributes
            .SingleOrDefault(x => x.AttributeType.FullName == "NamespaceAttribute");
        if (namespaceAttribute == null)
        {
            return null;
        }

        attributes.Remove(namespaceAttribute);
        var value = (string)namespaceAttribute.ConstructorArguments.First().Value;
        ValidateNamespace(value);
        return value;
    }

    static void ValidateNamespace(string value)
    {
        if (value is null || string.IsNullOrWhiteSpace(value))
        {
            throw new WeavingException("Invalid namespace");
        }
    }

    void AddConstructor(TypeDefinition newType)
    {
        var attributes = MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName;
        var method = new MethodDefinition(".ctor", attributes, TypeSystem.VoidReference);
        var objectConstructor = ModuleDefinition.ImportReference(TypeSystem.ObjectDefinition.GetConstructors().First());
        var processor = method.Body.GetILProcessor();
        processor.Emit(OpCodes.Ldarg_0);
        processor.Emit(OpCodes.Call, objectConstructor);
        processor.Emit(OpCodes.Ret);
        newType.Methods.Add(method);
    }

    void AddHelloWorld(TypeDefinition newType)
    {
        var method = new MethodDefinition("World", MethodAttributes.Public, TypeSystem.StringReference);
        var processor = method.Body.GetILProcessor();
        processor.Emit(OpCodes.Ldstr, "Hello World");
        processor.Emit(OpCodes.Ret);
        newType.Methods.Add(method);
    }

    public override bool ShouldCleanReference => true;
}

snippet source | anchor

BaseModuleWeaver.Execute

Called to perform the manipulation of the module. The current module can be accessed and manipulated via BaseModuleWeaver.ModuleDefinition.

public override void Execute()
{
    var ns = GetNamespace();
    var type = new TypeDefinition(ns, "Hello", TypeAttributes.Public, TypeSystem.ObjectReference);

    AddConstructor(type);

    AddHelloWorld(type);

    ModuleDefinition.Types.Add(type);
    WriteInfo("Added type 'Hello' with method 'World'.");
}

snippet source | anchor

Resultant injected code

In this case a new type is being injected into the target assembly that looks like this.

public class Hello
{
    public string World()
    {
        return "Hello World";
    }
}

BaseModuleWeaver.GetAssembliesForScanning

Called by Fody when it is building up a type cache for lookups. This method should return all possible assemblies that the weaver may require while resolving types. In this case BasicFodyAddin requires System.Object, so GetAssembliesForScanning returns netstandard and mscorlib. It is safe to return assembly names that are not used by the current target assembly as these will be ignored.

To use this type cache, a ModuleWeaver can call BaseModuleWeaver.FindType within Execute method.

public override IEnumerable<string> GetAssembliesForScanning()
{
    yield return "netstandard";
    yield return "mscorlib";
}

snippet source | anchor

BaseModuleWeaver.ShouldCleanReference

When BasicFodyAddin.dll is referenced by a consuming project, it is only for the purposes configuring the weaving via attributes. As such, it is not required at runtime. With this in mind BaseModuleWeaver has an opt in feature to remove the reference, meaning the target weaved application does not need BasicFodyAddin.dll at runtime. This feature can be opted in to via the following code in ModuleWeaver:

public override bool ShouldCleanReference => true;

snippet source | anchor

Logging

Many helpers exist for writing log entries to MSBuild:

using System.Collections.Generic;
using System.Linq;
using Fody;

public class MyLoggingWeaver :
    BaseModuleWeaver
{
    public override void Execute()
    {
        // Write a log entry with a specific MessageImportance
        WriteMessage("Message", MessageImportance.High);

        // Write a log entry with the MessageImportance.Low level
        WriteDebug("Message");

        // Write a log entry with the MessageImportance.Normal level
        WriteInfo("Message");

        // Write a warning
        WriteWarning("Message");

        // Write an error
        WriteError("Message");

        var type = ModuleDefinition.GetType("MyType");
        var method = type.Methods.First();

        // Write an error using the first SequencePoint
        // of a method for the line information
        WriteWarning("Message", method);

        // Write an error using the first SequencePoint
        // of a method for the line information
        WriteError("Message", method);

        var sequencePoint = method.DebugInformation.SequencePoints.First();

        // Write an warning using a SequencePoint
        // for the line information
        WriteWarning("Message", sequencePoint);

        // Write an error using a SequencePoint
        // for the line information
        WriteError("Message", sequencePoint);
    }

    public override IEnumerable<string> GetAssembliesForScanning() => Enumerable.Empty<string>();
}

snippet source | anchor

Other BaseModuleWeaver Members

BaseModuleWeaver has other members for extensibility: https://github.com/Fody/Fody/blob/master/FodyHelpers/BaseModuleWeaver.cs

Throwing exceptions

When writing an addin there are a points to note when throwing an Exception.

  • Exceptions thrown from an addin will be caught and interpreted as a build error. So this will stop the build.
  • The exception information will be logged to the MSBuild BuildEngine.LogErrorEvent method.
  • If the exception type is WeavingException then it will be interpreted as an "error". So the addin is explicitly throwing an exception with the intent of stopping processing and logging a message to the build log. In this case the message logged will be the contents of WeavingException.Message property. If the WeavingException has a property SequencePoint then that information will be passed to the build engine so a user can navigate to the error.
  • If the exception type is not a WeavingException then it will be interpreted as an "unhandled exception". So something has gone seriously wrong with the addin. It most likely has a bug. In this case message logged be much bore verbose and will contain the full contents of the Exception. The code for getting the message can be found here in ExceptionExtensions.

Passing config via to FodyWeavers.xml

This file exists at a project level in the users target project and is used to pass configuration to the 'ModuleWeaver'.

So if the FodyWeavers.xml file contains the following:

<Weavers>
  <BasicFodyAddin Namespace="MyNamespace"/>
</Weavers>

The property of the ModuleWeaver.Config will be an XElement containing:

<BasicFodyAddin Namespace="MyNamespace"/>

Supporting intellisense for FodyWeavers.xml

Fody will create or update a schema file (FodyWeavers.xsd) for every FodyWeavers.xml during compilation, adding all detected weavers. Every weaver now can provide a schema fragment describing it's individual properties and content that can be set. This file must be part of the weaver project and named <project name>.xcf. It contains the element describing the type of the configuration node. The file must be published side by side with the weaver file; however FodyPackaging will configure this correctly based on the convention WeaverName.Fody.xcf.

Sample content of the BasicFodyAddin.Fody.xcf:

<?xml version="1.0" encoding="utf-8" ?>
<xs:complexType xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:attribute name="Namespace"
                type="xs:string">
    <xs:annotation>
      <xs:documentation>Namespace to use for the injected type</xs:documentation>
    </xs:annotation>
  </xs:attribute>
</xs:complexType>

snippet source | anchor

Fody will then combine all .xcf fragments with the weavers information to the final .xsd:

<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
  <xs:element name="Weavers">
    <xs:complexType>
      <xs:all>
        <xs:element name="BasicFodyAddin" minOccurs="0" maxOccurs="1">
          <xs:complexType>
            <xs:attribute name="Namespace" type="xs:string">
              <xs:annotation>
                <xs:documentation>Namespace to use for the injected type</xs:documentation>
              </xs:annotation>
            </xs:attribute>
          </xs:complexType>
        </xs:element>
      </xs:all>
      <xs:attribute name="VerifyAssembly" type="xs:boolean">
        <xs:annotation>
          <xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
        </xs:annotation>
      </xs:attribute>
      <xs:attribute name="VerifyIgnoreCodes" type="xs:string">
        <xs:annotation>
          <xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
        </xs:annotation>
      </xs:attribute>
      <xs:attribute name="GenerateXsd" type="xs:boolean">
        <xs:annotation>
          <xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
        </xs:annotation>
      </xs:attribute>
    </xs:complexType>
  </xs:element>
</xs:schema>

snippet source | anchor

AssemblyToProcess Project

AssemblyToProcess.csproj

A target assembly to process and then validate with unit tests.

Tests Project

Tests.csproj

Contains all tests for the weaver.

The project has a NuGet dependency on FodyHelpers.

It has a reference to the AssemblyToProcess project, so that AssemblyToProcess.dll is copied to the bin directory of the test project.

FodyHelpers contains a utility WeaverTestHelper for executing test runs on a target assembly using a ModuleWeaver.

A test can then be run as follows:

public class WeaverTests
{
    static TestResult testResult;

    static WeaverTests()
    {
        var weavingTask = new ModuleWeaver();
        testResult = weavingTask.ExecuteTestRun("AssemblyToProcess.dll");
    }

    [Fact]
    public void ValidateHelloWorldIsInjected()
    {
        var type = testResult.Assembly.GetType("TheNamespace.Hello");
        var instance = (dynamic)Activator.CreateInstance(type);

        Assert.Equal("Hello World", instance.World());
    }
}

snippet source | anchor

By default ExecuteTestRun will perform a PeVerify on the resultant assembly.

<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFrameworks>net472;netcoreapp2.2</TargetFrameworks>
    <DisableFody>true</DisableFody>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="FodyHelpers" Version="6.6.4" />
    <PackageReference Include="Xunit" Version="2.4.2" />
    <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" PrivateAssets="all" />
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
    <ProjectReference Include="..\BasicFodyAddin.Fody\BasicFodyAddin.Fody.csproj" />
    <ProjectReference Include="..\BasicFodyAddin\BasicFodyAddin.csproj" />
    <ProjectReference Include="..\AssemblyToProcess\AssemblyToProcess.csproj" />
    <Reference Include="Microsoft.CSharp" />
  </ItemGroup>
</Project>

snippet source | anchor

Build Server

AppVeyor

To configure an adding to build using AppVeyor use the following appveyor.yml:

image: Visual Studio 2022
skip_commits:
  message: /docs|Merge pull request.*/
build_script:
- ps: >-
    dotnet build BasicFodyAddin --configuration Release

    dotnet test BasicFodyAddin --configuration Release --no-build --no-restore
test: off
artifacts:
- path: nugets\*.nupkg

snippet source | anchor

Usage

NuGet installation

Install the BasicFodyAddin.Fody NuGet package and update the Fody NuGet package:

PM> Install-Package Fody
PM> Install-Package BasicFodyAddin.Fody

The Install-Package Fody is required since NuGet always defaults to the oldest, and most buggy, version of any dependency.

Add to FodyWeavers.xml

<Weavers>
  <BasicFodyAddin />
</Weavers>

Deployment

Addins are deployed through NuGet packages. The package must:

  • Contain two weaver assemblies, one in each of the folders netclassicweaver and netstandardweaver, to support both .Net Classic and .Net Core.
  • Contain a runtime library, compiled for every supported framework, under the lib folder.
  • Contain an MsBuild .props file in the build folder that registers the weaver at compile time. The name of the file must be the package id with the .props extension. See Addin Discover for details.
  • Have an id with the same name of the weaver assembly should be the same and be suffixed with ".Fody". So in this case the BasicFodyAddin.Fody NuGet contains the weaver assembly BasicFodyAddin.Fody.dll and the reference assembly BasicFodyAddin.dll.
  • Have a single dependency on only the Fody NuGet package. Do not add any other NuGet dependencies as Fody does not support loading these files at compile time.

Note that the addins used via in-solution-weaving are handled differently.