Формат csproj-файла

Самым простым способом компиляции исходного кода в несколько фреймворков, например, .NET Framework 4.0, .NET Standard 2.0 или .NET 5, является семейство форматов csproj Microsoft.NET.Sdk. С полным списком целевых фреймворков можно ознакомиться здесь.

Целевые платформы

Каждой целевой платформе соответствует моникер (TFM, стандартизированный маркер), например, платформе .NET Framework 4.0 соответствует net40.

В классическом варианте в csproj-файле указывается одна целевая платформа с помощью свойства TargetFramework:

  <PropertyGroup>
    <TargetFramework>net40</TargetFramework>
  </PropertyGroup>

Для указания нескольких платформ необходимо использовать свойство TargetFrameworks:

  <PropertyGroup>
    <TargetFrameworks>net40;netcoreapp3.1;netstandard2.0</TargetFrameworks>
  </PropertyGroup>

.NET Standard

.NET Standard расположен в несколько иной плоскости относительно .NET Framework и .NET Core. .NET Standard определяет набор спецификаций для .NET API, который может быть реализован в различных фреймворках. Подробнее об поддержке версий .NET Standard можно почитать здесь.

net461 и netcoreapp3.1 не только реализуют контракты api netstandard2.0, но и содержат функционал выходящий за него. Таким образом, если некий сторонний пакет собирается под три целевые платформы net461;netcoreapp3.1;netstandard2.0, то по умолчанию будет выбрана максимально совместимая сборка.

Директивы препроцессора

Для разделения кода между платформами рекомендуется использовать вшитые в формат Microsoft.NET.Sdk константы, подробнее. Имеются константы определенные для все семейство платформ, например, NETFRAMEWORK для .NET Framework, так и для конкретной версии NETCOREAPP3_1 для netcoreapp3.1 соответственно.

Условия

Часто требуется указывать дополнительные правила, по которым осуществляется сборка для конкретной платформы или группы платформ. К таких правилам, например, относятся ссылки и пакетные зависимости, которые подключают функциональные модули реализованные отличными способами для разных платформ.

Для подключения сборки System.Configuration для семейства платформ .NET Framework используется ссылка:

  <ItemGroup Condition=" $(DefineConstants.Contains(NETFRAMEWORK)) ">
    <Reference Include="System.Configuration" />
  </ItemGroup>

Для .NET Standard или .NET Core:

  <ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' Or '$(TargetFramework)' == 'netcoreapp3.1' ">
    <PackageReference Include="System.Configuration.ConfigurationManager" Version="4.5.0" />
  </ItemGroup>

Про пакеты, сборки и .NET Framework

При переходе с классического формата csproj на Microsoft.NET.Sdk для .NET Framework стоит отметить, что может происходить неявное добавление ссылок на библиотеки платформы, например, System.Data.DataSetExtensions.

Таким образом следующие две конфигурации эквиваленты:

  <ItemGroup Condition=" '$(TargetFramework)' == 'net45' ">
    <Reference Include="System.Data.DataSetExtensions" />
  </ItemGroup>

  <ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' ">
    <PackageReference Include="System.Data.DataSetExtensions" Version="4.5.0" />
  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="System.Data.DataSetExtensions" Version="4.5.0" />
  </ItemGroup>

Не только по причине того, что ссылка на библиотеку добавляется автоматически, но и из-за того, что пакет System.Data.DataSetExtensions добавляет соответствующую библиотеку, как собственную зависимость в древо для .NETFramework4.5 :

    <frameworkAssemblies>
      <frameworkAssembly assemblyName="System.Data.DataSetExtensions" targetFramework=".NETFramework4.5" />
    </frameworkAssemblies>

Исключение файлов

[Не рекомендуется использовать]

  <ItemGroup Condition=" !$(DefineConstants.Contains(NETFRAMEWORK)) ">
    <Compile Remove="Helpers\HttpRequestHelper.cs" />
  </ItemGroup>

Миграция формата пакетных зависимостей

По умолчанию проекты на старом формате csproj-файла используют packages.config для регистрации пакетных зависимостей, кроме того в сам csproj-файл прописываются относительные пути (от .sln-файла) до библиотек в подключенном пакете. Однако можно сделать финт и перевести пакетные зависимости на новый формат, поддерживается начиная с Visual Studio 2017, имеется автоматический инструмент.

AspNet WebForms (.NET Framework)

Обратите внимание на зависимость Unity.AspNet.WebApi, иной способ конфигурации пакета приводит к ошибке публикации проекта.

  <ItemGroup>
    <PackageReference Include="Eais.Health.WebApi" Version="1.0.0-alpha02" />
    <PackageReference Include="Microsoft.AspNet.WebApi" Version="5.2.7" />
    <PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="5.0.3">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="Microsoft.Owin.Host.SystemWeb" Version="4.1.0" />
    <PackageReference Include="NewPlatform.Flexberry.AspNet.WebApi.Cors" Version="1.2.0" />
    <PackageReference Include="NewPlatform.Flexberry.ORM" Version="6.0.1-beta06" />
    <PackageReference Include="NewPlatform.Flexberry.ORM.PostgresDataService" Version="6.1.0-beta03" />
    <PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="Unity.AspNet.WebApi" Version="5.11.2">
      <PrivateAssets>all</PrivateAssets>
      <ExcludeAssets>contentFiles</ExcludeAssets>
    </PackageReference>
    <Reference Include="System" />
  </ItemGroup>

Условная компиляция

При поставке некого пакета\сборки, который должен реализовывать функциональную возможность под несовместимые платформы, например, глобальная обработка исключений в веб платформе (передача клиенту сообщения сформированного по определенным правилам и с определенной структурой):

  • .NET Framework - реализация интерфейса IExceptionHandler
  • .NET Core - реализация middleware аналогичной ExceptionHandlerMiddleware

Либо webapi-контроллер:

  • .NET Framework - класс наследуется от System.Web.Http.ApiController, ответы - System.Web.Http.IHttpActionResult
  • .NET Core - класс наследуется от Microsoft.AspNetCore.Mvc.ControllerBase, ответы - Microsoft.AspNetCore.Mvc.IActionResult

Разделение по блокам

Самый распространенный способ, позволяет легко обходить незначительные отличия в api разных платформ.

...
            foreach (var childItem in resourceInfoWrapper.NestedItems)
            {
#if NETFRAMEWORK
                IDictionary<string, object> props = readContext.Request.Properties;
#else
                IDictionary<object, object> props = readContext.Request.HttpContext.Items;
#endif
                if (!props.ContainsKey(Dictionary))
                {
                    props.Add(Dictionary, new Dictionary<string, object>());
                }

                var dictionary = (Dictionary<string, object>)props[Dictionary];
...

Данный код собирается под целевые платформы net45;net461;netcoreapp3.1;netstandard2.0.

Разделение по методам

Особо актуален для конструкторов и методов, которые сильно отличаются по сигнатуре и\или телу метода.

#if NETFRAMEWORK

        private readonly bool isSyncMode;
        
        private readonly IDataService dataService;
        
        public DataObjectODataBatchHandler(IDataService dataService, HttpServer httpServer, bool? isSyncMode = null)
            : base(httpServer)
        {
            this.dataService = dataService;
            this.isSyncMode = isSyncMode ?? Type.GetType("Mono.Runtime") != null;
        }
#else
        public DataObjectODataBatchHandler()
            : base()
        {
        }
#endif

Данный код собирается под целевые платформы net45;net461;netcoreapp3.1;netstandard2.0.

Псевдонимы

В некоторых случаях вместо многократного разделения по блокам проще переопределить используемые имена через alias-ы:

#if NETFRAMEWORK
    using DefaultAssembliesResolver = System.Web.Http.Dispatcher.DefaultAssembliesResolver;
    using IAssembliesResolver = System.Web.Http.Dispatcher.IAssembliesResolver;
#else
    using DefaultAssembliesResolver = Microsoft.AspNet.OData.Adapters.WebApiAssembliesResolver;
    using IAssembliesResolver = Microsoft.AspNet.OData.Interfaces.IWebApiAssembliesResolver;
#endif
...
    public partial class DataObjectController : ODataController
    {
        private static readonly IAssembliesResolver _defaultAssembliesResolver = new DefaultAssembliesResolver();

Данный код собирается под целевые платформы net45;net461;netcoreapp3.1;netstandard2.0.

Разделение файлами

Для случаев, когда api совершенно не совместим (пример об глобальной обработке исключенийиз родительского раздела), следует выделять все содержимое файла в блок условной компиляции:

#if NETFRAMEWORK
namespace Iis.Eais.Common.Web.Http
{
...
    /// <summary>
    /// Глобальный обработчик исключений.
    /// </summary>
    public class GlobalExceptionHandler : ExceptionHandler
    {
        /// <inheritdoc />
        public override bool ShouldHandle(ExceptionHandlerContext context)
        {
            return true;
        }

        /// <inheritdoc />
        public override void Handle(ExceptionHandlerContext context)
        {
...
        }
    }
}
#endif
#if NETSTANDARD
namespace Iis.Eais.Common.Web.Extensions
{
...
    /// <summary>
    /// Extension methods for enabling <see cref="ExceptionHandlerMiddleware" />.
    /// </summary>
    public static class ExceptionHandlerExtensions
    {
        /// <summary>
        /// Adds a middleware to the pipeline that will catch exceptions, log them, and re-execute the request in an alternate pipeline.
        /// The request will not be re-executed if the response has already started.
        /// </summary>
        /// <param name="app">The <see cref="IApplicationBuilder"/>.</param>
        /// <returns>A reference to the <paramref name="app"/> after the operation has completed.</returns>
        public static IApplicationBuilder UseCustomExceptionHandler(this IApplicationBuilder app)
        {
            return app.UseExceptionHandler(
                errorApp =>
                {
                    errorApp.Run(
                        context =>
                        {
...
                        });
                });
        }
    }
}
#endif

Данный код собирается под целевые платформы net45;netstandard2.0.

Перейти