-
Notifications
You must be signed in to change notification settings - Fork 392
Generate AOT-compatible C# primitive responses #11931
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
6fa6ca4
79a59cb
bea225a
e5e08e6
06ea139
9278405
248bdc9
bba90fc
0e496fe
c6bbcbe
b12eb64
d9df7e9
755b311
ca77e1d
26058cc
f7cd15f
0766471
59cd778
46fc557
30b9413
de7abee
cd41087
2c5ad78
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,15 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT License. | ||
|
|
||
| // cspell:ignore FEFF | ||
|
|
||
| using System; | ||
| using System.ClientModel; | ||
| using System.ClientModel.Primitives; | ||
| using System.Collections; | ||
| using System.Collections.Generic; | ||
| using System.Diagnostics.CodeAnalysis; | ||
| using System.Globalization; | ||
| using System.IO; | ||
| using System.Linq; | ||
| using System.Net.ServerSentEvents; | ||
|
|
@@ -294,8 +297,13 @@ .. GetStackVariablesForProtocolParamConversion(convenienceBodyParameters, out va | |
| .. GetStackVariablesForProtocolParamConversion(convenienceBodyParameters, out var paramDeclarations), | ||
| Declare("result", This.Invoke(protocolMethod.Signature, [.. GetProtocolMethodArguments(paramDeclarations)], isAsync).ToApi<ClientResponseApi>(), out ClientResponseApi result), | ||
| .. GetStackVariablesForReturnValueConversion(result, responseBodyType, isAsync, out var resultDeclarations), | ||
| // Text/plain primitive and enum responses are parsed directly from the response content, bypassing the | ||
| // BinaryData/JsonDocument conversion path entirely. Route them through GetResultConversionStatements even | ||
| // when the response body type isn't in the IsConvertibleFromBinaryData allow-list (e.g. Guid, Uri, or the | ||
| // byte/short/unsigned integer types), otherwise they would incorrectly fall back to JSON parsing. | ||
| IsConvertibleFromBinaryData(responseBodyType) | ||
| ? Return(result.FromValue(GetResultConversion(result, result.GetRawResponse(), responseBodyType, resultDeclarations), result.GetRawResponse())) | ||
| || (IsPlainTextParsableType(responseBodyType) && HasOnlyPlainTextContentType()) | ||
| ? GetResultConversionStatements(result, result.GetRawResponse(), responseBodyType, resultDeclarations) | ||
| : | ||
| new[] | ||
| { | ||
|
|
@@ -634,6 +642,23 @@ private IEnumerable<MethodBodyStatement> GetStackVariablesForReturnValueConversi | |
| out declarations); | ||
| } | ||
|
|
||
| if (IsConvertibleFromBinaryData(responseBodyType) | ||
| && (responseBodyType.IsFrameworkType || responseBodyType.IsEnum) | ||
| && !responseBodyType.Equals(typeof(BinaryData)) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Copilot why do we need the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| && !HasOnlyPlainTextContentType()) | ||
| { | ||
| var data = result.GetRawResponse().Content(); | ||
| // The stream overload preserves UTF-8 BOM handling from ToObjectFromJson. | ||
| var statements = new MethodBodyStatement[] | ||
| { | ||
| UsingDeclare("stream", data.ToStream(), out var stream), | ||
| UsingDeclare("document", JsonDocumentSnippets.Parse(stream), out var document) | ||
| }; | ||
| declarations["data"] = data; | ||
| declarations["document"] = document; | ||
| return statements; | ||
| } | ||
|
|
||
| return []; | ||
| } | ||
|
|
||
|
|
@@ -837,6 +862,49 @@ private MethodBodyStatement AddElement(ValueExpression? dictKey, ValueExpression | |
| return scopedApi.Add(element); | ||
| } | ||
|
|
||
| private MethodBodyStatement[] GetResultConversionStatements(ClientResponseApi result, HttpResponseApi response, CSharpType responseBodyType, Dictionary<string, ValueExpression> declarations) | ||
| { | ||
| var plainTextParseType = GetPlainTextParseType(responseBodyType, out var enumType); | ||
| if (!responseBodyType.Equals(typeof(string)) && plainTextParseType is not null && HasOnlyPlainTextContentType()) | ||
| { | ||
| var contentExpression = response.Content().InvokeToString().Invoke(nameof(string.TrimStart), Literal('\uFEFF')).As<string>(); | ||
| return | ||
| [ | ||
| Declare("content", typeof(string), contentExpression, out var content), | ||
| Declare("value", responseBodyType, GetPlainTextValueConversion(responseBodyType, plainTextParseType, enumType, content), out var value), | ||
| Return(result.FromValue(value, response)) | ||
| ]; | ||
| } | ||
|
|
||
| var isSpecialCaseType = responseBodyType.Equals(typeof(BinaryData)) | ||
| || responseBodyType.IsCollection | ||
| || (responseBodyType.Equals(typeof(string)) && HasOnlyPlainTextContentType()); | ||
|
|
||
| if (!isSpecialCaseType && (responseBodyType.IsFrameworkType || responseBodyType.IsEnum)) | ||
| { | ||
| var element = declarations["document"].As<JsonDocument>().RootElement(); | ||
| var deserializedValue = ScmCodeModelGenerator.Instance.TypeFactory.DeserializeJsonValue( | ||
| responseBodyType.WithNullable(false), | ||
| element, | ||
| declarations["data"].As<BinaryData>(), | ||
| ScmCodeModelGenerator.Instance.ModelSerializationExtensionsDefinition.WireOptionsField.As<ModelReaderWriterOptions>(), | ||
| responseBodyType.Equals(typeof(TimeSpan)) || responseBodyType.Equals(typeof(TimeSpan?)) | ||
| ? SerializationFormat.Duration_Constant | ||
| : SerializationFormat.Default); | ||
| var valueExpression = responseBodyType.IsNullable | ||
| ? new TernaryConditionalExpression(element.ValueKindEqualsNull(), Null.CastTo(responseBodyType), deserializedValue) | ||
| : deserializedValue; | ||
|
|
||
| return | ||
| [ | ||
| Declare("value", responseBodyType, valueExpression, out var value), | ||
| Return(result.FromValue(value, response)) | ||
| ]; | ||
| } | ||
|
|
||
| return [Return(result.FromValue(GetResultConversion(result, response, responseBodyType, declarations), response))]; | ||
| } | ||
|
|
||
| private ValueExpression GetResultConversion(ClientResponseApi result, HttpResponseApi response, CSharpType responseBodyType, Dictionary<string, ValueExpression> declarations) | ||
| { | ||
| if (responseBodyType.Equals(typeof(BinaryData))) | ||
|
|
@@ -855,19 +923,100 @@ private ValueExpression GetResultConversion(ClientResponseApi result, HttpRespon | |
| { | ||
| return declarations["value"].CastTo(new CSharpType(responseBodyType.OutputType.FrameworkType, responseBodyType.Arguments[0], responseBodyType.Arguments[1])); | ||
| } | ||
| if (responseBodyType.Equals(typeof(string)) && ServiceMethod.Operation.Responses.Any(r => r.IsErrorResponse is false && r.ContentTypes.Contains("text/plain"))) | ||
| if (responseBodyType.Equals(typeof(string)) && HasOnlyPlainTextContentType()) | ||
| { | ||
| return response.Content().InvokeToString(); | ||
| } | ||
| if (responseBodyType.IsFrameworkType) | ||
| return result.CastTo(responseBodyType); | ||
| } | ||
|
|
||
| private ValueExpression GetPlainTextValueConversion(CSharpType responseBodyType, Type parseType, CSharpType? enumType, ValueExpression content) | ||
| { | ||
| var invariantCulture = new MemberExpression(typeof(CultureInfo), nameof(CultureInfo.InvariantCulture)); | ||
| var deserializedValue = parseType switch | ||
| { | ||
| Type t when t == typeof(string) => content, | ||
| Type t when t == typeof(bool) => Static<bool>().Invoke(nameof(bool.Parse), content).As<bool>(), | ||
| Type t when t == typeof(Guid) => Static<Guid>().Invoke(nameof(Guid.Parse), content).As<Guid>(), | ||
| Type t when t == typeof(Uri) => New.Instance<Uri>(content, FrameworkEnumValue(UriKind.RelativeOrAbsolute)), | ||
| Type t when t == typeof(TimeSpan) => content.As<string>().ParseTimeSpan(Literal(SerializationFormat.Duration_Constant.ToFormatSpecifier() ?? throw new InvalidOperationException())), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P2] Honor the duration's wire encoding in the plain-text parser. --generated by Copilot |
||
| Type t when t == typeof(DateTimeOffset) => content.As<string>().ParseDateTimeOffset(Literal(GetResponseSerializationFormat().ToFormatSpecifier())), | ||
| // The remaining supported types are numeric and all expose a static Parse(string, IFormatProvider) method. | ||
| _ => Static(parseType).Invoke(nameof(int.Parse), [content, invariantCulture]).As(parseType) | ||
| }; | ||
|
|
||
| if (enumType is not null) | ||
| { | ||
| deserializedValue = enumType.ToEnum(deserializedValue); | ||
| } | ||
|
|
||
| return responseBodyType.IsNullable | ||
| ? new TernaryConditionalExpression(content.Equal(Literal("null")), Null.CastTo(responseBodyType), deserializedValue) | ||
| : deserializedValue; | ||
|
Comment on lines
+953
to
+955
|
||
| } | ||
|
|
||
| private static bool IsPlainTextParsableType(CSharpType responseBodyType) | ||
| => GetPlainTextParseType(responseBodyType, out _) is not null; | ||
|
|
||
| /// <summary> | ||
| /// Gets the framework type that a raw text response body is parsed into, or <c>null</c> when the response body | ||
| /// type isn't a primitive or enum that can be parsed from raw text. Types such as <see cref="BinaryData"/>, | ||
| /// collections and generated models keep their existing conversion. | ||
| /// </summary> | ||
| private static Type? GetPlainTextParseType(CSharpType responseBodyType, out CSharpType? enumType) | ||
| { | ||
| enumType = null; | ||
| var typeToParse = responseBodyType.WithNullable(false); | ||
| if (typeToParse is { IsEnum: true, UnderlyingEnumType: { } underlyingEnumType }) | ||
| { | ||
| return response.Content().ToObjectFromJson(responseBodyType); | ||
| enumType = typeToParse; | ||
| typeToParse = underlyingEnumType; | ||
| } | ||
| if (responseBodyType.IsEnum) | ||
|
|
||
| if (!typeToParse.IsFrameworkType) | ||
| { | ||
| return responseBodyType.ToEnum(response.Content().ToObjectFromJson(responseBodyType.UnderlyingEnumType)); | ||
| return null; | ||
| } | ||
| return result.CastTo(responseBodyType); | ||
|
|
||
| var frameworkType = typeToParse.FrameworkType; | ||
| return frameworkType switch | ||
| { | ||
| Type t when t == typeof(string) | ||
| || t == typeof(bool) | ||
| || t == typeof(Guid) | ||
| || t == typeof(Uri) | ||
| || t == typeof(TimeSpan) | ||
| || t == typeof(DateTimeOffset) | ||
| || t == typeof(byte) | ||
| || t == typeof(sbyte) | ||
| || t == typeof(short) | ||
| || t == typeof(ushort) | ||
| || t == typeof(int) | ||
| || t == typeof(uint) | ||
| || t == typeof(long) | ||
| || t == typeof(ulong) | ||
| || t == typeof(float) | ||
| || t == typeof(double) | ||
| || t == typeof(decimal) => frameworkType, | ||
| _ => null | ||
| }; | ||
| } | ||
|
|
||
| private bool HasOnlyPlainTextContentType() | ||
| { | ||
| var contentTypes = ServiceMethod.Operation.Responses | ||
| .Where(r => r.IsErrorResponse is false) | ||
| .SelectMany(r => r.ContentTypes); | ||
| return contentTypes.Any() && contentTypes.All(contentType => contentType == "text/plain"); | ||
|
jorgerangel-msft marked this conversation as resolved.
|
||
| } | ||
|
|
||
| private SerializationFormat GetResponseSerializationFormat() | ||
| { | ||
| var responseBodyType = ServiceMethod.Operation.Responses | ||
| .FirstOrDefault(r => r.IsErrorResponse is false)?.BodyType; | ||
| return responseBodyType is null | ||
| ? SerializationFormat.Default | ||
| : ScmCodeModelGenerator.Instance.TypeFactory.GetSerializationFormat(responseBodyType); | ||
| } | ||
|
|
||
| private static bool ShouldBuildStackVarForFrameworkType(CSharpType type) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| public virtual async global::System.Threading.Tasks.Task<global::System.ClientModel.ClientResult<string>> UpdateResourceAsync(string param1, int param2, bool param3, global::System.Threading.CancellationToken cancellationToken = default) | ||
| { | ||
| global::Sample.Argument.AssertNotNullOrEmpty(param1, nameof(param1)); | ||
|
|
||
| using global::System.ClientModel.BinaryContent content = global::System.ClientModel.BinaryContent.Create(global::System.BinaryData.FromString(param1)); | ||
| global::System.ClientModel.ClientResult result = await this.UpdateResourceAsync(content, param2, param3, cancellationToken.ToRequestOptions()).ConfigureAwait(false); | ||
| using global::System.IO.Stream stream = result.GetRawResponse().Content.ToStream(); | ||
| using global::System.Text.Json.JsonDocument document = global::System.Text.Json.JsonDocument.Parse(stream); | ||
| string value = document.RootElement.GetString(); | ||
| return global::System.ClientModel.ClientResult.FromValue(value, result.GetRawResponse()); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| public virtual global::System.ClientModel.ClientResult<string> UpdateResource(string param1, int param2, bool param3, global::System.Threading.CancellationToken cancellationToken = default) | ||
| { | ||
| global::Sample.Argument.AssertNotNullOrEmpty(param1, nameof(param1)); | ||
|
|
||
| using global::System.ClientModel.BinaryContent content = global::System.ClientModel.BinaryContent.Create(global::System.BinaryData.FromString(param1)); | ||
| global::System.ClientModel.ClientResult result = this.UpdateResource(content, param2, param3, cancellationToken.ToRequestOptions()); | ||
| using global::System.IO.Stream stream = result.GetRawResponse().Content.ToStream(); | ||
| using global::System.Text.Json.JsonDocument document = global::System.Text.Json.JsonDocument.Parse(stream); | ||
| string value = document.RootElement.GetString(); | ||
| return global::System.ClientModel.ClientResult.FromValue(value, result.GetRawResponse()); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| public virtual async global::System.Threading.Tasks.Task<global::System.ClientModel.ClientResult<string>> GetDataAsync(string param1, int param2, bool param3, global::System.Threading.CancellationToken cancellationToken = default) | ||
| { | ||
| global::Sample.Argument.AssertNotNullOrEmpty(param1, nameof(param1)); | ||
|
|
||
| using global::System.ClientModel.BinaryContent content = global::System.ClientModel.BinaryContent.Create(global::System.BinaryData.FromString(param1)); | ||
| global::System.ClientModel.ClientResult result = await this.GetDataAsync(param3, param2, content, cancellationToken.ToRequestOptions()).ConfigureAwait(false); | ||
| using global::System.IO.Stream stream = result.GetRawResponse().Content.ToStream(); | ||
| using global::System.Text.Json.JsonDocument document = global::System.Text.Json.JsonDocument.Parse(stream); | ||
| string value = document.RootElement.GetString(); | ||
| return global::System.ClientModel.ClientResult.FromValue(value, result.GetRawResponse()); | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.