Important Note
I would really appreciate if you leave me comments stating how you think this article can be improved. Thanks.
Introduction
In my C# programming experience, I came across many cases where extending a plain simple Enumeration would be of benefit. The most usual case is when I need to use an enumeration from a dll library that I cannot modify in my code while at the same time, I also need to use some extra enumeration values that the library does not contain.
A similar idea is presented by Sergey Kryukov in Enumeration Types do not Enumerate! Working around .NET and Language Limitations (see section 2.5 Mocking Programming by Extension).
Here I attempt to resolve this problem using Roslyn based VS extension for generating single files, similar to the approach to simulating multiple inheritance in Implementing Adapter Pattern and Imitating Multiple Inheritance in C# using Roslyn based VS Extension Wrapper Generator and the subsequent articles.
I am using the Visual Studio 2017 and the resulting VSIX file should only work in Visual Studio 2017.
Stating the Problem
Take a look at EnumDerivationSample
project. It contains a non-generated code large parts of which we later show can be generated. The project contains BaseEnum
enumeration:
public enum BaseEnum
{
A,
B
}
Also it has DerivedEnum
enumeration:
public enum DerivedEnum
{
A,
B,
C,
D,
E
}
Note that in the DerivedEnum
enumeration, the enumeration values A
and B
are the same as those of BaseEnum
enumeration both in their name and in their integer value.
File DerivedEnum.cs also contains static
DeriveEnumExtensions
class that has extensions for converting from BaseEnum
to DerivedEnum
and vice versa:
public static class DeriveEnumExtensions
{
public static BaseEnum ToBaseEnum(this DerivedEnum derivedEnum)
{
int intDerivedVal = (int)derivedEnum;
string derivedEnumTypeName = typeof(DerivedEnum).Name;
string baseEnumTypeName = typeof(BaseEnum).Name;
if (intDerivedVal > 1)
{
throw new Exception
(
"Cannot convert " + derivedEnumTypeName + "." +
derivedEnum + " value to " + baseEnumTypeName +
" type, since its integer value " +
intDerivedVal + " is greater than the max value 1 of " +
baseEnumTypeName + " enumeration."
);
}
BaseEnum baseEnum = (BaseEnum)intDerivedVal;
return baseEnum;
}
public static DerivedEnum ToDerivedEnum(this BaseEnum baseEnum)
{
int intBaseVal = (int)baseEnum;
DerivedEnum derivedEnum = (DerivedEnum)intBaseVal;
return derivedEnum;
}
}
As you can see, the conversion of the BaseEnum
value to DerivedEnum
value is always successful, while conversion in the opposite direction can throw an exception if the DerivedEnum
value is higher than 1 (which is the value of BaseEnum.B
- the highest value of BaseEnum
enumeration.
Program.Main(...)
function is used for testing the functionality:
static void Main(string[] args)
{
DerivedEnum derivedEnumConvertedValue = BaseEnum.A.ToDerivedEnum();
Console.WriteLine("Derived converted value is " + derivedEnumConvertedValue);
BaseEnum baseEnumConvertedValue = DerivedEnum.B.ToBaseEnum();
Console.WriteLine("Derived converted value is " + baseEnumConvertedValue);
DerivedEnum.C.ToBaseEnum();
}
It will print:
Derived converted value is A
Base converted value is B
And then it will throw an exception containing the following message:
"Cannot convert DerivedEnum.C value to BaseEnum type,
since its integer value 2 is greater than the max value 1 of BaseEnum enumeration."
Using the Visual Studio Extension to Generate Enumeration Inheritance
Now install the NP.DeriveEnum.vsix
Visual Studio extension from VSIX folder by double clicking the file.
Open project EnumDerivationWithCodeGenerationTest
. Its base enumeration is the same as in the previous project:
public enum BaseEnum
{
A,
B
}
Take a look at the file "DerivedEnum.cs":
[DeriveEnum(typeof(BaseEnum), "DerivedEnum")]
enum _DerivedEnum
{
C,
D,
E
}
It defines a enumeration _DerivedEnum
with an attribute: [DeriveEnum(typeof(BaseEnum), "DerivedEnum")]
. The attribute specifies the "super-enumeration" (BaseEnum
) and the name of the derived enumeration ("DerivedEnum"
). Note that since partial
enumerations are not supported in C#, we are forced to create a new enumeration combining the values from "super" and "sub" enumerations.
If you look at the properties of DerivedEnum.cs file, you'll see that its "Custom Tool" property is set to "DeriveEnumGenerator
" value:
Now, open DerivedEnum.cs file in Visual Studio and try to modify it (say by adding a space) and save it. You'll see that immediately file DerivedEnum.extension.cs is being created:
This file contains DerivedEnum
enumeration which combines all the fields from BaseEnum
and _DerivedEnum
enumerations, making sure that they have the same name and integer value as the corresponding fields in the original enumerations:
public enum DerivedEnum
{
A,
B,
C,
D,
E,
}
The VS extension also generates a static
class DerivedEnumExtensions
that contains conversion methods between the sub and super enumerations:
static public class DerivedEnumExtensions
{
public static BaseEnum ToBaseEnum(this DerivedEnum fromEnum)
{
int val = ((int)(fromEnum));
string exceptionMessage = "Cannot convert DerivedEnum.{0}
value to BaseEnum - there is no matching value";
if ((val > 1))
{
throw new System.Exception(string.Format(exceptionMessage, fromEnum));
}
BaseEnum result = ((BaseEnum)(val));
return result;
}
public static DerivedEnum ToDerivedEnum(this BaseEnum fromEnum)
{
int val = ((int)(fromEnum));
DerivedEnum result = ((DerivedEnum)(val));
return result;
}
}
Now if we use the same Program.Main(...)
method as in the previous sample, we'll obtain a very similar result:
static void Main(string[] args)
{
DerivedEnum derivedEnumConvertedValue = BaseEnum.A.ToDerivedEnum();
Console.WriteLine("Derived converted value is " + derivedEnumConvertedValue);
BaseEnum baseEnumConvertedValue = DerivedEnum.B.ToBaseEnum();
Console.WriteLine("Base converted value is " + baseEnumConvertedValue);
DerivedEnum.C.ToBaseEnum();
}
You can try to specify field values within both sub and super enumerations. The code generator is smart enough to generate the correct code. E.g., if we set BaseEnum.B
to 20
:
public enum BaseEnum
{
A,
B = 20
}
and _DerivedEnum.C
to 22
:
enum _DerivedEnum
{
C = 22,
D,
E
}
We'll get the following generated code:
public enum DerivedEnum
{
A,
B = 20,
C = 22,
D,
E,
}
The ToBaseEnum(...)
extension method will also be updated to throw an exception only when the integer value of DerivedEnum
field we are trying to convert is greater than 20
:
public static BaseEnum ToBaseEnum(this DerivedEnum fromEnum)
{
int val = ((int)(fromEnum));
string exceptionMessage = "Cannot convert DerivedEnum.{0}
value to BaseEnum - there is no matching value";
if ((val > 20))
{
throw new System.Exception(string.Format(exceptionMessage, fromEnum));
}
BaseEnum result = ((BaseEnum)(val));
return result;
}
Note that if you change the first field of the sub-enumeration to be smaller or equal to the last field of the super-enumeration, the generation won't take place and this condition will be reported as an error. For example, try changing _DerivedEnum.C
to 20
and saving the change. The file DerivedEnum.extension.cs is going to disappear and you'll see the following errors in the Error List:
Notes on the Code Generator Implementation
The code implementing the code generation is located under NP.DeriveEnum
project. The main project NP.DeriveEnum
was created using the "Visual Studio Package" template (just like it was done in Implementing Adapter Pattern and Imitating Multiple Inheritance in C# using Roslyn based VS Extension Wrapper Generator).
I also had to add the Roslyn and MEF2 packages in order to be able to use Roslyn functionality by running the following commands from "Nu Get Package Manager Console":
Install-Package Microsoft.CodeAnalysis -Pre
Install-Package Microsoft.Composition
The 'main
' class of the generator is called DeriveEnumGenerator
. It implements IVsSingleFileGenerator
interface. The interface has two methods - DefaultExtension(...)
and Generate(...)
.
Method DefaultExtension(...)
allows the developer to specify the extension of the generated file name:
public int DefaultExtension(out string pbstrDefaultExtension)
{
pbstrDefaultExtension = ".extension.cs";
return VSConstants.S_OK;
}
Method Generate(...)
allows the developer to specify the code that goes into the generated file:
public int Generate
(
string wszInputFilePath,
string bstrInputFileContents,
string wszDefaultNamespace,
IntPtr[] rgbOutputFileContents,
out uint pcbOutput,
IVsGeneratorProgress pGenerateProgress
)
{
byte[] codeBytes = null;
try
{
codeBytes = GenerateCodeBytes(wszInputFilePath, bstrInputFileContents,
wszDefaultNamespace);
}
catch(Exception e)
{
pGenerateProgress.GeneratorError(0, 0, e.Message, 0, 0);
pcbOutput = 0;
return VSConstants.E_FAIL;
}
int outputLength = codeBytes.Length;
rgbOutputFileContents[0] = Marshal.AllocCoTaskMem(outputLength);
Marshal.Copy(codeBytes, 0, rgbOutputFileContents[0], outputLength);
pcbOutput = (uint)outputLength;
return VSConstants.S_OK;
}
In our case, the code generation is actually done by GenerateCodeBytes(...)
method called by Generate(...)
method.
protected byte[] GenerateCodeBytes
(string filePath, string inputFileContent, string namespaceName)
{
string generatedCode = "";
DocumentId docId =
TheWorkspace
.CurrentSolution
.GetDocumentIdsWithFilePath(filePath).FirstOrDefault();
if (docId == null)
goto returnLabel;
Project project = TheWorkspace.CurrentSolution.GetProject(docId.ProjectId);
if (project == null)
goto returnLabel;
Compilation compilation = project.GetCompilationAsync().Result;
if (compilation == null)
goto returnLabel;
Document doc = project.GetDocument(docId);
if (doc == null)
goto returnLabel;
SyntaxTree docSyntaxTree = doc.GetSyntaxTreeAsync().Result;
if (docSyntaxTree == null)
goto returnLabel;
SemanticModel semanticModel = compilation.GetSemanticModel(docSyntaxTree);
if (semanticModel == null)
goto returnLabel;
EnumDeclarationSyntax enumNode =
docSyntaxTree.GetRoot()
.DescendantNodes()
.Where((node) => (node.CSharpKind() ==
SyntaxKind.EnumDeclaration)).FirstOrDefault() as EnumDeclarationSyntax;
if (enumNode == null)
goto returnLabel;
INamedTypeSymbol enumSymbol =
semanticModel.GetDeclaredSymbol(enumNode) as INamedTypeSymbol;
if (enumSymbol == null)
goto returnLabel;
generatedCode = enumSymbol.CreateEnumExtensionCode();
returnLabel:
byte[] bytes = Encoding.UTF8.GetBytes(generatedCode);
return bytes;
}
The Generate(...)
method has path to the C# file as one of the arguments. We use that path do get the Roslyn document Id of the document that we work with:
DocumentId docId =
TheWorkspace
.CurrentSolution
.GetDocumentIdsWithFilePath(filePath).FirstOrDefault();
From the document Id, we can get the project id by using dockId.ProjectId
property.
From the project id, we get the Roslyn Project
from Roslyn Workspace
:
Project project = TheWorkspace.CurrentSolution.GetProject(docId.ProjectId);
And from Project
, we get its compilation:
Compilation compilation = project.GetCompilationAsync().Result;
We also get the Roslyn Document
from the project:
Document doc = project.GetDocument(docId);
From the current document, we get its Roslyn SyntaxTree
:
SyntaxTree docSyntaxTree = doc.GetSyntaxTreeAsync().Result;
From the Roslyn Compilation
and SyntaxTree
, we get the semantic model:
SemanticModel semanticModel = compilation.GetSemanticModel(docSyntaxTree);
We also get the first enumeration syntax declared in the file from the SyntaxTree
:
EnumDeclarationSyntax enumNode =
docSyntaxTree.GetRoot()
.DescendantNodes()
.Where((node) => (node.CSharpKind() ==
SyntaxKind.EnumDeclaration)).FirstOrDefault() as EnumDeclarationSyntax;
Finally from the SemanticModel
and EnumerationDeclarationSyntax
, we can pull the INamedTypeSymbol
corresponding to the enumeration:
INamedTypeSymbol enumSymbol = semanticModel.GetDeclaredSymbol(enumNode) as INamedTypeSymbol;
As I mentioned in the previous Roslyn related articles, INamedTypeSymbol
is very similar to System.Reflection.Type
. You can get almost any information about the C# type from INamedTypeSymbol
object.
Extension method DOMCodeGenerator.CreateEnumExtensionCode()
generates and returns all the code:
generatedCode = enumSymbol.CreateEnumExtensionCode();
The rest of the code in charge of the code generation is located within NP.DOMGenerator
project.
As I mentioned before - I am using Roslyn only for analysis - for code generation, I am using CodeDOM, since it is less verbose and makes more sense.
There are two major static
classes under NP.DOMGenerator
project - RoslynExtensions
- for Roslyn analysis and DOMCodeGenerator
- for generating the code using CodeDOM functionality.
Conclusion
In this article, we've described creating a VS 2017 extension for generating sub-enumeration (akin to sub-classes).
History
- 22nd February, 2015: Initial version
- 14th November, 2017: Changed the code to work under Visual Studio 2017 with up to date Roslyn libraries