Publications / Проблематика совы и глобуса: подключение двух сборок с идентичными пространствами имен и названиями классов

Habr

Habr.com is a unique online community for IT professionals. This platform hosts in-depth articles, industry news, and engaging discussions, serving as a key resource for tech enthusiasts, developers, and IT specialists looking to stay ahead in the rapidly evolving world of technology.


Friday, 31 August 2018
Проблематика совы и глобуса: подключение двух сборок с идентичными пространствами имен и названиями классов

Сегодня вечером, с gelas завели разговор о том, как работают пакетные менеджеры на разных платформах. В ходе беседы, дошли до обсуждения ситуации, когда в проект на .NET Core необходимо подключить две библиотеки, которые содержат классы с одинаковым названием в одинаковых пространствах имен. Поскольку .NET Core я занимаюсь достаточно плотно, я захотел проверить, как можно решить подобную проблему. Что из этого вышло описано дальше


Дисклеймер. Часто ли встречаются такие ситуации? Мне за более чем 10 лет работы с .NET, с подобной ситуацией в реальном проекте не приходилось сталкиваться ни разу. Но вот провести эксперимент было интересно.


На всякий случай уточню, что эксперимент я буду проводить используя:


  • macOS 10.13,
  • .NET Core SDK 2.1.302
  • Rider 2018.2

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


Подготовка эксперимента


И так, для начала подготовим одну сову и два глобуса. В качестве совы у нас будет выступать проект с таргетом на netcoreapp2.1. В качестве глобусов создадим два проекта, один из готорых будет также с таргетом на netcoreapp2.1, а второй на netstandard2.0



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


Первый файл:


using System;

namespace Space
{
    public class Globe
    {
        public string GetColor() => "Green";
    }
}

Вторй файл:


using System;

namespace Space
{
    public class Globe
    {
        public string GetColor() => "Blue";
    }
}

Попытка номер один


Поскольку по условиям задачи мы должны работать с внешними сборками, а не проектами, то добавим соответственно в проект ссылки как будто они действительно являются просто библиотеками. Для этого сначала скомпилируем все проекты, чтобы у нас появились нужные нам Globe1.dll и Globe2.dll. Затем добавим на них ссылки в проект в таком виде:



Теперь попробуем создать переменную класса Globe:



Как видим, уже на этом этапе IDE предупреждает нас о том, что есть проблема с пониманием того, откуда должен быть взят нужный нам класс Globe.


Сначала кажется, что ситуация довольно типичная и на нее уже должны быть готовый, отлитый в граните, ответ на Stack Overflow. Как оказалось, для .NET Core решения подобной задачи пока еще предложено не было. Либо мой Гугл меня подвел. Но кое что полезное на Stack Overflow найти удалось.Единственная толковая публикация, которую удалось нагуглить — была за 2006 год и описывала подобную ситуацию для классической версии .NET. При этом, весьма похожа проблема обсуждается в репозитории проекта NuGet.


Пока не очень много полезно информации, но она все же есть:



Осталось понять, как сделать это в .NET Core.


К сожалению, в текущей версии документации довольно скромно рассказывается о возможностях подключения внешних пакетов/сборов. А описание csproj файла также никоим образом не проливает свет на возможности создания псевдонимов. Но тем не менее, методами проб и ошибок, мне удалось выяснить, что псевдонимы для сборок в .NET Core все-таки поддерживаются. И оформляются они следующем образом:


<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>netcoreapp2.1</TargetFramework>
    </PropertyGroup>

    <ItemGroup>
        <Reference Include="Globe1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
            <HintPath>..\Globe1\bin\Debug\netcoreapp2.1\Globe1.dll</HintPath>
            <Aliases>Lib1</Aliases>
        </Reference>

        <Reference Include="Globe2, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
            <HintPath>..\Globe2\bin\Debug\netstandard2.0\Globe2.dll</HintPath>
            <Aliases>Lib2</Aliases>
        </Reference>
    </ItemGroup>

</Project>

Теперь осталось научиться использовать эти псевдонимы. В этом нам поможет ранее уже упоминаемое ключевое слово extern:


В документации о нем пишут следующее:


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

При каждом объявлении псевдонима extern вводится дополнительное пространство имен корневого уровня, которое соответствует глобальному пространству имен (но не находится внутри него). Таким образом ссылки на типы из каждой сборки без неоднозначности могут создаваться с помощью их полного имени, корнем которого является соответствующий псевдоним пространства имен.

Тут правда не стоит забывать о том, что extern также используется в C# для объявления метода с внешней реализацией из неуправляемого кода. В этом случае extern обычно используется с атрибутом DllImport. Более детально об этом можно почитать в соответствующем разделе документации.


Итак, попробуем использовать наши псевдонимы:


extern alias Lib1;
extern alias Lib2;
using System;

namespace Owl
{   
...
    public class SuperOwl
    {
        private Lib1::Space.Globe _firstGlobe;
        private Lib2::Space.Globe _secondGlobe;

        public void IntegrateGlobe(Lib1::Space.Globe globe) => _firstGlobe = globe;

        public void IntegrateGlobe(Lib2::Space.Globe globe) => _secondGlobe = globe;

...

    }
}

Этот код уже даже работает. Причем работает правильно. Но все-таки хочется сделать его чуточку элегантнее. Сделать это можно весьма простым способом:


extern alias Lib1;
extern alias Lib2;
using System;
using SpaceOne=Lib1::Space;
using SpaceTwo=Lib2::Space;

Теперь можно использовать обычный и очевидный синтаксис:


var globe1 = new SpaceOne.Globe()
var globe2 = new SpaceTwo.Globe()

Испытания


Проведем испытания нашей совы:



Как видим, код отработал нормально и без ошибок. Интеграция совы и глобусов успешно завершена!


→ Код примера доступен на GitHub


Read publication