Rougamo.Fody
4.0.0-priview-1722831925
See the version list below for details.
dotnet add package Rougamo.Fody --version 4.0.0-priview-1722831925
NuGet\Install-Package Rougamo.Fody -Version 4.0.0-priview-1722831925
<PackageReference Include="Rougamo.Fody" Version="4.0.0-priview-1722831925" />
paket add Rougamo.Fody --version 4.0.0-priview-1722831925
#r "nuget: Rougamo.Fody, 4.0.0-priview-1722831925"
// Install Rougamo.Fody as a Cake Addin #addin nuget:?package=Rougamo.Fody&version=4.0.0-priview-1722831925&prerelease // Install Rougamo.Fody as a Cake Tool #tool nuget:?package=Rougamo.Fody&version=4.0.0-priview-1722831925&prerelease
Rougamo - 肉夹馍
中文 | English
What is Rougamo
Rougamo is an AOP component that takes effect at compile time. There are some well-known AOP components like Castle, Autofac and AspectCore. Unlike these components, which are implemented through dynamic proxying and IoC at runtime, Rougamo modifies target methods' IL during compilation. If you are familiar with the AOP component PostSharp, then Rougamo is similar to PostSharp.
Weaving Methods
Quick Start with Attribute
// 1.Add Rougamo.Fody package via NuGet
// 2.Define a class that inherits MoAttribute
public class LoggingAttribute : MoAttribute
{
public override void OnEntry(MethodContext context)
{
// We can get method arguments, target class instance and MethodBase throughs MethodContext
Log.Info("before method execute");
}
public override void OnException(MethodContext context)
{
Log.Error("while a exception was throwing", context.Exception);
}
public override void OnSuccess(MethodContext context)
{
Log.Info("method executed successfully");
}
public override void OnExit(MethodContext context)
{
Log.Info("before method exits");
}
}
// 3.Apply Attribute to methods
public class Service
{
[Logging]
public static int Sync(Model model)
{
// ...
}
[Logging]
public async Task<Data> Async(int id)
{
// ...
}
}
Flag Matching
In the quick start section, I introduced how to weave codes into a specific method. However, in real business scenarios, there may be many methods in a project that require applying this Attribute. Then it will be tedious and invasive. So MoAttribute
is designed to be applied to methods, classes, assemblies, and modules, and you can select methods that match the property MoAttribute.Flags
. The following features can be filtered through Flags
:
- Method accessibility (public or non-public)
- Is static method or not
- Method category:normal method / property / getter / setter / constructor
// 1. Override Flags property, InstancePublic is the default value (public and instance method or property).
public class LoggingAttribute : MoAttribute
{
// All public methods, both instance and static
public override AccessFlags Flags => AccessFlags.Public | AccessFlags.Method;
// All instance properties(include getters and setters)
// public override AccessFlags Flags => AccessFlags.Instance | AccessFlags.Property;
// ...
}
// 2. Apply
// 2.1. Apply to class
[Logging]
public class Service
{
// ...
}
// 2.2. Apply to assembly
[assembly: Logging]
Attention, when Flags
does not specify any of Method/PropertyGetter/PropertySetter/Property/Constructor
, the default is the method and property (Method|Property
). The reason is Method/PropertyGetter/PropertySetter/Property/Constructor
cannot be specified before 2.0, and the default matches methods and properties. This logic will continue to be compatible in 2.0.
Weaving Through Interface
In the previous way, we mainly applied the Attribute to methods, classes, or assemblies, which requires us to modify code. This is an intrusive weaving method, which is not friendly for frequent AOP embedding scenarios. At this time, weaving through interfaces can greatly reduce or even avoid invasive weaving.
// 1. Interface weaving allows us to directly implement the IMo interface, and of course we can also inherit from MoAttribute.
public class LoggingMo : IMo
{
// 1.1. Override Flags property
public AccessFlags Flags => AccessFlags.All | AccessFlags.Method;
public void OnEntry(MethodContext context)
{
// We can get method arguments, target class instance and MethodBase throughs MethodContext
Log.Info("before method execute");
}
public void OnException(MethodContext context)
{
Log.Error("while a exception was throwing", context.Exception);
}
public void OnSuccess(MethodContext context)
{
Log.Info("method executed successfully");
}
public void OnExit(MethodContext context)
{
Log.Info("before method exits");
}
}
// 2. Implement IRougamo interface
public class TheService : ITheService, IRougamo<LoggingMo>
{
// ...
}
In the above example, we only needs to implement an empty interface IRougamo<LoggingMo>
, and the methods that match the Flags
defined by LoggingMo
will be selected. You may have a question "Isn't this still about modifying the code? Isn't it invasive?" Yes, it is still invasive. But if your project has a common layer for encapsulation, such as all Service
have to implement IService
or inherit from Service
which is define in common layer, then you can operate on the parent/base interface at this time:
// Common interface implements IRougamo
public interface IService : IRougamo<LoggingMo>
{
// ...
}
// Bussiness interface implements common interface
public interface ITheService : IService
{
// ...
}
public class TheService : ITheService
{
// ...
}
Pattern Matching
Flag Matching is easy to use, but the matching granularity is too large. In this section, we will introduce the pattern matching introduced in version 2.0, which has more precise matching rules. The syntax refers to aspectj, but it is not exactly the same. So it is recommended to understand it before using it.
public class PatternAttribute : MoAttribute
{
// Match all methods that method name starts with Get
public override string? Pattern => "method(* *.Get*(..))";
// Match all public static methods
public override string? Pattern => "method(public static * *(..))";
// Match all property getter
public override string? Pattern => "getter(* *)";
// Match all methods that return type is int[] or implements System.Collections.Generic.IEnumerable<int>
public override string? Pattern => "method(int[]||System.Collections.Generic.IEnumerable<int>+ *(..))";
}
Basic Concepts
Flag matching overrides the Flags
property, and pattern matching overrides the Pattern
property. Since both flag matching and pattern matching are used for filtering methods, they cannot be used at the same time. Pattern matching has higher priority, Pattern
is used when it is not null, otherwise Flags
is used.
Pattern matching support seven matching rules:
method([modifier] returnType declaringType.methodName([parameters]))
getter([modifier] propertyType declaringType.propertyName)
setter([modifier] propertyType declaringType.propertyName)
property([modifier] propertyType declaringType.propertyName)
execution([modifier] returnType declaringType.methodName([parameters]))
attr(position [index] attributeType)
regex(REGEX)
getter
, setter
and property
means property getter, property setter, property getter and setter. method
means all normal methods(except getter, setter, constructor). execution
means all method(execpt constructor). regex
is a special case and will be introduced in Regex Matching.
Except for the special format of regex
, the contents of the other five matching rules mainly include the following five (or below) parts:
[modifier]
,the access modifier can be omitted. When omitted, it means matching all. The access modifier includes the following seven:private
internal
protected
public
privateprotected
,即private protected
protectedinternal
,即protected internal
static
,it should be noted that omitting this access modifier means matching both static and instances. If you want to match only instances, you can use it with the logical modifier!
like!static
returnType
,the method returns the value type or property type. The format of the type is more complex. For details, see Type Matching FormatdeclaringType
,the type of the class in which the method/property is declared, For details, see ype Matching FormatmethodName/propertyName
,the name of the method/property. The name can use*
for fuzzy matching, such as*Async
,Get*
,Get*V2
, etc.*
matches 0 or more characters[parameters]
,method parameter list, Rougamo's parameter list matching is relatively simple, not as complicated as aspectj, and only supports arbitrary matching and full matching.- Use
..
to match any number of parameters of any type. - Specify the number and type of parameters for matching. Parameter types are matched according to Type Matching Format. Rougamo cannot perform fuzzy matching on the number of parameters like aspectj. For example,
int,..,double
is not supported.
- Use
position
Where the attributes apply to, uses*
to match anywhere.type
Applies to types.exec
Applies to methods, properties, properties getter, properties setter.para
Applies to parameters. Must be used together with[index]
.ret
Applies to return value.*
Match all positions.
[index]
Only needed whenposition
ispara
. Usually,index
is an integer, starts at0
, which means the first parameter, and uses*
to match any parameter.
Type Matching Format
Type Format
First of all, we make it clear that there are several ways to express a certain type: type name; namespace + type name; assembly + namespace + type name. Since Rogamo's application limit is assembly, so Rogamo chooses to use namespace + type name to express a type. The connection between namespace and type name adopts our common point connection method, namely namespace.type name
.
Nested Type
Rougamo uses /
as the nested class connector, which is inconsistent with the connector +
in usual programming habits. The main reason is that +
is a special character, which means subclass. For easier reading, another symbol is used. For example, a.b.c.D/E
means the namespace is a.b.c
and the outer class is D
and the nested class is E
. Of course nested classes support multiple levels of nesting.
Generic Type
What needs to be declared first is that generics are the same as static
. They match all when not declared, it means match both non-generic types and generic types. If you want to match only non-generic types or only generic types, Additional definitions are required, and the relevant definitions of generics are expressed using <>
.
- Only match non-generic types:
a.b.C<!>
, use logical not!
to indicate not matching any generics - Match any generic:
a.b.C<..>
, use two dots..
to match at least one generics of any type - Matches a specified number of generics of any type:
a.b.C<,,>
. The example means matching three generics of any type. Each added,
means matching an additional generic of any type.a.b.C< >
means matching a generic of any type - Open and closed generic types: those with undetermined generic types are called open generic types, such as
List<T>
, and those with determined generic types are called closed generic types, such asList<int> >
. When writing a matching expression, if you want to specify a specific generic type instead of arbitrary matching as described above, then for open undetermined generic types, you can use our commonly usedT1, T2, TA, TX
, etc. indicate that for closed and determined generic types, the determined type can be used directly.public class Generic<T1, T2> { public static void M(T1 t1, int x, T2 t2) { } } public class TestAttribute : MoAttribute { // When defining a matching expression, for an open generic type, it does not need to be consistent with the generic name defined by the type. For example, it is called T1, T2 above, and TA, TB is used in the pattern. public override string? Pattern => "method(* *<TA,TB>.*(TA,int,TB))"; }
- Generic methods: In addition to classes that can define generic parameters, methods can also define generic parameters. The method of using generic parameters of a method is the same as that of a type, so there will be no additional introduction.
public class Generic<T1, T2> { public static void M<T3, T4>(T1 t1, T2 t2, T3 t3, T4 t4) { } } public class TestAttribute : MoAttribute { public override string? Pattern => "method(* *<TA,TB>.*<TX, TY>(TA,TB,TX,TY))"; // You can also use non-generic matching, any matching and any type matching // public override string? Pattern => "method(* *<TA,TB>.*<..>(TA,TB,*,*))"; }
Fuzzy Matching
Two types of fuzzy matching were introduced earlier, one is name fuzzy matching *
, and the other is parameter/generic arbitrary matching ..
. These two symbols are still used for type fuzzy matching.
As introduced in Type Format, the type format consists of two parts namespace.type name
, so type fuzzy matching can be divided into: namespace matching, type name matching, generic matching, Subclass matching. Generic Type was just introduced in the previous section, Subclass Matching will be introduced in the next section. This section mainly describes the basic fuzzy matching rules of types.
- Type name matching: Fuzzy matching of type names is very simple. You can use
*
to match 0 or more characters, such as*Service
,Mock*
,Next*Repo*V2
, etc. It should be noted that*
cannot directly match any nested type. For example, it is not feasible to use*Service*
to matchAbcService+Xyz
. The nested type needs to be clearly stated, such as*Service/*
, matching the nested class whose name ends withService
. If it is a second-level nested class, it also needs to be clearly pointed out*Service/*/*
- Namespace Matching
- Absent matching: When the namespace is absent, it means matching any namespace. For example, the expression
Abc
can matchl.m.n.Abc
orx.y.z.Abc
- Exact match: Do not use any wildcards, write a complete namespace
- Fuzzy name: The namespace has one or more segments, and each segment is connected with
.
. Just like the type name matching, the characters in each segment can be matched by themselves using*
, such as*.x*z.ab*.vv
- Multi-segment fuzzy: Use
..
to match 0 or multiple namespaces. For example,*..xyz.Abc
can matcha.b.xyz.Abc
orlmn.xyz.Abc
,..
can also match Used multiple times, such as usinga..internal..t*..Ab
to matcha.internal.tk.Ab
anda.b.internal.c.t.u.Ab
- Absent matching: When the namespace is absent, it means matching any namespace. For example, the expression
Subclass Matching
We can use +
to match itself and its subclasses. For example, method(a.b.c.IService+ *(..))
matches methods which return a type that implement interface a.b.c.IService
. In addition, subclass matching can also be used with wildcards. For example, method(* *(*Provider+))
indicates that the matching method parameter has only one subclass and the parameter type is a subclass of a type ending with Provider
.
Special Syntax
Primitive Types
For primitive types, Rougamo supports type abbreviations to make pattern look more concise and clear. The currently supported abbreviated types are bool
, byte
, short
, int
, long
, sbyte
, ushort
, uint
, ulong
, char
, string
, float
, double
, decimal
, object
, void
.
Nullable
Just like our usual programming, we can use ?
to represent the Nullable
type, for example int?
is Nullable<int>
. It should be noted that the Nullable syntax of reference types should not be regarded as the Nullable
type. For example, string?
is actually string
. In Rougamo, write string
directly instead of string?
.
Tuple ValueTuple
When we write C# code, we can directly use brackets to represent ValueTuple
, which is also supported in Rougamo. For example, (int, string)
means ValueTuple<int, string>
or Tuple<int, string>
.
Task ValueTask
Now asynchronous programming is a basic programming method, so there will be many methods whose return value is Task
or ValueTask
. At the same time, if you want to be compatible with the two return values of Task
and ValueTask
, patterns are need to use the logical operator ||
to connect, which will greatly increase the complexity of the pattern. Rougamo adds the familiar async
keyword to match Task
and ValueTask
return values. For example, Task<int>
and ValueTask<int>
can be written as async int
. For non-generic Type Task
and ValueTask
are written as async null
. It should be noted that there is currently no way to match async void
alone, void
will match void
and async void
.
Simplify Type and Method Name
As mentioned earlier, the pattern of type consists of namespace.typeName
. If we want to match any type, the standard writing method should be *..*
, where *..
represents any namespace, followed by *
represents any type name. For any type, we can simplify it as *
. Similarly, the standard writing method of any method of any type should be *..*.*
, we can also simplify this as *
. So method(*..* *..*.*(..))
and method(* *(..))
express the same meaning.
Regex Matching
For each method, Rougamo will generate a string signature for it, and the regex match is the match for this string of signatures. Its signature format is modifiers returnType declaringType.methodName([parameters])
.
modifiers
contains two parts. One part is the accessibility modifier, namelyprivate/protected/internal/public/privateprotected/protectedinternal
, and the other part is whether the static methodstatic
is used. Thestatic
keyword is omitted for non-static methods. Separate the two parts with a space.returnType/declaringType
are all full names ofnamespace.typeName
. It should be noted that all types in the regular matching signature are full names, and similarint
cannot be used to matchSystem.Int32
- Generics, types and methods may contain generics. For closed generic types, just use the full name of the type. For open generic types, we abide by the following rules. Generics start from
T1
and increase backward. , that is,T1/T2/T3...
, the order of increase is in the order ofdeclaringType
first and thenmethod
. For details, please see the following examples. parameters
, parameters can be expanded by the full name of each parameter- Nested types, nested types are connected using
/
namespace a.b.c;
public class Xyz
{
// signature:public System.Int32 a.b.c.Xyz.M1(System.String)
public int M1(string s) => default;
// signature:public static System.Void a.b.c.Xyz.M2<T1>(T1)
public static void M2<T>(T value) { }
public class Lmn<TU, TV>
{
// signature:internal System.Threading.Tasks.Task<System.DateTime> a.b.c.Xyz/Lmn<T1,T2>.M3<T3,T4>(T1,T2,T3,T4)
internal Task<DateTime> M3<TO, TP>(TU u, TV v, TO o, TP p) => Task.FromResult(DateTime.Now);
// signature:private static System.Threading.Tasks.ValueTask a.b.c.Xyz/Lmn<T1,T2>.M4()
private static async ValueTask M4() => await Task.Yeild();
}
}
Regex Matching has the problem of being complicated to write, and it does not support subclass matching, so Regex Matching are generally not written. They are mainly used as a supplement to other matching rules to support some more complex name matching. Since Rougamo supports logical operations, it also gives more auxiliary space for Regex Matching. For example, we want to find the return value method of Task/ValueTask
whose method name does not end with Async
method(async null *(..) ) && regex(^\S+ (static )?\S+ \S+?(?<!Async)\()
.
Weaving Ability
Overwrite Method Arguments
In OnEntry
, you can modify the parameter values of the method by modifying the elements in MethodContext.Arguments
. In order to make it clear that you want to modify the method parameters, you also need to set MethodContext.RewriteArguments
to true
to confirm the rewriting. parameter.
public class DefaultValueAttribute : MoAttribute
{
public override void OnEntry(MethodContext context)
{
context.RewriteArguments = true;
var parameters = context.Method.GetParameters();
for (var i = 0; i < parameters.Length; i++)
{
if (parameters[i].ParameterType == typeof(string) && context.Arguments[i] == null)
{
context.Arguments[i] = string.Empty;
}
}
}
}
public class Test
{
[DefaultValue]
public string EmptyIfNull(string value) => value;
}
Modify Return Value
In the OnEntry
and OnSuccess
methods, you can modify the actual return value of the method by calling the ReplaceReturnValue
method of MethodContext
. Special attention should be paid to not modifying the return value directly through the ReturnValue
property. ReplaceReturnValue
contains some other logic may be updated later. Also note that the return value of Iterator/AsyncIterator
cannot be modified.
public class TestAttribute : MoAttribute
{
public override void OnEntry(MethodContext context)
{
context.ReplaceReturnValue(this, newReturnValue);
}
public override void OnSuccess(MethodContext context)
{
context.ReplaceReturnValue(this, newReturnValue);
}
}
Exception Handle
In the OnException
method, you can indicate that the exception has been handled and set the return value by calling the HandledException
method of MethodContext
. Similarly, do not directly set the exception to be handled and set the return value directly through the properties of ReturnValue
and ExceptionHandled
. .
public class TestAttribute : MoAttribute
{
public override void OnException(MethodContext context)
{
context.HandledException(this, newReturnValue);
}
}
Retry
The retry function can re-execute the current method when a specified exception is encountered or the return value is unexpected. To do this, you need set MethodContext.RetryCount
value. After OnException
and OnSuccess
, if the value of MethodContext.RetryCount
is greater than 0, the current method will be re-executed.
internal class RetryAttribute : MoAttribute
{
public override void OnEntry(MethodContext context)
{
// Initialize the number of retries
context.RetryCount = 3;
}
public override void OnException(MethodContext context)
{
// If an exception occurs, the number of retries will be reduced by one. When RetryCount is reduced to 0, it means that the upper limit of retries has been reached and no more attempts will be made.
context.RetryCount--;
}
public override void OnSuccess(MethodContext context)
{
if (context.ReturnValue != ABC)
{
// The result is an unexpected reduction in the number of retries. When RetryCount decreases to 0, it means that the upper limit of retries has been reached and no more attempts will be made.
context.RetryCount--;
}
else
{
// The result is as expected, directly set the number of retries to 0 and no longer retry.
context.RetryCount = 0;
}
}
}
// The Test method will retry 3 times
[Retry]
public void Test()
{
throw new Exception();
}
For exception handling retry scenarios, I created an independent project Rougamo.Retry. If you only want to retry a certain exception, you can use Rougamo directly. .Retry
Please note the following points when using the retry function:
- When handling exceptions through
MethodContext.HandledException()
or modifying the return value throughMethodContext.ReplaceReturnValue()
,MethodContext.RetryCount
will be set to 0 directly, because manually handling exceptions and modifying the return value means that you have decided the final result of the method, so there is no need to retry OnEntry
andOnExit
ofMoAttribute
will only be executed once and will not be executed multiple times due to retries.- Try not to use the retry in
ExMoAttribute
unless you really know the actual processing logic. Think about the following code,ExMoAttribute
cannot re-execute the entire external method after reporting an error insideTask
public Task Test() { DoSomething(); return Task.Run(() => DoOtherThings()); }
Partially Weaving
Rougamo will weave all functions into the code by default, but generally not all functions are used. And with the iteration of the version, more and more functions are supported and more and more code is woven into it, which will increase the final assembly size. By specifying partial weaving, you can weave in only the functionality you need. Partial weaving is set up by overriding MoAttribute.Features
.
Value | functions |
---|---|
All | Contains all functions, default value |
OnEntry | Only OnEntry, parameter values cannot be modified, and return values cannot be modified |
OnException | OnException only, exceptions cannot be handled |
OnSuccess | OnSuccess only, the return value cannot be modified |
OnExit | OnExit |
RewriteArgs | Contains OnEntry, and parameter values can be modified in OnEntry |
EntryReplace | Contains OnEntry, and the return value can be modified in OnEntry |
ExceptionHandle | Contains OnException, and exceptions can be handled in OnEntry |
SuccessReplace | Contains OnSuccess, and the return value can be modified in OnSuccess |
ExceptionRetry | Contains OnException, and can retry in OnException |
SuccessRetry | Contains OnSuccess, and can retry in OnSuccess |
Retry | Contains OnException and OnSuccess, and can retry in OnException and OnSuccess |
Observe | Contains OnEntry, OnException, OnSuccess and OnExit |
NonRewriteArgs | Contains all functions except modifying parameters |
NonRetry | Contains all functionality except retry |
FreshArgs | Update the newest parameters value into MethodContext.Arguments before calls OnException, OnSuccess and OnExit |
Ignore Weaving
Rougamo has the ability to weave in batches. Although the current version provides richer matching rules, if the matching pattern is too complicated in order to exclude one or several methods, it will not be worth the gain. When excluding a specific method or methods, you can directly use IgnoreMoAttribute
. When applied to a method, the method will ignore weaving. When applied to a class, all methods of the class will ignore weaving. When applied to the assembly, all methods in the assembly ignore weaving.
In addition, you can specify the ignored type when ignoring weaving. For example, if a method applies multiple MoAttribute
, you can ignore one individually. If not specified, it means ignoring all.
// Current assembly ignores all weaving
[assembly: IgnoreMo]
// Current assembly ignores weaving of TheMoAttribute
[assembly: IgnoreMo(MoTypes = new[] { typeof(TheMoAttribute))]
// Current class ignores all weaving
[IgnoreMo]
class Class1
{
// ...
}
// The current class ignores weaving of TheMoAttribute
[IgnoreMo(MoTypes = new[] { typeof(TheMoAttribute))]
class Class2
{
// ...
}
Proxy Weaving
If you have used some third-party components to Attribute mark some methods, and now you want to perform aop operations on these marked methods, but do not want to manually add Rougamo's Attribute marks one by one. You can use a proxy in one step to complete aop weaving. If your project now has many obsolete methods marked with ObsoleteAttribute
. You want to output the call stack log when the expired methods are called to check which entries are currently using these expired methods. This can also be done in this way.
public class ObsoleteProxyMoAttribute : MoAttribute
{
public override void OnEntry(MethodContext context)
{
Log.Warning("The obsolete method was called:" + Environment.StackTrace);
}
}
// Proxy at the assembly level, all methods marked with ObsoleteAttribute will have ObsoleteProxyMoAttribute applied
[assembly: MoProxy(typeof(ObsoleteAttribute), typeof(ObsoleteProxyMoAttribute))]
ExMoAttribute
When we implement MoAttribute
, in the OnEntry/OnSuccess/OnException/OnExit
method, when we obtain the return value type through MethodContext.ReturnValue
, for the Task/ValueTask
method that returns a value, there are different performances when using and not using async/await
syntax. For example, when using async syntax for Task<int> M()
, the type of MethodContext.ReturnValue
is int
, but when not using async syntax, the type is Task <int>
.
This is not a bug, but an initial design. Just as when we use async syntax, we will return like return 123
for Task<int>
, and when we do not use async syntax, we will return like Task.FromResult(123)
. Furthermore, the performance after compilation is different between using and not using async
syntax. Using async
syntax will generate a state machine type at compile time, which will not be discussed in details here. So this design is not only based on the habits when writing code, but also based on the actual compiled code structure.
Although the design is like this, many times we do not use async/await
syntax for some simple method overloading, but we also hope that Rougamo can handle it like methods with async/await
syntax. Then it is recommended to use ExMoAttribute
at this time. Regardless of whether async
syntax is used, it can be processed just like using async
syntax.
Note that ExMoAttribute
and MoAttribute
have the following differences:
ExMoAttribute
overridable method namedExOnEntry/ExOnException/ExOnSuccess/ExOnExit
- The return value of
ExMoAttribute
is obtained throughMethodContext.ExReturnValue
, and the value obtained throughMethodContext.ReturnValue
will be the value ofTask/ValueTask
- Whether the return value of
ExMoAttribute
is replaced/set, it is obtained throughMethodContext.ExReturnValueReplaced
, and the value obtained throughMethodContext.ReturnValueReplaced
is generally true (because it is replaced with the Task returned by ContinueWith) - The return value type of
ExMoAttribute
is obtained throughMethodContext.ExReturnType
. What is the difference betweenReturnType/RealReturnType/ExReturnType
can be seen in the description of the respective attribute document or the type document description ofExMoAttribute
[Fact]
public async Task Test()
{
Assert.Equal(1, Sync());
Assert.Equal(-1, SyncFailed1());
Assert.Throws<InvalidOperationException>(() => SyncFailed3());
Assert.Equal(1, await NonAsync());
Assert.Equal(-1, await NonAsyncFailed1());
Assert.Equal(-1, await NonAsyncFailed2());
await Assert.ThrowsAsync<InvalidOperationException>(() => NonAsyncFailed3());
await Assert.ThrowsAsync<InvalidOperationException>(() => NonAsyncFailed4());
Assert.Equal(1, await Async());
Assert.Equal(-1, await AsyncFailed1());
Assert.Equal(-1, await AsyncFailed2());
await Assert.ThrowsAsync<InvalidOperationException>(() => AsyncFailed3());
await Assert.ThrowsAsync<InvalidOperationException>(() => AsyncFailed4());
}
[FixedInt]
static int Sync() => int.MaxValue;
[FixedInt]
static int SyncFailed1() => throw new NotImplementedException();
[FixedInt]
static int SyncFailed3() => throw new InvalidOperationException();
[FixedInt]
static Task<int> NonAsync() => Task.FromResult(int.MinValue);
[FixedInt]
static Task<int> NonAsyncFailed1() => throw new NotImplementedException();
[FixedInt]
static Task<int> NonAsyncFailed2() => Task.Run(int () => throw new NotImplementedException());
[FixedInt]
static Task<int> NonAsyncFailed3() => throw new InvalidOperationException();
[FixedInt]
static Task<int> NonAsyncFailed4() => Task.Run(int () => throw new InvalidOperationException());
[FixedInt]
static async Task<int> Async()
{
await Task.Yield();
return int.MaxValue / 2;
}
[FixedInt]
static async Task<int> AsyncFailed1() => throw new NotImplementedException();
[FixedInt]
static async Task<int> AsyncFailed2()
{
await Task.Yield();
throw new NotImplementedException();
}
[FixedInt]
static async Task<int> AsyncFailed3() => throw new InvalidOperationException();
[FixedInt]
static async Task<int> AsyncFailed4()
{
await Task.Yield();
throw new InvalidOperationException();
}
class FixedIntAttribute : ExMoAttribute
{
protected override void ExOnException(MethodContext context)
{
if (context.Exception is NotImplementedException)
{
context.HandledException(this, -1);
}
}
protected override void ExOnSuccess(MethodContext context)
{
context.ReplaceReturnValue(this, 1);
}
}
Mutex Weaving
Single Type Mutex
Since we have two weaving methods, Attribute tag and interface implementation, it may be applied at the same time, and if the content of the two weaving is the same, there will be repeated weaving. In order to avoid this as much as possible In this case, when the interface is defined, mutually exclusive types can be defined, that is, only one can take effect at the same time, and which one takes effect is determined according to Priority.
public class Mo1Attribute : MoAttribute
{
// ...
}
public class Mo2Attribute : MoAttribute
{
// ...
}
public class Mo3Attribute : MoAttribute
{
// ...
}
public class Test : IRougamo<Mo1Attribute, Mo2Attribute>
{
[Mo2]
public void M1()
{
// Mo2Attribute is applied to the method, the priority is higher than the Mo1Attribute implemented by the interface, and the Mo2Attribute will be applied
}
[Mo3]
public void M2()
{
// Mo1Attribute and Mo3Attribute are not mutually exclusive, both will be applied
}
}
Multiple Type Mutex
IRougamo<,>
can only be mutually exclusive with one type, IRepulsionsRougamo<,>
can be mutually exclusive with multiple types.
public class Mo1Attribute : MoAttribute
{
}
public class Mo2Attribute : MoAttribute
{
}
public class Mo3Attribute : MoAttribute
{
}
public class Mo4Attribute : MoAttribute
{
}
public class Mo5Attribute : MoAttribute
{
}
public class TestRepulsion : MoRepulsion
{
public override Type[] Repulsions => new[] { typeof(Mo2Attribute), typeof(Mo3Attribute) };
}
[assembly: Mo2]
[assembly: Mo5]
public class Class2 : IRepulsionsRougamo<Mo1Attribute, TestRepulsion>
{
[Mo3]
public void M1()
{
// Mo1 is mutually exclusive with Mo2 and Mo3, but since Mo3 has a higher priority than Mo1, when Mo1 does not take effect, all mutually exclusive types will take effect.
// So eventually Mo2Attribute, Mo3Attribute, Mo5Attribute will be applied.
Console.WriteLine("m1");
}
[Mo4]
public void M2()
{
// Mo1 is mutually exclusive with Mo2 and Mo3, but since Mo1 has a higher priority than Mo2, Mo2 will not take effect
// Eventually Mo1Attribute, Mo4Attribute, Mo5Attribute will be applied
Console.WriteLine("m2");
}
}
Through the above example, you may notice that this multi-type mutual exclusion is not mutual exclusion between multiple types, but the mutual exclusion of the first generic type and the type defined by the second generic type, and the second generic type is mutually exclusive. They are not mutually exclusive. Just like the above example, when Mo1Attribute
does not take effect, the mutually exclusive Mo2Attribute
and Mo3Attribute
will take effect. It needs to be understood here that the reason for defining mutual exclusion is the possible repeated application of Attribute and empty interface implementation, not to exclude all weaving repetitions. At the same time, it is not recommended to use multiple mutual exclusion definitions, which is prone to logical confusion. It is recommended to carefully consider a set of unified rules before application weaving, rather than random definitions, and then try to use multiple mutual exclusions to solve problems.
Performance Optimization
Structs
To accomplish AOP operations, it is inevitable to create objects of your custom implementation of IMo
or types derived from MoAttribute
every time a method is called. These objects, created during method calls, will be garbage collected after the method invocation, leading to an unavoidable increase in GC burden. Even if we handwrite AOP code, the same scenario arises. However, we can reduce the GC pressure in various ways, and one of them is using structs instead of classes.
One commonly used approach in Rougamo is to mark classes, methods, or assemblies with an Attribute. However, we cannot define attributes for structs directly. Therefore, we need to define a struct that implements the IMo
interface and then specify this struct through the RougamoAttribute
:
struct ValueMo : IMo
{
// Implement the interface, define AOP operations
}
[Rougamo(typeof(ValueMo))]
class Cls
{
// If the project uses C# 11 and above syntax, you can directly use the following generic Attribute
[Rougamo<ValueMo>]
public void M() { }
}
Omit
In the Partially Weaving section, we discussed reducing woven code size by selecting woven features using Features. Here, we'll introduce another feature, IMo.MethodContextOmits
, used to improve performance.
The MethodContext
contains contextual information about the method and serves as a messenger passed between multiple IMo
objects. However, there are times when we don't need all the information in these messages, and obtaining/storing this information has a significant cost. In such cases, we can use the MethodContextOmits
property to specify the data that is not required.
Currently, MethodContextOmits
can specify a combination of the following values:
None
, the default value, indicating that all data is needed.Mos
, indicating thatMethodContext.Mos
property is not needed. This property contains all theIMo
objects woven into the current method. If you are using structs, storing this property will involve boxing operations and will also increase anIReadOnlyList<IMo>
object. Note: When usingExMoAttribute
, do not specify this value.Arguments
, indicating thatMethodContext.Arguments
property is not needed. This property stores all input parameter values of the called method. If the parameters include value types, boxing operations will be performed, and an additionalobject[]
object will be created. Note: If the Features property includes eitherArgs
orRewriteArgs
orFreshArgs
, specifying this value will be ineffective, and the arguments will still be stored.
Other
Setting MoAttribute Built-in Properties When Applying
MoAttribute
comes with many built-in properties, such as Pattern
, Order
, etc. These properties can be directly overridden when inheriting from MoAttribute
. However, there are times when we want to set these attributes when applying the attribute. For example, when applying multiple attributes to a method, we may want to set their respective Order
for the method so that they execute in the desired order. Since the built-in properties of MoAttribute do not have a publicly accessible setter, we cannot set them directly. In such cases, we can use the new
keyword to re-expose the setter.
public class TestAttribute : MoAttribute
{
// Overriding the Order property of MoAttribute with the new keyword
// Making the setter public
// The virtual keyword is optional; if you are sure that no type will inherit from TestAttribute and override the Order property, you can omit virtual
// Here, a default value is set for Order, but it can also be omitted
public new virtual double Order { get; set; } = 10;
}
As for why we don't directly expose the setter for MoAttribute properties, it mainly considers friends using the framework as middleware. Middleware may set default values for some MoAttribute properties and may not want others to modify these properties when using the middleware, nor do they want to expose these property values for users to see. Unfortunately, once a parent class defines a property with a public setter, it cannot be hidden by a subclass. Therefore, the chosen approach is to default to hiding, allowing the subclass to decide whether to make it public or not.
Priority
IgnoreMoAttribute
- Method
MoAttribute
- Method
MoProxyAttribute
- Type
MoAttribute
- Type
MoProxyAttribute
- Type
IRougamo<>
,IRougamo<,>
,IRepulsionsRougamo<,>
- Assembly & Module
MoAttribute
Configuration
After referencing Rougamo, a FodyWeavers.xml
file will be generated in the project root directory during compilation, with the following format:
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<Rougamo />
</Weavers>
The way to add configuration is to add attributes and values directly to the Rougamo
node, such as <Rougamo enabled="false" />
.
The following table shows all configuration items:
Name( |
default value | description |
---|---|---|
enabled | true | Whether to enable rougamo |
composite-accessibility | false | Whether to use class+method comprehensive accessibility for matching. By default, only method accessibility is used for matching. For example, if the accessibility of a class is internal and the accessibility of a method is public, then by default the accessibility of the method is considered public. After setting this configuration to true, the accessibility of the method is considered internal |
moarray-threshold | 4 | When the MoAttribute actually effective on the method reaches this value, it will be saved in an array. This configuration is used to optimize weaving code. In most cases, there is only one MoAttribute on a method. In this case, using an array to save it will generate more IL code when calling its method |
iterator-returns( |
false | Whether to save the return value of the iterator to MethodContext.ReturnValue . Use this function with caution. If the iterator generates a large amount of data, enabling this function will occupy the same amount of memory |
reverse-call-nonentry( |
true | When there are multiple MoAttribute on a method, whether to execute OnException/OnSuccess/OnExit in the reverse order of OnEntry . By default, it is executed in reverse order |
except-type-patterns | Regular expression of the full name of the type. Types matching this expression will be globally excluded. Multiple regular expressions should be separated by commas or semicolons |
FAQ
Q: First time contacting Rougamo, but it doesn’t work.
A: When contacting for the first time, please note that the project must directly depend on Rougamo.Fody
to take effect. Indirect dependencies are invalid. For example, project A depends on B, and B references Rougamo.Fody
. At this time, the weaving in project A cannot take effect, and project A needs to directly reference Rougamo.Fody
.
Q: After using Rougamo to develop libraries, do other projects that reference my libraries still have to reference Rougamo.Fody
?
A: After the library references Rougamo.Fody
, you can manually modify the reference node in the project file and add PrivateAssets="contentfiles;analyzers"
. The modification is as follows (remember to modify the version number when copying). When others use the library, you don't need to depend on Rougamo.Fody
directly, but you need to directly depend on the your libraries.
<PackageReference Include="Rougamo.Fody" Version="2.0.0" PrivateAssets="contentfiles;analyzers" />
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. |
.NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
.NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
.NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
MonoAndroid | monoandroid was computed. |
MonoMac | monomac was computed. |
MonoTouch | monotouch was computed. |
Tizen | tizen40 was computed. tizen60 was computed. |
Xamarin.iOS | xamarinios was computed. |
Xamarin.Mac | xamarinmac was computed. |
Xamarin.TVOS | xamarintvos was computed. |
Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- Fody (>= 6.8.0)
- System.Threading.Tasks.Extensions (>= 4.5.4)
NuGet packages (42)
Showing the top 5 NuGet packages that depend on Rougamo.Fody:
Package | Downloads |
---|---|
HandeSoft.Core
Package Description |
|
HandeSoft.Web.Core
Package Description |
|
BotSharp.Abstraction
Package Description |
|
Ray.Infrastructure
This client library is a infrastructure that including extensions and helpers etc. |
|
HZY.Framework.Core
HZY Framework 核心 1、ScheduledAttribute 定时任务特性标记 2、IServerMetricMonitoringService 服务器指标监控 CPU、内存、硬盘、运行时长 3、HZY.Framework.DynamicApiController 动态Api控制器 4、HZY.Framework.DependencyInjection 依赖注入 |
GitHub repositories (5)
Showing the top 5 popular GitHub repositories that depend on Rougamo.Fody:
Repository | Stars |
---|---|
RayWangQvQ/BiliBiliToolPro
B 站(bilibili)自动任务工具,支持docker、青龙、k8s等多种部署方式。敏感肌也能用。
|
|
dotnetcore/FreeSql
🦄 .NET aot orm, C# orm, VB.NET orm, Mysql orm, Postgresql orm, SqlServer orm, Oracle orm, Sqlite orm, Firebird orm, 达梦 orm, 人大金仓 orm, 神通 orm, 翰高 orm, 南大通用 orm, 虚谷 orm, 国产 orm, Clickhouse orm, DuckDB orm, TDengine orm, QuestDB orm, MsAccess orm.
|
|
SciSharp/BotSharp
The AI Agent Framework in .NET
|
|
2881099/FreeSql.AdminLTE
这是一个 .NETCore MVC 中间件,基于 AdminLTE 前端框架动态产生 FreeSql 实体的增删查改界面。
|
|
ThingsGateway/ThingsGateway
ThingsGateway is a cross platform high-performance edge acquisition gateway based on Net8, providing underlying PLC communication libraries, communication debugging software, and more.
|
Version | Downloads | Last updated | |
---|---|---|---|
4.0.4 | 1,840 | 9/29/2024 | |
4.0.4-preview-1727349912 | 91 | 9/26/2024 | |
4.0.3 | 1,209 | 9/16/2024 | |
4.0.3-preview-1726120802 | 95 | 9/12/2024 | |
4.0.3-preview-1725957423 | 103 | 9/10/2024 | |
4.0.2 | 496 | 9/9/2024 | |
4.0.2-preview-1725956948 | 92 | 9/10/2024 | |
4.0.2-preview-1725875652 | 91 | 9/9/2024 | |
4.0.2-preview-1725466232 | 93 | 9/4/2024 | |
4.0.1 | 2,608 | 9/2/2024 | |
4.0.1-preview-1725141430 | 87 | 8/31/2024 | |
4.0.0 | 5,880 | 8/10/2024 | |
4.0.0-priview-1723306347 | 108 | 8/10/2024 | |
4.0.0-priview-1722831925 | 85 | 8/5/2024 | |
3.1.0 | 1,488 | 7/16/2024 | |
3.0.2 | 433 | 7/8/2024 | |
3.0.2-priview-1720363148 | 102 | 7/7/2024 | |
3.0.2-priview-1720251661 | 100 | 7/6/2024 | |
3.0.1 | 184 | 7/4/2024 | |
3.0.1-priview-1720089186 | 105 | 7/4/2024 | |
3.0.1-priview-1720085112 | 88 | 7/4/2024 | |
3.0.0 | 3,293 | 5/4/2024 | |
3.0.0-priview-1714754497 | 77 | 5/3/2024 | |
3.0.0-priview-1714407561 | 129 | 4/29/2024 | |
2.3.1 | 7,286 | 4/23/2024 | |
2.3.1-priview-1713854631 | 113 | 4/23/2024 | |
2.3.1-priview-1713791514 | 101 | 4/22/2024 | |
2.3.0 | 3,756 | 3/10/2024 | |
2.3.0-priview-1709894403 | 112 | 3/8/2024 | |
2.2.0 | 2,908 | 1/20/2024 | |
2.2.0-priview-1705656978 | 108 | 1/19/2024 | |
2.2.0-priview-1705571301 | 102 | 1/18/2024 | |
2.2.0-priview-1705566213 | 108 | 1/18/2024 | |
2.2.0-priview-1702899195 | 181 | 12/18/2023 | |
2.1.1 | 4,588 | 12/14/2023 | |
2.1.1-priview-1702545048 | 141 | 12/14/2023 | |
2.1.1-priview-1702542781 | 139 | 12/14/2023 | |
2.0.1 | 1,243 | 11/16/2023 | |
2.0.0 | 2,979 | 10/8/2023 | |
2.0.0-priview-1696783135 | 134 | 10/8/2023 | |
2.0.0-priview-1696592398 | 129 | 10/6/2023 | |
2.0.0-priview-1695658688 | 155 | 9/25/2023 | |
2.0.0-priview-1695465141 | 148 | 9/23/2023 | |
2.0.0-priview-1680984436 | 220 | 4/8/2023 | |
2.0.0-priview-1680981587 | 180 | 4/8/2023 | |
1.4.1 | 11,329 | 3/12/2023 | |
1.4.1-priview-1678603084 | 187 | 3/12/2023 | |
1.4.1-priview-1678557697 | 188 | 3/11/2023 | |
1.4.1-priview-1678557463 | 186 | 3/11/2023 | |
1.4.0 | 2,641 | 3/1/2023 | |
1.4.0-beta | 358 | 2/27/2023 | |
1.4.0-alpha | 233 | 2/25/2023 | |
1.3.4 | 55,672 | 2/17/2023 | |
1.3.3 | 975 | 1/17/2023 | |
1.3.2 | 18,193 | 12/20/2022 | |
1.3.1 | 340 | 12/20/2022 | |
1.3.1-beta | 177 | 12/14/2022 | |
1.3.0 | 1,280 | 12/8/2022 | |
1.2.3 | 346 | 1/17/2023 | |
1.2.2 | 319 | 12/20/2022 | |
1.2.2-beta | 167 | 12/14/2022 | |
1.2.1 | 694 | 11/29/2022 | |
1.2.1-beta | 162 | 11/29/2022 | |
1.2.0 | 2,059 | 9/14/2022 | |
1.2.0-beta | 182 | 9/12/2022 | |
1.2.0-alpha2 | 172 | 9/12/2022 | |
1.2.0-alpha1 | 179 | 8/31/2022 | |
1.2.0-alpha | 172 | 8/30/2022 | |
1.1.4 | 372 | 11/29/2022 | |
1.1.4-alpha | 186 | 12/25/2022 | |
1.1.3 | 530 | 9/11/2022 | |
1.1.2 | 1,646 | 8/22/2022 | |
1.1.2-beta | 179 | 8/22/2022 | |
1.1.1 | 2,768 | 8/8/2022 | |
1.1.1-beta | 189 | 8/1/2022 | |
1.1.0 | 629 | 7/28/2022 | |
1.1.0-beta | 204 | 7/15/2022 | |
1.1.0-alpha4 | 187 | 6/24/2022 | |
1.1.0-alpha3 | 174 | 6/24/2022 | |
1.1.0-alpha2 | 173 | 6/23/2022 | |
1.1.0-alpha1 | 175 | 6/22/2022 | |
1.1.0-alpha | 189 | 5/22/2022 | |
1.0.3 | 710 | 5/6/2022 | |
1.0.3-beta | 192 | 4/26/2022 | |
1.0.2 | 694 | 12/23/2021 | |
1.0.1 | 7,732 | 11/23/2021 | |
1.0.1-beta | 4,918 | 11/23/2021 |
- 新增异步切面功能
- `IMo`新增系列方法`OnEntryAsync`, `OnExceptionAsync`, `OnSuccessAsync`, `OnExitAsync`,当将肉夹馍应用到异步方法上时将调用对应的`OnXxxAsync`异步切面方法
- 新增`AsyncMoAttribute`,原有的`MoAttribute`提供了默认的`OnXxxAsync`实现并且不支持重写,而`AsyncMoAttribute`相反的提供了`OnXxx`的默认实现,同时可以重写`OnXxxAsync`系列方法
- 新增`RawMoAttribute`,同时支持重写同步切面的`OnXxx`和异步切面的`OnXxxAsync`,但需要自己注意同步切面方法与异步切面方法的调用关系,不可产生递归调用
- 新增`RawMo`, `Mo`, `AsyncMo`,对应`RawMoAttribute`, `MoAttribute`, `AsyncMoAttribute`,区别在于前者仅实现`IMo`接口,没有继承`Attribute`
- `IMo`新增`ForceSync`属性,可用于指定在异步方法上强制执行同步切面方法,用于性能优化
- [[#68](https://github.com/inversionhourglass/Rougamo/issues/68)] 在async void上应用Rougamo时,编译时在输出告警信息到MSBuild,目前还无法输出告警信息到错误列表(Error List)
- 异步方法在编织时将忽略`moarray-threshold`配置项,无论存在多少个`IMo`对象,都不会使用数组进行存储,因为异步切面的代码并不会因为使用数组而得到优化,反而会产生更多的操作指令。该配置对于同步方法依旧生效
- 优化ref struct参数/返回值的报错提示,一次编译会检查所有方法,并产生多个错误信息到错误列表(Error List)
- 肉夹馍产生的错误列表(Error List)中的错误信息都可以直接双击定位到对应产生该错误的方法,方便排查和反馈问题
- 删除`MethodContext`中的`IsAsync`, `IsIterator`, `MosNonEntryFIFO`, `Data`属性,将`RealReturnType`标记为过时并隐藏,同时新增`TaskReturnType`属性,该属性与`RealReturnType`具有类似功能
- 增加xcf文件,为`FodyWeavers.xml`中的`Rougamo`节点增加智能提示功能
---