Skip to content

ASP.NET Core library for handling MVC and Api requests from a single Controller Action

License

Notifications You must be signed in to change notification settings

davidikin45/AspNetCore.Mvc.MvcAsApi

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

60 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ASP.NET Core MVC as Api

nuget Downloads

By default ASP.NET Core doesn't allow a single controller action to handle request/response for both Mvc and Api requests or allow an Api request to bind to Body + Route/Query. This library allows you to do so plus it has a bunch of other features.

Features:

  1. Hybrid Model Binding
  2. MVC Dynamic/JObject ModelBinder
  3. Convert ViewResult to ObjectResult
  4. Enhanced Problem Details format (instance, traceId, timeGenerated) with option to expose ModelState errors in angular format
  5. Mvc Error/Exception handling
  6. Api Error/Exception handling
  7. Global Error/Exception Handling Problem Details Middleware

Most useful for the following scenarios:

  1. Allowing Developers to Test/Develop/Debug Mvc Forms without worrying about UI. Used by applying conventions.
  2. Integration Tests for Mvc without the need of WebApplicationFactory. Used by applying convention.
  3. Used in Production to allow specific Mvc controller actions to return model as json/xml data.
  4. Used in Production to allow specific Api controller actions to bind to Body + Query/Route.
  5. Used in Production to allow Mvc controller actions to return error responses/exceptions as Problem Details.
  6. Used in Production globally to apply a modern frontend (React/Angular/Vue.js) to an existing MVC application.

Installation

NuGet

PM> Install-Package AspNetCore.Mvc.MvcAsApi

.Net CLI

> dotnet add package AspNetCore.Mvc.MvcAsApi

Quick Start ASP.NET Core 2.2

var builder = services.AddMvc(options =>
{
	options.Filters.Add<ApiGenerateAntiForgeryTokenAttribute>();

    options.RespectBrowserAcceptHeader = false;
    options.ReturnHttpNotAcceptable = true;

    if(HostingEnvironment.IsDevelopment())
    {
        options.Conventions.Add(new MvcAsApiConvention());
    }
	
	//There seems to be issues with endpoint routing in 2.2 when generating links so suggest disabling it.
	//https://github.com/aspnet/AspNetCore/issues/5055
	options.EnableEndpointRouting = false;
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
//ModelState errors as camelCase
//Even though in 2.2 the default property naming strategy is camelCase, ProcessDictionaryKeys = false which means model state errors are not camelCase by default.
//https://stackoverflow.com/questions/43488932/how-to-set-modelstate-error-keys-to-camel-case
.AddJsonOptions(options => options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver());

//Optional - These could be used independently of MvcAsApiConvention
if(HostingEnvironment.IsDevelopment())
{
    //MVC Dynamic Model Binding
    builder.AddMvcDynamicModelBinder();

    builder.AddMvcEnhancedProblemDetailsClientErrorFactory();

    //Api Invalid ModelState Enhanced Problem Details (instance, traceId, timeGenerated, delegate factory), 400 vs 422
    builder.ConfigureMvcProblemDetailsInvalidModelStateFactory(options => options.EnableAngularErrors = false);
}

services.AddAntiforgery(o => {
	o.HeaderName = "X-XSRF-TOKEN";
});

Quick Start ASP.NET Core 3.0

var builder = services.AddMvc(options =>
{
	options.Filters.Add<ApiGenerateAntiForgeryTokenAttribute>();
	
    options.RespectBrowserAcceptHeader = false;
    options.ReturnHttpNotAcceptable = true;

    if(HostingEnvironment.IsDevelopment())
    {
        options.Conventions.Add(new MvcAsApiConvention());
    }

});

services.Configure<JsonOptions>(options =>
{
    //default
    options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;

    //BUG: Currently DictionaryKeyPolicy only works for deserialization but not serialization so model state errors are not camelCase!
    //https://github.com/dotnet/corefx/issues/38840
    options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase;
});

//Optional - These could be used independently of MvcAsApiConvention
if(HostingEnvironment.IsDevelopment())
{
    //MVC Dynamic Model Binding
    builder.AddMvcDynamicModelBinder();

    //Api StatusCodeResult Enhanced Problem Details (instance, traceId, timeGenerated, delegate factory)
    builder.AddMvcEnhancedProblemDetailsFactory(options => { options.EnableAngularErrors = true; });
    builder.AddMvcEnhancedProblemDetailsClientErrorFactory();
	
    //400 vs 422
    builder.ConfigureMvcProblemDetailsInvalidModelStateFactory();
}

services.AddAntiforgery(o => {
	o.HeaderName = "X-XSRF-TOKEN";
});

Usage

  • Currently to create a controller which handles Api and Mvc requests you would need to write something along the lines of below. Most developers would seperate these actions into two seperate controllers.
[Route("contact")]
[HttpGet]
public IActionResult ContactMvc()
{
    return View(new ContactViewModel());
}

[ValidateAntiForgeryToken]
[Route("contact")]
[HttpPost]
public IActionResult ContactMvc(ContactViewModel viewModel)
{
    if(ModelState.IsValid)
    {
        //Submit Contact Form

        return RedirectToAction("Home");
    }

    return View(viewModel);
}

[ExceptionFilter]
[ErrorFilter]
[Route("api/contact")]
[HttpGet]
public ActionResult<ContactViewModel> ContactApi()
{
    return new ContactViewModel();
}

[ExceptionFilter]
[ErrorFilter]
[Route("api/contact")]
[HttpPost]
public IActionResult ContactApi(ContactViewModel viewModel)
{
    if (ModelState.IsValid)
    {
        //Submit Contact Form
        return Ok();
    }
            
    return ValidationProblem(ModelState);
}
  • This library give thes ability to add attributes/conventions which allow an Mvc controller action to accept and return data as if it were an Api action method. An example of the attributes required can be seen below.
[MvcExceptionFilter]
[MvcErrorFilter]
[ApiExceptionFilter]
[ApiErrorFilter]
[ViewResultToObjectResult]
[Route("contact")]
[HttpGet]
public IActionResult ContactMvc()
{
    return View(new ContactViewModel());
}

[MvcExceptionFilter]
[MvcErrorFilter]
[ApiExceptionFilter]
[ApiErrorFilter]
[AutoValidateFormAntiForgeryToken]
[Route("contact")]
[HttpPost]
public IActionResult ContactMvc([FromBodyAndModelBinding] ContactViewModel viewModel)
{
    if(ModelState.IsValid)
    {
        //Submit Contact Form

        return RedirectToAction("Home");
    }

    return View(viewModel);
}
  • There are six conventions which add required binding attributes, handle Api Error Responses/Exceptions, handle Mvc Error Responses/Exceptions and switch [ValidateAntiForgeryToken] > [AutoValidateFormAntiForgeryToken]. This ensures AntiForgeryToken still occurs for Mvc but is bypassed for Api requests, by default the bypassing only occurs in Development environment, see Cross-Site Request Forgery (XSRF/CSRF).
  • The MvcAsApiConvention adds all six conventions in one line of code which is useful for Development.
var builder = services.AddMvc(options =>
{
    if(HostingEnvironment.IsDevelopment())
    {
        options.Conventions.Add(new MvcAsApiConvention());
        // OR
        options.Conventions.Add(new MvcAsApiConvention(o =>
        {
			o.DisableAntiForgeryForApiRequestsInDevelopmentEnvironment = true;
            o.DisableAntiForgeryForApiRequestsInAllEnvironments = false;
            o.MvcErrorOptions = (mvcErrorOptions) => {
	 
        };
        o.MvcExceptionOptions = (mvcExceptionOptions) => {

        };
        o.ApiErrorOptions = (apiErrorOptions) => {

        };
        o.ApiExceptionOptions = (apiExceptionOptions) => {

        };
    }));
    // OR
    //Does nothing by default.
    options.Conventions.Add(new MvcErrorFilterConvention(o => { o.ApplyToMvcActions = true; o.ApplyToApiControllerActions = true; }));
    //Intercepts OperationCanceledException, all other exceptions are logged/handled by UseExceptionHandler/UseDeveloperExceptionPage.
    options.Conventions.Add(new MvcExceptionFilterConvention(o => { o.ApplyToMvcActions = true; o.ApplyToApiControllerActions = true; }));
    //Return problem details in json/xml if an error response is returned via Api.
    options.Conventions.Add(new ApiErrorFilterConvention(o => { o.ApplyToMvcActions = true; o.ApplyToApiControllerActions = true; }));
    //Return problem details in json/xml if an exception is thrown via Api
    options.Conventions.Add(new ApiExceptionFilterConvention(o => { o.ApplyToMvcActions = true; o.ApplyToApiControllerActions = true; }));
    //Post data to MVC Controller from API
    options.Conventions.Add(new FromBodyAndOtherSourcesConvention(o => { o.ApplyToMvcActions = true; o.ApplyToApiControllerActions = true; o.EnableForParametersWithNoBinding = true; o.EnableForParametersWithFormRouteQueryBinding = true; o.ChangeFromBodyBindingsToFromBodyFormAndRouteQueryBinding = true; }));
    //Return data uisng output formatter when acccept header is application/json or application/xml
    options.Conventions.Add(new ConvertViewResultToObjectResultConvention(o => { o.ApplyToMvcActions = true; o.ApplyToApiControllerActions = true; }));
    }
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
//ModelState errors as camelCase
//Even though in 2.2 the default property naming strategy is camelCase, ProcessDictionaryKeys = false which means model state errors are not camelCase by default.
//https://stackoverflow.com/questions/43488932/how-to-set-modelstate-error-keys-to-camel-case
.AddJsonOptions(options => options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver());

//Optional
if(HostingEnvironment.IsDevelopment())
{
   //Api StatusCodeResult Enhanced Problem Details (traceId, timeGenerated, delegate factory)
	builder.AddMvcEnhancedProblemDetailsFactory(options => { options.EnableAngularErrors = true; });
	builder.AddMvcEnhancedProblemDetailsClientErrorFactory();

	//400 vs 422
	builder.ConfigureMvcProblemDetailsInvalidModelStateFactory();
}

[Route("contact")]
[HttpGet]
public IActionResult ContactMvc()
{
    return View(new ContactViewModel());
}

[ValidateAntiForgeryToken]
[Route("contact")]
[HttpPost]
public IActionResult ContactMvc(ContactViewModel viewModel)
{
    if(ModelState.IsValid)
    {
        //Submit Contact Form

        return RedirectToAction("Home");
    }

    return View(viewModel);
}
//Optional
services.AddDynamicModelBinder();

[Route("contact")]
[HttpGet]
public IActionResult ContactMvc()
{
    return View(new ContactViewModel());
}

[ValidateAntiForgeryToken]
[Route("contact")]
[HttpPost]
public IActionResult ContactMvc(dynamic viewModel)
{
    if (ModelState.IsValid)
    {
        return RedirectToAction(nameof(Index));
    }

    var viewModel = contactViewModel.ToObject<ContactViewModel>();

    return View(viewModel);
}

Content Negotiation

  • See Content Negotiation Process documentation
  • By default MvcOptions.RespectBrowserAcceptHeader is set to false which means when you hit an Api from your web browser and it contains accept header '*/*' the other accept headers are completely ignored.
  • This library uses the same logic + 'text/html' to distinguish Browser and Non-Browser requests in order to return ViewResult (Browser request) or ObjectResult (Non-Browser request).
  • Below is a typical browser request showing that most of the accept headerers are actually ignored.

alt text

 services.AddMvc(options =>
{	
    //Default = false. 
    //If the Request contains Accept header '*/*' the server ignores the Accept headers completely and uses the first output formatter that can format the object (usually json). 
    //For example when you hit an Api from a web browser.
    options.RespectBrowserAcceptHeader = false;
	
    //Default = false but good practice to set this to true.
    //If the Request does not contain Accept header '*/*' the server MUST find an output formatter based on accept header otherwise return statuscode 406 Not Acceptable. 
    //For example when making a json/xml/yaml request from postman. 
    //If this is left as false and request is sent in with accept header 'application/x-yaml', if the server doesn't have a yaml formatter it would use the first output formatter that can format the object (usually json) which is confusing for the client.
    options.ReturnHttpNotAcceptable = true;
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

Error Responses (Status Code >= 400) and Exceptions - MVC Filters

  • Api Controller Action error responses (Status Code >= 400) and Exceptions will be handled with these filters.
  • The [ApiErrorFilterAttribute] is gives similar functionality to the ClientErrorResultFilter that is applied when a controller is decorated with ApiController but gives the ability to pass in a decision delegate.
  • The [ApiExceptionFilterAttribute] allows api exceptions to be handled.
  • The [MvcExceptionFilterAttribute] allows browser request exceptions to be handled.
  • For handling 404 route not found and exceptions from other middleware you will need to implement Global Exception Handling. See below.
  • IClientErrorFactory will handle generating the problem details when an Error Response occurs. See default ProblemDetailsClientErrorFactory.
  • An enhanced IClientErrorFactory can be used as this adds traceId, timeGenerated and also handles generating the problem details when an exception is thrown.
  • Use ConfigureApiBehaviorOptions to configure problem detail type and title mapping.
builder.AddMvcProblemDetailsClientErrorAndExceptionFactory(options => options.ShowExceptionDetails = true);
Attribute Description
[ApiErrorFilterAttribute] Return problem details in json/xml if an error response is returned from Controller Action.
[ApiExceptionFilterAttribute] Converts exception to an Error Response of type ExceptionResult:StatusResult if an exception is thrown from Controller Action. The ApiErrorFilterAttribute can then handle the Error Response.
[MvcErrorFilterAttribute] Does nothing by default.
[MvcExceptionFilterAttribute] Intercepts OperationCanceledException and returns 499, all other exceptions are logged/handled by UseExceptionHandler/UseDeveloperExceptionPage.
  • Example Error Response
{
    "type": "about:blank",
    "title": "",
    "status": 450,
    "instance": "/new/contact",
    "traceId": "0HLNK5EJKEF4K:00000001",
    "timeGenerated": "2019-06-18T20:33:46.6609813Z"
}
  • Example Exception Response
{
    "type": "about:blank",
    "title": "An error has occured.",
    "status": 500,
    "detail": "System.Exception: Test\r\n   at DynamicForms.Web.Controllers.ApiFormController.Contact() in C:\\Development\\DynamicForms\\src\\DynamicForms.Web\\Controllers\\ApiFormController.cs:line 85\r\n   at lambda_method(Closure , Object , Object[] )\r\n   at Microsoft.Extensions.Internal.ObjectMethodExecutor.Execute(Object target, Object[] parameters)\r\n   at Microsoft.AspNetCore.Mvc.Internal.ActionMethodExecutor.SyncActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)\r\n   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeActionMethodAsync()\r\n   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeNextActionFilterAsync()\r\n   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Rethrow(ActionExecutedContext context)\r\n   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)\r\n   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeInnerFilterAsync()\r\n   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeNextExceptionFilterAsync()",
    "instance": "/new/contact",
    "traceId": "0HLNK5EJKEF4H:00000002",
    "timeGenerated": "2019-06-18T20:31:36.9343306Z"
}

Global Error Responses (Status Code >= 400) and Exceptions - Middleware

//If want to intercept content responses
//app.UseEndpointRouting(); //.NET Core 2.2 - There seems to be bugs with EndpointRoutingUrlHelper in 2.2
//OR
app.UseRouting(); //.NET Core 3.0
			
if (!env.IsProduction())
{
	//IsMvc and IsApi require access to IEndpointFeature which is what app.UseEndpointRouting/app.UseRouting/app.UseMvc provide.
	//When using app.UseWhen the delegate is evaluated when the response comes in before hitting MVC so routing hasn't been evaluated.
	//Unless we want to intercept content responses we can delay delegate evaluation until it comes back through the pipeline. 
	app.UseOutbound(appBranch =>
	{
		appBranch.UseWhen(context => context.Request.IsMvc(), mvcBranch => mvcBranch.UseDeveloperExceptionPage());
		appBranch.UseWhen(context => context.Request.IsApi(), apiBranch =>
		{
			apiBranch.UseProblemDetailsExceptionHandler(options => options.ShowExceptionDetails = true);
			apiBranch.UseProblemDetailsErrorResponseHandler(options => options.HandleContentResponses = false);
		});
	});

	//If handling content responses.
	//app.UseWhen(context => context.Request.IsApi(), apiBranch => apiBranch.UseProblemDetailsErrorResponseHandler(options => options.HandleContentResponses = true));

	app.UseDatabaseErrorPage();
}
else
{
	app.UseOutbound(appBranch =>
	{
		appBranch.UseWhen(context => context.Request.IsMvc(), mvcBranch => mvcBranch.UseExceptionHandler("/Home/Error"));
		appBranch.UseWhen(context => context.Request.IsApi(), apiBranch =>
		{
			apiBranch.UseProblemDetailsExceptionHandler(options => options.ShowExceptionDetails = false);
			apiBranch.UseProblemDetailsErrorResponseHandler(options => options.HandleContentResponses = false);
		});
	});

	//If handling content responses.
	//app.UseWhen(context => context.Request.IsApi(), apiBranch => apiBranch.UseProblemDetailsErrorResponseHandler(options => options.HandleContentResponses = true));
   
   app.UseHsts();
}
  • Example 404 Response
{
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.4",
    "title": "Not Found",
    "status": 404,
    "instance": "/new/test",
    "traceId": "0HLNK4OD71ENJ:00000004",
    "timeGenerated": "2019-06-18T20:04:50.3110301Z"
}
  • Example Exception Response
{
    "type": "about:blank",
    "title": "An error has occured.",
    "status": 500,
    "detail": "System.Exception: Test\r\n   at DynamicForms.Web.Controllers.ApiFormController.Contact() in C:\\Development\\DynamicForms\\src\\DynamicForms.Web\\Controllers\\ApiFormController.cs:line 85\r\n   at lambda_method(Closure , Object , Object[] )\r\n   at Microsoft.Extensions.Internal.ObjectMethodExecutor.Execute(Object target, Object[] parameters)\r\n   at Microsoft.AspNetCore.Mvc.Internal.ActionMethodExecutor.SyncActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)\r\n   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeActionMethodAsync()\r\n   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeNextActionFilterAsync()\r\n   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Rethrow(ActionExecutedContext context)\r\n   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)\r\n   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeInnerFilterAsync()\r\n   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeNextResourceFilter()\r\n   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Rethrow(ResourceExecutedContext context)\r\n   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)\r\n   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeFilterPipelineAsync()\r\n   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeAsync()\r\n   at Microsoft.AspNetCore.Routing.EndpointMiddleware.Invoke(HttpContext httpContext)\r\n   at Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware.Invoke(HttpContext httpContext)\r\n   at Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.Invoke(HttpContext context)\r\n   at Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.MigrationsEndPointMiddleware.Invoke(HttpContext context)\r\n   at Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.DatabaseErrorPageMiddleware.Invoke(HttpContext httpContext)\r\n   at Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.DatabaseErrorPageMiddleware.Invoke(HttpContext httpContext)\r\n   at AspNetCore.Mvc.HybridModelBindingAndViewToObjectResult.Middleware.ApiGlobalErrorResponseProblemDetailsMiddleware.InvokeAsync(HttpContext context) in C:\\Development\\HybridModelBindingAndViewToObjectResult\\src\\AspNetCore.Mvc.HybridModelBindingAndViewToObjectResult\\Middleware\\ApiGlobalErrorResponseProblemDetailsMiddleware.cs:line 34\r\n   at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.Invoke(HttpContext context)",
    "instance": "/new/contact",
    "traceId": "0HLNK55N49JII:00000002",
    "timeGenerated": "2019-06-18T20:13:39.6308515Z"
}

Application Builder Outbound Extension

 public static class ApplicationBuilderExtensions
{
    public static IApplicationBuilder UseOutbound(this IApplicationBuilder app, Action<IApplicationBuilder> configuration)
    {
        return app.UseOutboundWhen(_ => true, configuration);
    }

    public static IApplicationBuilder UseOutboundWhen(this IApplicationBuilder app, Func<HttpContext, bool> predicate, Action<IApplicationBuilder> configuration)
    {
        var outboundPipeline = app.New();
        outboundPipeline.UseWhen(predicate, appBranch => configuration(appBranch));
        outboundPipeline.Run(context =>
        {
            if (context.Items.ContainsKey("OutboundExceptionDispatchInfo"))
            {
                var edi = (ExceptionDispatchInfo)context.Items["OutboundExceptionDispatchInfo"];
                context.Items.Remove("OutboundExceptionDispatchInfo");
                edi.Throw();
            }
            return Task.CompletedTask;
        });

        var outboundHandler = outboundPipeline.Build();

        return app.Use(async (context, next) =>
        {
            try
            {
                await next.Invoke();
            }
            catch(Exception exception)
            {
                var edi = ExceptionDispatchInfo.Capture(exception);
                context.Items.Add("OutboundExceptionDispatchInfo", edi);
            }

            await outboundHandler(context);
        });
    }
}

Cross-Site Request Forgery (XSRF/CSRF)

  • APIS are vulnerable to XSRF/CSRF attack if the server uses authenticated session(cookies)
  • The solution is
    • Ensure that the 'safe' HTTP operations, such as GET, HEAD, OPTIONS, TRACE cannot be used to alter any server-side state.
    • Ensure that any 'unsafe' HTTP operations, such as POST, PUT, PATCH and DELETE, always require a valid CSRF token!
  • Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core
Attribute Description
[ApiGenerateAntiForgeryTokenAttribute] Generates AntiForgeryToken for GET, HEAD, OPTIONS, TRACE Api requests
[AutoValidateFormAntiforgeryTokenAttribute] Ensures only POST, PUT, PATCH and DELETE requests with Form content-type (By default only in Development) is checked for AntiForgeryToken.

Model Binding Attributes

Attribute Description
[FromBodyOrFormAttribute] Binds Model to Body or Form
[FromBodyOrQueryAttribute] Binds Model to Body or Query
[FromBodyOrRouteAttribute] Binds Model to Body or Route
[FromBodyOrFormRouteQueryAttribute] or [FromBodyOrModelBindingAttribute] Binds Model to Body or Form/Route/Query
[FromBodyAndQueryAttribute] Binds Model to Body and Query
[FromBodyAndRouteAttribute] Binds Model to Body and Route
[FromBodyFormAndRouteQueryAttribute] or [FromBodyAndModelBindingAttribute] Binds Model to Body/Form and Route/Query
[FromBodyExplicitAttribute] If conventions are used to change [FromBody] attributes this can be used to prevent doing so.

Model Binding Dynamic Mvc

services.AddDynamicModelBinder();
			
 public IActionResult Dynamic()
{
    return View(new ContactViewModel());
}

[ValidateAntiForgeryToken]
[HttpPost]
public IActionResult Dynamic(dynamic contactViewModel)
{
    if (ModelState.IsValid)
    {
        return RedirectToAction(nameof(Index));
    }

    var viewModel = contactViewModel.ToObject<ContactViewModel>();

    return View(viewModel);
}

Invalid Model State

  • I recommend using options.ConfigureProblemDetailsInvalidModelStateFactory() as this adds traceId and timeGenerated to the Invalid Model State Problem Details and also differentiates between 422 and 400 responses. 400 returned if action parameters are missing otherwise 422 returned.
  • There is an option to add angular formatted errors to the Invalid Model State Problem Details also.
 builder.ConfigureMvcProblemDetailsInvalidModelStateFactory(options => options.EnableAngularErrors = true);
  • Example invalid ModelState response
{
    "errors": {
        "Name": [
            "The Name field is required."
        ]
    },
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
    "title": "One or more validation errors occurred.",
    "status": 422,
    "detail": "Please refer to the errors property for additional details.",
    "instance": "/home/contact",
    "traceId": "8000003d-0006-fd00-b63f-84710c7967bb",
    "timeGenerated": "2019-06-18T21:18:03.7548395Z"
}
  • Example invalid ModelState response with Angular formatted errors
{
    "errors": {
        "Email": [
            "The Email field is not a valid e-mail address."
        ]
    },
    "type": "https://tools.ietf.org/html/rfc4918#section-11.2",
    "title": "One or more validation errors occurred.",
    "status": 422,
    "detail": "Please refer to the errors property for additional details.",
    "instance": "/api/values",
    "angularErrors": {
        "Email": [
            {
                "validatorKey": "",
                "message": "The Email field is not a valid e-mail address."
            }
        ]
    },
    "traceId": "80000006-0002-f900-b63f-84710c7967bb",
    "timeGenerated": "2019-06-21T20:12:37.44671Z"
}

Output Formatting Attributes

Attribute Description
[ConvertViewResultToObjectResultAttribute] Converts ViewResult to ObjectResult when Accept header matches output formatter SupportedMediaTypes.

Conventions

Convention Description
ApiErrorFilterConvention Adds ApiErrorFilterAttribute to Controller Actions.
ApiErrorExceptionFilterConvention Adds ApiExceptionFilterAttribute to Controller Actions.
MvcErrorFilterConvention Adds MvcErrorFilterAttribute to Controller Actions.
MvcErrorExceptionFilterConvention Adds MvcExceptionFilterAttribute to Controller Actions.
FromBodyAndOtherSourcesConvention Adds required attributes to Controllers, Actions and Parameters.
FromBodyOrOtherSourcesConvention Adds required attributes to Controllers, Actions and Parameters.
ConvertViewResultToObjectResultConvention Adds ConvertViewResultToObjectResultAttribute to Controller Actions.
MvcAsApiConvention Adds ApiErrorFilterConvention, ApiErrorExceptionFilterConvention, MvcErrorFilterConvention, MvcErrorExceptionFilterConvention, FromBodyOrOtherSourcesConvention and ConvertViewResultToObjectResultConvention to Controller Actions.

Api Response

  • If Accept Header matches OutputFormatter Supported Media Type and the ModelState is Valid, ViewResult is Converted to ObjectResult.
  • If Accept Header matches OutputFormatter Supported Media Type and the ModelState is Valid, ApiBehaviorOptions InvalidModelStateResponseFactory delegate is called which by default returns ValidationProblemDetails. See web API Documentation
  • If an error response or exception is thrown ProblemDetails are returned.

Authors

License

This project is licensed under the MIT License

Acknowledgments

About

ASP.NET Core library for handling MVC and Api requests from a single Controller Action

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages