Click here to Skip to main content
13,189,884 members (51,429 online)
Click here to Skip to main content
Add your own
alternative version

Stats

18.6K views
23 bookmarked
Posted 22 Feb 2015

Implementing Enumeration Inheritance using Roslyn based VS Extension

, 26 Feb 2015
Rate this:
Please Sign up or sign in to vote.
Describe VS2015 extension for generating sub-enumerations (akin to sub-classes)

Implementing Enumeration Inheritance using Roslyn based VS Extension

Important Note

I would really appreciate if you leave me comment 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.

As in previous articles, I am using VS 2015 preview. Once VS 2015 full version is released, I plan to update my article with the corresponding code.

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)
{
    // 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:

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 combinding the values from "super" and "sub" enumerations.

If you look at the properies 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)
{
    // 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:

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 is 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 se 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 using the 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 implementds 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
    {
        // 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.

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:

// 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 Rosly 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 2015 extension for generating sub-enumeration (akin to sub-classes)

License

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

Share

About the Author

Nick Polyak
Architect AWebPros
United States United States
I have 15 years of experience developing enterprise software, starting from C++ and Java on UNIX and moving towards C# on Windows platforms.
I am fascinated by the new .NET technologies especially WPF, Silverlight and LINQ.
Recently I decided to make a move and start my own contracting consulting and mentoring company AWebPros.
I can be contacted via my web site awebpros.com or through my blog at nickssoftwareblog.com

You may also be interested in...

Pro
Pro

Comments and Discussions

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

Permalink | Advertise | Privacy | Terms of Use | Mobile
Web04 | 2.8.171016.2 | Last Updated 26 Feb 2015
Article Copyright 2015 by Nick Polyak
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid