Skip to content

Instantly share code, notes, and snippets.

@Danielku15
Last active October 11, 2018 11:43
Show Gist options
  • Save Danielku15/4b2f12a45dbe69c9c61eeca91e98daa1 to your computer and use it in GitHub Desktop.
Save Danielku15/4b2f12a45dbe69c9c61eeca91e98daa1 to your computer and use it in GitHub Desktop.
Reproduction for aspnet/mvc #8578
//<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