This page uses the a sample addin called BasicFodyAddin to describe building an addin.
- 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 allinclude="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>
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.
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.
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>
This project must target netstandard2.0
.
It outputs a file named BasicFodyAddin.Fody
. The '.Fody' suffix is necessary to be picked up by Fody at compile time.
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;
}
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'.");
}
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";
}
}
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";
}
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;
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>();
}
BaseModuleWeaver
has other members for extensibility:
https://github.com/Fody/Fody/blob/master/FodyHelpers/BaseModuleWeaver.cs
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 ofWeavingException.Message
property. If theWeavingException
has a propertySequencePoint
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.
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"/>
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>
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>
A target assembly to process and then validate with unit tests.
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());
}
}
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>
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
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.
<Weavers>
<BasicFodyAddin />
</Weavers>
Addins are deployed through NuGet packages. The package must:
- Contain two weaver assemblies, one in each of the folders
netclassicweaver
andnetstandardweaver
, 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 assemblyBasicFodyAddin.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.