пʼятниця, 3 серпня 2012 р.

Как заставить проэкт VS компилироваться для разных фрэймворков


Есть C#(.NET) утилитная библиотека, которую нужно включать в проекты собираемые под разными .NET версиями. Например в библиотеке есть код использующий .NET Framework v4.0, а нам нужно включить её в проект для .NET 2.0

11 лет .NET фрэймворку будет 13 февраля 2013 года. Было выпущено целых 7 версий. Создано огромное количество программистов. Решено масса задач, концептуально решено огромное число проблем ставящихся перед программистами. 
А вопрос совместимости по прежнему актуален.
Понадобилось мне недавно из набора сырцов разношерстных сырцов слепить библитеку. Как-то устаканить их, влить в коде-базу, сделать все цивилизованно. Код разношерстный, часть использует .NET 4, часть зависит от сборки System.Web и масса кода предназначена только для веба и не имеет смысла в desktop приложении, часть является самописными древними полу-аналогами функционала появившегося в .NET 3-4 и не имеющим смысла для поздних версий фрэймворка. Короче: нужно разделять код, делать условную компиляцию, а главное dll с утилитным кодом не должна требовать наличия фрэймворка выше чем солюшин в который она включена или выше чем проэкт её использующий.
На живом примере: у нас есть класс осуществляющий кеширование объектов в памяти.
  • В случае .NET 4 используем код System.Runtime.Caching и соответственно вносим зависимость на System.Runtime.Caching assembly
  • В случае ASP.NET версии ниже 4.0 используем System.Web.Caching namespace и получаем зависимость на ассемблай
  • В случае desktop приложения для фрэймворка ниже 4.0 используем свой собственный класс (на основе dictionary например)
Последний код скомпилируется везде, но работать будет не так эффективно как первые два и будет менее удобен для программиста. Так как же сделать, чтобы внутри библиотеки был весь этот функционал, но компилятор бы не собирал код и не включал референсы на системные сборки, которые недоступны если мы собираемся для .NET 2.0 например? И включал и использовал функционал для .NET 4 если доступен таковой?

Указание компилятор игнорировать некоторый код у нас идут через директивы препроцессора (кстати, он есть в .NET-e?).
Вспоминаем #define из С/С++ отличная вещь: определяем константы и ветвим код. Замечательно выходит. 
Студия при сборке не определяет константу, подсказывающую компилятору версию фрэймворка. Упс. Ладно определим её сами. 
#define FW3 //если определана эта коснтанта будет включен код которой работает максимум на третьем фрэймворке.
Парням из MS имело бы смысл унифицировать имена подобных констант.
На всякий случай: #define в .NET действует только на тот файл в котором он определен. 
Опять упс. Не сильно удобно. Но идем в настройки проэкта и определяем там константу для препроцессора. Проблема решена?
Нет. Точнее не полностью: мы хардкоднули в проект target framework. Пока у нас dll используется только в одном проекте - всё хорошо. Как только мы начинаем её использовать для разных, приходим к необходимости менять в проекте target framework и переопределять константу для разных солюшенов.

Опытные перцы решают проблему одним из следующих способов:
  1. Держат утилитные классы в отдельной папке и включают всю папку в свой проект (не забыть определить константу для препроцессора).
  2. Держат утилитные классы в отдельной папке и включают всю папку (или файлы по необходимости) в свой локальный проект для солюшена.
  3. Создают несколько проэктных файлов под каждый фрэймворк рядышком использующих один и тот же набор исходников.
  4. Создают билд конфигурации (или схемы) определяющие какой фрэймворк и профайл будет использован для сборки (невозможно через интерфейс VS 2010).
  5. Использовать отдельный солюшин и в другие проекты копировать собранную dll .
  6. Не парится.

Первый вариант: утилитные классы в отдельной папке, которую мы просто закидываем в проект
Плюсы: никакого разбирательства с проектами, удобно дебажить
Минусы: необходимость прописывать для всего солюшена малозначащий дефайн для всего остального кода, пересборка всех сырцов каждый раз (тормозит работу, когда утилитного кода много, а его как правило много), сложности с добавлением новых классов, вероятность повяления дублирующего когда разные программисты изобретают один и тот же велосипед. 
Такой подход благодаря своей простоте хорошо работает в маленькой команде с малым числом проектов. Когда команда разрастается или проекты увеличиваются этот подход создаст больше пробле проблем чем пользы. В итоге одни и теже сырцы будут разнесены, скопированы, продублированы.
Не хорошо..

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

Третий: несколько проэктных файлов под каждый фрэймворк рядышком использующих один и тот же набор исходников 
Вот это уже совсем по взрослому. Взрослый минус: при изменении файлов в проекте необходимо во всех проэктных файлах лежащих рядом добавить или удалить файлы. Мы программисты любим повторяться? Не-а. Но по всем остальным подходам - это очень правильно и цивилизованно, что избавляет от огромного числа потенциальных будущих проблем (если мы конечно не работаем по принципу: написали, отдали заказчику, забыли)

Четвертый: билд конфигурации определяющие какой фрэймворк и профайл будет использован для сборки 
Один проект, все в одном месте. Даже удобнее, чем третий вариант. В чем подвох? Средствами студии создать такое нельзя. Дожились! 11 лет фрэймворку.
Открываем файл проекта в блокноте и в секции: <PropertyGroup> (та, что для всех) убираем теги: <TargetFrameworkVersion> и таргет схема. 
Далее в каждой проперти группе <PropertyGroup Condition=...> добавляем эти теги сконфигурировав уже как нужно:

<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'ReleaseFW3|AnyCPU'">
    <TargetFrameworkVersion>v3.5</TargetFrameworkVersion>
    <OutputPath>bin\Release\</OutputPath>
    <DefineConstants>TRACE;FW3</DefineConstants>

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

  <ItemGroup>
    <Reference Include="System" />
    <Reference Include="System.Data" />
    <Reference Include="System.Runtime.Caching">
<RequiredTargetFramework>4.0</RequiredTargetFramework>
</Reference>
    <Reference Include="System.Web" />
    <Reference Include="System.Xml" />
  </ItemGroup>
Этот код ставит зависимость на System.Runtime.Caching сборку, только если проект компилируется под .NET4




Вариант 5 мне нравится больше всего? А вам?
Правильно: "купание в море повышает настроение" (с) МЕГА. Айда купаться!

1 коментар:

Dmitry Klymenko сказав...

http://msdn.microsoft.com/ru-ru/library/bb126445.aspx