The HTTP standard is a dynamic document that is regularly reviewed and updated. The specification may be “done” to some developers’ eyes, but there are a ton of new and intriguing HTTP methods that are about to be proposed. The QUERY method, which has semantics identical to a GET method call but allows users to submit a request body, is one such HTTP method. As you might guess, this opens up a world of possibilities for searches that are more complicated than what would be possible with a standard query string.

In this article, we’ll go through how to enable ASP.NET Core to support experimental HTTP methods. We’ll also go through some reasons why you might not want to. Let’s get going.

The HTTP Query Method: What is it?

We should discuss HTTP Query and why others would want to utilize it in their API implementations before moving on to the C# implementation.

Queries are often delivered via the GET or POST method when developers create HTTP APIs. The URL path and the query parameters are included in GET requests. While complicated queries can be transmitted using the GET method, this approach is constrained by the HTTP standard. A URL may only contain a certain amount of data, and items in the query string must be URL encoded. The POST method, which permits more complicated query syntax in the request body, is frequently used by developers when they approach the boundaries of utilizing the GET method. Nevertheless, utilizing POST has disadvantages as well because these calls do not adhere to the same caching restrictions as GET requests and as a result, place greater strain on the host server.

With regard to caching and idempotency semantics, the QUERY method combines the advantages of the GET method with the flexibility for developers to publish a body akin to a publish request. Developers may now send any message to the server, even those using sophisticated query languages like GraphQL, SQL, LINQ, and others, thanks to the inclusion of the body.

A New HTTP Method Should Be Added To Simple API Endpoints

Fortunately, HTTP method detection in ASP.NET Core may be customized. As a result, you don’t need a lot of plumbing code to add any HTTP method. Let’s start by giving a Minimal API endpoint the QUERY function.

You may utilize the MapMethods method on IEndpointConventionBuilder when working with Minimal APIs. Any string value that ASP.NET Core could perceive as an HTTP method can be sent into the method. You may also create a MapQuery extension method to match the existing methods of MapGet, MapPost, etc. for user convenience.

public static class HttpQueryExtensions
{
    public static IEndpointConventionBuilder MapQuery(
        this IEndpointRouteBuilder endpoints,
        string pattern,
        Func<Query, IResult> requestDelegate)
    {
        return endpoints.MapMethods(pattern, new[] { "QUERY" }, requestDelegate);
    }

    public static IEndpointConventionBuilder MapQuery(
        this IEndpointRouteBuilder endpoints,
        string pattern,
        RequestDelegate requestDelegate)
    {
        return endpoints.MapMethods(pattern, new[] { "QUERY" }, requestDelegate);
    }
}

A <FuncQuery,IResult> is present in our extension function, as you would have seen. The purpose of the Query type is to model-bind our request’s body to a string. Examining this class, you may choose to implement the reading of the body in any manner you choose, perhaps including serializing it into a strongly-typed entity. However, doing so is absolutely optional.

public class Query
{
public string? Text { get; set; }
public static async ValueTask<Query> BindAsync(
HttpContext context,
ParameterInfo parameter)
{
string? text = null;
var request = context.Request;
if (!request.Body.CanSeek)
{
// We only do this if the stream isn't *already* seekable,
// as EnableBuffering will create a new stream instance
// each time it's called
request.EnableBuffering();
}
if (request.Body.CanRead)
{
request.Body.Position = 0;
var reader = new StreamReader(request.Body, Encoding.UTF8);
text = await reader.ReadToEndAsync().ConfigureAwait(false);
request.Body.Position = 0;
}
return new Query { Text = text };
}
public static implicit operator string(Query query) // implicit digit to byte conversion operator
{
return query.Text ?? string.Empty; // implicit conversion
}
}

Let’s finally put our objective into practice. I’ll turn the request’s body into a LINQ expression in this illustration. This shouldn’t be done in production since an expression is C# code and might create security flaws. This sample is being used as a demo.

app.MapQuery("/people", query =>
{
try
{
// database
var people = Enumerable.Range(1, 100)
.Select(i => new Person { Index = i, Name = $"Minion #{i}" });
// let's use the Query
var parameter = Expression.Parameter(typeof(IEnumerable<Person>), nameof(people));
var expression = DynamicExpressionParser.ParseLambda(new[] { parameter }, null, query.Text);
var compiled = expression.Compile();
// execute query
var result = compiled.DynamicInvoke(people);
return Results.Ok(new
{
query,
source = "endpoint",
results = result
});
}
catch (Exception e)
{
return Results.BadRequest(e);
}
})
.WithName("endpoint");

Later on, we’ll call this endpoint, but for now, let’s also give ASP.NET Core MVC QUERY support.

Enhance ASP.NET Core MVC With A New HTTP Method

It is considerably simpler to integrate experimental HTTP methods into ASP.NET Core MVC. Only a class inherited from HttpMethodAttribute must be implemented.

public class HttpQueryAttribute : HttpMethodAttribute
{
private static readonly IEnumerable<string> SupportedMethods = new[] { "QUERY" };
/// <summary>
/// Creates a new <see cref="Microsoft.AspNetCore.Mvc.HttpGetAttribute"/>.
/// </summary>
public HttpQueryAttribute()
: base(SupportedMethods)
{
}
/// <summary>
/// Creates a new <see cref="Microsoft.AspNetCore.Mvc.HttpGetAttribute"/> with the given route template.
/// </summary>
/// <param name="template">The route template. May not be null.</param>
public HttpQueryAttribute([StringSyntax("Route")] string template)
: base(SupportedMethods, template)
{
if (template == null)
{
throw new ArgumentNullException(nameof(template));
}
}
}

Once it has been implemented, all that is left to do is add the new attribute to your ASP.NET Core MVC method. Let’s have a look at an action that can now process QUERY queries as the final implementation.

[Route("api/people")]
public class PeopleController : Controller
{
[HttpQuery, Route("", Name = "controller")]
public async Task<IActionResult> Index()
{
try
{
var body = await new StreamReader(Request.Body).ReadToEndAsync();
var query = new Query { Text = body };
// database
var people = Enumerable.Range(1, 100)
.Select(i => new Person { Index = i, Name = $"Minion #{i}" });
// let's use the Query
var parameter = Expression.Parameter(typeof(IEnumerable<Person>), nameof(people));
var expression = DynamicExpressionParser.ParseLambda(new[] { parameter }, null, query.Text);
var compiled = expression.Compile();
// execute query
var result = compiled.DynamicInvoke(people);
return Ok(new
{
query,
source = "controller",
results = result
});
}
catch (Exception e)
{
return BadRequest(e);
}
}
}

Because ASP.NET Core MVC’s model binding can be too complex in this instance, I’ve avoided using it. Please be considerate and email me a link to your implementation if you decide to try to build it yourself. Before fully implementing the QUERY method, you might wish to take into account your query approach.

Great! Now that a Query call from a client can be responded to, we have a Minimal API endpoint and an ASP.NET Core MVC action. Let’s look at the call method for these endpoints.

Calling our QUERY Endpoints and Actions

The following is the first restriction you’ll probably encounter while using experimental HTTP methods: The majority of the tooling you use is unaware of the method’s existence. Unfortunately, you can’t use tools like Postman, Swagger, and similar tools because of the experimental nature. Your ally is the code. Fortunately, using the HttpClient class to call the APIs is rather simple.

Despite the fact that the request might be made by any C# code housed in any code base, I’ll utilize another GET endpoint to access our Query API.

app.MapGet("/", async (HttpContext ctx, LinkGenerator generator, string? q, string? e) =>
{
var client = new HttpClient();
var endpoint = e switch {
"endpoint" => "endpoint",
"controller" => "controller",
_ => "endpoint"
};
var request = new HttpRequestMessage(
new HttpMethod("QUERY"),
generator.GetUriByName(ctx, endpoint)
);
//language=C#
q ??= "people.OrderByDescending(p => p.Index).Take(10)";
request.Content = new StringContent(q, Encoding.UTF8, "text/plain");
var response = client.Send(request);
var result = await response.Content.ReadAsStringAsync();
return Results.Content(result, "application/json");
});

The use of the HttpMethod class is the main lesson to be learned from the code above. Any string value can be entered, and in our case, we want to set the method to QUERY. Additionally, a LINQ expression will be sent to filter our dataset. The remaining code is normal for a HttpClient application.

When our GET endpoint is called, our Query endpoints are also called, and our results are then obtained.

Effects of Experimentation

While considering and trying out experimental HTTP methods in your APIs can be interesting, you should first weigh the downsides.

  1. Because it’s experimental, the specification could change after you put it into practice.
  2. You’ll probably have to code all interactions because your preferred tools won’t likely support your experimental techniques.
  3. Typical ASP.NET Core libraries won’t function as intended. For instance, Swashbuckle (The OpenAPI library) will not materialize your endpoint schema in ASP.NET Core.
  4. I am unaware of any infrastructure-related advantages of caching, load balancers, etc. Even worse, you could cause problems with your infrastructure.

Conclusion

As long as you know what to implement, ASP.NET Core allows you to add any method. This was an enjoyable experiment that, if nothing else, clarified certain endpoint resolution methods.

Thank you for reading. Happy coding!

Leave a comment

Your email address will not be published.