CopyCat by Serhii Buta
Nuget / site data
Details
Info
Name: CopyCat
Decorator pattert source generator with user-defined template
Author: Serhii Buta
NuGet: https://www.nuget.org/packages/Copycat/
You can find more details at https://github.com/Otaman/Copycat/
Source : https://github.com/Otaman/Copycat/
Original Readme
Copycat
Source generator for creating decorators by templates. The source generator intents to simplify implementation of a Decorator Pattern.
The package is still in beta. Currently, caching is not implemented, so the source generator regenerates output for every input, which may slow down IDEs in larger projects.
Use Cases
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
}
}
//
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 :sunglasses:
To make Copycat generate something different from pass-through we need to define a template:
```C#
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.
Logging
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.
Retries
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}");
}
}
}
Advanced
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).
About
Implementation of the Decorator pattern in C# - only for not implemented methods
How to use
Example ( source csproj, source files )
- CSharp Project
- Program.cs
- ICoffee.cs
- Coffee.cs
- CoffeeWithLogging.cs
This is the CSharp Project that references CopyCat
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Copycat" Version="0.2.0-beta.1" OutputItemType="Analyzer" />
</ItemGroup>
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GX</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
</Project>
This is the use of CopyCat in Program.cs
using CCDemo;
ICoffee c =new Coffee();
c= new CoffeeWithLogging(c);
await c.Prepare();
This is the use of CopyCat in ICoffee.cs
namespace CCDemo;
internal interface ICoffee
{
//for the moment does not work for properties in interface
//string? Name { get; set; }
Task<bool> Prepare();
string[] GetIngredients();
}
This is the use of CopyCat in Coffee.cs
namespace CCDemo;
internal class Coffee : ICoffee
{
public string? Name { get; set; }
public async Task<bool> Prepare()
{
Console.WriteLine("start prepare coffee");
await Task.Delay(1000);
Console.WriteLine("finish prepare coffee");
return true;
}
public string[] GetIngredients() => new[] { "water", "coffee" };
}
This is the use of CopyCat in CoffeeWithLogging.cs
using Copycat;
namespace CCDemo;
[Decorate]
internal partial class CoffeeWithLogging: ICoffee
{
[Template]
private string[] AddLogging(Func<string[]> action)
{
try
{
Console.WriteLine($"start logging {nameof(action)} ");
return action();
}
catch (Exception e)
{
Console.WriteLine($"exception {nameof(action)} ");
throw;
}
finally
{
Console.WriteLine($"end logging {nameof(action)} ");
}
}
[Template]
public async Task<bool> AddLogging(Func<Task<bool>> action)
{
try
{
Console.WriteLine($"start logging {nameof(action)} ");
return await action();
}
catch (Exception e)
{
Console.WriteLine($"exception {nameof(action)} ");
throw;
}
finally
{
Console.WriteLine($"end logging {nameof(action)} ");
}
}
}
Generated Files
Those are taken from $(BaseIntermediateOutputPath)\GX
- CoffeeWithLogging.g.cs
// <auto-generated/>
using Copycat;
namespace CCDemo;
internal partial class CoffeeWithLogging
{
private CCDemo.ICoffee _decorated;
public CoffeeWithLogging(CCDemo.ICoffee decorated)
{
_decorated = decorated;
}
/// <see cref = "CoffeeWithLogging.AddLogging(Func{Task{bool}})"/>
public async //for the moment does not work for properties in interface
//string? Name { get; set; }
Task<bool> Prepare()
{
try
{
Console.WriteLine($"start logging {nameof(Prepare)} ");
return await _decorated.Prepare();
}
catch (Exception e)
{
Console.WriteLine($"exception {nameof(Prepare)} ");
throw;
}
finally
{
Console.WriteLine($"end logging {nameof(Prepare)} ");
}
}
/// <see cref = "CoffeeWithLogging.AddLogging(Func{string[]})"/>
public string[] GetIngredients()
{
try
{
Console.WriteLine($"start logging {nameof(GetIngredients)} ");
return _decorated.GetIngredients();
}
catch (Exception e)
{
Console.WriteLine($"exception {nameof(GetIngredients)} ");
throw;
}
finally
{
Console.WriteLine($"end logging {nameof(GetIngredients)} ");
}
}
}
Usefull
Download Example (.NET C# )
Share CopyCat
https://ignatandrei.github.io/RSCG_Examples/v2/docs/CopyCat