diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Primitives/ScmKnownParameters.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Primitives/ScmKnownParameters.cs index ccfe6ced84..142a73acf6 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Primitives/ScmKnownParameters.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Primitives/ScmKnownParameters.cs @@ -34,5 +34,15 @@ public static ParameterProvider ClientOptions(CSharpType clientOptionsType) public static readonly ParameterProvider MatchConditionsParameter = new("matchConditions", $"The content to send as the request conditions of the request.", ClientModelPlugin.Instance.TypeFactory.MatchConditionsType(), DefaultOf(ClientModelPlugin.Instance.TypeFactory.MatchConditionsType())); public static readonly ParameterProvider RequestOptions = new("options", $"The request options, which can override default behaviors of the client pipeline on a per-call basis.", typeof(RequestOptions)); public static readonly ParameterProvider BinaryContent = new("content", $"The content to send as the body of the request.", typeof(BinaryContent)) { Validation = ParameterValidationType.AssertNotNull }; + + // Known header parameters + public static readonly ParameterProvider RepeatabilityRequestId = new("repeatabilityRequestId", FormattableStringHelpers.Empty, typeof(Guid)) + { + DefaultValue = Static(typeof(Guid)).Invoke(nameof(Guid.NewGuid)).Invoke(nameof(string.ToString)) + }; + public static readonly ParameterProvider RepeatabilityFirstSent = new("repeatabilityFirstSent", FormattableStringHelpers.Empty, typeof(DateTimeOffset)) + { + DefaultValue = Static(typeof(DateTimeOffset)).Property(nameof(DateTimeOffset.Now)) + }; } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Providers/RestClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Providers/RestClientProvider.cs index 3d3c8bd84f..2a99952319 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Providers/RestClientProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Providers/RestClientProvider.cs @@ -4,6 +4,7 @@ using System; using System.ClientModel.Primitives; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Net.Http; @@ -21,6 +22,13 @@ namespace Microsoft.Generator.CSharp.ClientModel.Providers { public class RestClientProvider : TypeProvider { + private const string RepeatabilityRequestIdHeader = "Repeatability-Request-ID"; + private const string RepeatabilityFirstSentHeader = "Repeatability-First-Sent"; + private static readonly Dictionary _knownSpecialHeaderParams = new(StringComparer.OrdinalIgnoreCase) + { + { RepeatabilityRequestIdHeader, ScmKnownParameters.RepeatabilityRequestId }, + { RepeatabilityFirstSentHeader, ScmKnownParameters.RepeatabilityFirstSent } + }; private Dictionary? _methodCache; private Dictionary MethodCache => _methodCache ??= []; @@ -186,8 +194,6 @@ private PropertyProvider GetClassifier(InputOperation operation) private IEnumerable AppendHeaderParameters(ScopedApi request, InputOperation operation, Dictionary paramMap) { - //TODO handle special headers like Repeatability-First-Sent which shouldn't be params but sent as DateTimeOffset.Now.ToString("R") - //https://github.com/microsoft/typespec/issues/3936 List statements = new(operation.Parameters.Count); foreach (var inputParameter in operation.Parameters) @@ -328,6 +334,11 @@ private static void GetParamInfo(Dictionary paramMap, valueExpression = Literal((inputParam.Type as InputLiteralType)?.Value); format = ClientModelPlugin.Instance.TypeFactory.GetSerializationFormat(inputParam.Type).ToFormatSpecifier(); } + else if (TryGetSpecialHeaderParam(inputParam, out var parameterProvider)) + { + valueExpression = parameterProvider.DefaultValue!; + format = ClientModelPlugin.Instance.TypeFactory.GetSerializationFormat(inputParam.Type).ToFormatSpecifier(); + } else { var paramProvider = paramMap[inputParam.Name]; @@ -345,6 +356,17 @@ private static void GetParamInfo(Dictionary paramMap, } } + private static bool TryGetSpecialHeaderParam(InputParameter inputParameter, [NotNullWhen(true)] out ParameterProvider? parameterProvider) + { + if (inputParameter.Location == RequestLocation.Header) + { + return _knownSpecialHeaderParams.TryGetValue(inputParameter.NameInRequest, out parameterProvider); + } + + parameterProvider = null; + return false; + } + internal MethodProvider GetCreateRequestMethod(InputOperation operation) { _ = Methods; // Ensure methods are built @@ -356,7 +378,7 @@ internal static List GetMethodParameters(InputOperation opera List methodParameters = new(); foreach (InputParameter inputParam in operation.Parameters) { - if (inputParam.Kind != InputOperationParameterKind.Method) + if (inputParam.Kind != InputOperationParameterKind.Method || TryGetSpecialHeaderParam(inputParam, out var _)) continue; ParameterProvider? parameter = ClientModelPlugin.Instance.TypeFactory.CreateParameter(inputParam); diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/RestClientProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/ClientProviders/RestClientProviderTests.cs similarity index 65% rename from packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/RestClientProviderTests.cs rename to packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/ClientProviders/RestClientProviderTests.cs index c84ab0f86f..c182da5c4c 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/RestClientProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/ClientProviders/RestClientProviderTests.cs @@ -1,6 +1,7 @@ // 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.ClientModel.Providers; @@ -10,7 +11,7 @@ using Microsoft.Generator.CSharp.Tests.Common; using NUnit.Framework; -namespace Microsoft.Generator.CSharp.ClientModel.Tests.Providers +namespace Microsoft.Generator.CSharp.ClientModel.Tests.Providers.ClientProviders { public class RestClientProviderTests { @@ -35,7 +36,15 @@ public void TestRestClientMethods(InputOperation inputOperation) var parameters = signature.Parameters; Assert.IsNotNull(parameters); - Assert.AreEqual(inputOperation.Parameters.Count + 1, parameters.Count); + var specialHeaderParamCount = inputOperation.Parameters.Count(p => p.Location == RequestLocation.Header); + Assert.AreEqual(inputOperation.Parameters.Count - specialHeaderParamCount + 1, parameters.Count); + + if (specialHeaderParamCount > 0) + { + Assert.IsFalse(parameters.Any(p => + p.Name.Equals("repeatabilityFirstSent", StringComparison.OrdinalIgnoreCase) && + p.Name.Equals("repeatabilityRequestId", StringComparison.OrdinalIgnoreCase))); + } } [Test] @@ -97,10 +106,48 @@ public void ValidateProperties() Assert.IsFalse(pipelineMessageClassifier2xxAnd4xx.Body.HasSetter); } + [TestCaseSource(nameof(GetMethodParametersTestCases))] + public void TestGetMethodParameters(InputOperation inputOperation) + { + var methodParameters = RestClientProvider.GetMethodParameters(inputOperation); + + Assert.IsTrue(methodParameters.Count > 0); + + if (inputOperation.Parameters.Any(p => p.Location == RequestLocation.Header)) + { + // validate no special header parameters are in the method parameters + Assert.IsFalse(methodParameters.Any(p => + p.Name.Equals("repeatabilityFirstSent", StringComparison.OrdinalIgnoreCase) && + p.Name.Equals("repeatabilityRequestId", StringComparison.OrdinalIgnoreCase))); + } + } + + [Test] + public void ValidateClientWithSpecialHeaders() + { + var clientProvider = new ClientProvider(SingleOpInputClient); + var restClientProvider = new MockClientProvider(SingleOpInputClient, clientProvider); + var writer = new TypeProviderWriter(restClientProvider); + var file = writer.Write(); + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); + } + private readonly static InputOperation BasicOperation = InputFactory.Operation( "CreateMessage", parameters: [ + InputFactory.Parameter( + "repeatabilityFirstSent", + new InputDateTimeType(DateTimeKnownEncoding.Rfc7231, "utcDateTime", "TypeSpec.utcDateTime", InputPrimitiveType.String), + nameInRequest: "repeatability-first-sent", + location: RequestLocation.Header, + isRequired: false), + InputFactory.Parameter( + "repeatabilityRequestId", + InputPrimitiveType.String, + nameInRequest: "repeatability-request-ID", + location: RequestLocation.Header, + isRequired: false), InputFactory.Parameter("message", InputPrimitiveType.Boolean, isRequired: true) ]); @@ -110,5 +157,26 @@ public void ValidateProperties() [ new TestCaseData(BasicOperation) ]; + + private static IEnumerable GetMethodParametersTestCases => + [ + new TestCaseData(BasicOperation) + ]; + + private class MockClientProvider : RestClientProvider + { + public MockClientProvider(InputClient inputClient, ClientProvider clientProvider) : base(inputClient, clientProvider) { } + + protected override MethodProvider[] BuildMethods() + { + return [.. base.BuildMethods()]; + } + + protected override FieldProvider[] BuildFields() => []; + protected override ConstructorProvider[] BuildConstructors() => []; + protected override PropertyProvider[] BuildProperties() => []; + + protected override TypeProvider[] BuildNestedTypes() => []; + } } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/ClientProviders/TestData/RestClientProviderTests/ValidateClientWithSpecialHeaders.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/ClientProviders/TestData/RestClientProviderTests/ValidateClientWithSpecialHeaders.cs new file mode 100644 index 0000000000..43ec33c92c --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/ClientProviders/TestData/RestClientProviderTests/ValidateClientWithSpecialHeaders.cs @@ -0,0 +1,30 @@ +// + +#nullable disable + +using System; +using System.ClientModel; +using System.ClientModel.Primitives; + +namespace sample.namespace +{ + /// + public partial class TestClient + { + internal global::System.ClientModel.Primitives.PipelineMessage CreateCreateMessageRequest(global::System.ClientModel.BinaryContent content, global::System.ClientModel.Primitives.RequestOptions options) + { + global::System.ClientModel.Primitives.PipelineMessage message = Pipeline.CreateMessage(); + message.ResponseClassifier = PipelineMessageClassifier200; + global::System.ClientModel.Primitives.PipelineRequest request = message.Request; + request.Method = "GET"; + global::sample.namespace.ClientUriBuilder uri = new global::sample.namespace.ClientUriBuilder(); + uri.Reset(_endpoint); + request.Uri = uri.ToUri(); + request.Headers.Set("repeatability-first-sent", global::System.DateTimeOffset.Now.ToString("R")); + request.Headers.Set("repeatability-request-ID", global::System.Guid.NewGuid().ToString()); + request.Content = content; + message.Apply(options); + return message; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/ScmKnownParametersTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/ScmKnownParametersTests.cs index ce516d8adc..b45b8f18ae 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/ScmKnownParametersTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/ScmKnownParametersTests.cs @@ -1,20 +1,44 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; using Microsoft.Generator.CSharp.ClientModel.Primitives; using Microsoft.Generator.CSharp.Primitives; using NUnit.Framework; +using static Microsoft.Generator.CSharp.Snippets.Snippet; namespace Microsoft.Generator.CSharp.ClientModel.Tests { public class ScmKnownParametersTests { - [Test] - public void BinaryDataParametersHasValidation() + [OneTimeSetUp] + public void Setup() { MockHelpers.LoadMockPlugin(); + + } + + [Test] + public void BinaryDataParameterHasValidation() + { var parameter = ScmKnownParameters.BinaryContent; Assert.AreEqual(ParameterValidationType.AssertNotNull, parameter.Validation); } + + [Test] + public void RepeatabilityRequestIdParamHasDefaultValue() + { + var parameter = ScmKnownParameters.RepeatabilityRequestId; + var expectedDefaultValue = Static(typeof(Guid)).Invoke(nameof(Guid.NewGuid)).Invoke(nameof(string.ToString)); + Assert.AreEqual(expectedDefaultValue, parameter.DefaultValue); + } + + [Test] + public void RepeatabilityFirstSentParamHasDefaultValue() + { + var parameter = ScmKnownParameters.RepeatabilityFirstSent; + var expectedDefaultValue = Static(typeof(DateTimeOffset)).Property(nameof(DateTimeOffset.Now)); + Assert.AreEqual(expectedDefaultValue, parameter.DefaultValue); + } } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/common/InputFactory.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/common/InputFactory.cs index bb925de8a0..a332bb0944 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/common/InputFactory.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/common/InputFactory.cs @@ -55,6 +55,7 @@ public static InputConstant Int64(long value) public static InputParameter Parameter( string name, InputType type, + string? nameInRequest = null, InputConstant? defaultValue = null, RequestLocation location = RequestLocation.Body, bool isRequired = false, @@ -64,7 +65,7 @@ public static InputParameter Parameter( { return new InputParameter( name, - name, + nameInRequest ?? name, $"{name} description", type, location, 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 f8cf41d8fb..ffdb1447d0 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 @@ -210,7 +210,7 @@ internal PipelineMessage CreateFriendlyModelRequest(RequestOptions options) return message; } - internal PipelineMessage CreateAddTimeHeaderRequest(DateTimeOffset repeatabilityFirstSent, RequestOptions options) + internal PipelineMessage CreateAddTimeHeaderRequest(RequestOptions options) { PipelineMessage message = Pipeline.CreateMessage(); message.ResponseClassifier = PipelineMessageClassifier204; @@ -220,7 +220,7 @@ internal PipelineMessage CreateAddTimeHeaderRequest(DateTimeOffset repeatability uri.Reset(_endpoint); uri.AppendPath("/", false); request.Uri = uri.ToUri(); - request.Headers.Set("Repeatability-First-Sent", repeatabilityFirstSent.ToString("R")); + request.Headers.Set("Repeatability-First-Sent", DateTimeOffset.Now.ToString("R")); message.Apply(options); return message; } 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 d4f87840f8..637cd131a0 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 @@ -737,13 +737,12 @@ public virtual async Task> FriendlyModelAsync() /// /// /// - /// /// The request options, which can override default behaviors of the client pipeline on a per-call basis. /// Service returned a non-success status code. /// The response returned from the service. - public virtual ClientResult AddTimeHeader(DateTimeOffset repeatabilityFirstSent, RequestOptions options) + public virtual ClientResult AddTimeHeader(RequestOptions options) { - using PipelineMessage message = CreateAddTimeHeaderRequest(repeatabilityFirstSent, options); + using PipelineMessage message = CreateAddTimeHeaderRequest(options); return ClientResult.FromResponse(Pipeline.ProcessMessage(message, options)); } @@ -755,30 +754,27 @@ public virtual ClientResult AddTimeHeader(DateTimeOffset repeatabilityFirstSent, /// /// /// - /// /// The request options, which can override default behaviors of the client pipeline on a per-call basis. /// Service returned a non-success status code. /// The response returned from the service. - public virtual async Task AddTimeHeaderAsync(DateTimeOffset repeatabilityFirstSent, RequestOptions options) + public virtual async Task AddTimeHeaderAsync(RequestOptions options) { - using PipelineMessage message = CreateAddTimeHeaderRequest(repeatabilityFirstSent, options); + using PipelineMessage message = CreateAddTimeHeaderRequest(options); return ClientResult.FromResponse(await Pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false)); } /// addTimeHeader. - /// /// Service returned a non-success status code. - public virtual ClientResult AddTimeHeader(DateTimeOffset repeatabilityFirstSent) + public virtual ClientResult AddTimeHeader() { - return AddTimeHeader(repeatabilityFirstSent, null); + return AddTimeHeader(null); } /// addTimeHeader. - /// /// Service returned a non-success status code. - public virtual async Task AddTimeHeaderAsync(DateTimeOffset repeatabilityFirstSent) + public virtual async Task AddTimeHeaderAsync() { - return await AddTimeHeaderAsync(repeatabilityFirstSent, null).ConfigureAwait(false); + return await AddTimeHeaderAsync(null).ConfigureAwait(false); } ///