В этой главе описаны наиболее важные преимущества сборок (assemblies), включая упаковку компонентов .NET и управление их версиями. Вы также узнаете, как создавать однофайловые и многофайловые сборки с помощью утилиты Assembly Generation (al.exe), создавать совместно используемые сборки, применяя утилиту Strong Name (sn.exe), просматривать глобальный кэш сборки с помощью Assembly Cache Viewer (shfu-sion.dll) и как утилита Global Assembly Cache (gacutil.exe) помогает манипулировать кэшем сборки. В завершение мы рассмотрим несколько примеров и увидим, как обстоят дела с управлением версиями и как сборки наряду с политикой управления версиями .NET избавляют от "ужасов DLL".
В главе 16 сборки описаны как физические файлы, состоящие из нескольких РЕ-файлов (portable executable), генерируемых компилятором .NET. В контексте той главы это определение было приемлемым, но на самом деле сборки сложнее. Вот более полное определение: сборка является упаковкой, включающей декларацию (manifest), один или несколько модулей и (не обязательно) один или несколько ресурсов. Сборки позволяют семантически группировать функциональные единицы в единый файл для решения задач развертывания, контроля версий и сопровождения.
Все РЕ-файлы, работающие в исполняющей среде .NET, входят в ту или иную сборку.
Компилируя приложение компилятором С#, вы на самом деле создаете сборку. Вы
можете и не отдавать себе в этом отчета, если вы намеренно не пытались разместить
несколько модулей в единой сборке или задействовать преимущества характерных
для сборок функций, таких как управление версиями. Однако важно понимать, что
каждый раз, компонуя ЕХЕ или DLL (с помощью переключателя /t:librаrу), вы
создаете сборку с декларацией, которая предоставляет описание сборки для исполняющей
среды .NET. Кроме того, вы можете создать модуль (применив переключатель /tmodule),
который на самом деле является DLL (с расширением .netmodule) без декларации.
Иначе говоря, хотя логически это все равно DLL, она не принадлежит к сборке
и должна быть добавлена к сборке с помощью переключателя /odd-module при
компиляции приложения или утилиты Assembly Generation. Ниже вы увидите, как
это делается (см. раздел "Создание сборок").
Декларацию сборки можно хранить двумя способами. Если вы скомпилировали автономное приложение или DLL, декларация будет встроена в результирующий РЕ. Это называется однофайловой сборкой (single-file assembly). Можно сгенерировать и многофайловую сборку (multifile assembly), у которой декларация существует в виде отдельной сущности в составе сборки или прикрепляется к одному из модулей сборки.
Определение сборки в немалой степени зависит от того, как вы ее используете. С точки зрения клиента, сборка — это совокупность именованных модулей определенных версий, экспортируемых типов и (не обязательно) ресурсов. С точки зрения создателя сборки, это средство упаковки связанных модулей, типов и ресурсов, при этом экспортируется только то, что клиент может использовать. Отсюда следует, что именно декларация определяет соответствие между деталями реализации сборки и тем, что предназначено для использования клиентом. В декларации хранятся:
Использование сборок приносит разработчику массу преимуществ, включая возможности упаковки, развертывания и управления версиями.
Одним из преимуществ упаковки нескольких модулей в единый физический файл — повышение производительности. Когда вы создаете и развертываете приложение, применяя многофайловые сборки, исполняющей среде .NET требуется загрузить только нужные модули. В результате уменьшается размер рабочего пространства приложения.
Наименьшей единицей развертывания в .NET является сборка. Вы можете создать модуль .netmodule с помощью переключателя /t:module, но для его развертывания его нужно включить в состав сборки. Кроме того, хотя так и тянет сказать, что сборки являются средствами развертывания приложений, формально это не верно. Более точно рассматривать сборки в .NET как форму развертывания классов (во многом похожую на DLL в Win32), где единое приложение может состоять из многих сборок.
Поскольку сборки являются самоописывающимися, проще всего их развернуть, скопировав в нужную папку. Когда же вы попытаетесь запустить приложение, содержащееся в сборке, декларация проинформирует .NET о модулях, которые хранятся в сборке. Кроме того, сборка содержит ссылки на все внешние сборки, нужные приложению.
Чаще всего для развертывания применяют закрытые сборки (private assemblies), т. е. сборки, которые копируются в папки, не используемые совместно. Как задать закрытую сборку? Это установлено по умолчанию и происходит автоматически. Если же вы захотите явным образом сделать сборку совместно используемой (shared assembly), читайте ниже раздел "Создание совместно используемых сборок".
Еще одно крупное преимущество сборок — встроенное управление версиями, знаменующее собой конец "ада DLL". Речь идет о ситуации, когда одно приложение затирает DLL, нужную другому, обычно записывая поверх нее более раннюю версию этой же DLL, выводя из строя первое приложение. Хотя формат файлов ресурсов Win32 допускает наличие ресурса, определяющего версию, ОС не контролирует версии. Ответственность за это целиком и полностью несут прикладные программисты. Для решения этой проблемы в декларацию включена информация о версии сборки, а также список сборок, на которые ссылается данная сборка, с указанием их версий. Такая архитектура позволяет исполняющей среде .NET гарантировать поддержку политик управления версиями и продолжение функционирования приложения даже после установки в систему более новых, несовместимых версий совместно используемых DLL. Управление версиями описано в соответствующем разделе.
Если вы создаете DLL с переключателем /t:library, то не сможете добавить ее к другой сборке. Дело в том, что компилятор автоматически генерирует декларацию для этой DLL, которая поэтому сама является сборкой. Чтобы увидеть это в действии, рассмотрим пример. У нас есть DLL (ModulelServer.cs), у которой есть фиктивный тип Module!Server.
11 Module1Server.cs
// компоновка со следующими переключателями командной строки:
// esc /t:library Module1Server.cs
public class ModulelServer
{
}
Клиентский код в дальнейшем ссылается на эту DLL (Module 1-Client.cs):
// Module1ClientApp.cs
// компоновка со следующими переключателями командной строки:
// esc Module1ClientApp.cs /г:Module1Server.dll
using System;
using System.Diagnostics;
using System.Reflection;
class ModulelClientApp {
public static void Main() {
Assembly DLLAssembly = Assembly.6etAssembly(typeof (ModulelServer));
Console.WriteLine("Module1Server.dll Assembly Information");
Console.WriteLine("\t" + DLLAssembly);
Process p = Process.GetCurrentProcessO; string AssemblyName = p.ProcessName + ".exe"; Assembly ThisAssembly = Assembly.LoadFrom(AssemblyName); Console.WriteLine("Module1Client.exe Assembly Information"); Console.WriteLine("\t" + ThisAssembly); } }
А теперь допустим, что вы скомпоновали эти два модуля, используя переключатели:
esc /t:library ModulelServer.cs
esc ModulelClientApp.cs /r:Module1Server.dll
Если запустить программу, мы увидим информацию, доказывающую, что ЕХЕ и DLL существуют в своих собственных отдельных сборках:
Module1Server.dll Assembly Information
ModulelServer, Version=0.0.0.0, Culture=neutral,
PublicKeyToken=null Module1Client.dll Assembly Information
ModulelClient, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
На самом деле, если бы вы изменили модификатор доступа класса ModulelServer с public на internal, клиентский код перестал бы компилироваться, так как по определению модификатор доступа internal указывает, что модифицируемый тип доступен для другого кода только из той же сборки.
Создание сборок из нескольких модулей
Поместить оба модуля из нашего примера в одну сборку можно двумя способами. Во-первых, можно изменить переключатели, которые используются с компилятором:
// Module2Server.cs
// компоновка со следующими переключателями командной строки:
// esc /t:module Module2Server.cs
internal class Module2Server
{
}
Обратите внимание, что теперь мы можем указать модификатор доступа internal, при использовании которого класс доступен лишь коду в пределах этой сборки.
// Hodule2ClientApp.es
// компоновка со следующими переключателями командной строки:
// esc /addmodule:Module2Server.netmodule Module2ClientApp.cs
using System;
using System.Diagnostics;
using System.Reflection;
class Module2ClientApp {
public static void MainQ {
Assembly DLLAssembly =
Assembly.GetAssembly(typeof(Module2Server)); Console.WriteLine("Module1Server.dll Assembly Information"); Console.WriteLine("\t" + DLLAssembly);
Process p = Process.GetCurrentProcessO; string AssemblyName = p.ProcessName + ".exe"; Assembly ThisAssembly = Assembly.LoadFrom(AssemblyName); Console.WriteLine("Module1Client.dll Assembly Information"); Console.WriteLine("\t" + ThisAssembly); } }
Обратите внимание на то, как компонуются Module2Server.cs и Modu-le2Client.exe:
esc /t:module Module2Server.cs
esc /addmodule:Nodule2Server.netmodule Module2Client.cs
Прежде всего вы должны удалить переключатель /г, поскольку он служит только для ссылки на сборки, а сейчас оба модуля будут находиться в одной сборке. Далее вы должны вставить переключатель /addmodule, который указывает компилятору, какие модули добавить к создаваемой сборке.
При компоновке и запуске приложения теперь получатся следующие результаты:
Module1Server.dll Assembly Information
Module2Client, Version=0.0.0.0, Culture=neutral,
PublicKeyToken=null Module1Client.dll Assembly Information
Module2Client, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
Сборку можно создать и с помощью Assembly Generation. Эта утилита принимает один или несколько файлов — модулей .NET (содержащими MSIL) или файлов ресурсов и образов. На выходе получается файл с декларацией сборки. Так, чтобы объединить несколько DLL, которые вы хотели бы распространять как единый файл и контролировать их версии как у единого целого, обратитесь к услугам Assembly Generation. Если ваши DLL называются A.DLL, В.DLL и С.DLL, то вы создадите композитную сборку, используя приложение al.exe следующим образом:
al /out:COMPOSITE.DLL A.DLL В.DLL С. DLL
Совместное использование сборок имеет место, когда сборка предназначена для работы нескольких приложений и важно управление версиями (об управлении версиями см. следующий раздел). Для совместно используемой сборки необходимо создать совместно используемое имя (shared name, также известное как строгое имя, strong name) с помощью утилиты Strong Name из .NET SDK. Применение совместно используемых имен дает четыре главных преимущества.
Первым шагом на пути создания строгого имени является применение утилиты Strong Name для создания файла ключа для сборки. Для этого укажите переключатель -k и имя выходного файла, в котором будет содержаться ключ. Здесь мы сделаем файл ключа InsideCSharp.key:
sn -k InsideCSharp.key
После выполнения этой команды вы получите подтверждающее сообщение:
Key pair written to InsideCSharp.key
Теперь добавим к исходному файлу атрибут assembly:AssemblyKeyFile. Здесь я создал другой простой набор файлов, чтобы проиллюстрировать, как это делается:
// Module3Server.cs
// компоновка со следующими переключателями командной строки:
// esc /t:module Module3Server.cs
internal class ModuleSServer
{
}
// Module3ClientApp.cs
// компоновка со следующими переключателями командной строки:
// esc /addmodule:Module3Server.netmodule Module3ClientApp.cs
using System;
using System.Diagnostics;
using System.Reflection;
[assembly:AssemblyKeyFile("InsideCSharp.key")]
class ModuleSClientApp <
public static void MainQ {
Assembly DLLAssembly =
Assembly. GetAssembly(typeof(Module3Server)); Console.WriteLine("Module1Server.dll Assembly Information"); Console.WriteLine("\t" + DLLAssembly);
Process p = Process.GetCurrentProcessQ;
string AssemblyName = p.ProcessName + ".exe";
Assembly ThisAssembly = Assembly.LoadFrom(AssemblyName);
Console.WriteLine("Module1Client.dll Assembly Information"); Console.WriteLine("\t" + ThisAssembly); } }
Как видите, конструктор атрибута assembly-.AssemblyKey File принимает имя файла ключа, сгенерированного утилитой Strong Name, и позволяет задавать пару ключей, которая будет использована для создания строгого имени сборки. Важно понимать, что это атрибут уровня сборки. Поэтому он может быть помещен в любой из ее файлов и не прикреплен к какому-либо классу. Однако принято помещать этот атрибут сразу после операторов using перед определениями классов.
Запустив свое приложение, обратите внимание на значение Public-Key Token сборки. В двух предыдущих примерах оно было равно null, так как эти сборки считались закрытыми. Но теперь сборка определена как совместно используемая, и поэтому с ней ассоциирован маркер открытого ключа.
Module3Server.dll Assembly Information
ModuleSClient, Version=0.0.0.0, Culture=neutral,
PublicKeyToken=6ed7cefOc0065911 Module3Client.dll Assembly Information
ModuleSClient, Version=0.0.0.0, Culture=neutral,
PublicKeyToken=6ed7cefOc0065911
Объект Assembly, экземпляр которого мы создали для этой демонстрационной сборки, указывает, что она является совместно используемой. Но как узнать, какие сборки в системе под управлением .NET совместно используемые? Ответ — в глобальном кэше сборок.
Работа с глобальным кэшем сборок
В .NET есть кэш кода под названием глобальный кэш сборок (global assembly cache). Он выполняет три главных функции.
Взглянем на кэш, чтобы увидеть установленные в данный момент совместно используемые сборки. Из Microsoft Explorer откройте папку c:\winnt\ assembly. Для просмотра информации о сборках .NET представляет утилиту Assembly Cache Viewer (shfusion.dll). Она позволяет просматривать такую информацию о сборке, как номер версии, маркер открытого ключа и даже была ли сборка прекомпилирована.
Еще одно средство просмотра кэша — утилита Global Assembly Cache — позволяет решать некоторые задачи, задавая в командной строке такие (взаимоисключающие) ключи.
gacutil -i HelloWorld.DLL
Скоро вы увидите как добавить сборку Module3Client в кэш с помощью этого ключа.
gacutil -u HelloWorld
gacutil -u HelloWorld, ver=1,0,0,0
Итак, вы создали файл открытого ключа и назначили его сборке. Теперь добавим эту сборку в кэш. Для этого наберите в командной строке:
gacutil -i Module3ClientApp.exe
Если все идет нормально, вы должны получить подтверждение: Assembly successfully added to the cache
ПРИМЕЧАНИЕ В некоторых ранних бета-выпусках
.NET я обратил внимание на одну проблему: при просмотре папки c:\winnt\assembly
shfusion.dll не работала. Причиной этого было то, что shfusion.dll не регистрировалась
должным образом. Если это случилось в вашей системе, откройте командную строку
и выполните команду "regsvr32 shfusion.dll" из папки c:\winnt\Microsoft.net\framework\vJCHf
, где XXX — это номер версии .NET Framework, с которой вы работаете.
Очевидно, имя папки изменится перед началом поставок .NET, поскольку я работал
с бета-выпуском. Тогда найдите файл shfusion.dll, и используйте эту папку. Здесь
я использовал папку, представляющую мою текущую версию .NET: c:\winnt\microsoft.net\framework\
vl.0.2615>regsvr32 shfusion.dll.
А сейчас вы можете вызвать команду gacutil -/ для просмотра сборок в кэше и поиска ModuleSClient. Для этого можно использовать утилиту Assembly Cache Viewer, что мы и сделаем. Открыв кэш из Windows Explorer (C:\Winnt\Assembly или C:\Windows\Assembly), вы увидите в списке сборку ModuleSClient. Щелкните ее правой кнопкой и выберите Properties, что позволит узнать значение открытого ключа, номер версии и местоположение сборки на жестком диске. Ваш открытый ключ будет отличаться от моего, но он должен совпадать со значением, которое выводится при исполнении приложения ModuleSClientApp.
Декларация сборки содержит номер версии и список всех сборок (и связанную с ними информацию о версиях), на которые ссылается эта сборка. Номера версий подразделяются на четыре сегмента и принимают вид:
<старшая версияхмладшая версия><компоновка><ревизия>
В период выполнения это работает так: на основе информации о номере версии .NET решает, какую версию данной сборки использовать с приложением. Как вы вскоре увидите, поведение по умолчанию — оно называется политикой управления версиями — заключается в следующем: после установки приложения .NET будет автоматически использовать самую последнюю версию сборок, на которые ссылается это приложение, если номера их старших и младших версий совпадают. Это поведение по умолчанию позволяют изменить файлы конфигурации.
Управление версиями относится только к совместно используемым сборкам (для
закрытых сборок оно не требуется) и является, пожалуй, наиболее важным фактором,
определяющим решение о создании совместно используемых сборок. Поэтому рассмотрим
примеры, иллюстрирующие, как все это действует и как работать с версиями сборки.
Мой Код представляет собой упрощенный вариант примера управления версиями, поставляемого с .NET SDK. В него не входит материал Windows Forms, поскольку я хочу сосредоточиться на управлении версиями и действиях, осуществляемых для этого исполняющей средой.
У меня есть два исполняемых файла, представляющих два пакета бухгалтерского учета, — Personal и Business. Оба приложения обращаются к общей совместно используемой сборке Account. Класс Account может лишь сообщать свою версию, позволяя убедиться, что наши приложения обращаются именно к предназначенной для них версии класса Account. В этой связи в пример входят несколько версий класса Account, что поможет вам уяснить, как работает управление версиями при использовании политики по умолчанию и как с помощью XML устанавливается связь между приложением и конкретной версией сборки.
Для начала создайте папку Accounting, а в ней — пару ключей, которые будут использованы всеми версиями класса Account. Для этого в командной строке папки Accounting наберите:
sn /k account.key
Создайте в папке Accounting еще одну папку — Personal, а в ней — файл Personal.cs:
// Accounting\Personal\Personal.cs using System;
class PersonalAccounting {
public static void Main() {
Console.WriteLine
("PersonalAccounting calling Account.PrintVersion"); Account. PrintVersionO; } -}
В папке Personal создайте папку Account 1000, где будет располагаться первая версия класса Account нашего примера. Затем в папке Account 1000 создайте файл Account.cs:
// Accounting\Personal\Account1000\Account.cs
using System;
using System.Reflection;
[assembly:AssemblyKeyFile("..\\. .\\Account.key")] [assembly:AssemblyVersion("1.0.0.0")] public class Account <
public static void PrintVersionO
{
Console.WriteLine
("This is version 1.0.0.0 of the Account class"); >
}
Как видите, я использую атрибуты AssemblyeKeyFile и Assembly Version, чтобы указать компилятору С# ранее созданную пару ключей и явно задать версию класса Account. Теперь скомпонуйте DLL Account:
esc /t:library account.cs
Созданный класс Account нужно добавить к глобальному кэшу сборки: gacutil -i Account.dll
Если хотите, можете проверить, что сборка Account находится в кэше сборки. Теперь перейдите в папку Personal и скомпонуйте приложение, например, так:
esc Personal.cs /r:Account1000\Account.dll
При запуске этого приложения получится следующий результат:
PersonalAccounting calling Account.PrintVersion This is version 1.0.0.0 of the Account class
До сих пор мы не сделали ничего нового. Но посмотрим, что произойдет при установке еще одного приложения, использующего более новую версию класса Account.
Создайте в папке Accounting папку Business, а в ней — папку Ассо-untlOOl для представления новой версии класса Account. Этот класс находится в файле Account.cs и выглядит почти идентично предыдущей версии.
// Accounting\Business\Account10Q1\Account,cs
using System;
using System.Reflection;
[assembly:AssemblyKeyFile("..\\..\\Account.key")] [assembly:AssemblyVersion("1.0.0.1")] public class Account
{
public static void PrintVersionQ {
Console.WriteLine
("This is version 1.0.0.1 of the Account class"); } }
Как и прежде, скомпонуйте эту версию класса Account командами: esc /t:library Account, cs gacutil -i Account.dll
На этом этапе вы должны видеть в глобальном кэше сборки две версии класса Account. Теперь создайте (в папке Accounting\Business) файл Business, cs:
// Accounting\Business\Business.cs using System;
class PersonalAccounting {
public static void Main() {
Console.WriteLine
("BusinessAccounting calling Account.PrintVersion"); Account. PrintVersionO; } }
Скомпонуйте приложение Business командой: esc business.cs /r:Account1001\Account.dll
Запуск этого приложения даст следующие результаты. Это подтверждает тот факт, что приложение Business использует версию 1.0.0.1 сборки Account.
BusinessAccounting calling Account.PrintVersion This is version 1.0.0.1 of the Account class
А теперь запустите приложение Personal и посмотрите, что происходит! PersonalAccounting calling Account.PrintVersion This is version 1.0.0.1 of the Account class
Оба приложения, Personal и Business, обращаются к последней версии сборки
Account*. Почему? Так все и должно быть при использовании подхода QFE
(Quick Fix Engineering) и политики управления версиями .NET по умолчанию.
QFE и политика управления версиями по умолчанию
Обновления Quick Fix Engineering, или текущие исправления, — это внеплановые исправления, рассылаемые при возникновении серьезных проблем. Поскольку текущие исправления, как правило, не модифицируют интерфейс кода, вероятность неблагоприятного влияния на клиентский код минимальна. Поэтому политика управления версиями по умолчанию подразумевает автоматическую связь всего клиентского кода с новой, "исправленной" версией, если только у приложения нет файла конфигурации, явно связывающего приложение с конкретной версией сборки. Новая версия сборки считается QFE, если меняется только часть номера версии, являющаяся номером ревизии (revision).
Создание файла конфигурации для безопасного режима
Большую часть времени такая политика управления версиями по умолчанию может быть замечательной, ну а если потребуется, чтобы приложение Personal работало только с поставляемой с ним версией сборки? Здесь в дело вступают настроечные XML-файлы. У них те же имена, что и у файлов приложений, и находятся они в той же папке. При исполнении приложения файл конфигурации считывается, после чего .NET через содержащиеся в нем тэги XML указывает, какую версию сборки использовать.
Чтобы задать постоянное использование для приложения поставляемой с ним версии сборки, укажите "безопасный" ("safe") режим как желаемый режим связывания для приложения. В обиходе это иногда называется "перевести приложение в безопасный режим". Чтобы проверить это, создайте файл PersonalAccounting.cfg в папке Accounting/Personal и занесите в него следующие строки. Обратите внимание на тэг <AppBindingMode >.
<?xml version ="1.0"?> ,
<Configuration>
<BindingMode>
<AppBindingMode Mode="safe"/>
</BindingMode>
</Configuration>
Запустив приложение Personal, вы увидите такую выходную информацию:
PersonalAccounting calling Account.PrintVersion