Introduce a filter to send bad request results with details when ModelState is invalid (#6849)

* Introduce a filter to send bad request results with details when ModelState is invalid

Fixes #6789
This commit is contained in:
Pranav K 2017-09-21 11:09:32 -07:00 committed by GitHub
parent 6780f07b3c
commit 7f214492b8
20 changed files with 756 additions and 7 deletions

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics;
namespace Microsoft.AspNetCore.Mvc.Filters
{
@ -21,6 +22,7 @@ namespace Microsoft.AspNetCore.Mvc.Filters
/// For <see cref="IExceptionFilter"/> implementations, the filter runs only after an exception has occurred,
/// and so the observed order of execution will be opposite that of other filters.
/// </remarks>
[DebuggerDisplay("Filter = {Filter.ToString(),nq}, Order = {Order}")]
public class FilterDescriptor
{
/// <summary>
@ -43,9 +45,8 @@ namespace Microsoft.AspNetCore.Mvc.Filters
Filter = filter;
Scope = filterScope;
var orderedFilter = Filter as IOrderedFilter;
if (orderedFilter != null)
if (Filter is IOrderedFilter orderedFilter)
{
Order = orderedFilter.Order;
}

View File

@ -0,0 +1,37 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace Microsoft.AspNetCore.Mvc
{
/// <summary>
/// Options used to configure behavior for types annotated with <see cref="ApiControllerAttribute"/>.
/// </summary>
public class ApiBehaviorOptions
{
private Func<ActionContext, IActionResult> _invalidModelStateResponseFactory;
/// <summary>
/// Delegate invoked on actions annotated with <see cref="ApiControllerAttribute"/> to convert invalid
/// <see cref="ModelStateDictionary"/> into an <see cref="IActionResult"/>
/// <para>
/// By default, the delegate produces a <see cref="BadRequestObjectResult"/> using <see cref="ProblemDetails"/>
/// as the problem format.
/// </para>
/// </summary>
public Func<ActionContext, IActionResult> InvalidModelStateResponseFactory
{
get => _invalidModelStateResponseFactory;
set => _invalidModelStateResponseFactory = value ?? throw new ArgumentNullException(nameof(value));
}
/// <summary>
/// Disables the filter that returns an <see cref="BadRequestObjectResult"/> when
/// <see cref="ActionContext.ModelState"/> is invalid.
/// <seealso cref="InvalidModelStateResponseFactory"/>.
/// </summary>
public bool EnableModelStateInvalidFilter { get; set; } = true;
}
}

View File

@ -11,8 +11,8 @@ namespace Microsoft.AspNetCore.Mvc
/// this attribute can be used to target conventions, filters and other behaviors based on the purpose
/// of the controller.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class ApiControllerAttribute : ControllerAttribute , IApiBehaviorMetadata
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class ApiControllerAttribute : ControllerAttribute, IApiBehaviorMetadata
{
}
}

View File

@ -147,6 +147,8 @@ namespace Microsoft.Extensions.DependencyInjection
//
services.TryAddEnumerable(
ServiceDescriptor.Transient<IConfigureOptions<MvcOptions>, MvcCoreMvcOptionsSetup>());
services.TryAddEnumerable(
ServiceDescriptor.Transient<IConfigureOptions<ApiBehaviorOptions>, ApiBehaviorOptionsSetup>());
services.TryAddEnumerable(
ServiceDescriptor.Transient<IConfigureOptions<RouteOptions>, MvcCoreRouteOptionsSetup>());
@ -157,8 +159,11 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddEnumerable(
ServiceDescriptor.Transient<IApplicationModelProvider, DefaultApplicationModelProvider>());
services.TryAddEnumerable(
ServiceDescriptor.Transient<IApplicationModelProvider, ApiControllerApplicationModelProvider>());
services.TryAddEnumerable(
ServiceDescriptor.Transient<IActionDescriptorProvider, ControllerActionDescriptorProvider>());
services.TryAddSingleton<IActionDescriptorCollectionProvider, ActionDescriptorCollectionProvider>();
//

View File

@ -0,0 +1,60 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Mvc.Infrastructure
{
/// <summary>
/// A <see cref="IActionFilter"/> that responds to invalid <see cref="ActionContext.ModelState"/>. This filter is
/// added to all types and actions annotated with <see cref="ApiControllerAttribute"/>.
/// See <see cref="ApiBehaviorOptions"/> for ways to configure this filter.
/// </summary>
public class ModelStateInvalidFilter : IActionFilter, IOrderedFilter
{
private readonly ApiBehaviorOptions _apiBehaviorOptions;
private readonly ILogger _logger;
public ModelStateInvalidFilter(ApiBehaviorOptions apiBehaviorOptions, ILogger logger)
{
_apiBehaviorOptions = apiBehaviorOptions ?? throw new ArgumentNullException(nameof(apiBehaviorOptions));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <summary>
/// Gets the order value for determining the order of execution of filters. Filters execute in
/// ascending numeric value of the <see cref="Order"/> property.
/// </summary>
/// <remarks>
/// <para>
/// Filters are executed in a sequence determined by an ascending sort of the <see cref="Order"/> property.
/// </para>
/// <para>
/// The default Order for this attribute is -2000 so that it runs early in the pipeline.
/// </para>
/// <para>
/// Look at <see cref="IOrderedFilter.Order"/> for more detailed info.
/// </para>
/// </remarks>
public int Order => -2000;
/// <inheritdoc />
public bool IsReusable => true;
public void OnActionExecuted(ActionExecutedContext context)
{
}
public void OnActionExecuting(ActionExecutingContext context)
{
if (context.Result == null && !context.ModelState.IsValid)
{
_logger.ModelStateInvalidFilterExecuting();
context.Result = _apiBehaviorOptions.InvalidModelStateResponseFactory(context);
}
}
}
}

View File

@ -0,0 +1,45 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.Internal
{
public class ApiBehaviorOptionsSetup : IConfigureOptions<ApiBehaviorOptions>
{
private readonly IErrorDescriptionFactory _errorDescriptionFactory;
public ApiBehaviorOptionsSetup(IErrorDescriptionFactory errorDescriptionFactory)
{
_errorDescriptionFactory = errorDescriptionFactory;
}
public void Configure(ApiBehaviorOptions options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
options.InvalidModelStateResponseFactory = GetInvalidModelStateResponse;
IActionResult GetInvalidModelStateResponse(ActionContext context)
{
var errorDetails = _errorDescriptionFactory.CreateErrorDescription(
context.ActionDescriptor,
new ValidationProblemDetails(context.ModelState));
return new BadRequestObjectResult(errorDetails)
{
ContentTypes =
{
"application/problem+json",
"application/problem+xml",
},
};
}
}
}
}

View File

@ -0,0 +1,73 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics;
using System.Linq;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.Internal
{
public class ApiControllerApplicationModelProvider : IApplicationModelProvider
{
private readonly ApiBehaviorOptions _apiBehaviorOptions;
private readonly ModelStateInvalidFilter _modelStateInvalidFilter;
public ApiControllerApplicationModelProvider(IOptions<ApiBehaviorOptions> apiBehaviorOptions, ILoggerFactory loggerFactory)
{
_apiBehaviorOptions = apiBehaviorOptions.Value;
if (_apiBehaviorOptions.EnableModelStateInvalidFilter && _apiBehaviorOptions.InvalidModelStateResponseFactory == null)
{
throw new ArgumentException(Resources.FormatPropertyOfTypeCannotBeNull(
typeof(ApiBehaviorOptions),
nameof(ApiBehaviorOptions.InvalidModelStateResponseFactory)));
}
_modelStateInvalidFilter = new ModelStateInvalidFilter(
apiBehaviorOptions.Value,
loggerFactory.CreateLogger<ModelStateInvalidFilter>());
}
/// <remarks>
/// Order is set to execute after the <see cref="DefaultApplicationModelProvider"/>.
/// </remarks>
public int Order => -1000 + 10;
public void OnProvidersExecuted(ApplicationModelProviderContext context)
{
}
public void OnProvidersExecuting(ApplicationModelProviderContext context)
{
foreach (var controllerModel in context.Result.Controllers)
{
if (controllerModel.Attributes.OfType<IApiBehaviorMetadata>().Any())
{
if (_apiBehaviorOptions.EnableModelStateInvalidFilter)
{
Debug.Assert(_apiBehaviorOptions.InvalidModelStateResponseFactory != null);
controllerModel.Filters.Add(_modelStateInvalidFilter);
}
continue;
}
foreach (var actionModel in controllerModel.Actions)
{
if (actionModel.Attributes.OfType<IApiBehaviorMetadata>().Any())
{
if (_apiBehaviorOptions.EnableModelStateInvalidFilter)
{
Debug.Assert(_apiBehaviorOptions.InvalidModelStateResponseFactory != null);
actionModel.Filters.Add(_modelStateInvalidFilter);
}
}
}
}
}
}
}

View File

@ -79,7 +79,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
private static readonly Action<ILogger, Exception> _cannotApplyRequestFormLimits;
private static readonly Action<ILogger, Exception> _appliedRequestFormLimits;
private static readonly Action<ILogger, Exception> _modelStateInvalidFilterExecuting;
static MvcCoreLoggerExtensions()
{
@ -282,6 +283,12 @@ namespace Microsoft.AspNetCore.Mvc.Internal
LogLevel.Debug,
2,
"Applied the configured form options on the current request.");
_modelStateInvalidFilterExecuting = LoggerMessage.Define(
LogLevel.Debug,
1,
"The request has model state errors, returning an error response.");
}
public static IDisposable ActionScope(this ILogger logger, ActionDescriptor action)
@ -592,6 +599,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
_appliedRequestFormLimits(logger, null);
}
public static void ModelStateInvalidFilterExecuting(this ILogger logger) => _modelStateInvalidFilterExecuting(logger, null);
private class ActionLogScope : IReadOnlyList<KeyValuePair<string, object>>
{
private readonly ActionDescriptor _action;

View File

@ -248,6 +248,13 @@ namespace Microsoft.AspNetCore.Mvc
typeof(MvcCoreRouteOptionsSetup),
}
},
{
typeof(IConfigureOptions<ApiBehaviorOptions>),
new Type[]
{
typeof(ApiBehaviorOptionsSetup),
}
},
{
typeof(IActionConstraintProvider),
new Type[]
@ -288,6 +295,7 @@ namespace Microsoft.AspNetCore.Mvc
new Type[]
{
typeof(DefaultApplicationModelProvider),
typeof(ApiControllerApplicationModelProvider),
}
},
};

View File

@ -0,0 +1,84 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Infrastructure
{
public class ModelStateInvalidFilterTest
{
[Fact]
public void OnActionExecuting_NoOpsIfResultIsAlreadySet()
{
// Arrange
var options = new ApiBehaviorOptions
{
InvalidModelStateResponseFactory = _ => new BadRequestResult(),
};
var filter = new ModelStateInvalidFilter(options, NullLogger.Instance);
var context = GetActionExecutingContext();
var expected = new OkResult();
context.Result = expected;
// Act
filter.OnActionExecuting(context);
// Assert
Assert.Same(expected, context.Result);
}
[Fact]
public void OnActionExecuting_NoOpsIfModelStateIsValid()
{
// Arrange
var options = new ApiBehaviorOptions
{
InvalidModelStateResponseFactory = _ => new BadRequestResult(),
};
var filter = new ModelStateInvalidFilter(options, NullLogger.Instance);
var context = GetActionExecutingContext();
// Act
filter.OnActionExecuting(context);
// Assert
Assert.Null(context.Result);
}
[Fact]
public void OnActionExecuting_InvokesResponseFactoryIfModelStateIsInvalid()
{
// Arrange
var expected = new BadRequestResult();
var options = new ApiBehaviorOptions
{
InvalidModelStateResponseFactory = _ => expected,
};
var filter = new ModelStateInvalidFilter(options, NullLogger.Instance);
var context = GetActionExecutingContext();
context.ModelState.AddModelError("some-key", "some-error");
// Act
filter.OnActionExecuting(context);
// Assert
Assert.Same(expected, context.Result);
}
private static ActionExecutingContext GetActionExecutingContext()
{
return new ActionExecutingContext(
new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()),
Array.Empty<IFilterMetadata>(),
new Dictionary<string, object>(),
new object());
}
}
}

View File

@ -0,0 +1,139 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Internal
{
public class ApiControllerApplicationModelProviderTest
{
[Fact]
public void OnProvidersExecuting_AddsModelStateInvalidFilter_IfTypeIsAnnotatedWithAttribute()
{
// Arrange
var context = GetContext(typeof(TestApiController));
var options = new TestOptionsManager<ApiBehaviorOptions>(new ApiBehaviorOptions
{
InvalidModelStateResponseFactory = _ => null,
});
var provider = new ApiControllerApplicationModelProvider(options, NullLoggerFactory.Instance);
// Act
provider.OnProvidersExecuting(context);
// Assert
var controllerModel = Assert.Single(context.Result.Controllers);
Assert.IsType<ModelStateInvalidFilter>(controllerModel.Filters.Last());
}
[Fact]
public void OnProvidersExecuting_DoesNotAddModelStateInvalidFilterToController_IfFeatureIsDisabledViaOptions()
{
// Arrange
var context = GetContext(typeof(TestApiController));
var options = new TestOptionsManager<ApiBehaviorOptions>(new ApiBehaviorOptions
{
EnableModelStateInvalidFilter = false,
});
var provider = new ApiControllerApplicationModelProvider(options, NullLoggerFactory.Instance);
// Act
provider.OnProvidersExecuting(context);
// Assert
var controllerModel = Assert.Single(context.Result.Controllers);
Assert.DoesNotContain(typeof(ModelStateInvalidFilter), controllerModel.Filters.Select(f => f.GetType()));
}
[Fact]
public void OnProvidersExecuting_AddsModelStateInvalidFilter_IfActionIsAnnotatedWithAttribute()
{
// Arrange
var context = GetContext(typeof(SimpleController));
var options = new TestOptionsManager<ApiBehaviorOptions>(new ApiBehaviorOptions
{
InvalidModelStateResponseFactory = _ => null,
});
var provider = new ApiControllerApplicationModelProvider(options, NullLoggerFactory.Instance);
// Act
provider.OnProvidersExecuting(context);
// Assert
Assert.Collection(
Assert.Single(context.Result.Controllers).Actions.OrderBy(a => a.ActionName),
action =>
{
Assert.Contains(typeof(ModelStateInvalidFilter), action.Filters.Select(f => f.GetType()));
},
action =>
{
Assert.DoesNotContain(typeof(ModelStateInvalidFilter), action.Filters.Select(f => f.GetType()));
});
}
[Fact]
public void OnProvidersExecuting_SkipsAddingFilterToActionIfFeatureIsDisabledUsingOptions()
{
// Arrange
var context = GetContext(typeof(SimpleController));
var options = new TestOptionsManager<ApiBehaviorOptions>(new ApiBehaviorOptions
{
EnableModelStateInvalidFilter = false,
});
var provider = new ApiControllerApplicationModelProvider(options, NullLoggerFactory.Instance);
// Act
provider.OnProvidersExecuting(context);
// Assert
Assert.Collection(
Assert.Single(context.Result.Controllers).Actions.OrderBy(a => a.ActionName),
action =>
{
Assert.DoesNotContain(typeof(ModelStateInvalidFilter), action.Filters.Select(f => f.GetType()));
},
action =>
{
Assert.DoesNotContain(typeof(ModelStateInvalidFilter), action.Filters.Select(f => f.GetType()));
});
}
private static ApplicationModelProviderContext GetContext(Type type)
{
var context = new ApplicationModelProviderContext(new[] { type.GetTypeInfo() });
new DefaultApplicationModelProvider(new TestOptionsManager<MvcOptions>()).OnProvidersExecuting(context);
return context;
}
[ApiController]
private class TestApiController : Controller
{
public IActionResult TestAction() => null;
}
private class SimpleController : Controller
{
public IActionResult ActionWithoutFilter() => null;
[TestApiBehavior]
public IActionResult ActionWithFilter() => null;
}
[AttributeUsage(AttributeTargets.Method)]
private class TestApiBehavior : Attribute, IApiBehaviorMetadata
{
}
}
}

View File

@ -0,0 +1,101 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using BasicWebSite;
using BasicWebSite.Models;
using Newtonsoft.Json;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
{
public class ApiControllerAttributeTests : IClassFixture<MvcTestFixture<BasicWebSite.Startup>>
{
public ApiControllerAttributeTests(MvcTestFixture<BasicWebSite.Startup> fixture)
{
Client = fixture.Client;
}
public HttpClient Client { get; }
[Fact]
public async Task ActionsReturnBadRequest_WhenModelStateIsInvalid()
{
// Arrange
var contactModel = new Contact
{
Name = "Abc",
City = "Redmond",
State = "WA",
Zip = "Invalid",
};
var expected = new ValidationProblemDetails
{
Errors =
{
["Zip"] = new[] { @"The field Zip must match the regular expression '\d{5}'." },
["Name"] = new[] { "The field Name must be a string with a minimum length of 5 and a maximum length of 30." },
},
};
var contactString = JsonConvert.SerializeObject(contactModel);
// Act
var response = await Client.PostAsJsonAsync("/contact", contactModel);
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
Assert.Equal("application/problem+json", response.Content.Headers.ContentType.MediaType);
var actual = JsonConvert.DeserializeObject<ValidationProblemDetails>(await response.Content.ReadAsStringAsync());
Assert.Equal(expected.Errors.Count, actual.Errors.Count);
foreach (var error in expected.Errors)
{
Assert.Equal(error.Value, actual.Errors[error.Key]);
}
}
[Fact]
public async Task ActionsReturnBadRequest_UsesProblemDescriptionProviderAndApiConventionsToConfigureErrorResponse()
{
// Arrange
var contactModel = new Contact
{
Name = "Abc",
City = "Redmond",
State = "WA",
Zip = "Invalid",
};
var expected = new[]
{
new VndError
{
Path = "Name",
Message = "The field Name must be a string with a minimum length of 5 and a maximum length of 30.",
},
new VndError
{
Path = "Zip",
Message = @"The field Zip must match the regular expression '\d{5}'.",
},
};
var contactString = JsonConvert.SerializeObject(contactModel);
// Act
var response = await Client.PostAsJsonAsync("/contact/PostWithVnd", contactModel);
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
Assert.Equal("application/vnd.error+json", response.Content.Headers.ContentType.MediaType);
var actual = JsonConvert.DeserializeObject<VndError[]>(await response.Content.ReadAsStringAsync());
actual = actual.OrderBy(e => e.Path).ToArray();
Assert.Equal(expected.Length, actual.Length);
for (var i = 0; i < expected.Length; i++)
{
Assert.Equal(expected[i].Path, expected[i].Path);
Assert.Equal(expected[i].Message, expected[i].Message);
}
}
}
}

View File

@ -348,6 +348,13 @@ namespace Microsoft.AspNetCore.Mvc
typeof(MvcCoreRouteOptionsSetup),
}
},
{
typeof(IConfigureOptions<ApiBehaviorOptions>),
new Type[]
{
typeof(ApiBehaviorOptionsSetup),
}
},
{
typeof(IConfigureOptions<MvcViewOptions>),
new Type[]
@ -410,6 +417,7 @@ namespace Microsoft.AspNetCore.Mvc
typeof(CorsApplicationModelProvider),
typeof(AuthorizationApplicationModelProvider),
typeof(TempDataApplicationModelProvider),
typeof(ApiControllerApplicationModelProvider),
}
},
{

View File

@ -0,0 +1,25 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Linq;
using BasicWebSite.Models;
namespace BasicWebSite
{
public class ContactsRepository
{
private readonly List<Contact> _contacts = new List<Contact>();
public Contact GetContact(int id)
{
return _contacts.FirstOrDefault(f => f.ContactId == id);
}
public void Add(Contact contact)
{
contact.ContactId = _contacts.Count + 1;
_contacts.Add(contact);
}
}
}

View File

@ -0,0 +1,51 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BasicWebSite.Models;
using Microsoft.AspNetCore.Mvc;
namespace BasicWebSite
{
[ApiController]
[Route("/contact")]
public class ContactApiController : Controller
{
private readonly ContactsRepository _repository;
public ContactApiController(ContactsRepository repository)
{
_repository = repository;
}
[HttpGet("{id}")]
public ActionResult<Contact> Get(int id)
{
var contact = _repository.GetContact(id);
if (contact == null)
{
return NotFound();
}
return contact;
}
[HttpPost]
public ActionResult<Contact> Post([FromBody] Contact contact)
{
_repository.Add(contact);
return CreatedAtAction(nameof(Get), new { id = contact.ContactId }, contact);
}
[VndError]
[HttpPost("PostWithVnd")]
public ActionResult<Contact> PostWithVnd([FromBody] Contact contact)
{
_repository.Add(contact);
return CreatedAtAction(nameof(Get), new { id = contact.ContactId }, contact);
}
}
}

View File

@ -1,12 +1,15 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.ComponentModel.DataAnnotations;
namespace BasicWebSite.Models
{
public class Contact
{
public int ContactId { get; set; }
[StringLength(30, MinimumLength = 5)]
public string Name { get; set; }
public GenderType Gender { get; set; }
@ -17,6 +20,7 @@ namespace BasicWebSite.Models
public string State { get; set; }
[RegularExpression(@"\d{5}")]
public string Zip { get; set; }
public string Email { get; set; }

View File

@ -2,10 +2,13 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.IO;
using System.Linq;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
namespace BasicWebSite
@ -16,13 +19,30 @@ namespace BasicWebSite
public void ConfigureServices(IServiceCollection services)
{
services
.AddMvc(
options => { options.Conventions.Add(new ApplicationDescription("This is a basic website.")); })
.AddMvc(options => options.Conventions.Add(new ApplicationDescription("This is a basic website.")))
.AddXmlDataContractSerializerFormatters();
services.Configure<ApiBehaviorOptions>(options =>
{
var previous = options.InvalidModelStateResponseFactory;
options.InvalidModelStateResponseFactory = context =>
{
var result = (BadRequestObjectResult) previous(context);
if (context.ActionDescriptor.FilterDescriptors.Any(f => f.Filter is VndErrorAttribute))
{
result.ContentTypes.Clear();
result.ContentTypes.Add("application/vnd.error+json");
}
return result;
};
});
services.AddLogging();
services.AddSingleton<IActionDescriptorProvider, ActionDescriptorCreationCounter>();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddSingleton<ContactsRepository>();
services.AddSingleton<IErrorDescriptorProvider, VndErrorDescriptionProvider>();
services.AddScoped<RequestIdService>();
}

View File

@ -0,0 +1,23 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
namespace BasicWebSite
{
public class VndError
{
public string LogRef { get; set; }
public string Path { get; set; }
public string Message { get; set; }
}
}

View File

@ -0,0 +1,13 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Mvc.Filters;
namespace BasicWebSite
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class VndErrorAttribute : Attribute, IFilterMetadata
{
}
}

View File

@ -0,0 +1,43 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
namespace BasicWebSite
{
public class VndErrorDescriptionProvider : IErrorDescriptorProvider
{
public int Order => 0;
public void OnProvidersExecuting(ErrorDescriptionContext context)
{
if (context.ActionDescriptor.FilterDescriptors.Any(f => f.Filter is VndErrorAttribute) &&
context.Result is ValidationProblemDetails problemDetails)
{
var vndErrors = new List<VndError>();
foreach (var item in problemDetails.Errors)
{
foreach (var message in item.Value)
{
vndErrors.Add(new VndError
{
LogRef = problemDetails.Title,
Path = item.Key,
Message = message,
});
}
}
context.Result = vndErrors;
}
}
}
}