Skip to content

feat: add AOT path parameter support#2209

Draft
TimothyMakkison wants to merge 37 commits into
reactiveui:mainfrom
TimothyMakkison:aot_route
Draft

feat: add AOT path parameter support#2209
TimothyMakkison wants to merge 37 commits into
reactiveui:mainfrom
TimothyMakkison:aot_route

Conversation

@TimothyMakkison

Copy link
Copy Markdown
Collaborator

Follow on from #2182

Hey, here's my most recent take on adding path parameter support, don't know which one you prefer. I reckon I could modify Main to support object parameters and **param types, and possibly query params if needed, this may have some issues, I can elaborate as needed. I haven't bothered to rebase this PR onto main yet as I'm not sure if it's still wanted or what issues need to be resolved.

I'd be curious to see what the performance difference is between this and the current approach, my benchmarks suggest this uses a little less memory but might be slower due to dictionary lookups, plus I don't trust my laptop to do microbenchmarks during a heatwave. The advantage of adding each parameter individually is that we can prevent boxing of value types and potentially use ISpanFormattable when DefaultUrlFormatter is used for allocation free writes, ie an integer parameter with 2 digits will likely save 24+32 bytes with this approach, not a massive saving.

ValueStringBuilder

How to handle the use of ValueStringBuilder

Emit

Emit ValueStringBuilder as a inner class of Generated, would also have to emit AddPathParameter and AddPathObjectProperty as they need access to it. This would make testing function that rely on the emitted ValueStringBuilder very tricky.

Expose

Make ValueStringBuilder public, this means that people who use Refit will have access to it. Not a great idea IMO

Inner class

Make ValueStringBuilder an inner class of Generated, this way all emitted code can still reference it. We would have to make GeneratedRequestRunner an inner class of Generated so it can see ValueStringBuilder, this way users can't access ValueStringBuilder but we can still write tests.

Reflection

My approach to reflection is continued from #1965 where I've tried to make ICustomAttributeProvider identical to the runtime generated version. As I imagine some people internally casted the types back into ParameterInfo
If my understanding of AOT is correct, my current approach should safely access all reflection data, even if generics are used.
I did experiment with a version which didn't accept generic arrays and just used the generic argument count, but wouldn't close the method to correct generic type.

I could use #2174 approach, as you seem to be okay with this. This will have an edge case where it will break in the rare cases where a parameter has a generic attribute, however this can be fixed.

// This will break as the generated code will be outside the scope of the method and not know what T is.
Task Get<T>(MyAttribute<T> string input);

Failing tests

How to handle the failing query tests, most of the tests are failing because they expect query values alongside object path parameters.

  • I could disable the tests and then re add them when query support is added
  • Alternatively I could disable object property parameter support and add it back later
  • Write logic to determine when a query parameter may be expected and fall back to reflection driven code.

Generated code:

private static readonly global::System.Type[] ______typeParameters = new global::System.Type[] { typeof(int) };
/// <inheritdoc />
public global::System.Threading.Tasks.Task<global::System.Net.Http.HttpResponseMessage> DynamicRouteAsync(int @id)
{
    var refitSettings = _settings;
    global::System.Span<char> span = stackalloc char[256];
    var valueStringBuilder = new global::Refit.ValueStringBuilder(span);
    valueStringBuilder.Append("/users/");
    global::Refit.GeneratedRequestRunner.AddPathParameter<global::Refit.Benchmarks.IPerformanceService, int>(ref valueStringBuilder, @id, refitSettings, nameof(@id), ______typeParameters);
    var refitRequest = new global::System.Net.Http.HttpRequestMessage(global::System.Net.Http.HttpMethod.Get, global::Refit.GeneratedRequestRunner.BuildRelativeUri(this.Client, valueStringBuilder.ToString(), refitSettings.UrlResolution));
    #if NET6_0_OR_GREATER
    refitRequest.Version = refitSettings.Version;
    refitRequest.VersionPolicy = refitSettings.VersionPolicy;
    #endif
    global::Refit.GeneratedRequestRunner.AddConfiguredRequestOptions(refitRequest, refitSettings, typeof(global::Refit.Benchmarks.IPerformanceService));
    return global::Refit.GeneratedRequestRunner.SendAsync<global::System.Net.Http.HttpResponseMessage, global::System.Net.Http.HttpResponseMessage>(
        this.Client,
        refitRequest,
        refitSettings,
        false,
        false,
        false,
        global::System.Threading.CancellationToken.None);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant