Smooth rough ApiBehavior edges

Fixes #7262
This commit is contained in:
Pranav K 2018-01-19 15:19:12 -08:00
parent 2c6ba372c1
commit ebdb3c650a
18 changed files with 54 additions and 496 deletions

View File

@ -1,116 +0,0 @@
// 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 Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace Microsoft.AspNetCore.Mvc.ApiExplorer
{
public class ApiBehaviorApiDescriptionProvider : IApiDescriptionProvider
{
private readonly IModelMetadataProvider _modelMetadaProvider;
public ApiBehaviorApiDescriptionProvider(IModelMetadataProvider modelMetadataProvider)
{
_modelMetadaProvider = modelMetadataProvider;
}
/// <remarks>
/// The order is set to execute after the default provider.
/// </remarks>
public int Order => -1000 + 10;
public void OnProvidersExecuted(ApiDescriptionProviderContext context)
{
}
public void OnProvidersExecuting(ApiDescriptionProviderContext context)
{
foreach (var description in context.Results)
{
if (!AppliesTo(description))
{
continue;
}
foreach (var responseType in CreateProblemResponseTypes(description))
{
description.SupportedResponseTypes.Add(responseType);
}
}
}
public bool AppliesTo(ApiDescription description)
{
return description.ActionDescriptor.FilterDescriptors.Any(f => f.Filter is IApiBehaviorMetadata);
}
// Check if the parameter is named "id" (e.g. int id) or ends in Id (e.g. personId)
public bool IsIdParameter(ParameterDescriptor parameter)
{
if (parameter.Name == null)
{
return false;
}
if (string.Equals("id", parameter.Name, StringComparison.Ordinal))
{
return true;
}
// We're looking for a name ending with Id, but preceded by a lower case letter. This should match
// the normal PascalCase naming conventions.
if (parameter.Name.Length >= 3 &&
parameter.Name.EndsWith("Id", StringComparison.Ordinal) &&
char.IsLower(parameter.Name, parameter.Name.Length - 3))
{
return true;
}
return false;
}
public IEnumerable<ApiResponseType> CreateProblemResponseTypes(ApiDescription description)
{
if (description.ActionDescriptor.Parameters.Any() || description.ActionDescriptor.BoundProperties.Any())
{
// For validation errors.
yield return CreateProblemResponse(StatusCodes.Status400BadRequest);
if (description.ActionDescriptor.Parameters.Any(p => IsIdParameter(p)))
{
yield return CreateProblemResponse(StatusCodes.Status404NotFound);
}
}
yield return CreateProblemResponse(statusCode: 0, isDefaultResponse: true);
}
private ApiResponseType CreateProblemResponse(int statusCode, bool isDefaultResponse = false)
{
return new ApiResponseType
{
ApiResponseFormats = new List<ApiResponseFormat>
{
new ApiResponseFormat
{
MediaType = "application/problem+json",
},
new ApiResponseFormat
{
MediaType = "application/problem+xml",
},
},
IsDefaultResponse = isDefaultResponse,
ModelMetadata = _modelMetadaProvider.GetMetadataForType(typeof(ProblemDetails)),
StatusCode = statusCode,
Type = typeof(ProblemDetails),
};
}
}
}

View File

@ -26,8 +26,6 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddSingleton<IApiDescriptionGroupCollectionProvider, ApiDescriptionGroupCollectionProvider>();
services.TryAddEnumerable(
ServiceDescriptor.Transient<IApiDescriptionProvider, DefaultApiDescriptionProvider>());
services.TryAddEnumerable(
ServiceDescriptor.Transient<IApiDescriptionProvider, ApiBehaviorApiDescriptionProvider>());
}
}
}

View File

@ -10,4 +10,5 @@ using System.Runtime.CompilerServices;
[assembly: TypeForwardedTo(typeof(Microsoft.AspNetCore.Mvc.ApiExplorer.ApiParameterRouteInfo))]
[assembly: TypeForwardedTo(typeof(Microsoft.AspNetCore.Mvc.ApiExplorer.ApiRequestFormat))]
[assembly: TypeForwardedTo(typeof(Microsoft.AspNetCore.Mvc.ApiExplorer.ApiResponseFormat))]
[assembly: TypeForwardedTo(typeof(Microsoft.AspNetCore.Mvc.ApiExplorer.ApiResponseType))]
[assembly: TypeForwardedTo(typeof(Microsoft.AspNetCore.Mvc.ApiExplorer.ApiResponseType))]
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.ApiExplorer.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]

View File

@ -18,8 +18,8 @@ namespace Microsoft.AspNetCore.Mvc
/// 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.
/// By default, the delegate produces a <see cref="BadRequestObjectResult"/> that wraps a serialized form
/// of <see cref="ModelStateDictionary"/>.
/// </para>
/// </summary>
public Func<ActionContext, IActionResult> InvalidModelStateResponseFactory

View File

@ -3,6 +3,7 @@
using System;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.Internal
@ -29,16 +30,16 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{
var errorDetails = _errorDescriptionFactory.CreateErrorDescription(
context.ActionDescriptor,
new ValidationProblemDetails(context.ModelState));
context.ModelState);
return new BadRequestObjectResult(errorDetails)
{
ContentTypes =
{
"application/problem+json",
"application/problem+xml",
},
};
var result = (errorDetails is ModelStateDictionary modelState) ?
new BadRequestObjectResult(modelState) :
new BadRequestObjectResult(errorDetails);
result.ContentTypes.Add("application/problem+json");
result.ContentTypes.Add("application/problem+json");
return result;
}
}
}

View File

@ -1327,7 +1327,7 @@ namespace Microsoft.AspNetCore.Mvc.Core
=> string.Format(CultureInfo.CurrentCulture, GetString("UrlHelper_RelativePagePathIsNotSupported"), p0);
/// <summary>
/// One or more validation errors occured.
/// One or more validation errors occurred.
/// </summary>
internal static string ValidationProblemDescription_Title
{
@ -1335,7 +1335,7 @@ namespace Microsoft.AspNetCore.Mvc.Core
}
/// <summary>
/// One or more validation errors occured.
/// One or more validation errors occurred.
/// </summary>
internal static string FormatValidationProblemDescription_Title()
=> GetString("ValidationProblemDescription_Title");

View File

@ -413,7 +413,7 @@
<value>The relative page path '{0}' can only be used while executing a Razor Page. Specify a root relative path with a leading '/' to generate a URL outside of a Razor Page.</value>
</data>
<data name="ValidationProblemDescription_Title" xml:space="preserve">
<value>One or more validation errors occured.</value>
<value>One or more validation errors occurred.</value>
</data>
<data name="ApiController_AttributeRouteRequired" xml:space="preserve">
<value>Action methods on controllers annotated with {0} must have an attribute route.</value>

View File

@ -25,7 +25,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
=> string.Format(CultureInfo.CurrentCulture, GetString("EnumerableWrapperProvider_InvalidSourceEnumerableOfT"), p0);
/// <summary>
/// An error occured while deserializing input data.
/// An error occurred while deserializing input data.
/// </summary>
internal static string ErrorDeserializingInputData
{
@ -33,7 +33,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
}
/// <summary>
/// An error occured while deserializing input data.
/// An error occurred while deserializing input data.
/// </summary>
internal static string FormatErrorDeserializingInputData()
=> GetString("ErrorDeserializingInputData");

View File

@ -121,7 +121,7 @@
<value>The type must be an interface and must be or derive from '{0}'.</value>
</data>
<data name="ErrorDeserializingInputData" xml:space="preserve">
<value>An error occured while deserializing input data.</value>
<value>An error occurred while deserializing input data.</value>
</data>
<data name="RequiredProperty_MustHaveDataMemberRequired" xml:space="preserve">
<value>{0} does not recognize '{1}', so instead use '{2}' with '{3}' set to '{4}' for value type property '{5}' on type '{6}'.</value>

View File

@ -1,241 +0,0 @@
// 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 Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.ApiExplorer
{
public class ApiBehaviorApiDescriptionProviderTest
{
[Fact]
public void AppliesTo_ActionWithoutApiBehavior_ReturnsFalse()
{
// Arrange
var action = new ActionDescriptor()
{
FilterDescriptors = new List<FilterDescriptor>(),
};
var description = new ApiDescription()
{
ActionDescriptor = action,
};
var provider = new ApiBehaviorApiDescriptionProvider(new EmptyModelMetadataProvider());
// Act
var result = provider.AppliesTo(description);
// Assert
Assert.False(result);
}
[Fact]
public void AppliesTo_ActionWithApiBehavior_ReturnsTrue()
{
// Arrange
var action = new ActionDescriptor()
{
FilterDescriptors = new List<FilterDescriptor>()
{
new FilterDescriptor(Mock.Of<IApiBehaviorMetadata>(), FilterScope.Global),
}
};
var description = new ApiDescription()
{
ActionDescriptor = action,
};
var provider = new ApiBehaviorApiDescriptionProvider(new EmptyModelMetadataProvider());
// Act
var result = provider.AppliesTo(description);
// Assert
Assert.True(result);
}
[Theory]
[InlineData("id")]
[InlineData("personId")]
[InlineData("üId")]
public void IsIdParameter_ParameterNameMatchesConvention_ReturnsTrue(string name)
{
var parameter = new ParameterDescriptor()
{
Name = name,
};
var provider = new ApiBehaviorApiDescriptionProvider(new EmptyModelMetadataProvider());
// Act
var result = provider.IsIdParameter(parameter);
// Assert
Assert.True(result);
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData("i")]
[InlineData("Id")]
[InlineData("iD")]
[InlineData("persoNId")]
[InlineData("personid")]
[InlineData("ü Id")]
[InlineData("ÜId")]
public void IsIdParameter_ParameterNameDoesNotMatchConvention_ReturnsFalse(string name)
{
var parameter = new ParameterDescriptor()
{
Name = name,
};
var provider = new ApiBehaviorApiDescriptionProvider(new EmptyModelMetadataProvider());
// Act
var result = provider.IsIdParameter(parameter);
// Assert
Assert.False(result);
}
[Fact]
public void CreateProblemResponseTypes_NoParameters_IncludesDefaultResponse()
{
// Arrange
var action = new ActionDescriptor()
{
FilterDescriptors = new List<FilterDescriptor>()
{
new FilterDescriptor(Mock.Of<IApiBehaviorMetadata>(), FilterScope.Global),
},
BoundProperties = new List<ParameterDescriptor>(),
Parameters = new List<ParameterDescriptor>(),
};
var description = new ApiDescription()
{
ActionDescriptor = action,
};
var provider = new ApiBehaviorApiDescriptionProvider(new EmptyModelMetadataProvider());
// Act
var results = provider.CreateProblemResponseTypes(description);
// Assert
Assert.Collection(
results.OrderBy(r => r.StatusCode),
r =>
{
Assert.Equal(typeof(ProblemDetails), r.Type);
Assert.Equal(0, r.StatusCode);
Assert.True(r.IsDefaultResponse);
});
}
[Fact]
public void CreateProblemResponseTypes_WithBoundProperty_Includes400Response()
{
// Arrange
var action = new ActionDescriptor()
{
FilterDescriptors = new List<FilterDescriptor>()
{
new FilterDescriptor(Mock.Of<IApiBehaviorMetadata>(), FilterScope.Global),
},
BoundProperties = new List<ParameterDescriptor>()
{
new ParameterDescriptor()
},
Parameters = new List<ParameterDescriptor>(),
};
var description = new ApiDescription()
{
ActionDescriptor = action,
};
var provider = new ApiBehaviorApiDescriptionProvider(new EmptyModelMetadataProvider());
// Act
var results = provider.CreateProblemResponseTypes(description);
// Assert
Assert.Collection(
results.OrderBy(r => r.StatusCode),
r =>
{
Assert.Equal(typeof(ProblemDetails), r.Type);
Assert.Equal(0, r.StatusCode);
Assert.True(r.IsDefaultResponse);
},
r =>
{
Assert.Equal(typeof(ProblemDetails), r.Type);
Assert.Equal(400, r.StatusCode);
Assert.False(r.IsDefaultResponse);
});
}
[Fact]
public void CreateProblemResponseTypes_WithIdParameter_Includes404Response()
{
// Arrange
var action = new ActionDescriptor()
{
FilterDescriptors = new List<FilterDescriptor>()
{
new FilterDescriptor(Mock.Of<IApiBehaviorMetadata>(), FilterScope.Global),
},
BoundProperties = new List<ParameterDescriptor>()
{
},
Parameters = new List<ParameterDescriptor>()
{
new ParameterDescriptor()
{
Name = "customerId",
}
},
};
var description = new ApiDescription()
{
ActionDescriptor = action,
};
var provider = new ApiBehaviorApiDescriptionProvider(new EmptyModelMetadataProvider());
// Act
var results = provider.CreateProblemResponseTypes(description);
// Assert
Assert.Collection(
results.OrderBy(r => r.StatusCode),
r =>
{
Assert.Equal(typeof(ProblemDetails), r.Type);
Assert.Equal(0, r.StatusCode);
Assert.True(r.IsDefaultResponse);
},
r =>
{
Assert.Equal(typeof(ProblemDetails), r.Type);
Assert.Equal(400, r.StatusCode);
Assert.False(r.IsDefaultResponse);
},
r =>
{
Assert.Equal(typeof(ProblemDetails), r.Type);
Assert.Equal(404, r.StatusCode);
Assert.False(r.IsDefaultResponse);
});
}
}
}

View File

@ -282,7 +282,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
var entry = Assert.Single(bindingContext.ModelState);
Assert.Equal(string.Empty, entry.Key);
var errorMessage = Assert.Single(entry.Value.Errors).ErrorMessage;
Assert.Equal("An error occured while deserializing input data.", errorMessage);
Assert.Equal("An error occurred while deserializing input data.", errorMessage);
Assert.Null(entry.Value.Errors[0].Exception);
}
@ -366,7 +366,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
var entry = Assert.Single(bindingContext.ModelState);
Assert.Equal(string.Empty, entry.Key);
var errorMessage = Assert.Single(entry.Value.Errors).ErrorMessage;
Assert.Equal("An error occured while deserializing input data.", errorMessage);
Assert.Equal("An error occurred while deserializing input data.", errorMessage);
Assert.Null(entry.Value.Errors[0].Exception);
}

View File

@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Mvc
var problemDescription = new ValidationProblemDetails();
// Assert
Assert.Equal("One or more validation errors occured.", problemDescription.Title);
Assert.Equal("One or more validation errors occurred.", problemDescription.Title);
Assert.Empty(problemDescription.Errors);
}
@ -34,7 +34,7 @@ namespace Microsoft.AspNetCore.Mvc
var problemDescription = new ValidationProblemDetails(modelStateDictionary);
// Assert
Assert.Equal("One or more validation errors occured.", problemDescription.Title);
Assert.Equal("One or more validation errors occurred.", problemDescription.Title);
Assert.Collection(
problemDescription.Errors,
item =>
@ -64,7 +64,7 @@ namespace Microsoft.AspNetCore.Mvc
var problemDescription = new ValidationProblemDetails(modelStateDictionary);
// Assert
Assert.Equal("One or more validation errors occured.", problemDescription.Title);
Assert.Equal("One or more validation errors occurred.", problemDescription.Title);
Assert.Collection(
problemDescription.Errors,
item =>

View File

@ -1,6 +1,7 @@
// 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 System.Net;
using System.Net.Http;
@ -32,14 +33,6 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
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
@ -48,12 +41,22 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
// 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]);
}
var actual = JsonConvert.DeserializeObject<Dictionary<string, string[]>>(await response.Content.ReadAsStringAsync());
Assert.Collection(
actual.OrderBy(kvp => kvp.Key),
kvp =>
{
Assert.Equal("Name", kvp.Key);
var error = Assert.Single(kvp.Value);
Assert.Equal("The field Name must be a string with a minimum length of 5 and a maximum length of 30.", error);
},
kvp =>
{
Assert.Equal("Zip", kvp.Key);
var error = Assert.Single(kvp.Value);
Assert.Equal("The field Zip must match the regular expression '\\d{5}'.", error);
}
);
}
[Fact]

View File

@ -1065,85 +1065,6 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal("ApiExplorerInboundOutbound/SuppressedForLinkGeneration", description.RelativePath);
}
[Fact]
public async Task ProblemDetails_AddsProblemAsDefaultErrorResult()
{
// Act
var body = await Client.GetStringAsync("ApiExplorerApiController/ActionWithoutParameters");
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
// Assert
var description = Assert.Single(result);
Assert.Collection(
description.SupportedResponseTypes,
response =>
{
Assert.Equal(0, response.StatusCode);
Assert.True(response.IsDefaultResponse);
AssertProblemDetails(response);
});
}
[Fact]
public async Task ProblemDetails_AddsProblemAsErrorResultForBadResult_WhenActionHasParameters()
{
// Act
var body = await Client.GetStringAsync("ApiExplorerApiController/ActionWithSomeParameters");
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
// Assert
var description = Assert.Single(result);
Assert.Collection(
description.SupportedResponseTypes.OrderBy(r => r.StatusCode),
response =>
{
Assert.Equal(0, response.StatusCode);
Assert.True(response.IsDefaultResponse);
AssertProblemDetails(response);
},
response => Assert.Equal(200, response.StatusCode),
response =>
{
Assert.Equal(400, response.StatusCode);
Assert.False(response.IsDefaultResponse);
AssertProblemDetails(response);
});
}
[Theory]
[InlineData("ApiExplorerApiController/ActionWithIdParameter")]
[InlineData("ApiExplorerApiController/ActionWithIdSuffixParameter")]
public async Task ProblemDetails_AddsProblemAsErrorResultForNotFoundResult_WhenActionHasAnIdParameters(string url)
{
// Act
var body = await Client.GetStringAsync(url);
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
// Assert
var description = Assert.Single(result);
Assert.Collection(
description.SupportedResponseTypes.OrderBy(r => r.StatusCode),
response =>
{
Assert.Equal(0, response.StatusCode);
Assert.True(response.IsDefaultResponse);
AssertProblemDetails(response);
},
response => Assert.Equal(200, response.StatusCode),
response =>
{
Assert.Equal(400, response.StatusCode);
Assert.False(response.IsDefaultResponse);
AssertProblemDetails(response);
},
response =>
{
Assert.Equal(404, response.StatusCode);
Assert.False(response.IsDefaultResponse);
AssertProblemDetails(response);
});
}
[Fact]
public async Task ApiBehavior_AddsMultipartFormDataConsumesConstraint_ForActionsWithFormFileParameters()
{
@ -1157,15 +1078,6 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal("multipart/form-data", requestFormat.MediaType);
}
private void AssertProblemDetails(ApiExplorerResponseType response)
{
Assert.Equal("Microsoft.AspNetCore.Mvc.ProblemDetails", response.ResponseType);
Assert.Collection(
GetSortedMediaTypes(response),
mediaType => Assert.Equal("application/problem+json", mediaType),
mediaType => Assert.Equal("application/problem+xml", mediaType));
}
private IEnumerable<string> GetSortedMediaTypes(ApiExplorerResponseType apiResponseType)
{
return apiResponseType.ResponseFormats
@ -1238,4 +1150,4 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
public string FormatterType { get; set; }
}
}
}
}

View File

@ -46,7 +46,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
var data = await response.Content.ReadAsStringAsync();
Assert.Contains("An error occured while deserializing input data.", data);
Assert.Contains("An error occurred while deserializing input data.", data);
}
[Fact]
@ -112,4 +112,4 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
}
}
}
}
}

View File

@ -52,7 +52,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
var data = await response.Content.ReadAsStringAsync();
Assert.Contains("An error occured while deserializing input data.", data);
Assert.Contains("An error occurred while deserializing input data.", data);
}
}
}
}

View File

@ -447,7 +447,6 @@ namespace Microsoft.AspNetCore.Mvc
new Type[]
{
typeof(DefaultApiDescriptionProvider),
typeof(ApiBehaviorApiDescriptionProvider),
typeof(JsonPatchOperationsArrayProvider),
}
},

View File

@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.DependencyInjection;
namespace BasicWebSite
@ -20,18 +21,18 @@ namespace BasicWebSite
public void OnProvidersExecuting(ErrorDescriptionContext context)
{
if (context.ActionDescriptor.FilterDescriptors.Any(f => f.Filter is VndErrorAttribute) &&
context.Result is ValidationProblemDetails problemDetails)
context.Result is ModelStateDictionary dictionary)
{
var vndErrors = new List<VndError>();
foreach (var item in problemDetails.Errors)
foreach (var item in dictionary)
{
foreach (var message in item.Value)
foreach (var modelError in item.Value.Errors)
{
vndErrors.Add(new VndError
{
LogRef = problemDetails.Title,
LogRef = modelError.ErrorMessage,
Path = item.Key,
Message = message,
Message = modelError.ErrorMessage,
});
}
}
@ -40,4 +41,4 @@ namespace BasicWebSite
}
}
}
}
}