
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://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.
Taritsyn
Если ваш пакет полностью ориентирован на .NET Core или .NET 5, то вы можете использовать для развертывания нативных сборок механизм на базе каталогов RID. Вы просто копируете нативные сборки в директорию
runtimes/win-x64/native
пакета, гдеwin-x64
– это RID целевой платформы, а среда выполнения сама загружает их в память процесса.Можно также создавать гибридные решения, которые будут поддерживать сразу несколько сценариев развертывания нативных сборок:
.targets
или.props
.Install.ps1
иUninstall.ps1
.В качестве примера можете посмотреть исходный код пакета JavaScriptEngineSwitcher.ChakraCore.Native.win-x64.
bcwd Автор
Да, хорошее дополнение. На примере с пакетами по DB2, вижу что их делят по целевым системам Windows/Linux/OSX, возможно экономят на размере пакета:
www.nuget.org/packages/IBM.Data.DB2.Core
www.nuget.org/packages/IBM.Data.DB2.Core-lnx
www.nuget.org/packages/IBM.Data.DB2.Core-osx
Taritsyn
Если хотите больше узнать по этой теме, то рекомендую прочитать мой пост «Let's make the Microsoft ClearScript.V8 fully cross-platform» в репозитории проекта Microsoft ClearScript.
bcwd Автор
Спасибо, действительно всеобъемлющее решение, а я уже привык ориентироваться исключительно на .NET Core ну и в перспективе на .NET 5 (когда выйдет LTS)