Skip to content

Commit 935db7b

Browse files
committed
Normalize enums and add enum data readers
1 parent 20472d8 commit 935db7b

12 files changed

Lines changed: 338 additions & 5 deletions

File tree

src/FluentCommand.Generators/DataReaderFactoryGenerator.cs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ private static EntityProperty CreateProperty(IPropertySymbol propertySymbol, str
229229
var attributes = propertySymbol.GetAttributes();
230230
var jsonColumn = GetJsonColumn(attributes);
231231
var isJsonColumn = jsonColumn != null;
232+
var enumInfo = GetEnumInfo(propertySymbol.Type);
232233
var isNotMapped = (classIgnored?.Contains(propertyName) == true) || (!isJsonColumn && !IsSupportedType(propertySymbol.Type));
233234

234235
if (attributes == default || attributes.Length == 0)
@@ -243,7 +244,10 @@ private static EntityProperty CreateProperty(IPropertySymbol propertySymbol, str
243244
IsNotMapped = isNotMapped,
244245
HasGetter = hasGetter,
245246
HasSetter = hasSetter,
246-
IsJsonColumn = isJsonColumn
247+
IsJsonColumn = isJsonColumn,
248+
IsEnum = enumInfo.IsEnum,
249+
IsNullableEnum = enumInfo.IsNullableEnum,
250+
EnumUnderlyingType = enumInfo.UnderlyingType
247251
};
248252
}
249253

@@ -291,10 +295,30 @@ private static EntityProperty CreateProperty(IPropertySymbol propertySymbol, str
291295
IsJsonColumn = isJsonColumn,
292296
JsonOptionsProviderName = jsonOptionsProviderName,
293297
JsonContextName = jsonContextName,
294-
JsonTypeInfoPropertyName = jsonTypeInfoPropertyName
298+
JsonTypeInfoPropertyName = jsonTypeInfoPropertyName,
299+
IsEnum = enumInfo.IsEnum,
300+
IsNullableEnum = enumInfo.IsNullableEnum,
301+
EnumUnderlyingType = enumInfo.UnderlyingType
295302
};
296303
}
297304

305+
private static (bool IsEnum, bool IsNullableEnum, string? UnderlyingType) GetEnumInfo(ITypeSymbol type)
306+
{
307+
var isNullableEnum = false;
308+
var enumType = type;
309+
310+
if (type is INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T } namedType)
311+
{
312+
isNullableEnum = true;
313+
enumType = namedType.TypeArguments[0];
314+
}
315+
316+
if (enumType is not INamedTypeSymbol { TypeKind: TypeKind.Enum } namedEnum)
317+
return (false, false, null);
318+
319+
return (true, isNullableEnum, namedEnum.EnumUnderlyingType?.ToDisplayString());
320+
}
321+
298322
private static AttributeData? GetJsonColumn(ImmutableArray<AttributeData> attributes)
299323
{
300324
return attributes.FirstOrDefault(a => a.AttributeClass is

src/FluentCommand.Generators/DataReaderFactoryWriter.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,39 @@ private static void WriteEntityFactory(IndentedStringBuilder codeBuilder, Entity
344344
.AppendLine("break;")
345345
.DecrementIndent();
346346
}
347+
else if (entityProperty.IsEnum)
348+
{
349+
var underlyingType = entityProperty.EnumUnderlyingType ?? "int";
350+
351+
codeBuilder
352+
.IncrementIndent()
353+
.Append("v_")
354+
.Append(fieldName)
355+
.Append(" = ");
356+
357+
if (entityProperty.IsNullableEnum)
358+
{
359+
codeBuilder
360+
.Append("(")
361+
.Append(entityProperty.PropertyType)
362+
.Append(")dataRecord.GetValue<")
363+
.Append(underlyingType)
364+
.AppendLine("?>(__index);");
365+
}
366+
else
367+
{
368+
codeBuilder
369+
.Append("(")
370+
.Append(entityProperty.PropertyType)
371+
.Append(")dataRecord.")
372+
.Append(GetReaderName(underlyingType))
373+
.AppendLine("(__index);");
374+
}
375+
376+
codeBuilder
377+
.AppendLine("break;")
378+
.DecrementIndent();
379+
}
347380
else if (string.IsNullOrEmpty(entityProperty.ConverterName))
348381
{
349382
var readerName = GetReaderName(entityProperty.PropertyType);

src/FluentCommand.Generators/Models/EntityProperty.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,21 @@ public record EntityProperty
1414
public bool IsConcurrencyCheck { get; init; }
1515
public string? ForeignKey { get; init; }
1616
public bool IsRequired { get; init; }
17+
1718
public bool HasGetter { get; init; } = true;
1819
public bool HasSetter { get; init; } = true;
20+
1921
public string? DisplayName { get; init; }
2022
public string? DataFormatString { get; init; }
2123
public string? ColumnType { get; init; }
2224
public int? ColumnOrder { get; init; }
25+
2326
public bool IsJsonColumn { get; init; }
2427
public string? JsonOptionsProviderName { get; init; }
2528
public string? JsonContextName { get; init; }
2629
public string? JsonTypeInfoPropertyName { get; init; }
30+
31+
public bool IsEnum { get; init; }
32+
public bool IsNullableEnum { get; init; }
33+
public string? EnumUnderlyingType { get; init; }
2734
}

src/FluentCommand/DataParameterHandlers.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,19 @@ public static void AddTypeHandler<THandler>(THandler handler)
7070
/// <param name="parameter">The parameter to set the value on.</param>
7171
/// <param name="value">The value to set.</param>
7272
/// <param name="type">The data type to use.</param>
73+
/// <remarks>
74+
/// Enum and nullable enum values are converted to their numeric underlying type before mapping.
75+
/// </remarks>
7376
public static void SetValue(DbParameter parameter, object? value, Type type)
7477
{
7578
var valueType = type.GetUnderlyingType();
79+
if (valueType.IsEnum)
80+
{
81+
valueType = Enum.GetUnderlyingType(valueType);
82+
if (value is not null && value != DBNull.Value)
83+
value = Convert.ChangeType(value, valueType);
84+
}
85+
7686
var handler = GetTypeHandler(valueType);
7787

7888
value ??= DBNull.Value;

src/FluentCommand/Query/InsertBuilder.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@ public TBuilder Value<TValue>(
117117
/// <returns>
118118
/// The same builder instance for method chaining.
119119
/// </returns>
120+
/// <remarks>
121+
/// Enum and nullable enum values are normalized to their numeric underlying type.
122+
/// </remarks>
120123
/// <exception cref="ArgumentException">Thrown if <paramref name="columnName"/> is null or empty.</exception>
121124
/// <exception cref="ArgumentNullException">Thrown if <paramref name="parameterType"/> is <c>null</c>.</exception>
122125
public TBuilder Value(
@@ -134,7 +137,8 @@ public TBuilder Value(
134137
ColumnExpressions.Add(columnExpression);
135138
ValueExpressions.Add(paramterName);
136139

137-
QueryParameter parameter = new(paramterName, parameterValue, parameterType);
140+
var normalized = QueryParameterNormalizer.Normalize(parameterValue, parameterType);
141+
QueryParameter parameter = new(paramterName, normalized.Value, normalized.Type);
138142
Parameters.Add(parameter);
139143

140144
return (TBuilder)this;
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
namespace FluentCommand.Query;
2+
3+
/// <summary>
4+
/// Normalizes query parameter values and types before parameter creation.
5+
/// </summary>
6+
internal static class QueryParameterNormalizer
7+
{
8+
/// <summary>
9+
/// Normalizes enum and nullable enum values to their numeric underlying value and type.
10+
/// </summary>
11+
/// <param name="value">The parameter value.</param>
12+
/// <param name="type">The declared parameter type.</param>
13+
/// <returns>
14+
/// A tuple containing the normalized value and type.
15+
/// </returns>
16+
/// <exception cref="ArgumentNullException">Thrown if <paramref name="type"/> is <c>null</c>.</exception>
17+
public static (object? Value, Type Type) Normalize(object? value, Type type)
18+
{
19+
ArgumentNullException.ThrowIfNull(type);
20+
21+
var enumType = Nullable.GetUnderlyingType(type) ?? type;
22+
if (!enumType.IsEnum)
23+
return (value, type);
24+
25+
var underlyingType = Enum.GetUnderlyingType(enumType);
26+
if (value is null)
27+
return (null, underlyingType);
28+
29+
return (Convert.ChangeType(value, underlyingType), underlyingType);
30+
}
31+
}

src/FluentCommand/Query/UpdateBuilder.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,11 @@ public TBuilder Value<TValue>(
131131
/// <returns>
132132
/// The same builder instance for method chaining.
133133
/// </returns>
134+
/// <remarks>
135+
/// Enum and nullable enum values are normalized to their numeric underlying type.
136+
/// </remarks>
134137
/// <exception cref="ArgumentException">Thrown if <paramref name="columnName"/> is null or empty.</exception>
138+
/// <exception cref="ArgumentNullException">Thrown if <paramref name="parameterType"/> is <c>null</c>.</exception>
135139
public TBuilder Value(
136140
string columnName,
137141
object? parameterValue,
@@ -144,7 +148,8 @@ public TBuilder Value(
144148
var updateClause = new UpdateExpression(columnName, paramterName);
145149

146150
UpdateExpressions.Add(updateClause);
147-
Parameters.Add(new QueryParameter(paramterName, parameterValue, parameterType));
151+
var normalized = QueryParameterNormalizer.Normalize(parameterValue, parameterType);
152+
Parameters.Add(new QueryParameter(paramterName, normalized.Value, normalized.Type));
148153

149154
return (TBuilder)this;
150155
}

src/FluentCommand/Query/UpsertBuilder.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ public TBuilder Value<TValue>(
104104
/// <param name="parameterValue">The value to insert or update for the column.</param>
105105
/// <param name="parameterType">The type of the parameter value.</param>
106106
/// <returns>The same builder instance for method chaining.</returns>
107+
/// <remarks>
108+
/// Enum and nullable enum values are normalized to their numeric underlying type.
109+
/// </remarks>
107110
/// <exception cref="ArgumentException">Thrown if <paramref name="columnName"/> is null or empty.</exception>
108111
/// <exception cref="ArgumentNullException">Thrown if <paramref name="parameterType"/> is <c>null</c>.</exception>
109112
public TBuilder Value(
@@ -123,7 +126,8 @@ public TBuilder Value(
123126
ColumnExpressions.Add(columnExpression);
124127
ValueExpressions.Add(paramterName);
125128

126-
Parameters.Add(new QueryParameter(paramterName, parameterValue, parameterType));
129+
var normalized = QueryParameterNormalizer.Normalize(parameterValue, parameterType);
130+
Parameters.Add(new QueryParameter(paramterName, normalized.Value, normalized.Type));
127131

128132
return (TBuilder)this;
129133
}

test/FluentCommand.Generators.Tests/DataReaderFactoryWriterTests.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,43 @@ public void GenerateJsonColumnReader()
5555
Assert.Contains("v_dataWithOptions = dataRecord.GetFromJson<global::FluentCommand.Entities.UserImport>(__index, global::FluentCommand.Entities.UserImportJsonOptionsProvider.Options);", source);
5656
Assert.Contains("v_dataWithContext = dataRecord.GetFromJson<global::FluentCommand.Entities.UserImport>(__index, global::FluentCommand.Entities.UserImportJsonContext.Default.UserImport);", source);
5757
}
58+
59+
[Fact]
60+
public void GenerateEnumColumnReader()
61+
{
62+
var entityClass = new EntityClass
63+
{
64+
InitializationMode = InitializationMode.ObjectInitializer,
65+
FullyQualified = "global::FluentCommand.Entities.EnumLog",
66+
EntityNamespace = "FluentCommand.Entities",
67+
EntityName = "EnumLog",
68+
Properties = new EntityProperty[]
69+
{
70+
new()
71+
{
72+
PropertyName = "Status",
73+
ColumnName = "Status",
74+
PropertyType = "global::FluentCommand.Entities.BuilderStatus",
75+
MemberTypeName = "global::FluentCommand.Entities.BuilderStatus",
76+
IsEnum = true,
77+
EnumUnderlyingType = "short"
78+
},
79+
new()
80+
{
81+
PropertyName = "OptionalStatus",
82+
ColumnName = "OptionalStatus",
83+
PropertyType = "global::FluentCommand.Entities.BuilderStatus?",
84+
MemberTypeName = "global::FluentCommand.Entities.BuilderStatus",
85+
IsEnum = true,
86+
IsNullableEnum = true,
87+
EnumUnderlyingType = "short"
88+
}
89+
}
90+
};
91+
92+
var source = DataReaderFactoryWriter.Generate(entityClass);
93+
94+
Assert.Contains("v_status = (global::FluentCommand.Entities.BuilderStatus)dataRecord.GetInt16(__index);", source);
95+
Assert.Contains("v_optionalStatus = (global::FluentCommand.Entities.BuilderStatus?)dataRecord.GetValue<short?>(__index);", source);
96+
}
5897
}

test/FluentCommand.Tests/Query/InsertBuilderTest.cs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,76 @@ namespace FluentCommand.Tests.Query;
88

99
public class InsertBuilderTest
1010
{
11+
[Fact]
12+
public void InsertValueWithEnumAddsUnderlyingValueAndType()
13+
{
14+
var sqlProvider = new SqlServerGenerator();
15+
var parameters = new List<QueryParameter>();
16+
17+
var builder = new InsertBuilder(sqlProvider, parameters)
18+
.Into("EnumLog")
19+
.Value("Status", BuilderStatus.Active);
20+
21+
var queryStatement = builder.BuildStatement();
22+
var parameter = queryStatement!.Parameters.Single();
23+
24+
parameter.Value.Should().Be((short)BuilderStatus.Active);
25+
parameter.Type.Should().Be(typeof(short));
26+
}
27+
28+
[Fact]
29+
public void InsertValueWithNullableEnumAddsUnderlyingValueAndType()
30+
{
31+
var sqlProvider = new SqlServerGenerator();
32+
var parameters = new List<QueryParameter>();
33+
BuilderStatus? value = BuilderStatus.Active;
34+
35+
var builder = new InsertBuilder(sqlProvider, parameters)
36+
.Into("EnumLog")
37+
.Value("Status", value);
38+
39+
var queryStatement = builder.BuildStatement();
40+
var parameter = queryStatement!.Parameters.Single();
41+
42+
parameter.Value.Should().Be((short)BuilderStatus.Active);
43+
parameter.Type.Should().Be(typeof(short));
44+
}
45+
46+
[Fact]
47+
public void InsertValueWithNullNullableEnumAddsNullWithUnderlyingType()
48+
{
49+
var sqlProvider = new SqlServerGenerator();
50+
var parameters = new List<QueryParameter>();
51+
BuilderStatus? value = null;
52+
53+
var builder = new InsertBuilder(sqlProvider, parameters)
54+
.Into("EnumLog")
55+
.Value("Status", value);
56+
57+
var queryStatement = builder.BuildStatement();
58+
var parameter = queryStatement!.Parameters.Single();
59+
60+
parameter.Value.Should().BeNull();
61+
parameter.Type.Should().Be(typeof(short));
62+
}
63+
64+
[Fact]
65+
public void InsertEntityValuesWithEnumAddsUnderlyingValueAndType()
66+
{
67+
var sqlProvider = new SqlServerGenerator();
68+
var parameters = new List<QueryParameter>();
69+
var entity = new EnumLog { Status = BuilderStatus.Active };
70+
71+
var builder = new InsertEntityBuilder<EnumLog>(sqlProvider, parameters)
72+
.Values(entity);
73+
74+
var queryStatement = builder.BuildStatement();
75+
var parameter = queryStatement!.Parameters.Single();
76+
77+
parameter.Value.Should().Be((short)BuilderStatus.Active);
78+
parameter.Type.Should().Be(typeof(short));
79+
}
80+
1181
[Fact]
1282
public void InsertValueJsonWithOptionsAddsJsonStringParameter()
1383
{
@@ -75,4 +145,15 @@ private sealed class JsonLog
75145
{
76146
public ValueJsonModel Data { get; set; } = null!;
77147
}
148+
149+
private enum BuilderStatus : short
150+
{
151+
Inactive = 0,
152+
Active = 1
153+
}
154+
155+
private sealed class EnumLog
156+
{
157+
public BuilderStatus? Status { get; set; }
158+
}
78159
}

0 commit comments

Comments
 (0)