Skip to content

Commit 407df51

Browse files
committed
Handle nullable props and required JSON readers
1 parent 3b8d2e0 commit 407df51

8 files changed

Lines changed: 198 additions & 52 deletions

File tree

src/FluentCommand.Generators/DataReaderFactoryGenerator.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ private static EntityProperty CreateProperty(IPropertySymbol propertySymbol, str
230230
var jsonColumn = GetJsonColumn(attributes);
231231
var isJsonColumn = jsonColumn != null;
232232
var enumInfo = GetEnumInfo(propertySymbol.Type);
233+
var isNullable = IsNullableType(propertySymbol.Type);
233234
var isNotMapped = (classIgnored?.Contains(propertyName) == true) || (!isJsonColumn && !IsSupportedType(propertySymbol.Type));
234235

235236
if (attributes == default || attributes.Length == 0)
@@ -244,6 +245,7 @@ private static EntityProperty CreateProperty(IPropertySymbol propertySymbol, str
244245
IsNotMapped = isNotMapped,
245246
HasGetter = hasGetter,
246247
HasSetter = hasSetter,
248+
IsNullable = isNullable,
247249
IsJsonColumn = isJsonColumn,
248250
IsEnum = enumInfo.IsEnum,
249251
IsNullableEnum = enumInfo.IsNullableEnum,
@@ -286,6 +288,7 @@ private static EntityProperty CreateProperty(IPropertySymbol propertySymbol, str
286288
IsConcurrencyCheck = isConcurrencyCheck,
287289
ForeignKey = foreignKey,
288290
IsRequired = isRequired,
291+
IsNullable = isNullable,
289292
HasGetter = hasGetter,
290293
HasSetter = hasSetter,
291294
DisplayName = displayName,
@@ -319,6 +322,12 @@ private static (bool IsEnum, bool IsNullableEnum, string? UnderlyingType) GetEnu
319322
return (true, isNullableEnum, namedEnum.EnumUnderlyingType?.ToDisplayString());
320323
}
321324

325+
private static bool IsNullableType(ITypeSymbol type)
326+
{
327+
return type.NullableAnnotation == NullableAnnotation.Annotated
328+
|| type is INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T };
329+
}
330+
322331
private static AttributeData? GetJsonColumn(ImmutableArray<AttributeData> attributes)
323332
{
324333
return attributes.FirstOrDefault(a => a.AttributeClass is

src/FluentCommand.Generators/DataReaderFactoryWriter.cs

Lines changed: 45 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,7 @@ private static void WriteEntityFactory(IndentedStringBuilder codeBuilder, Entity
297297
.Append(" v_")
298298
.Append(fieldName)
299299
.Append(" = default")
300-
.AppendIf("!", _ => !entityProperty.PropertyType.EndsWith("?"))
300+
.AppendIf("!", _ => !entityProperty.IsNullable)
301301
.AppendLine(";");
302302
}
303303

@@ -332,56 +332,31 @@ private static void WriteEntityFactory(IndentedStringBuilder codeBuilder, Entity
332332

333333
if (entityProperty.IsJsonColumn)
334334
{
335+
var jsonReaderName = entityProperty.IsNullable
336+
? "GetFromJson"
337+
: "GetRequiredFromJson";
338+
335339
codeBuilder
336340
.IncrementIndent()
337341
.Append("v_")
338342
.Append(fieldName)
339-
.Append(" = dataRecord.GetFromJson<")
343+
.Append(" = dataRecord.")
344+
.Append(jsonReaderName)
345+
.Append("<")
340346
.Append(entityProperty.PropertyType)
341347
.Append(">(__index")
342348
.Append(GetJsonReaderArgument(entityProperty))
343-
.Append(")")
344-
.AppendIf("!", _ => !entityProperty.PropertyType.EndsWith("?"))
345-
.AppendLine(";")
349+
.AppendLine(");")
346350
.AppendLine("break;")
347351
.DecrementIndent();
348352
}
349353
else if (entityProperty.IsEnum)
350354
{
351-
var underlyingType = entityProperty.EnumUnderlyingType ?? "int";
352-
353-
codeBuilder
354-
.IncrementIndent()
355-
.Append("v_")
356-
.Append(fieldName)
357-
.Append(" = ");
358-
359-
if (entityProperty.IsNullableEnum)
360-
{
361-
codeBuilder
362-
.Append("(")
363-
.Append(entityProperty.PropertyType)
364-
.Append(")dataRecord.GetValue<")
365-
.Append(underlyingType)
366-
.AppendLine("?>(__index);");
367-
}
368-
else
369-
{
370-
codeBuilder
371-
.Append("(")
372-
.Append(entityProperty.PropertyType)
373-
.Append(")dataRecord.")
374-
.Append(GetReaderName(underlyingType))
375-
.AppendLine("(__index);");
376-
}
377-
378-
codeBuilder
379-
.AppendLine("break;")
380-
.DecrementIndent();
355+
WriteEnumColumnReader(codeBuilder, entityProperty, fieldName);
381356
}
382357
else if (string.IsNullOrEmpty(entityProperty.ConverterName))
383358
{
384-
var readerName = GetReaderName(entityProperty.PropertyType);
359+
var readerName = GetReaderName(entityProperty);
385360

386361
codeBuilder
387362
.IncrementIndent()
@@ -440,6 +415,29 @@ private static void WriteEntityFactory(IndentedStringBuilder codeBuilder, Entity
440415
.AppendLine();
441416
}
442417

418+
private static void WriteEnumColumnReader(IndentedStringBuilder codeBuilder, EntityProperty entityProperty, string fieldName)
419+
{
420+
codeBuilder
421+
.IncrementIndent()
422+
.Append("v_")
423+
.Append(fieldName)
424+
.Append(" = ")
425+
.Append(GetEnumReadExpression(entityProperty))
426+
.AppendLine(";")
427+
.AppendLine("break;")
428+
.DecrementIndent();
429+
}
430+
431+
private static string GetEnumReadExpression(EntityProperty entityProperty)
432+
{
433+
var underlyingType = entityProperty.EnumUnderlyingType ?? "int";
434+
435+
if (entityProperty.IsNullableEnum)
436+
return $"({entityProperty.PropertyType})dataRecord.GetValue<{underlyingType}?>(__index)";
437+
438+
return $"({entityProperty.PropertyType})dataRecord.{GetReaderName(underlyingType)}(__index)";
439+
}
440+
443441
private static void WriteReturnConstructor(IndentedStringBuilder codeBuilder, EntityClass entity)
444442
{
445443
codeBuilder
@@ -523,12 +521,18 @@ private static string GetAliasMap(string type)
523521
};
524522
}
525523

526-
private static string GetReaderName(string propertyType)
524+
private static string GetReaderName(EntityProperty entityProperty)
527525
{
528-
// remove nullable
529-
var type = propertyType.EndsWith("?") ? propertyType.Substring(0, propertyType.Length - 1) : propertyType;
526+
var propertyType = entityProperty.IsNullable
527+
? entityProperty.PropertyType.Substring(0, entityProperty.PropertyType.Length - 1)
528+
: entityProperty.PropertyType;
530529

531-
return type switch
530+
return GetReaderName(propertyType);
531+
}
532+
533+
private static string GetReaderName(string propertyType)
534+
{
535+
return propertyType switch
532536
{
533537
"System.Boolean" => "GetBoolean",
534538
"System.Byte" => "GetByte",
@@ -556,7 +560,7 @@ private static string GetReaderName(string propertyType)
556560
"long" => "GetInt64",
557561
"string" => "GetString",
558562
"FluentCommand.ConcurrencyToken" => "GetBytes",
559-
_ => $"GetValue<{type}>"
563+
_ => $"GetValue<{propertyType}>"
560564
};
561565
}
562566

src/FluentCommand.Generators/Models/EntityProperty.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public record EntityProperty
1515
public bool IsConcurrencyCheck { get; init; }
1616
public string? ForeignKey { get; init; }
1717
public bool IsRequired { get; init; }
18+
public bool IsNullable { get; init; }
1819

1920
public bool HasGetter { get; init; } = true;
2021
public bool HasSetter { get; init; } = true;

src/FluentCommand.Generators/TypeAccessorWriter.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -311,8 +311,6 @@ private static void WriteMemberAccessorClass(IndentedStringBuilder codeBuilder,
311311
// SetValue
312312
if (prop.HasSetter)
313313
{
314-
var isNullableType = prop.PropertyType.EndsWith("?");
315-
316314
codeBuilder
317315
.AppendLine("public void SetValue(object instance, object? value)")
318316
.AppendLine("{")
@@ -328,7 +326,7 @@ private static void WriteMemberAccessorClass(IndentedStringBuilder codeBuilder,
328326
.DecrementIndent()
329327
.AppendLine();
330328

331-
if (isNullableType)
329+
if (prop.IsNullable)
332330
{
333331
codeBuilder
334332
.Append("typed.")

src/FluentCommand/Extensions/JsonRecordExtensions.cs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,26 @@ public static class JsonRecordExtensions
2424
return JsonSerializer.Deserialize<T>(json, options);
2525
}
2626

27+
/// <summary>Deserializes the JSON value of the specified column to a required <typeparamref name="T"/> value.</summary>
28+
/// <typeparam name="T">The type to deserialize the JSON value into.</typeparam>
29+
/// <param name="dataRecord">The data record.</param>
30+
/// <param name="ordinal">The zero-based column ordinal.</param>
31+
/// <param name="options">Options to control the behavior during parsing.</param>
32+
/// <returns>The deserialized value.</returns>
33+
/// <exception cref="DataException">The column is <see langword="null"/> or deserializes to <see langword="null"/>.</exception>
34+
public static T GetRequiredFromJson<T>(this IDataRecord dataRecord, int ordinal, JsonSerializerOptions? options = null)
35+
{
36+
if (dataRecord.IsDBNull(ordinal))
37+
throw new DataException($"JSON column at ordinal {ordinal} is null but a value of type '{typeof(T)}' is required.");
38+
39+
var json = dataRecord.GetString(ordinal);
40+
var value = JsonSerializer.Deserialize<T>(json, options);
41+
42+
return value is null
43+
? throw new DataException($"JSON column at ordinal {ordinal} deserialized to null but a value of type '{typeof(T)}' is required.")
44+
: value;
45+
}
46+
2747
/// <summary>Deserializes the JSON value of the specified column to <typeparamref name="T"/>.</summary>
2848
/// <typeparam name="T">The type to deserialize the JSON value into.</typeparam>
2949
/// <param name="dataRecord">The data record.</param>
@@ -39,6 +59,26 @@ public static class JsonRecordExtensions
3959
return JsonSerializer.Deserialize(json, jsonTypeInfo);
4060
}
4161

62+
/// <summary>Deserializes the JSON value of the specified column to a required <typeparamref name="T"/> value.</summary>
63+
/// <typeparam name="T">The type to deserialize the JSON value into.</typeparam>
64+
/// <param name="dataRecord">The data record.</param>
65+
/// <param name="ordinal">The zero-based column ordinal.</param>
66+
/// <param name="jsonTypeInfo">Metadata about the type to convert.</param>
67+
/// <returns>The deserialized value.</returns>
68+
/// <exception cref="DataException">The column is <see langword="null"/> or deserializes to <see langword="null"/>.</exception>
69+
public static T GetRequiredFromJson<T>(this IDataRecord dataRecord, int ordinal, JsonTypeInfo<T> jsonTypeInfo)
70+
{
71+
if (dataRecord.IsDBNull(ordinal))
72+
throw new DataException($"JSON column at ordinal {ordinal} is null but a value of type '{typeof(T)}' is required.");
73+
74+
var json = dataRecord.GetString(ordinal);
75+
var value = JsonSerializer.Deserialize(json, jsonTypeInfo);
76+
77+
return value is null
78+
? throw new DataException($"JSON column at ordinal {ordinal} deserialized to null but a value of type '{typeof(T)}' is required.")
79+
: value;
80+
}
81+
4282
/// <summary>Deserializes the JSON value of the specified column to <typeparamref name="T"/>.</summary>
4383
/// <typeparam name="T">The type to deserialize the JSON value into.</typeparam>
4484
/// <param name="dataRecord">The data record.</param>
@@ -51,6 +91,19 @@ public static class JsonRecordExtensions
5191
return dataRecord.GetFromJson<T>(ordinal, options);
5292
}
5393

94+
/// <summary>Deserializes the JSON value of the specified column to a required <typeparamref name="T"/> value.</summary>
95+
/// <typeparam name="T">The type to deserialize the JSON value into.</typeparam>
96+
/// <param name="dataRecord">The data record.</param>
97+
/// <param name="name">The <paramref name="name"/> of the field to find.</param>
98+
/// <param name="options">Options to control the behavior during parsing.</param>
99+
/// <returns>The deserialized value.</returns>
100+
/// <exception cref="DataException">The column is <see langword="null"/> or deserializes to <see langword="null"/>.</exception>
101+
public static T GetRequiredFromJson<T>(this IDataRecord dataRecord, string name, JsonSerializerOptions? options = null)
102+
{
103+
int ordinal = dataRecord.GetOrdinal(name);
104+
return dataRecord.GetRequiredFromJson<T>(ordinal, options);
105+
}
106+
54107
/// <summary>Deserializes the JSON value of the specified column to <typeparamref name="T"/>.</summary>
55108
/// <typeparam name="T">The type to deserialize the JSON value into.</typeparam>
56109
/// <param name="dataRecord">The data record.</param>
@@ -62,4 +115,17 @@ public static class JsonRecordExtensions
62115
int ordinal = dataRecord.GetOrdinal(name);
63116
return dataRecord.GetFromJson<T>(ordinal, jsonTypeInfo);
64117
}
118+
119+
/// <summary>Deserializes the JSON value of the specified column to a required <typeparamref name="T"/> value.</summary>
120+
/// <typeparam name="T">The type to deserialize the JSON value into.</typeparam>
121+
/// <param name="dataRecord">The data record.</param>
122+
/// <param name="name">The <paramref name="name"/> of the field to find.</param>
123+
/// <param name="jsonTypeInfo">Metadata about the type to convert.</param>
124+
/// <returns>The deserialized value.</returns>
125+
/// <exception cref="DataException">The column is <see langword="null"/> or deserializes to <see langword="null"/>.</exception>
126+
public static T GetRequiredFromJson<T>(this IDataRecord dataRecord, string name, JsonTypeInfo<T> jsonTypeInfo)
127+
{
128+
int ordinal = dataRecord.GetOrdinal(name);
129+
return dataRecord.GetRequiredFromJson<T>(ordinal, jsonTypeInfo);
130+
}
65131
}

test/FluentCommand.Generators.Tests/DataReaderFactoryWriterTests.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,18 +44,18 @@ public void GenerateJsonColumnReader()
4444
{
4545
new() { PropertyName = "Id", ColumnName = "Id", PropertyType = "int", MemberTypeName = "int" },
4646
new() { PropertyName = "Data", ColumnName = "Data", PropertyType = "global::FluentCommand.Entities.UserImport", MemberTypeName = "global::FluentCommand.Entities.UserImport", IsJsonColumn = true },
47-
new() { PropertyName = "OptionalData", ColumnName = "OptionalData", PropertyType = "global::FluentCommand.Entities.UserImport?", MemberTypeName = "global::FluentCommand.Entities.UserImport?", IsJsonColumn = true },
47+
new() { PropertyName = "OptionalData", ColumnName = "OptionalData", PropertyType = "global::FluentCommand.Entities.UserImport?", MemberTypeName = "global::FluentCommand.Entities.UserImport?", IsJsonColumn = true, IsNullable = true },
4848
new() { PropertyName = "DataWithOptions", ColumnName = "DataWithOptions", PropertyType = "global::FluentCommand.Entities.UserImport", MemberTypeName = "global::FluentCommand.Entities.UserImport", IsJsonColumn = true, JsonOptionsProviderName = "global::FluentCommand.Entities.UserImportJsonOptionsProvider" },
4949
new() { PropertyName = "DataWithContext", ColumnName = "DataWithContext", PropertyType = "global::FluentCommand.Entities.UserImport", MemberTypeName = "global::FluentCommand.Entities.UserImport", IsJsonColumn = true, JsonContextName = "global::FluentCommand.Entities.UserImportJsonContext", JsonTypeInfoPropertyName = "UserImport" },
5050
}
5151
};
5252

5353
var source = DataReaderFactoryWriter.Generate(entityClass);
5454

55-
Assert.Contains("v_data = dataRecord.GetFromJson<global::FluentCommand.Entities.UserImport>(__index)!;", source);
55+
Assert.Contains("v_data = dataRecord.GetRequiredFromJson<global::FluentCommand.Entities.UserImport>(__index);", source);
5656
Assert.Contains("v_optionalData = dataRecord.GetFromJson<global::FluentCommand.Entities.UserImport?>(__index);", source);
57-
Assert.Contains("v_dataWithOptions = dataRecord.GetFromJson<global::FluentCommand.Entities.UserImport>(__index, global::FluentCommand.Entities.UserImportJsonOptionsProvider.Options)!;", source);
58-
Assert.Contains("v_dataWithContext = dataRecord.GetFromJson<global::FluentCommand.Entities.UserImport>(__index, global::FluentCommand.Entities.UserImportJsonContext.Default.UserImport)!;", source);
57+
Assert.Contains("v_dataWithOptions = dataRecord.GetRequiredFromJson<global::FluentCommand.Entities.UserImport>(__index, global::FluentCommand.Entities.UserImportJsonOptionsProvider.Options);", source);
58+
Assert.Contains("v_dataWithContext = dataRecord.GetRequiredFromJson<global::FluentCommand.Entities.UserImport>(__index, global::FluentCommand.Entities.UserImportJsonContext.Default.UserImport);", source);
5959
}
6060

6161
[Fact]
@@ -85,6 +85,7 @@ public void GenerateEnumColumnReader()
8585
PropertyType = "global::FluentCommand.Entities.BuilderStatus?",
8686
MemberTypeName = "global::FluentCommand.Entities.BuilderStatus",
8787
IsEnum = true,
88+
IsNullable = true,
8889
IsNullableEnum = true,
8990
EnumUnderlyingType = "short"
9091
}

test/FluentCommand.Generators.Tests/TypeAccessorWriterTests.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,8 @@ public async Task GenerateNullableReferenceType()
9696
{
9797
new() { PropertyName = "Id", ColumnName = "Id", PropertyType = "int", MemberTypeName = "int", IsKey = true, IsDatabaseGenerated = true },
9898
new() { PropertyName = "Name", ColumnName = "Name", PropertyType = "string", MemberTypeName = "string", IsRequired = true },
99-
new() { PropertyName = "Email", ColumnName = "Email", PropertyType = "string?", MemberTypeName = "string" },
100-
new() { PropertyName = "Age", ColumnName = "Age", PropertyType = "int?", MemberTypeName = "int?" },
99+
new() { PropertyName = "Email", ColumnName = "Email", PropertyType = "string?", MemberTypeName = "string", IsNullable = true },
100+
new() { PropertyName = "Age", ColumnName = "Age", PropertyType = "int?", MemberTypeName = "int?", IsNullable = true },
101101
},
102102
TableName = "Contact"
103103
};
@@ -211,8 +211,8 @@ public void SetValueNullableAcceptsNull()
211211
EntityName = "Item",
212212
Properties = new EntityProperty[]
213213
{
214-
new() { PropertyName = "Age", ColumnName = "Age", PropertyType = "int?", MemberTypeName = "int?" },
215-
new() { PropertyName = "Email", ColumnName = "Email", PropertyType = "string?", MemberTypeName = "string" },
214+
new() { PropertyName = "Age", ColumnName = "Age", PropertyType = "int?", MemberTypeName = "int?", IsNullable = true },
215+
new() { PropertyName = "Email", ColumnName = "Email", PropertyType = "string?", MemberTypeName = "string", IsNullable = true },
216216
},
217217
TableName = "Item"
218218
};

0 commit comments

Comments
 (0)