Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ $repoRoot = Resolve-Path (Join-Path $PSScriptRoot '..')

$failingSpecs = @(
Join-Path 'http' 'streaming' 'jsonl'
Join-Path 'http' 'payload' 'xml'
Join-Path 'http' 'response' 'status-code-range' # Response namespace conflicts with Azure.Response
# Azure scenarios not yet buildable
Join-Path 'http' 'azure' 'client-generator-core' 'alternate-type'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,27 @@
<ItemGroup>
<PackageReference Include="Azure.Core" />
<PackageReference Include="Azure.Core.Expressions.DataFactory" />
<PackageReference Include="Azure.ResourceManager" />
<PackageReference Include="Azure.ResourceManager" />
<PackageReference Include="Microsoft.TypeSpec.Generator.ClientModel" />
</ItemGroup>

<Target Name="CheckEmitterBuild" BeforeTargets="Build">
<!-- Check if the folder does not exist -->
<Error Condition="!Exists('..\..\..\node_modules\@typespec\http-client-csharp')" Text="Emitter has not been built please run `npm ci` from repo root folder, and followed by `npm run build` from ./eng/packages/http-client-csharp folder." />
</Target>
<Target Name="CheckEmitterBuild" BeforeTargets="Build">
<!-- Check if the folder does not exist -->
<Error Condition="!Exists('..\..\..\node_modules\@typespec\http-client-csharp')" Text="Emitter has not been built please run `npm ci` from repo root folder, and followed by `npm run build` from ./eng/packages/http-client-csharp folder." />
</Target>

<!-- Copy output to package dist path for local execution -->
<Target Name="CopyForNpmPackage" AfterTargets="Build">
<Message Text="Copying output to dist path" Importance="high" />
<ItemGroup>
<SourceDir Include="$(OutputPath)**\*.*" />
</ItemGroup>
<Copy SourceFiles="@(SourceDir)" DestinationFolder="$(MSBuildThisFileDirectory)..\..\..\dist\generator\%(RecursiveDir)" />
<!-- Copy output to package dist path for local execution -->
<Target Name="CopyForNpmPackage" AfterTargets="Build">
<Message Text="Copying output to dist path" Importance="high" />
<ItemGroup>
<SourceDir Include="$(OutputPath)**\*.*" />
</ItemGroup>
<Copy SourceFiles="@(SourceDir)" DestinationFolder="$(MSBuildThisFileDirectory)..\..\..\dist\generator\%(RecursiveDir)" />
<Copy SourceFiles="..\..\..\node_modules\@typespec\http-client-csharp\dist\generator\Microsoft.TypeSpec.Generator.runtimeconfig.json" DestinationFolder="$(MSBuildThisFileDirectory)..\..\..\dist\generator" />
</Target>
</Target>

<!-- Include shared code from Azure.Core. CopyToOutputDirectory is required to make roslyn reduce work -->
<ItemGroup>
<!-- Include shared code from Azure.Core. CopyToOutputDirectory is required to make roslyn reduce work -->
<ItemGroup>
<Compile Include="
$(MSBuildThisFileDirectory)..\..\..\..\..\..\sdk\core\Azure.Core\src\Shared\AzureKeyCredentialPolicy.cs;
$(MSBuildThisFileDirectory)..\..\..\..\..\..\sdk\core\Azure.Core\src\Shared\RawRequestUriBuilder.cs;
Expand All @@ -53,10 +53,12 @@
$(MSBuildThisFileDirectory)..\..\..\..\..\..\sdk\core\Azure.Core\src\Shared\IOperationSource.cs;
$(MSBuildThisFileDirectory)..\..\..\..\..\..\sdk\core\Azure.Core\src\Shared\TaskExtensions.cs;
$(MSBuildThisFileDirectory)..\..\..\..\..\..\sdk\core\Azure.Core\src\Shared\OperationPoller.cs;
$(MSBuildThisFileDirectory)..\..\..\..\..\..\sdk\core\Azure.Core\src\Shared\FixedDelayWithNoJitterStrategy.cs;">
$(MSBuildThisFileDirectory)..\..\..\..\..\..\sdk\core\Azure.Core\src\Shared\FixedDelayWithNoJitterStrategy.cs;
$(MSBuildThisFileDirectory)..\..\..\..\..\..\sdk\core\Azure.Core\src\Shared\IXmlSerializable.cs;
$(MSBuildThisFileDirectory)..\..\..\..\..\..\sdk\core\Azure.Core\src\Shared\XmlWriterContent.cs;">
<LinkBase>Shared/Core</LinkBase>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Compile>
</ItemGroup>

</Project>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -94,5 +94,6 @@ protected override void Configure()
AddVisitor(new SystemTextJsonConverterVisitor());
AddVisitor(new MultiPartFormDataVisitor());
AddVisitor(new InvokeDelimitedMethodVisitor());
AddVisitor(new XmlSerializableVisitor());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ protected override string GetSourceProjectFileContent()
"VoidValue.cs"
];

private static readonly IReadOnlyList<string> _xmlSerializationSharedFiles =
[
"IXmlSerializable.cs",
"XmlWriterContent.cs",
];

private static void TraverseInput(InputClient rootClient, ref bool hasOperation, ref bool hasLongRunningOperation)
{
if (hasOperation && hasLongRunningOperation)
Expand Down Expand Up @@ -179,6 +185,15 @@ protected override IReadOnlyList<CSharpProjectCompileInclude> BuildCompileInclud
compileIncludes.Add(new CSharpProjectCompileInclude(GetCompileInclude("TaskExtensions.cs"), SharedSourceLinkBase));
}

// Add IXmlSerializable if any model supports XML serialization
if (AzureClientGenerator.Instance.InputLibrary.HasXmlModelSerialization)
{
foreach (var file in _xmlSerializationSharedFiles)
{
compileIncludes.Add(new CSharpProjectCompileInclude(GetCompileInclude(file), SharedSourceLinkBase));
}
}

return compileIncludes;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,11 @@
"commandName": "Executable",
"executablePath": "dotnet"
},
"http-payload-xml": {
"commandLineArgs": "$(SolutionDir)/../dist/generator/Microsoft.TypeSpec.Generator.dll $(SolutionDir)/TestProjects/Spector/http/payload/xml -g AzureStubGenerator",
"commandName": "Executable",
"executablePath": "dotnet"
},
"http-resiliency-srv-driven-v1": {
"commandLineArgs": "$(SolutionDir)/../dist/generator/Microsoft.TypeSpec.Generator.dll $(SolutionDir)/TestProjects/Spector/http/resiliency/srv-driven/v1 -g AzureStubGenerator",
"commandName": "Executable",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Azure.Core;
using Microsoft.TypeSpec.Generator.Snippets;
using System.Xml;

namespace Azure.Generator.Snippets
{
internal static class XmlWriterContentSnippets
{
public static ScopedApi<XmlWriter> XmlWriter(this ScopedApi<XmlWriterContent> content)
=> content.Property(nameof(XmlWriterContent.XmlWriter)).As<XmlWriter>();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Azure.Core;
using Azure.Generator.Snippets;
using System.Collections.Generic;
using System.Linq;
using System.Xml;
using Microsoft.TypeSpec.Generator.ClientModel;
using Microsoft.TypeSpec.Generator.ClientModel.Providers;
using Microsoft.TypeSpec.Generator.Expressions;
using Microsoft.TypeSpec.Generator.Input;
using Microsoft.TypeSpec.Generator.Primitives;
using Microsoft.TypeSpec.Generator.Providers;
using Microsoft.TypeSpec.Generator.Statements;
using static Microsoft.TypeSpec.Generator.Snippets.Snippet;

namespace Azure.Generator.Visitors
{
/// <summary>
/// Visitor that adds <see cref="IXmlSerializable"/> interface implementation to models that support XML serialization.
/// </summary>
/// <remarks>
/// This visitor performs the following modifications:
/// <list type="number">
/// <item>
/// <description>
/// For models with <see cref="InputModelTypeUsage.Xml"/> usage, adds the <see cref="IXmlSerializable"/> interface
/// and implements the explicit <c>void IXmlSerializable.Write(XmlWriter writer, string nameHint)</c> method.
/// </description>
/// </item>
/// <item>
/// <description>
/// Updates the <c>WriteObjectValue</c> extension method in <see cref="ModelSerializationExtensionsDefinition"/>
/// to add a case for <see cref="IXmlSerializable"/> in the switch statement.
/// </description>
/// </item>
/// <item>
/// <description>
/// For models that ONLY support XML serialization (not JSON), updates the implicit <c>RequestContent</c> operator
/// to use <see cref="XmlWriterContent"/>.
/// </description>
/// </item>
/// </list>
/// </remarks>
internal class XmlSerializableVisitor : ScmLibraryVisitor
{
private const string WriteMethodName = "Write";
private const string WriteObjectValueMethodName = "WriteObjectValue";
private static readonly CSharpType IXmlSerializableType = typeof(IXmlSerializable);
private static readonly CSharpType RequestContentType = typeof(RequestContent);
private readonly Dictionary<TypeProvider, InputModelType> _xmlSerializationProviders = [];

protected override ModelProvider? PreVisitModel(InputModelType model, ModelProvider? type)
{
if (model.Usage.HasFlag(InputModelTypeUsage.Xml) && type is not null)
{
foreach (var serializationProvider in type.SerializationProviders)
{
_xmlSerializationProviders.TryAdd(serializationProvider, model);
}
}

return type;
}

protected override TypeProvider? VisitType(TypeProvider type)
{
if (type is MrwSerializationTypeDefinition serializationProvider)
{
if (_xmlSerializationProviders.TryGetValue(serializationProvider, out var inputModel))
{
AddIXmlSerializableImplementation(serializationProvider);

string? xmlElementName = inputModel.SerializationOptions?.Xml?.Name;
if (inputModel.Usage.HasFlag(InputModelTypeUsage.Json))
{
UpdateImplicitRequestContentOperatorForJsonAndXml(serializationProvider);
}
else if (xmlElementName is not null)
{
UpdateImplicitRequestContentOperatorForXmlOnly(serializationProvider, xmlElementName);
}
}
}
else if (type is ModelSerializationExtensionsDefinition modelSerializationExtensions)
{
UpdateWriteObjectValueMethod(modelSerializationExtensions);
}

return type;
}

private static void AddIXmlSerializableImplementation(TypeProvider serializationProvider)
{
var writerParameter = new ParameterProvider("writer", $"The XML writer.", typeof(XmlWriter));
var nameHintParameter = new ParameterProvider("nameHint", $"An optional name hint.", new CSharpType(typeof(string)));

var methodSignature = new MethodSignature(
WriteMethodName,
null,
MethodSignatureModifiers.None,
null,
null,
[writerParameter, nameHintParameter],
ExplicitInterface: IXmlSerializableType);

var bodyExpression = This.Invoke(
WriteMethodName,
[writerParameter, Static<ModelSerializationExtensionsDefinition>().Property("WireOptions"), nameHintParameter]);

var ixmlSerializableWriteMethod = new MethodProvider(methodSignature, bodyExpression, serializationProvider);

// Update the serialization provider with the new interface and method
var updatedImplements = new List<CSharpType>(serializationProvider.Implements) { IXmlSerializableType };
serializationProvider.Update(
implements: updatedImplements,
methods: [.. serializationProvider.Methods, ixmlSerializableWriteMethod]);
}

private static void UpdateWriteObjectValueMethod(ModelSerializationExtensionsDefinition type)
{
var writeObjectValueMethod = type.Methods
.FirstOrDefault(m => m.Signature.Name == WriteObjectValueMethodName &&
m.Signature.Parameters.Count >= 2 &&
m.Signature.Parameters[0].Type.Equals(typeof(XmlWriter)));

if (writeObjectValueMethod is null)
{
return;
}

var existingBody = writeObjectValueMethod.BodyStatements;
if (existingBody is null)
{
return;
}

// Add nameHint parameter to the method signature
var nameHintParam = new ParameterProvider(
"nameHint",
$"An optional name hint.",
new CSharpType(typeof(string),
isNullable: true),
DefaultOf(new CSharpType(typeof(string),
isNullable: true)));
var updatedParams = new List<ParameterProvider>(writeObjectValueMethod.Signature.Parameters) { nameHintParam };
writeObjectValueMethod.Signature.Update(parameters: updatedParams);

var writerParam = writeObjectValueMethod.Signature.Parameters[0];
var caseMatch = new DeclarationExpression(IXmlSerializableType, "xmlSerializable", out var xmlSerializableVar);
var caseBody = new MethodBodyStatements(
[
xmlSerializableVar.Invoke(WriteMethodName, [writerParam, nameHintParam]).Terminate(),
Break
]);
var ixmlSerializableCase = new SwitchCaseStatement(
caseMatch,
caseBody);

List<MethodBodyStatement> newBodyStatements = [];
bool caseAdded = false;

// Handle different body statement structures
IEnumerable<MethodBodyStatement> statements = existingBody switch
{
MethodBodyStatements methodBodyStatements => methodBodyStatements.Statements,
_ => [existingBody]
};

foreach (var statement in statements)
{
if (statement is SwitchStatement switchStatement && !caseAdded)
{
var newCases = new List<SwitchCaseStatement> { ixmlSerializableCase };
newCases.AddRange(switchStatement.Cases);

var newSwitchStatement = new SwitchStatement(switchStatement.MatchExpression, [.. newCases]);
newBodyStatements.Add(newSwitchStatement);
caseAdded = true;
}
else
{
newBodyStatements.Add(statement);
}
}

if (caseAdded)
{
writeObjectValueMethod.Update(bodyStatements: newBodyStatements);
}
}

private static void UpdateImplicitRequestContentOperatorForXmlOnly(TypeProvider serializationProvider, string xmlElementName)
{
var implicitOperator = serializationProvider.Methods
.FirstOrDefault(m => m.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Implicit) &&
m.Signature.ReturnType?.Equals(RequestContentType) == true &&
m.Signature.Parameters.Count == 1);

if (implicitOperator is null)
{
return;
}

var modelParameter = implicitOperator.Signature.Parameters[0];

// Build new method body:
// if (model == null) { return null; }
// var content = new XmlWriterContent();
// content.XmlWriter.WriteObjectValue(model, ModelSerializationExtensions.WireOptions, "XmlElementName");
// return content;
var newBody = new MethodBodyStatements(
[
new IfStatement(modelParameter.Equal(Null))
{
Return(Null)
},
Declare("content", typeof(XmlWriterContent), New.Instance(typeof(XmlWriterContent)), out var contentVar),
contentVar.As<XmlWriterContent>().XmlWriter().Invoke(
WriteObjectValueMethodName,
[modelParameter, Static<ModelSerializationExtensionsDefinition>().Property("WireOptions"), Literal(xmlElementName)]).Terminate(),
Return(contentVar)
]);

implicitOperator.Update(bodyStatements: newBody);
}

private static void UpdateImplicitRequestContentOperatorForJsonAndXml(TypeProvider serializationProvider)
{
// Find the implicit operator RequestContent method
var implicitOperator = serializationProvider.Methods
.FirstOrDefault(m => m.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Implicit) &&
m.Signature.ReturnType?.Equals(RequestContentType) == true &&
m.Signature.Parameters.Count == 1);

if (implicitOperator is null)
{
return;
}

var modelParameter = implicitOperator.Signature.Parameters[0];
var modelSerializationExtensions = Static<ModelSerializationExtensionsDefinition>();

var newBody = new MethodBodyStatements(
[
new IfStatement(modelParameter.Equal(Null))
{
Return(Null)
},
Return(Static(RequestContentType).Invoke(nameof(RequestContent.Create), [modelParameter, modelSerializationExtensions.Property("WireOptions")]))
]);

implicitOperator.Update(bodyStatements: newBody);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
if ((dualFormatModel == null))
{
return null;
}
return global::Azure.Core.RequestContent.Create(dualFormatModel, global::Samples.ModelSerializationExtensions.WireOptions);
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
this.Write(writer, global::Samples.ModelSerializationExtensions.WireOptions, nameHint)
Loading