Skip to content

Commit b3660c7

Browse files
committed
Include MemberTypeName to support nullable types
1 parent ae90be1 commit b3660c7

7 files changed

Lines changed: 205 additions & 17 deletions

File tree

src/FluentCommand.Generators/DataReaderFactoryGenerator.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ protected static List<IPropertySymbol> GetProperties(INamedTypeSymbol targetSymb
139139
protected static EntityProperty CreateProperty(IPropertySymbol propertySymbol, string? parameterName = null)
140140
{
141141
var propertyType = propertySymbol.Type.ToDisplayString();
142+
var memberTypeName = propertySymbol.Type.WithNullableAnnotation(NullableAnnotation.NotAnnotated).ToDisplayString();
142143
var propertyName = propertySymbol.Name;
143144
var hasGetter = propertySymbol.GetMethod != null;
144145
var hasSetter = propertySymbol.SetMethod != null && !propertySymbol.SetMethod.IsInitOnly;
@@ -150,6 +151,7 @@ protected static EntityProperty CreateProperty(IPropertySymbol propertySymbol, s
150151
PropertyName: propertyName,
151152
ColumnName: propertyName,
152153
PropertyType: propertyType,
154+
MemberTypeName: memberTypeName,
153155
ParameterName: parameterName,
154156
HasGetter: hasGetter,
155157
HasSetter: hasSetter
@@ -174,6 +176,7 @@ protected static EntityProperty CreateProperty(IPropertySymbol propertySymbol, s
174176
PropertyName: propertyName,
175177
ColumnName: columnName,
176178
PropertyType: propertyType,
179+
MemberTypeName: memberTypeName,
177180
ParameterName: parameterName,
178181
ConverterName: converterName,
179182
IsKey: isKey,

src/FluentCommand.Generators/Models/EntityProperty.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ public record EntityProperty(
44
string PropertyName,
55
string ColumnName,
66
string PropertyType,
7+
string MemberTypeName,
78
string? ParameterName = null,
89
string? ConverterName = null,
910
bool IsKey = false,

src/FluentCommand.Generators/TypeAccessorWriter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ private static void WriteMemberAccessorClass(IndentedStringBuilder codeBuilder,
189189
// MemberType
190190
codeBuilder
191191
.Append("public global::System.Type MemberType => typeof(")
192-
.Append(prop.PropertyType)
192+
.Append(prop.MemberTypeName)
193193
.AppendLine(");");
194194

195195
// Name

src/FluentCommand.SqlServer/SqlDataRecordAdapter.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,11 @@ public IEnumerator<SqlDataRecord> GetEnumerator()
5858

5959
private static (SqlMetaData[] MetaData, IMemberAccessor[] Columns) GetCachedMetaData()
6060
{
61-
return _metaDataCache.GetOrAdd(typeof(T), _ => BuildMetaData());
61+
// short-circuit to avoid the overhead of GetOrAdd and BuildMetaData
62+
if (_metaDataCache.TryGetValue(typeof(T), out var cached))
63+
return cached;
64+
65+
return _metaDataCache.GetOrAdd(typeof(T), static _ => BuildMetaData());
6266
}
6367

6468
private static (SqlMetaData[] MetaData, IMemberAccessor[] Columns) BuildMetaData()

test/FluentCommand.Generators.Tests/DataReaderFactoryWriterTests.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ public async Task Generate()
1616
"Status",
1717
new EntityProperty[]
1818
{
19-
new("Id", "Id", typeof(int).FullName!),
20-
new("Name", "Name", typeof(string).FullName!),
21-
new("IsActive", "IsActive", typeof(bool).FullName!),
22-
new("Updated", "Updated", typeof(DateTimeOffset).FullName!),
23-
new("RowVersion", "RowVersion", typeof(byte[]).FullName!),
19+
new("Id", "Id", typeof(int).FullName!, typeof(int).FullName!),
20+
new("Name", "Name", typeof(string).FullName!, typeof(string).FullName!),
21+
new("IsActive", "IsActive", typeof(bool).FullName!, typeof(bool).FullName!),
22+
new("Updated", "Updated", typeof(DateTimeOffset).FullName!, typeof(DateTimeOffset).FullName!),
23+
new("RowVersion", "RowVersion", typeof(byte[]).FullName!, typeof(byte[]).FullName!),
2424
}
2525
);
2626

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
// <auto-generated />
2+
#nullable enable
3+
4+
namespace FluentCommand.Entities
5+
{
6+
internal sealed class ContactGeneratedTypeAccessor : global::FluentCommand.Reflection.TypeAccessor
7+
{
8+
internal static readonly global::FluentCommand.Reflection.IMemberAccessor IdMember = new IdMemberAccessor();
9+
internal static readonly global::FluentCommand.Reflection.IMemberAccessor NameMember = new NameMemberAccessor();
10+
internal static readonly global::FluentCommand.Reflection.IMemberAccessor EmailMember = new EmailMemberAccessor();
11+
internal static readonly global::FluentCommand.Reflection.IMemberAccessor AgeMember = new AgeMemberAccessor();
12+
13+
public ContactGeneratedTypeAccessor()
14+
: base(typeof(global::FluentCommand.Entities.Contact), "Contact", null, static () => new global::FluentCommand.Entities.Contact())
15+
{
16+
RegisterMember("Id", IdMember);
17+
RegisterMember("Name", NameMember);
18+
RegisterMember("Email", EmailMember);
19+
RegisterMember("Age", AgeMember);
20+
21+
RegisterProperties(
22+
global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.Static | global::System.Reflection.BindingFlags.Instance,
23+
new global::FluentCommand.Reflection.IMemberAccessor[]
24+
{
25+
IdMember,
26+
NameMember,
27+
EmailMember,
28+
AgeMember
29+
});
30+
}
31+
32+
private sealed class IdMemberAccessor : global::FluentCommand.Reflection.IMemberAccessor
33+
{
34+
public global::System.Type MemberType => typeof(int);
35+
public string Name => "Id";
36+
public string Column => "Id";
37+
public string? ColumnType => null;
38+
public int? ColumnOrder => null;
39+
public bool IsKey => true;
40+
public bool IsNotMapped => false;
41+
public bool IsDatabaseGenerated => true;
42+
public bool IsConcurrencyCheck => false;
43+
public string? ForeignKey => null;
44+
public bool HasGetter => true;
45+
public bool HasSetter => true;
46+
public bool IsRequired => false;
47+
public string DisplayName => "Id";
48+
public string? DataFormatString => null;
49+
50+
public object? GetValue(object instance)
51+
=> ((global::FluentCommand.Entities.Contact)instance).Id;
52+
53+
public void SetValue(object instance, object? value)
54+
=> ((global::FluentCommand.Entities.Contact)instance).Id = (int)value!;
55+
}
56+
57+
private sealed class NameMemberAccessor : global::FluentCommand.Reflection.IMemberAccessor
58+
{
59+
public global::System.Type MemberType => typeof(string);
60+
public string Name => "Name";
61+
public string Column => "Name";
62+
public string? ColumnType => null;
63+
public int? ColumnOrder => null;
64+
public bool IsKey => false;
65+
public bool IsNotMapped => false;
66+
public bool IsDatabaseGenerated => false;
67+
public bool IsConcurrencyCheck => false;
68+
public string? ForeignKey => null;
69+
public bool HasGetter => true;
70+
public bool HasSetter => true;
71+
public bool IsRequired => true;
72+
public string DisplayName => "Name";
73+
public string? DataFormatString => null;
74+
75+
public object? GetValue(object instance)
76+
=> ((global::FluentCommand.Entities.Contact)instance).Name;
77+
78+
public void SetValue(object instance, object? value)
79+
=> ((global::FluentCommand.Entities.Contact)instance).Name = (string)value!;
80+
}
81+
82+
private sealed class EmailMemberAccessor : global::FluentCommand.Reflection.IMemberAccessor
83+
{
84+
public global::System.Type MemberType => typeof(string);
85+
public string Name => "Email";
86+
public string Column => "Email";
87+
public string? ColumnType => null;
88+
public int? ColumnOrder => null;
89+
public bool IsKey => false;
90+
public bool IsNotMapped => false;
91+
public bool IsDatabaseGenerated => false;
92+
public bool IsConcurrencyCheck => false;
93+
public string? ForeignKey => null;
94+
public bool HasGetter => true;
95+
public bool HasSetter => true;
96+
public bool IsRequired => false;
97+
public string DisplayName => "Email";
98+
public string? DataFormatString => null;
99+
100+
public object? GetValue(object instance)
101+
=> ((global::FluentCommand.Entities.Contact)instance).Email;
102+
103+
public void SetValue(object instance, object? value)
104+
=> ((global::FluentCommand.Entities.Contact)instance).Email = (string?)value!;
105+
}
106+
107+
private sealed class AgeMemberAccessor : global::FluentCommand.Reflection.IMemberAccessor
108+
{
109+
public global::System.Type MemberType => typeof(int?);
110+
public string Name => "Age";
111+
public string Column => "Age";
112+
public string? ColumnType => null;
113+
public int? ColumnOrder => null;
114+
public bool IsKey => false;
115+
public bool IsNotMapped => false;
116+
public bool IsDatabaseGenerated => false;
117+
public bool IsConcurrencyCheck => false;
118+
public string? ForeignKey => null;
119+
public bool HasGetter => true;
120+
public bool HasSetter => true;
121+
public bool IsRequired => false;
122+
public string DisplayName => "Age";
123+
public string? DataFormatString => null;
124+
125+
public object? GetValue(object instance)
126+
=> ((global::FluentCommand.Entities.Contact)instance).Age;
127+
128+
public void SetValue(object instance, object? value)
129+
=> ((global::FluentCommand.Entities.Contact)instance).Age = (int?)value!;
130+
}
131+
132+
}
133+
134+
internal static class ContactTypeAccessorRegistration
135+
{
136+
#if NET5_0_OR_GREATER
137+
[global::System.Runtime.CompilerServices.ModuleInitializer]
138+
#endif
139+
internal static void Register()
140+
{
141+
global::FluentCommand.Reflection.TypeAccessor.Register(
142+
typeof(global::FluentCommand.Entities.Contact),
143+
new ContactGeneratedTypeAccessor());
144+
}
145+
}
146+
}

test/FluentCommand.Generators.Tests/TypeAccessorWriterTests.cs

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ public async Task GenerateObjectInitializer()
1414
"Status",
1515
new EntityProperty[]
1616
{
17-
new("Id", "Id", typeof(int).FullName!, IsKey: true, IsDatabaseGenerated: true),
18-
new("Name", "Name", typeof(string).FullName!, IsRequired: true),
19-
new("IsActive", "IsActive", typeof(bool).FullName!),
20-
new("Updated", "Updated", typeof(DateTimeOffset).FullName!, IsConcurrencyCheck: true),
21-
new("RowVersion", "RowVersion", typeof(byte[]).FullName!),
17+
new("Id", "Id", typeof(int).FullName!, typeof(int).FullName!, IsKey: true, IsDatabaseGenerated: true),
18+
new("Name", "Name", typeof(string).FullName!, typeof(string).FullName!, IsRequired: true),
19+
new("IsActive", "IsActive", typeof(bool).FullName!, typeof(bool).FullName!),
20+
new("Updated", "Updated", typeof(DateTimeOffset).FullName!, typeof(DateTimeOffset).FullName!, IsConcurrencyCheck: true),
21+
new("RowVersion", "RowVersion", typeof(byte[]).FullName!, typeof(byte[]).FullName!),
2222
},
2323
TableName: "Status",
2424
TableSchema: "dbo"
@@ -42,9 +42,9 @@ public async Task GenerateWithColumnMapping()
4242
"User",
4343
new EntityProperty[]
4444
{
45-
new("Id", "user_id", typeof(int).FullName!, IsKey: true, IsDatabaseGenerated: true, ColumnType: "int"),
46-
new("Name", "user_name", typeof(string).FullName!, IsRequired: true, DisplayName: "User Name"),
47-
new("Email", "Email", typeof(string).FullName!),
45+
new("Id", "user_id", typeof(int).FullName!, typeof(int).FullName!, IsKey: true, IsDatabaseGenerated: true, ColumnType: "int"),
46+
new("Name", "user_name", typeof(string).FullName!, typeof(string).FullName!, IsRequired: true, DisplayName: "User Name"),
47+
new("Email", "Email", typeof(string).FullName!, typeof(string).FullName!),
4848
},
4949
TableName: "Users"
5050
);
@@ -67,8 +67,8 @@ public async Task GenerateConstructorMode()
6767
"UserRecord",
6868
new EntityProperty[]
6969
{
70-
new("Id", "Id", typeof(int).FullName!, ParameterName: "id", IsKey: true),
71-
new("Name", "Name", typeof(string).FullName!, ParameterName: "name"),
70+
new("Id", "Id", typeof(int).FullName!, typeof(int).FullName!, ParameterName: "id", IsKey: true),
71+
new("Name", "Name", typeof(string).FullName!, typeof(string).FullName!, ParameterName: "name"),
7272
}
7373
);
7474

@@ -79,4 +79,38 @@ await Verifier
7979
.UseDirectory("Snapshots")
8080
.ScrubLinesContaining("GeneratedCodeAttribute");
8181
}
82+
83+
[Fact]
84+
public async Task GenerateNullableReferenceType()
85+
{
86+
var entityClass = new EntityClass(
87+
InitializationMode.ObjectInitializer,
88+
"global::FluentCommand.Entities.Contact",
89+
"FluentCommand.Entities",
90+
"Contact",
91+
new EntityProperty[]
92+
{
93+
new("Id", "Id", "int", "int", IsKey: true, IsDatabaseGenerated: true),
94+
new("Name", "Name", "string", "string", IsRequired: true),
95+
new("Email", "Email", "string?", "string"),
96+
new("Age", "Age", "int?", "int?"),
97+
},
98+
TableName: "Contact"
99+
);
100+
101+
var source = TypeAccessorWriter.Generate(entityClass);
102+
103+
// verify typeof() uses MemberTypeName (no nullable annotation)
104+
// and setter cast uses PropertyType (preserving nullable annotation)
105+
Assert.Contains("typeof(string)", source);
106+
Assert.DoesNotContain("typeof(string?)", source);
107+
Assert.Contains("typeof(int?)", source);
108+
Assert.Contains("= (string?)value!", source);
109+
Assert.Contains("= (int?)value!", source);
110+
111+
await Verifier
112+
.Verify(source)
113+
.UseDirectory("Snapshots")
114+
.ScrubLinesContaining("GeneratedCodeAttribute");
115+
}
82116
}

0 commit comments

Comments
 (0)