Start Debugging

Миграция Azure Functions с модели in-process на изолированный worker (.NET 8 / .NET 11)

Пошаговый чек-лист для перевода .NET-приложения Azure Functions с модели in-process на изолированный worker до прекращения поддержки 10 ноября 2026 года, с диффами csproj, переписыванием сигнатур и развёртыванием через обмен слотами.

Типичное in-process-приложение Azure Functions на .NET 8 переходит на изолированный worker примерно за один сосредоточенный день изменений кода плюс поэтапное окно релиза. То, что ломается, является механическим и предсказуемым: ссылка на SDK в вашем .csproj, ваш Startup.cs, каждый атрибут [FunctionName], каждый пакет Microsoft.Azure.WebJobs.Extensions.* и любая функция, которая принимала параметр ILogger. Ничто из этого не сложно, но всё обязательно, потому что поддержка модели in-process прекращается 10 ноября 2026 года, и после этой даты Azure перестаёт предоставлять in-process-приложениям обновления безопасности и функций. Это руководство представляет собой полный чек-лист: что менять, точный дифф для каждого изменения, как проверять каждый шаг и как развернуть смену среды выполнения через слот staging, чтобы продакшен никогда не увидел сломанное промежуточное состояние.

Это нацелено на хост Azure Functions v4 и описывает миграцию dotnet-приложения (in-process) на .NET 6 или .NET 8 LTS на модель изолированного worker dotnet-isolated на .NET 8 LTS (рекомендуемый быстрый путь) или .NET 11. Указанные версии пакетов являются последними стабильными по состоянию на июнь 2026 года: Microsoft.Azure.Functions.Worker 2.0.x, Microsoft.Azure.Functions.Worker.Sdk 2.0.x и семейство Microsoft.Azure.Functions.Worker.Extensions.*. Если ваше приложение всё ещё на хосте v1, v2 или v3, сначала выполните обновление хоста с версии 3.x на 4.x; это руководство встраивает переход на изолированный worker в тот же проход.

Если вы всё ещё решаете, мигрировать ли вообще, сначала прочитайте изолированный worker против in-process в .NET 11. Этот пост исходит из того, что вы уже решили и хотите механический рецепт.

Почему эта миграция не опциональна

Что ломается

ОбластьИзменениеСерьёзность
Ссылка на SDKMicrosoft.NET.Sdk.Functions удалён; заменить на Microsoft.Azure.Functions.Worker + .Worker.Sdkвысокая
Тип выводав .csproj нужен <OutputType>Exe</OutputType>; worker теперь настоящий консольный процессвысокая
StartupFunctionsStartup / Startup.cs заменён на Program.cs с HostBuilderвысокая
Атрибут функции[FunctionName("X")] становится [Function("X")]высокая
Пакеты привязоккаждый Microsoft.Azure.WebJobs.Extensions.* меняется на Microsoft.Azure.Functions.Worker.Extensions.*высокая
Параметр ILoggerвнедряемый через метод ILogger log становится внедряемым через конструктор ILogger<T>средняя
Тип возврата HTTPIActionResult продолжает работать только если вы включаете интеграцию с ASP.NET Core; иначе HttpResponseDataсредняя
Привязки входа / выходавходные привязки получают суффикс Input, выходные - суффикс Output, выходы покидают список параметровсредняя
IBinder / IAsyncCollector<T>IBinder удалён; IAsyncCollector<T> становится возвратом T[] или внедрённым клиентомсредняя
FUNCTIONS_WORKER_RUNTIMEзначение меняется с dotnet на dotnet-isolated локально и в настройках приложения в Azureвысокая
Фильтрация логовhost.json больше не фильтрует логи вашего кода; фильтрация переходит в Program.csнизкая

Чек-лист подготовки

Прежде чем тронуть строку кода:

Шаги миграции

Каждый шаг ниже - это отдельное изменение со строкой проверки. Выполняйте их по порядку; проект не соберётся чисто, пока не сделан последний, что ожидаемо.

  1. Преобразуйте SDK и пакеты .csproj. Удалите ссылку на Microsoft.NET.Sdk.Functions, добавьте пакеты worker и добавьте <OutputType>Exe</OutputType>. До и после:

    <!-- BEFORE: in-process, .NET 8, host v4 -->
    <Project Sdk="Microsoft.NET.Sdk">
      <PropertyGroup>
        <TargetFramework>net8.0</TargetFramework>
        <AzureFunctionsVersion>v4</AzureFunctionsVersion>
      </PropertyGroup>
      <ItemGroup>
        <PackageReference Include="Microsoft.NET.Sdk.Functions" Version="4.5.0" />
      </ItemGroup>
    </Project>
    <!-- AFTER: isolated worker, .NET 8, host v4 -->
    <Project Sdk="Microsoft.NET.Sdk">
      <PropertyGroup>
        <TargetFramework>net8.0</TargetFramework>
        <AzureFunctionsVersion>v4</AzureFunctionsVersion>
        <OutputType>Exe</OutputType>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
      </PropertyGroup>
      <ItemGroup>
        <FrameworkReference Include="Microsoft.AspNetCore.App" />
        <PackageReference Include="Microsoft.Azure.Functions.Worker" Version="2.0.0" />
        <PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="2.0.0" />
        <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version="2.0.0" />
        <PackageReference Include="Microsoft.ApplicationInsights.WorkerService" Version="2.23.0" />
        <PackageReference Include="Microsoft.Azure.Functions.Worker.ApplicationInsights" Version="2.0.0" />
      </ItemGroup>
    </Project>

    Чтобы вместо этого нацелиться на .NET 11, установите <TargetFramework>net11.0</TargetFramework>. FrameworkReference на Microsoft.AspNetCore.App - это то, что позволяет HTTP-триггерам продолжать возвращать IActionResult; сохраните его, даже если у вас нет HTTP-триггеров, потому что он улучшает запуск worker. Проверка: dotnet restore завершается успешно и подтягивает пакеты worker. Сборка всё ещё будет падать на коде, и это нормально.

  2. Замените каждый пакет расширения привязки. Для каждого Microsoft.Azure.WebJobs.Extensions.*, который вы инвентаризировали, перейдите на эквивалент worker. Распространённые:

    Microsoft.Azure.WebJobs.Extensions.Storage.Blobs
      -> Microsoft.Azure.Functions.Worker.Extensions.Storage.Blobs
    Microsoft.Azure.WebJobs.Extensions.ServiceBus
      -> Microsoft.Azure.Functions.Worker.Extensions.ServiceBus
    Microsoft.Azure.WebJobs.Extensions.CosmosDB
      -> Microsoft.Azure.Functions.Worker.Extensions.CosmosDB
    Microsoft.Azure.WebJobs.Extensions.EventHubs
      -> Microsoft.Azure.Functions.Worker.Extensions.EventHubs
    Microsoft.Azure.WebJobs.Extensions.DurableTask
      -> Microsoft.Azure.Functions.Worker.Extensions.DurableTask

    Триггер таймера требует явного добавления Microsoft.Azure.Functions.Worker.Extensions.Timer. Полностью удалите Microsoft.Azure.Functions.Extensions; изолированный worker предоставляет запуск DI нативно. Проверка: не остаётся ни одной ссылки на какой-либо пакет Microsoft.Azure.WebJobs.*. dotnet list package | Select-String WebJobs (PowerShell) не должен ничего выводить.

  3. Добавьте Program.cs и удалите Startup.cs. Изолированный worker - это консольное приложение, и ему нужна точка входа. Перенесите всё из вашего FunctionsStartup.Configure в ConfigureServices:

    // Program.cs -- .NET 8 / .NET 11, Microsoft.Azure.Functions.Worker 2.0.x
    using Microsoft.Azure.Functions.Worker;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Hosting;
    
    var builder = FunctionsApplication.CreateBuilder(args);
    
    // ConfigureFunctionsWebApplication() opts into ASP.NET Core integration so HTTP
    // triggers can keep using HttpRequest / IActionResult. Use
    // ConfigureFunctionsWorkerDefaults() instead if you have no HTTP triggers.
    builder.ConfigureFunctionsWebApplication();
    
    builder.Services
        .AddApplicationInsightsTelemetryWorkerService()
        .ConfigureFunctionsApplicationInsights();
    
    // Everything that used to be in Startup.Configure goes here:
    builder.Services.AddHttpClient();
    builder.Services.AddSingleton<IOrderStore, OrderStore>();
    
    builder.Build().Run();

    Затем удалите класс, который нёс атрибут [assembly: FunctionsStartup(typeof(Startup))]. Проверка: файл компилируется изолированно, и нигде не остаётся атрибута FunctionsStartup (Select-String FunctionsStartup -Path **/*.cs).

  4. Переименуйте атрибуты функций. [FunctionName("X")] становится [Function("X")]. Сигнатура идентична, так что это безопасная замена строк по всему проекту. Проверка: Select-String "FunctionName" -Path **/*.cs ничего не возвращает.

  5. Перенесите ILogger из параметра в конструктор. In-process позволял принимать параметр ILogger log в методе функции. Изолированный worker использует внедрение через конструктор. Преобразуйте каждый затронутый класс функции в первичный конструктор:

    // BEFORE (in-process)
    public static class HttpTriggerCSharp
    {
        [FunctionName("HttpTriggerCSharp")]
        public static IActionResult Run(
            [HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequest req,
            ILogger log)
        {
            log.LogInformation("processed a request");
            return new OkObjectResult($"Hello, {req.Query["name"]}!");
        }
    }
    // AFTER (isolated worker, .NET 8 / .NET 11, C# 12+ primary constructor)
    public class HttpTriggerCSharp(ILogger<HttpTriggerCSharp> logger)
    {
        [Function("HttpTriggerCSharp")]
        public IActionResult Run(
            [HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequest req)
        {
            logger.LogInformation("processed a request");
            return new OkObjectResult($"Hello, {req.Query["name"]}!");
        }
    }

    Обратите внимание, что функция больше не static, класс больше не static, и ILogger покинул сигнатуру метода. Проверка: класс функции компилируется, и ни один метод функции больше не имеет параметра ILogger.

  6. Исправьте usings и имена атрибутов триггеров и привязок. Удалите using Microsoft.Azure.WebJobs;, добавьте using Microsoft.Azure.Functions.Worker;. Триггеры обычно сохраняют своё имя (QueueTrigger остаётся QueueTrigger), входные привязки получают суффикс Input (CosmosDB становится CosmosDBInput), а выходные привязки получают суффикс Output (Queue становится QueueOutput, Blob становится BlobOutput). Выходные привязки также покидают список параметров: один выход идёт на тип возврата, а несколько выходов - на свойства небольшого класса результата. Замените IAsyncCollector<T> на возврат T[] и удалите любой параметр IBinder в пользу внедрённого клиента. Проверка: dotnet build теперь завершается успешно с нулём ошибок.

  7. Обновите local.settings.json. Измените значение среды выполнения worker:

    {
      "IsEncrypted": false,
      "Values": {
        "AzureWebJobsStorage": "UseDevelopmentStorage=true",
        "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated"
      }
    }

    Изменений в host.json не требуется. Проверка: func start запускает приложение локально, и ваши функции появляются в баннере запуска со своими маршрутами.

Проверка

После того как сборка стала зелёной, запустите этот дымовой тест, прежде чем приближаться к Azure:

Развёртывание в Azure и откат

Сторона Azure - это два изменения, которые должны произойти вместе: установить FUNCTIONS_WORKER_RUNTIME в dotnet-isolated и развернуть изолированную полезную нагрузку. Если приземляется только одно, приложение оказывается в состоянии ошибки, потому что развёрнутый код не соответствует настроенной среде выполнения. Никогда не делайте это на месте в продакшене. Вместо этого:

  1. Создайте или повторно используйте слот развёртывания staging.
  2. На слоте staging установите настройку приложения FUNCTIONS_WORKER_RUNTIME в dotnet-isolated. Не помечайте её как настройку слота. Если вы также изменили версию .NET, обновите конфигурацию стека на слоте тоже.
  3. Опубликуйте мигрированный проект в слот staging. Вы увидите временные ошибки в логах слота во время промежуточного состояния; дождитесь, пока они прекратятся.
  4. Проведите дымовой тест слота staging против реальных зависимостей Azure. Подтвердите, что ошибки прекратились и поведение соответствует продакшену.
  5. Поменяйте местами (swap) слот staging в продакшен. Swap происходит как одно атомарное обновление, поэтому продакшен никогда не видит сломанное промежуточное состояние.
  6. Подтвердите, что продакшен здоров.

Откат: это полностью обратимо, пока вы сохраняете in-process-сборку. Чтобы откатиться, поменяйте слоты обратно: предыдущая продакшен-нагрузка (всё ещё in-process) возвращается в продакшен-слот одной операцией. Поскольку swap атомарен, откат - это тот же шаг в один клик, что и развёртывание. Сохраняйте in-process-ветку и её последний хороший артефакт, пока новая модель не отработает под реальным трафиком хотя бы несколько дней. Миграция становится односторонней только в тот момент, когда вы удаляете этот in-process-артефакт, так что не удаляйте его в первый день.

Подводные камни, на которые мы наткнулись

Логи молча исчезают из Application Insights. В модели in-process host.json управлял фильтрацией логов для всего, включая ваш код. В изолированном worker host.json фильтрует только среду выполнения хоста Functions; логи вашего приложения фильтруются конфигурацией логирования в Program.cs. Если вы мигрируете и ваши логи Information исчезают, добавьте явную фильтрацию в Program.cs вместо того, чтобы ожидать, что host.json их покроет.

HTTP-ответы меняют форму, если вы полагались на Newtonsoft. In-process HTTP-триггеры, возвращавшие IActionResult, сериализовались через MVC-форматтеры хоста, которые многие приложения настроили с резолвером контрактов Newtonsoft. Изолированный worker по умолчанию использует System.Text.Json. Если ваш JSON внезапно использует другой регистр или теряет пользовательский конвертер, зарегистрируйте свой сериализатор на worker. Если вы взвешиваете, сохранять ли Newtonsoft вообще, прочитайте System.Text.Json против Newtonsoft.Json в 2026.

Оркестраторы Durable Functions, которые внедряли сервисы, начинают недетерминированно воспроизводиться. DI хоста in-process был достаточно снисходителен, что вызов сервиса с областью экземпляра внутри оркестратора иногда работал случайно. На изолированном worker тот же код может привести к взаимной блокировке или недетерминированно воспроизводиться. Исправление - это стандартное правило Durable, которое модель in-process позволяла нарушать: оркестраторы вызывают только функции активности, а побочные эффекты живут в активностях, а не во внедрённых сервисах.

Приложение становится красным в Azure во время деплоя, и это нормально. В первый раз, когда вы пушите изолированную нагрузку в слот, чей FUNCTIONS_WORKER_RUNTIME всё ещё dotnet (или наоборот), слот сообщает о состоянии ошибки. Это то промежуточное несоответствие, о котором предупреждает документация. Именно поэтому развёртывание идёт через слот staging, и оно проясняется, как только и настройка, и нагрузка совпадают.

Выходные привязки в списке параметров не скомпилируются. Самая распространённая ошибка сборки после смены пакетов - это выходная привязка, всё ещё сидящая как параметр out или IAsyncCollector<T>. Перенесите её на тип возврата (один выход) или класс результата (несколько выходов). Ошибка компилятора указывает на параметр, но исправление структурное, а не приведение типа.

Связанное

Источники

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Назад