From e85035349e8d016af2feb4b2c17a7157e53367c8 Mon Sep 17 00:00:00 2001 From: Jeffrey Bulanadi <41933086+jeffreybulanadi@users.noreply.github.com> Date: Sat, 2 May 2026 06:51:04 +0800 Subject: [PATCH] emit valid C# initializers for date/time type defaults in constructor --- CHANGELOG.md | 1 + .../Writers/CSharp/CodeMethodWriter.cs | 35 +++++++++++++ .../Writers/CSharp/CodeMethodWriterTests.cs | 52 +++++++++++++++++++ 3 files changed, 88 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36c3d298be..42639240da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed `kiota download` returning exit code 0 (success) when no results are found or multiple ambiguous matches exist. [#7643](https://github.com/microsoft/kiota/pull/7643) - Fixed incorrect command hints and telemetry in `kiota plugin generate` handler referencing "client" instead of "plugin". [#7642](https://github.com/microsoft/kiota/pull/7642) - Fixed Ruby `isStream` always evaluating to false in `CodeMethodWriter`, causing stream/binary responses to never use `send_primitive_async`. [#7639](https://github.com/microsoft/kiota/pull/7639) +- Fixed C# codegen emitting invalid initializer expressions for properties with date/time default values (`Time`, `Date`, `DateTimeOffset`, `TimeSpan`); invalid values (e.g. `24:00:00`) are now silently skipped rather than generating uncompilable code. [#6251](https://github.com/microsoft/kiota/issues/6251) ## [1.31.1] - 2026-04-13 diff --git a/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs index 5e1d20b7c4..f6b623b422 100644 --- a/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs +++ b/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using Kiota.Builder.CodeDOM; using Kiota.Builder.Extensions; @@ -270,6 +271,15 @@ private void WriteConstructorBody(CodeClass parentClass, CodeMethod currentMetho { defaultValue = defaultValue.TrimQuotes(); } + else if (propWithDefault.Type is CodeType dateTimePropType && + DateTimeTypeNames.Contains(dateTimePropType.Name) && + defaultValue.StartsWith('"') && defaultValue.EndsWith('"')) + { + var expression = GetDateTimeDefaultValueExpression(dateTimePropType.Name, defaultValue.TrimQuotes()); + if (string.IsNullOrEmpty(expression)) + continue; + defaultValue = expression; + } else if (defaultValue.StartsWith('"') && defaultValue.EndsWith('"')) { // cannot use TrimQuotes() as it would greedily remove the explicitly set quotes on both ends of the string @@ -298,6 +308,31 @@ private void WriteConstructorBody(CodeClass parentClass, CodeMethod currentMetho } private const string NullValueString = "null"; + private static readonly HashSet DateTimeTypeNames = new(StringComparer.OrdinalIgnoreCase) + { + "Date", "Time", "DateTimeOffset", "TimeSpan" + }; + private static string GetDateTimeDefaultValueExpression(string typeName, string rawValue) + { + if (typeName.Equals("Time", StringComparison.OrdinalIgnoreCase) && + TimeOnly.TryParse(rawValue, CultureInfo.InvariantCulture, out var timeValue)) + return $"new Time({timeValue.Hour}, {timeValue.Minute}, {timeValue.Second})"; + + if (typeName.Equals("Date", StringComparison.OrdinalIgnoreCase) && + DateOnly.TryParse(rawValue, CultureInfo.InvariantCulture, out var dateValue)) + return $"new Date({dateValue.Year}, {dateValue.Month}, {dateValue.Day})"; + + var escapedRawValue = rawValue.Replace("\"", "\\\"", StringComparison.Ordinal); + if (typeName.Equals("DateTimeOffset", StringComparison.OrdinalIgnoreCase) && + DateTimeOffset.TryParse(rawValue, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind | DateTimeStyles.AssumeUniversal, out _)) + return $"DateTimeOffset.Parse(\"{escapedRawValue}\", null, global::System.Globalization.DateTimeStyles.RoundtripKind)"; + + if (typeName.Equals("TimeSpan", StringComparison.OrdinalIgnoreCase) && + !string.IsNullOrEmpty(rawValue)) + return $"global::System.Xml.XmlConvert.ToTimeSpan(\"{escapedRawValue}\")"; + + return string.Empty; + } private string DefaultDeserializerValue => $"new Dictionary>"; private void WriteDeserializerBody(bool shouldHide, CodeMethod codeElement, CodeClass parentClass, LanguageWriter writer) { diff --git a/tests/Kiota.Builder.Tests/Writers/CSharp/CodeMethodWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/CSharp/CodeMethodWriterTests.cs index a34ff3ab1a..46ed03bba4 100644 --- a/tests/Kiota.Builder.Tests/Writers/CSharp/CodeMethodWriterTests.cs +++ b/tests/Kiota.Builder.Tests/Writers/CSharp/CodeMethodWriterTests.cs @@ -2038,6 +2038,58 @@ public void WritesConstructor() Assert.Contains($"{escapedQuotesPropName.ToFirstCharacterUpperCase()} = {expectedValueEscapedQuotes}", result); } [Fact] + public void WritesConstructorWithDateTimeTypeDefaults() + { + setup(); + method.Kind = CodeMethodKind.Constructor; + + parentClass.AddProperty(new CodeProperty + { + Name = "startTime", + DefaultValue = "\"13:00:00\"", + Kind = CodePropertyKind.Custom, + Type = new CodeType { Name = "Time" }, + }); + parentClass.AddProperty(new CodeProperty + { + Name = "startDate", + DefaultValue = "\"2023-04-19\"", + Kind = CodePropertyKind.Custom, + Type = new CodeType { Name = "Date" }, + }); + parentClass.AddProperty(new CodeProperty + { + Name = "createdAt", + DefaultValue = "\"2023-04-19T13:00:00Z\"", + Kind = CodePropertyKind.Custom, + Type = new CodeType { Name = "DateTimeOffset" }, + }); + parentClass.AddProperty(new CodeProperty + { + Name = "duration", + DefaultValue = "\"PT1H\"", + Kind = CodePropertyKind.Custom, + Type = new CodeType { Name = "TimeSpan" }, + }); + parentClass.AddProperty(new CodeProperty + { + Name = "invalidTime", + DefaultValue = "\"24:00:00\"", + Kind = CodePropertyKind.Custom, + Type = new CodeType { Name = "Time" }, + }); + + writer.Write(method); + var result = tw.ToString(); + + Assert.Contains("StartTime = new Time(13, 0, 0)", result); + Assert.Contains("StartDate = new Date(2023, 4, 19)", result); + Assert.Contains("CreatedAt = DateTimeOffset.Parse(\"2023-04-19T13:00:00Z\", null, global::System.Globalization.DateTimeStyles.RoundtripKind)", result); + Assert.Contains("Duration = global::System.Xml.XmlConvert.ToTimeSpan(\"PT1H\")", result); + Assert.DoesNotContain("InvalidTime", result); + AssertExtensions.CurlyBracesAreClosed(result, 1); + } + [Fact] public void WritesWithUrl() { setup();