Source generator for creating decorators by templates. The source generator intents to simplify implementation of a Decorator Pattern.
Les't begin from simple scenario. We need to decorate ISomeInterface:
public interface ISomeInterface
{
void DoSomething();
void DoSomethingElse(int a, string b);
}
To activate generator, use [Decorate] attribute on a class. The class must be partial and have exactly one interface to decorate:
using Copycat;
[Decorate]
public partial class SimpleDecorator : ISomeInterface { }
In this example, Copycat generates pass-through decorator:
// <auto-generated/>
public partial class SimpleDecorator
{
private ISomeInterface _decorated;
public SimpleDecorator(ISomeInterface decorated)
{
_decorated = decorated;
}
public void DoSomething() => _decorated.DoSomething();
public void DoSomethingElse(int a, string b) => _decorated.DoSomethingElse(a, b);
}
Pass-through decorators don't do much, but still can be useful for changing behaviour of particular methods without touching others:
Here and after we skip
using Copycat;
and combine user-defined and auto-generated code for brevity
[Decorate]
public partial class SimpleDecorator : ISomeInterface
{
public void DoSomething()
{
// actually, do nothing
}
}
// <auto-generated/>
public partial class SimpleDecorator
{
private ISomeInterface _decorated;
public SimpleDecorator(ISomeInterface decorated)
{
_decorated = decorated;
}
public void DoSomethingElse(int a, string b) => _decorated.DoSomethingElse(a, b);
}
As we see, Copycat now generates pass-through only for non-implemented methods (DoSomethingElse), allowing us to concentrate on important changes.
But what if we want to override behaviour for one method, but throw for all others (assuming we got some huge legacy interface, where most methods are useless for us)? Now it's time to play with templates 😎
To make Copycat generate something different from pass-through we need to define a template:
public interface IAmPartiallyUseful
{
void DoSomethingUseful();
void DoSomething();
void DoSomethingElse();
}
[Decorate]
public partial class ThrowDecorator : IAmPartiallyUseful
{
public void DoSomethingUseful() => Console.WriteLine("I did some work!");
[Template]
private void Throw(Action action) => throw new NotImplementedException();
}
// <auto-generated/>
public partial class ThrowDecorator
{
private IAmPartiallyUseful _decorated;
public ThrowDecorator(IAmPartiallyUseful decorated)
{
_decorated = decorated;
}
/// <see cref = "ThrowDecorator.Throw(Action)"/>
public void DoSomething() => throw new NotImplementedException();
/// <see cref = "ThrowDecorator.Throw(Action)"/>
public void DoSomethingElse() => throw new NotImplementedException();
}
That's better, now we do some work on DoSomethingUseful and throw on DoSomething or DoSomethingElse, but how? We defined a template:
[Template]
private void Throw(Action action) {...}
Template is a method that takes parameterless delegate which has the same return type as the method itself. We can use any names for the template method and a delegate (as usual, it's better to keep them self-explanatory).
We didn't use the delegate in the pevious example because we limited ourselves to simple examples where it wasn't needed. Now it's time to explore more real-world scenarios. Decorators fit nicely for aspect-oriented programming (AOP) when using them as wrappers.
One of the aspects, than can be separated easily is logging. For example:
using System.Diagnostics;
public interface ISomeInterface
{
void DoNothing();
void DoSomething();
void DoSomethingElse(int a, string b);
}
[Decorate]
public partial class SimpleDecorator : ISomeInterface
{
private readonly ISomeInterface _decorated;
public SimpleDecorator(ISomeInterface decorated) =>
_decorated = decorated;
[Template]
public void CalculateElapsedTime(Action action)
{
var sw = Stopwatch.StartNew();
action();
Console.WriteLine($"{nameof(action)} took {sw.ElapsedMilliseconds} ms");
}
public void DoNothing() { }
}
public partial class SimpleDecorator
{
/// <see cref = "SimpleDecorator.CalculateElapsedTime(Action)"/>
public void DoSomething()
{
var sw = Stopwatch.StartNew();
_decorated.DoSomething();
Console.WriteLine($"{nameof(DoSomething)} took {sw.ElapsedMilliseconds} ms");
}
/// <see cref = "SimpleDecorator.CalculateElapsedTime(Action)"/>
public void DoSomethingElse(int a, string b)
{
var sw = Stopwatch.StartNew();
_decorated.DoSomethingElse(a, b);
Console.WriteLine($"{nameof(DoSomethingElse)} took {sw.ElapsedMilliseconds} ms");
}
}
Here DoSomething and DoSomething else are generated as specified by the template CalculateElapsedTime. Copycat has convention to replace delegate invocation with decorated method invocation (includes passing all parameters). For convenience, nameof(delegate) also replaced with nameof(MethodName) for easier use in templating.
Let's make our generator do some more interesting task. In most situations Polly nuget package is the best choice for retries. But for simple cases it may bring unnecessary complexity, like here:
public interface ICache<T>
{
Task<T> Get(string key);
Task<T> Set(string key, T value);
}
[Decorate]
public partial class CacheDecorator<T> : ICache<T>
{
private readonly ICache<T> _decorated;
public CacheDecorator(ICache<T> decorated) => _decorated = decorated;
[Template]
public async Task<T> RetryOnce(Func<Task<T>> action, string key)
{
try
{
return await action();
}
catch (Exception e)
{
Console.WriteLine($"Retry {nameof(action)} for {key} due to {e.Message}");
return await action();
}
}
}
public partial class CacheDecorator<T>
{
/// <see cref = "CacheDecorator.RetryOnce(Func{Task{T}}, string)"/>
public async Task<T> Get(string key)
{
try
{
return await _decorated.Get(key);
}
catch (Exception e)
{
Console.WriteLine($"Retry {nameof(Get)} for {key} due to {e.Message}");
return await _decorated.Get(key);
}
}
/// <see cref = "CacheDecorator.RetryOnce(Func{Task{T}}, string)"/>
public async Task<T> Set(string key, T value)
{
try
{
return await _decorated.Set(key, value);
}
catch (Exception e)
{
Console.WriteLine($"Retry {nameof(Set)} for {key} due to {e.Message}");
return await _decorated.Set(key, value);
}
}
}
Caching should be fast, so we can't retry many times. One is ok, especially with some log message about the problem. Pay attention to key parameter in the template, it matches nicely our interface methods parameter.
If additional parameters defined in template, then generator applies this template only for methods that have same exact parameter. Actually, we can implement more complext retry patterns, too:
[Template]
public async Task<T> Retry<T>(Func<Task<T>> action)
{
var retryCount = 0;
while (true)
{
try
{
return await action();
}
catch (Exception e)
{
if (retryCount++ >= 3)
throw;
Console.WriteLine($"Retry {nameof(action)} {retryCount} due to {e.Message}");
}
}
}
There are plenty use cases, than can be covered with Copycat. Feel free to explore them in src/Copycat/Copycat.IntegrationTests
(and Generated
folder inside).
For instance, defining template in base class (see RetryWrapperWithBase.cs) or using multiple template to match methods with different signature see TestMultipleTemplates.cs).