Обновление на Revit API 2025 — .NET8

Я уже ранее писал про то, как в Visual Studio написать один плагин сразу под несколько версий Revit. Но Autodesk в Revit 2025 перешел на новый фреймворк — .NET8 вместо Net Framework. Это серьезное изменение, и для поддержки версии 2025 старые проекты придется сильно переделать. Разбираю всё в этой статье.

Немного предыстории

Изначально .Net был только для Windows (что было главной критикой C# по сравнению с Java), но потом энтузиасты создали мультиплатформенную .Net Core, а затем и сами Microsoft отказались от .Net Framework, и начиная с пятой версии есть только ветка .NET, продолжение .Net Core. NetFramework теперь считается устаревшим, и остановился на версии 4.8.

Конечно, глобально это хорошие новости — наш любимый C# теперь станет популярнее, можно будет легко кодить и на Линуксах, что круто для серверов и бэкенда, и вообще .Net теперь весь такой стильный-модный-опенсорсный. Но у нас-то Ревит работает только на Windows, все эти плюшки нам без надобности. С версии Revit 2021 использовал NetFramework4.8, и в общем-то всё нормально было. Но Autodesk почему-то решил переделать всё на версию .Net8 (ну да, других задач ж нет).

Проблема в том, что .Net, как наследник Net Core, сильно отличается от Net Framework. Там нет некоторых функций, которые были в Net Framework, скорее всего не будут работать сторонние библиотеки, написанные с NetFramework, и вообще надо переделывать саму структуру csproj файлов.

Итак, что нам потребуется:

1. Обновляем старые проекты

NET8 поддерживается только в Visual Studio 2022 и более новых версиях, так что сначала обновите вашу VS до актуальной версии через «Установку программ».

Далее откройте ваш проект, щелкните правой кнопкой по проекту в обозревателе и выберите Upgrade:

Но при первом запуске появится сообщение, что надо установить Upgrade Assistant. Переходим в браузер, скачиваем и ставим эту утилитку (обратите внимание на рейтинг, хех):

Установка стандартная:

Снова открываем наш проект, выбираем Upgrade — Upgrade project features — Upgrade to SDK-Style:

И вот у меня всё получилось:

Но если почитать, что народ пишет, то это мне сильно повезло — у многих просто выдает ошибки (вспоминаем про рейтинг!). В этом случае файл можно обновить вручную, ну и не проблема, всё равно нам надо будет лезть в файл csproj руками.

2. Правим csproj-файл

Если открыть csproj файл, то сразу видно отличия — в новом .NET всё короче и лаконичнее, вот пример с одним и тем же простым консольным приложением:

А ещё редактировать csproj-файл можно прямо в Студии, надо просто щелкнуть по заголовку проекта в обозревателе:

Хотя если обновление через Assistant не удалось, придется ковырять в Notepad++.

Итак, открываем ваш файл csproj и изучаем. Первая строка будет:

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

Далее блок общих настроек:

<PropertyGroup>
  <OutputType>Library</OutputType>
  <ImportWindowsDesktopTargets>true</ImportWindowsDesktopTargets>
  <GenerateAssemblyInfo>true</GenerateAssemblyInfo>
  <UseWindowsForms>true</UseWindowsForms>
  <ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch>
     None
  </ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch>
  <AppendTargetFrameworkToOutputpath>false</AppendTargetFrameworkToOutputpath>
  <Configurations>R2017;R2018;R2019;R2020;R2021;R2022;R2023;R2024;R2025</Configurations>
</PropertyGroup>

Здесь:

  • OutputType — Library значит, что будет генерироваться dll-ка
  • ImportWindowsDesktopTargets — подтягивает все нужные Windows-библиотеки
  • GenerateAssemblyInfo — можно будет удалить файл AssemblyInfo.cs, использовавшийся в NetFramework (ещё см. далее)
  • UseWindowsForms и UseWPF — автоматически подключает кучу библиотек и для создания окошек, и для работы с картинками (PresentationCore и т.д.), советую на всякий случай оставлять во всех плагинах. Ещё есть <UseWPF>true</UseWPF> соответственно для WPF.
  • ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch — убирает надоедающее предупреждение о несоответствии архитектуры процессора
  • AppendTargetFrameworkToOutputpath — отключает раскидывание разных сборок в разные папки (нам не надо, т.к. под каждую версию Ревита уже будет своя папка)
  • Configurations — перечисляем ключи для каждой версии Revit, я использую такие же, как были в моей старой статье.

 

Далее прописываем конфигурации под каждую версию Revit:

<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'R2017|AnyCPU' ">
  <DebugSymbols>true</DebugSymbols>
  <Optimize>false</Optimize>
  <TargetFramework>net46</TargetFramework>
  <OutputPath>bin\R2017\</OutputPath>
  <DefineConstants>DEBUG;R2017</DefineConstants>
  <AssemblyName>$(AssemblyName)_2017</AssemblyName>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'R2018|AnyCPU' ">
  <DebugSymbols>true</DebugSymbols>
  <Optimize>false</Optimize>
  <TargetFramework>net46</TargetFramework>
  <OutputPath>bin\R2018\</OutputPath>
  <DefineConstants>DEBUG;R2018</DefineConstants>
  <AssemblyName>$(AssemblyName)_2018</AssemblyName>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'R2019|AnyCPU' ">
  <DebugSymbols>true</DebugSymbols>
  <Optimize>false</Optimize>
  <TargetFramework>net47</TargetFramework>
  <OutputPath>bin\R2019\</OutputPath>
  <DefineConstants>DEBUG;R2019</DefineConstants>
  <AssemblyName>$(AssemblyName)_2019</AssemblyName>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'R2020|AnyCPU' ">
  <DebugSymbols>true</DebugSymbols>
  <Optimize>false</Optimize>
  <TargetFramework>net47</TargetFramework>
  <OutputPath>bin\R2020\</OutputPath>
  <DefineConstants>DEBUG;R2020</DefineConstants>
  <AssemblyName>$(AssemblyName)_2020</AssemblyName>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'R2021|AnyCPU' ">
  <DebugSymbols>true</DebugSymbols>
  <Optimize>false</Optimize>
  <TargetFramework>net48</TargetFramework>
  <OutputPath>bin\R2021\</OutputPath>
  <DefineConstants>DEBUG;R2021</DefineConstants>
  <AssemblyName>$(AssemblyName)_2021</AssemblyName>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'R2022|AnyCPU' ">
  <DebugSymbols>true</DebugSymbols>
  <Optimize>false</Optimize>
  <TargetFramework>net48</TargetFramework>
  <OutputPath>bin\R2022\</OutputPath>
  <DefineConstants>DEBUG;R2022</DefineConstants>
  <AssemblyName>$(AssemblyName)_2022</AssemblyName>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'R2023|AnyCPU' ">
  <DebugSymbols>true</DebugSymbols>
  <Optimize>false</Optimize>
  <TargetFramework>net48</TargetFramework>
  <OutputPath>bin\R2023\</OutputPath>
  <DefineConstants>DEBUG;R2023</DefineConstants>
  <AssemblyName>$(AssemblyName)_2023</AssemblyName>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'R2024|AnyCPU' ">
  <DebugSymbols>true</DebugSymbols>
  <Optimize>false</Optimize>
  <TargetFramework>net48</TargetFramework>
  <OutputPath>bin\R2024\</OutputPath>
  <DefineConstants>DEBUG;R2024</DefineConstants>
  <AssemblyName>$(AssemblyName)_2024</AssemblyName>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'R2025|AnyCPU' ">
  <DebugSymbols>true</DebugSymbols>
  <Optimize>false</Optimize>
  <TargetFramework>net8.0-windows</TargetFramework>
  <OutputPath>bin\R2025\</OutputPath>
  <DefineConstants>DEBUG;R2025</DefineConstants>
  <AssemblyName>$(AssemblyName)_2025</AssemblyName>
</PropertyGroup>

Тут похоже на то, что было и в старой версии:

  • DebugSymbols и Optimize — чтобы работала отладка
  • TargetFramework: net46 для 2017-2018, net47 для 2019-2020, net48 для 2021-2024, и net8.0-windows для Revit 2025
  • OutputPath — отдельная папка для каждой версии
  • DefineConstants — то что можно будет прописывать в «символах компиляции» в коде плагина

 

Далее блок Choose-When, для подключения разных библиотек Revit API под каждую версию, тут можно скопировать без изменений:

<Choose>
		<When Condition=" '$(Configuration)'=='R2017' or '$(Configuration)'=='Debug'">
			<ItemGroup>
				<Reference Include="RevitAPI">
					<HintPath>C:\Program Files\Autodesk\Revit 2017\RevitAPI.dll</HintPath>
					<Private>False</Private>
				</Reference>
				<Reference Include="RevitAPIUI">
					<HintPath>C:\Program Files\Autodesk\Revit 2017\RevitAPIUI.dll</HintPath>
					<Private>False</Private>
				</Reference>
			</ItemGroup>
		</When>
		<When Condition=" '$(Configuration)'=='R2018' ">
			<ItemGroup>
				<Reference Include="RevitAPI">
					<HintPath>C:\Program Files\Autodesk\Revit 2018\RevitAPI.dll</HintPath>
					<Private>False</Private>
				</Reference>
				<Reference Include="RevitAPIUI">
					<HintPath>C:\Program Files\Autodesk\Revit 2018\RevitAPIUI.dll</HintPath>
					<Private>False</Private>
				</Reference>
			</ItemGroup>
		</When>
		<When Condition=" '$(Configuration)'=='R2019' ">
			<ItemGroup>
				<Reference Include="RevitAPI">
					<HintPath>C:\Program Files\Autodesk\Revit 2019\RevitAPI.dll</HintPath>
					<Private>False</Private>
				</Reference>
				<Reference Include="RevitAPIUI">
					<HintPath>C:\Program Files\Autodesk\Revit 2019\RevitAPIUI.dll</HintPath>
					<Private>False</Private>
				</Reference>
			</ItemGroup>
		</When>
		<When Condition=" '$(Configuration)'=='R2020' ">
			<ItemGroup>
				<Reference Include="RevitAPI">
					<HintPath>C:\Program Files\Autodesk\Revit 2020\RevitAPI.dll</HintPath>
					<Private>False</Private>
				</Reference>
				<Reference Include="RevitAPIUI">
					<HintPath>C:\Program Files\Autodesk\Revit 2020\RevitAPIUI.dll</HintPath>
					<Private>False</Private>
				</Reference>
			</ItemGroup>
		</When>
		<When Condition=" '$(Configuration)'=='R2021' ">
			<ItemGroup>
				<Reference Include="RevitAPI">
					<HintPath>C:\Program Files\Autodesk\Revit 2021\RevitAPI.dll</HintPath>
					<Private>False</Private>
				</Reference>
				<Reference Include="RevitAPIUI">
					<HintPath>C:\Program Files\Autodesk\Revit 2021\RevitAPIUI.dll</HintPath>
					<Private>False</Private>
				</Reference>
			</ItemGroup>
		</When>
		<When Condition=" '$(Configuration)'=='R2022' ">
			<ItemGroup>
				<Reference Include="RevitAPI">
					<HintPath>C:\Program Files\Autodesk\Revit 2022\RevitAPI.dll</HintPath>
					<Private>False</Private>
				</Reference>
				<Reference Include="RevitAPIUI">
					<HintPath>C:\Program Files\Autodesk\Revit 2022\RevitAPIUI.dll</HintPath>
					<Private>False</Private>
				</Reference>
			</ItemGroup>
		</When>
		<When Condition=" '$(Configuration)'=='R2023' ">
			<ItemGroup>
				<Reference Include="RevitAPI">
					<HintPath>C:\Program Files\Autodesk\Revit 2023\RevitAPI.dll</HintPath>
					<Private>False</Private>
				</Reference>
				<Reference Include="RevitAPIUI">
					<HintPath>C:\Program Files\Autodesk\Revit 2023\RevitAPIUI.dll</HintPath>
					<Private>False</Private>
				</Reference>
			</ItemGroup>
		</When>
		<When Condition=" '$(Configuration)'=='R2024' ">
			<ItemGroup>
				<Reference Include="RevitAPI">
					<HintPath>C:\Program Files\Autodesk\Revit 2024\RevitAPI.dll</HintPath>
					<Private>False</Private>
				</Reference>
				<Reference Include="RevitAPIUI">
					<HintPath>C:\Program Files\Autodesk\Revit 2024\RevitAPIUI.dll</HintPath>
					<Private>False</Private>
				</Reference>
			</ItemGroup>
		</When>
		<When Condition=" '$(Configuration)'=='R2025' ">
			<ItemGroup>
				<Reference Include="RevitAPI">
					<HintPath>C:\Program Files\Autodesk\Revit 2025\RevitAPI.dll</HintPath>
					<Private>False</Private>
				</Reference>
				<Reference Include="RevitAPIUI">
					<HintPath>C:\Program Files\Autodesk\Revit 2025\RevitAPIUI.dll</HintPath>
					<Private>False</Private>
				</Reference>
			</ItemGroup>
		</When>
	</Choose>

Следующая ItemGroup — подключение nuget-пакетов. Фишка .NET в том, теперь всё подтягивается Nuget-пакетами, даже сам обработчик Charp. Так что везде будут вот эти две строки:

<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="System.Data.DataSetExtensions" Version="4.5.0" />

Если вы подключите, например, NewtonsoftJson, то он тоже прописывается сюда:

ItemGroup с Include Properties удаляем, а также удаляем из проекта папку Properties с файлом AssemblyInfo:

Если у вас есть картинки, то внедрить их в сборку можно так:

<ItemGroup>
    <EmbeddedResource Include="Resources\PrintBig.png" />
    <EmbeddedResource Include="Resources\PrintSmall.png" />
  </ItemGroup>

Если надо закинуть какие-нибудь сторонние файлы, то вот так:

<ItemGroup>
    <Content Include="formats.txt">
      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </Content>
  </ItemGroup>

3. Исправляем код

Скрестив пальцы открываем наш файл sln, и всё должно открыться-прогрузиться, проверьте что переключаются конфигурации:

Но собираться ваш проект, скорее всего, не будет — слишком уж много изменений у нового фрейворка, а тем более нам надо, чтобы один и тот же код работал и под NetFramework, и NET8.

Например, я во всех плагинах использовал логирование через Debug.Listeners, но в .NET8 его просто нет:

Благо, есть Trace.Listeners, который есть и в NetFramework, так что просто заменяю всё на Trace через Поиск и Замену:

Ещё в Revit 2025 изменился вызов ElementId.IntegerValue — теперь он вызывается как ElementId.Value и возвращает long. Используется это очень часто, и чтобы не писать #if R2025 в каждом месте — я вынес его в отдельный метод расширения:

    public static class Extensions
    {
        public static long GetValue(this ElementId elementId)
        {
#if R2017 || R2018 || R2019 || R2020 || R2021 || R2022 || R2023
            return elementId.IntegerValue;
#else
            return elementId.Value;
#endif
        }
    }

Теперь во всем коде достаточно через Поиск-Замену поменять .IntegerValue на .GetValue():

#if #else #endif — это «Символы условной компиляции», я подробно о них рассказывал в предыдущей статье.

4. Проверяем ссылки на библиотеки

Но были и более серьезные проблемы. Например, у меня есть плагин «Поиск переопределений графики«, в котором отчет выводится в виде удобного списка-дерева:

Этот же плагин в Revit 2025 никаких ошибок не выдавал, но просто показывал пустое окно:

Стал разбираться, и оказалось что библиотека ObjectListView, которую я использовал, не обновлялась с 2016 года, разработчик просто её забросил, поэтому под .NET и не работало:

Благо, народные умельцы сделали форки этой библиотеки и обновили под .NET, я попробовал несколько и выбрал ObjectListView.Repack.NET6Plus от nasisakk, она пока доступна только под NET7, но под NET8 работает нормально и ничего в коде менять не надо:

Какие ещё неожиданные глюки могут всплыть у вас — хз, но готовьтесь заранее.

5. RIP Batch Build

Если вам было мало, то вот ещё. При подключении .NET8 вы больше не сможете пользоваться Пакетной сборкой, чтобы разом собрать плагины под все версии Revit!

Обычно у меня собирается только под две-три версии, а затем начинает выдавать ошибку «error NETSDK1005: Assets file project.assets.json doesn’t have a target for …«. При этом, если переключать конфигурации вручную и пересобирать — ошибок нет!

Как я понял, это проблема как раз с переходом на подгрузку всего через Nuget-пакеты — пакетная сборка идет так быстро, что нужные пакеты не успевают подгружаться. Проблема эта известная, висит в Issues, решения нет! Что за нафиг? Там советуют вручную очищать папки bin/obj, но это помогает только для сборщика Nuke, и вообще не выглядит как нормальное решение. На Stackoverflow есть куча самых разных советов — обновить Nuget, сделать restore и кучу всего другого, мне ничего не помогло.

В общем, поминаем добрым словом Майкрософты, Автодески и всех остальных, и пересобираем плагины, выбирая конфигурации вручную. Ну, хотя бы плагины работают, уже победа. Такие дела.

 

UPD:

Столкнулся с ещё одной внезапной проблемой. Если используются WindowsForms с включенной Localizable (т.е. переключение языка интерфейса через файлы .resx), то такие формы не надо хранить во вложенных папках, только в «корне» проекта. Иначе не подхватываются файлы ресурсов и вылезает ошибка «Не удалось найти ресурсы, соответствующие указанной культуре» («could not find any resources appropriate for the specified culture»), хотя файлы все на месте:

Судя по всему, если формы лежат во вложенных папках, то и ресурсы он пытается искать во вложенных папках, хотя компилятор закидывает их вместе. Причем в Revit 2025 ошибки нет — похоже, NET8 умнее и знает, как искать такие файлы, а более старый NetFramework — нет.
Помогло перетащить формы из вложенных папок в корень проекта:

Похоже на какой-то костыль, но нам что, в первый раз чтоли…