Skip to content

Commit

Permalink
Allow simple types to be read from body (#445)
Browse files Browse the repository at this point in the history
  • Loading branch information
Kaliumhexacyanoferrat authored Jan 17, 2024
1 parent 485b65c commit 6975c4d
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 29 deletions.
21 changes: 21 additions & 0 deletions Modules/Reflection/FromBodyAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System;

namespace GenHTTP.Modules.Reflection
{

/// <summary>
/// Marking an argument of a service method with this attribute will
/// cause the parameter to be read from the body of the request.
/// </summary>
/// <remarks>
/// This attribute can be used on all parameters with simple types
/// (such as string or int). Complex types will always be deserialized
/// from the body without the need of marking it explicitly.
/// </remarks>
[AttributeUsage(AttributeTargets.Parameter)]
public sealed class FromBodyAttribute : Attribute
{

}

}
73 changes: 48 additions & 25 deletions Modules/Reflection/MethodHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Runtime.ExceptionServices;
using System.Text.RegularExpressions;
Expand Down Expand Up @@ -130,43 +131,65 @@ public MethodHandler(IHandler parent, MethodInfo method, MethodRouting routing,
if (injector.Supports(par.ParameterType))
{
targetArguments[i] = injector.GetValue(this, request, par.ParameterType);

injected = true;
break;
}
}

if (injected) continue;

if ((par.Name is not null) && par.CheckSimple())
{
// is there a named parameter?
if (sourceParameters is not null)
{
var sourceArgument = sourceParameters.Groups[par.Name];
// should the value be read from the body?
var fromBody = par.GetCustomAttribute<FromBodyAttribute>();

if (sourceArgument.Success)
if (fromBody != null)
{
if (request.Content != null)
{
targetArguments[i] = sourceArgument.Value.ConvertTo(par.ParameterType);
continue;
using var reader = new StreamReader(request.Content, leaveOpen: true);

var body = await reader.ReadToEndAsync();

if (!string.IsNullOrWhiteSpace(body))
{
targetArguments[i] = body.ConvertTo(par.ParameterType);
}

request.Content.Seek(0, SeekOrigin.Begin);
}
}

// is there a query parameter?
if (request.Query.TryGetValue(par.Name, out var queryValue))
else
{
targetArguments[i] = queryValue.ConvertTo(par.ParameterType);
continue;
}
// is there a named parameter?
if (sourceParameters is not null)
{
var sourceArgument = sourceParameters.Groups[par.Name];

// is there a parameter from the body?
if (bodyArguments is not null)
{
if (bodyArguments.TryGetValue(par.Name, out var bodyValue))
if (sourceArgument.Success)
{
targetArguments[i] = sourceArgument.Value.ConvertTo(par.ParameterType);
continue;
}
}

// is there a query parameter?
if (request.Query.TryGetValue(par.Name, out var queryValue))
{
targetArguments[i] = bodyValue.ConvertTo(par.ParameterType);
targetArguments[i] = queryValue.ConvertTo(par.ParameterType);
continue;
}

// is there a parameter from the body?
if (bodyArguments is not null)
{
if (bodyArguments.TryGetValue(par.Name, out var bodyValue))
{
targetArguments[i] = bodyValue.ConvertTo(par.ParameterType);
continue;
}
}
}

// assume the default value
Expand All @@ -191,7 +214,7 @@ public MethodHandler(IHandler parent, MethodInfo method, MethodRouting routing,
{
targetArguments[i] = await deserializer.DeserializeAsync(request.Content, par.ParameterType);
}
catch (Exception e)
catch (Exception e)
{
throw new ProviderException(ResponseStatus.BadRequest, "Failed to deserialize request body", e);
}
Expand Down Expand Up @@ -411,17 +434,17 @@ private bool TryResolvePath(ContentHint input, [MaybeNullWhen(returnValue: false

return task.Result;
}
else if (type == typeof(ValueTask) || type == typeof(Task) )
else if (type == typeof(ValueTask) || type == typeof(Task))
{
dynamic task = result;

await task;

return null;
}

return result;
}
}

#endregion

Expand Down
6 changes: 2 additions & 4 deletions Testing/Acceptance/Modules/Controllers/ActionTests.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using System.Net;
using System.IO;
using System.Net.Http.Headers;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using System.Collections.Generic;

using Microsoft.VisualStudio.TestTools.UnitTesting;

Expand All @@ -12,8 +12,6 @@
using GenHTTP.Modules.Controllers;
using GenHTTP.Modules.IO;
using GenHTTP.Modules.Layouting;
using System.Collections.Generic;
using System.Net.Http.Headers;

namespace GenHTTP.Testing.Acceptance.Modules.Controllers
{
Expand Down
96 changes: 96 additions & 0 deletions Testing/Acceptance/Modules/Reflection/ParameterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using System.IO;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;

using GenHTTP.Modules.Functional;
using GenHTTP.Modules.Reflection;

using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace GenHTTP.Testing.Acceptance.Modules.Reflection
{

[TestClass]
public sealed class ParameterTests
{

#region Tests

[TestMethod]
public async Task TestCanReadSimpleTypesFromBody()
{
var inline = Inline.Create()
.Post(([FromBody] string body1, [FromBody] string body2) => $"{body1}-{body2}");

using var runner = TestHost.Run(inline);

using var response = await PostAsync(runner, "1");

await response.AssertStatusAsync(HttpStatusCode.OK);

Assert.AreEqual("1-1", await response.GetContentAsync());
}

[TestMethod]
public async Task TestCanPassEmptyString()
{
var inline = Inline.Create()
.Post(([FromBody] int number) => number);

using var runner = TestHost.Run(inline);

using var response = await PostAsync(runner, " ");

await response.AssertStatusAsync(HttpStatusCode.OK);

Assert.AreEqual("0", await response.GetContentAsync());
}

[TestMethod]
public async Task TestCanAccessBothBodyAndStream()
{
var inline = Inline.Create()
.Post(([FromBody] int number, Stream body) =>
{
using var reader = new StreamReader(body);
return $"{number} - {reader.ReadToEnd()}";
});

using var runner = TestHost.Run(inline);

using var response = await PostAsync(runner, "1");

await response.AssertStatusAsync(HttpStatusCode.OK);

Assert.AreEqual("1 - 1", await response.GetContentAsync());
}

[TestMethod]
public async Task TestConversionError()
{
var inline = Inline.Create()
.Post(([FromBody] int number) => number);

using var runner = TestHost.Run(inline);

using var response = await PostAsync(runner, "ABC");

await response.AssertStatusAsync(HttpStatusCode.BadRequest);
}

private static Task<HttpResponseMessage> PostAsync(TestHost host, string body)
{
var request = host.GetRequest();

request.Method = HttpMethod.Post;
request.Content = new StringContent(body, null, "text/plain");

return host.GetResponseAsync(request);
}

#endregion

}

}

0 comments on commit 6975c4d

Please sign in to comment.