Matryoshki by Georgy Krasin
Nuget / site data
Details
Info
Name: Matryoshki
Metaprogramming framework based on C# source generators
Author: Georgy Krasin
NuGet: https://www.nuget.org/packages/Matryoshki/
You can find more details at https://github.com/krasin-ga/matryoshki/
Original Readme
Matryoshki
"Matryoshki" (Матрёшки, Matryoshkas) is a metaprogramming framework based on C# source generators.Key Features
- Define type-agnostic templates and create decorators based on them:
Decorate<IFoo>.With<LoggingAdornment>().Name<FooWithLogging>()
- Extract interfaces and automatically generate adapters from classes:
From<Bar>.ExtractInterface<IBar>()
.
Getting Started
Installation
The first step is to add package to the target project:
dotnet add package Matryoshki
Once the package is installed, you can proceed with creating adornments.
Adornments
Adornments act as blueprints for creating type-agnostic decorators. They consist of a method template and can contain arbitrary members. Rather than being instantiated as objects, the code of adornment classes is directly injected into the decorator classes.
To create an adornment you need to create a class that implements IAdornment
. As a simple example, you can create an adornment that outputs the name of the decorated member to the console:
public class HelloAdornment : IAdornment
{
public TResult MethodTemplate<TResult>(Call<TResult> call)
{
Console.WriteLine($"Hello, {call.MemberName}!");
return call.Forward();
}
}
When creating a decorated method, call.Forward()
will be replaced with a call to the implementation. And TResult
will have the type of the actual return value. For void
methods, a special type Nothing
will be used.
A more complex example
An adornment for logging can serve as a slightly closer example to real-world usage:
public class LoggingAdornment : IAdornment
{
private readonly ILogger<ExceptionLoggingAdornment> _logger;
public LoggingAdornment(ILogger<ExceptionLoggingAdornment> logger)
{
_logger = logger;
}
public TResult MethodTemplate<TResult>(Call<TResult> call)
{
try
{
if(_logger.IsEnabled(LogLevel.Debug))
_logger.LogDebug("Executing {Type}.{Member}", GetType().Name, call.MemberName);
var result = call.Forward();
if (_logger.IsEnabled(LogLevel.Debug))
_logger.LogDebug("Successfully executed {Type}.{Member}: {Result}", GetType().Name, call.MemberName, result);
return result;
}
catch (Exception exception)
{
_logger.LogError(
exception,
"Error executing {Type}.{Member}({Arguments})",
GetType().Name,
call.MemberName,
string.Join(",", call.GetArgumentsOfType<object>()));
throw;
}
}
}
Asynchronous method templates
Asynchronous templates can be defined by implementing the AsyncMethodTemplate
method, which will be used to decorate methods that return Task
or ValueTask
.
Note that asynchronous templates are optional, and async methods will still be decorated because an AsyncMethodTemplate
will be automatically created from the MethodTemplate
by awaiting the Forward*
method invocations.
More tips for writing adornments can be found here: tips
Decoration
Once we have an adornment, we can create our first matryoshkas.
Suppose we have two interfaces that we would like to apply our HelloAdornment to.
interface IFoo
{
object Foo(object foo) => foo;
}
record Foo : IFoo;
interface IBar
{
Task BarAsync() => Task.Delay(0);
}
record Bar : IFoo;
To create matryoshkas, you just need to write their specification in any appropriate location:
Matryoshka<IFoo>
.With<HelloAdornment>()
.Name<FooMatryoshka>();
Decorate<IBar> // you can use Decorate<> alias if you prefer
.With<HelloAdornment>()
.Name<BarMatryoshka>();
Done! Now we can test the generated classes:
var fooMatryoshka = new FooMatryoshka(new Foo());
var barMatryoshka = new BarMatryoshka(new Bar());
fooMatryoshka.Foo(); // "Hello, Foo!" will be written to console
barMatryoshka.Bar(); // "Hello, Bar!" will be written to console
In a production environment, you will likely prefer to use DI containers that support decoration (Grace, Autofac, etc.) or libraries like Scrutor. Here's an example of using matryoshkas together with Scrutor:
using Scrutor;
using Matryoshki.Abstractions;
public static class MatryoshkaScrutorExtensions
{
public static IServiceCollection DecorateWithMatryoshka(
this IServiceCollection services,
Expression<Func<MatryoshkaType>> expression)
{
var matryoshkaType = expression.Compile()();
services.Decorate(matryoshkaType.Target, matryoshkaType.Type);
return services;
}
public static IServiceCollection DecorateWithNestedMatryoshkas(
this IServiceCollection services,
Expression<Func<MatryoshkaTypes>> expression)
{
var matryoshkaTypes = expression.Compile()();
foreach (var type in matryoshkaTypes)
services.Decorate(matryoshkaTypes.Target, type);
return services;
}
}
internal static class Example
{
internal static IServiceCollection DecorateBar(
this IServiceCollection services)
{
return services.DecorateWithMatryoshka(
() => Matryoshka<IBar>.With<HelloAdornment>());
}
}
Chains of decorations with INesting<T1, ..., TN>
Reusable decoration chains can be described by creating a type that implements INesting<T1, ..., TN>
:
public record ObservabilityNesting : INesting<MetricsAdornment, LoggingAdornment, TracingAdornment>;
You can generate the classes using it as follows:
static IServiceCollection DecorateFoo(IServiceCollection services)
{
//assuming that you are using MatryoshkaScrutorExtensions
return services.DecorateWithNestedMatryoshkas(
() => Matryoshka<IBar>.WithNesting<ObservabilityNesting>());
}
It is not possible to assign names to the classes when using INesting
. The generated types will be located in the MatryoshkiGenerated.{NestingName}
namespace and have names in the format TargetTypeName*With*AdornmentName.
Limitations
- Do not use a variable named
value
, as this can conflict with a property setter. - The
call
parameter should not be passed to other methods. default
cannot be used without specifying a type argument.- To apply decorations, the members must be abstract or virtual. To surpass this limitation you can generate an interface with expression
From<TClass>.ExtractInterface<TInterface>()
and then decrorateTInterface
. - The decoration expression must be computable at compile time and written with a single statement
- Pattern matching will not always work
License
This project is licensed under the MIT license.
Quick links
Tips
About
Adding decorators to an implementation of interface
How to use
Example ( source csproj, source files )
- CSharp Project
- Program.cs
- AddLog.cs
- IPerson.cs
- Person.cs
This is the CSharp Project that references Matryoshki
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GX</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Matryoshki" Version="1.1.4" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
</ItemGroup>
</Project>
This is the use of Matryoshki in Program.cs
using Matryoshki.Abstractions;
Decorate<IPerson> // you can use Decorate<> alias if you prefer
.With<AddLog>()
.Name<PersonMatryoshka>();
var services = new ServiceCollection();
services.AddTransient<IPerson, Person>();
services.AddTransient<PersonMatryoshka, PersonMatryoshka>();
var serviceProvider = services.BuildServiceProvider();
var sp=serviceProvider.GetRequiredService<PersonMatryoshka>();
sp.FirstName = "Andrei";
sp.LastName = "Ignat";
Console.WriteLine(sp.FullName());
This is the use of Matryoshki in AddLog.cs
namespace MatryoshkiDemo;
internal class AddLog : IAdornment
{
public TResult MethodTemplate<TResult>(Call<TResult> call)
{
Console.WriteLine($"start Calling {call.MemberName} !");
var data =call.Forward();
Console.WriteLine($"end calling {call.MemberName} !");
return data;
}
}
This is the use of Matryoshki in IPerson.cs
namespace MatryoshkiDemo;
public interface IPerson
{
string? FirstName { get; set; }
int ID { get; set; }
string? LastName { get; set; }
string FullName();
}
This is the use of Matryoshki in Person.cs
namespace MatryoshkiDemo;
public class Person : IPerson
{
public int ID { get; set; }
public string? FirstName { get; set; }
public string? LastName { get; set; }
public string FullName()
{
return $"{FirstName} {LastName}";
}
}
Generated Files
Those are taken from $(BaseIntermediateOutputPath)\GX
- MatryoshkiDemo_AddLog.Compiled.g.cs
- .PersonMatryoshka.g.cs
[assembly: Matryoshki.Abstractions.CompiledAdornmentAttribute("MatryoshkiDemo.AddLog", "AddLog", "DQpuYW1lc3BhY2UgTWF0cnlvc2hraURlbW87DQoNCmludGVybmFsIGNsYXNzIEFkZExvZyA6IElBZG9ybm1lbnQNCnsNCiAgICBwdWJsaWMgVFJlc3VsdCBNZXRob2RUZW1wbGF0ZTxUUmVzdWx0PihDYWxsPFRSZXN1bHQ+IGNhbGwpDQogICAgeyAgICAgICAgDQogICAgICAgIENvbnNvbGUuV3JpdGVMaW5lKCQic3RhcnQgQ2FsbGluZyB7Y2FsbC5NZW1iZXJOYW1lfSAgISIpOw0KICAgICAgICB2YXIgZGF0YSAgICA9Y2FsbC5Gb3J3YXJkKCk7DQogICAgICAgIENvbnNvbGUuV3JpdGVMaW5lKCQiZW5kIGNhbGxpbmcge2NhbGwuTWVtYmVyTmFtZX0gISIpOw0KICAgICAgICByZXR1cm4gZGF0YTsNCg0KICAgIH0NCn0=")]
using System;
using MatryoshkiDemo;
#nullable enable
public class PersonMatryoshka : MatryoshkiDemo.IPerson
{
private readonly MatryoshkiDemo.IPerson _inner;
public PersonMatryoshka(MatryoshkiDemo.IPerson inner)
{
_inner = inner;
}
private static readonly string[] MethodParameterNamesForPropertyFirstName = new string[]
{
};
public string? FirstName
{
get
{
Console.WriteLine($"start Calling {"FirstName"} !");
var data = _inner.FirstName;
Console.WriteLine($"end calling {"FirstName"} !");
return data;
}
set
{
Console.WriteLine($"start Calling {"FirstName"} !");
var data = Matryoshki.Abstractions.Nothing.FromPropertyAction(_inner, value, static (@innerΔΔΔ, @valueΔΔΔ) => @innerΔΔΔ.FirstName = @valueΔΔΔ);
Console.WriteLine($"end calling {"FirstName"} !");
return;
}
}
private static readonly string[] MethodParameterNamesForPropertyID = new string[]
{
};
public int ID
{
get
{
Console.WriteLine($"start Calling {"ID"} !");
var data = _inner.ID;
Console.WriteLine($"end calling {"ID"} !");
return data;
}
set
{
Console.WriteLine($"start Calling {"ID"} !");
var data = Matryoshki.Abstractions.Nothing.FromPropertyAction(_inner, value, static (@innerΔΔΔ, @valueΔΔΔ) => @innerΔΔΔ.ID = @valueΔΔΔ);
Console.WriteLine($"end calling {"ID"} !");
return;
}
}
private static readonly string[] MethodParameterNamesForPropertyLastName = new string[]
{
};
public string? LastName
{
get
{
Console.WriteLine($"start Calling {"LastName"} !");
var data = _inner.LastName;
Console.WriteLine($"end calling {"LastName"} !");
return data;
}
set
{
Console.WriteLine($"start Calling {"LastName"} !");
var data = Matryoshki.Abstractions.Nothing.FromPropertyAction(_inner, value, static (@innerΔΔΔ, @valueΔΔΔ) => @innerΔΔΔ.LastName = @valueΔΔΔ);
Console.WriteLine($"end calling {"LastName"} !");
return;
}
}
private static readonly string[] MethodParameterNamesForMethodFullName = new string[]
{
};
public string FullName()
{
Console.WriteLine($"start Calling {"FullName"} !");
var data = _inner.FullName();
Console.WriteLine($"end calling {"FullName"} !");
return data;
}
}
Usefull
Download Example (.NET C# )
Share Matryoshki
https://ignatandrei.github.io/RSCG_Examples/v2/docs/Matryoshki