Compare commits

..

2 Commits
main ... dev

Author SHA1 Message Date
Mathias Beaulieu-Duncan
a05ebad7fc Fix CS8601 in generated proto→command list mappings
All checks were successful
Publish NuGets / build (release) Successful in 28s
Generated CommandServiceImpl.g.cs had warnings like:
    Slug = request.Slug?.ToList(),   // CS8601 if Slug is non-nullable List<T>

The ?. was over-defensive: proto3 repeated fields are emitted as
RepeatedField<T> in C# and are NEVER null. The conditional access
made the result List<T>? which then triggered CS8601 when assigned
to a non-nullable target on the command POCO.

Dropped ?. in 4 emission sites in GrpcGenerator.cs covering:
- Top-level primitive list mapping (line 872)
- Top-level Guid list mapping (line 861)
- Nested primitive list mapping in NestedPropertyAssignment (line 1083)
- Complex list .Select chain in GenerateComplexListMapping (line 974,
  conditional: kept ?. for value-type collections where source.Items is
  read off a possibly-null wrapper message)

Real fix in the generator instead of CS8601 NoWarn suppression in
consumer csprojs. Consumers can drop the suppression after bumping.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 19:42:17 -04:00
Mathias Beaulieu-Duncan
ee3ad866d9 Use InvariantCulture for decimal.Parse in generated gRPC mappers
Generated code was using locale-dependent parsing for decimal values.
On systems with comma decimal separator (e.g., French locale), parsing
"0.95" would throw FormatException because the system expected "0,95".

Switched all 4 decimal.Parse() call sites in the generated proto→domain
mappers to pass System.Globalization.CultureInfo.InvariantCulture for
consistent behavior across locales.

Inspired by JP's commit 599204d on feat/grpc-generator-improvements
(applied manually since cherry-pick had heavy context conflicts).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 19:33:27 -04:00

View File

@ -858,7 +858,8 @@ public class GrpcGenerator : IIncrementalGenerator
var constructorType = prop.FullyQualifiedType.TrimEnd('?'); var constructorType = prop.FullyQualifiedType.TrimEnd('?');
return $"{indent}{prop.Name} = new {constructorType}({source}?.Items?.Select(x => System.Guid.Parse(x)).ToArray() ?? System.Array.Empty<System.Guid>()),"; return $"{indent}{prop.Name} = new {constructorType}({source}?.Items?.Select(x => System.Guid.Parse(x)).ToArray() ?? System.Array.Empty<System.Guid>()),";
} }
return $"{indent}{prop.Name} = {source}?.Select(x => System.Guid.Parse(x)).ToList(),"; // proto repeated fields are never null — drop ?. to avoid CS8601 on assignment to non-nullable target
return $"{indent}{prop.Name} = {source}.Select(x => System.Guid.Parse(x)).ToList(),";
} }
else if (prop.IsValueTypeCollection) else if (prop.IsValueTypeCollection)
{ {
@ -869,7 +870,8 @@ public class GrpcGenerator : IIncrementalGenerator
else else
{ {
// Primitive list: just ToList() // Primitive list: just ToList()
return $"{indent}{prop.Name} = {source}?.ToList(),"; // proto repeated fields are never null — drop ?. to avoid CS8601 on assignment to non-nullable target
return $"{indent}{prop.Name} = {source}.ToList(),";
} }
} }
@ -884,11 +886,11 @@ public class GrpcGenerator : IIncrementalGenerator
{ {
if (prop.IsNullable) if (prop.IsNullable)
{ {
return $"{indent}{prop.Name} = string.IsNullOrEmpty({source}) ? null : decimal.Parse({source}),"; return $"{indent}{prop.Name} = string.IsNullOrEmpty({source}) ? null : decimal.Parse({source}, System.Globalization.CultureInfo.InvariantCulture),";
} }
else else
{ {
return $"{indent}{prop.Name} = decimal.Parse({source}),"; return $"{indent}{prop.Name} = decimal.Parse({source}, System.Globalization.CultureInfo.InvariantCulture),";
} }
} }
@ -969,7 +971,9 @@ public class GrpcGenerator : IIncrementalGenerator
var sb = new StringBuilder(); var sb = new StringBuilder();
// For value type collections, the proto message has an Items field containing the repeated elements // For value type collections, the proto message has an Items field containing the repeated elements
var itemsSource = prop.IsValueTypeCollection ? $"{source}?.Items" : source; var itemsSource = prop.IsValueTypeCollection ? $"{source}?.Items" : source;
sb.AppendLine($"{indent}{prop.Name} = {itemsSource}?.Select(x => new {prop.ElementType}"); // Value-type wrapper messages can be null (?.Items needs ?.). Plain proto repeated is never null.
var selectAccess = prop.IsValueTypeCollection ? "?." : ".";
sb.AppendLine($"{indent}{prop.Name} = {itemsSource}{selectAccess}Select(x => new {prop.ElementType}");
sb.AppendLine($"{indent}{{"); sb.AppendLine($"{indent}{{");
foreach (var nestedProp in prop.ElementNestedProperties!) foreach (var nestedProp in prop.ElementNestedProperties!)
@ -1031,11 +1035,11 @@ public class GrpcGenerator : IIncrementalGenerator
{ {
if (prop.IsNullable) if (prop.IsNullable)
{ {
return $"{indent}{prop.Name} = string.IsNullOrEmpty({source}) ? null : decimal.Parse({source}),"; return $"{indent}{prop.Name} = string.IsNullOrEmpty({source}) ? null : decimal.Parse({source}, System.Globalization.CultureInfo.InvariantCulture),";
} }
else else
{ {
return $"{indent}{prop.Name} = decimal.Parse({source}),"; return $"{indent}{prop.Name} = decimal.Parse({source}, System.Globalization.CultureInfo.InvariantCulture),";
} }
} }
@ -1078,7 +1082,8 @@ public class GrpcGenerator : IIncrementalGenerator
var constructorType = prop.FullyQualifiedType.TrimEnd('?'); var constructorType = prop.FullyQualifiedType.TrimEnd('?');
return $"{indent}{prop.Name} = new {constructorType}({source}?.ToArray() ?? System.Array.Empty<{prop.ElementType ?? "object"}>()),"; return $"{indent}{prop.Name} = new {constructorType}({source}?.ToArray() ?? System.Array.Empty<{prop.ElementType ?? "object"}>()),";
} }
return $"{indent}{prop.Name} = {source}?.ToList(),"; // proto repeated fields are never null — drop ?. to avoid CS8601
return $"{indent}{prop.Name} = {source}.ToList(),";
} }
// Handle complex types // Handle complex types