RefactorMCP catalog

Extract Decorator

Generates a decorator for an interface: a class that implements the interface, wraps another implementation of it, and forwards every member to the wrapped instance. Behaviour is then added by editing the decorator's members, and callers opt in by wrapping an instance.

This is a generator. Many decorators would be correct; the fixtures pin the one design described below rather than a unique answer.

Arguments

ArgumentRequiredMeaning
namenoThe decorator's name. Defaults to the interface's name without its leading I, followed by Decorator: IGreeter gives GreeterDecorator

The target is the interface, or a class that implements exactly one interface, by symbol: "target": { "symbol": "T:Shop.IGreeter" }.

Precondition

  • The target is an interface, or declares exactly one interface in its base list. Interfaces it only inherits through a base class do not count.
  • No type in the target's namespace has the decorator's name, and no file with the decorator's name exists beside the target's file.
  • The result compiles.

Generated shape

  • public class <name> : <interface> in a new file <name>.cs beside the target's file, in the target's namespace, written in the namespace style (block or file-scoped) of the target's file, with the usings its signatures need.
  • A private readonly field _inner of the interface type, and a public constructor taking inner that assigns it.
  • One public member per interface member, the interface's own first and then those of each interface it inherits, in declaration order. Each forwards to _inner with an expression body:
  • methods pass their arguments with the same ref, out or in, restate params, default values, type parameters and constraints, and pass type arguments explicitly: _inner.Project<TResult>(item, map);
  • properties and indexers declare the accessors the interface declares; a get-only property is expression-bodied, one with a setter has get => ...; and set => ...; on separate lines;
  • events have add and remove accessors that subscribe to and unsubscribe from the wrapped instance's event.
  • A member an earlier member already implements, because it has the same signature and type, is not repeated. One whose type differs, such as a property a derived interface hides with new, is implemented explicitly through a cast: object IReader.Current => ((IReader)_inner).Current;.
  • Nullable annotations in the interface's signatures are kept.
  • A generic interface gives a generic decorator with the same type parameters and constraints. A class implementing a constructed interface, such as IRepository<Order>, gives a non-generic decorator of that interface.
  • The decorator's members carry no comments; the interface's documentation applies to them.

Behaviour

Nothing changes. No existing code refers to the decorator, and a decorator that only forwards behaves exactly as the instance it wraps.

Preserved

  • Every existing file, including the interface's comments, regions and layout.

Limitations

  • A class implementing several interfaces is refused; extract one interface that covers what is to be decorated, or decorate the interface itself.
  • Static interface members are not implemented, so an interface with static abstract members cannot be decorated.
  • Explicitly implemented members do not restate constraints, which C# does not allow there.
  • Callers are not changed to use the decorator.

Error codes

CodeMeaning
no-interfacethe class implements no interface
ambiguous-interfacethe class implements several interfaces
type-already-existsthe namespace already has a type, or the folder a file, with the decorator's name

Cases

·

Diffs show each file from before to after. /*[*/ … /*]*/ marks a selection and /*^*/ a caret; the runner removes them before the refactoring runs.

#across-projects

Places the decorator in the class's project when the interface lives in a project it references

success
Targetsymbol T:App.Greeter
ProjectsCore; App → Core
App/GreeterDecorator.csadded
1+using Core;
2+
3+namespace App
4+{
5+ public class GreeterDecorator : IGreeter
6+ {
7+ private readonly IGreeter _inner;
8+
9+ public GreeterDecorator(IGreeter inner)
10+ {
11+ _inner = inner;
12+ }
13+
14+ public string Greet(string name) => _inner.Greet(name);
15+ }
16+}
App/Greeter.csunchanged
11 using Core;
22
33 namespace App
44 {
55 public class Greeter : IGreeter
66 {
77 public string Greet(string name) => "Hello " + name;
88 }
99 }
Core/IGreeter.csunchanged
11 namespace Core
22 {
33 public interface IGreeter
44 {
55 string Greet(string name);
66 }
77 }

#all-member-kinds

Delegates events, properties, indexers and methods with out, ref, params and default parameters, under a chosen name

success
Targetsymbol T:Shop.IStore
Arguments
name"AuditedStore"
AuditedStore.csadded
1+using System;
2+using System.Collections.Generic;
3+
4+namespace Shop
5+{
6+ public class AuditedStore : IStore
7+ {
8+ private readonly IStore _inner;
9+
10+ public AuditedStore(IStore inner)
11+ {
12+ _inner = inner;
13+ }
14+
15+ public event EventHandler Changed
16+ {
17+ add => _inner.Changed += value;
18+ remove => _inner.Changed -= value;
19+ }
20+
21+ public string Name
22+ {
23+ get => _inner.Name;
24+ set => _inner.Name = value;
25+ }
26+
27+ public int Count => _inner.Count;
28+
29+ public string this[int index]
30+ {
31+ get => _inner[index];
32+ set => _inner[index] = value;
33+ }
34+
35+ public void Clear() => _inner.Clear();
36+
37+ public bool TryGet(string key, out string value) => _inner.TryGet(key, out value);
38+
39+ public void Swap(ref int first, ref int second) => _inner.Swap(ref first, ref second);
40+
41+ public int Sum(params int[] values) => _inner.Sum(values);
42+
43+ public void Add(string key, string value = "none") => _inner.Add(key, value);
44+
45+ public IEnumerable<string> Keys() => _inner.Keys();
46+ }
47+}
IStore.csunchanged
11 using System;
22 using System.Collections.Generic;
33
44 namespace Shop
55 {
66 public interface IStore
77 {
88 event EventHandler Changed;
99
1010 string Name { get; set; }
1111
1212 int Count { get; }
1313
1414 string this[int index] { get; set; }
1515
1616 void Clear();
1717
1818 bool TryGet(string key, out string value);
1919
2020 void Swap(ref int first, ref int second);
2121
2222 int Sum(params int[] values);
2323
2424 void Add(string key, string value = "none");
2525
2626 IEnumerable<string> Keys();
2727 }
2828 }

#ambiguous-interface

Refuses a class that implements several interfaces, since it is unclear which one to decorate

refusal
Targetsymbol T:Shop.Greeter
Refusesambiguous-interface mentioning “IDisposable”; every file is left unchanged
Greeter.csinput
1 using System;
2
3 namespace Shop
4 {
5 public interface IGreeter
6 {
7 string Greet(string name);
8 }
9
10 public class Greeter : IGreeter, IDisposable
11 {
12 public string Greet(string name) => "Hello " + name;
13
14 public void Dispose()
15 {
16 }
17 }
18 }

#class-target

Decorates the interface a class implements, declared in another file and namespace, placing the decorator beside the class

success
Targetsymbol T:Shop.Greeter
GreeterDecorator.csadded
1+using Shop.Contracts;
2+
3+namespace Shop
4+{
5+ public class GreeterDecorator : IGreeter
6+ {
7+ private readonly IGreeter _inner;
8+
9+ public GreeterDecorator(IGreeter inner)
10+ {
11+ _inner = inner;
12+ }
13+
14+ public string Greet(string name) => _inner.Greet(name);
15+
16+ public void Wave() => _inner.Wave();
17+ }
18+}
Contracts/IGreeter.csunchanged
11 namespace Shop.Contracts
22 {
33 public interface IGreeter
44 {
55 string Greet(string name);
66
77 void Wave();
88 }
99 }
Greeter.csunchanged
11 using System;
22 using Shop.Contracts;
33
44 namespace Shop
55 {
66 public class Greeter : IGreeter
77 {
88 public string Greet(string name) => $"Hello {name}";
99
1010 public void Wave() => Console.WriteLine("o/");
1111 }
1212 }
Program.csunchanged
11 using Shop.Contracts;
22
33 namespace Shop
44 {
55 public static class Program
66 {
77 public static string Run()
88 {
99 IGreeter greeter = new Greeter();
1010 return greeter.Greet("Ada");
1111 }
1212 }
1313 }

#closed-generic-interface

Decorates the constructed generic interface a class implements with a non-generic decorator

success
Targetsymbol T:Shop.OrderRepository
RepositoryDecorator.csadded
1+namespace Shop
2+{
3+ public class RepositoryDecorator : IRepository<Order>
4+ {
5+ private readonly IRepository<Order> _inner;
6+
7+ public RepositoryDecorator(IRepository<Order> inner)
8+ {
9+ _inner = inner;
10+ }
11+
12+ public Order Find(int id) => _inner.Find(id);
13+
14+ public void Save(Order item) => _inner.Save(item);
15+ }
16+}
OrderRepository.csunchanged
11 using System.Collections.Generic;
22
33 namespace Shop
44 {
55 public class Order
66 {
77 public int Id { get; set; }
88 }
99
1010 public interface IRepository<T>
1111 {
1212 T Find(int id);
1313
1414 void Save(T item);
1515 }
1616
1717 public class OrderRepository : IRepository<Order>
1818 {
1919 private readonly Dictionary<int, Order> _orders = new Dictionary<int, Order>();
2020
2121 public Order Find(int id) => _orders[id];
2222
2323 public void Save(Order item) => _orders[item.Id] = item;
2424 }
2525 }

#documented-interface

Leaves the interface's comments and region where they are and gives the decorator's members none

success
Targetsymbol T:Shop.IClock
ClockDecorator.csadded
1+using System;
2+
3+namespace Shop
4+{
5+ public class ClockDecorator : IClock
6+ {
7+ private readonly IClock _inner;
8+
9+ public ClockDecorator(IClock inner)
10+ {
11+ _inner = inner;
12+ }
13+
14+ public DateTime Now => _inner.Now;
15+
16+ public DateTime UtcNow => _inner.UtcNow;
17+ }
18+}
IClock.csunchanged
11 using System;
22
33 namespace Shop
44 {
55 /// <summary>Tells the time.</summary>
66 public interface IClock
77 {
88 #region Time
99
1010 /// <summary>The current local time.</summary>
1111 DateTime Now { get; }
1212
1313 // Callers comparing times should use this one.
1414 DateTime UtcNow { get; }
1515
1616 #endregion
1717 }
1818 }

#generic-interface

Gives the decorator of a generic interface its type parameters and constraints, restates a generic method's constraints, and follows the file-scoped namespace style

success
Targetsymbol T:Shop.IRepository`1
RepositoryDecorator.csadded
1+using System;
2+using System.Collections.Generic;
3+
4+namespace Shop;
5+
6+public class RepositoryDecorator<T> : IRepository<T> where T : class
7+{
8+ private readonly IRepository<T> _inner;
9+
10+ public RepositoryDecorator(IRepository<T> inner)
11+ {
12+ _inner = inner;
13+ }
14+
15+ public T Find(int id) => _inner.Find(id);
16+
17+ public IReadOnlyList<T> All() => _inner.All();
18+
19+ public TResult Project<TResult>(T item, Func<T, TResult> map) where TResult : struct => _inner.Project<TResult>(item, map);
20+}
IRepository.csunchanged
11 using System;
22 using System.Collections.Generic;
33
44 namespace Shop;
55
66 public interface IRepository<T> where T : class
77 {
88 T Find(int id);
99
1010 IReadOnlyList<T> All();
1111
1212 TResult Project<TResult>(T item, Func<T, TResult> map) where TResult : struct;
1313 }

#inherited-interface

Delegates inherited interface members and overloads, implementing a member the derived interface hides explicitly

success
Targetsymbol T:Shop.IStream
StreamDecorator.csadded
1+namespace Shop
2+{
3+ public class StreamDecorator : IStream
4+ {
5+ private readonly IStream _inner;
6+
7+ public StreamDecorator(IStream inner)
8+ {
9+ _inner = inner;
10+ }
11+
12+ public string Current => _inner.Current;
13+
14+ public void Write(string text) => _inner.Write(text);
15+
16+ object IReader.Current => ((IReader)_inner).Current;
17+
18+ public string Read() => _inner.Read();
19+
20+ public string Read(int count) => _inner.Read(count);
21+ }
22+}
IReader.csunchanged
11 namespace Shop
22 {
33 public interface IReader
44 {
55 object Current { get; }
66
77 string Read();
88
99 string Read(int count);
1010 }
1111 }
IStream.csunchanged
11 namespace Shop
22 {
33 public interface IStream : IReader
44 {
55 new string Current { get; }
66
77 void Write(string text);
88 }
99 }

#interface-target

Generates a decorator for an interface that delegates its one method to the wrapped instance

success
Targetsymbol T:Shop.IGreeter
GreeterDecorator.csadded
1+namespace Shop
2+{
3+ public class GreeterDecorator : IGreeter
4+ {
5+ private readonly IGreeter _inner;
6+
7+ public GreeterDecorator(IGreeter inner)
8+ {
9+ _inner = inner;
10+ }
11+
12+ public string Greet(string name) => _inner.Greet(name);
13+ }
14+}
IGreeter.csunchanged
11 namespace Shop
22 {
33 public interface IGreeter
44 {
55 string Greet(string name);
66 }
77 }

#no-interface

Refuses a class that implements no interface, since a decorator needs one to stand in for the class

refusal
Targetsymbol T:Shop.Greeter
Refusesno-interface mentioning “Greeter”; every file is left unchanged
Greeter.csinput
1 namespace Shop
2 {
3 public class Greeter
4 {
5 public string Greet(string name) => "Hello " + name;
6 }
7 }

#nullable-annotations

Keeps nullable annotations in the delegated signatures

success
Targetsymbol T:Shop.IDirectory
Projectnullable enable
DirectoryDecorator.csadded
1+using System;
2+
3+namespace Shop
4+{
5+ public class DirectoryDecorator : IDirectory
6+ {
7+ private readonly IDirectory _inner;
8+
9+ public DirectoryDecorator(IDirectory inner)
10+ {
11+ _inner = inner;
12+ }
13+
14+ public event EventHandler? Changed
15+ {
16+ add => _inner.Changed += value;
17+ remove => _inner.Changed -= value;
18+ }
19+
20+ public string? Find(string key) => _inner.Find(key);
21+
22+ public void Add(string key, string? value) => _inner.Add(key, value);
23+ }
24+}
IDirectory.csunchanged
11 using System;
22
33 namespace Shop
44 {
55 public interface IDirectory
66 {
77 event EventHandler? Changed;
88
99 string? Find(string key);
1010
1111 void Add(string key, string? value);
1212 }
1313 }

#type-already-exists

Refuses when the namespace already has a type with the decorator's name

refusal
Targetsymbol T:Shop.IGreeter
Refusestype-already-exists mentioning “GreeterDecorator”; every file is left unchanged
IGreeter.csinput
1 namespace Shop
2 {
3 public interface IGreeter
4 {
5 string Greet(string name);
6 }
7
8 public class GreeterDecorator
9 {
10 }
11 }