Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ _TeamCity*
*.coveragexml

# NCrunch
_NCrunch_*
*NCrunch*
.*crunch*.local.xml
nCrunchTemp_*

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Text;
using FunctionMonkey.Infrastructure;
using Xunit;

namespace FunctionMonkey.Testing.Tests.Infrastructure
{
public class NullableTestObject
{
public int A { get; set; }
public int? B { get; set; }
}

public class HttpPropertyExtractorIsNullableShould
{
[Theory]
[InlineData(typeof(int?), typeof(int))]
[InlineData(typeof(float?), typeof(float))]
[InlineData(typeof(bool?), typeof(bool))]
public void IdentifyAsNullable(Type t, Type nullT)
{
Assert.Equal(nullT, Nullable.GetUnderlyingType(t));
Assert.True(HttpParameterExtractor.IsNullableType(t));
}

[Theory]
[InlineData(typeof(int))]
[InlineData(typeof(float))]
[InlineData(typeof(bool))]
[InlineData(typeof(string))]
[InlineData(typeof(object))]
[InlineData(typeof(NullableTestObject))]
public void IdentifyAsNotNullable(Type t)
{
Assert.Null(Nullable.GetUnderlyingType(t));
Assert.False(HttpParameterExtractor.IsNullableType(t));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using AzureFromTheTrenches.Commanding.Abstractions;
using FunctionMonkey;
using FunctionMonkey.Abstractions.Builders.Model;
using FunctionMonkey.Builders;
using FunctionMonkey.Infrastructure;
using FunctionMonkey.Model;
using Microsoft.Extensions.DependencyInjection;
using Xunit;

namespace FunctionMonkey.Testing.Tests.Infrastructure
{
public class TestCommand : ICommand
{
public int IntRequired { get; set; }
public int? IntOptional { get; set; }
}

public class HttpPropertyExtractorWithQueryParamsShould
{
private readonly HttpParameterExtractor _parameterExtractor;

public HttpPropertyExtractorWithQueryParamsShould()
{
var definitions = new List<AbstractFunctionDefinition>();
var routeConfiguration = new HttpRouteConfiguration();
ConnectionStringSettingNames connStringSettingsNames = null;

var functionBuilder = new HttpFunctionBuilder(connStringSettingsNames, routeConfiguration, definitions)
.HttpFunction<TestCommand>("/testcommand");

var httpFunctionDefinition = definitions.First() as HttpFunctionDefinition;
httpFunctionDefinition.RouteParameters = new HttpParameter[0];

_parameterExtractor = new HttpParameterExtractor(httpFunctionDefinition);
}

[Fact]
public void ExtractPossibleQueryParametersAsOptional()
{

var httpParams = _parameterExtractor.ExtractPossibleQueryParameters();

var param = httpParams.First(x => x.Name == "IntRequired");
Assert.Equal(typeof(int), param.Type);
Assert.Equal("System.Int32", param.TypeName);
Assert.False(param.IsOptional);
Assert.False(param.IsNullable);
Assert.False(param.IsNullableType);
Assert.False(param.IsString);
}

[Fact]
public void ExtractPossibleQueryParametersAsOptional2()
{

var httpParams = _parameterExtractor.ExtractPossibleQueryParameters();

var param = httpParams.First(x => x.Name == "IntOptional");
Assert.Equal(typeof(int?), param.Type);
Assert.Equal("System.Nullable<System.Int32>", param.TypeName);
Assert.True(param.IsOptional);
Assert.True(param.IsNullable);
Assert.True(param.IsNullableType);
Assert.False(param.IsString);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using AzureFromTheTrenches.Commanding.Abstractions;
using FunctionMonkey;
using FunctionMonkey.Abstractions.Builders.Model;
using FunctionMonkey.Builders;
using FunctionMonkey.Infrastructure;
using FunctionMonkey.Model;
using Microsoft.Extensions.DependencyInjection;
using Xunit;

namespace FunctionMonkey.Testing.Tests.Infrastructure
{
public class RouteParamTestCommand : ICommand
{
public int A { get; set; }
public int? B { get; set; }
public int? C { get; set; }
}

public class HttpPropertyExtractorWithRouteParamsShould
{
private readonly HttpParameterExtractor _parameterExtractor;

public HttpPropertyExtractorWithRouteParamsShould()
{
var definitions = new List<AbstractFunctionDefinition>();
var routeConfiguration = new HttpRouteConfiguration();
ConnectionStringSettingNames connStringSettingsNames = null;

var functionBuilder = new HttpFunctionBuilder(connStringSettingsNames, routeConfiguration, definitions)
.HttpFunction<RouteParamTestCommand>("/testcommand/{A}/{B}/{C?}");

var httpFunctionDefinition = definitions.First() as HttpFunctionDefinition;

_parameterExtractor = new HttpParameterExtractor(httpFunctionDefinition);
}

[Fact]
public void ExtractPossibleRouteParametersAsNotOptional()
{

var httpParams = _parameterExtractor.ExtractRouteParameters();

var param = httpParams.First(x => x.Name == "A");
Assert.Equal(typeof(int), param.Type);
Assert.Equal("System.Int32", param.TypeName);
Assert.False(param.IsOptional);
Assert.False(param.IsNullable);
Assert.False(param.IsNullableType);
Assert.False(param.IsString);
}

[Fact]
public void ExtractPossibleRouteParametersAsNotOptional2()
{

var httpParams = _parameterExtractor.ExtractRouteParameters();

var param = httpParams.First(x => x.Name == "B");
Assert.Equal(typeof(int?), param.Type);
Assert.Equal("System.Nullable<System.Int32>", param.TypeName);
Assert.False(param.IsOptional);
Assert.True(param.IsNullable);
Assert.True(param.IsNullableType);
Assert.False(param.IsString);
}

[Fact]
public void ExtractPossibleRouteParametersAsOptional()
{

var httpParams = _parameterExtractor.ExtractRouteParameters();

var param = httpParams.First(x => x.Name == "C");
Assert.Equal(typeof(int?), param.Type);
Assert.Equal("System.Nullable<System.Int32>", param.TypeName);
Assert.True(param.IsOptional);
Assert.True(param.IsNullable);
Assert.True(param.IsNullableType);
Assert.False(param.IsString);
}
}
}
6 changes: 3 additions & 3 deletions Source/FunctionMonkey/Builders/HttpFunctionBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

namespace FunctionMonkey.Builders
{
class HttpFunctionBuilder : IHttpFunctionBuilder
public class HttpFunctionBuilder : IHttpFunctionBuilder
{
private static readonly HttpMethod DefaultMethod = HttpMethod.Get;

Expand All @@ -22,8 +22,8 @@ public HttpFunctionBuilder(
List<AbstractFunctionDefinition> definitions)
{
_connectionStringSettingNames = connectionStringSettingNames;
_routeConfiguration = routeConfiguration;
_definitions = definitions;
_routeConfiguration = routeConfiguration ?? throw new System.ArgumentNullException(nameof(routeConfiguration));
_definitions = definitions ?? throw new System.ArgumentNullException(nameof(definitions));
}

public IHttpFunctionConfigurationBuilder<TCommand> HttpFunction<TCommand>() where TCommand : ICommand
Expand Down
164 changes: 164 additions & 0 deletions Source/FunctionMonkey/Infrastructure/HttpParameterExtractor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using AzureFromTheTrenches.Commanding.Abstractions;
using FunctionMonkey.Abstractions.Extensions;
using FunctionMonkey.Model;
using Microsoft.AspNetCore.Http;

namespace FunctionMonkey.Infrastructure
{
public class HttpParameterExtractor
{
private readonly HttpFunctionDefinition _httpFunctionDefinition;

public HttpParameterExtractor(HttpFunctionDefinition httpFunctionDefinition)
{
this._httpFunctionDefinition = httpFunctionDefinition ?? throw new ArgumentNullException(nameof(httpFunctionDefinition));
}

public HttpParameter[] ExtractPossibleQueryParameters()
{
//Debug.Assert(_httpFunctionDefinition.RouteParameters != null);

var properties = _httpFunctionDefinition
.CommandType
.GetProperties(BindingFlags.Instance | BindingFlags.Public);

properties = properties
.Where(x => x.GetCustomAttribute<SecurityPropertyAttribute>() == null
&& x.SetMethod != null
&& (x.PropertyType == typeof(string)
|| x.PropertyType.GetMethods(BindingFlags.Public | BindingFlags.Static).Any(y => y.Name == "TryParse")
|| x.PropertyType.IsEnum
|| HttpParameterExtractor.IsNullableType(x.PropertyType))
&& _httpFunctionDefinition.RouteParameters.All(y => y.Name != x.Name) // we can't be a query parameter and a route parameter
).ToArray();

return properties.Select(x => CreateHttpParameter(x)).ToArray();
}

private HttpParameter CreateHttpParameter(PropertyInfo x, bool? optional = null)
{
var isOptional = optional ?? !x.PropertyType.IsValueType || HttpParameterExtractor.IsNullableType(x.PropertyType);

return new HttpParameter
{
Name = x.Name,
Type = x.PropertyType,
TypeName = x.PropertyType.EvaluateType(),
IsOptional = isOptional,
IsNullableType = HttpParameterExtractor.IsNullableType(x.PropertyType),
};
}

public HttpParameter[] ExtractPossibleFormParameters()
{
return _httpFunctionDefinition
.CommandType
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(x => x.GetCustomAttribute<SecurityPropertyAttribute>() == null
&& x.SetMethod != null
&& (x.PropertyType == typeof(IFormCollection)))
.Select(x => CreateHttpParameter(x))
.ToArray();
}

public HttpParameter[] ExtractRouteParameters()
{
List<HttpParameter> routeParameters = new List<HttpParameter>();
if (_httpFunctionDefinition.Route == null)
{
_httpFunctionDefinition.RouteParameters = routeParameters;
return routeParameters.ToArray();
}

PropertyInfo[] candidateCommandProperties = _httpFunctionDefinition
.CommandType
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(x => x.GetCustomAttribute<SecurityPropertyAttribute>() == null
&& x.SetMethod != null
&& (x.PropertyType == typeof(string)
|| HttpParameterExtractor.IsNullableType(x.PropertyType)
|| x.PropertyType.GetMethods(BindingFlags.Public | BindingFlags.Static).Any(y => y.Name == "TryParse"))).ToArray();
Regex regex = new Regex("{(.*?)}");
MatchCollection matches = regex.Matches(_httpFunctionDefinition.Route);
foreach (Match match in matches) //you can loop through your matches like this
{
string routeParameter = match.Groups[1].Value;
bool isOptional = routeParameter.EndsWith("?");
string[] routeParameterParts = routeParameter.Split(':');
if (routeParameterParts.Length == 0)
{
throw new ConfigurationException($"Bad route parameter in route {_httpFunctionDefinition.Route} for command type {_httpFunctionDefinition.CommandType.FullName}");
}

string routeParameterName = routeParameterParts[0].TrimEnd('?');
PropertyInfo[] candidateProperties = candidateCommandProperties
.Where(p => p.Name.ToLower() == routeParameterName.ToLower()).ToArray();
PropertyInfo matchedProperty = null;
if (candidateProperties.Length == 1)
{
matchedProperty = candidateProperties[0];
}
else if (candidateProperties.Length > 1)
{
matchedProperty = candidateProperties.SingleOrDefault(x => x.Name == routeParameterName);
}

if (matchedProperty == null)
{
throw new ConfigurationException($"Unable to match route parameter {routeParameterName} to property on command type {_httpFunctionDefinition.CommandType}");
}

bool isPropertyNullable = !matchedProperty.PropertyType.IsValueType ||
HttpParameterExtractor.IsNullableType(matchedProperty.PropertyType);

string routeTypeName;
if (isOptional && matchedProperty.PropertyType.IsValueType &&
HttpParameterExtractor.IsNullableType(matchedProperty.PropertyType))
{
routeTypeName = $"{matchedProperty.PropertyType.EvaluateType()}?";
}
else
{
routeTypeName = matchedProperty.PropertyType.EvaluateType();
}

var param = CreateHttpParameter(matchedProperty, isOptional);
param.RouteName = routeParameterName;
param.RouteTypeName = routeTypeName;
routeParameters.Add(param);
}

return routeParameters.ToArray();

/*string lowerCaseRoute = httpFunctionDefinition1.Route.ToLower();
httpFunctionDefinition1.RouteParameters = httpFunctionDefinition1
.CommandType
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(x => x.GetCustomAttribute<SecurityPropertyAttribute>() == null
&& x.SetMethod != null
&& (x.PropertyType == typeof(string) || x.PropertyType
.GetMethods(BindingFlags.Public | BindingFlags.Static).Any(y => y.Name == "TryParse"))
&& lowerCaseRoute.Contains("{" + x.Name.ToLower() + "}"))
.Select(x => new HttpParameter
{
Name = x.Name,
TypeName = x.PropertyType.EvaluateType(),
Type = x.PropertyType
})
.ToArray();*/
}

public static bool IsNullableType(Type t)
{
return Nullable.GetUnderlyingType(t) != null;
//return (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Nullable<>));
}
}
}
Loading