company_banner

Наследование в Nuget-пакетах

    image

    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.

    Здесь есть несколько решений.

    1. Предложить потребителю пакета ViennaNET.Orm.DB2.Win учитывать транзитивную зависимость от пакета IBM.Data.DB2.Core. Это можно сделать вручную или с помощью автоматизированного инструмента типа ранее упоминаемого мною Paket.
    2. Полностью продублировать в проекте пакета ViennaNET.Orm.DB2.Win содержимое и поведение пакета IBM.Data.DB2.Core.
    3. Реализовать наследование содержимого и поведения пакета IBM.Data.DB2.Core в пакете ViennaNET.Orm.DB2.Win.

    Решения №1 и №2 лежат на поверхности, поэтому здесь я опишу только решение №3. Оно позволит упростить использование пакета конечным потребителем и снизит возможные риски копирайта, которые могут возникнуть при дублировании содержимого одного проекта в другом.

    После того, как вы скачаете и разархивируете пакет IBM.Data.DB2.Core, вы увидите примерно такую структуру каталога:

    image

    В папке 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, в папку проекта они не копируются:

    image

    Теперь, хотелось бы повторить такое же поведение для пакета ViennaNET.Orm.DB2.Win в отношении его потребителя.

    Тут пришлось изрядно порыть MSDN и StackOverflow. Как мне кажется, в итоге сформировалось решение достойное того, чтобы поведать о нем общественности.

    Для реализации в вашем пакете наследования контента и поведения от другого пакета достаточно выполнить следующие действия в файле проекта вашего пакета.

    1. В ссылке на базовый пакет вам нужно указать атрибут GeneratePathProperty="true". Это позволит создать переменную процесса сборки с именем, соответствующим имени пакета. Она нам нужна, так как будет указывать путь к папке с содержимым этого пакета. В самом имени символ '.' будет заменен на символ '_'.

      <ItemGroup>
          <PackageReference Include="IBM.Data.DB2.Core" 
              Version="1.3.0.100" 
              GeneratePathProperty="true" />
      </ItemGroup>
      
    2. Добавить контент, ссылающийся на контент базового пакета. Символы '**' обозначают рекурсивное использование всех файлов.

      <Content Include="$(PkgIBM_Data_DB2_Core)\build\clidriver\**" 
          Pack="true" PackagePath="build\clidriver" 
          PackageCopyToOutput="false" />
      
    3. Добавить контент, ссылающийся на 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.
    Райффайзенбанк
    Развеиваем мифы об IT в банках

    Похожие публикации

    Комментарии 6

      +3

      Если ваш пакет полностью ориентирован на .NET Core или .NET 5, то вы можете использовать для развертывания нативных сборок механизм на базе каталогов RID. Вы просто копируете нативные сборки в директорию runtimes/win-x64/native пакета, где win-x64 – это RID целевой платформы, а среда выполнения сама загружает их в память процесса.


      Можно также создавать гибридные решения, которые будут поддерживать сразу несколько сценариев развертывания нативных сборок:


      1. Для .NET Core и .NET 5 использовать каталоги RID.
      2. Для проектов .NET Framework использовать MSBuild-скрипты .targets или .props.
      3. Для сайтов ASP.NET 4.X использовать PowerShell-скрипты Install.ps1 и Uninstall.ps1.

      В качестве примера можете посмотреть исходный код пакета JavaScriptEngineSwitcher.ChakraCore.Native.win-x64.

        0
        Да, хорошее дополнение. На примере с пакетами по 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
          +1
          На примере с пакетами по DB2, вижу что их делят по целевым системам Windows/Linux/OSX, возможно экономят на размере пакета:

          Если хотите больше узнать по этой теме, то рекомендую прочитать мой пост «Let's make the Microsoft ClearScript.V8 fully cross-platform» в репозитории проекта Microsoft ClearScript.

            +1
            Спасибо, действительно всеобъемлющее решение, а я уже привык ориентироваться исключительно на .NET Core ну и в перспективе на .NET 5 (когда выйдет LTS)
        +2
        Но если вы в каком-то новом проекте подключите только пакет ViennaNET.Orm.DB2.Win, то IBM.Data.DB2.Core автоматически не подключится.

        Есть специальная папка /buildTransitive, если вы при билде положите туда файлы *.props и *.targets, упакуете это всё в nuget пакет A, потом сделаете другой пакет B, который зависит от пакета А, то можно к проектам подключать только пакет B, а все файлы MSBuild будет подтягивать из пакета A автоматически. (Документация)

        (Это если я правильно понял суть проблемы)
          +1
          Все верно, и авторы IBM.Data.DB2.Core в последних версиях своих пакетов используют именно папку «buildTransitive». Это транзитивная зависимость в чистом виде. В описываемом примере, я рассказывал пример, когда вы в своем пакете наследуете контент и скрипты от другого пакета, при этом транзитивно на него не ссылаясь. Это может применяться например для пакетов, у которых нет папки «buildTransitive»…

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое