Search code examples
c#.net-coreasp.net-core-2.0modelstateasp.net-core-mvc-2.0

How can I remove the ModelState prefix on error messages in ASP.NET Core 2.0?


I am working on an ASP.NET Core 2.0 API that will be consumed by my customers. One of the issues I am running into is that when I use ModelState for input validation of the request payload, the resultant error message that the consumer sees has a [objectPrefix].PropertyName in the response JSON. Our API documentation lists the Property name but not the object class and so the prefix is creating a problem when the consumer writes code that deserializes the JSON response in to their local object model.

Is there any option I can set in Startup.cs ConfigureServices method for Service.AddMvc, or something similar, that will disable this prefix?

I am using the Microsoft.AspNetCore.All(2.0.7) dependency in my API, .NET Core 2.0.4 and VS2016 v15.5.7 if that matters.

I am using Data Annotations from the System.ComponentModel.DataAnnotations lib and decorating my creation DTO class properties like below;

    [Required]
    [MaxLength(14)]
    public string AccountNumber
    {
        get => _accountNumber;
        set => _accountNumber = !string.IsNullOrWhiteSpace(value) ? value.Trim() : string.Empty;
    }

When the consumer doesn't provide an account number in the request payload, the error that returns looks like this;

{
    "[AccountDto].AccountNumber": [
        "The AccountNumber field is required."
    ]
}

What I want to do is elimintate the [AccountDto]. prefix so that the error JSON then looks like this;

{
    "AccountNumber": [
        "The AccountNumber field is required."
    ]
}

I found this SO post but it seems to reference the older ASP.NET.

Currently, I am having my client do a string replace on the json response but I really would like to have a better solution.

Any ideas?

UPDATE 5/16/18

It seems that the issue with the prefix is related to my use of the Validate method in my *ForCreationDtos.

For example,

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {

        if (CompanyId == 0)
        {
            yield return new ValidationResult("A Company ID is required.", new[] { "CompanyId" });
        }

    }

However, I have found a work around by using a global ModelState handler and modifying it to parse out the prefix.

public class ValidateModelAttribute : ActionFilterAttribute
{

    /// <summary>
    /// Validates model state upon action execution
    /// </summary>
    /// <param name="context">ActionExecutingContext object</param>
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        if (context.ModelState.IsValid) return;
        var errorList = context.ModelState.Where(ms => ms.Value.Errors.Any()).ToDictionary(
            kvp => kvp.Key.Replace("[0].", ""),
            kvp => kvp.Value.Errors.Select(e => string.IsNullOrEmpty(e.ErrorMessage) ? e.Exception.Message : e.ErrorMessage).ToArray()
        );
        var globalErrorDto = new GlobalErrorDto { Errors = errorList };
        context.Result = new BadRequestObjectResult(globalErrorDto);
    }
}

This is a bit crude and assumes "[0]." as the prefix but that is the one that I get whenever I implement the Validate method in the DTO class. This seems to have solved my specific issue.


Solution

  • I am using Microsoft.AspNetCore.All v2.0.8, Microsoft.NETCore.App v2.0.7 and Visual Studio Community 2017 v15.7.1, and everything worked out like what you want.

    Screenshot #1: No account number - 400

    No account number - 400

    Screenshot #2: Account number too long - 400

    Account number too long - 400

    Screenshot #3: Valid account number - 201

    Valid account number - 201

    I can't reproduce your issue. I even thought maybe I just created the model within the web project so I even created a separate class project to contain the DTOs. It still works out like what you want!

    DTO

    using System.ComponentModel.DataAnnotations;
    
    namespace DL.SO.ModelState.Dto.Users
    {
        public class AccountModel
        {
            [Required]
            [MaxLength(14)]
            [Display(Name = "account number")]
            public string AccountNumber { get; set; }
        }
    }
    

    Controller

    using DL.SO.ModelState.Dto.Users;
    using Microsoft.AspNetCore.Mvc;
    
    namespace DL.SO.ModelState.Controllers
    {
        [Route("api/[controller]")]
        public class UsersController : ControllerBase
        {
            [HttpGet("{id}")]
            public IActionResult GetById(string id)
            {
                // Just testing 
                return Ok(id);
            }
    
            [HttpPost]
            public IActionResult Post(AccountModel model)
            {
                if (!ModelState.IsValid)
                {
                    return BadRequest(ModelState);
                }
    
                // Just testing so I pass in null
                return CreatedAtAction(nameof(GetById), 
                     new { id = model.AccountNumber }, null);
            }
        }
    }
    

    Startup

    using Microsoft.AspNetCore.Builder;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;
    
    namespace DL.SO.ModelState
    {
        public class Startup
        {
            public Startup(IConfiguration configuration)
            {
                Configuration = configuration;
            }
    
            public IConfiguration Configuration { get; }
    
            public void ConfigureServices(IServiceCollection services)
            {
                services.AddMvc();
            }
    
            public void Configure(IApplicationBuilder app, IHostingEnvironment env)
            {
                if (env.IsDevelopment())
                {
                    app.UseDeveloperExceptionPage();
                }
    
                app.UseMvc();
            }
        }
    }