Skip to content

Commit c8ba0ce

Browse files
authored
Implement IDownstreamApi overloads that take JsonTypeInfo<T> as a parameter to enable source generated Json deserialization for NativeAOT (#2959)
Addresses #2930 Update Microsoft.Identity.Abstractions to 7.0.0 Implementation for new DownstreamApi interfaces in Microsoft.Identity.Abstractions for source gen Json serialization. Implementaitons are generated or copied as the existing with just the addition of jsonTypeInfo params
1 parent 783d999 commit c8ba0ce

File tree

8 files changed

+971
-20
lines changed

8 files changed

+971
-20
lines changed

Directory.Build.props

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,7 @@
8787
<MicrosoftGraphVersion>4.36.0</MicrosoftGraphVersion>
8888
<MicrosoftGraphBetaVersion>4.57.0-preview</MicrosoftGraphBetaVersion>
8989
<MicrosoftExtensionsHttpVersion>3.1.3</MicrosoftExtensionsHttpVersion>
90-
<!-- IdentityWeb v3.0.1 is not compatible with Abstractions v7; 6.0.0 ≤ version < 7.0.0 -->
91-
<MicrosoftIdentityAbstractionsVersion Condition="'$(MicrosoftIdentityAbstractionsVersion)' == ''">[6.0.0, 7.0.0)</MicrosoftIdentityAbstractionsVersion>
90+
<MicrosoftIdentityAbstractionsVersion>7.0.0</MicrosoftIdentityAbstractionsVersion>
9291
<NetNineRuntimeVersion>9.0.0-preview.7.24405.7</NetNineRuntimeVersion>
9392
<AspNetCoreNineRuntimeVersion>9.0.0-preview.7.24406.2</AspNetCoreNineRuntimeVersion>
9493
<!--CVE-2024-30105-->

src/Microsoft.Identity.Web.DownstreamApi/DownstreamApi.HttpMethods.cs

Lines changed: 714 additions & 1 deletion
Large diffs are not rendered by default.

src/Microsoft.Identity.Web.DownstreamApi/DownstreamApi.HttpMethods.tt

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,30 @@ using System.Threading;
1515
using System.Threading.Tasks;
1616
using Microsoft.Identity.Abstractions;
1717

18+
#if NET8_0_OR_GREATER
19+
using System.Text.Json.Serialization.Metadata;
20+
#endif
21+
1822
namespace Microsoft.Identity.Web
1923
{
2024
/// <inheritdoc/>
2125
internal partial class DownstreamApi : IDownstreamApi
2226
{
2327
<#
2428
bool firstMethod = true;
29+
30+
foreach(string framework in new string[]{ "all", "net8" } )
31+
{
32+
if (framework == "net8")
33+
{
34+
#>
35+
36+
#if NET8_0_OR_GREATER
37+
<#
38+
}
2539
foreach(string httpMethod in new string[]{ "Get", "Post", "Put", "Patch", "Delete"} )
2640
{
27-
if (httpMethod == "Patch")
41+
if (httpMethod == "Patch" && framework != "net8")
2842
{
2943
#>
3044

@@ -60,6 +74,12 @@ namespace Microsoft.Identity.Web
6074
string? serviceName,
6175
<# if (hasInput){ #>
6276
TInput input,
77+
<# } #>
78+
<# if (hasInput && framework == "net8"){ #>
79+
JsonTypeInfo<TInput> inputJsonTypeInfo,
80+
<# } #>
81+
<# if (hasOutput && framework == "net8"){ #>
82+
JsonTypeInfo<TOutput> outputJsonTypeInfo,
6383
<# } #>
6484
Action<DownstreamApiOptionsReadOnlyHttpMethod>? downstreamApiOptionsOverride = null,
6585
<# if (!hasApp){ #>
@@ -71,9 +91,13 @@ namespace Microsoft.Identity.Web
7191
<# } #>
7292
{
7393
DownstreamApiOptions effectiveOptions = MergeOptions(serviceName, downstreamApiOptionsOverride, HttpMethod.<#=httpMethod#>);
74-
<# if (hasInput){ #>
94+
<# if (hasInput) {
95+
if (framework == "net8"){
96+
#>
97+
HttpContent? effectiveInput = SerializeInput(input, effectiveOptions, inputJsonTypeInfo);
98+
<# } else { #>
7599
HttpContent? effectiveInput = SerializeInput(input, effectiveOptions);
76-
<# } #>
100+
<# } } #>
77101
try
78102
{
79103
HttpResponseMessage response = await CallApiInternalAsync(serviceName, effectiveOptions, <#= hasApp ? "true" : "false" #>, <#= content #>, <#= user #>, cancellationToken).ConfigureAwait(false);
@@ -86,8 +110,12 @@ namespace Microsoft.Identity.Web
86110
}
87111
response.EnsureSuccessStatusCode();
88112
<# }
89-
if (hasOutput)
113+
if (hasOutput && framework == "net8")
90114
{ #>
115+
return await DeserializeOutput<TOutput>(response, effectiveOptions, outputJsonTypeInfo).ConfigureAwait(false);
116+
<# }
117+
else if (hasOutput)
118+
{#>
91119
return await DeserializeOutput<TOutput>(response, effectiveOptions).ConfigureAwait(false);
92120
<# } #>
93121
}
@@ -108,13 +136,17 @@ namespace Microsoft.Identity.Web
108136
}
109137
#>
110138
<#
111-
if (httpMethod == "Patch")
139+
if (httpMethod == "Patch" && framework != "net8")
112140
{
113141
#>
114142

115143
#endif // !NETFRAMEWORK && !NETSTANDARD2_0
116144
<#
117145
}
118146
}
119-
#> }
147+
if (framework == "net8") {#>
148+
#endif // NET8_0_OR_GREATER
149+
<#}
150+
}
151+
#> }
120152
}

src/Microsoft.Identity.Web.DownstreamApi/DownstreamApi.cs

Lines changed: 125 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.Security.Claims;
1010
using System.Text;
1111
using System.Text.Json;
12+
using System.Text.Json.Serialization.Metadata;
1213
using System.Threading;
1314
using System.Threading.Tasks;
1415
using Microsoft.Extensions.Logging;
@@ -166,8 +167,101 @@ public Task<HttpResponseMessage> CallApiForAppAsync(
166167
HttpResponseMessage response = await CallApiInternalAsync(serviceName, effectiveOptions, false,
167168
null, user, cancellationToken).ConfigureAwait(false);
168169
return await DeserializeOutput<TOutput>(response, effectiveOptions).ConfigureAwait(false);
169-
}
170+
}
171+
172+
#if NET8_0_OR_GREATER
173+
/// <inheritdoc/>
174+
public async Task<TOutput?> CallApiForUserAsync<TInput, TOutput>(
175+
string? serviceName,
176+
TInput input,
177+
JsonTypeInfo<TInput> inputJsonTypeInfo,
178+
JsonTypeInfo<TOutput> outputJsonTypeInfo,
179+
Action<DownstreamApiOptions>? downstreamApiOptionsOverride = null,
180+
ClaimsPrincipal? user = default,
181+
CancellationToken cancellationToken = default)
182+
where TOutput : class
183+
{
184+
DownstreamApiOptions effectiveOptions = MergeOptions(serviceName, downstreamApiOptionsOverride);
185+
HttpContent? effectiveInput = SerializeInput(input, effectiveOptions, inputJsonTypeInfo);
186+
187+
HttpResponseMessage response = await CallApiInternalAsync(serviceName, effectiveOptions, false,
188+
effectiveInput, user, cancellationToken).ConfigureAwait(false);
189+
190+
// Only dispose the HttpContent if was created here, not provided by the caller.
191+
if (input is not HttpContent)
192+
{
193+
effectiveInput?.Dispose();
194+
}
195+
196+
return await DeserializeOutput<TOutput>(response, effectiveOptions, outputJsonTypeInfo).ConfigureAwait(false);
197+
}
198+
199+
/// <inheritdoc/>
200+
public async Task<TOutput?> CallApiForUserAsync<TOutput>(
201+
string serviceName,
202+
JsonTypeInfo<TOutput> outputJsonTypeInfo,
203+
Action<DownstreamApiOptions>? downstreamApiOptionsOverride = null,
204+
ClaimsPrincipal? user = default,
205+
CancellationToken cancellationToken = default)
206+
where TOutput : class
207+
{
208+
DownstreamApiOptions effectiveOptions = MergeOptions(serviceName, downstreamApiOptionsOverride);
209+
HttpResponseMessage response = await CallApiInternalAsync(serviceName, effectiveOptions, false,
210+
null, user, cancellationToken).ConfigureAwait(false);
211+
return await DeserializeOutput<TOutput>(response, effectiveOptions, outputJsonTypeInfo).ConfigureAwait(false);
212+
}
213+
214+
/// <inheritdoc/>
215+
public async Task<TOutput?> CallApiForAppAsync<TInput, TOutput>(
216+
string? serviceName,
217+
TInput input,
218+
JsonTypeInfo<TInput> inputJsonTypeInfo,
219+
JsonTypeInfo<TOutput> outputJsonTypeInfo,
220+
Action<DownstreamApiOptions>? downstreamApiOptionsOverride = null,
221+
CancellationToken cancellationToken = default)
222+
where TOutput : class
223+
{
224+
DownstreamApiOptions effectiveOptions = MergeOptions(serviceName, downstreamApiOptionsOverride);
225+
HttpContent? effectiveInput = SerializeInput(input, effectiveOptions, inputJsonTypeInfo);
226+
HttpResponseMessage response = await CallApiInternalAsync(serviceName, effectiveOptions, true,
227+
effectiveInput, null, cancellationToken).ConfigureAwait(false);
170228

229+
// Only dispose the HttpContent if was created here, not provided by the caller.
230+
if (input is not HttpContent)
231+
{
232+
effectiveInput?.Dispose();
233+
}
234+
235+
return await DeserializeOutput<TOutput>(response, effectiveOptions, outputJsonTypeInfo).ConfigureAwait(false);
236+
}
237+
238+
/// <inheritdoc/>
239+
public async Task<TOutput?> CallApiForAppAsync<TOutput>(
240+
string serviceName,
241+
JsonTypeInfo<TOutput> outputJsonTypeInfo,
242+
Action<DownstreamApiOptions>? downstreamApiOptionsOverride = null,
243+
CancellationToken cancellationToken = default)
244+
where TOutput : class
245+
{
246+
DownstreamApiOptions effectiveOptions = MergeOptions(serviceName, downstreamApiOptionsOverride);
247+
HttpResponseMessage response = await CallApiInternalAsync(serviceName, effectiveOptions, true,
248+
null, null, cancellationToken).ConfigureAwait(false);
249+
250+
return await DeserializeOutput<TOutput>(response, effectiveOptions, outputJsonTypeInfo).ConfigureAwait(false);
251+
}
252+
253+
internal static HttpContent? SerializeInput<TInput>(TInput input, DownstreamApiOptions effectiveOptions, JsonTypeInfo<TInput> inputJsonTypeInfo)
254+
{
255+
return SerializeInputImpl(input, effectiveOptions, inputJsonTypeInfo);
256+
}
257+
258+
internal static async Task<TOutput?> DeserializeOutput<TOutput>(HttpResponseMessage response, DownstreamApiOptions effectiveOptions, JsonTypeInfo<TOutput> outputJsonTypeInfo)
259+
where TOutput : class
260+
{
261+
return await DeserializeOutputImpl<TOutput>(response, effectiveOptions, outputJsonTypeInfo);
262+
}
263+
#endif
264+
171265
/// <summary>
172266
/// Merge the options from configuration and override from caller.
173267
/// </summary>
@@ -217,9 +311,14 @@ public Task<HttpResponseMessage> CallApiForAppAsync(
217311
DownstreamApiOptionsReadOnlyHttpMethod clonedOptions = new DownstreamApiOptionsReadOnlyHttpMethod(options, httpMethod.ToString());
218312
calledApiOptionsOverride?.Invoke(clonedOptions);
219313
return clonedOptions;
314+
}
315+
316+
internal static HttpContent? SerializeInput<TInput>(TInput input, DownstreamApiOptions effectiveOptions)
317+
{
318+
return SerializeInputImpl(input, effectiveOptions, null);
220319
}
221320

222-
internal static HttpContent? SerializeInput<TInput>(TInput input, DownstreamApiOptions effectiveOptions)
321+
private static HttpContent? SerializeInputImpl<TInput>(TInput input, DownstreamApiOptions effectiveOptions, JsonTypeInfo<TInput>? inputJsonTypeInfo = null)
223322
{
224323
HttpContent? httpContent;
225324

@@ -234,17 +333,29 @@ public Task<HttpResponseMessage> CallApiForAppAsync(
234333
{
235334
HttpContent content => content,
236335
string str when !string.IsNullOrEmpty(effectiveOptions.ContentType) && effectiveOptions.ContentType.StartsWith("text", StringComparison.OrdinalIgnoreCase) => new StringContent(str),
237-
string str => new StringContent(JsonSerializer.Serialize(str), Encoding.UTF8, "application/json"),
336+
string str => new StringContent(
337+
inputJsonTypeInfo == null ? JsonSerializer.Serialize(str) : JsonSerializer.Serialize(str, inputJsonTypeInfo),
338+
Encoding.UTF8,
339+
"application/json"),
238340
byte[] bytes => new ByteArrayContent(bytes),
239341
Stream stream => new StreamContent(stream),
240342
null => null,
241-
_ => new StringContent(JsonSerializer.Serialize(input), Encoding.UTF8, "application/json"),
343+
_ => new StringContent(
344+
inputJsonTypeInfo == null ? JsonSerializer.Serialize(input) : JsonSerializer.Serialize(input, inputJsonTypeInfo),
345+
Encoding.UTF8,
346+
"application/json"),
242347
};
243348
}
244349
return httpContent;
245350
}
246351

247352
internal static async Task<TOutput?> DeserializeOutput<TOutput>(HttpResponseMessage response, DownstreamApiOptions effectiveOptions)
353+
where TOutput : class
354+
{
355+
return await DeserializeOutputImpl<TOutput>(response, effectiveOptions, null);
356+
}
357+
358+
private static async Task<TOutput?> DeserializeOutputImpl<TOutput>(HttpResponseMessage response, DownstreamApiOptions effectiveOptions, JsonTypeInfo<TOutput>? outputJsonTypeInfo = null)
248359
where TOutput : class
249360
{
250361
try
@@ -284,7 +395,14 @@ public Task<HttpResponseMessage> CallApiForAppAsync(
284395
string stringContent = await content.ReadAsStringAsync();
285396
if (mediaType == "application/json")
286397
{
287-
return JsonSerializer.Deserialize<TOutput>(stringContent, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
398+
if (outputJsonTypeInfo != null)
399+
{
400+
return JsonSerializer.Deserialize<TOutput>(stringContent, outputJsonTypeInfo);
401+
}
402+
else
403+
{
404+
return JsonSerializer.Deserialize<TOutput>(stringContent, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
405+
}
288406
}
289407
if (mediaType != null && !mediaType.StartsWith("text/", StringComparison.OrdinalIgnoreCase))
290408
{
@@ -361,8 +479,8 @@ internal async Task UpdateRequestAsync(
361479
effectiveOptions.Scopes,
362480
effectiveOptions,
363481
user,
364-
cancellationToken).ConfigureAwait(false);
365-
482+
cancellationToken).ConfigureAwait(false);
483+
366484
httpRequestMessage.Headers.Add(Authorization, authorizationHeader);
367485
}
368486
else

tests/E2E Tests/IntegrationTestService/Controllers/WeatherForecast2Controller.cs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License.
33

44
using System.Net.Http;
5+
using System.Text.Json.Serialization;
56
using System.Threading.Tasks;
67
using Microsoft.AspNetCore.Authorization;
78
using Microsoft.AspNetCore.Mvc;
@@ -17,7 +18,7 @@ namespace IntegrationTestService.Controllers
1718
[Authorize(AuthenticationSchemes = TestConstants.CustomJwtScheme2)]
1819
[Route("SecurePage2")]
1920
[RequiredScope("user_impersonation")]
20-
public class WeatherForecast2Controller : ControllerBase
21+
public partial class WeatherForecast2Controller : ControllerBase
2122
{
2223
private readonly IDownstreamApi _downstreamApi;
2324
private readonly ITokenAcquisition _tokenAcquisition;
@@ -62,6 +63,27 @@ public async Task<HttpResponseMessage> CallDownstreamWebApiAsync()
6263
return user?.DisplayName;
6364
}
6465

66+
[JsonSerializable(typeof(UserInfo))]
67+
internal partial class UserInfoJsonContext : JsonSerializerContext
68+
{
69+
}
70+
71+
#if NET8_0_OR_GREATER
72+
[HttpGet(TestConstants.SecurePage2CallDownstreamWebApiGenericAotInternal)]
73+
public async Task<string?> CallDownstreamWebApiGenericAsyncAotInternal()
74+
{
75+
var user = await _downstreamApi.GetForUserAsync<UserInfo>(
76+
TestConstants.SectionNameCalledApi,
77+
UserInfoJsonContext.Default.UserInfo,
78+
options =>
79+
{
80+
options.RelativePath = "me";
81+
options.AcquireTokenOptions.AuthenticationOptionsName = TestConstants.CustomJwtScheme2;
82+
});
83+
return user?.DisplayName;
84+
}
85+
#endif
86+
6587
[HttpGet(TestConstants.SecurePage2CallMicrosoftGraph)]
6688
public async Task<string> CallMicrosoftGraphAsync()
6789
{

tests/Microsoft.Identity.Web.Test.Common/TestConstants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ public static class TestConstants
155155
public const string SecurePage2GetTokenForAppAsync = "/SecurePage2/GetTokenForAppAsync";
156156
public const string SecurePage2CallDownstreamWebApi = "/SecurePage2/CallDownstreamWebApiAsync";
157157
public const string SecurePage2CallDownstreamWebApiGeneric = "/SecurePage2/CallDownstreamWebApiGenericAsync";
158+
public const string SecurePage2CallDownstreamWebApiGenericAotInternal = "/SecurePage2/CallDownstreamWebApiGenericAsyncAotInternal";
158159
public const string SecurePage2CallDownstreamWebApiGenericWithTokenAcquisitionOptions = "/SecurePage2/CallDownstreamWebApiGenericWithTokenAcquisitionOptionsAsync";
159160
public const string SecurePage2CallMicrosoftGraph = "/SecurePage2/CallMicrosoftGraph";
160161
public const string SectionNameCalledApi = "CalledApi";

tests/Microsoft.Identity.Web.Test.Integration/AcquireTokenForUserIntegrationTests.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
using Microsoft.AspNetCore.Mvc.Testing;
1515
using Microsoft.Extensions.DependencyInjection;
1616
using Microsoft.Identity.Client;
17-
using Microsoft.Identity.Web.Test.Common;
1817
using Microsoft.Identity.Lab.Api;
18+
using Microsoft.Identity.Web.Test.Common;
1919
using Microsoft.Identity.Web.Test.Common.TestHelpers;
2020
using Microsoft.Identity.Web.TokenCacheProviders.Distributed;
2121
using Microsoft.Identity.Web.TokenCacheProviders.InMemory;
@@ -67,6 +67,10 @@ public async Task GetTokenForUserAsync(
6767
[InlineData(TestConstants.SecurePage2CallMicrosoftGraph, false)]
6868
[InlineData(TestConstants.SecurePage2CallDownstreamWebApi, false)]
6969
[InlineData(TestConstants.SecurePage2CallDownstreamWebApiGeneric, false)]
70+
#if NET8_0_OR_GREATER
71+
[InlineData(TestConstants.SecurePage2CallDownstreamWebApiGenericAotInternal)]
72+
[InlineData(TestConstants.SecurePage2CallDownstreamWebApiGenericAotInternal, false)]
73+
#endif
7074
public async Task GetTokenForUserWithDifferentAuthSchemeAsync(
7175
string webApiUrl,
7276
bool addInMemoryTokenCache = true)
@@ -132,7 +136,7 @@ public async Task TestSigningKeyIssuer()
132136
}
133137
}
134138
#endif
135-
139+
136140

137141
private static async Task<HttpResponseMessage> CreateHttpResponseMessage(string webApiUrl, HttpClient client, AuthenticationResult result)
138142
{
@@ -208,4 +212,4 @@ private static async Task<AuthenticationResult> AcquireTokenForLabUserAsync()
208212
}
209213
}
210214
#endif //FROM_GITHUB_ACTION
211-
}
215+
}

0 commit comments

Comments
 (0)