Exception not handling globally in .net core blazor - c#

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>();

Related

Blazor Server - Local Storage after login

I'm working on a Blazor Server project using the default Microsoft Identity Platform.
My goal is to get/create a user in my db and save it to local storage after microsoft login is completed.
In the startup I'm sucessfully able to use the OnTokenValidated event to do some action after login. However it's not possible to write to local storage in this stage since the page isn't rendered yet.
I'd like to do something like this which is possible with Webassembly.
<RemoteAuthenticatorView Action="#Action" OnLogInSucceeded="SomeCode" />
Does anyone know a way to do this without using a solution like adding OnAfterRenderAsync in the MainLayout, which will fire on each page reload. I'd like to call a method after the Identity login redirects back to my site in a state where LocalStorage is accessible.
You do need to put some code in OnAfterRenderAsync, but in this demo I've put it in App and check if the component has already rendered.
Here's a demo and some code to interact with LocalStorage. You should be able to adapt it to fit your needs.
First a service to encapsulate getting and setting to Local Storage
using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
public class LocalStorageService
{
private readonly ProtectedLocalStorage _storage;
public LocalStorageService(ProtectedLocalStorage storage)
=> _storage = storage;
public async ValueTask<CommandResult> SaveAsync<TRecord>(CommandRequest<TRecord> request)
{
if (request.Record is not null)
await _storage.SetAsync(request.StorageName, request.Record);
// No return so we return success!
return CommandResult.Success();
}
public async ValueTask<RecordQueryResult<TRecord>> ReadAsync<TRecord>(RecordQueryRequest<TRecord> request)
{
// We need to cover the situation were the component calling this is in the initial page
// and Blazor server is trying to statically render the page
try
{
var result = await _storage.GetAsync<TRecord>(request.StorageName);
return new RecordQueryResult<TRecord> { Successful = result.Success, Record = result.Value, Message = $"Failed to retrieve a value for {request.StorageName}" };
}
catch
{
return new RecordQueryResult<TRecord> { Successful = false, Message = $"Failed to retrieve a value for {request.StorageName}" };
}
}
}
The CQS Request and Result objects:
public record CommandRequest<TRecord>(string StorageName, TRecord Record);
public record RecordQueryRequest<TRecord>(string StorageName);
public record CommandResult
{
public bool Successful { get; init; }
public string Message { get; init; } = string.Empty;
public static CommandResult Success()
=> new CommandResult { Successful = true };
public static CommandResult Failure(string message)
=> new CommandResult { Successful = false };
}
public record RecordQueryResult<TRecord>
{
public TRecord? Record { get; init; }
public bool Successful { get; init; }
public string Message { get; init; } = string.Empty;
public static RecordQueryResult<TRecord> Success(TRecord record)
=> new RecordQueryResult<TRecord> { Record = record, Successful = true };
public static RecordQueryResult<TRecord> Failure(string message)
=> new RecordQueryResult<TRecord> { Successful = false };
}
Registered like this:
builder.Services.AddScoped<LocalStorageService>();
My simple Data:
public record TestData( string LastSaved);
Add code to App to set as if you are getting data after login. This implements a custom after render handler.
#inject LocalStorageService Service
#implements IHandleAfterRender
<CascadingAuthenticationState>
<Router AppAssembly="#typeof(App).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="#routeData" DefaultLayout="#typeof(MainLayout)" />
<FocusOnNavigate RouteData="#routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="#typeof(MainLayout)">
<p role="alert">Sorry, theres nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>
#code {
private bool _hasCalledOnAfterRender;
// implements a custom IHandleAfterRender handler
async Task IHandleAfterRender.OnAfterRenderAsync()
{
// Only do if first render and the data in local storage is empty
if (!_hasCalledOnAfterRender && !await GetData())
{
var newData = new TestData($"Saved at {DateTime.Now.ToLongTimeString()}");
var result = await this.Service.SaveAsync<TestData>(new CommandRequest<TestData>("TestData", newData));
_hasCalledOnAfterRender = true;
}
}
private async Task<bool> GetData()
{
var result = await this.Service.ReadAsync<TestData>(new RecordQueryRequest<TestData>("TestData"));
return result?.Successful ?? false;
}
}
And my test route/page to display the data.
#page "/"
#inject LocalStorageService Service
#implements IDisposable
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<SurveyPrompt Title="How is Blazor working for you?" />
<div class="bg-black text-white m-3">
Test Data Last Saved at : #this.data.LastSaved
</div>
<div class="m-3">
<button class="btn btn-primary" #onclick=SaveToLocal>Save Data to Local</button>
</div>
#code {
private TestData data = new TestData(string.Empty);
protected override async Task OnInitializedAsync()
{
await this.GetData();
this.Service.StorageChanged += this.DataChanged;
}
private async void DataChanged(object? sender, EventArgs e)
{
await this.GetData();
await this.InvokeAsync(StateHasChanged);
}
private async Task<bool> GetData()
{
var result = await this.Service.ReadAsync<TestData>(new RecordQueryRequest<TestData>("TestData"));
data = result?.Record ?? new TestData(string.Empty);
return result?.Successful ?? false;
}
private async Task SaveToLocal()
{
var newData = new TestData($"Saved at {DateTime.Now.ToLongTimeString()}");
var result = await this.Service.SaveAsync<TestData>(new CommandRequest<TestData>("TestData", newData));
await this.GetData();
}
public void Dispose()
=> this.Service.StorageChanged -= this.DataChanged;
}

onpaste input event in blazor and typescript

The #onpaste is a blazor DOM event executed when content is pasted in an input.
now, I want to get this text content from typescript (blazor doesn't support it).
razor:
#inject IJSRuntime _Js
<input class="otp-input"
#ref="myInputRef"
#onpaste="HandleOnPaste" />
#code {
private ElementReference myInputRef { get; set; }
private async Task HandleOnPaste()
{
var pastedData = await _js.InvokeAsync<string?>("OtpInput.getPastedData")
// DO Something
}
}
ts:
class OtpInput {
static getPastedData(e: ClipboardEvent) {
let pastedData = e.clipboardData!.getData('text');
return pastedData;
}
}
this does not work and return error: TypeError: Cannot read properties of undefined (reading 'clipboardData')
You can use a different approach. Add event listener to the input element using javascript then call dotnet method and pass the event data. I use javascript but you can modify accordingly for typescript.
pasteInteropHelper.js:
export function addOnPasteEventListener(elem, componentInstance) {
elem.onpaste = (e) => {
var text = e.clipboardData.getData('text');
componentInstance.invokeMethod('HandlePaste', text);
}
}
razor component:
#inject IJSRuntime JS
<input #ref="_inputRef" />
#code {
private ElementReference _inputRef;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
var module = await JS.InvokeAsync<IJSObjectReference>(
"import", "./pasteInteropHelper.js");
var selfReference = DotNetObjectReference.Create(this);
await module.InvokeVoidAsync("addOnPasteEventListener", _inputRef, selfReference);
}
}
[JSInvokable]
public void HandlePaste(string text)
{
Console.WriteLine(text);
}
}

Error while trying to get data from API - Blazor [duplicate]

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

Calling Twilio Voice API with Blazor

I am working on a project with blazor server and I am trying to make a voice call using Twilio.
I followed the documentation online from twilio for this to the letter but getting this error:
System.Net.Http.HttpRequestException: Response status code does not indicate success: 404 (Not Found).
at System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode()
at QUBeMyGuest.Pages.GuestArrivals.EmergencyContact.GetClientToken() in C:\Pages\GuestArrivals\EmergencyContact.razor:line 98
at QUBeMyGuest.Pages.GuestArrivals.EmergencyContact.OnAfterRenderAsync(Boolean firstRender) in C:\Users\\Pages\GuestArrivals\EmergencyContact.razor:line 58
at Microsoft.AspNetCore.Components.RenderTree.Renderer.GetErrorHandledTask(Task taskToHandle)
The documentation stated that I should open a new asp.net core api project within my solution and add this class to the controller folder:
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Twilio.Jwt;
using Twilio.Jwt.Client;
using Twilio.TwiML;
using Twilio.Types;
using System.Net.Http;
namespace api.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TwilioBackEndController : ControllerBase
{
public readonly string AccountSid = "xxxxxxxxxx";
public readonly string AuthToken = "xxxxxxxx";
public readonly string AppSid = "xxxxxxxx";
public readonly string PhoneNumber = "xxxxxxxx";
[HttpGet("token")]
public async Task<IActionResult> GetToken()
{
var scopes = new HashSet<IScope>
{
new OutgoingClientScope(AppSid),
new IncomingClientScope("tester")
};
var capability = new ClientCapability(AccountSid, AuthToken, scopes: scopes);
return await Task.FromResult(Content(capability.ToJwt(), "application/jwt"));
}
[HttpPost("voice")]
public async Task<IActionResult> PostVoiceRequest([FromForm] string phone)
{
var destination = !phone.StartsWith('+') ? $"+{phone}" : phone;
var response = new VoiceResponse();
var dial = new Twilio.TwiML.Voice.Dial
{
CallerId = PhoneNumber
};
dial.Number(new PhoneNumber(destination));
response.Append(dial);
return await Task.FromResult(Content(response.ToString(), "application/xml"));
}
}
}
I then set up the page to make the call with my blazor project
#page "/guest/emergencycall"
#using System.ComponentModel.DataAnnotations
#inject HttpClient httpClient
#using Microsoft.Extensions.DependencyInjection
#using System.Net.Http
<EditForm Model="Input" OnValidSubmit="InitiatePhoneCall">
<DataAnnotationsValidator />
<ValidationSummary />
<p>
<label for="phoneNumber">Enter Phone Number:</label>
<InputText id="phoneNumber" #bind-Value="Input.PhoneNumber"></InputText>
<button type="submit" class="btn btn-primary" disabled="#IsDialDisabled">DIAL</button>
<button type="button" id="endBtn" class="btn btn-primary" disabled="#IsEndDisabled" #onclick="EndPhoneCall">END</button>
<button type="button" id="clearBtn" class="btn btn-primary" disabled="#IsClearDisabled" #onclick="ClearPhoneNumber">CLEAR</button>
</p>
</EditForm>
<hr />
#if (Logs.Count == 0)
{
<p>No Logs available yet</p>
}
else
{
<ul>
#foreach (var log in Logs)
{
<li>#log</li>
}
</ul>
}
#code {
private string _tokenUrl = "https://xxxxxxxxxxxxxxx";
private bool appSetupRun = false;
protected bool IsDialDisabled { get; set; } = false;
protected bool IsEndDisabled { get { return !IsDialDisabled; } }
protected bool IsClearDisabled { get { return string.IsNullOrEmpty(Input.PhoneNumber); } }
protected List<string> Logs { get; set; } = new List<string>();
protected InputModel Input { get; set; } = new InputModel();
[Inject]
protected IJSRuntime JSRuntime { get; set; }
[Inject]
protected IHttpClientFactory HttpClientFactory { get; set; }
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && !appSetupRun)
{
var token = await GetClientToken();
await JSRuntime.InvokeVoidAsync("appFunctions.setup", token);
appSetupRun = true;
}
}
protected async Task InitiatePhoneCall()
{
IsDialDisabled = true;
await LogMessage($"Calling the number {Input.PhoneNumber}");
await JSRuntime.InvokeVoidAsync("appFunctions.placeCall", Input.PhoneNumber);
await LogMessage($"Called the number {Input.PhoneNumber}");
StateHasChanged();
}
protected async Task EndPhoneCall()
{
IsDialDisabled = false;
await LogMessage($"Ending the call to {Input.PhoneNumber}");
await JSRuntime.InvokeVoidAsync("appFunctions.endCall");
await LogMessage($"Ended the call to {Input.PhoneNumber}");
StateHasChanged();
}
protected async Task ClearPhoneNumber()
{
await LogMessage("Clearing the phone number entry");
Input.PhoneNumber = string.Empty;
await LogMessage("Cleared the phone number entry");
StateHasChanged();
}
private async Task<string> GetClientToken()
{
var uri = new Uri(_tokenUrl);
using var client = HttpClientFactory.CreateClient();
var response = await client.GetAsync(uri);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
[JSInvokable]
public async Task LogMessage(string message)
{
Logs.Add($"{DateTimeOffset.Now} - {message}");
await Task.CompletedTask;
}
public class InputModel
{
[Required]
[Phone(ErrorMessage = "Please enter your phone number in a proper format")]
public string PhoneNumber { get; set; }
}
}
and then added this JS function:
window.appFunctions = {
setup: function (token) {
console.log('Getting connected');
// Setup Twilio Device
Twilio.Device.setup(token);
Twilio.Device.ready(() => {
console.log('We are connected and ready to do the thing');
});
Twilio.Device.error((err) => {
console.error('This should not have been reached. We need to do something here');
console.error(err);
});
},
placeCall: function (destination) {
console.log(`Calling ${destination}`);
Twilio.Device.connect({ phone: destination });
console.log(`Successfully called ${destination}`);
},
endCall: function () {
console.log('Ending the call');
Twilio.Device.disconnectAll();
console.log('Successfully ended the call');
}
};
in my startup file I then added this, but doesn't seem to have made a difference:
services.AddHttpClient();
if (!services.Any(x => x.ServiceType == typeof(HttpClient)))
{
services.AddScoped<HttpClient>(s =>
{
var uriHelper = s.GetRequiredService<NavigationManager>();
return new HttpClient
{
BaseAddress = new Uri(uriHelper.BaseUri)
};
});
}
Any suggestions on what I could be doing wrong?
When seeing a 404, the first thing that should come to mind is an incorrect URL. Since the 404 is coming from GetClientToken in your razor file. That means your _tokenUrl is probably incorrect. Looking at your controller, your URL should look something like https://{host}/token/. It's possible that you hardcoded the link in the documentation, meanwhile, it's supposed to be based on your controller.
PS: HTTP 404 means that the requested resource cannot be found. Get familiar with other HTTP response codes too, might save you a ton of debugging.

Component is not re-rendered after invoke StateHasChanged

I have read the article "3 Ways to Communicate Between Components in Blazor" and have tried to do the same.
I have message component under the #body and depending on user actions in the #body components message has to be changed
#inject ClientMessage clientMessage
#inherits LayoutComponentBase
#using Site.Shared.Components
<div class="sidebar">
<AdminMenu />
</div>
<div class="main">
<div class="content px-4">
#Body
</div>
<Message/>
</div>
#code
{
protected async Task ChangeState()
{
await InvokeAsync(StateHasChanged);
}
protected override void OnInitialized()
{
clientMessage.MsgChange += ChangeState;
}
}
Message component:
#inject ClientMessage clientMessage
<div style="#(IsVisble ? "display:block" : "display:none")" class="#MsgClass" role="alert">
#if (clientMessage != null)
{
#clientMessage.Message
}
</div>
#code {
public bool IsVisble
{
get
{
if (string.IsNullOrEmpty(#clientMessage.Message))
{
return false;
}
return true;
}
}
public string MsgClass
{
get
{
if (clientMessage == null)
{
return string.Empty;
}
string msgClass;
switch (clientMessage.MsgType)
{
case EMsgType.Info:
msgClass = "alert alert-info";
break;
case EMsgType.Success:
msgClass = "alert alert-success";
break;
case EMsgType.Warning:
msgClass = "alert alert-warning";
break;
case EMsgType.Error:
msgClass = "alert alert-danger";
break;
case EMsgType.NoMsg:
default:
msgClass = string.Empty;
break;
}
return msgClass;
}
}
}
Message class
public class ClientMessage
{
public event Func<Task> MsgChange;
public ClientMessage(string msg, EMsgType msgType)
{
this.Message = msg;
this.MsgType = msgType;
NotifyStateChanged();
}
public void SetMsg(string msg, EMsgType msgType)
{
this.Message = msg;
this.MsgType = msgType;
NotifyStateChanged();
}
public string Message { get; set; }
public EMsgType MsgType { get; set; }
private void NotifyStateChanged()
{
if (MsgChange != null)
{
MsgChange.Invoke();
}
}
}
ClientMessage class is injected as a singleton by DI. If I invoke SetMsg(newMsgm, msgType) in the #body components then ChangeState() method is invoked but nothing happened, I mean component is not re-rendered. If I instead "InvokeAsync" use "Invoke" I have an error "The current thread is not associated with the Dispatcher. Use InvokeAsync() to switch execution to the Dispatcher when triggering rendering or component state.". If I reload the page I can see Message.
What do I wrong and how can I force to re-render message component?
I resolved this issue. Maybe it will help someone.
I localized the problem - when #body was changed after NavigationManager.NavigateTo("some_page") had been invoked StateHasChanged() does not re-rendered message component. I tried different places where I can fire StateHasChanged() and if I moved it to Message.razor it stars to work as expected.
It is better to read the documentation at the beggining than articles:)

Categories

Resources