RefactorMCP catalog

Create Adapter

Generates an adapter: a class that implements an interface a client expects by wrapping an existing class, the adaptee, and forwarding each interface member to the adaptee member that does the same job.

This is a generator. The fixtures pin the one design described below; other adapters, such as one that inherits from the adaptee, would also be correct.

Arguments

ArgumentRequiredMeaning
interfaceyesThe interface to implement, as the adaptee's file would name it, such as ILogger or IRepository<Order>. A name the file does not import is looked up across the solution and its references
nameyesThe adapter's name
mapnoInterface member names mapped to adaptee member names, as an object: { "Log": "Write" }

The target is the adaptee class, by symbol: "target": { "symbol": "T:Shop.LegacyLogger" }.

Precondition

  • interface names an interface.
  • Every key of map names a member of the interface, and every value names an instance member of the adaptee (or a base class) that the adapter can reach.
  • Each mapped member has a compatible counterpart: see below.
  • No type in the adaptee's namespace has the adapter's name, and no file with that name exists beside the adaptee's file.
  • The result compiles.

Matching members

An interface member forwards to the adaptee member it is mapped to, or, when it is not mapped, to an adaptee member of the same name. A candidate is compatible when it is the same kind of member and:

  • a method has as many parameters with the same ref, out or in, each interface parameter type converts implicitly to the adaptee's, and the adaptee's return type converts implicitly to the interface's (any result may be discarded when the interface returns void);
  • a property has a usable getter whose type converts implicitly to the interface's when the interface reads it, and a usable setter taking the interface's type when the interface writes it; an indexer also needs compatible parameters;
  • an event has the same delegate type.

Among overloads, one whose types match exactly is preferred to one that needs conversions. An unmapped member with no compatible counterpart is generated throwing NotImplementedException; a mapped one is refused.

Generated shape

  • public class <name> : <interface> in a new file <name>.cs beside the adaptee's file, in the adaptee's namespace and namespace style, with the usings its signatures need.
  • A private readonly field _adaptee of the adaptee's type, and a public constructor taking adaptee 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, with the interface's parameter names and nullable annotations. Each has an expression body forwarding to its counterpart, public void Log(string message) => _adaptee.Write(message);, or => throw new NotImplementedException(); when it has none. Property and event accessors are laid out one to a line.

Behaviour

No existing behaviour changes: nothing refers to the adapter yet. The adapter's members throw where the adaptee has no counterpart, which a caller of the interface would observe once it is given an adapter.

Preserved

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

Limitations

  • Only instance members of the adaptee are considered; static ones are not.
  • Indexers and events are matched by kind, not by name, and cannot be mapped.
  • A counterpart must take the interface member's parameters in the same order; reordering or computing arguments is left to editing the adapter.
  • A property is never mapped to a method, nor a method to a property.
  • Nullability differences between the interface and the adaptee are not checked; the compiler reports them as warnings.

Error codes

CodeMeaning
interface-not-foundno type has the interface's name
not-an-interfacethe named type is a class, struct or other type rather than an interface
member-not-founda mapped name is not a member of the interface, or not a member of the adaptee
incompatible-signaturea mapped adaptee member cannot stand in for its interface member
type-already-existsthe namespace already has a type, or the folder a file, with the adapter'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.

#convertible-types

Accepts adaptee members whose parameters take, and whose results convert implicitly to, the interface's types

success
Targetsymbol T:Shop.Meter
Arguments
interface"IGauge"
name"MeterGauge"
map{"Read": "Measure", "Scale": "Factor"}
MeterGauge.csadded
1+namespace Shop
2+{
3+ public class MeterGauge : IGauge
4+ {
5+ private readonly Meter _adaptee;
6+
7+ public MeterGauge(Meter adaptee)
8+ {
9+ _adaptee = adaptee;
10+ }
11+
12+ public long Read(int channel) => _adaptee.Measure(channel);
13+
14+ public double Scale => _adaptee.Factor;
15+ }
16+}
Meter.csunchanged
11 namespace Shop
22 {
33 public interface IGauge
44 {
55 long Read(int channel);
66
77 double Scale { get; }
88 }
99
1010 public class Meter
1111 {
1212 public float Factor { get; set; } = 1f;
1313
1414 public int Measure(long channel) => (int)(channel * Factor);
1515 }
1616 }

#generic-interface

Adapts to a constructed generic interface declared in another file and namespace, importing it into the adapter

success
Targetsymbol T:Shop.LegacyOrderStore
Arguments
interface"IRepository<Order>"
name"OrderRepository"
map{"Find": "Load", "Save": "Store"}
OrderRepository.csadded
1+using Shop.Contracts;
2+
3+namespace Shop
4+{
5+ public class OrderRepository : IRepository<Order>
6+ {
7+ private readonly LegacyOrderStore _adaptee;
8+
9+ public OrderRepository(LegacyOrderStore adaptee)
10+ {
11+ _adaptee = adaptee;
12+ }
13+
14+ public Order Find(int id) => _adaptee.Load(id);
15+
16+ public void Save(Order item) => _adaptee.Store(item);
17+ }
18+}
Contracts/IRepository.csunchanged
11 namespace Shop.Contracts
22 {
33 public interface IRepository<T>
44 {
55 T Find(int id);
66
77 void Save(T item);
88 }
99 }
LegacyOrderStore.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 class LegacyOrderStore
1111 {
1212 private readonly Dictionary<int, Order> _orders = new Dictionary<int, Order>();
1313
1414 public Order Load(int id) => _orders[id];
1515
1616 public void Store(Order order) => _orders[order.Id] = order;
1717 }
1818 }

#incompatible-signature

Refuses a mapping to a member whose parameters cannot take the interface's arguments

refusal
Targetsymbol T:Shop.LegacyLogger
Arguments
interface"ILogger"
name"LegacyLoggerAdapter"
map{"Log": "Write"}
Refusesincompatible-signature mentioning “Write”; every file is left unchanged
LegacyLogger.csinput
1 using System;
2
3 namespace Shop
4 {
5 public interface ILogger
6 {
7 void Log(string message);
8 }
9
10 public class LegacyLogger
11 {
12 public void Write(int code) => Console.WriteLine(code);
13 }
14 }

#inherited-interface

Implements the members an interface inherits as well as its own

success
Targetsymbol T:Shop.Tape
Arguments
interface"IStream"
name"TapeStream"
map{"Read": "Play", "Write": "Record"}
TapeStream.csadded
1+namespace Shop
2+{
3+ public class TapeStream : IStream
4+ {
5+ private readonly Tape _adaptee;
6+
7+ public TapeStream(Tape adaptee)
8+ {
9+ _adaptee = adaptee;
10+ }
11+
12+ public void Write(string text) => _adaptee.Record(text);
13+
14+ public string Read() => _adaptee.Play();
15+ }
16+}
Tape.csunchanged
11 namespace Shop
22 {
33 public interface IReader
44 {
55 string Read();
66 }
77
88 public interface IStream : IReader
99 {
1010 void Write(string text);
1111 }
1212
1313 public class Tape
1414 {
1515 private string _recorded = "";
1616
1717 public string Play() => _recorded;
1818
1919 public void Record(string text) => _recorded += text;
2020 }
2121 }

#interface-not-found

Refuses an interface name that names no type

refusal
Targetsymbol T:Shop.LegacyLogger
Arguments
interface"IAuditLog"
name"LegacyLoggerAdapter"
Refusesinterface-not-found mentioning “IAuditLog”; every file is left unchanged
LegacyLogger.csinput
1 using System;
2
3 namespace Shop
4 {
5 public interface ILogger
6 {
7 void Log(string message);
8 }
9
10 public class LegacyLogger
11 {
12 public void Write(string message) => Console.WriteLine(message);
13 }
14 }

#mapped-method

Implements an interface over an existing class, delegating a method to the differently named one it maps to

success
Targetsymbol T:Shop.LegacyLogger
Arguments
interface"ILogger"
name"LegacyLoggerAdapter"
map{"Log": "Write"}
LegacyLoggerAdapter.csadded
1+namespace Shop
2+{
3+ public class LegacyLoggerAdapter : ILogger
4+ {
5+ private readonly LegacyLogger _adaptee;
6+
7+ public LegacyLoggerAdapter(LegacyLogger adaptee)
8+ {
9+ _adaptee = adaptee;
10+ }
11+
12+ public void Log(string message) => _adaptee.Write(message);
13+ }
14+}
ILogger.csunchanged
11 namespace Shop
22 {
33 public interface ILogger
44 {
55 void Log(string message);
66 }
77 }
LegacyLogger.csunchanged
11 using System;
22
33 namespace Shop
44 {
55 public class LegacyLogger
66 {
77 public void Write(string message)
88 {
99 Console.WriteLine(message);
1010 }
1111 }
1212 }

#member-not-found

Refuses a mapping to a member the adaptee does not have

refusal
Targetsymbol T:Shop.LegacyLogger
Arguments
interface"ILogger"
name"LegacyLoggerAdapter"
map{"Log": "Print"}
Refusesmember-not-found mentioning “Print”; every file is left unchanged
LegacyLogger.csinput
1 using System;
2
3 namespace Shop
4 {
5 public interface ILogger
6 {
7 void Log(string message);
8 }
9
10 public class LegacyLogger
11 {
12 public void Write(string message) => Console.WriteLine(message);
13 }
14 }

#not-an-interface

Refuses a target type that is a class rather than an interface

refusal
Targetsymbol T:Shop.LegacyLogger
Arguments
interface"Logger"
name"LegacyLoggerAdapter"
Refusesnot-an-interface mentioning “Logger”; every file is left unchanged
LegacyLogger.csinput
1 using System;
2
3 namespace Shop
4 {
5 public abstract class Logger
6 {
7 public abstract void Log(string message);
8 }
9
10 public class LegacyLogger
11 {
12 public void Write(string message) => Console.WriteLine(message);
13 }
14 }

#nullable-annotations

Keeps the interface's nullable annotations in the adapter's signatures

success
Targetsymbol T:Shop.LegacyCache
Arguments
interface"ICache"
name"CacheAdapter"
map{"Get": "Lookup", "Put": "Remember"}
Projectnullable enable
CacheAdapter.csadded
1+namespace Shop
2+{
3+ public class CacheAdapter : ICache
4+ {
5+ private readonly LegacyCache _adaptee;
6+
7+ public CacheAdapter(LegacyCache adaptee)
8+ {
9+ _adaptee = adaptee;
10+ }
11+
12+ public string? Get(string key) => _adaptee.Lookup(key);
13+
14+ public void Put(string key, string? value) => _adaptee.Remember(key, value);
15+ }
16+}
Cache.csunchanged
11 using System.Collections.Generic;
22
33 namespace Shop
44 {
55 public interface ICache
66 {
77 string? Get(string key);
88
99 void Put(string key, string? value);
1010 }
1111
1212 public class LegacyCache
1313 {
1414 private readonly Dictionary<string, string?> _entries = new Dictionary<string, string?>();
1515
1616 public string? Lookup(string key) => _entries.TryGetValue(key, out var value) ? value : null;
1717
1818 public void Remember(string key, string? value) => _entries[key] = value;
1919 }
2020 }

#overloads

Maps each overload of an interface method to the adaptee overload whose parameters it can pass

success
Targetsymbol T:Shop.OldPrinter
Arguments
interface"IPrinter"
name"PrinterAdapter"
map{"Print": "Output"}
PrinterAdapter.csadded
1+namespace Shop
2+{
3+ public class PrinterAdapter : IPrinter
4+ {
5+ private readonly OldPrinter _adaptee;
6+
7+ public PrinterAdapter(OldPrinter adaptee)
8+ {
9+ _adaptee = adaptee;
10+ }
11+
12+ public void Print(string text) => _adaptee.Output(text);
13+
14+ public void Print(string text, int copies) => _adaptee.Output(text, copies);
15+ }
16+}
Printing.csunchanged
11 using System;
22
33 namespace Shop
44 {
55 public interface IPrinter
66 {
77 void Print(string text);
88
99 void Print(string text, int copies);
1010 }
1111
1212 public class OldPrinter
1313 {
1414 public void Output(int code) => Console.WriteLine(code);
1515
1616 public void Output(string text) => Console.WriteLine(text);
1717
1818 public void Output(string text, int copies)
1919 {
2020 for (var i = 0; i < copies; i++)
2121 Console.WriteLine(text);
2222 }
2323 }
2424 }

#type-already-exists

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

refusal
Targetsymbol T:Shop.LegacyLogger
Arguments
interface"ILogger"
name"LoggerAdapter"
map{"Log": "Write"}
Refusestype-already-exists mentioning “LoggerAdapter”; every file is left unchanged
LegacyLogger.csinput
1 using System;
2
3 namespace Shop
4 {
5 public interface ILogger
6 {
7 void Log(string message);
8 }
9
10 public class LegacyLogger
11 {
12 public void Write(string message) => Console.WriteLine(message);
13 }
14
15 public class LoggerAdapter
16 {
17 }
18 }

#unmatched-members

Delegates a member the adaptee has by the same name and leaves members with no counterpart throwing NotImplementedException, without touching the interface's comments

success
Targetsymbol T:Shop.SystemClock
Arguments
interface"IClock"
name"SystemClockAdapter"
SystemClockAdapter.csadded
1+using System;
2+
3+namespace Shop
4+{
5+ public class SystemClockAdapter : IClock
6+ {
7+ private readonly SystemClock _adaptee;
8+
9+ public SystemClockAdapter(SystemClock adaptee)
10+ {
11+ _adaptee = adaptee;
12+ }
13+
14+ public DateTime Now => _adaptee.Now;
15+
16+ public void Freeze(DateTime at) => throw new NotImplementedException();
17+
18+ public event EventHandler Ticked
19+ {
20+ add => throw new NotImplementedException();
21+ remove => throw new NotImplementedException();
22+ }
23+ }
24+}
IClock.csunchanged
11 using System;
22
33 namespace Shop
44 {
55 /// <summary>What the scheduler needs from a clock.</summary>
66 public interface IClock
77 {
88 #region Time
99
1010 /// <summary>The current local time.</summary>
1111 DateTime Now { get; }
1212
1313 // Tests freeze the clock with this.
1414 void Freeze(DateTime at);
1515
1616 #endregion
1717
1818 event EventHandler Ticked;
1919 }
2020 }
SystemClock.csunchanged
11 using System;
22
33 namespace Shop
44 {
55 public class SystemClock
66 {
77 // Local time, as the operating system reports it.
88 public DateTime Now => DateTime.Now;
99 }
1010 }