Click here to Skip to main content
16,022,236 members
Please Sign up or sign in to vote.
0.00/5 (No votes)
I'm struggling with the following:

I have a ASP.NET Core web API that provides User registration as follows
C#
 public async Task<IActionResult> RegisterUser([FromBody] NewUser user)
 {
     User appuser = new User()
     {
         UserName = user.Username,
         Email = user.EmailAddress,
         FirstName = user.FirstName,
         LastName = user.LastName,
         Address = user.Address,
         PhoneNumber = user.PhoneNumber,
         CreateAt = DateTime.Today,
         IsBanned = false,
         UnderInvestigation = false
     };
     
     IdentityResult result = await _userManager.CreateAsync(appuser, user.Password);
     if (result.Succeeded)
         return Ok($"User Name {user.Username} was sucessfuly Registered");
     else
     {
         foreach (IdentityError error in result.Errors)
             return Ok(error.Description);
    
          //  ModelState.AddModelError("", error.Description);
    
         return BadRequest(ModelState);
     }
}

Up to this point my understating is that I don't have to check in the above code whether the ModelState is valid ( just only on the client side right?) . When I run the code in Swagger, all the validation messages pop up e.g Password must have non alphanumeric character, uppercase, if the Username is already taken, etc. All this messages are shown in a response body with a 200 Staus Code.

Back to the client application that consumes the WEB API, I have the following Code
C#
[HttpPost]
public async Task<iactionresult> Registration([FromForm] RegisterViewModel model)
{
    if (ModelState.IsValid)
    {
        var httpClient = _httpClientFactory.CreateClient("HousingWebAPI");
        var body = new StringContent(JsonConvert.SerializeObject(model), Encoding.UTF8, "application/json");
        using (var response = await httpClient.PostAsync("https://localhost:7129/api/Auth/Register?", body))
        {
            /* Checking first if the API is sending a successful StatusCode */
            if (!response.IsSuccessStatusCode)
            {
                var responseContent = response.Content == null
                    ? "NoContent"
                    : await response.Content.ReadAsStringAsync();
              
                TempData["testmsg"] = response;
                return View("/Views/Registration/Index.cshtml");
            }

            else
            {
                TempData["testmsg"] = response.Content.ReadAsStringAsync();              
               
               return RedirectToAction("Index", "Registration");
            }
        }
    }
    return View("/Views/Registration/Index.cshtml");
    
}

My questions are:

a) How I show to the client via a bootstrap message box all the messages coming from the WEB API?

b) Does the ModelState.Is Valid assures that all the Validations take place in the customer side hence the WEB Api doesn't have to do it?

c) In the event that any developer "forgets"to check the validation, is there any way that the Validation messages from the WEB API can be shown?

d) Is the correct approach to set the response in a TempData and then show it in a Bootstrap message box? if so, how to do it.

Thanking you.

Jose

What I have tried:

I have searched the Forum for similar situation.
Posted
Updated 22-Aug-24 2:44am
v2

Hmmm, I have so many concerns about this code. First, let's address this:
C#
foreach (IdentityError error in result.Errors)
  return Ok(error.Description);
This is no different to doing
C#
IdentityError error = result.Errors.FirstOrDefault();
return error ?? BadRequest(ModelState);
The foreach is unnecessary. Secondly, you are returning a 200 status for something that is in error. That's extremely bad practice. Again, this is like catching and swallowing an exception. If you have a failure here, return that failure to the user with an appropriate code (say a 400 if it's a validation failure for instance). Then, when you have that, your front-end code could add an interceptor that catches all 400 errors and displays a nice friendly error banner. I would start by addressing those issues first, if it were me doing this.
 
Share this answer
 
Your first code block seems a little strange. If the registration fails, you return a success response with the description of the first error. Your client code is going to have to rely on parsing the response content to see whether it's a "successful success" or a "failed success" response, rather than just checking the IsSuccessStatusCode property.

According to the documentation, the automatic "bad request" responses for model state errors only happen if your controller or its base class is decorated with the [ApiController] attribute:
Create web APIs with ASP.NET Core | Microsoft Learn[^]

If you're not using that attribute, then you still need to check ModelState.IsValid.

The validity of the model state should always be checked in the API. You should never trust client input, and for an API, the model is that input. It's safest to always assume there's a team of people trying to break into your system by sending malformed requests. :)

Assuming you fix the response from your API so that it doesn't return 200 OK when the registration fails, then I'd be inclined to rewrite your application code slightly:
C#
[HttpPost]
public async Task<IActionResult> Registration([FromForm] RegisterViewModel model)
{
    if (ModelState.IsValid)
    {
        var httpClient = _httpClientFactory.CreateClient("HousingWebAPI");
        var body = new StringContent(JsonConvert.SerializeObject(model), Encoding.UTF8, "application/json");
        using (var response = await httpClient.PostAsync("https://localhost:7129/api/Auth/Register?", body))
        {
            if (response.IsSuccessStatusCode)
            {
                // Redirect the user to a "registration succeeded" action:
                TempData["RegistrationMessage"] = await response.Content.ReadAsStringAsync(); 
                return RedirectToAction("RegistrationSuccess", "Registration");
            }
            
            // TODO: Parse the response to extract the error messages. Eg:
            var problem = await response.Content.ReadFromJsonAsync<HttpValidationProblemDetails>();
            ModelState.AddModelError("*", problem.Detail);
            
            if (problem.Errors is not null)
            {
                foreach (var (propertyName, errors) in problem.Errors)
                {
                    foreach (string error in errors)
                    {
                        ModelState.AddModelError(propertyName, error);
                    }
                }
            }
        }
    }
    
    return View();
}

public IActionResult RegistrationSuccess()
{
    ViewBag.Message = TempData["RegistrationMessage"];
    return View();
}
The register view can then rely on a standard validation summary / validation message to display the errors. The success view can read ViewBag.Message to get the "registration succeeded" message to display.
 
Share this answer
 
Comments
chocusatergus 22-Aug-24 12:03pm    
Hi Richard. Thanks for the explanation ...much appreciated. As per your suggestion, I changed the code in the API so it returns BadRequest (400) when the registration fails. I rewrote my code as per your explanation above, but I the parsing of the response throws an exception

var problem = await response.Content.ReadFromJsonAsync<httpvalidationproblemdetails>();

I put a breakpoint and I see that the response variable catches the BadRequest(400) from the WEB Api but is throwing an exception
JsonException: The JSON value could not be converted to Microsoft.AspNetCore.Http.HttpValidationProblemDetails. Path: $ | LineNumber: 0 | BytePositionInLine: 74.
System.Text.Json.ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(Type propertyType)

System.Text.Json.ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(Type propertyType)
System.Text.Json.Serialization.Converters.ObjectDefaultConverter<t>.OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, out T value)
System.Text.Json.Serialization.JsonConverter<t>.TryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, out T value, out bool isPopulatedValue)
System.Text.Json.Serialization.JsonConverter<t>.ReadCore(ref Utf8JsonReader reader, JsonSerializerOptions options, ref ReadStack state)
System.Text.Json.Serialization.Metadata.JsonTypeInfo<t>.ContinueDeserialize(ref ReadBufferState bufferState, ref JsonReaderState jsonReaderState, ref ReadStack readStack)
System.Text.Json.Serialization.Metadata.JsonTypeInfo<t>.DeserializeAsync(Stream utf8Json, CancellationToken cancellationToken)
System.Threading.Tasks.ValueTask<tresult>.get_Result()
System.Net.Http.Json.HttpContentJsonExtensions.ReadFromJsonAsyncCore<t>(HttpContent content, JsonSerializerOptions options, CancellationToken cancellationToken)
WebDevelopment.Controllers.RegistrationController.Registration(RegisterViewModel model) in RegistrationController.cs
+
var problem = await response.Content.ReadFromJsonAsync<httpvalidationproblemdetails>();
Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor+TaskOfIActionResultExecutor.Execute(ActionContext actionContext, IActionResultTypeMapper mapper, ObjectMethodExecutor executor, object controller, object[] arguments)
System.Threading.Tasks.ValueTask<tresult>.get_Result()
Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<invokeactionmethodasync>g__Awaited|12_0(ControllerActionInvoker invoker, ValueTask<iactionresult> actionResultValueTask)
Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<invokenextactionfilterasync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, object state, bool isCompleted)
Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(ref State next, ref Scope scope, ref object state, ref bool isCompleted)
Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<invokeinnerfilterasync>g__Awaited|13_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, object state, bool isCompleted)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<invokenextresourcefilter>g__Awaited|25_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, object state, bool isCompleted)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(ref State next, ref Scope scope, ref object state, ref bool isCompleted)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<invokefilterpipelineasync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, object state, b
Richard Deeming 23-Aug-24 3:11am    
You'll need to check what data the response is actually returning, and deserialize to an appropriate type.

The recommended approach for an API is to return a "problem details" object:
Handle errors in ASP.NET Core controller-based web APIs | Microsoft Learn[^]

But if your code isn't set up to do that, then you won't be able to read the response as a problem details object.

This content, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)



CodeProject, 20 Bay Street, 11th Floor Toronto, Ontario, Canada M5J 2N8 +1 (416) 849-8900