Skip to main content

CopyCat by Serhii Buta

Nuget / site data

Nuget GitHub last commit GitHub Repo stars

Details

Info

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

note

Copycat NuGet Badge

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 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 :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

note

Implementation of the Decorator pattern in C# - only for not implemented methods

How to use

Example ( source csproj, source files )

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>

Generated Files

Those are taken from $(BaseIntermediateOutputPath)\GX

// <auto-generated/>
using Copycat;

namespace CCDemo;
internal partial class CofeeWithLogging
{
private CCDemo.ICoffee _decorated;
public CofeeWithLogging(CCDemo.ICoffee decorated)
{
_decorated = decorated;
}

/// <see cref = "CofeeWithLogging.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 = "CofeeWithLogging.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

In the same category (Interface) - 8 other generators

Biwen.AutoClassGen

Farskeptic.AutoCompose

MakeInterface.Generator

Matryoshki

NetAutomaticInterface

ProxyGen

Roozie.AutoInterface

RSCG_Static