Last active
October 11, 2018 11:43
-
-
Save Danielku15/4b2f12a45dbe69c9c61eeca91e98daa1 to your computer and use it in GitHub Desktop.
Reproduction for aspnet/mvc #8578
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//<Project Sdk="Microsoft.NET.Sdk"> | |
// <PropertyGroup> | |
// <TargetFramework>netcoreapp2.1</TargetFramework> | |
// <IsPackable>false</IsPackable> | |
// </PropertyGroup> | |
// <ItemGroup> | |
// <PackageReference Include="Microsoft.AspNetCore" Version="2.1.4" /> | |
// <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.1.3" /> | |
// <PackageReference Include="Microsoft.AspNetCore.TestHost" Version="2.1.1" /> | |
// <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" /> | |
// <PackageReference Include="MSTest.TestAdapter" Version="1.3.2" /> | |
// <PackageReference Include="MSTest.TestFramework" Version="1.3.2" /> | |
// </ItemGroup> | |
//</Project> | |
using System; | |
using System.Collections.Generic; | |
using System.Globalization; | |
using System.Linq; | |
using System.Net.Http; | |
using System.Reflection; | |
using System.Text.RegularExpressions; | |
using System.Threading.Tasks; | |
using Microsoft.AspNetCore; | |
using Microsoft.AspNetCore.Builder; | |
using Microsoft.AspNetCore.Hosting; | |
using Microsoft.AspNetCore.Http; | |
using Microsoft.AspNetCore.Http.Extensions; | |
using Microsoft.AspNetCore.Mvc; | |
using Microsoft.AspNetCore.Mvc.Abstractions; | |
using Microsoft.AspNetCore.Mvc.ApplicationParts; | |
using Microsoft.AspNetCore.Mvc.Controllers; | |
using Microsoft.AspNetCore.Mvc.Infrastructure; | |
using Microsoft.AspNetCore.Mvc.Internal; | |
using Microsoft.AspNetCore.Routing; | |
using Microsoft.AspNetCore.TestHost; | |
using Microsoft.Extensions.DependencyInjection; | |
using Microsoft.Extensions.Logging; | |
using Microsoft.VisualStudio.TestTools.UnitTesting; | |
namespace DateTimeOffsetBug | |
{ | |
[TestClass] | |
public class DateTimeOffsetTest | |
{ | |
[TestMethod] | |
public async Task FunctionsWorksOnDateTime() | |
{ | |
// Arrange | |
const string Expected = "2012-12-22T01:02:03.0000000+00:00"; | |
const string Uri = "http://localhost/DateTimeOffset/ToString(2012-12-22T01:02:03Z)"; | |
HttpClient client = GetClient(); | |
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, Uri); | |
// Act | |
HttpResponseMessage response = await client.SendAsync(request); | |
// Assert | |
Assert.IsTrue(response.IsSuccessStatusCode); | |
Assert.AreEqual(Expected, await response.Content.ReadAsStringAsync()); | |
} | |
private static HttpClient GetClient() | |
{ | |
var controllers = new[] { typeof(DateTimeOffsetController) }; | |
IWebHostBuilder builder = WebHost.CreateDefaultBuilder(); | |
builder.ConfigureServices(services => | |
{ | |
services.AddMvc(); | |
services.AddSingleton<IActionSelector, DateTimeFunctionActionSelector>(); | |
services.Configure<RouteOptions>(options => | |
{ | |
options.ConstraintMap.Add(DateTimeFunctionRouteConstraint.DateTimeFunctionNameKey, typeof(DateTimeFunctionRouteConstraint)); | |
}); | |
}); | |
builder.Configure(app => | |
{ | |
app.UseMvc(routeBuilder => | |
{ | |
var applicationPartManager = routeBuilder.ApplicationBuilder.ApplicationServices.GetRequiredService<ApplicationPartManager>(); | |
applicationPartManager.ApplicationParts.Clear(); | |
applicationPartManager.ApplicationParts.Add(new AssemblyPart(new MockAssembly(controllers))); | |
routeBuilder.MapRoute( | |
name: "function", | |
template: "{controller}/{f:" + DateTimeFunctionRouteConstraint.DateTimeFunctionNameKey + "}"); | |
}); | |
}); | |
var server = new TestServer(builder); | |
return server.CreateClient(); | |
} | |
} | |
/// <summary> | |
/// A special route constraint which reads reads some values from the URL and then adds it as routing value. | |
/// Format is: FunctionName({value}) where {value} must be a DateTimeOffset | |
/// </summary> | |
public class DateTimeFunctionRouteConstraint : IRouteConstraint | |
{ | |
public const string DateTimeFunctionNameKey = "DateTimeFunction"; | |
public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, | |
RouteDirection routeDirection) | |
{ | |
if (routeDirection == RouteDirection.IncomingRequest) | |
{ | |
var (functionName, value) = ParseUri(httpContext.Request); | |
if (!string.IsNullOrEmpty(functionName)) | |
{ | |
values.Add(DateTimeFunctionNameKey, functionName); | |
if (value != null) | |
{ | |
values.Add("value", value); | |
} | |
return true; | |
} | |
return false; | |
} | |
return true; | |
} | |
private static readonly Regex FunctionPattern = | |
new Regex(@"(?<FunctionName>[a-z0-9]+)\((?<FunctionValue>[^\)]+)\)", RegexOptions.Compiled | RegexOptions.IgnoreCase); | |
private (string functionName, object value) ParseUri(HttpRequest request) | |
{ | |
var requestUri = new Uri(request.GetEncodedUrl()); | |
var path = requestUri.GetLeftPart(UriPartial.Path); | |
if (string.IsNullOrEmpty(path)) return (null, null); | |
var match = FunctionPattern.Match(path); | |
if (!match.Success) return (null, null); | |
if (DateTimeOffset.TryParse(match.Groups["FunctionValue"].Value, out var dto)) | |
{ | |
return (match.Groups["FunctionName"].Value, dto); | |
} | |
return (null, null); | |
} | |
} | |
/// <summary> | |
/// A special action selector which already will fill some route values as deserialized objects | |
/// </summary> | |
public class DateTimeFunctionActionSelector : IActionSelector | |
{ | |
private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider; | |
private readonly ActionSelector _innerSelector; | |
public DateTimeFunctionActionSelector( | |
IActionDescriptorCollectionProvider actionDescriptorCollectionProvider, | |
ActionConstraintCache actionConstraintProviders, | |
ILoggerFactory loggerFactory) | |
{ | |
_actionDescriptorCollectionProvider = actionDescriptorCollectionProvider; | |
_innerSelector = new ActionSelector(actionDescriptorCollectionProvider, actionConstraintProviders, loggerFactory); | |
} | |
public IReadOnlyList<ActionDescriptor> SelectCandidates(RouteContext context) | |
{ | |
//HttpRequest request = routeContext.HttpContext.Request; | |
if (context.RouteData.Values.TryGetValue("Controller", out var controllerValue) && | |
context.RouteData.Values.TryGetValue(DateTimeFunctionRouteConstraint.DateTimeFunctionNameKey, out var functionPathValue)) | |
{ | |
var functionName = functionPathValue.ToString(); | |
var controllerName = controllerValue.ToString(); | |
var actionDescriptors = _actionDescriptorCollectionProvider.ActionDescriptors.Items.OfType<ControllerActionDescriptor>(); | |
if (!string.IsNullOrEmpty(functionName)) | |
{ | |
return actionDescriptors.Where(c => | |
string.Equals(c.ControllerName, controllerName, StringComparison.OrdinalIgnoreCase) | |
&& string.Equals(functionName, functionName, StringComparison.OrdinalIgnoreCase)).ToList(); | |
} | |
} | |
return _innerSelector.SelectCandidates(context); | |
} | |
public ActionDescriptor SelectBestCandidate(RouteContext context, IReadOnlyList<ActionDescriptor> candidates) | |
{ | |
return candidates.First(); | |
} | |
} | |
/// <summary> | |
/// A test controller with a test ToString function | |
/// </summary> | |
public class DateTimeOffsetController : ControllerBase | |
{ | |
public IActionResult ToString(DateTimeOffset value) | |
{ | |
return Ok(value.ToString("O")); | |
} | |
} | |
/// <summary> | |
/// A test assembly for the API test host | |
/// </summary> | |
public sealed class MockAssembly : Assembly | |
{ | |
private readonly Type[] _types; | |
public MockAssembly(params Type[] types) | |
{ | |
_types = types; | |
} | |
public override Type[] GetTypes() | |
{ | |
return _types; | |
} | |
public override IEnumerable<TypeInfo> DefinedTypes | |
{ | |
get { return _types.AsEnumerable().Select(a => a.GetTypeInfo()); } | |
} | |
public override string Location => null; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment