The last few years have seen a huge proliferation of interfaces for C# classes. These interfaces are very useful for DI, testing and mocking. But they come at a cost: while extracting an interface is fairly easy, you now have to keep interface and class in sync. Especially in an evolving project, this can quickly become annoying. Change a parameter on the class? Edit the interface, too! Change to class-doc? Edit the interface, too. This is tedious, and smaller things like parameter names and documentation get out of sync quickly.
Luckily, C# Source Generators offer the ability to automatically generate these interfaces for us. I even wrote a post about this topic last year, but the birth of our second child prevented me from finishing my idea. Finally, I was able to complete the code and publish it as a NuGet Package: AutomaticInterface.
AutomaticInterface takes care of creating an interface for a class – it automatically copies all public properties, methods, events and the documentation to the interface. You can use it like a normal interface and every change to your class will be picked up by the interface in the next build.
How to use it
- Install the nuget:
dotnet add package AutomaticInterface
- Create an attribute with the name
[GenerateAutomaticInterface]
. You can just copy the minimal code from the repo. It’s the easiest way to get that attribute because you cannot reference any code from the analyzer package. - Let your class implement the interface, e.g.
SomeClass: ISomeClass
- Build the solution, the interface should now be available.
using AutomaticInterfaceAttribute;
using System;
namespace AutomaticInterfaceExample
{
/// <summary>
/// Class Documentation will be copied
/// </summary>
[GenerateAutomaticInterface] // you need to create an attribute with exactly this name in your solution. You cannot reference code from the analyzer.
class DemoClass: IDemoClass // You Interface will get the Name I+classname, here IDemoclass.
// Generics, including constraints are allowed, too. E.g. MyClass<T> where T: class
{
/// <summary>
/// Property Documentation will be copied
/// </summary>
public string Hello { get; set; } // included, get and set are copied to the interface when public
public string OnlyGet { get; } // included, get and set are copied to the interface when public
/// <summary>
/// Method Documentation will be copied
/// </summary>
public string AMethod(string x, string y) // included
{
return BMethod(x,y);
}
private string BMethod(string x, string y) // ignored because not public
{
return x + y;
}
public static string StaticProperty => "abc"; // static property, ignored
public static string StaticMethod() // static method, ignored
{
return "static" + DateTime.Now;
}
/// <summary>
/// event Documentation will be copied
/// </summary>
public event EventHandler ShapeChanged; // included
private event EventHandler ShapeChanged2; // ignored because not public
private readonly int[] arr = new int[100];
public int this[int index] // currently ignored
{
get => arr[index];
set => arr[index] = value;
}
}
} |
using AutomaticInterfaceAttribute;
using System; namespace AutomaticInterfaceExample
{
/// <summary>
/// Class Documentation will be copied
/// </summary>
[GenerateAutomaticInterface] // you need to create an attribute with exactly this name in your solution. You cannot reference code from the analyzer.
class DemoClass: IDemoClass // You Interface will get the Name I+classname, here IDemoclass.
// Generics, including constraints are allowed, too. E.g. MyClass<T> where T: class
{ /// <summary>
/// Property Documentation will be copied
/// </summary>
public string Hello { get; set; } // included, get and set are copied to the interface when public public string OnlyGet { get; } // included, get and set are copied to the interface when public /// <summary>
/// Method Documentation will be copied
/// </summary>
public string AMethod(string x, string y) // included
{
return BMethod(x,y);
} private string BMethod(string x, string y) // ignored because not public
{
return x + y;
} public static string StaticProperty => "abc"; // static property, ignored public static string StaticMethod() // static method, ignored
{
return "static" + DateTime.Now;
} /// <summary>
/// event Documentation will be copied
/// </summary> public event EventHandler ShapeChanged; // included private event EventHandler ShapeChanged2; // ignored because not public private readonly int[] arr = new int[100]; public int this[int index] // currently ignored
{
get => arr[index];
set => arr[index] = value;
}
}
}
This will create this interface:
using System.CodeDom.Compiler;
using AutomaticInterfaceAttribute;
using System;
/// <summary>
/// Result of the generator
/// </summary>
namespace AutomaticInterfaceExample
{
/// <summary>
/// Bla bla
/// </summary>
[GeneratedCode("AutomaticInterface", "")]
public partial interface IDemoClass
{
/// <summary>
/// Property Documentation will be copied
/// </summary>
string Hello { get; set; }
string OnlyGet { get; }
/// <summary>
/// Method Documentation will be copied
/// </summary>
string AMethod(string x, string y);
/// <summary>
/// event Documentation will be copied
/// </summary>
event System.EventHandler ShapeChanged;
}
} |
using System.CodeDom.Compiler;
using AutomaticInterfaceAttribute;
using System; /// <summary>
/// Result of the generator
/// </summary>
namespace AutomaticInterfaceExample
{
/// <summary>
/// Bla bla
/// </summary>
[GeneratedCode("AutomaticInterface", "")]
public partial interface IDemoClass
{
/// <summary>
/// Property Documentation will be copied
/// </summary>
string Hello { get; set; } string OnlyGet { get; } /// <summary>
/// Method Documentation will be copied
/// </summary>
string AMethod(string x, string y); /// <summary>
/// event Documentation will be copied
/// </summary>
event System.EventHandler ShapeChanged;
}
}