Nuget-пакет это не только архив с переиспользуемыми сборками, но и контент с target-скриптами, которые задают поведение MsBuild при сборке приложения. Это дает нам возможность рассматривать nuget-пакет в качестве самостоятельного объекта, у которого есть состояние и поведение.
А раз у нас есть объект, то что мешает попробовать посмотреть на работу с ним со стороны объектно-ориентированной парадигмы? Давайте попробуем применить для nuget-пакетов один из основных принципов ООП наследование.
Предположим, вам нужно сделать nuget-пакет на основе уже существующего, немного изменив его поведение.
Для примера, рассмотрим nuget-пакет с драйверами базы данных DB2: IBM.Data.DB2.Core.
Этот пакет обладает специальным поведением. При сборке использующего его проекта происходит копирование unmanaged-библиотеки с драйверами в результирующую папку билда: в проект-потребитель пакета в процессе сборки автоматически добавляются контент-ссылки на файлы unmanaged-библиотек с драйверами.
Предположим, что у вас есть фреймворк, который использует ORM, например NHibernate. Он содержит специальную обвязку драйвера DB2, которая нужна для обеспечения доступа к этой базе данных через этот ORM: ViennaNET.Orm.DB2.Win.
Сборки внутри
ViennaNET.Orm.DB2.Win
конечно же
ссылаются на IBM.Data.DB2.Core
. Но если вы в каком-то
новом проекте подключите только пакет
ViennaNET.Orm.DB2.Win
, то
IBM.Data.DB2.Core
автоматически не подключится. То
есть, драйвера, необходимые для работы с БД, не появятся, если вы
не используете менеджер пакетов, который разрешает транзитивные
зависимости. В качестве примера такого менеджера можно упомянуть
Paket.Здесь есть несколько решений.
- Предложить потребителю пакета
ViennaNET.Orm.DB2.Win
учитывать транзитивную зависимость от пакетаIBM.Data.DB2.Core
. Это можно сделать вручную или с помощью автоматизированного инструмента типа ранее упоминаемого мною Paket. - Полностью продублировать в проекте пакета
ViennaNET.Orm.DB2.Win
содержимое и поведение пакетаIBM.Data.DB2.Core
. - Реализовать наследование содержимого и поведения пакета
IBM.Data.DB2.Core
в пакетеViennaNET.Orm.DB2.Win
.
Решения 1 и 2 лежат на поверхности, поэтому здесь я опишу только решение 3. Оно позволит упростить использование пакета конечным потребителем и снизит возможные риски копирайта, которые могут возникнуть при дублировании содержимого одного проекта в другом.
После того, как вы скачаете и разархивируете пакет
IBM.Data.DB2.Core
, вы увидите примерно такую структуру
каталога:В папке
build
находится папка clidriver
с
unmanaged-драйвером DB2 и targets-скрипт
IBM.Data.DB2.Core.targets
, который будет выполняться
при сборке проекта, использующего этот пакет. Скрипт содержит
следующие инструкции:
<Project xmlns="http://personeltest.ru/away/schemas.microsoft.com/developer/msbuild/2003"> <ItemGroup> <None Include="$(MSBuildThisFileDirectory)clidriver\**" > <Link>clidriver\%(RecursiveDir)%(FileName)%(Extension)</Link> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </None> </ItemGroup></Project>
В скрипте говорится, что в проект, который использует данный пакет, на все файлы из папки
build\clidriver
нужно рекурсивно
добавить контент-ссылки. Это необходимо, чтобы в процессе билда
файлы с драйверами были помещены в итоговую папку сборки
проекта.После сборки проекта в нем появиться папка с контент-ссылками. Причем это именно ссылки на файлы находящиеся в папке пакета
IBM.Data.DB2.Core
, в папку проекта они не
копируются:Теперь, хотелось бы повторить такое же поведение для пакета
ViennaNET.Orm.DB2.Win
в отношении его потребителя.Тут пришлось изрядно порыть MSDN и StackOverflow. Как мне кажется, в итоге сформировалось решение достойное того, чтобы поведать о нем общественности.
Для реализации в вашем пакете наследования контента и поведения от другого пакета достаточно выполнить следующие действия в файле проекта вашего пакета.
- В ссылке на базовый пакет вам нужно указать атрибут
GeneratePathProperty="true"
. Это позволит создать переменную процесса сборки с именем, соответствующим имени пакета. Она нам нужна, так как будет указывать путь к папке с содержимым этого пакета. В самом имени символ '.' будет заменен на символ '_'.
<ItemGroup> <PackageReference Include="IBM.Data.DB2.Core" Version="1.3.0.100" GeneratePathProperty="true" /></ItemGroup>
- Добавить контент, ссылающийся на контент базового пакета.
Символы '**' обозначают рекурсивное использование всех файлов.
<Content Include="$(PkgIBM_Data_DB2_Core)\build\clidriver\**" Pack="true" PackagePath="build\clidriver" PackageCopyToOutput="false" />
- Добавить контент, ссылающийся на target-скрипт базового
проекта. При этом, чтобы его выполнил MsBuild, необходимо
переименовать target-скрипт по имени текущего проекта. Это можно
сделать в атрибуте
PackagePath
.
<Content Include="$(PkgIBM_Data_DB2_Core)\build\*.targets" Pack="true" PackagePath="build\$(TargetName).targets" PackageCopyToOutput="false" />
На этом всё.
Теперь при сборке пакета
ViennaNET.Orm.DB2.Win
в него
будут добавлены файлы unmanaged-драйвера DB2 и target-скрипт из
пакета IBM.Data.DB2.Core
. Это позволит при подключении
пакета ViennaNET.Orm.DB2.Win
к новому проекту
обеспечить размещение драйверов DB2 в папке сборки так, как это
происходило бы при подключении пакета
IBM.Data.DB2.Core
.Общий вид файла проекта будет выглядеть так:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> <RuntimeIdentifiers>win-x64</RuntimeIdentifiers> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> <OutputPath>..\Bin</OutputPath> <DocumentationFile>..\Bin\ViennaNET.Orm.DB2.Win.xml</DocumentationFile> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <OutputPath>..\Bin</OutputPath> <DocumentationFile>..\Bin\ViennaNET.Orm.DB2.Win.xml</DocumentationFile> </PropertyGroup> <ItemGroup> <ProjectReference Include="..\ViennaNET.Orm\ViennaNET.Orm.csproj" /> <ProjectReference Include="..\ViennaNET.Protection\ViennaNET.Protection.csproj" /> </ItemGroup> <ItemGroup> <PackageReference Include="IBM.Data.DB2.Core" Version="1.3.0.100" GeneratePathProperty="true" /> </ItemGroup> <ItemGroup> <Content Include="$(PkgIBM_Data_DB2_Core)\build\clidriver\**" Pack="true" PackagePath="build\clidriver" PackageCopyToOutput="false" /> <Content Include="$(PkgIBM_Data_DB2_Core)\build\*.targets" Pack="true" PackagePath="build\$(TargetName).targets" PackageCopyToOutput="false" /> </ItemGroup></Project>
Таким способом вы сможете обеспечить наследование контента и поведения в ваших nuget-пакетах от других nuget-пакетов.
Практическую реализацию решения с наследованием контента и поведения nuget-пакетов можно посмотреть в проекте ViennaNET на GitHub.