diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Providers/ClientOptionsProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Providers/ClientOptionsProvider.cs index a936f139f3..fff48b17ef 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Providers/ClientOptionsProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Providers/ClientOptionsProvider.cs @@ -43,6 +43,7 @@ public ClientOptionsProvider(InputClient inputClient, ClientProvider clientProvi } } + internal PropertyProvider? VersionProperty => _versionProperty; private TypeProvider? ServiceVersionEnum => _serviceVersionEnum?.Value; private FieldProvider? LatestVersionField => _latestVersionField ??= BuildLatestVersionField(); diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Providers/ClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Providers/ClientProvider.cs index cc3cfbb3e0..67e1153b12 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Providers/ClientProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Providers/ClientProvider.cs @@ -34,11 +34,13 @@ public class ClientProvider : TypeProvider private readonly FieldProvider? _apiKeyAuthField; private readonly FieldProvider? _authorizationHeaderConstant; private readonly FieldProvider? _authorizationApiKeyPrefixConstant; + private FieldProvider? _apiVersionField; private readonly ParameterProvider[] _subClientInternalConstructorParams; private IReadOnlyList>? _subClients; private ParameterProvider? _clientOptionsParameter; private ClientOptionsProvider? _clientOptions; private RestClientProvider? _restClient; + private readonly InputParameter[] _allClientParameters; private ParameterProvider? ClientOptionsParameter => _clientOptionsParameter ??= ClientOptions != null ? ScmKnownParameters.ClientOptions(ClientOptions.Type) @@ -105,6 +107,8 @@ public ClientProvider(InputClient inputClient) } _endpointParameterName = new(GetEndpointParameterName); + + _allClientParameters = _inputClient.Parameters.Concat(_inputClient.Operations.SelectMany(op => op.Parameters).Where(p => p.Kind == InputOperationParameterKind.Client)).DistinctBy(p => p.Name).ToArray(); } private List? _uriParameters; @@ -160,19 +164,23 @@ protected override FieldProvider[] BuildFields() } } - // Add optional client parameters as fields - foreach (var p in _inputClient.Parameters) + foreach (var p in _allClientParameters) { if (!p.IsEndpoint) { var type = ClientModelPlugin.Instance.TypeFactory.CreateCSharpType(p.Type); if (type != null) { - fields.Add(new( + FieldProvider field = new( FieldModifiers.Private | FieldModifiers.ReadOnly, type, "_" + p.Name.ToVariableName(), - this)); + this); + if (p.IsApiVersion) + { + _apiVersionField = field; + } + fields.Add(field); } } } @@ -241,9 +249,9 @@ private IReadOnlyList GetRequiredParameters() _uriParameters = []; ParameterProvider? currentParam = null; - foreach (var parameter in _inputClient.Parameters) + foreach (var parameter in _allClientParameters) { - if (parameter.IsRequired && !parameter.IsEndpoint) + if (parameter.IsRequired && !parameter.IsEndpoint && !parameter.IsApiVersion) { currentParam = ClientModelPlugin.Instance.TypeFactory.CreateParameter(parameter); currentParam.Field = Fields.FirstOrDefault(f => f.Name == "_" + parameter.Name); @@ -304,10 +312,16 @@ private MethodBodyStatement[] BuildPrimaryConstructorBody(IReadOnlyList(signature.Parameters.ToDictionary(p => p.Name)); + foreach (var param in ClientProvider.GetUriParameters()) { paramMap[param.Name] = param; } + /* add client-level parameter.*/ + foreach (var inputParam in operation.Parameters) + { + if (inputParam.Kind == InputOperationParameterKind.Client && !paramMap.ContainsKey(inputParam.Name)) + { + var param = ClientModelPlugin.Instance.TypeFactory.CreateParameter(inputParam); + param.Field = ClientProvider.Fields.FirstOrDefault(f => f.Name == "_" + inputParam.Name); + paramMap[inputParam.Name] = param; + } + } + var classifier = GetClassifier(operation); return new MethodProvider( diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/ClientProviders/ClientProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/ClientProviders/ClientProviderTests.cs index 4df37f6099..eb66522855 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/ClientProviders/ClientProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/ClientProviders/ClientProviderTests.cs @@ -400,6 +400,26 @@ public void ValidateClientWithSpread(InputClient inputClient) } + [Test] + public void TestApiVersionOfClient() + { + var client = InputFactory.Client(TestClientName, + operations: [ + InputFactory.Operation("OperationWithApiVersion", + parameters: [InputFactory.Parameter("apiVersion", InputPrimitiveType.String, isRequired: true, location: RequestLocation.Query, kind: InputOperationParameterKind.Client)]) + ]); + var clientProvider = new ClientProvider(client); + Assert.IsNotNull(clientProvider); + + /* verify that the client has apiVersion field */ + Assert.IsNotNull(clientProvider.Fields.FirstOrDefault(f => f.Name.Equals("_apiVersion"))); + + var method = clientProvider.Methods.FirstOrDefault(m => m.Signature.Name.Equals("OperationWithApiVersion")); + Assert.IsNotNull(method); + /* verify that the method does not have apiVersion parameter */ + Assert.IsNull(method?.Signature.Parameters.FirstOrDefault(p => p.Name.Equals("apiVersion"))); + } + private static InputClient GetEnumQueryParamClient() => InputFactory.Client( TestClientName, diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/ClientProviders/RestClientProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/ClientProviders/RestClientProviderTests.cs index 39ac3d5c2b..5197c96e1f 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/ClientProviders/RestClientProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/ClientProviders/RestClientProviderTests.cs @@ -12,6 +12,7 @@ using Microsoft.Generator.CSharp.Tests.Common; using NUnit.Framework; using Microsoft.Generator.CSharp.Snippets; +using Microsoft.Generator.CSharp.Statements; namespace Microsoft.Generator.CSharp.ClientModel.Tests.Providers.ClientProviders { @@ -195,6 +196,26 @@ public void ValidateClientWithSpecialHeaders() Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); } + [Test] + public void ValidateClientWithApiVersion() + { + var client = InputFactory.Client("TestClient", + operations: [ + InputFactory.Operation("OperationWithApiVersion", + parameters: [InputFactory.Parameter("apiVersion", InputPrimitiveType.String, isRequired: true, location: RequestLocation.Query, kind: InputOperationParameterKind.Client)]) + ]); + var clientProvider = new ClientProvider(client); + var restClientProvider = new MockClientProvider(client, clientProvider); + var method = restClientProvider.Methods.FirstOrDefault(m => m.Signature.Name == "CreateOperationWithApiVersionRequest"); + Assert.IsNotNull(method); + /* verify that there is no apiVersion parameter in method signature. */ + Assert.IsNull(method?.Signature.Parameters.FirstOrDefault(p => p.Name.Equals("apiVersion"))); + var bodyStatements = method?.BodyStatements as MethodBodyStatements; + Assert.IsNotNull(bodyStatements); + /* verify that it will use client _apiVersion field to append query parameter. */ + Assert.IsTrue(bodyStatements!.Statements.Any(s => s.ToDisplayString() == "uri.AppendQuery(\"apiVersion\", _apiVersion, true);\n")); + } + private readonly static InputOperation BasicOperation = InputFactory.Operation( "CreateMessage", parameters: diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/Unbranded-TypeSpec.tsp b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/Unbranded-TypeSpec.tsp index 7849eacd77..a16e47d719 100644 --- a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/Unbranded-TypeSpec.tsp +++ b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/Unbranded-TypeSpec.tsp @@ -367,3 +367,9 @@ op stillConvenient(): void; @head @convenientAPI(true) op headAsBoolean(@path id: string): void; + +@route("/WithApiVersion") +@doc("Return hi again") +@get +@convenientAPI(true) +op WithApiVersion(@header p1: string, @query apiVersion: string): void; diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/UnbrandedTypeSpecClient.RestClient.cs b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/UnbrandedTypeSpecClient.RestClient.cs index ab5ad5c181..79873cfb4d 100644 --- a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/UnbrandedTypeSpecClient.RestClient.cs +++ b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/UnbrandedTypeSpecClient.RestClient.cs @@ -320,6 +320,22 @@ internal PipelineMessage CreateHeadAsBooleanRequest(string id, RequestOptions op return message; } + internal PipelineMessage CreateWithApiVersionRequest(string p1, RequestOptions options) + { + PipelineMessage message = Pipeline.CreateMessage(); + message.ResponseClassifier = PipelineMessageClassifier204; + PipelineRequest request = message.Request; + request.Method = "GET"; + ClientUriBuilder uri = new ClientUriBuilder(); + uri.Reset(_endpoint); + uri.AppendPath("/WithApiVersion", false); + uri.AppendQuery("apiVersion", _apiVersion, true); + request.Uri = uri.ToUri(); + request.Headers.Set("p1", p1); + message.Apply(options); + return message; + } + private class Classifier2xxAnd4xx : PipelineMessageClassifier { public override bool TryClassify(PipelineMessage message, out bool isError) diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/UnbrandedTypeSpecClient.cs b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/UnbrandedTypeSpecClient.cs index edf710ec3a..b7b7de193e 100644 --- a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/UnbrandedTypeSpecClient.cs +++ b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/UnbrandedTypeSpecClient.cs @@ -19,6 +19,7 @@ public partial class UnbrandedTypeSpecClient private const string AuthorizationHeader = "my-api-key"; /// A credential used to authenticate to the service. private readonly ApiKeyCredential _keyCredential; + private readonly string _apiVersion; /// Initializes a new instance of UnbrandedTypeSpecClient for mocking. protected UnbrandedTypeSpecClient() @@ -48,6 +49,7 @@ public UnbrandedTypeSpecClient(Uri endpoint, ApiKeyCredential keyCredential, Unb _endpoint = endpoint; _keyCredential = keyCredential; Pipeline = ClientPipeline.Create(options, Array.Empty(), new PipelinePolicy[] { ApiKeyAuthenticationPolicy.CreateHeaderApiKeyPolicy(_keyCredential, AuthorizationHeader) }, Array.Empty()); + _apiVersion = options.Version; } /// The HTTP pipeline for sending and receiving REST requests and responses. @@ -1160,5 +1162,69 @@ public virtual async Task HeadAsBooleanAsync(string id) return await HeadAsBooleanAsync(id, null).ConfigureAwait(false); } + + /// + /// [Protocol Method] Return hi again + /// + /// + /// This protocol method allows explicit creation of the request and processing of the response for advanced scenarios. + /// + /// + /// + /// + /// The request options, which can override default behaviors of the client pipeline on a per-call basis. + /// is null. + /// Service returned a non-success status code. + /// The response returned from the service. + public virtual ClientResult WithApiVersion(string p1, RequestOptions options) + { + Argument.AssertNotNull(p1, nameof(p1)); + + using PipelineMessage message = CreateWithApiVersionRequest(p1, options); + return ClientResult.FromResponse(Pipeline.ProcessMessage(message, options)); + } + + /// + /// [Protocol Method] Return hi again + /// + /// + /// This protocol method allows explicit creation of the request and processing of the response for advanced scenarios. + /// + /// + /// + /// + /// The request options, which can override default behaviors of the client pipeline on a per-call basis. + /// is null. + /// Service returned a non-success status code. + /// The response returned from the service. + public virtual async Task WithApiVersionAsync(string p1, RequestOptions options) + { + Argument.AssertNotNull(p1, nameof(p1)); + + using PipelineMessage message = CreateWithApiVersionRequest(p1, options); + return ClientResult.FromResponse(await Pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false)); + } + + /// Return hi again. + /// + /// is null. + /// Service returned a non-success status code. + public virtual ClientResult WithApiVersion(string p1) + { + Argument.AssertNotNull(p1, nameof(p1)); + + return WithApiVersion(p1, null); + } + + /// Return hi again. + /// + /// is null. + /// Service returned a non-success status code. + public virtual async Task WithApiVersionAsync(string p1) + { + Argument.AssertNotNull(p1, nameof(p1)); + + return await WithApiVersionAsync(p1, null).ConfigureAwait(false); + } } } diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/tspCodeModel.json b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/tspCodeModel.json index cbce2fcc40..c8f4572fc3 100644 --- a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/tspCodeModel.json +++ b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/tspCodeModel.json @@ -3641,10 +3641,94 @@ "GenerateConvenienceMethod": true, "CrossLanguageDefinitionId": "UnbrandedTypeSpec.headAsBoolean", "Decorators": [] + }, + { + "$id": "339", + "Name": "WithApiVersion", + "ResourceName": "UnbrandedTypeSpec", + "Description": "Return hi again", + "Accessibility": "public", + "Parameters": [ + { + "$ref": "193" + }, + { + "$id": "340", + "Name": "p1", + "NameInRequest": "p1", + "Type": { + "$id": "341", + "kind": "string", + "name": "string", + "crossLanguageDefinitionId": "TypeSpec.string", + "decorators": [] + }, + "Location": "Header", + "IsApiVersion": false, + "IsContentType": false, + "IsEndpoint": false, + "Explode": false, + "IsRequired": true, + "Kind": "Method", + "Decorators": [], + "SkipUrlEncoding": false + }, + { + "$id": "342", + "Name": "apiVersion", + "NameInRequest": "apiVersion", + "Type": { + "$id": "343", + "kind": "string", + "name": "string", + "crossLanguageDefinitionId": "TypeSpec.string", + "decorators": [] + }, + "Location": "Query", + "IsApiVersion": true, + "IsContentType": false, + "IsEndpoint": false, + "Explode": false, + "IsRequired": true, + "Kind": "Client", + "DefaultValue": { + "$id": "344", + "Type": { + "$id": "345", + "kind": "string", + "name": "string", + "crossLanguageDefinitionId": "TypeSpec.string" + }, + "Value": "2024-08-16-preview" + }, + "Decorators": [], + "SkipUrlEncoding": false + } + ], + "Responses": [ + { + "$id": "346", + "StatusCodes": [ + 204 + ], + "BodyMediaType": "Json", + "Headers": [], + "IsErrorResponse": false + } + ], + "HttpMethod": "GET", + "RequestBodyMediaType": "None", + "Uri": "{unbrandedTypeSpecUrl}", + "Path": "/WithApiVersion", + "BufferResponse": true, + "GenerateProtocolMethod": true, + "GenerateConvenienceMethod": true, + "CrossLanguageDefinitionId": "UnbrandedTypeSpec.WithApiVersion", + "Decorators": [] } ], "Protocol": { - "$id": "339" + "$id": "347" }, "Parameters": [ { @@ -3655,9 +3739,9 @@ } ], "Auth": { - "$id": "340", + "$id": "348", "ApiKey": { - "$id": "341", + "$id": "349", "Name": "my-api-key" } }