Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/Otaman/Copycat

Source generators for creating decorators by templates
https://github.com/Otaman/Copycat

Last synced: about 1 month ago
JSON representation

Source generators for creating decorators by templates

Awesome Lists containing this project

README

        

# Copycat [![NuGet Badge](https://buildstats.info/nuget/Copycat?includePreReleases=true)](https://www.nuget.org/packages/Copycat)
Source generator for creating decorators by templates.
The source generator intents to simplify implementation of a [Decorator Pattern](https://en.wikipedia.org/wiki/Decorator_pattern).

## Use Cases

Les't begin from simple scenario. We need to decorate ISomeInterface:
```C#
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:
```C#
using Copycat;

[Decorate]
public partial class SimpleDecorator : ISomeInterface { }
```

In this example, Copycat generates pass-through decorator:
```C#
//
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
```C#
[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();
}

//
public partial class ThrowDecorator
{
private IAmPartiallyUseful _decorated;
public ThrowDecorator(IAmPartiallyUseful decorated)
{
_decorated = decorated;
}

///
public void DoSomething() => throw new NotImplementedException();
///
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:
```C#
[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:
```C#
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
{
///
public void DoSomething()
{
var sw = Stopwatch.StartNew();
_decorated.DoSomething();
Console.WriteLine($"{nameof(DoSomething)} took {sw.ElapsedMilliseconds} ms");
}

///
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:
```C#
public interface ICache
{
Task Get(string key);
Task Set(string key, T value);
}

[Decorate]
public partial class CacheDecorator : ICache
{
private readonly ICache _decorated;

public CacheDecorator(ICache decorated) => _decorated = decorated;

[Template]
public async Task RetryOnce(Func> 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
{
///
public async Task 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);
}
}

///
public async Task 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:
```C#
[Template]
public async Task Retry(Func> 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).