Click here to Skip to main content
15,881,173 members
Articles / Programming Languages / C#

Implementing Enumeration Inheritance using Roslyn based VS Extension

Rate me:
Please Sign up or sign in to vote.
4.93/5 (21 votes)
14 Nov 2017CPOL6 min read 39.5K   292   25   12
Describe VS2015 extension for generating sub-enumerations (akin to sub-classes)

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:

C#
public enum BaseEnum
{
    A,
    B
} 

Also it has DerivedEnum enumeration:

C#
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:

C#
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:

C#
static void Main(string[] args)
{
    // convert from based to derived value
    DerivedEnum derivedEnumConvertedValue = BaseEnum.A.ToDerivedEnum();
    Console.WriteLine("Derived converted value is " + derivedEnumConvertedValue);

    // convert from derived to base value
    BaseEnum baseEnumConvertedValue = DerivedEnum.B.ToBaseEnum();
    Console.WriteLine("Derived converted value is " + baseEnumConvertedValue);

    // throw a conversion exception trying to convert from derived to 
    // base value, because such value does not exist in the base enumeration:
    DerivedEnum.C.ToBaseEnum();
}  

It will print:

C#
Derived converted value is A
Base converted value is B  

And then it will throw an exception containing the following message:

C#
"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:

C#
public enum BaseEnum
{
    A,
    B
}  

Take a look at the file "DerivedEnum.cs":

C#
[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:

Image 1

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:

Image 2

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:

C#
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:

C#
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:

C#
static void Main(string[] args)
{
    // convert from based to derived value
    DerivedEnum derivedEnumConvertedValue = BaseEnum.A.ToDerivedEnum();
    Console.WriteLine("Derived converted value is " + derivedEnumConvertedValue);

    // convert from derived to base value
    BaseEnum baseEnumConvertedValue = DerivedEnum.B.ToBaseEnum();
    Console.WriteLine("Base converted value is " + baseEnumConvertedValue);

    // throw a conversion exception trying to convert from derived to 
    // base value, because such value does not exist in the base enumeration:
    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:

C#
public enum BaseEnum
{
    A,
    B = 20
}  

and _DerivedEnum.C to 22:

C#
enum _DerivedEnum
{
    C = 22,
    D,
    E
}  

We'll get the following generated code:

C#
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:

C#
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:

Image 3

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":

C#
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:

C#
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:

C#
public int Generate
(
    string wszInputFilePath,
    string bstrInputFileContents,
    string wszDefaultNamespace,
    IntPtr[] rgbOutputFileContents,
    out uint pcbOutput,
    IVsGeneratorProgress pGenerateProgress
)
{
    byte[] codeBytes = null;

    try
    {
        // generate the code
        codeBytes = GenerateCodeBytes(wszInputFilePath, bstrInputFileContents, 
                                      wszDefaultNamespace);
    }
    catch(Exception e)
    {
        // add the error to the "Error List"
        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.

C#
protected byte[] GenerateCodeBytes
(string filePath, string inputFileContent, string namespaceName)
{
    // set generatedCode to empty string
    string generatedCode = "";

    // get the id of the .cs file for which we are 
    // trying to generate code based on the class'es DeriveEnum attribute
    DocumentId docId =
        TheWorkspace
            .CurrentSolution
            .GetDocumentIdsWithFilePath(filePath).FirstOrDefault();

    if (docId == null)
        goto returnLabel;

    // get the project that contains the file for which 
    // we are generating the code.
    Project project = TheWorkspace.CurrentSolution.GetProject(docId.ProjectId);
    if (project == null)
        goto returnLabel;

    // get the compilation of the project. 
    Compilation compilation = project.GetCompilationAsync().Result;

    if (compilation == null)
        goto returnLabel;

    // get the document based on which we 
    // generate the code
    Document doc = project.GetDocument(docId);

    if (doc == null)
        goto returnLabel;

    // get the Roslyn syntax tree of the document
    SyntaxTree docSyntaxTree = doc.GetSyntaxTreeAsync().Result;
    if (docSyntaxTree == null)
        goto returnLabel;

    // get the Roslyn semantic model for the document
    SemanticModel semanticModel = compilation.GetSemanticModel(docSyntaxTree);
    if (semanticModel == null)
        goto returnLabel;

    // get the document's class node
    // Note that we assume that the top class within the 
    // file is the one that we want to generate the wrappers for
    // It is better to make it the only class within the file. 
    EnumDeclarationSyntax enumNode =
        docSyntaxTree.GetRoot()
            .DescendantNodes()
            .Where((node) => (node.CSharpKind() == 
             SyntaxKind.EnumDeclaration)).FirstOrDefault() as EnumDeclarationSyntax;

    if (enumNode == null)
        goto returnLabel;

    // get the enum type.
    INamedTypeSymbol enumSymbol = 
          semanticModel.GetDeclaredSymbol(enumNode) as INamedTypeSymbol;
    if (enumSymbol == null)
        goto returnLabel;

    // get the generated code
    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:

C#
// get the id of the .cs file for which we are 
// trying to generate wrappers based on the class'es Wrapper Attributes
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:

C#
Project project = TheWorkspace.CurrentSolution.GetProject(docId.ProjectId);  

And from Project, we get its compilation:

C#
Compilation compilation = project.GetCompilationAsync().Result;  

We also get the Roslyn Document from the project:

C#
Document doc = project.GetDocument(docId);

From the current document, we get its Roslyn SyntaxTree:

C#
SyntaxTree docSyntaxTree = doc.GetSyntaxTreeAsync().Result;

From the Roslyn Compilation and SyntaxTree, we get the semantic model:

C#
SemanticModel semanticModel = compilation.GetSemanticModel(docSyntaxTree);  

We also get the first enumeration syntax declared in the file from the SyntaxTree:

C#
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:

C#
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:

C#
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect AWebPros
United States United States
I am a software architect and a developer with great passion for new engineering solutions and finding and applying design patterns.

I am passionate about learning new ways of building software and sharing my knowledge with others.

I worked with many various languages including C#, Java and C++.

I fell in love with WPF (and later Silverlight) at first sight. After Microsoft killed Silverlight, I was distraught until I found Avalonia - a great multiplatform package for building UI on Windows, Linux, Mac as well as within browsers (using WASM) and for mobile platforms.

I have my Ph.D. from RPI.

here is my linkedin profile

Comments and Discussions

 
GeneralMy vote of 5 Pin
Sergey Alexandrovich Kryukov23-Feb-15 14:19
mvaSergey Alexandrovich Kryukov23-Feb-15 14:19 
QuestionWorking around .NET and language limitation Pin
Sergey Alexandrovich Kryukov23-Feb-15 14:17
mvaSergey Alexandrovich Kryukov23-Feb-15 14:17 
AnswerRe: Working around .NET and language limitation Pin
Nick Polyak23-Feb-15 15:06
mvaNick Polyak23-Feb-15 15:06 
GeneralRe: Working around .NET and language limitation Pin
Sergey Alexandrovich Kryukov23-Feb-15 17:30
mvaSergey Alexandrovich Kryukov23-Feb-15 17:30 
AnswerRe: Working around .NET and language limitation Pin
Nick Polyak23-Feb-15 15:15
mvaNick Polyak23-Feb-15 15:15 
GeneralWhat about Roslyn Pin
Sergey Alexandrovich Kryukov23-Feb-15 17:32
mvaSergey Alexandrovich Kryukov23-Feb-15 17:32 
GeneralRe: What about Roslyn Pin
Nick Polyak24-Feb-15 2:54
mvaNick Polyak24-Feb-15 2:54 
GeneralRe: What about Roslyn Pin
Sergey Alexandrovich Kryukov24-Feb-15 2:57
mvaSergey Alexandrovich Kryukov24-Feb-15 2:57 
GeneralMy vote of 5 Pin
Thomas Maierhofer (Tom)23-Feb-15 1:31
Thomas Maierhofer (Tom)23-Feb-15 1:31 
GeneralRe: My vote of 5 Pin
Nick Polyak23-Feb-15 2:57
mvaNick Polyak23-Feb-15 2:57 
GeneralMy vote of 5 Pin
Tomas Takac23-Feb-15 0:36
Tomas Takac23-Feb-15 0:36 
GeneralRe: My vote of 5 Pin
Nick Polyak23-Feb-15 2:56
mvaNick Polyak23-Feb-15 2:56 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.