Введение

В С# программировании необходима возможность расширять простое уравнение. Например, когда нужно использовать перечисления из библиотеки dll, которые невозможно изменить в коде, но и в то же время нужно использовать дополнительные значения, которых нет в библиотеке. Решить проблему можно при помощи системы Roslyn, основанной на расширении VS для создания отдельных файлов. Данный подход похож на моделирование множественного наследования в шаблоне "Implementing Adapter Pattern" и "Imitating Multiple Inheritance" в C# с использованием системы Roslyn, основанной на VS Extension Wrapper Generator.

 

Roslyn


 

Формулировка проблематики

Обратите внимание на схему EnumDerivationSample. Она содержит негенерированный код, большая часть которого будет сгенерирована позже. Схема содержит тип перечисления BaseEnum:

 

public enum BaseEnum

{

       A,

       B

}

 

Также в ней присутствует тип перечисления DerivedEnum

 

 public enum DerivedEnum

{

       A,

       B,

       C,

       D,

       E

}

 

В перечислении DerivedEnum перечень значений А и В такие же, как в перечислении BaseEnum.

Файл DerivedEnum.cs также содержит статический класс DeriveEnumExtensions для конвертации BaseEnum в DerivedEnum и наоборот:

 

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;

       }

}

 

Преобразование значений BaseEnum в DerivedEnum всегда проходит успешно, в то время как преобразование в обратном направлении может быть проблематичным. Например, если значение DerivedEnum больше 1 (значение BaseEnum.B – наибольшее значение в типе перечисления BaseEnum). Функция Program .Main (...) используется для тестирования функциональных характеристик:

 

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();

}

 

Будет выводиться:

 

Derived converted value is A

Base converted value is B

 

И тогда появится такое сообщение:

 

"Cannot convert DerivedEnum.C value to BaseEnum type, since its integer value 2 is greater than the max value 1 of BaseEnum enumeration."

 

Использование Visual Studio Extension для формирования наследования перечислений.

Установите расширение NP.DeriveEnum.vsix Visual Studio из папки VSIX, дважды кликнув на файл. Откройте схему EnumDerivationWithCodeGenerationTest. Тип ее перечислений такой же, как и в предыдущей схеме:

 

public enum BaseEnum

{

       A,

       B

}

 

Посмотрите на файл "DerivedEnum.cs":

 

[DeriveEnum(typeof(BaseEnum), "DerivedEnum")]

enum _DerivedEnum

{

       C,

       D,

       E

}

 

Он определяет такой тип перечисления _DerivedEnum с атрибутом: [DeriveEnum (TypeOf (BaseEnum), "DerivedEnum")]. Атрибут определяет "супер-перечисления" (BaseEnum) и названия производного перечисления ("DerivedEnum»). Обратите внимание, что поскольку частичные перечисления не поддерживаются в C#, нам придется создать новый тип перечисления, объединив значение от "супер" до "суб" перечислений.

Посмотрите характеристики файла DerivedEnum.cs, его "специальные инструменты (Custom Tool)" уже содержатся в "DeriveEnumGenerator":

 

Характеристики файла

 

Теперь откройте файл DerivedEnum.cs в Visual Studio, попробуйте изменить его (скажем, добавив пробел) и сохраните его. Вы увидите, что сразу будет создан файл DerivedEnum.extension.cs:

 

Пример изменения файла

 

Этот файл содержит тип перечисления DerivedEnum, который объединяет все поля перечислений BaseEnum и _DerivedEnum. Для начала убедитесь, что они имеют одинаковое имя и полное значение, а также имеют соответствующие поля в исходных перечислениях:

 

public enum DerivedEnum

{

       A,

       B,

       C,

       D,

       E,

}

 

Расширение VS также формирует статический класс DerivedEnumExtensions, содержащий методы преобразования между суб и супер перечислениями:

 

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;

       }

}

 

Если использовать метод Program.Main (...), как и в предыдущем образце, получим достаточно похожий результат:

 

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();

}

 

Вы можете указать значение поля как в суб, так и в супер перечислениях. Генератор кода достаточно развит для того, чтобы выдавать правильный код. Например, если мы поставим значение BaseEnum.B 20:

 

public enum BaseEnum

{

       A,

       B = 20

}

 

И _DerivedEnum.C 22:

 

enum _DerivedEnum

{

       C = 22,

       D,

       E

}

 

Получим такой генерируемый код:

 

public enum DerivedEnum

{

       A,

       B = 20,

       C = 22,

       D,

       E,

}

 

Метод расширения ToBaseEnum(...) также будет обновляться так, чтобы показывать исключение только тогда, когда мы пытаемся увеличить целое значение области DerivedEnum до 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;

}

 

Обратите внимание, что изменив значение первого поля суб-перечисления на меньшее или равное последнему полю супер-перечисления, генерация кода не осуществится, и это состояние будет отображаться, как ошибка. Например, попробуйте изменить значение _DerivedEnum.C на 20 и сохранить изменения. Файл DerivedEnum.extension.cs  будет отображаться в списке ошибок.

 

Список ошибок

 

Примечания о введении генератора объектного кода.

Код ввода кода генерирования содержится в схеме NP.DeriveEnum. Основная схема NP.DeriveEnum была создана с помощью шаблона "Visual Studio Package" (также, как это было сделано при Implementing Adapter Pattern и Imitating Multiple Inheritance в C# с использованием системы Roslyn, основанной на VS Extension Wrapper Generator).

Нам пришлось добавить пакеты Roslyn и MEF2, чтобы использовать функции Roslyn при таких командах, как "Nu Get Package Manager Console":

 

Install - Package Microsoft.CodeAnalysis - Pre

Install - Package Microsoft.Composition

 

Класс main генератора называется DeriveEnumGenerator. Он вводит интерфейс IVsSingleFileGenerator. У интерфейса есть два метода - DefaultExtension(...) и Generate(...). Метод DefaultExtension(...) позволяет разработчику указать расширение генерируемого файла:

 

public int DefaultExtension(out string pbstrDefaultExtension)

{

       pbstrDefaultExtension = ".extension.cs";

 

       return VSConstants.S_OK;

}

 

Метод Generate(...) позволяет разработчику указать код, который входит в состав созданного файла:

 

 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;

}

 

В данном случае генерация кода получена за счет метода GenerateCodeBytes (...).

 

 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;

}

 

Метод Generate (...) имеет доступ к C # в качестве одного из параметров. Мы используем такой способ, чтобы получить Id документа в системе Roslyn:

 

DocumentId docId =

TheWorkspace

.CurrentSolution

.GetDocumentIdsWithFilePath(filePath).FirstOrDefault();

 

Из документа Id можно получить идентификатор схемы, используя dockId.

Из схемы Id получаем Roslyn Project от Rosly Workspace:

 

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

 

Из Project получаем следующее:

 

Compilation compilation = project.GetCompilationAsync().Result;

 

Также получаем Roslyn Document:

 

Document doc = project.GetDocument(docId);

 

С данного документа получаем Roslyn SyntaxTree:

 

SyntaxTree docSyntaxTree = doc.GetSyntaxTreeAsync().Result;

 

С компиляции Roslyn и SyntaxTree образовывается семантическая модель:

 

SemanticModel semanticModel = compilation.GetSemanticModel(docSyntaxTree);

 

Вы также получите синтаксис перечисления в файле SyntaxTree:

 

EnumDeclarationSyntax enumNode =

docSyntaxTree.GetRoot()

.DescendantNodes()

.Where((node) = > (node.CSharpKind() == SyntaxKind.EnumDeclaration)).FirstOrDefault() as EnumDeclarationSyntax;

 

Наконец, из SemanticModel и EnumerationDeclarationSyntax Вы можете вытянуть INamedTypeSymbol, соответствующий перечислению:

 

INamedTypeSymbol enumSymbol = semanticModel.GetDeclaredSymbol(enumNode) as INamedTypeSymbol;

 

INamedTypeSymbol очень похож на System.Reflection.Type. Практически всю информацию про тип С# можно получить от объекта INamedTypeSymbol.

Метод расширения DOMCodeGenerator. CreateEnumExtensionCode() генерирует и возвращает весь код.

 

generatedCode = enumSymbol.CreateEnumExtensionCode();

 

Другая часть кода, отвечающая за код генерации, входит в состав NP.DOMGenerator. Система Roslyn используется только для анализа, для генерации кода используется CodeDOM, так как он меньше по объему и удобнее.

Есть два основных статических класса в программе NP.DOMGenerator: RoslynExtensions - для анализа Roslyn и DOMCodeGenerator - для генерирования кода, используя функции CodeDOM.

Источник: http://www.codeproject.com/Articles/879129/Implementing-Enumeration-Inheritance-using-Roslyn