Adding Support for consumes.

Consumes has overriding behavior and the one closest to action wins.
This commit is contained in:
Harsh Gupta 2015-01-09 09:32:47 -08:00
parent dfb02e58f8
commit 60fa4a6f45
20 changed files with 949 additions and 1 deletions

17
Mvc.sln
View File

@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
VisualStudioVersion = 14.0.22416.0
VisualStudioVersion = 14.0.22303.1
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}"
EndProject
@ -116,6 +116,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "LoggingWebSite", "test\WebS
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ErrorPageMiddlewareWebSite", "test\WebSites\ErrorPageMiddlewareWebSite\ErrorPageMiddlewareWebSite.kproj", "{AD545A5B-2BA5-4314-88AC-FC2ACF2CC718}"
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ActionConstraintsWebSite", "test\WebSites\ActionConstraintsWebSite\ActionConstraintsWebSite.kproj", "{AF210F69-9D31-43AF-AC3A-CD366E252218}"
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "CustomRouteWebSite", "test\WebSites\CustomRouteWebSite\CustomRouteWebSite.kproj", "{364EC3C6-C9DB-45E0-A0F2-1EE61E4B429B}"
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ResponseCacheWebSite", "test\WebSites\ResponseCacheWebSite\ResponseCacheWebSite.kproj", "{BDEEBE09-C0C4-433C-B0B8-8478C9776996}"
@ -644,6 +646,18 @@ Global
{AD545A5B-2BA5-4314-88AC-FC2ACF2CC718}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{AD545A5B-2BA5-4314-88AC-FC2ACF2CC718}.Release|x86.ActiveCfg = Release|Any CPU
{AD545A5B-2BA5-4314-88AC-FC2ACF2CC718}.Release|x86.Build.0 = Release|Any CPU
{AF210F69-9D31-43AF-AC3A-CD366E252218}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AF210F69-9D31-43AF-AC3A-CD366E252218}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AF210F69-9D31-43AF-AC3A-CD366E252218}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{AF210F69-9D31-43AF-AC3A-CD366E252218}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{AF210F69-9D31-43AF-AC3A-CD366E252218}.Debug|x86.ActiveCfg = Debug|Any CPU
{AF210F69-9D31-43AF-AC3A-CD366E252218}.Debug|x86.Build.0 = Debug|Any CPU
{AF210F69-9D31-43AF-AC3A-CD366E252218}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AF210F69-9D31-43AF-AC3A-CD366E252218}.Release|Any CPU.Build.0 = Release|Any CPU
{AF210F69-9D31-43AF-AC3A-CD366E252218}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{AF210F69-9D31-43AF-AC3A-CD366E252218}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{AF210F69-9D31-43AF-AC3A-CD366E252218}.Release|x86.ActiveCfg = Release|Any CPU
{AF210F69-9D31-43AF-AC3A-CD366E252218}.Release|x86.Build.0 = Release|Any CPU
{364EC3C6-C9DB-45E0-A0F2-1EE61E4B429B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{364EC3C6-C9DB-45E0-A0F2-1EE61E4B429B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{364EC3C6-C9DB-45E0-A0F2-1EE61E4B429B}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
@ -724,6 +738,7 @@ Global
{0A6BB4C0-48D3-4E7F-952B-B8917345E075} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{0AD78AB5-D67C-49BC-81B1-0C51BFA82B5E} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{AD545A5B-2BA5-4314-88AC-FC2ACF2CC718} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{AF210F69-9D31-43AF-AC3A-CD366E252218} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{364EC3C6-C9DB-45E0-A0F2-1EE61E4B429B} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{BDEEBE09-C0C4-433C-B0B8-8478C9776996} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
EndGlobalSection

View File

@ -0,0 +1,160 @@
// Copyright (c) Microsoft Open Technologies, Inc. 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.Net.Http.Headers;
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// Specifies the allowed content types which can be used to select the action based on request's content-type.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class ConsumesAttribute : Attribute, IResourceFilter, IConsumesActionConstraint
{
public static readonly int ConsumesActionConstraintOrder = 200;
/// <summary>
/// Creates a new instance of <see cref="ConsumesAttribute"/>.
/// </summary>
public ConsumesAttribute([NotNull] string contentType, params string[] otherContentTypes)
{
ContentTypes = GetContentTypes(contentType, otherContentTypes);
}
// The value used is a non default value so that it avoids getting mixed with other action constraints
// with default order.
/// <inheritdoc />
int IActionConstraint.Order { get; } = ConsumesActionConstraintOrder;
/// <inheritdoc />
public IList<MediaTypeHeaderValue> ContentTypes { get; set; }
/// <inheritdoc />
public void OnResourceExecuting([NotNull] ResourceExecutingContext context)
{
// Only execute if the current filter is the one which is closest to the action.
// Ignore all other filters. This is to ensure we have a overriding behavior.
if (IsApplicable(context.ActionDescriptor))
{
MediaTypeHeaderValue requestContentType = null;
MediaTypeHeaderValue.TryParse(context.HttpContext.Request.ContentType, out requestContentType);
// Only execute if this is the last filter before calling the action.
// This ensures that we only run the filter which is closest to the action.
if (requestContentType != null &&
!ContentTypes.Any(contentType => contentType.IsSubsetOf(requestContentType)))
{
context.Result = new UnsupportedMediaTypeResult();
}
}
}
/// <inheritdoc />
public void OnResourceExecuted([NotNull] ResourceExecutedContext context)
{
}
public bool Accept(ActionConstraintContext context)
{
// If this constraint is not closest to the action, it will be skipped.
if (!IsApplicable(context.CurrentCandidate.Action))
{
// Since the constraint is to be skipped, returning true here
// will let the current candidate ignore this constraint and will
// be selected based on other constraints for this action.
return true;
}
MediaTypeHeaderValue requestContentType = null;
MediaTypeHeaderValue.TryParse(context.RouteContext.HttpContext.Request.ContentType, out requestContentType);
// If the request content type is null we need to act like pass through.
// In case there is a single candidate with a constraint it should be selected.
// If there are multiple actions with consumes action constraints this should result in ambiguous exception
// unless there is another action without a consumes constraint.
if (requestContentType == null)
{
var isActionWithoutConsumeConstraintPresent = context.Candidates.Any(
candidate => candidate.Constraints == null ||
!candidate.Constraints.Any(constraint => constraint is IConsumesActionConstraint));
return !isActionWithoutConsumeConstraintPresent;
}
if (ContentTypes.Any(c => c.IsSubsetOf(requestContentType)))
{
return true;
}
var firstCandidate = context.Candidates[0];
if (firstCandidate != context.CurrentCandidate)
{
// If the current candidate is not same as the first candidate,
// we need not probe other candidates to see if they apply.
// Only the first candidate is allowed to probe other candidates and based on the result select itself.
return false;
}
// Run the matching logic for all IConsumesActionConstraints we can find, and see what matches.
// 1). If we have a unique best match, then only that constraint should return true.
// 2). If we have multiple matches, then all constraints that match will return true
// , resulting in ambiguity(maybe).
// 3). If we have no matches, then we choose the first constraint to return true.It will later return a 415
foreach (var candidate in context.Candidates)
{
if (candidate == firstCandidate)
{
continue;
}
var tempContext = new ActionConstraintContext()
{
Candidates = context.Candidates,
RouteContext = context.RouteContext,
CurrentCandidate = candidate
};
if (candidate.Constraints == null || candidate.Constraints.Count() == 0 ||
candidate.Constraints.Any(constraint => constraint is IConsumesActionConstraint &&
constraint.Accept(tempContext)))
{
// There is someone later in the chain which can handle the request.
// end the process here.
return false;
}
}
// There is no one later in the chain that can handle this content type return a false positive so that
// later we can detect and return a 415.
return true;
}
private bool IsApplicable(ActionDescriptor actionDescriptor)
{
// If there are multiple IConsumeActionConstraints which are defined at the class and
// at the action level, the one closest to the action overrides the others. To ensure this
// we take advantage of the fact that ConsumesAttribute is both an IActionFilter and an
// IConsumeActionConstraint. Since filterdescriptor collection is ordered (the last filter is the one
// closest to the action), we apply this constraint only if there is no IConsumeActionConstraint after this.
return actionDescriptor.FilterDescriptors.Last(
filter => filter.Filter is IConsumesActionConstraint).Filter == this;
}
private List<MediaTypeHeaderValue> GetContentTypes(string firstArg, string[] args)
{
var contentTypes = new List<MediaTypeHeaderValue>();
contentTypes.Add(MediaTypeHeaderValue.Parse(firstArg));
foreach (var item in args)
{
var contentType = MediaTypeHeaderValue.Parse(item);
contentTypes.Add(contentType);
}
return contentTypes;
}
}
}

View File

@ -0,0 +1,13 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// An <see cref="IActionConstraint"/> constraint that identifies a type which can be used to select an action
/// based on incoming request.
/// </summary>
public interface IConsumesActionConstraint : IActionConstraint
{
}
}

View File

@ -0,0 +1,19 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// A <see cref="HttpStatusCodeResult"/> that when
/// executed will produce a UnsupportedMediaType (415) response.
/// </summary>
public class UnsupportedMediaTypeResult : HttpStatusCodeResult
{
/// <summary>
/// Creates a new instance of <see cref="UnsupportedMediaTypeResult"/>.
/// </summary>
public UnsupportedMediaTypeResult() : base(415)
{
}
}
}

View File

@ -0,0 +1,347 @@
// Copyright (c) Microsoft Open Technologies, Inc. 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.AspNet.Http.Core;
using Microsoft.AspNet.Routing;
using Moq;
using Xunit;
namespace Microsoft.AspNet.Mvc
{
public class ConsumesAttributeTests
{
[Theory]
[InlineData("application")]
[InlineData("")]
[InlineData(null)]
public void Constructor_ForInvalidContentType_Throws(string contentType)
{
// Arrange
var expectedMessage = string.Format("Invalid value '{0}'.", contentType ?? "<null>");
// Act & Assert
var exception = Assert.Throws<FormatException>(() => new ConsumesAttribute(contentType));
Assert.Equal(expectedMessage, exception.Message);
}
[Theory]
[InlineData("application/json")]
[InlineData("application/json;Parameter1=12")]
[InlineData("text/xml")]
public void Accept_MatchesForMachingRequestContentType(string contentType)
{
// Arrange
var constraint = new ConsumesAttribute("application/json", "text/xml");
var action = new ActionDescriptor()
{
FilterDescriptors =
new List<FilterDescriptor>() { new FilterDescriptor(constraint, FilterScope.Action) }
};
var context = new ActionConstraintContext();
context.Candidates = new List<ActionSelectorCandidate>()
{
new ActionSelectorCandidate(action, new [] { constraint }),
};
context.CurrentCandidate = context.Candidates[0];
context.RouteContext = CreateRouteContext(contentType: contentType);
// Act & Assert
Assert.True(constraint.Accept(context));
}
[Fact]
public void Accept_TheFirstCandidateReturnsFalse_IfALaterOneMatches()
{
// Arrange
var constraint1 = new ConsumesAttribute("application/json", "text/xml");
var action1 = new ActionDescriptor()
{
FilterDescriptors =
new List<FilterDescriptor>() { new FilterDescriptor(constraint1, FilterScope.Action) }
};
var constraint2 = new Mock<ITestConsumeConstraint>();
var action2 = new ActionDescriptor()
{
FilterDescriptors =
new List<FilterDescriptor>() { new FilterDescriptor(constraint2.Object, FilterScope.Action) }
};
constraint2.Setup(o => o.Accept(It.IsAny<ActionConstraintContext>()))
.Returns(true);
var context = new ActionConstraintContext();
context.Candidates = new List<ActionSelectorCandidate>()
{
new ActionSelectorCandidate(action1, new [] { constraint1 }),
new ActionSelectorCandidate(action2, new [] { constraint2.Object }),
};
context.CurrentCandidate = context.Candidates[0];
context.RouteContext = CreateRouteContext(contentType: "application/custom");
// Act & Assert
Assert.False(constraint1.Accept(context));
}
[Theory]
[InlineData("application/custom")]
[InlineData("")]
[InlineData(null)]
public void Accept_ForNoMatchingCandidates_SelectsTheFirstCandidate(string contentType)
{
// Arrange
var constraint1 = new ConsumesAttribute("application/json", "text/xml");
var action1 = new ActionDescriptor()
{
FilterDescriptors =
new List<FilterDescriptor>() { new FilterDescriptor(constraint1, FilterScope.Action) }
};
var constraint2 = new Mock<ITestConsumeConstraint>();
var action2 = new ActionDescriptor()
{
FilterDescriptors =
new List<FilterDescriptor>() { new FilterDescriptor(constraint2.Object, FilterScope.Action) }
};
constraint2.Setup(o => o.Accept(It.IsAny<ActionConstraintContext>()))
.Returns(false);
var context = new ActionConstraintContext();
context.Candidates = new List<ActionSelectorCandidate>()
{
new ActionSelectorCandidate(action1, new [] { constraint1 }),
new ActionSelectorCandidate(action2, new [] { constraint2.Object }),
};
context.CurrentCandidate = context.Candidates[0];
context.RouteContext = CreateRouteContext(contentType: contentType);
// Act & Assert
Assert.True(constraint1.Accept(context));
}
[Theory]
[InlineData("")]
[InlineData(null)]
public void Accept_ForNoRequestType_SelectsTheCandidateWithoutConstraintIfPresent(string contentType)
{
// Arrange
var constraint1 = new ConsumesAttribute("application/json");
var actionWithConstraint = new ActionDescriptor()
{
FilterDescriptors =
new List<FilterDescriptor>() { new FilterDescriptor(constraint1, FilterScope.Action) }
};
var constraint2 = new ConsumesAttribute("text/xml");
var actionWithConstraint2 = new ActionDescriptor()
{
FilterDescriptors =
new List<FilterDescriptor>() { new FilterDescriptor(constraint2, FilterScope.Action) }
};
var actionWithoutConstraint = new ActionDescriptor();
var context = new ActionConstraintContext();
context.Candidates = new List<ActionSelectorCandidate>()
{
new ActionSelectorCandidate(actionWithConstraint, new [] { constraint1 }),
new ActionSelectorCandidate(actionWithConstraint2, new [] { constraint2 }),
new ActionSelectorCandidate(actionWithoutConstraint, new List<IActionConstraint>()),
};
context.RouteContext = CreateRouteContext(contentType: contentType);
// Act & Assert
context.CurrentCandidate = context.Candidates[0];
Assert.False(constraint1.Accept(context));
context.CurrentCandidate = context.Candidates[1];
Assert.False(constraint2.Accept(context));
}
[Theory]
[InlineData("application/xml")]
[InlineData("application/custom")]
[InlineData("invalid/invalid")]
public void Accept_UnrecognizedMediaType_SelectsTheCandidateWithoutConstraintIfPresent(string contentType)
{
// Arrange
var actionWithoutConstraint = new ActionDescriptor();
var constraint1 = new ConsumesAttribute("application/json");
var actionWithConstraint = new ActionDescriptor()
{
FilterDescriptors =
new List<FilterDescriptor>() { new FilterDescriptor(constraint1, FilterScope.Action) }
};
var constraint2 = new ConsumesAttribute("text/xml");
var actionWithConstraint2 = new ActionDescriptor()
{
FilterDescriptors =
new List<FilterDescriptor>() { new FilterDescriptor(constraint2, FilterScope.Action) }
};
var context = new ActionConstraintContext();
context.Candidates = new List<ActionSelectorCandidate>()
{
new ActionSelectorCandidate(actionWithConstraint, new [] { constraint1 }),
new ActionSelectorCandidate(actionWithConstraint2, new [] { constraint2 }),
new ActionSelectorCandidate(actionWithoutConstraint, new List<IActionConstraint>()),
};
context.RouteContext = CreateRouteContext(contentType: contentType);
// Act & Assert
context.CurrentCandidate = context.Candidates[0];
Assert.False(constraint1.Accept(context));
context.CurrentCandidate = context.Candidates[1];
Assert.False(constraint2.Accept(context));
}
[Theory]
[InlineData("")]
[InlineData(null)]
public void Accept_ForNoRequestType_ReturnsTrueForAllConstraints(string contentType)
{
// Arrange
var constraint1 = new ConsumesAttribute("application/json");
var actionWithConstraint = new ActionDescriptor()
{
FilterDescriptors =
new List<FilterDescriptor>() { new FilterDescriptor(constraint1, FilterScope.Action) }
};
var constraint2 = new ConsumesAttribute("text/xml");
var actionWithConstraint2 = new ActionDescriptor()
{
FilterDescriptors =
new List<FilterDescriptor>() { new FilterDescriptor(constraint2, FilterScope.Action) }
};
var actionWithoutConstraint = new ActionDescriptor();
var context = new ActionConstraintContext();
context.Candidates = new List<ActionSelectorCandidate>()
{
new ActionSelectorCandidate(actionWithConstraint, new [] { constraint1 }),
new ActionSelectorCandidate(actionWithConstraint2, new [] { constraint2 }),
};
context.RouteContext = CreateRouteContext(contentType: contentType);
// Act & Assert
context.CurrentCandidate = context.Candidates[0];
Assert.True(constraint1.Accept(context));
context.CurrentCandidate = context.Candidates[1];
Assert.True(constraint2.Accept(context));
}
[Theory]
[InlineData("application/xml")]
[InlineData("application/custom")]
public void OnResourceExecuting_ForNoContentTypeMatch_SetsUnsupportedMediaTypeResult(string contentType)
{
// Arrange
var httpContext = new DefaultHttpContext();
httpContext.Request.ContentType = contentType;
var consumesFilter = new ConsumesAttribute("application/json");
var actionWithConstraint = new ActionDescriptor()
{
ActionConstraints = new List<IActionConstraintMetadata>() { consumesFilter },
FilterDescriptors =
new List<FilterDescriptor>() { new FilterDescriptor(consumesFilter, FilterScope.Action) }
};
var actionContext = new ActionContext(httpContext, new RouteData(), actionWithConstraint);
var resourceExecutingContext = new ResourceExecutingContext(actionContext, new[] { consumesFilter });
// Act
consumesFilter.OnResourceExecuting(resourceExecutingContext);
// Assert
Assert.NotNull(resourceExecutingContext.Result);
Assert.IsType<UnsupportedMediaTypeResult>(resourceExecutingContext.Result);
}
[Theory]
[InlineData("")]
[InlineData(null)]
public void OnResourceExecuting_NullOrEmptyRequestContentType_IsNoOp(string contentType)
{
// Arrange
var httpContext = new DefaultHttpContext();
httpContext.Request.ContentType = contentType;
var consumesFilter = new ConsumesAttribute("application/json");
var actionWithConstraint = new ActionDescriptor()
{
ActionConstraints = new List<IActionConstraintMetadata>() { consumesFilter },
FilterDescriptors =
new List<FilterDescriptor>() { new FilterDescriptor(consumesFilter, FilterScope.Action) }
};
var actionContext = new ActionContext(httpContext, new RouteData(), actionWithConstraint);
var resourceExecutingContext = new ResourceExecutingContext(actionContext, new[] { consumesFilter });
// Act
consumesFilter.OnResourceExecuting(resourceExecutingContext);
// Assert
Assert.Null(resourceExecutingContext.Result);
}
[Theory]
[InlineData("application/xml")]
[InlineData("application/json")]
public void OnResourceExecuting_ForAContentTypeMatch_IsNoOp(string contentType)
{
// Arrange
var httpContext = new DefaultHttpContext();
httpContext.Request.ContentType = contentType;
var consumesFilter = new ConsumesAttribute("application/json", "application/xml");
var actionWithConstraint = new ActionDescriptor()
{
ActionConstraints = new List<IActionConstraintMetadata>() { consumesFilter },
FilterDescriptors =
new List<FilterDescriptor>() { new FilterDescriptor(consumesFilter, FilterScope.Action) }
};
var actionContext = new ActionContext(httpContext, new RouteData(), actionWithConstraint);
var resourceExecutingContext = new ResourceExecutingContext(actionContext, new[] { consumesFilter });
// Act
consumesFilter.OnResourceExecuting(resourceExecutingContext);
// Assert
Assert.Null(resourceExecutingContext.Result);
}
private static RouteContext CreateRouteContext(string contentType = null, object routeValues = null)
{
var httpContext = new DefaultHttpContext();
if (contentType != null)
{
httpContext.Request.ContentType = contentType;
}
var routeContext = new RouteContext(httpContext);
routeContext.RouteData = new RouteData();
foreach (var kvp in new RouteValueDictionary(routeValues))
{
routeContext.RouteData.Values.Add(kvp.Key, kvp.Value);
}
return routeContext;
}
public interface ITestConsumeConstraint : IConsumesActionConstraint, IResourceFilter
{
}
}
}

View File

@ -0,0 +1,161 @@
// Copyright (c) Microsoft Open Technologies, Inc. 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.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Serialization;
using ActionConstraintsWebSite;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.TestHost;
using Newtonsoft.Json;
using Xunit;
namespace Microsoft.AspNet.Mvc.FunctionalTests
{
public class ConsumesAttributeTests
{
private readonly IServiceProvider _provider = TestHelper.CreateServices("ActionConstraintsWebSite");
private readonly Action<IApplicationBuilder> _app = new Startup().Configure;
[Fact]
public async Task NoRequestContentType_SelectsActionWithoutConstraint()
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
var request = new HttpRequestMessage(
HttpMethod.Post,
"http://localhost/ConsumesAttribute_Company/CreateProduct");
// Act
var response = await client.SendAsync(request);
var product = JsonConvert.DeserializeObject<Product>(
await response.Content.ReadAsStringAsync());
// Assert
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
Assert.Null(product);
}
[Fact]
public async Task NoRequestContentType_Throws_IfMultipleActionsWithConstraints()
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
var request = new HttpRequestMessage(
HttpMethod.Post,
"http://localhost/ConsumesAttribute_AmbiguousActions/CreateProduct");
// Act
var response = await client.SendAsync(request);
var exception = response.GetServerException();
// Assert
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
Assert.Equal(typeof(AmbiguousActionException).FullName, exception.ExceptionType);
Assert.Equal(
"Multiple actions matched. The following actions matched route data and had all constraints "+
"satisfied:____ActionConstraintsWebSite.ConsumesAttribute_NoFallBackActionController."+
"CreateProduct__ActionConstraintsWebSite.ConsumesAttribute_NoFallBackActionController.CreateProduct",
exception.ExceptionMessage);
}
[Fact]
public async Task NoRequestContentType_Selects_IfASingleActionWithConstraintIsPresent()
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
var request = new HttpRequestMessage(
HttpMethod.Post,
"http://localhost/ConsumesAttribute_PassThrough/CreateProduct");
// Act
var response = await client.SendAsync(request);
var product = JsonConvert.DeserializeObject<Product>(
await response.Content.ReadAsStringAsync());
// Assert
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
Assert.Null(product);
}
[Theory]
[InlineData("application/json")]
[InlineData("text/json")]
public async Task Selects_Action_BasedOnRequestContentType(string requestContentType)
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
var input = "{SampleString:\""+requestContentType+"\"}";
var request = new HttpRequestMessage(
HttpMethod.Post,
"http://localhost/ConsumesAttribute_AmbiguousActions/CreateProduct");
request.Content = new StringContent(input, Encoding.UTF8, requestContentType);
// Act
var response = await client.SendAsync(request);
var product = JsonConvert.DeserializeObject<Product>(
await response.Content.ReadAsStringAsync());
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(requestContentType, product.SampleString);
}
[Theory]
[InlineData("application/json")]
[InlineData("text/json")]
public async Task ActionLevelAttribute_OveridesClassLevel(string requestContentType)
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
var input = "{SampleString:\"" + requestContentType + "\"}";
var request = new HttpRequestMessage(
HttpMethod.Post,
"http://localhost/ConsumesAttribute_OverridesBase/CreateProduct");
request.Content = new StringContent(input, Encoding.UTF8, requestContentType);
var expectedString = "ConsumesAttribute_OverridesBaseController_" + requestContentType;
// Act
var response = await client.SendAsync(request);
var product = JsonConvert.DeserializeObject<Product>(
await response.Content.ReadAsStringAsync());
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(expectedString, product.SampleString);
}
[Fact]
public async Task DerivedClassLevelAttribute_OveridesBaseClassLevel()
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
var input = "<Product xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\" " +
"xmlns=\"http://schemas.datacontract.org/2004/07/ActionConstraintsWebSite\">" +
"<SampleString>application/xml</SampleString></Product>";
var request = new HttpRequestMessage(
HttpMethod.Post,
"http://localhost/ConsumesAttribute_Overrides/CreateProduct");
request.Content = new StringContent(input, Encoding.UTF8, "application/xml");
var expectedString = "ConsumesAttribute_OverridesController_application/xml";
// Act
var response = await client.SendAsync(request);
var responseString = await response.Content.ReadAsStringAsync();
var product = JsonConvert.DeserializeObject<Product>(responseString);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(expectedString, product.SampleString);
}
}
}

View File

@ -3,6 +3,7 @@
"warningsAsErrors": "true"
},
"dependencies": {
"ActionConstraintsWebSite": "1.0.0",
"ActionResultsWebSite": "1.0.0",
"ActivatorWebSite": "1.0.0",
"AddServicesWebSite": "1.0.0",

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="__ToolsVersion__" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>af210f69-9d31-43af-ac3a-cd366e252218</ProjectGuid>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x86'" Label="Configuration">
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x86'" Label="Configuration">
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
<DevelopmentServerPort>41642</DevelopmentServerPort>
</PropertyGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@ -0,0 +1,23 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNet.Mvc;
namespace ActionConstraintsWebSite
{
[Route("ConsumesAttribute_AmbiguousActions/[action]")]
public class ConsumesAttribute_NoFallBackActionController : Controller
{
[Consumes("application/json", "text/json")]
public Product CreateProduct([FromBody] Product_Json jsonInput)
{
return jsonInput;
}
[Consumes("application/xml")]
public Product CreateProduct([FromBody] Product_Xml xmlInput)
{
return xmlInput;
}
}
}

View File

@ -0,0 +1,26 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNet.Mvc;
namespace ActionConstraintsWebSite
{
[Consumes("application/json")]
public class ConsumesAttribute_OverridesBaseController : Controller
{
[Consumes("text/json")]
public Product CreateProduct([FromBody] Product_Json product)
{
// should be picked if request content type is application/xml and not application/json.
product.SampleString = "ConsumesAttribute_OverridesBaseController_text/json";
return product;
}
public virtual IActionResult CreateProduct([FromBody] Product product)
{
// should be picked if request content type is application/json.
product.SampleString = "ConsumesAttribute_OverridesBaseController_application/json";
return new ObjectResult(product);
}
}
}

View File

@ -0,0 +1,18 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNet.Mvc;
namespace ActionConstraintsWebSite
{
[Consumes("application/xml")]
public class ConsumesAttribute_OverridesController : ConsumesAttribute_OverridesBaseController
{
public override IActionResult CreateProduct([FromBody] Product product)
{
// should be picked if request content type is text/json.
product.SampleString = "ConsumesAttribute_OverridesController_application/xml";
return new JsonResult(product);
}
}
}

View File

@ -0,0 +1,18 @@
// Copyright (c) Microsoft Open Technologies, Inc. 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.AspNet.Mvc;
namespace ActionConstraintsWebSite
{
[Route("ConsumesAttribute_PassThrough/[action]")]
public class ConsumesAttribute_PassThroughController : Controller
{
[Consumes("application/json")]
public Product CreateProduct([FromBody] Product_Json jsonInput)
{
return jsonInput;
}
}
}

View File

@ -0,0 +1,29 @@
// Copyright (c) Microsoft Open Technologies, Inc. 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.AspNet.Mvc;
namespace ActionConstraintsWebSite
{
[Route("ConsumesAttribute_Company/[action]")]
public class ConsumesAttribute_WithFallbackActionController : Controller
{
[Consumes("application/json")]
public Product CreateProduct([FromBody] Product_Json jsonInput)
{
return jsonInput;
}
[Consumes("application/xml")]
public Product CreateProduct([FromBody] Product_Xml xmlInput)
{
return xmlInput;
}
public Product CreateProduct([FromBody] Product_Text defaultInput)
{
return defaultInput;
}
}
}

View File

@ -0,0 +1,16 @@
// Copyright (c) Microsoft Open Technologies, Inc. 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 ActionConstraintsWebSite
{
public class Product
{
[Range(10, 100)]
public int SampleInt { get; set; }
[MinLength(15)]
public string SampleString { get; set; }
}
}

View File

@ -0,0 +1,9 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace ActionConstraintsWebSite
{
public class Product_Json : Product
{
}
}

View File

@ -0,0 +1,9 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace ActionConstraintsWebSite
{
public class Product_Xml : Product
{
}
}

View File

@ -0,0 +1,9 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace ActionConstraintsWebSite
{
public class Product_Text : Product
{
}
}

View File

@ -0,0 +1,36 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Routing;
using Microsoft.Framework.DependencyInjection;
namespace ActionConstraintsWebSite
{
public class Startup
{
public void Configure(IApplicationBuilder app)
{
var configuration = app.GetTestConfiguration();
app.UseServices(services =>
{
services.AddMvc(configuration);
services.Configure<MvcOptions>(options =>
{
options.AddXmlDataContractSerializerFormatter();
});
});
app.UseErrorReporter();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller}/{action}/{id?}");
});
}
}
}

View File

@ -0,0 +1,19 @@
{
"commands": {
"web": "Microsoft.AspNet.Hosting server=Microsoft.AspNet.Server.WebListener server.urls=http://localhost:5001",
"kestrel": "Microsoft.AspNet.Hosting --server Kestrel --server.urls http://localhost:5000"
},
"dependencies": {
"Kestrel": "1.0.0-*",
"Microsoft.AspNet.Mvc": "6.0.0-*",
"Microsoft.AspNet.Mvc.TestConfiguration": "1.0.0",
"Microsoft.AspNet.Server.IIS": "1.0.0-*",
"Microsoft.AspNet.Server.WebListener": "1.0.0-*",
"Microsoft.AspNet.StaticFiles": "1.0.0-*"
},
"frameworks": {
"aspnet50": { },
"aspnetcore50": { }
},
"webroot": "wwwroot"
}