Skip to content

Commit 769e952

Browse files
authored
Add AOT and trimming support for .NET 10+ (#2018)
Introduces AOT compatibility and trimming support by adding new polyfills, attributes, and conditional logic for .NET 10 and higher. Updates project files to mark assemblies as trimmable and set related properties. Enhances RequestBuilder APIs with trimming annotations and DynamicallyAccessedMembers attributes to ensure compatibility with reflection and code trimming. Also updates dependencies for netstandard2.0/net462 and suppresses a new code analysis warning in tests.
1 parent a6a1304 commit 769e952

File tree

10 files changed

+143
-56
lines changed

10 files changed

+143
-56
lines changed

Refit.HttpClientFactory/Refit.HttpClientFactory.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<Description>Refit HTTP Client Factory Extensions</Description>
66
<TargetFrameworks>$(RefitTargets)</TargetFrameworks>
77
<Nullable>enable</Nullable>
8+
<IsTrimmable>true</IsTrimmable>
89
</PropertyGroup>
910

1011
<ItemGroup>

Refit.Newtonsoft.Json/Refit.Newtonsoft.Json.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
<GenerateDocumentationFile Condition=" '$(Configuration)' == 'Release' ">true</GenerateDocumentationFile>
88
<RootNamespace>Refit</RootNamespace>
99
<Nullable>enable</Nullable>
10+
<IsTrimmable>true</IsTrimmable>
1011
</PropertyGroup>
1112

1213
<ItemGroup>

Refit.Tests/API/_snapshots/ApiApprovalTests.Refit.DotNet10_0.verified.txt

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Refit.HttpClientFactory")]
1+
[assembly: System.Runtime.CompilerServices.DisableRuntimeMarshalling]
2+
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Refit.HttpClientFactory")]
23
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Refit.Newtonsoft.Json")]
34
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Refit.Tests")]
45
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Refit.Xml")]
@@ -367,10 +368,14 @@ namespace Refit
367368
}
368369
public static class RequestBuilder
369370
{
371+
[System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Refit uses reflection to analyze interface methods. Ensure referenced interfaces " +
372+
"and DTOs are preserved when trimming.")]
370373
public static Refit.IRequestBuilder ForType(System.Type refitInterfaceType) { }
371-
public static Refit.IRequestBuilder ForType(System.Type refitInterfaceType, Refit.RefitSettings? settings) { }
372-
public static Refit.IRequestBuilder<T> ForType<T>() { }
373-
public static Refit.IRequestBuilder<T> ForType<T>(Refit.RefitSettings? settings) { }
374+
[System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Refit uses reflection to analyze interface methods. Ensure referenced interfaces " +
375+
"and DTOs are preserved when trimming.")]
376+
public static Refit.IRequestBuilder ForType([System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicMethods | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicMethods)] System.Type refitInterfaceType, Refit.RefitSettings? settings) { }
377+
public static Refit.IRequestBuilder<T> ForType<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicMethods | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicMethods)] T>() { }
378+
public static Refit.IRequestBuilder<T> ForType<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicMethods | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicMethods)] T>(Refit.RefitSettings? settings) { }
374379
}
375380
public class RestMethodInfo : System.IEquatable<Refit.RestMethodInfo>
376381
{

Refit.Tests/Refit.Tests.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<Deterministic>false</Deterministic>
88
<!-- Some tests rely on CallerFilePath -->
99
<RestoreAdditionalProjectSources>https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json</RestoreAdditionalProjectSources>
10-
<NoWarn>$(NoWarn);CS1591;CA1819;CA2000;CA2007;CA1056;CA1707;CA1861;xUnit1031</NoWarn>
10+
<NoWarn>$(NoWarn);CS1591;CA1819;CA2000;CA2007;CA1056;CA1707;CA1861;CA1515;xUnit1031</NoWarn>
1111
<MockHttpVersion>6.0.0</MockHttpVersion>
1212
<IsPackable>false</IsPackable>
1313
</PropertyGroup>

Refit.Xml/Refit.Xml.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
<GenerateDocumentationFile Condition=" '$(Configuration)' == 'Release' ">true</GenerateDocumentationFile>
88
<RootNamespace>Refit</RootNamespace>
99
<Nullable>enable</Nullable>
10+
<IsTrimmable>true</IsTrimmable>
1011
</PropertyGroup>
1112

1213
<ItemGroup Condition="'$(TargetFramework)' == 'net462'">

Refit/AotCompatibility.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using System.Runtime.CompilerServices;
2+
using System.Diagnostics.CodeAnalysis;
3+
#if NET7_0_OR_GREATER
4+
using System.Runtime.InteropServices;
5+
#endif
6+
7+
namespace Refit
8+
{
9+
internal static class AotCompatibility
10+
{
11+
// Intentionally left blank to avoid changing public API surface (e.g., assembly-level attributes)
12+
// while keeping a central place for any future AOT-related initializers if needed.
13+
}
14+
}

Refit/Polyfills.Trimming.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#if NETSTANDARD2_0 || NET462
2+
namespace System.Diagnostics.CodeAnalysis
3+
{
4+
[System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Constructor | System.AttributeTargets.Method | System.AttributeTargets.Property | System.AttributeTargets.Event, Inherited = false)]
5+
internal sealed class RequiresUnreferencedCodeAttribute : System.Attribute
6+
{
7+
public RequiresUnreferencedCodeAttribute(string message) => Message = message;
8+
public string Message { get; }
9+
public string? Url { get; set; }
10+
}
11+
12+
[System.AttributeUsage(System.AttributeTargets.Constructor | System.AttributeTargets.Method, AllowMultiple = true)]
13+
internal sealed class DynamicDependencyAttribute : System.Attribute
14+
{
15+
public DynamicDependencyAttribute(DynamicallyAccessedMemberTypes memberTypes, System.Type type) { }
16+
public DynamicDependencyAttribute(string memberSignature, System.Type type) { }
17+
}
18+
}
19+
#endif

Refit/Refit.csproj

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,23 @@
99
</PropertyGroup>
1010
<PropertyGroup Condition="$(TargetFramework.StartsWith('net8.0')) or $(TargetFramework.StartsWith('net9.0')) or $(TargetFramework.StartsWith('net10.0'))">
1111
<IsAotCompatible>true</IsAotCompatible>
12+
<PublishAotSupported>true</PublishAotSupported>
13+
<TrimMode>link</TrimMode>
14+
<IsTrimmable>true</IsTrimmable>
15+
<IlcGenerateCompleteTypeMetadata>true</IlcGenerateCompleteTypeMetadata>
16+
</PropertyGroup>
17+
18+
<!-- Ensure DisableRuntimeMarshalling is only emitted for .NET 10, not .NET 8/9 -->
19+
<PropertyGroup Condition="$(TargetFramework.StartsWith('net8.0')) or $(TargetFramework.StartsWith('net9.0'))">
20+
<DisableRuntimeMarshalling>false</DisableRuntimeMarshalling>
21+
</PropertyGroup>
22+
<PropertyGroup Condition="$(TargetFramework.StartsWith('net10.0'))">
23+
<DisableRuntimeMarshalling>true</DisableRuntimeMarshalling>
1224
</PropertyGroup>
1325

1426
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0' Or '$(TargetFramework)' == 'net462'">
15-
<PackageReference Include="System.Text.Json" Version="9.0.3" />
16-
<PackageReference Include="System.Net.Http.Json" Version="9.0.3" />
27+
<PackageReference Include="System.Text.Json" Version="9.0.9" />
28+
<PackageReference Include="System.Net.Http.Json" Version="9.0.9" />
1729
</ItemGroup>
1830

1931
<ItemGroup Condition="'$(TargetFramework)' == 'net462'">

Refit/RequestBuilder.cs

Lines changed: 48 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,77 @@
11
using System.Net.Http;
2+
#if NET10_0_OR_GREATER
3+
using System.Diagnostics.CodeAnalysis;
4+
#endif
25

36
namespace Refit
47
{
5-
/// <summary>
6-
/// IRequestBuilder.
7-
/// </summary>
88
public interface IRequestBuilder
99
{
10-
/// <summary>
11-
/// Builds the rest result function for method.
12-
/// </summary>
13-
/// <param name="methodName">Name of the method.</param>
14-
/// <param name="parameterTypes">The parameter types.</param>
15-
/// <param name="genericArgumentTypes">The generic argument types.</param>
16-
/// <returns></returns>
1710
Func<HttpClient, object[], object?> BuildRestResultFuncForMethod(
1811
string methodName,
1912
Type[]? parameterTypes = null,
2013
Type[]? genericArgumentTypes = null
2114
);
2215
}
2316

24-
/// <summary>
25-
///
26-
/// </summary>
27-
/// <typeparam name="T"></typeparam>
2817
public interface IRequestBuilder<T> : IRequestBuilder { }
2918

30-
/// <summary>
31-
/// RequestBuilder.
32-
/// </summary>
3319
public static class RequestBuilder
3420
{
3521
static readonly RequestBuilderFactory PlatformRequestBuilderFactory = new();
3622

37-
/// <summary>
38-
/// Fors the type.
39-
/// </summary>
40-
/// <typeparam name="T"></typeparam>
41-
/// <param name="settings">The settings.</param>
42-
/// <returns></returns>
23+
#if NET10_0_OR_GREATER
24+
public static IRequestBuilder<T> ForType< [
25+
DynamicallyAccessedMembers(
26+
DynamicallyAccessedMemberTypes.None |
27+
DynamicallyAccessedMemberTypes.PublicMethods |
28+
DynamicallyAccessedMemberTypes.NonPublicMethods
29+
)] T >(RefitSettings? settings) =>
30+
PlatformRequestBuilderFactory.Create<T>(settings);
31+
#else
4332
public static IRequestBuilder<T> ForType<T>(RefitSettings? settings) =>
4433
PlatformRequestBuilderFactory.Create<T>(settings);
34+
#endif
4535

46-
/// <summary>
47-
/// Fors the type.
48-
/// </summary>
49-
/// <typeparam name="T"></typeparam>
50-
/// <returns></returns>
36+
#if NET10_0_OR_GREATER
37+
public static IRequestBuilder<T> ForType< [
38+
DynamicallyAccessedMembers(
39+
DynamicallyAccessedMemberTypes.None |
40+
DynamicallyAccessedMemberTypes.PublicMethods |
41+
DynamicallyAccessedMemberTypes.NonPublicMethods
42+
)] T >() =>
43+
PlatformRequestBuilderFactory.Create<T>(null);
44+
#else
5145
public static IRequestBuilder<T> ForType<T>() =>
5246
PlatformRequestBuilderFactory.Create<T>(null);
47+
#endif
5348

54-
/// <summary>
55-
/// Fors the type.
56-
/// </summary>
57-
/// <param name="refitInterfaceType">Type of the refit interface.</param>
58-
/// <param name="settings">The settings.</param>
59-
/// <returns></returns>
60-
public static IRequestBuilder ForType(Type refitInterfaceType, RefitSettings? settings) =>
61-
PlatformRequestBuilderFactory.Create(refitInterfaceType, settings);
49+
#if NET10_0_OR_GREATER
50+
[RequiresUnreferencedCode("Refit uses reflection to analyze interface methods. Ensure referenced interfaces and DTOs are preserved when trimming.")]
51+
public static IRequestBuilder ForType(
52+
[DynamicallyAccessedMembers(
53+
DynamicallyAccessedMemberTypes.None |
54+
DynamicallyAccessedMemberTypes.PublicMethods |
55+
DynamicallyAccessedMemberTypes.NonPublicMethods
56+
)] Type refitInterfaceType,
57+
RefitSettings? settings
58+
)
59+
#else
60+
public static IRequestBuilder ForType(
61+
Type refitInterfaceType,
62+
RefitSettings? settings
63+
)
64+
#endif
65+
{
66+
return new CachedRequestBuilderImplementation(
67+
new RequestBuilderImplementation(refitInterfaceType, settings)
68+
);
69+
}
6270

63-
/// <summary>
64-
/// Fors the type.
65-
/// </summary>
66-
/// <param name="refitInterfaceType">Type of the refit interface.</param>
67-
/// <returns></returns>
71+
#if NET10_0_OR_GREATER
72+
[RequiresUnreferencedCode("Refit uses reflection to analyze interface methods. Ensure referenced interfaces and DTOs are preserved when trimming.")]
73+
#endif
6874
public static IRequestBuilder ForType(Type refitInterfaceType) =>
69-
PlatformRequestBuilderFactory.Create(refitInterfaceType, null);
75+
ForType(refitInterfaceType, null);
7076
}
7177
}

Refit/RequestBuilderImplementation.cs

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,31 @@
11
using System.Collections;
22
using System.Collections.Concurrent;
33
using System.Diagnostics;
4-
using System.Diagnostics.CodeAnalysis;
54
using System.Net.Http;
65
using System.Reflection;
76
using System.Text;
87
using System.Web;
8+
#if NET5_0_OR_GREATER
9+
using System.Diagnostics.CodeAnalysis;
10+
#endif
911

1012
namespace Refit
1113
{
12-
class RequestBuilderImplementation<TApi>(RefitSettings? refitSettings = null)
13-
: RequestBuilderImplementation(typeof(TApi), refitSettings),
14-
IRequestBuilder<TApi> { }
14+
#if NET5_0_OR_GREATER
15+
class RequestBuilderImplementation<
16+
[
17+
DynamicallyAccessedMembers(
18+
DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods
19+
)] TApi> : RequestBuilderImplementation, IRequestBuilder<TApi>
20+
#else
21+
class RequestBuilderImplementation<TApi> : RequestBuilderImplementation, IRequestBuilder<TApi>
22+
#endif
23+
{
24+
public RequestBuilderImplementation(RefitSettings? refitSettings = null)
25+
: base(typeof(TApi), refitSettings)
26+
{
27+
}
28+
}
1529

1630
partial class RequestBuilderImplementation : IRequestBuilder
1731
{
@@ -27,7 +41,15 @@ readonly ConcurrentDictionary<
2741
readonly RefitSettings settings;
2842
public Type TargetType { get; }
2943

44+
#if NET5_0_OR_GREATER
45+
[RequiresUnreferencedCode("RequestBuilder uses reflection on the provided Refit interface and DTO types. Ensure necessary members are preserved when trimming.")]
46+
#endif
3047
public RequestBuilderImplementation(
48+
#if NET5_0_OR_GREATER
49+
[DynamicallyAccessedMembers(
50+
DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods
51+
)]
52+
#endif
3153
Type refitInterfaceType,
3254
RefitSettings? refitSettings = null
3355
)
@@ -65,7 +87,10 @@ static string GetLookupKeyForMethod(MethodInfo methodInfo)
6587
}
6688

6789
void AddInterfaceHttpMethods(
68-
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)]Type interfaceType,
90+
#if NET5_0_OR_GREATER
91+
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)]
92+
#endif
93+
Type interfaceType,
6994
Dictionary<string, List<RestMethodInfoInternal>> methods
7095
)
7196
{
@@ -175,6 +200,9 @@ RestMethodInfoInternal CloseGenericMethodIfNeeded(
175200
return restMethodInfo;
176201
}
177202

203+
#if NET5_0_OR_GREATER
204+
[RequiresUnreferencedCode("Building rest result delegates uses reflection over interface methods and DTOs. Ensure required members are preserved.")]
205+
#endif
178206
public Func<HttpClient, object[], object?> BuildRestResultFuncForMethod(
179207
string methodName,
180208
Type[]? parameterTypes = null,
@@ -1197,12 +1225,12 @@ var value in ParseEnumerableQueryParameterValue(
11971225
CollectionFormat.Ssv => " ",
11981226
CollectionFormat.Tsv => "\t",
11991227
CollectionFormat.Pipes => "|",
1200-
_ => ","
1228+
_ => ","
12011229
};
12021230

12031231
// Missing a "default" clause was preventing the collection from serializing at all, as it was hitting "continue" thus causing an off-by-one error
12041232
var formattedValues = paramValues
1205-
.Cast<object>()
1233+
.Cast<object> ()
12061234
.Select(
12071235
v =>
12081236
settings.UrlParameterFormatter.Format(

0 commit comments

Comments
 (0)