From 20e466811a8801356e814a7751db9376f2086803 Mon Sep 17 00:00:00 2001 From: jolov Date: Sun, 13 Oct 2024 16:50:29 -0700 Subject: [PATCH 1/9] Add canonical view and fix customized struct serialization --- .../MrwSerializationTypeDefinition.cs | 2 +- .../ClientProviderCustomizationTests.cs | 9 +- .../CanReplaceStructMethod.cs | 0 .../CanReplaceStructMethod.cs | 23 +++ .../ModelCustomizationTests.cs | 1 - .../SerializationCustomizationTests.cs | 31 ++++ .../CanCustomizeExtensibleEnum(int32).cs | 145 ++++++++++++++++++ .../MockInputModel.cs | 17 ++ .../CanCustomizeExtensibleEnum(string).cs | 145 ++++++++++++++++++ .../MockInputModel.cs | 17 ++ .../src/Expressions/MemberExpression.cs | 3 +- .../src/Providers/CanonicalTypeProvider.cs | 134 ++++++++++++++++ .../src/Providers/ModelProvider.cs | 18 +-- .../src/Providers/NamedTypeSymbolProvider.cs | 10 +- .../src/Providers/PropertyProvider.cs | 4 +- .../src/Providers/TypeProvider.cs | 61 ++------ .../src/Utilities/TypeSymbolExtensions.cs | 6 +- .../Providers/CanonicalTypeProviderTests.cs | 99 ++++++++++++ .../NamedTypeSymbolProviderTests.cs | 111 +------------- .../test/TestHelpers/TestNamedSymbol.cs | 97 ++++++++++++ .../test/TestHelpers/TestPropertyType.cs | 20 +++ 21 files changed, 778 insertions(+), 175 deletions(-) rename packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderCustomizationTests/{CanReplaceStructMethod => CanReplaceStructMethod(False)}/CanReplaceStructMethod.cs (100%) create mode 100644 packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderCustomizationTests/CanReplaceStructMethod(True)/CanReplaceStructMethod.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/SerializationCustomizationTests/CanCustomizeExtensibleEnum(int32).cs create mode 100644 packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/SerializationCustomizationTests/CanCustomizeExtensibleEnum(int32)/MockInputModel.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/SerializationCustomizationTests/CanCustomizeExtensibleEnum(string).cs create mode 100644 packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/SerializationCustomizationTests/CanCustomizeExtensibleEnum(string)/MockInputModel.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/CanonicalTypeProvider.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Providers/CanonicalTypeProviderTests.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/TestHelpers/TestNamedSymbol.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/TestHelpers/TestPropertyType.cs diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs index 7399051b5e..62b4313382 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs @@ -1291,7 +1291,7 @@ private static MethodBodyStatement ThrowValidationFailException(ValueExpression /// private MethodBodyStatement[] CreateWritePropertiesStatements() { - var properties = _model.Properties.Concat(_model.CustomCodeView?.Properties.Where(p => p.WireInfo != null) ?? []); + var properties = _model.CanonicalView!.Properties; List propertyStatements = new(); foreach (var property in properties) { diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/ClientProviders/ClientProviderCustomizationTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/ClientProviders/ClientProviderCustomizationTests.cs index 46e74868ac..ac529cee6f 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/ClientProviders/ClientProviderCustomizationTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/ClientProviders/ClientProviderCustomizationTests.cs @@ -153,7 +153,9 @@ public async Task CanReplaceOpMethod() // Validates that a method with a struct parameter can be replaced [Test] - public async Task CanReplaceStructMethod() + [TestCase(true)] + [TestCase(false)] + public async Task CanReplaceStructMethod(bool isStructCustomized) { var inputOperation = InputFactory.Operation("HelloAgain", parameters: [ @@ -162,7 +164,7 @@ public async Task CanReplaceStructMethod() var inputClient = InputFactory.Client("TestClient", operations: [inputOperation]); var plugin = await MockHelpers.LoadMockPluginAsync( clients: () => [inputClient], - compilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync(isStructCustomized.ToString())); // Find the client provider var clientProvider = plugin.Object.OutputLibrary.TypeProviders.SingleOrDefault(t => t is ClientProvider); @@ -188,7 +190,8 @@ public async Task CanReplaceStructMethod() var customMethodParams = customMethods[0].Signature.Parameters; Assert.AreEqual(1, customMethodParams.Count); Assert.AreEqual("p1", customMethodParams[0].Name); - Assert.AreEqual("MyStruct", customMethodParams[0].Type.Name); + Assert.AreEqual(isStructCustomized ? "Sample.TestClient.MyStruct" : "MyStruct", customMethodParams[0].Type.Name); + Assert.IsTrue(customMethodParams[0].Type.IsStruct); Assert.IsTrue(customMethodParams[0].Type.IsNullable); } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderCustomizationTests/CanReplaceStructMethod/CanReplaceStructMethod.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderCustomizationTests/CanReplaceStructMethod(False)/CanReplaceStructMethod.cs similarity index 100% rename from packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderCustomizationTests/CanReplaceStructMethod/CanReplaceStructMethod.cs rename to packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderCustomizationTests/CanReplaceStructMethod(False)/CanReplaceStructMethod.cs diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderCustomizationTests/CanReplaceStructMethod(True)/CanReplaceStructMethod.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderCustomizationTests/CanReplaceStructMethod(True)/CanReplaceStructMethod.cs new file mode 100644 index 0000000000..cc6387df2e --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderCustomizationTests/CanReplaceStructMethod(True)/CanReplaceStructMethod.cs @@ -0,0 +1,23 @@ + +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Sample +{ + /// + public partial class TestClient + { + public virtual ClientResult HelloAgain(MyStruct? p1) + { + + } + + public readonly partial struct MyStruct + { + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/ModelCustomizationTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/ModelCustomizationTests.cs index 3191a5287b..2e6f620b5b 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/ModelCustomizationTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/ModelCustomizationTests.cs @@ -35,7 +35,6 @@ public async Task CanChangePropertyName() // validate the methods use the custom member name var writer = new TypeProviderWriter(modelProvider); var file = writer.Write(); - var expected = Helpers.GetExpectedFromFile(); Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/SerializationCustomizationTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/SerializationCustomizationTests.cs index 3f8ff4525e..bdb970db0a 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/SerializationCustomizationTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/SerializationCustomizationTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.Generator.CSharp.ClientModel.Providers; @@ -180,6 +181,36 @@ public async Task CanChangeDictionaryToBinaryData() Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); } + private static IEnumerable ExtensibleEnumCases => + [ + new TestCaseData(InputPrimitiveType.String), + new TestCaseData(InputPrimitiveType.Int32), + ]; + + [Test] + [TestCaseSource(nameof(ExtensibleEnumCases))] + public async Task CanCustomizeExtensibleEnum(InputPrimitiveType enumType) + { + var props = new[] + { + InputFactory.Property("Prop1", InputFactory.Enum("EnumType", enumType, isExtensible: true)) + }; + + var inputModel = InputFactory.Model("mockInputModel", properties: props, usage: InputModelTypeUsage.Json); + var plugin = await MockHelpers.LoadMockPluginAsync( + inputModels: () => [inputModel], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync(enumType.Name)); + + var modelProvider = plugin.Object.OutputLibrary.TypeProviders.Single(t => t is ModelProvider); + var serializationProvider = modelProvider.SerializationProviders.Single(t => t is MrwSerializationTypeDefinition); + Assert.IsNotNull(serializationProvider); + Assert.AreEqual(0, serializationProvider.Fields.Count); + + var writer = new TypeProviderWriter(serializationProvider); + var file = writer.Write(); + Assert.AreEqual(Helpers.GetExpectedFromFile(enumType.Name), file.Content); + } + [Test] public async Task CanReplaceSerializationMethod() { diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/SerializationCustomizationTests/CanCustomizeExtensibleEnum(int32).cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/SerializationCustomizationTests/CanCustomizeExtensibleEnum(int32).cs new file mode 100644 index 0000000000..4032f62c2b --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/SerializationCustomizationTests/CanCustomizeExtensibleEnum(int32).cs @@ -0,0 +1,145 @@ +// + +#nullable disable + +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.Text.Json; +using Sample; + +namespace Sample.Models +{ + /// + public partial class MockInputModel : global::System.ClientModel.Primitives.IJsonModel + { + void global::System.ClientModel.Primitives.IJsonModel.Write(global::System.Text.Json.Utf8JsonWriter writer, global::System.ClientModel.Primitives.ModelReaderWriterOptions options) + { + writer.WriteStartObject(); + this.JsonModelWriteCore(writer, options); + writer.WriteEndObject(); + } + + /// The JSON writer. + /// The client options for reading and writing models. + protected virtual void JsonModelWriteCore(global::System.Text.Json.Utf8JsonWriter writer, global::System.ClientModel.Primitives.ModelReaderWriterOptions options) + { + string format = (options.Format == "W") ? ((global::System.ClientModel.Primitives.IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + if ((format != "J")) + { + throw new global::System.FormatException($"The model {nameof(global::Sample.Models.MockInputModel)} does not support writing '{format}' format."); + } + writer.WritePropertyName("prop1"u8); + writer.WriteNumberValue(Prop1.ToSerialInt32()); + if (((options.Format != "W") && (_additionalBinaryDataProperties != null))) + { + foreach (var item in _additionalBinaryDataProperties) + { + writer.WritePropertyName(item.Key); +#if NET6_0_OR_GREATER + writer.WriteRawValue(item.Value); +#else + using (global::System.Text.Json.JsonDocument document = global::System.Text.Json.JsonDocument.Parse(item.Value)) + { + global::System.Text.Json.JsonSerializer.Serialize(writer, document.RootElement); + } +#endif + } + } + } + + global::Sample.Models.MockInputModel global::System.ClientModel.Primitives.IJsonModel.Create(ref global::System.Text.Json.Utf8JsonReader reader, global::System.ClientModel.Primitives.ModelReaderWriterOptions options) => ((global::Sample.Models.MockInputModel)this.JsonModelCreateCore(ref reader, options)); + + /// The JSON reader. + /// The client options for reading and writing models. + protected virtual global::Sample.Models.MockInputModel JsonModelCreateCore(ref global::System.Text.Json.Utf8JsonReader reader, global::System.ClientModel.Primitives.ModelReaderWriterOptions options) + { + string format = (options.Format == "W") ? ((global::System.ClientModel.Primitives.IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + if ((format != "J")) + { + throw new global::System.FormatException($"The model {nameof(global::Sample.Models.MockInputModel)} does not support reading '{format}' format."); + } + using global::System.Text.Json.JsonDocument document = global::System.Text.Json.JsonDocument.ParseValue(ref reader); + return global::Sample.Models.MockInputModel.DeserializeMockInputModel(document.RootElement, options); + } + + internal static global::Sample.Models.MockInputModel DeserializeMockInputModel(global::System.Text.Json.JsonElement element, global::System.ClientModel.Primitives.ModelReaderWriterOptions options) + { + if ((element.ValueKind == global::System.Text.Json.JsonValueKind.Null)) + { + return null; + } + global::Sample.Models.EnumType prop1 = default; + global::System.Collections.Generic.IDictionary additionalBinaryDataProperties = new global::Sample.ChangeTrackingDictionary(); + foreach (var prop in element.EnumerateObject()) + { + if (prop.NameEquals("prop1"u8)) + { + if ((prop.Value.ValueKind == global::System.Text.Json.JsonValueKind.Null)) + { + prop1 = null; + continue; + } + prop1 = new global::Sample.Models.EnumType(prop.Value.GetInt32()); + continue; + } + if ((options.Format != "W")) + { + additionalBinaryDataProperties.Add(prop.Name, global::System.BinaryData.FromString(prop.Value.GetRawText())); + } + } + return new global::Sample.Models.MockInputModel(prop1, additionalBinaryDataProperties); + } + + global::System.BinaryData global::System.ClientModel.Primitives.IPersistableModel.Write(global::System.ClientModel.Primitives.ModelReaderWriterOptions options) => this.PersistableModelWriteCore(options); + + /// The client options for reading and writing models. + protected virtual global::System.BinaryData PersistableModelWriteCore(global::System.ClientModel.Primitives.ModelReaderWriterOptions options) + { + string format = (options.Format == "W") ? ((global::System.ClientModel.Primitives.IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + switch (format) + { + case "J": + return global::System.ClientModel.Primitives.ModelReaderWriter.Write(this, options); + default: + throw new global::System.FormatException($"The model {nameof(global::Sample.Models.MockInputModel)} does not support writing '{options.Format}' format."); + } + } + + global::Sample.Models.MockInputModel global::System.ClientModel.Primitives.IPersistableModel.Create(global::System.BinaryData data, global::System.ClientModel.Primitives.ModelReaderWriterOptions options) => ((global::Sample.Models.MockInputModel)this.PersistableModelCreateCore(data, options)); + + /// The data to parse. + /// The client options for reading and writing models. + protected virtual global::Sample.Models.MockInputModel PersistableModelCreateCore(global::System.BinaryData data, global::System.ClientModel.Primitives.ModelReaderWriterOptions options) + { + string format = (options.Format == "W") ? ((global::System.ClientModel.Primitives.IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + switch (format) + { + case "J": + using (global::System.Text.Json.JsonDocument document = global::System.Text.Json.JsonDocument.Parse(data)) + { + return global::Sample.Models.MockInputModel.DeserializeMockInputModel(document.RootElement, options); + } + default: + throw new global::System.FormatException($"The model {nameof(global::Sample.Models.MockInputModel)} does not support reading '{options.Format}' format."); + } + } + + string global::System.ClientModel.Primitives.IPersistableModel.GetFormatFromOptions(global::System.ClientModel.Primitives.ModelReaderWriterOptions options) => "J"; + + /// The to serialize into . + public static implicit operator BinaryContent(global::Sample.Models.MockInputModel mockInputModel) + { + return global::System.ClientModel.BinaryContent.Create(mockInputModel, global::Sample.ModelSerializationExtensions.WireOptions); + } + + /// The to deserialize the from. + public static explicit operator MockInputModel(global::System.ClientModel.ClientResult result) + { + using global::System.ClientModel.Primitives.PipelineResponse response = result.GetRawResponse(); + using global::System.Text.Json.JsonDocument document = global::System.Text.Json.JsonDocument.Parse(response.Content); + return global::Sample.Models.MockInputModel.DeserializeMockInputModel(document.RootElement, global::Sample.ModelSerializationExtensions.WireOptions); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/SerializationCustomizationTests/CanCustomizeExtensibleEnum(int32)/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/SerializationCustomizationTests/CanCustomizeExtensibleEnum(int32)/MockInputModel.cs new file mode 100644 index 0000000000..a27e763526 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/SerializationCustomizationTests/CanCustomizeExtensibleEnum(int32)/MockInputModel.cs @@ -0,0 +1,17 @@ +#nullable disable + +using Microsoft.Generator.CSharp.Customization; +using Microsoft.Generator.CSharp.Primitives; + +namespace Sample.Models +{ + public partial class MockInputModel + { + public EnumType Prop1 { get; set; } + } + + public readonly partial struct EnumType + { + public static EnumType Foo = new EnumType(1); + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/SerializationCustomizationTests/CanCustomizeExtensibleEnum(string).cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/SerializationCustomizationTests/CanCustomizeExtensibleEnum(string).cs new file mode 100644 index 0000000000..68d078fc24 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/SerializationCustomizationTests/CanCustomizeExtensibleEnum(string).cs @@ -0,0 +1,145 @@ +// + +#nullable disable + +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.Text.Json; +using Sample; + +namespace Sample.Models +{ + /// + public partial class MockInputModel : global::System.ClientModel.Primitives.IJsonModel + { + void global::System.ClientModel.Primitives.IJsonModel.Write(global::System.Text.Json.Utf8JsonWriter writer, global::System.ClientModel.Primitives.ModelReaderWriterOptions options) + { + writer.WriteStartObject(); + this.JsonModelWriteCore(writer, options); + writer.WriteEndObject(); + } + + /// The JSON writer. + /// The client options for reading and writing models. + protected virtual void JsonModelWriteCore(global::System.Text.Json.Utf8JsonWriter writer, global::System.ClientModel.Primitives.ModelReaderWriterOptions options) + { + string format = (options.Format == "W") ? ((global::System.ClientModel.Primitives.IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + if ((format != "J")) + { + throw new global::System.FormatException($"The model {nameof(global::Sample.Models.MockInputModel)} does not support writing '{format}' format."); + } + writer.WritePropertyName("prop1"u8); + writer.WriteStringValue(Prop1.ToString()); + if (((options.Format != "W") && (_additionalBinaryDataProperties != null))) + { + foreach (var item in _additionalBinaryDataProperties) + { + writer.WritePropertyName(item.Key); +#if NET6_0_OR_GREATER + writer.WriteRawValue(item.Value); +#else + using (global::System.Text.Json.JsonDocument document = global::System.Text.Json.JsonDocument.Parse(item.Value)) + { + global::System.Text.Json.JsonSerializer.Serialize(writer, document.RootElement); + } +#endif + } + } + } + + global::Sample.Models.MockInputModel global::System.ClientModel.Primitives.IJsonModel.Create(ref global::System.Text.Json.Utf8JsonReader reader, global::System.ClientModel.Primitives.ModelReaderWriterOptions options) => ((global::Sample.Models.MockInputModel)this.JsonModelCreateCore(ref reader, options)); + + /// The JSON reader. + /// The client options for reading and writing models. + protected virtual global::Sample.Models.MockInputModel JsonModelCreateCore(ref global::System.Text.Json.Utf8JsonReader reader, global::System.ClientModel.Primitives.ModelReaderWriterOptions options) + { + string format = (options.Format == "W") ? ((global::System.ClientModel.Primitives.IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + if ((format != "J")) + { + throw new global::System.FormatException($"The model {nameof(global::Sample.Models.MockInputModel)} does not support reading '{format}' format."); + } + using global::System.Text.Json.JsonDocument document = global::System.Text.Json.JsonDocument.ParseValue(ref reader); + return global::Sample.Models.MockInputModel.DeserializeMockInputModel(document.RootElement, options); + } + + internal static global::Sample.Models.MockInputModel DeserializeMockInputModel(global::System.Text.Json.JsonElement element, global::System.ClientModel.Primitives.ModelReaderWriterOptions options) + { + if ((element.ValueKind == global::System.Text.Json.JsonValueKind.Null)) + { + return null; + } + global::Sample.Models.EnumType prop1 = default; + global::System.Collections.Generic.IDictionary additionalBinaryDataProperties = new global::Sample.ChangeTrackingDictionary(); + foreach (var prop in element.EnumerateObject()) + { + if (prop.NameEquals("prop1"u8)) + { + if ((prop.Value.ValueKind == global::System.Text.Json.JsonValueKind.Null)) + { + prop1 = null; + continue; + } + prop1 = new global::Sample.Models.EnumType(prop.Value.GetString()); + continue; + } + if ((options.Format != "W")) + { + additionalBinaryDataProperties.Add(prop.Name, global::System.BinaryData.FromString(prop.Value.GetRawText())); + } + } + return new global::Sample.Models.MockInputModel(prop1, additionalBinaryDataProperties); + } + + global::System.BinaryData global::System.ClientModel.Primitives.IPersistableModel.Write(global::System.ClientModel.Primitives.ModelReaderWriterOptions options) => this.PersistableModelWriteCore(options); + + /// The client options for reading and writing models. + protected virtual global::System.BinaryData PersistableModelWriteCore(global::System.ClientModel.Primitives.ModelReaderWriterOptions options) + { + string format = (options.Format == "W") ? ((global::System.ClientModel.Primitives.IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + switch (format) + { + case "J": + return global::System.ClientModel.Primitives.ModelReaderWriter.Write(this, options); + default: + throw new global::System.FormatException($"The model {nameof(global::Sample.Models.MockInputModel)} does not support writing '{options.Format}' format."); + } + } + + global::Sample.Models.MockInputModel global::System.ClientModel.Primitives.IPersistableModel.Create(global::System.BinaryData data, global::System.ClientModel.Primitives.ModelReaderWriterOptions options) => ((global::Sample.Models.MockInputModel)this.PersistableModelCreateCore(data, options)); + + /// The data to parse. + /// The client options for reading and writing models. + protected virtual global::Sample.Models.MockInputModel PersistableModelCreateCore(global::System.BinaryData data, global::System.ClientModel.Primitives.ModelReaderWriterOptions options) + { + string format = (options.Format == "W") ? ((global::System.ClientModel.Primitives.IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + switch (format) + { + case "J": + using (global::System.Text.Json.JsonDocument document = global::System.Text.Json.JsonDocument.Parse(data)) + { + return global::Sample.Models.MockInputModel.DeserializeMockInputModel(document.RootElement, options); + } + default: + throw new global::System.FormatException($"The model {nameof(global::Sample.Models.MockInputModel)} does not support reading '{options.Format}' format."); + } + } + + string global::System.ClientModel.Primitives.IPersistableModel.GetFormatFromOptions(global::System.ClientModel.Primitives.ModelReaderWriterOptions options) => "J"; + + /// The to serialize into . + public static implicit operator BinaryContent(global::Sample.Models.MockInputModel mockInputModel) + { + return global::System.ClientModel.BinaryContent.Create(mockInputModel, global::Sample.ModelSerializationExtensions.WireOptions); + } + + /// The to deserialize the from. + public static explicit operator MockInputModel(global::System.ClientModel.ClientResult result) + { + using global::System.ClientModel.Primitives.PipelineResponse response = result.GetRawResponse(); + using global::System.Text.Json.JsonDocument document = global::System.Text.Json.JsonDocument.Parse(response.Content); + return global::Sample.Models.MockInputModel.DeserializeMockInputModel(document.RootElement, global::Sample.ModelSerializationExtensions.WireOptions); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/SerializationCustomizationTests/CanCustomizeExtensibleEnum(string)/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/SerializationCustomizationTests/CanCustomizeExtensibleEnum(string)/MockInputModel.cs new file mode 100644 index 0000000000..93fccae2ce --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/SerializationCustomizationTests/CanCustomizeExtensibleEnum(string)/MockInputModel.cs @@ -0,0 +1,17 @@ +#nullable disable + +using Microsoft.Generator.CSharp.Customization; +using Microsoft.Generator.CSharp.Primitives; + +namespace Sample.Models +{ + public partial class MockInputModel + { + public EnumType Prop1 { get; set; } + } + + public readonly partial struct EnumType + { + public static EnumType Foo = new EnumType("Foo"); + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/MemberExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/MemberExpression.cs index fb36ee9e55..e53cecca15 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/MemberExpression.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/MemberExpression.cs @@ -12,7 +12,8 @@ internal override void Write(CodeWriter writer) Inner.Write(writer); writer.AppendRaw("."); } - writer.AppendRaw(MemberName); + // workaround to avoid Roslyn reducing properties named Object to object + writer.AppendRaw(MemberName == "Object" && Inner == null ? $"this.{MemberName}" : MemberName); } } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/CanonicalTypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/CanonicalTypeProvider.cs new file mode 100644 index 0000000000..1614d32971 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/CanonicalTypeProvider.cs @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Generator.CSharp.Primitives; +using Microsoft.Generator.CSharp.SourceInput; + +namespace Microsoft.Generator.CSharp.Providers +{ + internal class CanonicalTypeProvider : TypeProvider + { + private readonly TypeProvider _generatedTypeProvider; + + public CanonicalTypeProvider(TypeProvider generatedTypeProvider) + { + _generatedTypeProvider = generatedTypeProvider; + } + protected override string BuildRelativeFilePath() => throw new InvalidOperationException("This type should not be writing in generation"); + + protected override string BuildName() => _generatedTypeProvider.Name; + + protected override string GetNamespace() => _generatedTypeProvider.Namespace; + + protected override TypeSignatureModifiers GetDeclarationModifiers() => _generatedTypeProvider.DeclarationModifiers; + + private protected override PropertyProvider[] FilterCustomizedProperties(PropertyProvider[] canonicalProperties) => canonicalProperties; + + private protected override CanonicalTypeProvider? GetCanonicalView() => null; + + protected override PropertyProvider[] BuildProperties() + { + var specProperties = _generatedTypeProvider.SpecProperties; + var specPropertiesMap = specProperties.ToDictionary(p => p.Name, p => p); + var generatedProperties = _generatedTypeProvider.Properties; + var customProperties = _generatedTypeProvider.CustomCodeView?.Properties ?? []; + + Dictionary serializedNameMapping = BuildSerializationNameMap(); + + // Update the serializedName of generated properties if necessary + foreach (var generatedProperty in generatedProperties) + { + if (serializedNameMapping.TryGetValue(generatedProperty.Name, out var serializedName)) + { + generatedProperty.WireInfo!.SerializedName = serializedName; + } + } + + Dictionary specToCustomPropertiesMap = BuildSpecToCustomPropertyMap(customProperties, specPropertiesMap); + + foreach (var customProperty in customProperties) + { + PropertyProvider? specProperty = null; + + if (((customProperty.OriginalName != null && specPropertiesMap.TryGetValue(customProperty.OriginalName, out var candidateSpecProperty)) + || specPropertiesMap.TryGetValue(customProperty.Name, out candidateSpecProperty)) + // Ensure that the spec property is mapped to this custom property + && specToCustomPropertiesMap[candidateSpecProperty] == customProperty) + { + specProperty = candidateSpecProperty; + var serializedName = specProperty.WireInfo!.SerializedName; + + // Update the serializedName of custom properties if necessary + if (serializedNameMapping.TryGetValue(customProperty.Name, out var customSerializedName) || + (customProperty.OriginalName != null && serializedNameMapping.TryGetValue(customProperty.OriginalName, out customSerializedName))) + { + serializedName = customSerializedName; + } + + customProperty.WireInfo = specProperty.WireInfo; + customProperty.WireInfo!.SerializedName = serializedName; + } + + // handle customized extensible enums, since the custom type would not be an enum, but the spec type would be an enum + if (specProperty?.Type is { IsEnum: true, IsStruct: true }) + { + customProperty.Type = new CSharpType( + customProperty.Type.Name, + customProperty.Type.Namespace, + customProperty.Type.IsValueType, + customProperty.Type.IsNullable, + customProperty.Type.DeclaringType, + customProperty.Type.Arguments, + customProperty.Type.IsPublic, + customProperty.Type.IsStruct, + customProperty.Type.BaseType, + specProperty.Type.UnderlyingEnumType); + } + } + + return [..generatedProperties, ..customProperties]; + } + + private static Dictionary BuildSpecToCustomPropertyMap( + IReadOnlyList customProperties, + Dictionary specPropertiesMap) + { + var specToCustomPropertiesMap = new Dictionary(); + // Create a map from spec properties to custom properties so that we know which custom properties are replacing spec properties + foreach (var customProperty in customProperties) + { + if ((customProperty.OriginalName != null && specPropertiesMap.TryGetValue(customProperty.OriginalName, out var specProperty)) + || specPropertiesMap.TryGetValue(customProperty.Name, out specProperty)) + { + // If the spec property is not already mapped to a custom property, map it to this custom property + specToCustomPropertiesMap.TryAdd(specProperty, customProperty); + } + } + return specToCustomPropertiesMap; + } + + private Dictionary BuildSerializationNameMap() + { + var serializationAttributes = _generatedTypeProvider.CustomCodeView?.GetAttributes(). + Where(a => a.AttributeClass?.Name == CodeGenAttributes.CodeGenSerializationAttributeName) ?? []; + var serializedNameMapping = new Dictionary(); + foreach (var serializationAttribute in serializationAttributes) + { + if (CodeGenAttributes.TryGetCodeGenSerializationAttributeValue( + serializationAttribute, + out var propertyName, + out string? serializationName, + out _, + out _, + out _) && serializationName != null) + { + serializedNameMapping[propertyName] = serializationName; + } + } + return serializedNameMapping; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/ModelProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/ModelProvider.cs index 030367ebff..5b97fb5739 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/ModelProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/ModelProvider.cs @@ -84,8 +84,6 @@ public ModelProvider? BaseModelProvider internal bool SupportsBinaryDataAdditionalProperties => AdditionalPropertyProperties.Any(p => p.Type.ElementType.Equals(_additionalPropsUnknownType)); public ConstructorProvider FullConstructor => _fullConstructor ??= BuildFullConstructor(); - internal IReadOnlyList AllSpecProperties => Properties.Concat(CustomCodeView?.Properties.Where(p => p.WireInfo != null) ?? []).ToList(); - protected override string GetNamespace() => CodeModelPlugin.Instance.Configuration.ModelNamespace; protected override CSharpType? GetBaseType() @@ -458,14 +456,14 @@ private ConstructorProvider BuildFullConstructor() if (isPrimaryConstructor) { // the primary ctor should only include the properties of the direct base model - baseProperties = BaseModelProvider?.Properties ?? []; + baseProperties = BaseModelProvider?.CanonicalView!.Properties ?? []; } else if (BaseModelProvider?.FullConstructor.Signature != null) { baseParameters.AddRange(BaseModelProvider.FullConstructor.Signature.Parameters); } - HashSet overriddenProperties = AllSpecProperties.Where(p => p.BaseProperty is not null).Select(p => p.BaseProperty!).ToHashSet(); + HashSet overriddenProperties = CanonicalView!.Properties.Where(p => p.BaseProperty is not null).Select(p => p.BaseProperty!).ToHashSet(); // add the base parameters, if any foreach (var property in baseProperties) @@ -476,7 +474,7 @@ private ConstructorProvider BuildFullConstructor() // construct the initializer using the parameters from base signature var constructorInitializer = new ConstructorInitializer(true, [.. baseParameters.Select(p => GetExpressionForCtor(p, overriddenProperties, isPrimaryConstructor))]); - foreach (var property in AllSpecProperties) + foreach (var property in CanonicalView.Properties) { AddInitializationParameterForCtor(constructorParameters, property, Type.IsStruct, isPrimaryConstructor); } @@ -507,7 +505,7 @@ p.Property is null { if (_inputModel.BaseModel is not null && _inputModel.DiscriminatorValue is not null) { - var discriminator = BaseModelProvider?.Properties.Where(p => p.IsDiscriminator).FirstOrDefault(); + var discriminator = BaseModelProvider?.CanonicalView!.Properties.Where(p => p.IsDiscriminator).FirstOrDefault(); if (discriminator != null) { var type = discriminator.Type; @@ -517,12 +515,12 @@ p.Property is null } else { - if (!type.IsFrameworkType && type.IsEnum) + if (!type.IsFrameworkType && type.IsEnum && _inputModel.BaseModel.DiscriminatorProperty!.Type is InputEnumType inputEnumType) { /* TODO: when customize the discriminator type to a enum, then we may not be able to get the correct TypeProvider in this way. * We will handle this when issue https://github.com/microsoft/typespec/issues/4313 is resolved. * */ - var discriminatorProvider = CodeModelPlugin.Instance.TypeFactory.CreateEnum(enumType: (InputEnumType)_inputModel.BaseModel.DiscriminatorProperty!.Type); + var discriminatorProvider = CodeModelPlugin.Instance.TypeFactory.CreateEnum(enumType: inputEnumType); var enumMember = discriminatorProvider!.EnumValues.FirstOrDefault(e => e.Value.ToString() == _inputModel.DiscriminatorValue) ?? throw new InvalidOperationException($"invalid discriminator value {_inputModel.DiscriminatorValue}"); /* {KindType}.{enumMember} */ return TypeReferenceExpression.FromType(type).Property(enumMember.Name); @@ -618,10 +616,10 @@ private MethodBodyStatement GetPropertyInitializers( bool isPrimaryConstructor, IReadOnlyList? parameters = null) { - List methodBodyStatements = new(Properties.Count + 1); + List methodBodyStatements = new(CanonicalView!.Properties.Count + 1); Dictionary parameterMap = parameters?.ToDictionary(p => p.Name) ?? []; - foreach (var property in AllSpecProperties) + foreach (var property in CanonicalView.Properties) { // skip those non-spec properties if (property.WireInfo == null) diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/NamedTypeSymbolProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/NamedTypeSymbolProvider.cs index dc5a15b153..0fb6f041aa 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/NamedTypeSymbolProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/NamedTypeSymbolProvider.cs @@ -8,6 +8,7 @@ using System.Xml.Linq; using Microsoft.CodeAnalysis; using Microsoft.Generator.CSharp.Primitives; +using Microsoft.Generator.CSharp.SourceInput; using Microsoft.Generator.CSharp.Statements; namespace Microsoft.Generator.CSharp.Providers @@ -108,6 +109,13 @@ protected override PropertyProvider[] BuildProperties() List properties = new List(); foreach (var propertySymbol in _namedTypeSymbol.GetMembers().OfType()) { + var codeGenAttribute = propertySymbol.GetAttributes().SingleOrDefault( + a => a.AttributeClass?.Name == CodeGenAttributes.CodeGenMemberAttributeName); + string? originalName = null; + if (codeGenAttribute != null) + { + CodeGenAttributes.TryGetCodeGenMemberAttributeValue(codeGenAttribute, out originalName); + } var propertyProvider = new PropertyProvider( GetSymbolXmlDoc(propertySymbol, "summary"), GetAccessModifier(propertySymbol.DeclaredAccessibility), @@ -116,7 +124,7 @@ protected override PropertyProvider[] BuildProperties() new AutoPropertyBody(propertySymbol.SetMethod is not null), this) { - Attributes = propertySymbol.GetAttributes() + OriginalName = originalName }; properties.Add(propertyProvider); } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/PropertyProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/PropertyProvider.cs index d56284fbce..8b2ab9480f 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/PropertyProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/PropertyProvider.cs @@ -23,7 +23,7 @@ public class PropertyProvider public FormattableString Description { get; } public XmlDocSummaryStatement XmlDocSummary { get; } public MethodSignatureModifiers Modifiers { get; internal set; } - public CSharpType Type { get; } + public CSharpType Type { get; internal set; } public string Name { get; internal set; } public PropertyBody Body { get; internal set; } public CSharpType? ExplicitInterface { get; } @@ -42,7 +42,7 @@ public class PropertyProvider public TypeProvider EnclosingType { get; } - internal IEnumerable? Attributes { get; init; } + internal string? OriginalName { get; init; } // for mocking #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/TypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/TypeProvider.cs index 996b686736..cf6319f6bd 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/TypeProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/TypeProvider.cs @@ -16,10 +16,12 @@ namespace Microsoft.Generator.CSharp.Providers public abstract class TypeProvider { private Lazy _customCodeView; + private Lazy _canonicalView; protected TypeProvider() { _customCodeView = new(GetCustomCodeView); + _canonicalView = new(GetCanonicalView); } private protected virtual NamedTypeSymbolProvider? GetCustomCodeView() @@ -27,6 +29,9 @@ protected TypeProvider() public NamedTypeSymbolProvider? CustomCodeView => _customCodeView.Value; + private protected virtual CanonicalTypeProvider? GetCanonicalView() => new CanonicalTypeProvider(this); + public TypeProvider? CanonicalView => _canonicalView.Value; + protected string? _deprecated; /// @@ -130,7 +135,10 @@ private TypeSignatureModifiers GetDeclarationModifiersInternal() public IReadOnlyList Implements => _implements ??= BuildImplements(); private IReadOnlyList? _properties; - public IReadOnlyList Properties => _properties ??= BuildPropertiesInternal(); + private PropertyProvider[]? _specProperties; + + internal PropertyProvider[] SpecProperties => _specProperties ??= BuildProperties(); + public IReadOnlyList Properties => _properties ??= FilterCustomizedProperties(SpecProperties); private IReadOnlyList? _methods; public IReadOnlyList Methods => _methods ??= BuildMethodsInternal(); @@ -154,7 +162,7 @@ private TypeSignatureModifiers GetDeclarationModifiersInternal() protected virtual CSharpType[] GetTypeArguments() => []; - private PropertyProvider[] BuildPropertiesInternal() + private protected virtual PropertyProvider[] FilterCustomizedProperties(PropertyProvider[] specProperties) { var properties = new List(); var customProperties = new Dictionary(); @@ -162,15 +170,12 @@ private PropertyProvider[] BuildPropertiesInternal() foreach (var customProperty in CustomCodeView?.Properties ?? []) { customProperties.Add(customProperty.Name, customProperty); - foreach (var attribute in customProperty.Attributes ?? []) + if (customProperty.OriginalName != null) { - if (CodeGenAttributes.TryGetCodeGenMemberAttributeValue(attribute, out var originalName)) - { - renamedProperties.Add(originalName, customProperty); - } + renamedProperties.Add(customProperty.OriginalName, customProperty); } } - foreach (var property in BuildProperties()) + foreach (var property in specProperties) { if (ShouldGenerate(property, customProperties, renamedProperties)) { @@ -178,7 +183,7 @@ private PropertyProvider[] BuildPropertiesInternal() } } - return properties.ToArray(); + return [..properties]; } private MethodProvider[] BuildMethodsInternal() @@ -192,7 +197,7 @@ private MethodProvider[] BuildMethodsInternal() } } - return methods.ToArray(); + return [..methods]; } private ConstructorProvider[] BuildConstructorsInternal() @@ -361,43 +366,9 @@ private bool ShouldGenerate(PropertyProvider property, IDictionary "[" + GetFullyQualifiedName(arg) + "]")]; @@ -164,7 +166,7 @@ private static CSharpType ConstructCSharpTypeFromSymbol( var typeArg = namedTypeSymbol?.TypeArguments.FirstOrDefault(); bool isValueType = typeSymbol.IsValueType; bool isEnum = typeSymbol.TypeKind == TypeKind.Enum; - bool isNullable = typeSymbol.NullableAnnotation == NullableAnnotation.Annotated; + bool isNullable = fullyQualifiedName.StartsWith("System.Nullable`1"); bool isNullableUnknownType = isNullable && typeArg?.TypeKind == TypeKind.Error; string name = isNullableUnknownType ? fullyQualifiedName : typeSymbol.Name; string[] pieces = fullyQualifiedName.Split('.'); diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Providers/CanonicalTypeProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Providers/CanonicalTypeProviderTests.cs new file mode 100644 index 0000000000..83f0a03321 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Providers/CanonicalTypeProviderTests.cs @@ -0,0 +1,99 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.Generator.CSharp.Input; +using Microsoft.Generator.CSharp.Primitives; +using Microsoft.Generator.CSharp.Providers; +using Microsoft.Generator.CSharp.Tests.Providers.NamedTypeSymbolProviders; +using NUnit.Framework; + +namespace Microsoft.Generator.CSharp.Tests.Providers +{ + public class CanonicalTypeProviderTests + { + private NamedTypeSymbolProvider _namedTypeSymbolProvider; + private NamedSymbol _namedSymbol; + private readonly TestTypeProvider _typeProvider; + private readonly Compilation _compilation; + + public CanonicalTypeProviderTests() + { + _namedSymbol = new NamedSymbol(name: "TestName"); + _compilation = CompilationHelper.LoadCompilation([_namedSymbol, new PropertyType()]); + var iNamedSymbol = CompilationHelper.GetSymbol(_compilation.Assembly.Modules.First().GlobalNamespace, "TestName"); + + _namedTypeSymbolProvider = new NamedTypeSymbolProvider(iNamedSymbol!); + _typeProvider = new TestTypeProvider(); + } + + [SetUp] + public async Task Setup() + { + await MockHelpers.LoadMockPluginAsync(compilation: () => Task.FromResult(_compilation)); + } + + [Test] + public void ValidateModifiers() + { + var modifiers = _typeProvider.CanonicalView!.DeclarationModifiers; + Assert.IsTrue(modifiers.HasFlag(TypeSignatureModifiers.Internal | TypeSignatureModifiers.Partial | TypeSignatureModifiers.Class)); + } + + [Test] + public void ValidateName() + { + Assert.AreEqual(_typeProvider.Name, _typeProvider.CanonicalView!.Name); + } + + [Test] + public void ValidateNamespace() + { + Assert.AreEqual(_typeProvider.Namespace, _typeProvider.CanonicalView!.Namespace); + } + + [Test] + public void ValidateProperties() + { + Dictionary properties = _typeProvider.CanonicalView!.Properties.ToDictionary(p => p.Name); + Assert.AreEqual(5, properties.Count); + Assert.AreEqual(1, _typeProvider.Properties.Count); + Assert.AreEqual(4, _typeProvider.CustomCodeView!.Properties.Count); + foreach (var expected in _namedSymbol.Properties) + { + var actual = properties[expected.Name]; + + Assert.IsTrue(properties.ContainsKey(expected.Name)); + Assert.AreEqual(expected.Name, actual.Name); + Assert.AreEqual($"{expected.Description}.", actual.Description.ToString()); // the writer adds a period + Assert.AreEqual(expected.Modifiers, actual.Modifiers); + Assert.AreEqual(expected.Type, actual.Type); + Assert.AreEqual(expected.Body.GetType(), actual.Body.GetType()); + Assert.AreEqual(expected.Body.HasSetter, actual.Body.HasSetter); + } + } + + + private class TestTypeProvider : TypeProvider + { + protected override string BuildRelativeFilePath() => "NamedSymbol"; + + protected override string BuildName() => "TestName"; + + protected override string GetNamespace() => CodeModelPlugin.Instance.Configuration.ModelNamespace; + + protected override TypeSignatureModifiers GetDeclarationModifiers() => TypeSignatureModifiers.Internal | TypeSignatureModifiers.Partial |TypeSignatureModifiers.Class; + + protected override PropertyProvider[] BuildProperties() + { + return + [ + // customized by the NamedSymbol + new PropertyProvider($"Foo property", MethodSignatureModifiers.Public, typeof(int), "IntProperty", new AutoPropertyBody(true), this, wireInfo: new PropertyWireInformation(SerializationFormat.Default, true, true, true, false, "intProperty")), + // not customized by the NamedSymbol + new PropertyProvider($"Bar property", MethodSignatureModifiers.Public, typeof(string), "SpecProperty", new AutoPropertyBody(false), this, wireInfo: new PropertyWireInformation(SerializationFormat.Default, true, true, true, false, "stringProperty")), + ]; + } + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Providers/NamedTypeSymbolProviders/NamedTypeSymbolProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Providers/NamedTypeSymbolProviders/NamedTypeSymbolProviderTests.cs index 2d996ebf02..381059663a 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Providers/NamedTypeSymbolProviders/NamedTypeSymbolProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Providers/NamedTypeSymbolProviders/NamedTypeSymbolProviderTests.cs @@ -4,12 +4,9 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis; using Microsoft.Generator.CSharp.Primitives; using Microsoft.Generator.CSharp.Providers; using NUnit.Framework; -using static Microsoft.Generator.CSharp.Snippets.Snippet; namespace Microsoft.Generator.CSharp.Tests.Providers.NamedTypeSymbolProviders { @@ -22,7 +19,7 @@ public NamedTypeSymbolProviderTests() { _namedSymbol = new NamedSymbol(); var compilation = CompilationHelper.LoadCompilation([_namedSymbol, new PropertyType()]); - var iNamedSymbol = GetSymbol(compilation.Assembly.Modules.First().GlobalNamespace, "NamedSymbol"); + var iNamedSymbol = CompilationHelper.GetSymbol(compilation.Assembly.Modules.First().GlobalNamespace, "NamedSymbol"); _namedTypeSymbolProvider = new NamedTypeSymbolProvider(iNamedSymbol!); } @@ -89,7 +86,7 @@ public void ValidatePropertyTypes(Type propertyType) var namedSymbol = new NamedSymbol(propertyType); _namedSymbol = namedSymbol; var compilation = CompilationHelper.LoadCompilation([namedSymbol, new PropertyType()]); - var iNamedSymbol = GetSymbol(compilation.Assembly.Modules.First().GlobalNamespace, "NamedSymbol"); + var iNamedSymbol = CompilationHelper.GetSymbol(compilation.Assembly.Modules.First().GlobalNamespace, "NamedSymbol"); _namedTypeSymbolProvider = new NamedTypeSymbolProvider(iNamedSymbol!); @@ -191,109 +188,5 @@ public void ValidateFields() Assert.AreEqual(expected.InitializationValue, actual.InitializationValue); } } - - private class NamedSymbol : TypeProvider - { - private readonly Type? _propertyType; - protected override string BuildRelativeFilePath() => "."; - - protected override string BuildName() => "NamedSymbol"; - - protected override string GetNamespace() => CodeModelPlugin.Instance.Configuration.ModelNamespace; - - public NamedSymbol(Type? propertyType = null) : base() - { - _propertyType = propertyType; - } - - protected override FieldProvider[] BuildFields() - { - return - [ - new FieldProvider(FieldModifiers.Public, typeof(int), "IntField", new TestTypeProvider(), $"PublicIntField field"), - new FieldProvider(FieldModifiers.Private, typeof(string), "StringField", new TestTypeProvider(), $"PrivateStringField field no setter"), - new FieldProvider(FieldModifiers.Internal, typeof(double), "DoubleField", new TestTypeProvider(), $"InternalDoubleField field"), - new FieldProvider(FieldModifiers.Public | FieldModifiers.Static, typeof(float), "FloatField", new TestTypeProvider(), $"PublicStaticFloatField field"), - ]; - } - - protected override PropertyProvider[] BuildProperties() - { - if (_propertyType == null) - { - return - [ - new PropertyProvider($"IntProperty property", MethodSignatureModifiers.Public, typeof(int), "IntProperty", new AutoPropertyBody(true), this), - new PropertyProvider($"StringProperty property no setter", MethodSignatureModifiers.Public, typeof(string), "StringProperty", new AutoPropertyBody(false), this), - new PropertyProvider($"InternalStringProperty property no setter", MethodSignatureModifiers.Public, typeof(string), "InternalStringProperty", new AutoPropertyBody(false), this), - new PropertyProvider($"PropertyTypeProperty property", MethodSignatureModifiers.Public, new PropertyType().Type, "PropertyTypeProperty", new AutoPropertyBody(true), this), - ]; - } - - return - [ - new PropertyProvider($"p1", MethodSignatureModifiers.Public, _propertyType, "P1", new AutoPropertyBody(true), this) - ]; - } - - protected override ConstructorProvider[] BuildConstructors() - { - var intParam = new ParameterProvider("intParam", $"intParam", new CSharpType(typeof(int))); - - return - [ - new ConstructorProvider( - new ConstructorSignature(Type, $"Initializes a new instance of {Type}", MethodSignatureModifiers.Public, [intParam]), - Throw(New.Instance(typeof(NotImplementedException))), - this) - ]; - } - - protected override MethodProvider[] BuildMethods() - { - var intParam = new ParameterProvider("intParam", $"intParam", new CSharpType(typeof(int))); - - return - [ - new MethodProvider( - new MethodSignature("Method1", $"Description of method1", MethodSignatureModifiers.Public | MethodSignatureModifiers.Virtual, typeof(Task), null, [intParam]), - Throw(New.Instance(typeof(NotImplementedException))), - this) - ]; - } - } - - private class PropertyType : TypeProvider - { - protected override PropertyProvider[] BuildProperties() - { - return - [ - new PropertyProvider($"Foo property", MethodSignatureModifiers.Public, typeof(int), "Foo", new AutoPropertyBody(true), this), - ]; - } - - protected override string BuildRelativeFilePath() => "."; - - protected override string BuildName() => "PropertyType"; - } - - internal static INamedTypeSymbol? GetSymbol(INamespaceSymbol namespaceSymbol, string name) - { - foreach (var childNamespaceSymbol in namespaceSymbol.GetNamespaceMembers()) - { - return GetSymbol(childNamespaceSymbol, name); - } - - foreach (INamedTypeSymbol symbol in namespaceSymbol.GetTypeMembers()) - { - if (symbol.MetadataName == name) - { - return symbol; - } - } - - return null; - } } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/TestHelpers/TestNamedSymbol.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/TestHelpers/TestNamedSymbol.cs new file mode 100644 index 0000000000..4a065909e5 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/TestHelpers/TestNamedSymbol.cs @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; +using Microsoft.Generator.CSharp.Primitives; +using Microsoft.Generator.CSharp.Providers; +using Microsoft.Generator.CSharp.Snippets; +using static Microsoft.Generator.CSharp.Snippets.Snippet; + +namespace Microsoft.Generator.CSharp.Tests +{ + public class NamedSymbol : TypeProvider + { + private readonly Type? _propertyType; + private readonly string _typeName; + protected override string BuildRelativeFilePath() => "."; + + protected override string BuildName() => _typeName; + + protected override string GetNamespace() => CodeModelPlugin.Instance.Configuration.ModelNamespace; + + public NamedSymbol(Type? propertyType = null, string name = "NamedSymbol") + { + _propertyType = propertyType; + _typeName = name; + } + + protected override FieldProvider[] BuildFields() + { + return + [ + new FieldProvider(FieldModifiers.Public, typeof(int), "IntField", new TestTypeProvider(), + $"PublicIntField field"), + new FieldProvider(FieldModifiers.Private, typeof(string), "StringField", new TestTypeProvider(), + $"PrivateStringField field no setter"), + new FieldProvider(FieldModifiers.Internal, typeof(double), "DoubleField", new TestTypeProvider(), + $"InternalDoubleField field"), + new FieldProvider(FieldModifiers.Public | FieldModifiers.Static, typeof(float), "FloatField", + new TestTypeProvider(), $"PublicStaticFloatField field"), + ]; + } + + protected override PropertyProvider[] BuildProperties() + { + if (_propertyType == null) + { + return + [ + new PropertyProvider($"IntProperty property", MethodSignatureModifiers.Public, typeof(int), + "IntProperty", new AutoPropertyBody(true), this), + new PropertyProvider($"StringProperty property no setter", MethodSignatureModifiers.Public, + typeof(string), "StringProperty", new AutoPropertyBody(false), this), + new PropertyProvider($"InternalStringProperty property no setter", MethodSignatureModifiers.Public, + typeof(string), "InternalStringProperty", new AutoPropertyBody(false), this), + new PropertyProvider($"PropertyTypeProperty property", MethodSignatureModifiers.Public, + new PropertyType().Type, "PropertyTypeProperty", new AutoPropertyBody(true), this), + ]; + } + + return + [ + new PropertyProvider($"p1", MethodSignatureModifiers.Public, _propertyType, "P1", + new AutoPropertyBody(true), this) + ]; + } + + protected override ConstructorProvider[] BuildConstructors() + { + var intParam = new ParameterProvider("intParam", $"intParam", new CSharpType(typeof(int))); + + return + [ + new ConstructorProvider( + new ConstructorSignature(Type, $"Initializes a new instance of {Type}", + MethodSignatureModifiers.Public, [intParam]), + Throw(Snippet.New.Instance(typeof(NotImplementedException))), + this) + ]; + } + + protected override MethodProvider[] BuildMethods() + { + var intParam = new ParameterProvider("intParam", $"intParam", new CSharpType(typeof(int))); + + return + [ + new MethodProvider( + new MethodSignature("Method1", $"Description of method1", + MethodSignatureModifiers.Public | MethodSignatureModifiers.Virtual, typeof(Task), null, + [intParam]), + Throw(Snippet.New.Instance(typeof(NotImplementedException))), + this) + ]; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/TestHelpers/TestPropertyType.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/TestHelpers/TestPropertyType.cs new file mode 100644 index 0000000000..52199b9bfe --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/TestHelpers/TestPropertyType.cs @@ -0,0 +1,20 @@ +using Microsoft.Generator.CSharp.Primitives; +using Microsoft.Generator.CSharp.Providers; + +namespace Microsoft.Generator.CSharp.Tests +{ + public class PropertyType : TypeProvider + { + protected override PropertyProvider[] BuildProperties() + { + return + [ + new PropertyProvider($"Foo property", MethodSignatureModifiers.Public, typeof(int), "Foo", new AutoPropertyBody(true), this), + ]; + } + + protected override string BuildRelativeFilePath() => "."; + + protected override string BuildName() => "PropertyType"; + } +} From 21e49f33b2c2bf45eb5c8c3914fa4bf95b0bf892 Mon Sep 17 00:00:00 2001 From: jolov Date: Sun, 13 Oct 2024 16:55:39 -0700 Subject: [PATCH 2/9] use const --- .../src/Utilities/TypeSymbolExtensions.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Utilities/TypeSymbolExtensions.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Utilities/TypeSymbolExtensions.cs index 64c2ec97ce..a171c04939 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Utilities/TypeSymbolExtensions.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Utilities/TypeSymbolExtensions.cs @@ -11,6 +11,7 @@ namespace Microsoft.Generator.CSharp internal static class TypeSymbolExtensions { private const string GlobalPrefix = "global::"; + private const string NullableTypeName = "System.Nullable"; public static bool IsSameType(this INamedTypeSymbol symbol, CSharpType type) { @@ -110,7 +111,6 @@ public static string GetFullyQualifiedName(this ITypeSymbol typeSymbol) // Handle nullable types if (typeSymbol.NullableAnnotation == NullableAnnotation.Annotated && !IsCollectionType(namedTypeSymbol)) { - const string nullableTypeName = "System.Nullable"; var argTypeSymbol = namedTypeSymbol.TypeArguments.FirstOrDefault(); if (argTypeSymbol != null) @@ -119,11 +119,11 @@ public static string GetFullyQualifiedName(this ITypeSymbol typeSymbol) // arg may not be fully qualified, but it is better than not having any type information at all. if (argTypeSymbol.TypeKind == TypeKind.Error) { - return $"{nullableTypeName}`1[{argTypeSymbol}]"; + return $"{NullableTypeName}`1[{argTypeSymbol}]"; } string[] typeArguments = [.. namedTypeSymbol.TypeArguments.Select(arg => "[" + GetFullyQualifiedName(arg) + "]")]; - return $"{nullableTypeName}`{namedTypeSymbol.TypeArguments.Length}[{string.Join(", ", typeArguments)}]"; + return $"{NullableTypeName}`{namedTypeSymbol.TypeArguments.Length}[{string.Join(", ", typeArguments)}]"; } } else if (namedTypeSymbol.TypeArguments.Length > 0 && !IsCollectionType(namedTypeSymbol)) @@ -166,7 +166,7 @@ private static CSharpType ConstructCSharpTypeFromSymbol( var typeArg = namedTypeSymbol?.TypeArguments.FirstOrDefault(); bool isValueType = typeSymbol.IsValueType; bool isEnum = typeSymbol.TypeKind == TypeKind.Enum; - bool isNullable = fullyQualifiedName.StartsWith("System.Nullable`1"); + bool isNullable = fullyQualifiedName.StartsWith(NullableTypeName); bool isNullableUnknownType = isNullable && typeArg?.TypeKind == TypeKind.Error; string name = isNullableUnknownType ? fullyQualifiedName : typeSymbol.Name; string[] pieces = fullyQualifiedName.Split('.'); From c8862a6c93324b415e7f6d2338ad2271a3cc6a2c Mon Sep 17 00:00:00 2001 From: jolov Date: Mon, 14 Oct 2024 11:33:47 -0700 Subject: [PATCH 3/9] Use InputModelType --- .../src/Providers/CanonicalTypeProvider.cs | 69 +++++++++++++------ .../src/Providers/ModelProvider.cs | 2 +- .../src/Providers/TypeProvider.cs | 52 +++++++++----- .../src/TypeFactory.cs | 48 ++++++------- 4 files changed, 106 insertions(+), 65 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/CanonicalTypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/CanonicalTypeProvider.cs index 1614d32971..bde02f9f99 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/CanonicalTypeProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/CanonicalTypeProvider.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.Generator.CSharp.Input; using Microsoft.Generator.CSharp.Primitives; using Microsoft.Generator.CSharp.SourceInput; @@ -11,11 +12,13 @@ namespace Microsoft.Generator.CSharp.Providers { internal class CanonicalTypeProvider : TypeProvider { + private readonly InputModelType? _inputModel; private readonly TypeProvider _generatedTypeProvider; - public CanonicalTypeProvider(TypeProvider generatedTypeProvider) + public CanonicalTypeProvider(TypeProvider generatedTypeProvider, InputType? inputType) { _generatedTypeProvider = generatedTypeProvider; + _inputModel = inputType as InputModelType; } protected override string BuildRelativeFilePath() => throw new InvalidOperationException("This type should not be writing in generation"); @@ -27,31 +30,31 @@ public CanonicalTypeProvider(TypeProvider generatedTypeProvider) private protected override PropertyProvider[] FilterCustomizedProperties(PropertyProvider[] canonicalProperties) => canonicalProperties; - private protected override CanonicalTypeProvider? GetCanonicalView() => null; + private protected override CanonicalTypeProvider GetCanonicalView() => this; protected override PropertyProvider[] BuildProperties() { - var specProperties = _generatedTypeProvider.SpecProperties; + var specProperties = _inputModel?.Properties ?? []; var specPropertiesMap = specProperties.ToDictionary(p => p.Name, p => p); var generatedProperties = _generatedTypeProvider.Properties; var customProperties = _generatedTypeProvider.CustomCodeView?.Properties ?? []; - Dictionary serializedNameMapping = BuildSerializationNameMap(); + Dictionary serializedNameMapping = BuildSerializationNameMap(); // Update the serializedName of generated properties if necessary foreach (var generatedProperty in generatedProperties) { - if (serializedNameMapping.TryGetValue(generatedProperty.Name, out var serializedName)) + if (serializedNameMapping.TryGetValue(generatedProperty.Name, out var serializedName) && serializedName != null) { generatedProperty.WireInfo!.SerializedName = serializedName; } } - Dictionary specToCustomPropertiesMap = BuildSpecToCustomPropertyMap(customProperties, specPropertiesMap); + Dictionary specToCustomPropertiesMap = BuildSpecToCustomPropertyMap(customProperties, specPropertiesMap); foreach (var customProperty in customProperties) { - PropertyProvider? specProperty = null; + InputModelProperty? specProperty = null; if (((customProperty.OriginalName != null && specPropertiesMap.TryGetValue(customProperty.OriginalName, out var candidateSpecProperty)) || specPropertiesMap.TryGetValue(customProperty.Name, out candidateSpecProperty)) @@ -59,21 +62,45 @@ protected override PropertyProvider[] BuildProperties() && specToCustomPropertiesMap[candidateSpecProperty] == customProperty) { specProperty = candidateSpecProperty; - var serializedName = specProperty.WireInfo!.SerializedName; + // var serializedName = specProperty.WireInfo!.SerializedName; - // Update the serializedName of custom properties if necessary - if (serializedNameMapping.TryGetValue(customProperty.Name, out var customSerializedName) || - (customProperty.OriginalName != null && serializedNameMapping.TryGetValue(customProperty.OriginalName, out customSerializedName))) + customProperty.WireInfo = new PropertyWireInformation(specProperty); + // customProperty.WireInfo!.SerializedName = serializedName; + } + + string? serializedName = specProperty?.SerializedName; + bool hasCustomSerialization = false; + // Update the serializedName of custom properties if necessary + if (serializedNameMapping.TryGetValue(customProperty.Name, out var customSerializedName) || + (customProperty.OriginalName != null && serializedNameMapping.TryGetValue(customProperty.OriginalName, out customSerializedName))) + { + hasCustomSerialization = true; + if (customSerializedName != null) { serializedName = customSerializedName; } + } - customProperty.WireInfo = specProperty.WireInfo; - customProperty.WireInfo!.SerializedName = serializedName; + if (serializedName != null || hasCustomSerialization) + { + if (specProperty == null) + { + customProperty.WireInfo = new( + SerializationFormat.Default, + false, + !customProperty.Body.HasSetter, + customProperty.Type.IsNullable, + false, + serializedName ?? customProperty.Name.ToVariableName());; + } + else + { + customProperty.WireInfo!.SerializedName = serializedName!; + } } // handle customized extensible enums, since the custom type would not be an enum, but the spec type would be an enum - if (specProperty?.Type is { IsEnum: true, IsStruct: true }) + if (specProperty?.Type is InputEnumType { IsExtensible: true } inputEnumType) { customProperty.Type = new CSharpType( customProperty.Type.Name, @@ -85,18 +112,18 @@ protected override PropertyProvider[] BuildProperties() customProperty.Type.IsPublic, customProperty.Type.IsStruct, customProperty.Type.BaseType, - specProperty.Type.UnderlyingEnumType); + TypeFactory.CreatePrimitiveCSharpTypeCore(inputEnumType.ValueType)); } } return [..generatedProperties, ..customProperties]; } - private static Dictionary BuildSpecToCustomPropertyMap( + private static Dictionary BuildSpecToCustomPropertyMap( IReadOnlyList customProperties, - Dictionary specPropertiesMap) + Dictionary specPropertiesMap) { - var specToCustomPropertiesMap = new Dictionary(); + var specToCustomPropertiesMap = new Dictionary(); // Create a map from spec properties to custom properties so that we know which custom properties are replacing spec properties foreach (var customProperty in customProperties) { @@ -110,11 +137,11 @@ private static Dictionary BuildSpecToCustomP return specToCustomPropertiesMap; } - private Dictionary BuildSerializationNameMap() + private Dictionary BuildSerializationNameMap() { var serializationAttributes = _generatedTypeProvider.CustomCodeView?.GetAttributes(). Where(a => a.AttributeClass?.Name == CodeGenAttributes.CodeGenSerializationAttributeName) ?? []; - var serializedNameMapping = new Dictionary(); + var serializedNameMapping = new Dictionary(); foreach (var serializationAttribute in serializationAttributes) { if (CodeGenAttributes.TryGetCodeGenSerializationAttributeValue( @@ -123,7 +150,7 @@ private Dictionary BuildSerializationNameMap() out string? serializationName, out _, out _, - out _) && serializationName != null) + out _)) { serializedNameMapping[propertyName] = serializationName; } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/ModelProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/ModelProvider.cs index 78b8620781..03537a67a6 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/ModelProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/ModelProvider.cs @@ -30,7 +30,7 @@ public sealed class ModelProvider : TypeProvider private ModelProvider? _baseModelProvider; private ConstructorProvider? _fullConstructor; - public ModelProvider(InputModelType inputModel) + public ModelProvider(InputModelType inputModel) : base(inputModel) { _inputModel = inputModel; Description = inputModel.Description != null ? FormattableStringHelpers.FromString(inputModel.Description) : $"The {Name}."; diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/TypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/TypeProvider.cs index f529a010e5..15dc3d02d0 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/TypeProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/TypeProvider.cs @@ -17,12 +17,21 @@ namespace Microsoft.Generator.CSharp.Providers public abstract class TypeProvider { private Lazy _customCodeView; - private Lazy _canonicalView; + private Lazy _canonicalView; + private readonly InputType? _inputType; - protected TypeProvider() + protected TypeProvider(InputType? inputType = default) { _customCodeView = new(GetCustomCodeView); _canonicalView = new(GetCanonicalView); + _inputType = inputType; + } + + // for mocking +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + protected TypeProvider() : this(null) +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + { } private protected virtual NamedTypeSymbolProvider? GetCustomCodeView() @@ -30,8 +39,25 @@ protected TypeProvider() public NamedTypeSymbolProvider? CustomCodeView => _customCodeView.Value; - private protected virtual CanonicalTypeProvider? GetCanonicalView() => new CanonicalTypeProvider(this); - public TypeProvider? CanonicalView => _canonicalView.Value; + internal IReadOnlyList GetAllCustomProperties() + { + var allCustomProperties = CustomCodeView?.Properties != null + ? new List(CustomCodeView.Properties) + : []; + var baseTypeCustomCodeView = BaseTypeProvider?.CustomCodeView; + + // add all custom properties from base types + while (baseTypeCustomCodeView != null) + { + allCustomProperties.AddRange(baseTypeCustomCodeView.Properties); + baseTypeCustomCodeView = baseTypeCustomCodeView.BaseTypeProvider?.CustomCodeView; + } + + return allCustomProperties; + } + + private protected virtual CanonicalTypeProvider GetCanonicalView() => new CanonicalTypeProvider(this, _inputType); + public TypeProvider CanonicalView => _canonicalView.Value; protected string? _deprecated; @@ -138,10 +164,8 @@ private TypeSignatureModifiers GetDeclarationModifiersInternal() public IReadOnlyList Implements => _implements ??= BuildImplements(); private IReadOnlyList? _properties; - private PropertyProvider[]? _specProperties; - internal PropertyProvider[] SpecProperties => _specProperties ??= BuildProperties(); - public IReadOnlyList Properties => _properties ??= FilterCustomizedProperties(SpecProperties); + public IReadOnlyList Properties => _properties ??= FilterCustomizedProperties(BuildProperties()); private IReadOnlyList? _methods; public IReadOnlyList Methods => _methods ??= BuildMethodsInternal(); @@ -170,19 +194,8 @@ private protected virtual PropertyProvider[] FilterCustomizedProperties(Property var properties = new List(); var customProperties = new Dictionary(); var renamedProperties = new Dictionary(); - var allCustomProperties = CustomCodeView?.Properties != null - ? new List(CustomCodeView.Properties) - : []; - var baseTypeCustomCodeView = BaseTypeProvider?.CustomCodeView; - // add all custom properties from base types - while (baseTypeCustomCodeView != null) - { - allCustomProperties.AddRange(baseTypeCustomCodeView.Properties); - baseTypeCustomCodeView = baseTypeCustomCodeView.BaseTypeProvider?.CustomCodeView; - } - - foreach (var customProperty in allCustomProperties) + foreach (var customProperty in GetAllCustomProperties()) { customProperties.Add(customProperty.Name, customProperty); if (customProperty.OriginalName != null) @@ -190,6 +203,7 @@ private protected virtual PropertyProvider[] FilterCustomizedProperties(Property renamedProperties.Add(customProperty.OriginalName, customProperty); } } + foreach (var property in specProperties) { if (ShouldGenerate(property, customProperties, renamedProperties)) diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypeFactory.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypeFactory.cs index ac5028d74c..5dbaecfa82 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypeFactory.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypeFactory.cs @@ -105,34 +105,34 @@ protected internal TypeFactory() /// /// The to convert. /// An instance of . - private CSharpType CreatePrimitiveCSharpTypeCore(InputType inputType) => inputType switch + internal static Type CreatePrimitiveCSharpTypeCore(InputType inputType) => inputType switch { InputPrimitiveType primitiveType => primitiveType.Kind switch { - InputPrimitiveTypeKind.Boolean => new CSharpType(typeof(bool)), - InputPrimitiveTypeKind.Bytes => new CSharpType(typeof(BinaryData)), - InputPrimitiveTypeKind.PlainDate => new CSharpType(typeof(DateTimeOffset)), - InputPrimitiveTypeKind.Decimal => new CSharpType(typeof(decimal)), - InputPrimitiveTypeKind.Decimal128 => new CSharpType(typeof(decimal)), - InputPrimitiveTypeKind.PlainTime => new CSharpType(typeof(TimeSpan)), - InputPrimitiveTypeKind.Float32 => new CSharpType(typeof(float)), - InputPrimitiveTypeKind.Float64 => new CSharpType(typeof(double)), - InputPrimitiveTypeKind.Int8 => new CSharpType(typeof(sbyte)), - InputPrimitiveTypeKind.UInt8 => new CSharpType(typeof(byte)), - InputPrimitiveTypeKind.Int32 => new CSharpType(typeof(int)), - InputPrimitiveTypeKind.Int64 => new CSharpType(typeof(long)), - InputPrimitiveTypeKind.SafeInt => new CSharpType(typeof(long)), - InputPrimitiveTypeKind.Integer => new CSharpType(typeof(long)), // in typespec, integer is the base type of int related types, see type relation: https://typespec.io/docs/language-basics/type-relations - InputPrimitiveTypeKind.Float => new CSharpType(typeof(double)), // in typespec, float is the base type of float32 and float64, see type relation: https://typespec.io/docs/language-basics/type-relations - InputPrimitiveTypeKind.Numeric => new CSharpType(typeof(double)), // in typespec, numeric is the base type of number types, see type relation: https://typespec.io/docs/language-basics/type-relations - InputPrimitiveTypeKind.Stream => new CSharpType(typeof(Stream)), - InputPrimitiveTypeKind.String => new CSharpType(typeof(string)), - InputPrimitiveTypeKind.Url => new CSharpType(typeof(Uri)), - InputPrimitiveTypeKind.Unknown => new CSharpType(typeof(BinaryData)), - _ => new CSharpType(typeof(object)), + InputPrimitiveTypeKind.Boolean => typeof(bool), + InputPrimitiveTypeKind.Bytes => typeof(BinaryData), + InputPrimitiveTypeKind.PlainDate => typeof(DateTimeOffset), + InputPrimitiveTypeKind.Decimal => typeof(decimal), + InputPrimitiveTypeKind.Decimal128 => typeof(decimal), + InputPrimitiveTypeKind.PlainTime => typeof(TimeSpan), + InputPrimitiveTypeKind.Float32 => typeof(float), + InputPrimitiveTypeKind.Float64 => typeof(double), + InputPrimitiveTypeKind.Int8 => typeof(sbyte), + InputPrimitiveTypeKind.UInt8 => typeof(byte), + InputPrimitiveTypeKind.Int32 => typeof(int), + InputPrimitiveTypeKind.Int64 => typeof(long), + InputPrimitiveTypeKind.SafeInt => typeof(long), + InputPrimitiveTypeKind.Integer => typeof(long), // in typespec, integer is the base type of int related types, see type relation: https://typespec.io/docs/language-basics/type-relations + InputPrimitiveTypeKind.Float => typeof(double), // in typespec, float is the base type of float32 and float64, see type relation: https://typespec.io/docs/language-basics/type-relations + InputPrimitiveTypeKind.Numeric => typeof(double), // in typespec, numeric is the base type of number types, see type relation: https://typespec.io/docs/language-basics/type-relations + InputPrimitiveTypeKind.Stream => typeof(Stream), + InputPrimitiveTypeKind.String => typeof(string), + InputPrimitiveTypeKind.Url => typeof(Uri), + InputPrimitiveTypeKind.Unknown => typeof(BinaryData), + _ => typeof(object), }, - InputDateTimeType dateTimeType => new CSharpType(typeof(DateTimeOffset)), - InputDurationType durationType => new CSharpType(typeof(TimeSpan)), + InputDateTimeType dateTimeType => typeof(DateTimeOffset), + InputDurationType durationType => typeof(TimeSpan), _ => throw new InvalidOperationException($"Unknown type: {inputType}") }; From f0d6ba1532717f68e01597b0dd1975c1badd0526 Mon Sep 17 00:00:00 2001 From: jolov Date: Mon, 14 Oct 2024 11:42:16 -0700 Subject: [PATCH 4/9] pr fb --- .../src/Providers/TypeProvider.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/TypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/TypeProvider.cs index 15dc3d02d0..bc472969e9 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/TypeProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/TypeProvider.cs @@ -443,7 +443,7 @@ private static bool IsMatch(TypeProvider enclosingType, MethodSignatureBase sign var parameterType = ((ITypeSymbol)parameterTypes[i]!).GetCSharpType(); // we ignore nullability for reference types as these are generated the same regardless of nullability // TODO - switch to using CSharpType.Equals once https://github.com/microsoft/typespec/issues/4624 is fixed. - if (!parameterType.Name.EndsWith(signature.Parameters[i].Type.Name) || + if (GetTypeOrMethodName(parameterType.Name) != signature.Parameters[i].Type.Name || (parameterType.IsValueType && parameterType.IsNullable != signature.Parameters[i].Type.IsNullable)) { return false; @@ -455,14 +455,14 @@ private static bool IsMatch(TypeProvider enclosingType, MethodSignatureBase sign private static bool IsMatch(MethodSignatureBase customMethod, MethodSignatureBase method) { - if (customMethod.Parameters.Count != method.Parameters.Count || !customMethod.Name.EndsWith(method.Name)) + if (customMethod.Parameters.Count != method.Parameters.Count || GetTypeOrMethodName(customMethod.Name) != method.Name) { return false; } for (int i = 0; i < customMethod.Parameters.Count; i++) { - if (!customMethod.Parameters[i].Type.Name.EndsWith(method.Parameters[i].Type.Name)) + if (GetTypeOrMethodName(customMethod.Parameters[i].Type.Name) != method.Parameters[i].Type.Name) { return false; } @@ -471,6 +471,12 @@ private static bool IsMatch(MethodSignatureBase customMethod, MethodSignatureBas return true; } + private static string GetTypeOrMethodName(string fullyQualifiedName) + { + var parts = fullyQualifiedName.Split('.'); + return parts[^1]; + } + private static void ValidateArguments(TypeProvider type, AttributeData attributeData) { var arguments = attributeData.ConstructorArguments; From f052dfd8c0a3fcaace4513f70b89c65a8e898bba Mon Sep 17 00:00:00 2001 From: jolov Date: Mon, 14 Oct 2024 13:04:49 -0700 Subject: [PATCH 5/9] remove ! --- .../src/Providers/MrwSerializationTypeDefinition.cs | 2 +- .../src/Providers/ModelProvider.cs | 8 ++++---- .../test/Providers/CanonicalTypeProviderTests.cs | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs index 7fa415b6b5..53dd9ba593 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs @@ -1311,7 +1311,7 @@ private static MethodBodyStatement ThrowValidationFailException(ValueExpression /// private MethodBodyStatement[] CreateWritePropertiesStatements() { - var properties = _model.CanonicalView!.Properties; + var properties = _model.CanonicalView.Properties; List propertyStatements = new(); foreach (var property in properties) { diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/ModelProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/ModelProvider.cs index 03537a67a6..2e68330ec3 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/ModelProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/ModelProvider.cs @@ -457,14 +457,14 @@ private ConstructorProvider BuildFullConstructor() if (isPrimaryConstructor) { // the primary ctor should only include the properties of the direct base model - baseProperties = BaseModelProvider?.CanonicalView!.Properties ?? []; + baseProperties = BaseModelProvider?.CanonicalView.Properties ?? []; } else if (BaseModelProvider?.FullConstructor.Signature != null) { baseParameters.AddRange(BaseModelProvider.FullConstructor.Signature.Parameters); } - HashSet overriddenProperties = CanonicalView!.Properties.Where(p => p.BaseProperty is not null).Select(p => p.BaseProperty!).ToHashSet(); + HashSet overriddenProperties = CanonicalView.Properties.Where(p => p.BaseProperty is not null).Select(p => p.BaseProperty!).ToHashSet(); // add the base parameters, if any foreach (var property in baseProperties) @@ -506,7 +506,7 @@ p.Property is null { if (_inputModel.BaseModel is not null && _inputModel.DiscriminatorValue is not null) { - var discriminator = BaseModelProvider?.CanonicalView!.Properties.Where(p => p.IsDiscriminator).FirstOrDefault(); + var discriminator = BaseModelProvider?.CanonicalView.Properties.Where(p => p.IsDiscriminator).FirstOrDefault(); if (discriminator != null) { var type = discriminator.Type; @@ -617,7 +617,7 @@ private MethodBodyStatement GetPropertyInitializers( bool isPrimaryConstructor, IReadOnlyList? parameters = null) { - List methodBodyStatements = new(CanonicalView!.Properties.Count + 1); + List methodBodyStatements = new(CanonicalView.Properties.Count + 1); Dictionary parameterMap = parameters?.ToDictionary(p => p.Name) ?? []; foreach (var property in CanonicalView.Properties) diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Providers/CanonicalTypeProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Providers/CanonicalTypeProviderTests.cs index 83f0a03321..da9b9301f8 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Providers/CanonicalTypeProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Providers/CanonicalTypeProviderTests.cs @@ -36,26 +36,26 @@ public async Task Setup() [Test] public void ValidateModifiers() { - var modifiers = _typeProvider.CanonicalView!.DeclarationModifiers; + var modifiers = _typeProvider.CanonicalView.DeclarationModifiers; Assert.IsTrue(modifiers.HasFlag(TypeSignatureModifiers.Internal | TypeSignatureModifiers.Partial | TypeSignatureModifiers.Class)); } [Test] public void ValidateName() { - Assert.AreEqual(_typeProvider.Name, _typeProvider.CanonicalView!.Name); + Assert.AreEqual(_typeProvider.Name, _typeProvider.CanonicalView.Name); } [Test] public void ValidateNamespace() { - Assert.AreEqual(_typeProvider.Namespace, _typeProvider.CanonicalView!.Namespace); + Assert.AreEqual(_typeProvider.Namespace, _typeProvider.CanonicalView.Namespace); } [Test] public void ValidateProperties() { - Dictionary properties = _typeProvider.CanonicalView!.Properties.ToDictionary(p => p.Name); + Dictionary properties = _typeProvider.CanonicalView.Properties.ToDictionary(p => p.Name); Assert.AreEqual(5, properties.Count); Assert.AreEqual(1, _typeProvider.Properties.Count); Assert.AreEqual(4, _typeProvider.CustomCodeView!.Properties.Count); From 3c703cd7f961ecf53056b176bd9b4c95bd9c4aac Mon Sep 17 00:00:00 2001 From: jolov Date: Mon, 14 Oct 2024 13:07:42 -0700 Subject: [PATCH 6/9] ref issue --- .../src/Expressions/MemberExpression.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/MemberExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/MemberExpression.cs index e53cecca15..b02f4e7c4c 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/MemberExpression.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/MemberExpression.cs @@ -13,6 +13,7 @@ internal override void Write(CodeWriter writer) writer.AppendRaw("."); } // workaround to avoid Roslyn reducing properties named Object to object + // Should come up with a better approach - https://github.com/microsoft/typespec/issues/4724 writer.AppendRaw(MemberName == "Object" && Inner == null ? $"this.{MemberName}" : MemberName); } } From 8c860526770917d77e20cdd529270de6d4ea9f5f Mon Sep 17 00:00:00 2001 From: jolov Date: Mon, 14 Oct 2024 13:31:04 -0700 Subject: [PATCH 7/9] pr fb --- .../src/Providers/CanonicalTypeProvider.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/CanonicalTypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/CanonicalTypeProvider.cs index bde02f9f99..9ee8a1aa04 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/CanonicalTypeProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/CanonicalTypeProvider.cs @@ -62,10 +62,7 @@ protected override PropertyProvider[] BuildProperties() && specToCustomPropertiesMap[candidateSpecProperty] == customProperty) { specProperty = candidateSpecProperty; - // var serializedName = specProperty.WireInfo!.SerializedName; - customProperty.WireInfo = new PropertyWireInformation(specProperty); - // customProperty.WireInfo!.SerializedName = serializedName; } string? serializedName = specProperty?.SerializedName; From b5e38498fb6a25afe9ba06879d8641d4b77035b3 Mon Sep 17 00:00:00 2001 From: jolov Date: Mon, 14 Oct 2024 13:42:37 -0700 Subject: [PATCH 8/9] remove attr --- .../SerializationCustomizationTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/SerializationCustomizationTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/SerializationCustomizationTests.cs index f235da4701..6dfbe6c893 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/SerializationCustomizationTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/SerializationCustomizationTests.cs @@ -219,7 +219,6 @@ public async Task CanChangeDictionaryToBinaryData() new TestCaseData(InputPrimitiveType.Int32), ]; - [Test] [TestCaseSource(nameof(ExtensibleEnumCases))] public async Task CanCustomizeExtensibleEnum(InputPrimitiveType enumType) { From 0b28cf42eb269c71bcab5fb350164dd76a244f91 Mon Sep 17 00:00:00 2001 From: jolov Date: Mon, 14 Oct 2024 14:16:52 -0700 Subject: [PATCH 9/9] Add todo comment --- .../src/Providers/CanonicalTypeProvider.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/CanonicalTypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/CanonicalTypeProvider.cs index 9ee8a1aa04..ade5f63349 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/CanonicalTypeProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/CanonicalTypeProvider.cs @@ -32,6 +32,8 @@ public CanonicalTypeProvider(TypeProvider generatedTypeProvider, InputType? inpu private protected override CanonicalTypeProvider GetCanonicalView() => this; + // TODO - Implement BuildMethods, BuildConstructors, etc as needed + protected override PropertyProvider[] BuildProperties() { var specProperties = _inputModel?.Properties ?? [];