Search code examples
aspnetboilerplateasp.net-core-2.2abp-framework

Invalid DTO posted throws exception


Invalid DTO posted to MVC endpoint throws an exception, expecting it to record it in ModelState.

How according to Abp should such exception be handled gracefully?

I see ABP documentation on validating DTOs where it throws the exception for invalid models, but how to pass the error messages to a view.

Using

  1. aspnetboilerplate sample for MVC + .NET Core
  2. Abp package version 4.3
  3. Net Core 2.2

What I did:

Added a DTO

public class ExamDtoBaseTmp
    {
        [Required(ErrorMessage ="Can't add without a name")]

        [StringLength(EntityCommons.MaxExamNameLength,ErrorMessage ="Keep is short")]
        public string Name { get; set; }
        [Required(ErrorMessage ="Description is needed")]
        public string Description { get; set; }
    }

Added a POST endpoint that accepted above model

[HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Create(ExamDtoBaseTmp model)
        {
            if (!ModelState.IsValid)
            {
                return View();
            }

            try
            {
                // TODO: Add insert logic here

                return RedirectToAction(nameof(Index));
            }
            catch
            {
                return View();
            }
        }

Submitted empty form and got below exception:

AbpValidationException: Method arguments are not valid! See ValidationErrors for details.

    Abp.Runtime.Validation.Interception.MethodInvocationValidator.ThrowValidationError() in MethodInvocationValidator.cs
    Abp.Runtime.Validation.Interception.MethodInvocationValidator.Validate() in MethodInvocationValidator.cs
    Abp.AspNetCore.Mvc.Validation.AbpValidationActionFilter.OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) in AbpValidationActionFilter.cs
    Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeNextActionFilterAsync()
    Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Rethrow(ActionExecutedContext context)
    Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Next(ref State next, ref Scope scope, ref object state, ref bool isCompleted)
    Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeInnerFilterAsync()
    Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeNextExceptionFilterAsync()
    Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Rethrow(ExceptionContext context)
    Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Next(ref State next, ref Scope scope, ref object state, ref bool isCompleted)
    Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeNextResourceFilter()
    Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Rethrow(ResourceExecutedContext context)
    Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Next(ref State next, ref Scope scope, ref object state, ref bool isCompleted)
    Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeFilterPipelineAsync()
    Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeAsync()
    Microsoft.AspNetCore.Builder.RouterMiddleware.Invoke(HttpContext httpContext)
    Microsoft.AspNetCore.Builder.RouterMiddleware.Invoke(HttpContext httpContext)
    Nivra.Authentication.JwtBearer.JwtTokenMiddleware+<>c__DisplayClass0_0+<<UseJwtTokenMiddleware>b__0>d.MoveNext() in JwtTokenMiddleware.cs

                    await next();

Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context) Nivra.Web.Startup.Startup+<>c+<<Configure>b__4_1>d.MoveNext() in Startup.cs

                    await next();

Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.Invoke(HttpContext context) Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

Startup.cs

using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Castle.Facilities.Logging;
using Abp.AspNetCore;
using Abp.Castle.Logging.Log4Net;
using Nivra.Authentication.JwtBearer;
using Nivra.Configuration;
using Nivra.Identity;
using Nivra.Web.Resources;
using Abp.AspNetCore.SignalR.Hubs;
using System.IO;
using Microsoft.AspNetCore.SpaServices.AngularCli;
using System.Linq;
using Abp.Extensions;
using Swashbuckle.AspNetCore.Swagger;
using Microsoft.AspNetCore.Mvc.Cors.Internal;
using Nivra.OnlineExam.Models;
using Nivra.OnlineExam.Service;
using Microsoft.Extensions.Options;

namespace Nivra.Web.Startup
{
    public class Startup
    {
        private const string _defaultCorsPolicyName = "localhost";
        private readonly IConfigurationRoot _appConfiguration;

        public Startup(IHostingEnvironment env)
        {
            _appConfiguration = env.GetAppConfiguration();
        }

        public IServiceProvider ConfigureServices(IServiceCollection services)
        {

            services.Configure<ExamDatabaseSettings>(
                _appConfiguration.GetSection(nameof(ExamDatabaseSettings))
            );

            services.AddSingleton<IExamDatabaseSettings>(sp =>
                sp.GetRequiredService<IOptions<ExamDatabaseSettings>>().Value);

            services.AddSingleton<ExamService>();

            // MVC
            services.AddMvc(
                options =>
                options.Filters.Add(new CorsAuthorizationFilterFactory(_defaultCorsPolicyName))
                //options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute())

            );

            services.AddSpaStaticFiles(c =>
            {
                c.RootPath = "wwwroot/dist";
            });


            IdentityRegistrar.Register(services);
            AuthConfigurer.Configure(services, _appConfiguration);


            services.AddSignalR();


            // Configure CORS for angular2 UI
            services.AddCors(
                options => options.AddPolicy(
                    _defaultCorsPolicyName,
                    builder => builder
                        .AllowAnyOrigin()
                        //.WithOrigins(
                        //    // App:CorsOrigins in appsettings.json can contain more than one address separated by comma.
                        //    _appConfiguration["App:CorsOrigins"]
                        //        .Split(",", StringSplitOptions.RemoveEmptyEntries)
                        //        .Select(o => o.RemovePostFix("/"))
                        //        .ToArray()
                        //)
                        .AllowAnyHeader()
                        .AllowAnyMethod()
                        .AllowCredentials()
                )
            );

            // Swagger - Enable this line and the related lines in Configure method to enable swagger UI
            services.AddSwaggerGen(options =>
            {
                options.SwaggerDoc("v1", new Info { Title = "aspnetCoreNg API", Version = "v1" });
                options.DocInclusionPredicate((docName, description) => true);

                // Define the BearerAuth scheme that's in use
                options.AddSecurityDefinition("bearerAuth", new ApiKeyScheme()
                {
                    Description = "JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"",
                    Name = "Authorization",
                    In = "header",
                    Type = "apiKey"
                });
            });



            services.AddScoped<IWebResourceManager, WebResourceManager>();

            var configurations = AppConfigurations.Get(AppDomain.CurrentDomain.BaseDirectory);

            services.Configure<ExamDatabaseSettings>(configurations.GetSection(nameof(ExamDatabaseSettings)));
            // Configure Abp and Dependency Injection
            return services.AddAbp<NivraWebMvcModule>(
                // Configure Log4Net logging
                options => options.IocManager.IocContainer.AddFacility<LoggingFacility>(
                    f => f.UseAbpLog4Net().WithConfig("log4net.config")
                )
            );
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseAbp(options => { options.UseAbpRequestLocalization = false; }); // Initializes ABP framework.

            app.UseCors(_defaultCorsPolicyName); // Enable CORS!

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
            }

            app.UseStaticFiles();
            app.UseSpaStaticFiles();
            app.Use(async (context, next) =>
            {
                await next();
                if (
                    context.Response.StatusCode == 404
                    && !Path.HasExtension(context.Request.Path.Value)
                    && !context.Request.Path.Value.StartsWith("/api/services", StringComparison.InvariantCultureIgnoreCase)
                    )
                { context.Request.Path = "/index.html"; await next(); }
            });


            app.UseAuthentication();

            app.UseJwtTokenMiddleware();

            app.UseSignalR(routes =>
            {
                routes.MapHub<AbpCommonHub>("/signalr");
            });

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "defaultWithArea",
                    template: "{area}/{controller=Home}/{action=Index}/{id?}");

                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });

            // app.Map("/spa", l =>
            //    l.UseSpa(spa =>
            //    {
            //        // To learn more about options for serving an Angular SPA from ASP.NET Core,
            //        // see https://go.microsoft.com/fwlink/?linkid=864501

            //        spa.Options.SourcePath = "./";

            //        if (env.IsDevelopment())
            //        {
            //            spa.UseAngularCliServer(npmScript: "start");
            //        }
            //    })
            //);

            app.UseSwagger();
            //Enable middleware to serve swagger - ui assets(HTML, JS, CSS etc.)
            app.UseSwaggerUI(options =>
            {
                options.SwaggerEndpoint("/swagger/v1/swagger.json", "AbpZeroTemplate API V1");
            }); //URL: /swagger
        }
    }
}

Solution

  • Abp wraps only exception thrown in actions with ObjectResult return type by default. e.g. public JsonResult Create();

    If your action is return a view result, it is recommended to catch the exception within the action yourself and add to ModelState manually.

    see https://aspnetboilerplate.com/Pages/Documents/AspNet-Core#exception-filter

    Referring above link

    Exception handling and logging behaviour can be changed using the WrapResult and DontWrapResult attributes for methods and classes.