diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a3fa5fe..da4f3f6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.7.1] - 2023-11-13 + +### Changed + +- Fixed an issue where path and query parameters of enum type would not be expanded properly. [microsoft/kiota#3693](https://github.com/microsoft/kiota/issues/3693) + ## [1.7.0] - 2023-11-07 ### Added diff --git a/Microsoft.Kiota.Abstractions.Tests/Mocks/TestEnum.cs b/Microsoft.Kiota.Abstractions.Tests/Mocks/TestEnum.cs new file mode 100644 index 00000000..ba4fa818 --- /dev/null +++ b/Microsoft.Kiota.Abstractions.Tests/Mocks/TestEnum.cs @@ -0,0 +1,11 @@ +using System.Runtime.Serialization; + +namespace Microsoft.Kiota.Abstractions.Tests.Mocks; + +public enum TestEnum +{ + [EnumMember(Value = "1")] + First, + [EnumMember(Value = "2")] + Second, +} \ No newline at end of file diff --git a/Microsoft.Kiota.Abstractions.Tests/RequestInformationTests.cs b/Microsoft.Kiota.Abstractions.Tests/RequestInformationTests.cs index 292bfdac..10439093 100644 --- a/Microsoft.Kiota.Abstractions.Tests/RequestInformationTests.cs +++ b/Microsoft.Kiota.Abstractions.Tests/RequestInformationTests.cs @@ -206,7 +206,7 @@ public void SetsPathParametersOfDateType() }; // Act - var date = new Date(2023,10,26); + var date = new Date(2023, 10, 26); var pathParameters = new Dictionary { { "%24date", date } @@ -228,7 +228,7 @@ public void SetsPathParametersOfTimeType() }; // Act - var time = new Time(6,0,0); + var time = new Time(6, 0, 0); var pathParameters = new Dictionary { { "%24time", time } @@ -446,6 +446,62 @@ public void SetsBoundaryOnMultipartBody() Assert.Single(contentType); Assert.Equal("multipart/form-data; boundary=" + multipartBody.Boundary, contentType.First()); } + [Fact] + public void SetsEnumValueInQueryParameters() + { + // Arrange + var testRequest = new RequestInformation() + { + HttpMethod = Method.GET, + UrlTemplate = "http://localhost/me{?dataset}" + }; + // Act + testRequest.AddQueryParameters(new GetQueryParameters { DataSet = TestEnum.First }); + // Assert + Assert.Equal("http://localhost/me?dataset=1", testRequest.URI.ToString()); + } + [Fact] + public void SetsEnumValuesInQueryParameters() + { + // Arrange + var testRequest = new RequestInformation() + { + HttpMethod = Method.GET, + UrlTemplate = "http://localhost/me{?datasets}" + }; + // Act + testRequest.AddQueryParameters(new GetQueryParameters { DataSets = new TestEnum[] { TestEnum.First, TestEnum.Second } }); + // Assert + Assert.Equal("http://localhost/me?datasets=1,2", testRequest.URI.ToString()); + } + [Fact] + public void SetsEnumValueInPathParameters() + { + // Arrange + var testRequest = new RequestInformation() + { + HttpMethod = Method.GET, + UrlTemplate = "http://localhost/{dataset}" + }; + // Act + testRequest.PathParameters.Add("dataset", TestEnum.First); + // Assert + Assert.Equal("http://localhost/1", testRequest.URI.ToString()); + } + [Fact] + public void SetsEnumValuesInPathParameters() + { + // Arrange + var testRequest = new RequestInformation() + { + HttpMethod = Method.GET, + UrlTemplate = "http://localhost/{dataset}" + }; + // Act + testRequest.PathParameters.Add("dataset", new TestEnum[] { TestEnum.First, TestEnum.Second }); + // Assert + Assert.Equal("http://localhost/1,2", testRequest.URI.ToString()); + } } /// The messages in a mailbox or folder. Read-only. Nullable. @@ -471,5 +527,11 @@ internal class GetQueryParameters public string Search { get; set; } /// Restrict to TenantId public string TenantId { get; set; } + /// Which Dataset to use + [QueryParameter("dataset")] + public TestEnum DataSet { get; set; } + /// Which Dataset to use + [QueryParameter("datasets")] + public TestEnum[] DataSets { get; set; } } } diff --git a/Microsoft.Kiota.Abstractions.Tests/Serialization/Mocks/TestEntity.cs b/Microsoft.Kiota.Abstractions.Tests/Serialization/Mocks/TestEntity.cs index 958ddca4..4e98957f 100644 --- a/Microsoft.Kiota.Abstractions.Tests/Serialization/Mocks/TestEntity.cs +++ b/Microsoft.Kiota.Abstractions.Tests/Serialization/Mocks/TestEntity.cs @@ -2,39 +2,38 @@ using System.Collections.Generic; using Microsoft.Kiota.Abstractions.Serialization; -namespace Microsoft.Kiota.Abstractions.Tests.Serialization.Mocks +namespace Microsoft.Kiota.Abstractions.Tests.Serialization.Mocks; +public class TestEntity : IParsable, IAdditionalDataHolder { - public class TestEntity : IParsable, IAdditionalDataHolder + /// Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. + public IDictionary AdditionalData { get; set; } + /// Read-only. + public string Id { get; set; } + /// Read-only. + public TimeSpan? WorkDuration { get; set; } + /// Read-only. + public Date? BirthDay { get; set; } + /// Read-only. + public Time? StartWorkTime { get; set; } + /// Read-only. + public Time? EndWorkTime { get; set; } + /// Read-only. + public DateTimeOffset? CreatedDateTime { get; set; } + /// Read-only. + public string OfficeLocation { get; set; } + /// + /// Instantiates a new entity and sets the default values. + /// + public TestEntity() { - /// Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. - public IDictionary AdditionalData { get; set; } - /// Read-only. - public string Id { get; set; } - /// Read-only. - public TimeSpan? WorkDuration { get; set; } - /// Read-only. - public Date? BirthDay { get; set; } - /// Read-only. - public Time? StartWorkTime { get; set; } - /// Read-only. - public Time? EndWorkTime { get; set; } - /// Read-only. - public DateTimeOffset? CreatedDateTime { get; set; } - /// Read-only. - public string OfficeLocation { get; set; } - /// - /// Instantiates a new entity and sets the default values. - /// - public TestEntity() - { - AdditionalData = new Dictionary(); - } - /// - /// The deserialization information for the current model - /// - public virtual IDictionary> GetFieldDeserializers() - { - return new Dictionary> { + AdditionalData = new Dictionary(); + } + /// + /// The deserialization information for the current model + /// + public virtual IDictionary> GetFieldDeserializers() + { + return new Dictionary> { {"id", n => { Id = n.GetStringValue(); } }, {"createdDateTime", n => { CreatedDateTime = n.GetDateTimeOffsetValue(); } }, {"officeLocation", n => { OfficeLocation = n.GetStringValue(); } }, @@ -43,32 +42,31 @@ public virtual IDictionary> GetFieldDeserializers() {"startWorkTime", n => { StartWorkTime = n.GetTimeValue(); } }, {"endWorkTime", n => { EndWorkTime = n.GetTimeValue(); } }, }; - } - /// - /// Serializes information the current object - /// Serialization writer to use to serialize this model - /// - public virtual void Serialize(ISerializationWriter writer) - { - _ = writer ?? throw new ArgumentNullException(nameof(writer)); - writer.WriteStringValue("id", Id); - writer.WriteDateTimeOffsetValue("createdDateTime", CreatedDateTime); - writer.WriteStringValue("officeLocation", OfficeLocation); - writer.WriteTimeSpanValue("workDuration", WorkDuration); - writer.WriteDateValue("birthDay", BirthDay); - writer.WriteTimeValue("startWorkTime", StartWorkTime); - writer.WriteTimeValue("endWorkTime", EndWorkTime); - writer.WriteAdditionalData(AdditionalData); - } - public static TestEntity CreateFromDiscriminator(IParseNode parseNode) + } + /// + /// Serializes information the current object + /// Serialization writer to use to serialize this model + /// + public virtual void Serialize(ISerializationWriter writer) + { + _ = writer ?? throw new ArgumentNullException(nameof(writer)); + writer.WriteStringValue("id", Id); + writer.WriteDateTimeOffsetValue("createdDateTime", CreatedDateTime); + writer.WriteStringValue("officeLocation", OfficeLocation); + writer.WriteTimeSpanValue("workDuration", WorkDuration); + writer.WriteDateValue("birthDay", BirthDay); + writer.WriteTimeValue("startWorkTime", StartWorkTime); + writer.WriteTimeValue("endWorkTime", EndWorkTime); + writer.WriteAdditionalData(AdditionalData); + } + public static TestEntity CreateFromDiscriminator(IParseNode parseNode) + { + var discriminatorValue = parseNode.GetChildNode("@odata.type")?.GetStringValue(); + return discriminatorValue switch { - var discriminatorValue = parseNode.GetChildNode("@odata.type")?.GetStringValue(); - return discriminatorValue switch - { - "microsoft.graph.user" => new TestEntity(), - "microsoft.graph.group" => new TestEntity(), - _ => new TestEntity(), - }; - } + "microsoft.graph.user" => new TestEntity(), + "microsoft.graph.group" => new TestEntity(), + _ => new TestEntity(), + }; } } diff --git a/src/Microsoft.Kiota.Abstractions.csproj b/src/Microsoft.Kiota.Abstractions.csproj index 8f7c77b6..5b232d2d 100644 --- a/src/Microsoft.Kiota.Abstractions.csproj +++ b/src/Microsoft.Kiota.Abstractions.csproj @@ -14,7 +14,7 @@ https://aka.ms/kiota/docs true true - 1.7.0 + 1.7.1 true false diff --git a/src/RequestInformation.cs b/src/RequestInformation.cs index 10bfdeeb..e9b4950e 100644 --- a/src/RequestInformation.cs +++ b/src/RequestInformation.cs @@ -8,6 +8,8 @@ using System.Diagnostics; using System.IO; using System.Linq; +using System.Reflection; +using System.Runtime.Serialization; using Microsoft.Kiota.Abstractions.Extensions; using Microsoft.Kiota.Abstractions.Serialization; #if NET5_0_OR_GREATER @@ -116,7 +118,7 @@ public Uri URI Guid guid => guid.ToString("D"),// Default of 32 digits separated by hyphens Date date => date.ToString(), //Default to string format of the custom date object Time time => time.ToString(), //Default to string format of the custom time object - _ => value,//return object as is as the ToString method is good enough. + _ => ReplaceEnumValueByStringRepresentation(value),//return object as is as the ToString method is good enough. }; /// @@ -161,9 +163,42 @@ public void AddQueryParameters(T source) !string.IsNullOrEmpty(x.Value.ToString()) && // no need to add an empty string value (x.Value is not ICollection collection || collection.Count > 0))) // no need to add empty collection { - QueryParameters.AddOrReplace(property.Name!, property.Value!); + QueryParameters.AddOrReplace(property.Name!, ReplaceEnumValueByStringRepresentation(property.Value!)); } } + private static object ReplaceEnumValueByStringRepresentation(object source) + { + if(source is Enum enumValue && GetEnumName(enumValue) is string enumValueName) + { + return enumValueName; + } + else if(source is Array collection && collection.Length > 0 && collection.GetValue(0) is Enum) + { + var passedArray = new string[collection.Length]; + for(var i = 0; i < collection.Length; i++) + {// this is ugly but necessary due to covariance limitations with pattern matching + passedArray[i] = GetEnumName((Enum)collection.GetValue(i)!)!; + } + return passedArray; + } + else return source; + } +#if NET5_0_OR_GREATER + private static string? GetEnumName<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(T value) where T : Enum +#else + private static string? GetEnumName(T value) where T : Enum +#endif + { + var type = value.GetType(); + + if(Enum.GetName(type, value) is not { } name) + throw new ArgumentException($"Invalid Enum value {value} for enum of type {type}"); + + if(type.GetMember(name).FirstOrDefault()?.GetCustomAttribute() is { } attribute) + return attribute.Value; + + return name.ToFirstCharacterLowerCase(); + } /// /// The Request Headers. ///