传递两个 Action<T> 参数并根据异步 API 调用的 JSON 结果执行其中之一

Passing two Action<T> parameters and executing one of them based on the JSON result of an async API call

在我的 Blazor WASM 应用程序中,我编写了一个(客户端)服务 class,其中包含一个 API 调用 Web API 的方法。服务器将 return IEnumerable<WeatherForecast> 的预期结果或 Microsoft.AspNetCore.Mvc.ProblemDetails 解释错误原因的对象。

调用方法时,UI (FetchData.razor) 传递一个 Action<IEnumerable<WeatherForecast>> 和一个 Action<ProblemDetails>。根据服务器 return 编辑的内容,只应执行其中一项操作。这允许服务 class 根据 API 调用的反序列化 JSON 结果选择要执行的操作。

用法(在FetchData.razor):

@page "/fetchdata"
@using BlazorApp1.Shared
@inject HttpClient Http
@inject WeatherForecastsService Service

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from the server.</p>

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    private IEnumerable<WeatherForecast> forecasts;

    protected override async Task OnInitializedAsync()
    {
        await Service.GetAllAsync(
            success => forecasts = success,
            problem => Console.WriteLine("Handle this problem: " + problem.Detail));
    }

}

我在下面的实施尝试不起作用。我确定 API 调用到达正确的 API 端点并返回 JSON,但我的剃须刀页面没有填充 WeatherForecasts,也没有将问题详细信息写入控制台。在 Blazor WASM 中调试(虽然有很大改进)仍然相当困难。

几天来我一直在摆弄这段代码,但都失败了。谁能帮我看看我做错了什么?

    public class WeatherForecastsService : ServiceBase
    {
        public WeatherForecastsService(
            HttpClient client) : base(client)
        {

        }

        public async Task GetAllAsync(
            Action<IEnumerable<WeatherForecast>> actionOnSuccess,
            Action<ProblemDetails> actionOnFailure,
            CancellationToken cancellationToken = default)
        {
            await GetManyAsync("weatherforecast",
                actionOnSuccess,
                actionOnFailure,
                cancellationToken);
        }
    }

   public abstract class ServiceBase
    {
        public ServiceBase(HttpClient client)
        {
            Client = client;
        }

        protected HttpClient Client
        {
            get;
        }


        protected virtual async Task GetManyAsync<TExpected>(
            string path,
            Action<IEnumerable<TExpected>> actionOnSuccess,
            Action<ProblemDetails> actionOnProblem,
            CancellationToken cancellationToken = default)
            where TExpected : class
        {
            string json = await GetJsonAsync(path, cancellationToken);
            ProblemDetails? problem = Deserialize<ProblemDetails>(json);

            if (problem is { })
            {
                var taskOnProblem = TaskFromAction(actionOnProblem, problem);
                await taskOnProblem;
            }
            else
            {
                IEnumerable<TExpected>? expected = Deserialize<IEnumerable<TExpected>>(json);
                expected = EnsureNotNull(expected);

                var taskOnSuccess = TaskFromAction(actionOnSuccess, expected);
                await taskOnSuccess;
            }
        }

        private Task TaskFromAction<T>(Action<T> action, T state)
        {
            return new Task(ActionOfObjectFromActionOfT(action), state);
        }

        private Action<object> ActionOfObjectFromActionOfT<T>(Action<T> actionOfT)
        {
            return new Action<object>(o => actionOfT((T)o));
        }

        private IEnumerable<T> EnsureNotNull<T>(IEnumerable<T>? enumerable)
        {
            if (enumerable is null)
            {
                enumerable = new List<T>();
            }

            return enumerable;
        }

        private async Task<string> GetJsonAsync(string path, CancellationToken cancellationToken = default)
        {
            var response = await Client.GetAsync(path, cancellationToken);
            return await response.Content.ReadAsStringAsync();
        }


        private T? Deserialize<T>(string json)
            where T : class
        {
            try
            {
                return JsonSerializer.Deserialize<T>(json, null);
            }
            catch (JsonException)
            {
                return default;
            }
        }
    }


可以在此处找到我尝试解决此问题的失败的最小可重现示例: https://github.com/BenjaminCharlton/AsyncBlazorRepro

谢谢!

已修复!

此问题与异步等待问题无关。这一切都与反序列化问题有关。

在此处查看 ASP .NET Core 源代码:

https://github.com/dotnet/aspnetcore/blob/master/src/Components/Blazor/Http/src/HttpClientJsonExtensions.cs

你会注意到 Microsoft.AspNetCore.Components.HttpClientJsonExtensions 中的方法都将 JsonSerializerOptions 传递给 Deserialize 方法,但在我的代码中我只是传递 null 因为我不认为这很重要。由于区分大小写,JsonSerializer 忽略了每一个 属性!

我改变了我的反序列化方法如下:

       private T? Deserialize<T>(string json)
            where T : class
        {
            var jsonOptions = new JsonSerializerOptions()
            {
                PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
                PropertyNameCaseInsensitive = true
            };

            try
            {
                return JsonSerializer.Deserialize<T>(json, jsonOptions);
            }
            catch (JsonException)
            {
                return default;
            }
        }

正如 Henk 在评论中指出的那样,我也写了一些不必要的复杂内容。我不需要使用毫无意义的 TaskFromAction 方法将 Actions 变成 Tasks。您可以将它们保留为 Actions。如果您也想为调用者提供异步选项,您还可以创建一个采用 Func<TExpected, Task> 的重载。

我已经用工作代码更新了 GitHub 上的重现项目,以防其他人希望以这种方式封装他们的 Blazor API 调用。

https://github.com/BenjaminCharlton/AsyncBlazorRepro