This question already has answers here:
Blazor problem rendering data returned from Web Api
(2 answers)
Closed 1 year ago.
I built an API in ASP.NET Core and the code looks like this:
public async Task<IEnumerable<Applicant>> GetApplicants()
{
return await appDbContext.Applicants.ToListAsync();
}
[HttpGet]
public async Task<ActionResult> GetApplicants()
{
try
{
return Ok(await applicantRepository.GetApplicants());
}
catch (Exception)
{
return StatusCode(StatusCodes.Status500InternalServerError, "Error retreiving data from the database");
}
}
Here we have how it looks in browser(guess that is fine):
Blazor (server) code:
public interface IApplicantService
{
Task<IEnumerable<Applicant>> GetApplicants();
}
public class ApplicantService : IApplicantService
{
private readonly HttpClient httpClient;
public ApplicantService(HttpClient httpClient)
{
this.httpClient = httpClient;
}
public async Task<IEnumerable<Applicant>> GetApplicants()
{
return await httpClient.GetJsonAsync<Applicant[]>("api/applicants");
}
}
public class ApplicantList : ComponentBase
{
[Inject]
public IApplicantService ApplicantService { get; set; }
public IEnumerable<Applicant> Applicants { get; set; }
protected override async Task OnInitializedAsync()
{
Applicants = (await ApplicantService.GetApplicants()).ToList();
}
}
And the page:
#page "/"
#inherits ApplicantList
<h1>Applicants</h1>
<div class="card-deck">
#foreach (var applicant in Applicants)
{
<div class="card m-3" style="min-width: 18rem; max-width:30.5%;">
<div class="card-header">
<h3>#applicant.Name</h3>
</div>
<div class="card-footer text-center">
View
Edit
Delete
</div>
</div>
}
</div>
I am facing null reference error. While debugging I see that Applicants is null
The solution to this is simple: wrap the foreach loop with an if statement like this:
#if( Applicants != null)
{
#foreach (var applicant in Applicants)
{
//...
}
}
Explanation: When you call the ApplicantService.GetApplicants() method in the lifecycle OnInitializedAsync method, the following occurs:
ApplicantService.GetApplicants() is called and awaited, the execution control is yielded to the calling code, till GetApplicants() completes...Blazor starts rendering the view portion of your component, but alas, the Applicants variable is not yet populated with data. It contains the null value, thus the exception.
Note: that when the GetApplicants() method completes, re-rendering occurs again, this time the Applicants variable contains the retrieved data.
Note: Don't use the GetJsonAsync method. Use instead the new set of methods and objects: Install-Package System.Net.Http.Json -Version 5.0.0
Related
I'm using an API to show every pokemon in the pokedex, however after I add the JSON returned values to my public List<Data> pokemonDataList by calling the API with pokemon.results[i].url the #if (pokemon != null) statemen shows that pokemon is null again. I tried the debuggin the code and it shows how its getting all the values from the API call, but somehow after it finishes it sets everything back to null?
`
#page "/"
#using System.Globalization
#inject HttpClient Client
#using System.Net.Http.Json
#using System.Net.Http
#inject HttpClient Http
#if (pokemon != null) //Shows null, does not enter here
{
for (int i = 0; i < pokemon.results.Length; i++)
{
<div class="div2">
<p>#pokemon.results[i].name</p>
#if (pokemonDataList != null)
{
<img src="#pokemonDataList[i].sprites.front_default" width="250px" height="250px" />
}
</div>
}
}
#code {
public Rootobject pokemon { get; set; }
Data pokemonData;
public List<Data> pokemonDataList { get; set; } = new List<Data>();
protected override async Task OnInitializedAsync() // Orria kargatzerakoan erakutsiko du
{
await GetPokemon();
}
async Task GetPokemon()
{
pokemon = await Client.GetFromJsonAsync<Rootobject>(Endpoints.GetPokemonById());
for (int i = 0; i < pokemon.results.Length; i++)
{
pokemonDataList.Add(await GetPokemonData(i)); // Gets every pokemon on the list 1154
}
}
async Task<Data> GetPokemonData(int i)
{
return await Http.GetFromJsonAsync<Data>(pokemon.results[i].url);
}
}
`
I tried various if(!null) and while(!null) but it doesn't seem to work
Edit: After debugging it and changing #if (pokemon != null) to #if (pokemon == null) it looks like the methods are executed afterwards loading the html, despite putting await on the methods.
I am working in a larger project, but because it is very complex I made my own small project to make an Azure Blobs Gallery, that displays images from Azure Blobs using container, to implement into the large project after that.
I was able to create this Gallery with the following .Net 6 / C# (MVC) code:
Controller
public class ResourceController : Controller
{
const string blobContainerName = "thumbnails";
static BlobContainerClient blobContainer;
private IConfiguration _configuration;
public ResourceController(IConfiguration configuration)
{
_configuration = configuration;
}
public ActionResult Index()
{
var model = LoadModelMethodAsync();
return View(model);
}
public ActionResult Info()
{
var model = LoadModelMethodAsync();
return View(model);
}
public List<Uri> LoadModelMethodAsync()
{
try
{
var s = _configuration.GetConnectionString("AzureStorageConnectionString");
BlobServiceClient blobServiceClient = new BlobServiceClient(s);
blobContainer = blobServiceClient.GetBlobContainerClient(blobContainerName);
blobContainer.CreateIfNotExistsAsync(PublicAccessType.Blob);
List<Uri> allBlobs = new List<Uri>();
foreach (BlobItem blob in blobContainer.GetBlobs())
{
if (blob.Properties.BlobType == BlobType.Block)
allBlobs.Add(blobContainer.GetBlobClient(blob.Name).Uri);
}
return allBlobs;
}
catch (Exception ex)
{
ViewData["message"] = ex.Message;
ViewData["trace"] = ex.StackTrace;
throw;
}
}
}
}
ViewModel
using Microsoft.AspNetCore.Mvc;
namespace EBP_V1.ViewModels {
public class ResourceViewModel
{
public string RequestId { get; set; }
public bool? ShowRequestId => !string.IsNullOrEmpty(RequestId);
}
}
Partial
#using AzureBlobLearning.Models
#model List<Uri>
#{
}
#if (Model != null && Model.Count > 0)
{
foreach (var item in Model)
{
<div class="imageBlock" value="GetGallery">
<img class="thumb" src="#item" alt="images"/><br />
<div class="deleteDiv"><img class="deleteIcon" src="~/Images/deleteImage.png" title="Delete Image" onclick="deleteImage('#item');" /></div>
</div>
}
}
View: Info.cshtml
<partial name="~/Views/Resource/_ResourcePartial.cshtml"/>
This small project works well, but when I try to implement it in the large project, the page isn't working anymore, even though it doesn't throw any error, and when I try to debug it all the data the same as it was before.
Controller in the large project ( everything else is identical with the previous one)
[HttpGet]
public async Task<ViewResult> Info(Guid? id, string filterTxt )
{
await SetCompanyName(id, filterTxt);
var model = LoadModelMethodAsync();
return View(model);
}
The interesting thing is that even if I don't call the model variable in the View the page still won't work anymore.
Question:
Why my code doesn't work in the large project?
What should I change to make it work?
P.S.:
- I am a completely beginner in .Net 6 / C# world, my first language
is Javascript, so please be understanding.
- If you have any questions, suggestions don't hesitate to ask.
I have API calls utility in my blazor web project. I have added condition where if I get unauthorized response from API, I am throwing the unauthorized error and trying to catch it in program.cs file so I can redirect user to login page. while throwing error blazor engine returning error in browser.
Utitlity.cs
public async Task<CurrentResponse> GetAsync(IHttpClientFactory _httpClient, string url)
{
try
{
var request = new HttpRequestMessage(HttpMethod.Get, url);
request.Headers.Clear();
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", GetClaimValue(CustomClaimTypes.AccessToken));
var client = _httpClient.CreateClient("FSMAPI");
HttpResponseMessage httpResponseMessage = await client.SendAsync(request);
if(httpResponseMessage.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
ManageUnAuthorizedError();
}
CurrentResponse response = JsonConvert.DeserializeObject<CurrentResponse>(httpResponseMessage.Content.ReadAsStringAsync().Result);
return response;
}
catch (Exception exc)
{
throw exc;
}
}
private void ManageUnAuthorizedError(/*IHttpClientFactory _httpClient*/)
{
throw new UnauthorizedAccessException(HttpStatusCode.Unauthorized.ToString());
}
Program.cs
app.UseExceptionHandler(c => c.Run(async context =>
{
var exception = context.Features
.Get<IExceptionHandlerPathFeature>()
.Error;
var response = new { error = exception.Message };
if(exception.Message == HttpStatusCode.Unauthorized.ToString())
{
context.Response.Redirect("/Login");
}
}));
Here's a possible solution based on the information you've provided in the question.
You need to interact with the UI through Services
A notification service:
public class NeedToAuthenticateService
{
public string ErrorMessage { get; set; } = string.Empty;
public event EventHandler? AuthenticationRequired;
public void NotifyAuthenticationRequired()
=> AuthenticationRequired?.Invoke(this, new EventArgs());
}
This is a "simple" emulation of your API call done through a service that interfaces with the NeedToAuthenticateService and raises the AuthenticationRequired event.
public class APIReaderService
{
private NeedToAuthenticateService needToAuthenticateService;
public APIReaderService(NeedToAuthenticateService needToAuthenticateService)
{
this.needToAuthenticateService = needToAuthenticateService;
}
public void GetData()
{
// If you get an error
needToAuthenticateService.ErrorMessage = "You need to log in!";
needToAuthenticateService.NotifyAuthenticationRequired();
}
}
A simple demo Login page showing the message.
#page "/Logon"
<h3>Logon</h3>
#inject NeedToAuthenticateService needToAuthenticateService
<div class="p-3">
#this.needToAuthenticateService.ErrorMessage
</div>
#code {
}
A modified MainLayout page which registers and event handler with NeedToAuthenticateService and triggers a navigate event when AuthenticationRequired is raised.
#inherits LayoutComponentBase
#inject NeedToAuthenticateService needToAuthenticateService
#inject NavigationManager NavManager
#implements IDisposable
<PageTitle>BlazorApp1</PageTitle>
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<main>
<div class="top-row px-4">
About
</div>
<article class="content px-4">
#Body
</article>
</main>
</div>
#code {
protected override void OnInitialized()
=> this.needToAuthenticateService.AuthenticationRequired += GoToLogIn;
private void GoToLogIn(object? sender, EventArgs e)
=> NavManager.NavigateTo("/Logon");
public void Dispose()
=> this.needToAuthenticateService.AuthenticationRequired -= GoToLogIn;
}
And finally the registered services in Program
builder.Services.AddSingleton<WeatherForecastService>();
builder.Services.AddScoped<APIReaderService>();
builder.Services.AddScoped<NeedToAuthenticateService>();
Ok i have a wasm App that call the server side web api endpoints.
The problem is that I get the index.html page from the /wwwroot directory for an endpoint as an answer. But when I address the endpoint with Postman I get the json answer as expected.
Okay i will show how i do this with my code.
Client Side data flow
Search.razor Page
Here I call the Web API endpoint when a search text has been entered in the form field. That works as expected.
... Snip
// UPDATED INFO
<div class="form-group">
<label for="objectType">Select object type</label>
<select id="objectType" class="form-control" #bind="#_searchByNameObjectTypeUuid">
#if (_objectTypes != null)
{
#foreach (var objectType in _objectTypes)
{
#if (objectType.TypeName == "Music")
{
#* This selection value is not set. But why?
<option value="#objectType.Uuid.ToString("D")" selected="selected">#objectType.TypeName</option>
}
else
{
<option value="#objectType.Uuid.ToString("D")">#objectType.TypeName</option>
}
}
}
</select>
</div>
// UPDATED INFO END
<div class="form-group">
<label for="objectName">Object name:<br/></label>
<input type="text" class="form-control" id="objectName" #onkeydown="#SearchByNameOnEnter" #bind-value="#_searchByNameObjectNamePhrase" #bind-value:event="oninput"/>
</div>
...Snip
#code {
private string _searchByNameObjectNamePhrase = string.Empty;
private async Task SearchByNameOnEnter(KeyboardEventArgs e)
{
if ((e.Code == "Enter" || e.Code == "NumpadEnter") && !string.IsNullOrWhiteSpace(_searchByNameObjectNamePhrase))
{
_searchResult = await ServerApiClient.SearchObjectsByNamePhrase(_searchByNameObjectTypeUuid, _searchByNameObjectNamePhrase);
}
}
}
ServerApiClientService.cs Web API Client service
With this I call different Web API endpoints that get the data from a database in the backend.
The GetDdsObjectAttributeValueCount() method works as expected.
The method SearchObjectsByNamePhrase(string objTypeUuid, string searchTermPhrase) sends me the file /wwwroot/index.html as an answer. (Show comments in code for details)
namespace WebAssembly.Client.Services
{
public class ServerApiClientService : IServerApiClientService
{
#region Constants - Static fields - Fields
private readonly HttpClient _httpClient;
#endregion
#region Constructors and Destructors
public ServerApiClientService(HttpClient httpClient)
{
_httpClient = httpClient;
}
#endregion
#region Methods
// This endpoint request work as expected
public async Task<IEnumerable<ObjectAttributeValueCount>> GetDdsObjectAttributeValueCount()
{
IEnumerable<ObjectAttributeValueCount> result =
await _httpClient
.GetFromJsonAsync<List<ObjectAttributeValueCount>>("/api/DdsDashboard/GetDdsObjectAttributeValueCount");
return (result ?? Array.Empty<ObjectAttributeValueCount>()).AsQueryable();
}
// This endpoint request NOT work as expected
public async Task<IEnumerable<SearchResultItem>> SearchObjectsByNamePhrase(string objTypeUuid, string searchTermPhrase)
{
// For test i have called as string and i get HTML response. wwwroot/index.html is comming back.
var asJsonString =
await _httpClient
.GetStringAsync($"/api/DdsDashboard/SearchObjectsByNamePhrase/{objTypeUuid}/{searchTermPhrase}");
// And here i get the exception "System.Text.Json.JsonReaderException"
// '<' is an invalid start of a value
IEnumerable<SearchResultItem> result =
await _httpClient
.GetFromJsonAsync<List<SearchResultItem>>($"/api/DdsDashboard/SearchObjectsByNamePhrase/{objTypeUuid}/{searchTermPhrase}");
return (result ?? Array.Empty<SearchResultItem>()).AsQueryable();
}
#endregion
}
}
Server Side data flow
DdsDashboardController.cs as the Web API Controller
All methods (routes) in this controller work perfectly when I address them with Postman.
The route [HttpGet("GetDdsObjectAttributeValueCount")] and the route [HttpGet("GetDdsObjectTypeStatistic")] also work with the ServerApiClientService.cs.
Only the route [HttpGet ("SearchObjectsByNamePhrase / {objTypeId} / {searchTerm}")] only works in Postman.
namespace WebAssembly.Server.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class DdsDashboardController : ControllerBase
{
#region Constants - Static fields - Fields
private readonly IDdsRepository _repository;
#endregion
#region Constructors and Destructors
public DdsDashboardController(IDdsRepository repo)
{
_repository = repo;
}
#endregion
#region Methods
[HttpGet("GetDdsObjectAttributeValueCount")]
public async Task<IEnumerable<ObjectAttributeValueCount>> GetDdsObjectAttributeValueCount()
{
return await _repository.GetDdsObjectAttributeValueCount();
}
[HttpGet("GetDdsObjectTypeStatistic")]
public async Task<IEnumerable<ObjectTypeStatistic>> GetDdsObjectTypeStatistic()
{
return await _repository.GetDdsObjectTypeStatistic();
}
// This method is called and worked as expected. When i call this endpoint with Postman all is fine. Correct JSON response.
[HttpGet("SearchObjectsByNamePhrase/{objTypeId}/{searchTerm}")]
public async Task<IEnumerable<SearchResultItem>> SearchObjectsByNamePhrase(string objTypeId, string searchTerm)
{
// Correct result from my database. I have checked with an breakpoint.
var result = await _repository.SearchObjectsByNamePhrase(objTypeId, searchTerm);
return result;
}
#endregion
}
}
Startup.cs
Configure method
public void Configure(IApplicationBuilder app)
{
if (Env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseWebAssemblyDebugging();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
app.UseHttpsRedirection();
}
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapControllers();
endpoints.MapFallbackToFile("index.html");
});
}
ConfigureServices method
public void ConfigureServices(IServiceCollection services)
{
SqlMapper.AddTypeHandler(new MySqlGuidTypeHandler());
SqlMapper.RemoveTypeMap(typeof(Guid));
SqlMapper.RemoveTypeMap(typeof(Guid?));
services.AddControllersWithViews();
services.AddRazorPages();
services.AddScoped<IDdsRepository, DdsRepository>();
var dbConnectionSettings = new DdsDbConnectionConfiguration(Configuration.GetSection("DdsDbSettings"));
services.AddSingleton(dbConnectionSettings);
if (!Env.IsDevelopment())
{
services.AddHttpsRedirection(options =>
{
options.RedirectStatusCode = StatusCodes.Status308PermanentRedirect;
options.HttpsPort = 443;
});
}
}
Request with Postman
I hope I have given enough information to be able to give me an indication of why this not work.
Update
Ok. The problem is that the binding for the form control worked only when i make manualy a selection change. Set a selected while rendering is not working.
<div class="form-group">
<label for="objectType">Select object type</label>
<select id="objectType" class="form-control" #bind="#_searchByNameObjectTypeUuid">
#if (_objectTypes != null)
{
#foreach (var objectType in _objectTypes)
{
#if (objectType.TypeName == "Music")
{
<option value="#objectType.Uuid.ToString("D")" selected="selected">#objectType.TypeName</option>
}
else
{
<option value="#objectType.Uuid.ToString("D")">#objectType.TypeName</option>
}
}
}
</select>
</div>
And that's why the _searchByNameObjectTypeUuid value is not set.
And with that endpoints.MapFallbackToFile(" index.html ").
I have set the value of _searchByNameObjectTypeUuid in the OnInitializedAsync() method where also i load the _objectTypes.
protected override async Task OnInitializedAsync()
{
_objectTypes = await DdsApiClient.GetObjectTypes();
_searchByNameObjectTypeUuid = _objectTypes.SingleOrDefault(x => x.TypeName == "Music")?.Uuid.ToString("D");
}
If anyone knows how to set the value with the foreach loop while rendering, I would be grateful to hear about it.
Thanks to #Neil W for help.
I don't have an answer for your question directly, but when I first started encountering challenges with WebAPI from Blazor wasm client, I create a client API base class, thus:
public abstract class ClientAPI
{
protected readonly HttpClient Http;
private readonly string BaseRoute;
protected ClientAPI(string baseRoute, HttpClient http)
{
BaseRoute = baseRoute;
Http = http;
}
protected async Task<TReturn> GetAsync<TReturn>(string relativeUri)
=> await ProcessHttpResponse<TReturn>(await Http.GetAsync($"{BaseRoute}/{relativeUri}"));
protected async Task<TReturn> PostAsync<TReturn, TRequest>(string relativeUri, TRequest request)
=> await ProcessHttpResponse<TReturn>(await Http.PostAsJsonAsync($"{BaseRoute}/{relativeUri}", request));
private static async Task<TReturn> ProcessHttpResponse<TReturn>(HttpResponseMessage response)
{
if (response.IsSuccessStatusCode)
return await response.Content.ReadFromJsonAsync<TReturn>();
string msg = await response.Content.ReadAsStringAsync();
Console.WriteLine(msg);
throw new Exception(msg);
}
}
Then my derived Client API class would call GetAsync on the base class. That will then either resolve the Json response or if the HttpResponseMessage had a failure status code, it would log the error.
Use from derived class like this:
public class BackOfficeClientAPI : ClientAPI
{
public BackOfficeClientAPI(HttpClient http) : base("api/backoffice", http) { }
public async Task<IEnumerable<Category>> GetCategoriesAsync(Guid organisationId)
=> await GetAsync<IEnumerable<Category>>($"getcategories?organisationId={organisationId}");
public async Task<Category> AddCategoryAsync(AddCategoryRequest request)
=> await PostAsync<Category, AddCategoryRequest>("addcategory", request);
PS. I'm using querystring instead of route parameters, but the principle is the same.
I've found it a nice pattern to catch exceptions of this type.
In my Blazor WASM application, I have written a (client-side) service class with a method to make an API call to the web API. The server will return either the expected result of IEnumerable<WeatherForecast> or a Microsoft.AspNetCore.Mvc.ProblemDetails object explaining what went wrong.
When calling the method, the UI (FetchData.razor) passes an Action<IEnumerable<WeatherForecast>> and an Action<ProblemDetails>. Only one of these actions should ever be executed, depending on what is returned by the server. This allows the service class to choose what to do based on the deserialized JSON result of the API call.
Usage (in 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));
}
}
My attempt at implementation, below, does not work. I am sure that the API call is reaching the correct API endpoint and getting JSON back, but my razor page is not getting populated with the WeatherForecasts and it is not writing the problem detail to the console either. Debugging in Blazor WASM (though much improved) is still quite difficult.
I have been fiddling with this code for days but have failed. Can anybody help me see what I am doing wrong please?
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;
}
}
}
A minimal reproducible example of my failed attempt at this problem can be found here:
https://github.com/BenjaminCharlton/AsyncBlazorRepro
Thank you!
Fixed it!
This problem had nothing to do with async-await problems. It was all to do with deserialization problems.
Looking at the ASP .NET Core source code here:
https://github.com/dotnet/aspnetcore/blob/master/src/Components/Blazor/Http/src/HttpClientJsonExtensions.cs
You'll notice that the methods in Microsoft.AspNetCore.Components.HttpClientJsonExtensions all pass a JsonSerializerOptions to the Deserialize method, but in my code I was just passing null because I didn't think it was important. The JsonSerializer was ignoring every single property because of case-sensitivity!
I changed my Deserialize method as below:
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;
}
}
As Henk pointed out in the comments, I had also written in some unnecessary complexity. I didn't need to turn the Actions into Tasks using my pointless TaskFromAction method. You can just leave them as Actions. You can also create an overload that takes Func<TExpected, Task> if you want to give callers an asynchronous option too.
I have updated the repro project on GitHub with working code in case anybody else wishes to encapsulate their Blazor API calls this way.
https://github.com/BenjaminCharlton/AsyncBlazorRepro