Логин:
Пароль:
Меню
Главная Программы Исходники Электронные книги FAQ C# Online учебник Android Java FAQ Android, Java Помощь сайту Наши баннеры О нас Связь с администрацией
Облако тегов

Показать все теги
Архив
  • Февраль 2018
  • Декабрь 2017
  • Май 2016
  • Апрель 2015
  • Март 2015
  • Март 2013
  • Март 2012

  • Получение метаданных с помощью отражения

    ГЛАВА 16
    Получение метаданных с помощью отражения





    В главе 2 я говорил, что компилятор генерирует переносимый в Win32 исполняемый модуль (portable executable, РЕ), состоящий главным образом из MSIL-кода и метаданных. Одна из очень мощных возможностей .NET позволяет вам писать код, чтобы обращаться к метаданным приложения посредством отражения (reflection). Если просто, то отражение — это способность получать информацию о типе в период выполнения. В этой главе будет описан API отражения и способы его использования при обработке модулей и типов, входящих в сборки для получения различных характеристик типа, определенных в период разработки. Вы также познакомитесь с некоторыми усложненными способами применения отражения, такими как динамический вызов методов и использование информации о типе (через позднее связывание) и даже создание и исполнение MSIL-кода в период выполнения!

     
    Иерархия API отражения



    API отражения .NET представляет собой иерархию классов (рис. 16-1), определенную в пространстве имен System.Reflection. Эти классы позволяют логически прослеживать информацию о сборке и о типе. Вы можете начинать с любого места в иерархии в зависимости от конкретных потребностей при разработке приложения.


    В эти классы входит изрядная доля функциональности. Я не буду перечислять
    все методы и поля каждого класса, а дам обзор ключевых классов и затем покажу
    пример; иллюстрирующий функциональность, которую вам скорее всего потребуется
    включить в свои приложения.


     


    Рис. 16.1. Часть иерархии классов .NET System.Reflection.

     
    Класс Type



    Центральное место в отражении занимает System. Type — абстрактный класс, который представляет тип в CTS (Common Type System) и позволяет запрашивать имя типа, включающий его модуль и пространство имен, а также является ли этот тип размерным или ссылочным.

     
    Получение информации о типе экземпляра



    Вот как можно получить объект Туре для экземпляра типа inf.



    using System;



    using System.Reflection;



    class TypeObjectFromlnstanceApp



    {



    public static void Main(string[] args) {



    int 1=6;



    Type t = i.GetTypeO; Console.WriteLine(t.Name); } }

     
    Получение информации о типе из имени



    Кроме получения объекта Туре из переменной, можно создавать этот объект на основании имени типа. Другими словами, иметь экземпляр типа не обязательно. Вот как это сделать для типа System.Int32:



    using System;



    using System.Reflection;



    class TypeObjectFromNameApp {



    public static void Main(string[] args) {



    Type t = Type.GetType("System.Int32"); Console.WriteLine(t.Name); } }



    При вызове метода Type.GetType нельзя использовать псевдонимы С#, так как этот метод используется всеми языками. Поэтому вы не можете указывать применяемый в С# псевдоним int вместо System.Int32.

     
    Опрос типов



    Класс System. Type также позволяет запрашивать практически все атрибуты типа, включая модификатор доступа, является ли тип вложенным, его СОМ-свойства и т. д. Взгляните на этот код, использующий несколько обычных и демонстрационных типов:



    using System;



    using System.Reflection;



    interface Demolnterface



    {



    }



    class DemoAttr : System.Attribute



    {



    >



    enum DemoEnum



    {



    >



    public class DemoBaseClass



    {



    }



    public class DemoDerivedClass : DemoBaseClass



    < }



    class DemoStruct



    {



    }



    class QueryTypesApp {



    public static void QueryType(string typeName)



    {



    try



    {



    Type type = Type.GetType(typeName);



    Console.Writel_ine("Type name: {0}", type.FullName);



    Console. Writel_ine( "\tHasElementType = {0}",



    type.HasElementType);



    Console.Writel_ine("\tIsAbstract = {0}", type.IsAbstract); Console.WriteLine("\tIsAnsiClass = {0}",



    type.IsAnsiClass);



    Console.WriteLine("\tIsArray = {0}", type.IsArray); Console.WriteLine("\tIsAutoClass = {0}",



    type.IsAutoClass); Console.WriteLine("\tIsAutoLayout = {0}",



    type.IsAutoLayout);



    Console.WriteLine("\tIsByRef = {0}", type.IsByRef); Console.WriteLine("\tIsClass = {0}", type.IsClass); Console.WriteLine("\tIsCOMObject = {0}",



    type.IsCOMObject); Console.WriteLine("\tIsContextful = {0}",



    type.IsContextful);



    Console.WriteLine("\tIsEnum = {0}", type.IsEnum); Console.WriteLine("\tIsExplicitLayout = {0}",



    type.IsExplicit Layout);



    Console.WriteLine("\tlslmport = {0}", type.lslmport); Console.WriteLine("\tlslnterface = {0}",



    type.lslnterface); Console.WriteLine("\tIsLayoutSequential = {0}",



    type.IsLayoutSequential); Console.WriteLine("\tIsMarshalByRef = {0}",



    type.IsMarshalByRef); Console.WriteLine("\tIsNestedAssembly = {0}",



    type.IsNestedAssembly); Console.WriteLine("\tIsNestedFamANDAssem = {0}",



    type.IsNestedFamANDAssem); Console.WriteLine("\tIsNestedFamily = {0}",



    type.IsNestedFamily); Console.WriteLine("\tIsNestedFamORAssera = {0}",



    type.IsNestedFamORAssem); Console.WriteLine("\tIsNestedPrivate = {0}",



    type.IsNestedPrivate); Console.WriteLine("\tIsNestedPublic = {0}",



    type.IsNestedPublic); Console.WriteLine("\tIsNotPublic = {0}",



    type.IsNotPublic); Console.WriteLine("\tIsPointer = {0}",



    type.IsPointer); Console.WriteLine("\tIsPrimitive = {Q}",



    type.IsPrimitive); Console.WriteLine("\tIsPublic = {O}",



    type.IsPublic); Console.WriteLine("\tIsSealed = {0}",



    type.IsSealed); Console.WriteLine("\tIsSeriallzable = {0}",



    type.IsSerializable); Console.WriteLine("\tIsServicedComponent = {0}",



    type.IsServicedComponent); Console.WriteLine("\tIsSpecialName = {0}",



    type.IsSpecialName); Console.WriteLine("\tIsUnicodeClass = {0}",



    type.IsUnicodeClass); Console.WriteLine("\tIsValueType = {0}",



    type.IsValueType); }



    catch(System.NullReferenceException) {



    Console.WriteLine



    ("{0} is not a valid type", typeName); } }



    public static void Main(string[] args) <



    Que ryType("System.Int32");



    QueryType("System.Int64");



    QueryType("System.Type");



    QueryTypeC'DemoAttr"); QueryType("DemoEnum");



    QueryType("DemoBaseClass"); Que ryType("DemoDe rivedClass"); QueryTypeC'DemoStruct"); } }

     
    Работа со сборками и модулями



    Подробнее мы рассмотрим сборки в главе 18. Пока нам достаточно знать, что сборка (assembly) — это физический файл, состоящий из нескольких РЕ-файлов .NET. Главное преимущество сборки в том, что она позволяет семантически группировать функциональность, что облегчает развертывание приложения и управление его версиями. Представлением сборки периода выполнения в .NET является класс Assembly (он ^ке — вершина иерархии объектов отражения).


    Класс Assembly позволяет выполнить много действий, в том числе:

    • просмотреть типы сборки;
    • перечислить модули сборки;
    • определить идентификационную информацию, такую как имя и местоположение
      физического файла сборки;
    • изучить информацию о версиях и защите;
    • получить точки входа сборки.

     
    Просмотр типов сборки



    Для последовательного просмотра всех типов данной сборки вам нужно лишь создать экземпляр объекта Assembly и запросить массив Types для этой сборки, например:



    using System;



    using System.Diagnostics;



    using System.Reflection;



    class DemoAttr : System.Attribute



    < >



    enum DemoEnum



    {



    }



    class DemoBaseClass



    {



    }



    class DemoDerivedClass : DemoBaseClass {



    }



    class DemoStruct



    {



    }



    class GetTypesApp {



    protected static string GetAssemblyName(string[] args)



    {



    string assemblyName;



    if (0 == args.Length) {



    Process p = Process.GetCurrentProcessQ;



    assemblyName = p.ProcessName + ".exe"; } else



    assemblyName = args[0]; return assemblyName; }



    public static void Main(string[] args) <



    string assemblyName = GetAssemblyName(args);



    Console.WriteLine("Loading info for " + assemblyName); Assembly a = Assembly.LoadFrom(assemblyName); Type[] types = a.GetTypes(); foreach(Type t in types) {



    Console.WM.teLine("\nType information for: " +



    t.FullName); Console.WriteLine("\tBase class = " +



    t.BaseType.FullName); } } }


    ПРИМЕЧАНИЕ Для запуска кода, которому
    требуется проверка защиты (например, кода, использующего API отражения) в интрасети,
    вам придется изменить политику, например, с помощью утилиты Code Access Security
    Policy (caspol.exe). Вот как это сделать:



    caspol -addgroup 1.2 -url "file://somecomputer/someshare/*"



    SkipVerification


    В этом примере на основе URL запускаемого кода предоставляются дополнительные
    права доступа, в данном случае SkipVerification. Вы также можете изменить
    политику для всего кода в данной зоне или только для конкретной сборки в зависимости
    от его электронной подписи или даже хэша. Для просмотра аргументов утилиты caspol.exe
    наберите в командной строке caspol - ? или обратитесь к онлайновой документации
    MSDN.



    Первая часть метода Main не очень-то интересна: этот код определяет, передали ли вы приложению имя сборки. Если нет, то для определения имени исполняемого в текущий момент приложения используется статический метод GetProcessName класса Process.



    После этого вы начнете понимать, как легко решать большинство задач с помощью отражения. Легче всего создать экземпляр объекта Assembly, вызвав Assembly. LoadFrom. Этот метод принимает единственный аргумент — строку, представляющую имя физического файла, который вы хотите загрузить. Вызванный после этого метод Assembly.GetTypes возвращает массив объектов Туре. Теперь у нас есть объект, описывающий каждый отдельный тип в целой сборке! В результате приложение выводит имя своего базового класса.



    Вот выходная информация, полученная при запуске этого приложения с указанием в качестве параметра файла gettypes.exe или без аргументов вообще:



    Loading info for GetTypes.exe



    Type information for: DemoAttr Base class = System.Attribute



    Type information for: DemoEnum Base class = System.Enum



    Type information for: DemoBaseClass Base class = System.Object



    Type information for: DemoDerivedClass



    Base class = DemoBaseClass



    Type information for: DemoStruct Base class = System.Object



    Type information for: AssemblyGetTypesApp Base class = System.Object

     
    Вывод списка модулей сборки



    Хотя большинство приложений в этой книге состоят из одного модуля, вы можете создавать сборки, состоящие из нескольких модулей. Получать имена модулей из объекта Assembly можно двумя способами. Первый — это запрос массива всех модулей. При этом осуществляется проход по всем модулям и вывод любых нужных данных. Второй способ — получение информации о конкретном модуле.



    Чтобы проиллюстрировать циклический опрос модулей, нужно создать сборку, состоящую из более чем одного модуля. Я сделаю это путем перевода Get Assembly Name в собственный класс и размещения этого класса в отдельном файле с именем Assembly Utils.netmodule, например, так: using System.Diagnostics;



    namespace MyUtilities {



    public class AssemblyUtils {



    public static string GetAssembiyName(string[] args) {



    string assemblyName;



    if (0 == args.Length) <



    Process p = Process.GetCurrentProcessQ;



    assemblyName = p.ProcessName + ".exe"; } else



    assemblyName = args[0];



    ч



    return assemblyName; } } }



    После этого создаем модуль netmodule командой: esc /target:module AssemblyUtils.es



    Переключатель /target-.module затавляет компилятор генерировать модуль, который позже будет включен в сборку. Приведенная команда создаст файл AssemblyUtils.netmodule. Подробнее о создании сборок и модулей см. главу 18.



    А сейчас я хочу создать вспомогательный модуль, чтобы у нас был предмет для отражения. Приведенное ниже приложение будет использовать класс AssemblyUtils. Обратите внимание на оператор using, в котором указано пространство имен My Utilities.



    using System;



    using System.Reflection;



    using MyUtilities;



    class GetModulesApp {



    public static void Hain(string[] args)



    {



    string assemblyNarae = AssemblyUtils.GetAssemblyName(args);



    Console.WriteLine("Loading info for " + assemblyName); Assembly a = Assembly.LoadFrom(assemblyName);



    Module[] modules = a.GetModulesO; foreach(Module m in modules) <



    Console.WriteLine("Module: " + m.Name); } } }



    Чтобы скомпилировать это приложение и добавить к сборке модуль AssemblyUtils.netmodule, нужно задействовать переключатели командной строки:



    esc /addmodule:AssemblyUtils.netmodule GetModules.es



    Теперь у нас есть сборка из двух модулей. Чтобы увидеть это, запустите приложение. При этом получатся такие результаты:



    Loading info for GetModulesApp.exe



    Module: GetModulesApp.exe



    Module: AssemblyUtils.netmodule



    Как стало ясно из кода, я просто создал экземпляр объекта Assembly и вызвал его метод GetModules. Затем я циклически обработал возвращенный массив и вывел имя каждого из них.

     
    Позднее связывание и отражение



    Несколько лет назад я работал в IBM Multimedia division над продуктом IBM/World Book Multimedia Encyclopedia. Нам нужно было создать приложение, позволяющее пользователю настраивать коммуникационные протоколы для работы с серверами World Book. Это решение должно было быть динамическим, чтобы пользователи могли непрерывно добавлять в систему и удалять из нее различные протоколы (TCP/IP, IGN, CompuServ и т. д.). Однако это приложение должно было «знать», какие протоколы присутствуют в системе, чтобы пользователь мог выбрать конкретный протокол для настройки и применения. Мы решили создать DLL со специальными расширениями и установить их в папку приложения. Когда у пользователя возникало желание увидеть список установленных протоколов, приложение вызывало \Ут32-функцию LoadLibrary чтобы загрузить DLL, а затем — функцию GetProcAddress, чтобы получить указатель на нужную функцию. Это замечательный пример позднего связывания в традиционном \Ут32-программировании, когда компилятор ничего не знает об этих вызовах во время компоновки. Как вы увидите из следующего примера, в .NET ту же задачу позволяет решить класс Assembly, отражение типов и новый класс — Activator.



    Чтобы заставить все шестеренки этого механизма крутиться, создадим абстрактный класс CommProtocol. Я определю этот класс в его собственной DLL. В результате его могут совместно использовать несколько DLL, которым потребуются производные от него классы (обратите внимание на параметры командной строки в комментарии к коду). // CommProtocol.cs



    // Компоновка со следующими переключателями командной строки: // esc /t:library commprotocol.es public abstract class CommProtocol {



    public static string DLLMask = "CommProtocob.dll"; public abstract void DisplayNameO; }



    А сейчас я создам две отдельные DLL, каждая из которых реализует какой-то коммуникационный протокол и содержит класс, производный от абстрактного класса CommProtocol. Заметьте: обе должны ссылаться на CommProtocol.dll при компиляции. Вот DLL для протокола IGN:



    // CommProtocolI6N.cs



    // Компоновка со следующими переключателями командной строки: // esc /t:libгагу CommProtocolIGN.cs /г:CommProtocol.dll using System;



    public class CommProtooolIGN : CommProtocol {



    public override void DisplayNameO {



    Console.WriteLine("This is the IBM Global Network"); > }



    А вот DLL для TCP/IP: // CommProtocolTcpIp.es



    // Компоновка со следующими переключателями командной строки: // esc /t:library CommProtocolTcpIp.es /r:CommProtocol.dll using System;



    public class CommProtocolTcpIp : CommProtocol {



    public override void DisplayNameO {



    Console.WriteLine("This is the TCP/IP protocol"); } }



    Посмотрим, насколько легко осуществляется динамическая загрузка, поиск типа и создание его экземпляра, а также вызов одного из его методов (кстати, на прилагаемом к книге диске есть командный файл BuildLateBmdmg.cmd, который также осуществляет компоновку всех этих файлов):



    using System;



    using System.Reflection;



    using System.10;



    class LateBindingApp {



    public static void Main()



    <



    string[] fileNames = Directory.GetFiles



    (Environment.CurrentDirectory,



    CommProtocol.DLLMask); foreach(string fileName in fileNames) {



    Console.WriteLine("Loading DLL '{0}'", fileName);



    Assembly a = Assembly.LoadFrom(fileName);



    Type[] types = a.GetTypes(); foreach(Type t in types) {



    if (t.IsSubclassOf(typeof(CommProtocol)))



    {



    object о = Activator.Createlnstance(t);



    Methodlnfo mi = t.GetMethod("DisplayName");



    Console.Write("\t"); mi.Invoke(o, null); }



    else {



    Console.WriteLine("\tThis DLL does not have " + "CommProtocol-derived class defined"); } } > > }



    Сначала с помощью класса System.IO. Directory мы находим все DLL в данной папке по маске CommProtocol*. dll. Метод Directory.GetFiles вернет массив объектов типа string, представляющий имена файлов, соответствующих критерию поиска. Затем я могу задействовать цикл/огеасй для циклической обработки массива, вызывая метод Assembly. LoadFrom, о котором вы узнали выше. После создания сборки для данной DLL я циклически опрашиваю все типы сборки, вызывая метод Type.SubClassOf, чтобы определить, есть ли у сборки тип, производный от CommProtocol. Я предполагаю, что если будет найден хоть один такой тип, то я работаю с нужной DLL. Найдя сборку, у которой есть тип, производный от CommProtocol, я создаю экземпляр объекта Activator и передаю его конструктору объект type. Как вы, вероятно, догадались, класс Activator используется для динамического создания, или активизации, типа.



    Затем я использовал метод Ту ре.Get Method, чтобы создать объект Methodlnfo, указав имя метода DisplayName. Сделав это, я могу задействовать метод Invoke объекта Methodlnfo, передавая ему активизированный тип, и — пожалуйста! — метод DLL DisplayName вызван!

     
    Создание и исполнение кода в период выполнения



    Вы уже видели, как отражать типы в период выполнения, осуществлять позднее связывание с кодом и динамическое исполнение кода. Сделаем следующий шаг в логической последовательности — рассмотрим создание кода «на лету». При создании типов в период выполнения используется пространство имен System.Reflection.Emit. С помощью классов из этого пространства имен можно определять сборку в памяти, создавать для нее модули, определять для модулей новые типы (и их члены) и даже генерировать коды операций MSIL для реализации прикладной логики.



    Несмотря на чрезвычайную простоту кода этого, примера, я отделил серверный код — DLL, содержащую класс, который создает метод Hello-World, — от клиентского кода, приложения, которое создает экземпляр класса, генерирующего код, и вызывает его метод Hello World (обратите внимание на переключатели компилятора в комментариях). Объяснение этого кода DLL приводится ниже:



    using System;



    using System.Reflection;



    using System.Reflection.Emit;



    namespace ILGenServer {



    public class CodeGenerator {



    public CodeGenerator() {



    // Получить текущий currentDomain. currentDomain = AppDomain.CurrentDomain;



    // Создать сборку в текущем currentDomain. assemblyName = new AssemblyNameQ; assemblyName.Name = "TempAssembly";



    assemblyBuilder =



    currentDomain.DefineDynamicAssembly



    (assemblyName, AssemblyBuilderAccess.Run);



    // Создать в этой сборке модуль moduleBuilder = assemblyBuilder.DefineOynamicModule ("TempModule");



    // Создать тип в этой сборке typeBuilder = moduleBuilder.DefineType ("TempClass", TypeAttributes.Public); // Добавить к типу член (метод) methodBuilder = typeBuilder.DefineMethod ("HelloWorld", MethodAttributes.Public, null,null);



    // Генерировать MSIL.



    msil = methodBuilder.GetlLGeneratorO;



    rasil.EmitWriteLine("Hello World11);



    msil.Emit(OpCodes.Ret);



    // Последний шаг: создание типа, t = typeBuilder.CreateTypeO;



    >



    AppDomain currentDomain; AssemblyName assemblyName; AssemblyBuilder assemblyBuilder; ModuleBuilder moduleBuilder; TypeBuilder typeBuilder; MethodBuilder roethodBuilder; ILGenerator msil; object o;



    Type t; public Type T {



    get {



    return this.t; } > } }



    Сначала мы создали экземпляр объекта AppDomain из текущей области (в главе 17 вы увидите, что прикладные области в функциональном плане похожи на процессы Win32). После этого мы создали экземпляр объекта Assembly Name. Класс AssemblyName подробно описан в главе 18, а вообще этот класс используется диспетчером кэша сборки для получения информации о ней. Получив текущую прикладную область и инициализированное имя сборки, вызываем метод AppDomain.Defme-DynamicAssembly, чтобы создать новую сборку. Заметьте, что два передаваемые нами аргумента являются именем сборки и описанием режима доступа к ней. Assembly Builder Access.Run указывает, что сборка может быть исполнена из памяти, но не может быть сохранена. Метод AppDomain. Define DynamicAssembly возвращает объект Assembly Builder, который мы затем приводим к объекту Assembly. На этом этапе у нас есть полнофункциональная сборка в памяти. Теперь нам нужно создать ее временный модуль и его тип.



    Начнем с вызова метода Assembly. DefineDynamicModule для получения объекта ModuleBuilder. Получив этот объект, мы вызываем его метод DefineType, чтобы создать объект ТуреВтШег, передавая методу имя типа («TempClass») и используемые для его определения атрибуты (TypeAttri-butes.Public). Теперь, имея объект TypeBuilder, можно создать член любого нужного нам типа. В данном случае мы создаем новый метод с помощью метода TypeBuilder. DefineMethod.



    В результате получаем совершенно новый тип TempClass с встроенным методом HelloWorld. Теперь все, что нам осталось сделать, — это решить, какой код поместить в этот метод. Для этого код создает экземпляр объекта ILGenerator с помощью метода MethodBuilder.GetlLGenerator и вызывает различные методы IWenerator для записи в метод MSIL-кода.



    Здесь мы можем использовать стандартный код типа Console. WriteLine с помощью различных методов IWenerator или генерировать коды операций MSIL, используя метод IWenerator. Emit. Метод IWenerator.Emit в качестве единственного аргумента принимает поле члена класса OpCodes, непосредственно связанное с кодом операции MSIL.



    В завершение вызываем метод TypeBuilder.CreateType. Это действие всегда должно выполняться последним, после того как вы определили члены нового типа. Далее мы получаем объект Type для нового типа с помощью метода Type.GetType. Этот объект будет храниться в члене-переменной клиентского приложения, которому он впоследствии понадобится.


    Теперь все, что осталось сделать клиенту, — это получить член Type класса
    CodeGenerator, создать экземпляр активатора, экземпляр объекта Methodlnfo
    из типа и затем вызвать метод. Вот код, выполняющий эти действия, к которому
    добавлена небольшая проверка на наличие ошибок, чтобы быть уверенным, что все
    работает как надо:



    using System;



    using System.Reflection;



    using ILGenServer;



    public class ILGenClientApp {



    public static void Main() {



    Console.WriteLine("Calling DLL function to generate " + "a new type and method in memory..."); CodeGenerator gen = new CodeGeneratorQ;



    Console.WriteLine("Retrieving dynamically generated



    type..."); Type t = gen.T; if (null != t) {



    Console.WriteLine("Instantiating the new type...");



    object о = Activator.Createlnstance(t);



    Console.WriteLine("Retrieving the type's " +



    "HelloWorld method...");



    Methodlnfo helloWorld = t.GetMethod("HelloWorld"); if (null != helloWorld) {



    Console.WriteLine("Invoking our dynamically " +



    "created HelloWorld method..."); helloWorld.Invoke(o, null); }



    else <



    Console.WriteLine("Could not locate " + "HelloWorld method"); } >



    else {



    Console.WriteLin&("Could not access Type from server"); > > >



    Скомпоновав и исполнив это приложение, вы увидите:



    Calling DLL function to generate a new type and method in memory...



    Retrieving dynamically generated type...



    Instantiating the new type...



    Retrieving the type's HelloWorld method...



    Invoking our dynamically created HelloWorld method...



    Hello World



    Подведем итоги



    Отражение позволяет получить информацию о типе в период выполнения. API отражения обеспечивает выполнение таких действий, как циклическая обработка модулей и типов сборки, получение различных характеристик типа периода разработки. Более сложные задачи, решаемые с помощью отражения, включают динамический вызов методов и использование типов (через позднее связывание) и даже создание и исполнение MSIL-кода в период выполнения.

    Популярные статьи
    Online Учебник по С# Бесплатная альтернатива Microsoft Visual Studio .NET для новичков Язык программирования C# 2005 и платформа .NET 2.0 ASP.NET MVC 4 Framework с примерами на C# 5.0 для профессионалов. 4-е изд. Язык программирования C# 5.0 и платформа .NET 4.5
    Реклама