Introduce opinionated API defaults.
* Introduce ProblemDescriptionAttribute to enhance some 4xx messages and produce better API description. * Introduce IErrorDescriptionProvider to modify the shape of error response. Fixes #6785, Fixes #6786
This commit is contained in:
parent
037c1ec47d
commit
776c2604f8
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||
|
|
@ -9,6 +10,7 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
|||
/// <summary>
|
||||
/// Represents an API exposed by this application.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{ActionDescriptor.DisplayName,nq}")]
|
||||
public class ApiDescription
|
||||
{
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
services.TryAddSingleton<IApiDescriptionGroupCollectionProvider, ApiDescriptionGroupCollectionProvider>();
|
||||
services.TryAddEnumerable(
|
||||
ServiceDescriptor.Transient<IApiDescriptionProvider, DefaultApiDescriptionProvider>());
|
||||
services.TryAddEnumerable(
|
||||
ServiceDescriptor.Transient<IApiDescriptionProvider, ProblemDetailsApiDescriptionProvider>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
// 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.ModelBinding;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||
{
|
||||
public class ProblemDetailsApiDescriptionProvider : IApiDescriptionProvider
|
||||
{
|
||||
private readonly IModelMetadataProvider _modelMetadaProvider;
|
||||
|
||||
public ProblemDetailsApiDescriptionProvider(IModelMetadataProvider modelMetadataProvider)
|
||||
{
|
||||
_modelMetadaProvider = modelMetadataProvider;
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The order is set to execute after the <see cref="DefaultApiDescriptionProvider"/>.
|
||||
/// </remarks>
|
||||
public int Order => -1000 + 10;
|
||||
|
||||
public void OnProvidersExecuted(ApiDescriptionProviderContext context)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnProvidersExecuting(ApiDescriptionProviderContext context)
|
||||
{
|
||||
foreach (var apiDescription in context.Results)
|
||||
{
|
||||
if (!apiDescription.ActionDescriptor.FilterDescriptors.Any(f => f.Filter is ProblemDetailsAttribute))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var parameters = apiDescription.ActionDescriptor.Parameters.Concat(apiDescription.ActionDescriptor.BoundProperties);
|
||||
if (parameters.Any())
|
||||
{
|
||||
apiDescription.SupportedResponseTypes.Add(CreateProblemResponse(StatusCodes.Status400BadRequest));
|
||||
|
||||
if (parameters.Any(p => p.Name.EndsWith("id", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
apiDescription.SupportedResponseTypes.Add(CreateProblemResponse(StatusCodes.Status404NotFound));
|
||||
}
|
||||
}
|
||||
|
||||
// We don't have a good way to signal a "default" response type. We'll use 0 to indicate this until we come up
|
||||
// with something better.
|
||||
apiDescription.SupportedResponseTypes.Add(CreateProblemResponse(statusCode: 0));
|
||||
}
|
||||
}
|
||||
|
||||
private ApiResponseType CreateProblemResponse(int statusCode)
|
||||
{
|
||||
return new ApiResponseType
|
||||
{
|
||||
ApiResponseFormats = new List<ApiResponseFormat>
|
||||
{
|
||||
new ApiResponseFormat
|
||||
{
|
||||
MediaType = "application/problem+json",
|
||||
},
|
||||
new ApiResponseFormat
|
||||
{
|
||||
MediaType = "application/problem+xml",
|
||||
},
|
||||
},
|
||||
ModelMetadata = _modelMetadaProvider.GetMetadataForType(typeof(ProblemDetails)),
|
||||
StatusCode = statusCode,
|
||||
Type = typeof(ProblemDetails),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1503,7 +1503,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// </summary>
|
||||
/// <returns>The created <see cref="BadRequestObjectResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual BadRequestObjectResult ValidationProblem(ValidationProblemDescription descriptor)
|
||||
public virtual ActionResult ValidationProblem(ValidationProblemDetails descriptor)
|
||||
{
|
||||
if (descriptor == null)
|
||||
{
|
||||
|
|
@ -1518,14 +1518,14 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// </summary>
|
||||
/// <returns>The created <see cref="BadRequestObjectResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual BadRequestObjectResult ValidationProblem(ModelStateDictionary modelStateDictionary)
|
||||
public virtual ActionResult ValidationProblem(ModelStateDictionary modelStateDictionary)
|
||||
{
|
||||
if (modelStateDictionary == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(modelStateDictionary));
|
||||
}
|
||||
|
||||
var validationProblem = new ValidationProblemDescription(modelStateDictionary);
|
||||
var validationProblem = new ValidationProblemDetails(modelStateDictionary);
|
||||
return new BadRequestObjectResult(validationProblem);
|
||||
}
|
||||
|
||||
|
|
@ -1535,9 +1535,9 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// </summary>
|
||||
/// <returns>The created <see cref="BadRequestObjectResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual BadRequestObjectResult ValidationProblem()
|
||||
public virtual ActionResult ValidationProblem()
|
||||
{
|
||||
var validationProblem = new ValidationProblemDescription(ModelState);
|
||||
var validationProblem = new ValidationProblemDetails(ModelState);
|
||||
return new BadRequestObjectResult(validationProblem);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -204,6 +204,9 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
services.TryAddTransient<RequestSizeLimitFilter>();
|
||||
services.TryAddTransient<DisableRequestSizeLimitFilter>();
|
||||
|
||||
// Error description
|
||||
services.TryAddSingleton<IErrorDescriptionFactory, DefaultErrorDescriptorFactory>();
|
||||
|
||||
//
|
||||
// ModelBinding, Validation
|
||||
//
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
// 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 Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||
{
|
||||
public class ErrorDescriptionContext
|
||||
{
|
||||
public ErrorDescriptionContext(ActionDescriptor actionDescriptor)
|
||||
{
|
||||
ActionDescriptor = actionDescriptor;
|
||||
}
|
||||
|
||||
public ActionDescriptor ActionDescriptor { get; }
|
||||
|
||||
public object Result { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +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 Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines a contract for creating or modifying an error response.
|
||||
/// </summary>
|
||||
public interface IErrorDescriptionFactory
|
||||
{
|
||||
object CreateErrorDescription(ActionDescriptor actionDescriptor, object result);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||
{
|
||||
public interface IErrorDescriptorProvider
|
||||
{
|
||||
int Order { get; }
|
||||
|
||||
void OnProvidersExecuting(ErrorDescriptionContext context);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Internal
|
||||
{
|
||||
public class DefaultErrorDescriptorFactory : IErrorDescriptionFactory
|
||||
{
|
||||
private readonly IErrorDescriptorProvider[] _providers;
|
||||
|
||||
public DefaultErrorDescriptorFactory(IEnumerable<IErrorDescriptorProvider> providers)
|
||||
{
|
||||
if (providers == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(providers));
|
||||
}
|
||||
|
||||
_providers = providers.OrderBy(p => p.Order).ToArray();
|
||||
}
|
||||
|
||||
public object CreateErrorDescription(ActionDescriptor actionDescriptor, object result)
|
||||
{
|
||||
if (actionDescriptor == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(actionDescriptor));
|
||||
}
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(result));
|
||||
}
|
||||
|
||||
var context = new ErrorDescriptionContext(actionDescriptor)
|
||||
{
|
||||
Result = result,
|
||||
};
|
||||
|
||||
for (var i = 0; i < _providers.Length; i++)
|
||||
{
|
||||
_providers[i].OnProvidersExecuting(context);
|
||||
}
|
||||
|
||||
return context.Result ?? result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// <summary>
|
||||
/// A machine-readable format for specifying errors in HTTP API responses based on https://tools.ietf.org/html/rfc7807.
|
||||
/// </summary>
|
||||
public class ProblemDescription
|
||||
public class ProblemDetails
|
||||
{
|
||||
/// <summary>
|
||||
/// A URI reference [RFC3986] that identifies the problem type. This specification encourages that, when
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
// 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 Microsoft.AspNetCore.Mvc
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds an <see cref="IFilterMetadata"/> that indicates to the framework that the current action conforms to well-known API behavior.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
|
||||
public class ProblemDetailsAttribute : Attribute, IFilterMetadata
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
|
||||
Example:
|
||||
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
|
|
@ -26,36 +26,36 @@
|
|||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
|
|
|
|||
|
|
@ -9,19 +9,19 @@ using Microsoft.AspNetCore.Mvc.ModelBinding;
|
|||
namespace Microsoft.AspNetCore.Mvc
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="ProblemDescription"/> for validation errors.
|
||||
/// A <see cref="ProblemDetails"/> for validation errors.
|
||||
/// </summary>
|
||||
public class ValidationProblemDescription : ProblemDescription
|
||||
public class ValidationProblemDetails : ProblemDetails
|
||||
{
|
||||
/// <summary>
|
||||
/// Intializes a new instance of <see cref="ValidationProblemDescription"/>.
|
||||
/// Intializes a new instance of <see cref="ValidationProblemDetails"/>.
|
||||
/// </summary>
|
||||
public ValidationProblemDescription()
|
||||
public ValidationProblemDetails()
|
||||
{
|
||||
Title = Resources.ValidationProblemDescription_Title;
|
||||
}
|
||||
|
||||
public ValidationProblemDescription(ModelStateDictionary modelState)
|
||||
public ValidationProblemDetails(ModelStateDictionary modelState)
|
||||
: this()
|
||||
{
|
||||
if (modelState == null)
|
||||
|
|
@ -62,7 +62,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the validation errors associated with this instance of <see cref="ValidationProblemDescription"/>.
|
||||
/// Gets or sets the validation errors associated with this instance of <see cref="ValidationProblemDetails"/>.
|
||||
/// </summary>
|
||||
public IDictionary<string, string[]> Errors { get; } = new Dictionary<string, string[]>(StringComparer.Ordinal);
|
||||
}
|
||||
|
|
@ -6,13 +6,13 @@ using Xunit;
|
|||
|
||||
namespace Microsoft.AspNetCore.Mvc
|
||||
{
|
||||
public class ValidationProblemDescriptionTest
|
||||
public class ValidationProblemDetailsTest
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_SetsTitle()
|
||||
{
|
||||
// Arrange & Act
|
||||
var problemDescription = new ValidationProblemDescription();
|
||||
var problemDescription = new ValidationProblemDetails();
|
||||
|
||||
// Assert
|
||||
Assert.Equal("One or more validation errors occured.", problemDescription.Title);
|
||||
|
|
@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
modelStateDictionary.AddModelError("key3", "error3");
|
||||
|
||||
// Act
|
||||
var problemDescription = new ValidationProblemDescription(modelStateDictionary);
|
||||
var problemDescription = new ValidationProblemDetails(modelStateDictionary);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("One or more validation errors occured.", problemDescription.Title);
|
||||
|
|
@ -1065,6 +1065,88 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
Assert.Equal("ApiExplorerInboundOutbound/SuppressedForLinkGeneration", description.RelativePath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProblemDetails_AddsProblemAsDefaultErrorResult()
|
||||
{
|
||||
// Act
|
||||
var body = await Client.GetStringAsync("ApiExplorerProblemDetails/ActionWithoutParameters");
|
||||
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
|
||||
|
||||
// Assert
|
||||
var description = Assert.Single(result);
|
||||
Assert.Collection(
|
||||
description.SupportedResponseTypes,
|
||||
response =>
|
||||
{
|
||||
Assert.Equal(0, response.StatusCode);
|
||||
AssertProblemDetails(response);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProblemDetails_AddsProblemAsErrorResultForBadResult_WhenActionHasParameters()
|
||||
{
|
||||
// Act
|
||||
var body = await Client.GetStringAsync("ApiExplorerProblemDetails/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);
|
||||
AssertProblemDetails(response);
|
||||
},
|
||||
response => Assert.Equal(200, response.StatusCode),
|
||||
response =>
|
||||
{
|
||||
Assert.Equal(400, response.StatusCode);
|
||||
AssertProblemDetails(response);
|
||||
});
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("ApiExplorerProblemDetails/ActionWithIdParameter")]
|
||||
[InlineData("ApiExplorerProblemDetails/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);
|
||||
AssertProblemDetails(response);
|
||||
},
|
||||
response => Assert.Equal(200, response.StatusCode),
|
||||
response =>
|
||||
{
|
||||
Assert.Equal(400, response.StatusCode);
|
||||
AssertProblemDetails(response);
|
||||
},
|
||||
response =>
|
||||
{
|
||||
Assert.Equal(404, response.StatusCode);
|
||||
AssertProblemDetails(response);
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ using Microsoft.AspNetCore.Mvc.Razor;
|
|||
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
|
||||
using Microsoft.AspNetCore.Mvc.Razor.Internal;
|
||||
using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages.Internal;
|
||||
using Microsoft.AspNetCore.Mvc.TagHelpers;
|
||||
|
|
@ -418,6 +417,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
new Type[]
|
||||
{
|
||||
typeof(DefaultApiDescriptionProvider),
|
||||
typeof(ProblemDetailsApiDescriptionProvider),
|
||||
typeof(JsonPatchOperationsArrayProvider),
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
// 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 Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ApiExplorerWebSite
|
||||
{
|
||||
[Route("ApiExplorerProblemDetails/[action]")]
|
||||
[ProblemDetails]
|
||||
public class ApiExplorerProblemDetailsController : Controller
|
||||
{
|
||||
public IActionResult ActionWithoutParameters() => Ok();
|
||||
|
||||
public void ActionWithSomeParameters(object input)
|
||||
{
|
||||
}
|
||||
|
||||
public void ActionWithIdParameter(int id, string name)
|
||||
{
|
||||
}
|
||||
|
||||
public void ActionWithIdSuffixParameter(int personId, string personName)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue