diff --git a/packages/http-client-csharp/emitter/src/lib/type-converter.ts b/packages/http-client-csharp/emitter/src/lib/type-converter.ts index e4dccc29ca8..f49918d1fc6 100644 --- a/packages/http-client-csharp/emitter/src/lib/type-converter.ts +++ b/packages/http-client-csharp/emitter/src/lib/type-converter.ts @@ -29,6 +29,7 @@ import { InputDurationType, InputEnumType, InputEnumValueType, + InputExternalType, InputLiteralType, InputModelProperty, InputModelType, @@ -80,6 +81,13 @@ export function fromSdkType( return retVar as any; } + // Check if this type references an external type + if ((sdkType as any).external) { + retVar = fromSdkExternalType(sdkContext, sdkType); + sdkContext.__typeCache.updateSdkTypeReferences(sdkType, retVar); + return retVar as any; + } + switch (sdkType.kind) { case "nullable": const nullableType: InputNullableType = { @@ -463,6 +471,20 @@ function fromSdkEndpointType(): InputPrimitiveType { }; } +function fromSdkExternalType( + sdkContext: CSharpEmitterContext, + sdkType: SdkType, +): InputExternalType { + const external = (sdkType as any).external; + return { + kind: "external", + identity: external.identity, + package: external.package, + minVersion: external.minVersion, + decorators: sdkType.decorators, + }; +} + /** * @beta */ diff --git a/packages/http-client-csharp/emitter/src/type/input-type.ts b/packages/http-client-csharp/emitter/src/type/input-type.ts index 297b8fa8d72..8dfc5383fe9 100644 --- a/packages/http-client-csharp/emitter/src/type/input-type.ts +++ b/packages/http-client-csharp/emitter/src/type/input-type.ts @@ -65,7 +65,8 @@ export type InputType = | InputEnumValueType | InputArrayType | InputDictionaryType - | InputNullableType; + | InputNullableType + | InputExternalType; export interface InputPrimitiveType extends InputTypeBase { kind: SdkBuiltInKinds; @@ -271,3 +272,10 @@ export interface InputDictionaryType extends InputTypeBase { keyType: InputType; valueType: InputType; } + +export interface InputExternalType extends InputTypeBase { + kind: "external"; + identity: string; + package?: string; + minVersion?: string; +} diff --git a/packages/http-client-csharp/emitter/test/Unit/type-converter.test.ts b/packages/http-client-csharp/emitter/test/Unit/type-converter.test.ts index a3163cb14cb..abe4b08e49e 100644 --- a/packages/http-client-csharp/emitter/test/Unit/type-converter.test.ts +++ b/packages/http-client-csharp/emitter/test/Unit/type-converter.test.ts @@ -87,3 +87,88 @@ describe("Enum value references", () => { } }); }); + +describe("External types", () => { + let runner: TestHost; + + beforeEach(async () => { + runner = await createEmitterTestHost(); + }); + + it("should convert external type from @alternateType decorator", async () => { + const program = await typeSpecCompile( + ` + @alternateType({ + identity: "Azure.Core.Expressions.DataFactoryExpression", + package: "Azure.Core.Expressions", + minVersion: "1.0.0", + }, "csharp") + union Dfe { + T, + DfeExpression: string + } + + model TestModel { + prop: Dfe; + } + + op test(@body input: TestModel): void; + `, + runner, + { IsTCGCNeeded: true }, + ); + const context = createEmitterContext(program); + const sdkContext = await createCSharpSdkContext(context); + const root = createModel(sdkContext); + + const testModel = root.models.find((m) => m.name === "TestModel"); + ok(testModel, "TestModel should exist"); + + const prop = testModel.properties.find((p) => p.name === "prop"); + ok(prop, "prop should exist"); + + // The type should be an external type + strictEqual(prop.type.kind, "external"); + strictEqual((prop.type as any).identity, "Azure.Core.Expressions.DataFactoryExpression"); + strictEqual((prop.type as any).package, "Azure.Core.Expressions"); + strictEqual((prop.type as any).minVersion, "1.0.0"); + }); + + it("should convert external type on model", async () => { + const program = await typeSpecCompile( + ` + @alternateType({ + identity: "System.Text.Json.JsonElement", + package: "System.Text.Json", + minVersion: "8.0.0", + }, "csharp") + model JsonData { + data: string; + } + + model TestModel { + jsonElement: JsonData; + } + + op test(@body input: TestModel): void; + `, + runner, + { IsTCGCNeeded: true }, + ); + const context = createEmitterContext(program); + const sdkContext = await createCSharpSdkContext(context); + const root = createModel(sdkContext); + + const testModel = root.models.find((m) => m.name === "TestModel"); + ok(testModel, "TestModel should exist"); + + const jsonElementProp = testModel.properties.find((p) => p.name === "jsonElement"); + ok(jsonElementProp, "jsonElement property should exist"); + + // The type should be an external type + strictEqual(jsonElementProp.type.kind, "external"); + strictEqual((jsonElementProp.type as any).identity, "System.Text.Json.JsonElement"); + strictEqual((jsonElementProp.type as any).package, "System.Text.Json"); + strictEqual((jsonElementProp.type as any).minVersion, "8.0.0"); + }); +}); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputTypes/InputExternalType.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputTypes/InputExternalType.cs new file mode 100644 index 00000000000..422c33ed2fd --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputTypes/InputExternalType.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.TypeSpec.Generator.Input +{ + /// + /// Represents an external type reference in the input model. + /// + public sealed class InputExternalType : InputType + { + /// + /// Construct a new instance + /// + /// The fully qualified name of the external type. + /// The package that exports the external type. + /// The minimum version of the package. + public InputExternalType(string identity, string? package, string? minVersion) : base("external") + { + Identity = identity; + Package = package; + MinVersion = minVersion; + } + + /// + /// The fully qualified name of the external type. For example, "Azure.Core.Expressions.DataFactoryExpression" + /// + public string Identity { get; } + + /// + /// The package that exports the external type. For example, "Azure.Core.Expressions" + /// + public string? Package { get; } + + /// + /// The minimum version of the package to use for the external type. For example, "1.0.0" + /// + public string? MinVersion { get; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputTypes/Serialization/InputExternalTypeConverter.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputTypes/Serialization/InputExternalTypeConverter.cs new file mode 100644 index 00000000000..32cf22e3714 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputTypes/Serialization/InputExternalTypeConverter.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.TypeSpec.Generator.Input +{ + internal class InputExternalTypeConverter : JsonConverter + { + private readonly TypeSpecReferenceHandler _referenceHandler; + + public InputExternalTypeConverter(TypeSpecReferenceHandler referenceHandler) + { + _referenceHandler = referenceHandler; + } + + public override InputExternalType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => reader.ReadReferenceAndResolve(_referenceHandler.CurrentResolver) ?? CreateInputExternalType(ref reader, null, options, _referenceHandler.CurrentResolver); + + public override void Write(Utf8JsonWriter writer, InputExternalType value, JsonSerializerOptions options) + => throw new NotSupportedException("Writing not supported"); + + public static InputExternalType CreateInputExternalType(ref Utf8JsonReader reader, string? id, JsonSerializerOptions options, ReferenceResolver resolver) + { + string? identity = null; + string? package = null; + string? minVersion = null; + IReadOnlyList? decorators = null; + + while (reader.TokenType != JsonTokenType.EndObject) + { + var isKnownProperty = reader.TryReadReferenceId(ref id) + || reader.TryReadString("identity", ref identity) + || reader.TryReadString("package", ref package) + || reader.TryReadString("minVersion", ref minVersion) + || reader.TryReadComplexType("decorators", options, ref decorators); + + if (!isKnownProperty) + { + reader.SkipProperty(); + } + } + + identity = identity ?? throw new JsonException("InputExternalType must have identity"); + + var externalType = new InputExternalType(identity, package, minVersion) + { + Decorators = decorators ?? [] + }; + + if (id != null) + { + resolver.AddReference(id, externalType); + } + + return externalType; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputTypes/Serialization/InputTypeConverter.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputTypes/Serialization/InputTypeConverter.cs index eef96ae8571..98fb96a7dd1 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputTypes/Serialization/InputTypeConverter.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputTypes/Serialization/InputTypeConverter.cs @@ -57,6 +57,7 @@ private static InputType CreateInputType(ref Utf8JsonReader reader, JsonSerializ private const string UtcDateTimeKind = "utcDateTime"; private const string OffsetDateTimeKind = "offsetDateTime"; private const string DurationKind = "duration"; + private const string ExternalKind = "external"; private static InputType CreateDerivedType(ref Utf8JsonReader reader, string? id, string? kind, string? name, JsonSerializerOptions options, ReferenceResolver resolver) => kind switch { @@ -71,6 +72,7 @@ private static InputType CreateInputType(ref Utf8JsonReader reader, JsonSerializ UtcDateTimeKind or OffsetDateTimeKind => InputDateTimeTypeConverter.CreateDateTimeType(ref reader, id, name, options, resolver), DurationKind => InputDurationTypeConverter.CreateDurationType(ref reader, id, name, options, resolver), NullableKind => TypeSpecInputNullableTypeConverter.CreateNullableType(ref reader, id, name, options, resolver), + ExternalKind => InputExternalTypeConverter.CreateInputExternalType(ref reader, id, options, resolver), _ => InputPrimitiveTypeConverter.CreatePrimitiveType(ref reader, id, kind, name, options, resolver), }; } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/TypeFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/TypeFactory.cs index 1883f3d990b..3fd2182a70d 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/TypeFactory.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/TypeFactory.cs @@ -125,6 +125,9 @@ protected internal TypeFactory() case InputNullableType nullableType: type = CreateCSharpType(nullableType.Type)?.WithNullable(true); break; + case InputExternalType externalType: + type = CreateExternalType(externalType); + break; default: type = CreatePrimitiveCSharpTypeCore(inputType); break; @@ -229,6 +232,29 @@ protected internal TypeFactory() protected virtual EnumProvider? CreateEnumCore(InputEnumType enumType, TypeProvider? declaringType) => EnumProvider.Create(enumType, declaringType); + /// + /// Factory method for creating a based on an external type reference . + /// + /// The to convert. + /// A representing the external type, or null if the type cannot be resolved. + private CSharpType? CreateExternalType(InputExternalType externalType) + { + // Try to create a framework type from the fully qualified name + var frameworkType = CreateFrameworkType(externalType.Identity); + if (frameworkType != null) + { + return new CSharpType(frameworkType); + } + + // External types that cannot be resolved as framework types are not supported + // Report a diagnostic to inform the user + CodeModelGenerator.Instance.Emitter.ReportDiagnostic( + "unsupported-external-type", + $"External type '{externalType.Identity}' is not currently supported."); + + return null; + } + /// /// Factory method for creating a based on an input parameter . /// diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs index a40d0aec425..2d41c03710f 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs @@ -1158,5 +1158,101 @@ public void ConstantPropertiesAccessibility( Assert.AreEqual(shouldBePublic, prop!.Modifiers.HasFlag(MethodSignatureModifiers.Public)); Assert.AreEqual(shouldHaveSetter, prop.Body.HasSetter); } + + [Test] + public void ExternalTypeModelUsedAsProperty() + { + // Test a model decorated with alternateType that references System.Uri + var externalType = InputFactory.External("System.Uri"); + var modelWithExternal = InputFactory.Model("ExternalModel"); + + // Create a model that uses the external type as a property + var containerModel = InputFactory.Model( + "ContainerModel", + properties: + [ + InputFactory.Property("externalProp", externalType), + InputFactory.Property("normalProp", InputPrimitiveType.String) + ]); + + MockHelpers.LoadMockGenerator( + inputModelTypes: [modelWithExternal, containerModel]); + + var containerProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders + .SingleOrDefault(t => t.Name == "ContainerModel") as ModelProvider; + Assert.IsNotNull(containerProvider); + + // The property with external type should be resolved to System.Uri + var externalProp = containerProvider!.Properties.FirstOrDefault(p => p.Name == "ExternalProp"); + Assert.IsNotNull(externalProp); + Assert.AreEqual(typeof(Uri), externalProp!.Type.FrameworkType); + + // Normal property should still work + var normalProp = containerProvider.Properties.FirstOrDefault(p => p.Name == "NormalProp"); + Assert.IsNotNull(normalProp); + Assert.AreEqual(typeof(string), normalProp!.Type.FrameworkType); + } + + [Test] + public void ExternalTypePropertyIsResolved() + { + // Test a property decorated with alternateType + var externalType = InputFactory.External("System.Net.IPAddress", "System.Net.Primitives", "4.3.0"); + + var model = InputFactory.Model( + "ModelWithExternalProperty", + properties: + [ + InputFactory.Property("ipAddress", externalType), + InputFactory.Property("name", InputPrimitiveType.String) + ]); + + MockHelpers.LoadMockGenerator( + inputModelTypes: [model]); + + var modelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders + .SingleOrDefault(t => t.Name == "ModelWithExternalProperty") as ModelProvider; + Assert.IsNotNull(modelProvider); + + // The property with external type should be resolved + var ipAddressProp = modelProvider!.Properties.FirstOrDefault(p => p.Name == "IpAddress"); + Assert.IsNotNull(ipAddressProp); + Assert.IsNotNull(ipAddressProp!.Type.FrameworkType); + + // Verify it's the correct framework type + var normalProp = modelProvider.Properties.FirstOrDefault(p => p.Name == "Name"); + Assert.IsNotNull(normalProp); + Assert.AreEqual(typeof(string), normalProp!.Type.FrameworkType); + } + + [Test] + public void UnsupportedExternalTypeEmitsDiagnostic() + { + // Test an external type that cannot be resolved (non-framework type) + var externalType = InputFactory.External("Azure.Core.Expressions.DataFactoryExpression"); + + var model = InputFactory.Model( + "ModelWithUnsupportedExternal", + properties: + [ + InputFactory.Property("expression", externalType), + InputFactory.Property("value", InputPrimitiveType.String) + ]); + + MockHelpers.LoadMockGenerator( + inputModelTypes: [model]); + + var modelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders + .SingleOrDefault(t => t.Name == "ModelWithUnsupportedExternal") as ModelProvider; + Assert.IsNotNull(modelProvider); + + // The unsupported external type property should be skipped (null type results in skipped property) + // Only the normal property should remain + var props = modelProvider!.Properties; + + // The value property should exist + var valueProp = props.FirstOrDefault(p => p.Name == "Value"); + Assert.IsNotNull(valueProp); + } } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/common/InputFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/common/InputFactory.cs index 006891c3231..7508737ba6f 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/common/InputFactory.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/common/InputFactory.cs @@ -623,6 +623,11 @@ public static InputServiceMethodResponse ServiceMethodResponse(InputType? type, return new InputServiceMethodResponse(type, resultSegments); } + public static InputExternalType External(string identity, string? package = null, string? minVersion = null) + { + return new InputExternalType(identity, package, minVersion); + } + private static readonly Dictionary> _childClientsCache = new(); public static InputClient Client(string name, string clientNamespace = "Sample", string? doc = null, IEnumerable? methods = null, IEnumerable? parameters = null, InputClient? parent = null, string? crossLanguageDefinitionId = null, IEnumerable? apiVersions = null)