Блокировка UI при выполнении HTTP запроса

Стек: C#, MAUI, MVVM, DDD, Refit, Polly, Microsoft.Extensions.Http.Resilience, Microsoft.Extensions.DependencyInjection

Возникла проблема при выполнении HTTP к WebAPI. При выпадении ошибки блочится UI.

System.Threading.Tasks.TaskCanceledException: A task was canceled. at System.Threading.Tasks.TaskCompletionSourceWithCancellation`1.d__1[[System.Net.Http.HttpConnection, System.Net.Http, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a]].MoveNext() at System.Net.Http.HttpConnectionPool.SendWithVersionDetectionAndRetryAsync(HttpRequestMessage request, Boolean async, Boolean doRequestAuth, CancellationToken cancellationToken) at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken) at System.Net.Http.SocketsHttpHandler.g__CreateHandlerAndSendAsync|115_0(HttpRequestMessage request, CancellationToken cancellationToken) at Microsoft.Extensions.Http.Logging.LoggingHttpMessageHandler.g__Core|4_0(HttpRequestMessage request, Boolean useAsync, CancellationToken cancellationToken) at Microsoft.Extensions.Http.Resilience.ResilienceHandler.<>c.<b__3_0>d.MoveNext()

Регистрация HttpClient

using GroupTracker.Application.Interfaces;
using GroupTracker.Infrastructure.Maui.Configuration;
using GroupTracker.Infrastructure.Maui.Interfaces;
using GroupTracker.Infrastructure.Maui.Local.Mappings;
using GroupTracker.Infrastructure.Maui.Persistence;
using GroupTracker.Infrastructure.Maui.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http.Resilience;
using Polly;
using Refit;

namespace GroupTracker.Infrastructure.Maui
{
    public static class InfrastructureMauiDependencyInjection
    {
        public static IServiceCollection AddMauiInfrastructure(this IServiceCollection services)
        {
            services.AddDbContext<MauiDbContext>(options => options.UseSqlite(ConfigurationHelper.GetConnectionString()));
            services.AddAutoMapper(cfg =>
            {
            }, typeof(DomainToInfrastructureMauiEntityProfile).Assembly);
            services.AddSingleton<IAppSettingsService, AppSettingsService>();
            services.AddRefitClient<IApiService>().ConfigureHttpClient((sp, client) =>
            {
                client.BaseAddress = new Uri(sp.GetRequiredService<IAppSettingsService>().GetBaseUrl());
                client.Timeout = TimeSpan.FromSeconds(5);
            }).AddResilienceHandler("api-pipeline", builder =>
            {
                builder.AddRetry(new HttpRetryStrategyOptions { MaxRetryAttempts = 3, BackoffType = DelayBackoffType.Exponential, UseJitter = true });
            });
            services.AddScoped<IMauiDbService, MauiDbService>();
            services.AddSingleton<ITokenService, TokenService>();
            return services;
        }
    }
}

ApiService

using GroupTracker.Application.DTOs;
using GroupTracker.Contracts.Responses;
using Refit;

namespace GroupTracker.Infrastructure.Maui.Interfaces
{
    public interface IApiService
    {
        [Post("/Authentication/Verification")]
        Task<Response<AuthResponseDto>> VerificationAsync([Body] UserCredentialsDto credentials);
        [Post("/Users/AssignUniversity")]
        Task<Response<bool>> AssignUniversityAsync(long userId, [Body] UserDto user);
        [Get("/Universities")]
        Task<Response<List<UniversityDto>>> GetUniversitiesAsync();
        [Get("/Profiles")]
        Task<Response<List<ProfileTypeDto>>> GetProfilesAsync();
    }
}

Вызов API с клиента

namespace GroupTracker.MauiClient.ViewModels
{
    [ChildOf(typeof(StartViewModel))]
    public class RegistrationUserViewModel : BaseContentViewModel
    {
        private readonly IApiService _apiService;
        private readonly IMapper _mapper;
        private string _fullName;
        private DateTime _age = DateTime.Today.AddYears(-18);
        private List<ProfileTypeModel> _profiles;
        private ProfileTypeModel _selectedProfile;
        public RegistrationUserViewModel(IApiService apiService, IMapper mapper)
        {
            _apiService = apiService;
            _mapper = mapper;
        }

        protected override async Task InitializeAsync() => await ExecuteWithLoadingAsync(async () =>
        {
            var profilesResult = await ExecuteRequestAsync(() => _apiService.GetProfilesAsync(), data => _mapper.Map<List<ProfileTypeModel>>(data), err => ErrorType = err) ?? new List<ProfileTypeModel>();
            Profiles = profilesResult;
        });

Обертки

protected async Task<TResult?> ExecuteRequestAsync<TResponse, TResult>(Func<Task<Response<TResponse>>> action, Func<TResponse, TResult> mapFunc, Action<ErrorType>? onError = null)
{
    try
    {
        var result = await action().ConfigureAwait(false);
        ;
        if (!result.IsSuccess)
        {
            onError?.Invoke(result.ErrorType);
            return default;
        }

        if (result.Data == null)
        {
            onError?.Invoke(ErrorType.EmptyData);
            return default;
        }

        return mapFunc(result.Data);
    }
    catch (TaskCanceledException ex)
    {
        if (!ex.CancellationToken.IsCancellationRequested || ex.InnerException is TimeoutException)
            onError?.Invoke(ErrorType.ServerUnreachable);
        else
            onError?.Invoke(ErrorType.Default);
    }
    catch (HttpRequestException)
    {
        onError?.Invoke(ErrorType.ServerUnreachable);
    }
    catch (ApiException)
    {
        onError?.Invoke(ErrorType.ServerUnreachable);
    }
    catch (Exception)
    {
        onError?.Invoke(ErrorType.Default);
    }

    return default;
}

protected async Task ExecuteWithLoadingAsync(Func<Task> action)
{
    Loading = true;
    try
    {
        await action();
    }
    finally
    {
        Loading = false;
    }
}

public virtual async Task OnAppearingAsync()
{
    Task? taskToWait;
    lock (_initLock)
    {
        if (_initializationTask != null)
            taskToWait = _initializationTask;
        else
            taskToWait = InitializeAsyncSafe();
    }

    try
    {
        await taskToWait;
    }
    finally
    {
        lock (_initLock)
            if (_initializationTask == taskToWait)
                _initializationTask = null;
    }
}

private async Task InitializeAsyncSafe()
{
    try
    {
        await InitializeAsync();
    }
    catch
    {
        lock (_initLock)
            _initializationTask = null;
        throw;
    }
}

protected virtual Task InitializeAsync() => Task.CompletedTask;

Фриз на UI При преобразовании gif фриз срезался. Когда анимация останавливается фриз примерно на пол секунды - секунду.


Ответы (1 шт):

Автор решения: Ilya Atykov

Проблема в эмуляторе. HTTP не блокировал и все потоки корректно работали и обрабатывались. На локальном устройстве фриз пропал.

→ Ссылка