• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

loresoft / FluentCommand / 24158009941

08 Apr 2026 08:50PM UTC coverage: 56.524% (-0.9%) from 57.452%
24158009941

push

github

pwelter34
Support IgnoreProperty and generator improvements

1412 of 3182 branches covered (44.37%)

Branch coverage included in aggregate %.

0 of 65 new or added lines in 3 files covered. (0.0%)

1 existing line in 1 file now uncovered.

4289 of 6904 relevant lines covered (62.12%)

328.41 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/FluentCommand.Generators/DataReaderFactoryGenerator.cs
1
using System.Collections.Immutable;
2

3
using FluentCommand.Generators.Models;
4

5
using Microsoft.CodeAnalysis;
6

7
namespace FluentCommand.Generators;
8

9
public abstract class DataReaderFactoryGenerator
10
{
11
    protected static void WriteDataReaderSource(SourceProductionContext context, EntityClass entityClass)
12
    {
13
        var qualifiedName = entityClass.EntityNamespace is null
×
14
            ? entityClass.EntityName
×
15
            : $"{entityClass.EntityNamespace}.{entityClass.EntityName}";
×
16

17
        var source = DataReaderFactoryWriter.Generate(entityClass);
×
18

19
        context.AddSource($"{qualifiedName}DataReaderExtensions.g.cs", source);
×
20
    }
×
21

22
    protected static void WriteTypeAccessorSource(SourceProductionContext context, EntityClass entityClass)
23
    {
24
        var qualifiedName = entityClass.EntityNamespace is null
×
25
            ? entityClass.EntityName
×
26
            : $"{entityClass.EntityNamespace}.{entityClass.EntityName}";
×
27

28
        var source = TypeAccessorWriter.Generate(entityClass);
×
29

30
        context.AddSource($"{qualifiedName}TypeAccessor.g.cs", source);
×
31
    }
×
32

33

34
    protected static EntityClass? CreateClass(INamedTypeSymbol targetSymbol)
35
    {
36
        if (targetSymbol == null)
×
37
            return null;
×
38

39
        var fullyQualified = targetSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
×
40
        var classNamespace = targetSymbol.ContainingNamespace.ToDisplayString();
×
41
        var className = targetSymbol.Name;
×
42

43
        // extract table mapping info
44
        var typeAttributes = targetSymbol.GetAttributes();
×
45
        var tableAttribute = FindSchemaAttribute(typeAttributes, "TableAttribute");
×
46

47
        string? tableName = null;
×
48
        string? tableSchema = null;
×
49

50
        if (tableAttribute != null)
×
51
        {
52
            if (tableAttribute.ConstructorArguments.Length > 0 && tableAttribute.ConstructorArguments[0].Value is string name)
×
53
                tableName = name;
×
54

55
            tableSchema = GetNamedString(tableAttribute, "Schema");
×
56
        }
57

58
        var mode = targetSymbol.Constructors.Any(c => c.Parameters.Length == 0)
×
59
            ? InitializationMode.ObjectInitializer
×
60
            : InitializationMode.Constructor;
×
61

NEW
62
        var classIgnored = GetClassIgnoredProperties(typeAttributes);
×
UNCOV
63
        var propertySymbols = GetProperties(targetSymbol);
×
64

65
        if (mode == InitializationMode.ObjectInitializer)
×
66
        {
67
            var propertyArray = propertySymbols
×
NEW
68
                .Select(p => CreateProperty(p, classIgnored: classIgnored))
×
69
                .ToArray();
×
70

71
            return new EntityClass(
×
72
                InitializationMode: mode,
×
73
                FullyQualified: fullyQualified,
×
74
                EntityNamespace: classNamespace,
×
75
                EntityName: className,
×
76
                Properties: propertyArray,
×
77
                TableName: tableName,
×
78
                TableSchema: tableSchema
×
79
            );
×
80
        }
81

82
        // constructor initialization
83

84
        // constructor with same number of parameters as mappable properties
NEW
85
        var mappableCount = propertySymbols
×
NEW
86
            .Count(p => !classIgnored.Contains(p.Name) && !HasIgnorePropertyAttribute(p.GetAttributes()));
×
87

NEW
88
        var constructor = targetSymbol.Constructors.FirstOrDefault(c => c.Parameters.Length == mappableCount);
×
89
        if (constructor == null)
×
90
            return null;
×
91

92
        var properties = new List<EntityProperty>();
×
93
        foreach (var propertySymbol in propertySymbols)
×
94
        {
95
            // find matching constructor name
96
            var parameter = constructor
×
97
                .Parameters
×
98
                .FirstOrDefault(p => string.Equals(p.Name, propertySymbol.Name, StringComparison.InvariantCultureIgnoreCase));
×
99

100
            if (parameter == null)
×
101
                continue;
102

NEW
103
            var property = CreateProperty(propertySymbol, parameter.Name, classIgnored: classIgnored);
×
104
            properties.Add(property);
×
105
        }
106

107
        return new EntityClass(
×
108
            InitializationMode: mode,
×
109
            FullyQualified: fullyQualified,
×
110
            EntityNamespace: classNamespace,
×
111
            EntityName: className,
×
112
            Properties: properties,
×
113
            TableName: tableName,
×
114
            TableSchema: tableSchema
×
115
        );
×
116
    }
117

118
    protected static List<IPropertySymbol> GetProperties(INamedTypeSymbol targetSymbol)
119
    {
120
        var properties = new Dictionary<string, IPropertySymbol>();
×
121

122
        var currentSymbol = targetSymbol;
×
123

124
        // get nested properties
125
        while (currentSymbol != null)
×
126
        {
127
            var propertySymbols = currentSymbol
×
128
                .GetMembers()
×
129
                .Where(m => m.Kind == SymbolKind.Property)
×
130
                .OfType<IPropertySymbol>()
×
131
                .Where(IsIncluded)
×
132
                .Where(p => !properties.ContainsKey(p.Name));
×
133

134
            foreach (var propertySymbol in propertySymbols)
×
135
                properties.Add(propertySymbol.Name, propertySymbol);
×
136

137
            currentSymbol = currentSymbol.BaseType;
×
138
        }
139

NEW
140
        return [.. properties.Values];
×
141
    }
142

143
    protected static EntityProperty CreateProperty(IPropertySymbol propertySymbol, string? parameterName = null, HashSet<string>? classIgnored = null)
144
    {
145
        var propertyType = propertySymbol.Type.ToDisplayString();
×
146
        var memberTypeName = propertySymbol.Type.WithNullableAnnotation(NullableAnnotation.NotAnnotated).ToDisplayString();
×
147
        var propertyName = propertySymbol.Name;
×
148
        var hasGetter = propertySymbol.GetMethod != null;
×
NEW
149
        var hasSetter = propertySymbol.SetMethod?.IsInitOnly == false;
×
NEW
150
        var isNotMapped = (classIgnored?.Contains(propertyName) == true) || !IsSupportedType(propertySymbol.Type);
×
151

152
        var attributes = propertySymbol.GetAttributes();
×
153
        if (attributes == default || attributes.Length == 0)
×
154
        {
155
            return new EntityProperty(
×
156
                PropertyName: propertyName,
×
157
                ColumnName: propertyName,
×
158
                PropertyType: propertyType,
×
159
                MemberTypeName: memberTypeName,
×
160
                ParameterName: parameterName,
×
NEW
161
                IsNotMapped: isNotMapped,
×
162
                HasGetter: hasGetter,
×
163
                HasSetter: hasSetter
×
164
            );
×
165
        }
166

167
        var columnName = GetColumnName(attributes) ?? propertyName;
×
168
        var converterName = GetConverterName(attributes);
×
169

170
        var isKey = HasDataAnnotationAttribute(attributes, "KeyAttribute");
×
171

NEW
172
        isNotMapped = isNotMapped
×
NEW
173
            || IsNotMapped(attributes)
×
NEW
174
            || HasIgnorePropertyAttribute(attributes);
×
175

176
        var isDatabaseGenerated = GetIsDatabaseGenerated(attributes);
×
177
        var isConcurrencyCheck = HasDataAnnotationAttribute(attributes, "ConcurrencyCheckAttribute");
×
178
        var foreignKey = GetSchemaAttributeConstructorStringArg(attributes, "ForeignKeyAttribute");
×
179
        var isRequired = HasDataAnnotationAttribute(attributes, "RequiredAttribute");
×
180
        var displayName = GetNamedString(FindDataAnnotationAttribute(attributes, "DisplayAttribute"), "Name");
×
181
        var dataFormatString = GetNamedString(FindDataAnnotationAttribute(attributes, "DisplayFormatAttribute"), "DataFormatString");
×
182
        var columnType = GetNamedString(FindSchemaAttribute(attributes, "ColumnAttribute"), "TypeName");
×
183
        var columnOrder = GetNamedNumber(FindSchemaAttribute(attributes, "ColumnAttribute"), "Order");
×
184

185
        return new EntityProperty(
×
186
            PropertyName: propertyName,
×
187
            ColumnName: columnName,
×
188
            PropertyType: propertyType,
×
189
            MemberTypeName: memberTypeName,
×
190
            ParameterName: parameterName,
×
191
            ConverterName: converterName,
×
192
            IsKey: isKey,
×
193
            IsNotMapped: isNotMapped,
×
194
            IsDatabaseGenerated: isDatabaseGenerated,
×
195
            IsConcurrencyCheck: isConcurrencyCheck,
×
196
            ForeignKey: foreignKey,
×
197
            IsRequired: isRequired,
×
198
            HasGetter: hasGetter,
×
199
            HasSetter: hasSetter,
×
200
            DisplayName: displayName,
×
201
            DataFormatString: dataFormatString,
×
202
            ColumnType: columnType,
×
203
            ColumnOrder: columnOrder
×
204
        );
×
205
    }
206

207
    protected static string? GetColumnName(ImmutableArray<AttributeData> attributes)
208
    {
209
        var columnAttribute = FindSchemaAttribute(attributes, "ColumnAttribute");
×
210

211
        if (columnAttribute == null)
×
212
            return null;
×
213

214
        // attribute constructor [Column("Name")]
215
        var converterType = columnAttribute.ConstructorArguments.FirstOrDefault();
×
216
        if (converterType.Value is string stringValue)
×
217
            return stringValue;
×
218

219
        return null;
×
220
    }
221

222

223
    private static string? GetConverterName(ImmutableArray<AttributeData> attributes)
224
    {
225
        var converter = attributes
×
226
            .FirstOrDefault(a => a.AttributeClass is
×
227
            {
×
228
                Name: "DataFieldConverterAttribute",
×
229
                ContainingNamespace.Name: "FluentCommand"
×
230
            });
×
231

232
        if (converter == null)
×
233
            return null;
×
234

235
        // attribute constructor
236
        var converterType = converter.ConstructorArguments.FirstOrDefault();
×
237
        if (converterType.Value is INamedTypeSymbol converterSymbol)
×
238
            return converterSymbol.ToDisplayString();
×
239

240
        // generic attribute
241
        var attributeClass = converter.AttributeClass;
×
242
        if (attributeClass is { IsGenericType: true }
×
243
            && attributeClass.TypeArguments.Length == attributeClass.TypeParameters.Length
×
244
            && attributeClass.TypeArguments.Length == 1)
×
245
        {
246
            var typeArgument = attributeClass.TypeArguments[0];
×
247
            return typeArgument.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
×
248
        }
249

250
        return null;
×
251
    }
252

253
    private static AttributeData? FindDataAnnotationAttribute(ImmutableArray<AttributeData> attributes, string name)
254
    {
255
        return attributes.FirstOrDefault(a => a.AttributeClass is
×
256
        {
×
257
            ContainingNamespace:
×
258
            {
×
259
                Name: "DataAnnotations",
×
260
                ContainingNamespace:
×
261
                {
×
262
                    Name: "ComponentModel",
×
263
                    ContainingNamespace.Name: "System"
×
264
                }
×
265
            }
×
266
        } && a.AttributeClass.Name == name);
×
267
    }
268

269
    private static bool HasDataAnnotationAttribute(ImmutableArray<AttributeData> attributes, string name)
270
    {
271
        return FindDataAnnotationAttribute(attributes, name) != null;
×
272
    }
273

274
    protected static AttributeData? FindSchemaAttribute(ImmutableArray<AttributeData> attributes, string name)
275
    {
276
        return attributes.FirstOrDefault(a =>
×
277
            a.AttributeClass is
×
278
            {
×
279
                ContainingNamespace:
×
280
                {
×
281
                    Name: "Schema",
×
282
                    ContainingNamespace:
×
283
                    {
×
284
                        Name: "DataAnnotations",
×
285
                        ContainingNamespace:
×
286
                        {
×
287
                            Name: "ComponentModel",
×
288
                            ContainingNamespace.Name: "System"
×
289
                        }
×
290
                    }
×
291
                }
×
292
            }
×
293
            && a.AttributeClass.Name == name
×
294
        );
×
295
    }
296

297
    private static string? GetSchemaAttributeConstructorStringArg(ImmutableArray<AttributeData> attributes, string name)
298
    {
299
        var attribute = FindSchemaAttribute(attributes, name);
×
300
        if (attribute?.ConstructorArguments.Length > 0 && attribute.ConstructorArguments[0].Value is string value)
×
301
            return value;
×
302

303
        return null;
×
304
    }
305

306
    private static bool GetIsDatabaseGenerated(ImmutableArray<AttributeData> attributes)
307
    {
308
        var attribute = FindSchemaAttribute(attributes, "DatabaseGeneratedAttribute");
×
309
        if (attribute == null)
×
310
            return false;
×
311

312
        if (attribute.ConstructorArguments.Length > 0 && attribute.ConstructorArguments[0].Value is int option)
×
313
            return option != 0; // DatabaseGeneratedOption.None = 0
×
314

315
        return false;
×
316
    }
317

318
    private static string? GetNamedString(AttributeData? attribute, string argName)
319
    {
320
        if (attribute == null)
×
321
            return null;
×
322

323
        foreach (var namedArg in attribute.NamedArguments)
×
324
        {
325
            if (namedArg.Key == argName && namedArg.Value.Value is string value)
×
326
                return value;
×
327
        }
328

329
        return null;
×
330
    }
331

332
    private static int? GetNamedNumber(AttributeData? attribute, string argName)
333
    {
334
        if (attribute == null)
×
335
            return null;
×
336

337
        foreach (var namedArg in attribute.NamedArguments)
×
338
        {
339
            if (namedArg.Key == argName && namedArg.Value.Value is int value)
×
340
                return value;
×
341
        }
342

343
        return null;
×
344
    }
345

346
    protected static bool IsIncluded(IPropertySymbol propertySymbol)
347
    {
NEW
348
        return !propertySymbol.IsIndexer
×
NEW
349
            && !propertySymbol.IsAbstract
×
NEW
350
            && propertySymbol.DeclaredAccessibility == Accessibility.Public;
×
351
    }
352

353
    private static bool IsSupportedType(ITypeSymbol type)
354
    {
355
        // handle nullable value types
NEW
356
        if (type is INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T } namedType)
×
NEW
357
            return IsSupportedType(namedType.TypeArguments[0]);
×
358

359
        // enums are stored as their underlying integer type
NEW
360
        if (type.TypeKind == TypeKind.Enum)
×
NEW
361
            return true;
×
362

363
        // primitives and string
NEW
364
        switch (type.SpecialType)
×
365
        {
366
            case SpecialType.System_Boolean:
367
            case SpecialType.System_Byte:
368
            case SpecialType.System_Char:
369
            case SpecialType.System_Decimal:
370
            case SpecialType.System_Double:
371
            case SpecialType.System_Single:
372
            case SpecialType.System_Int16:
373
            case SpecialType.System_Int32:
374
            case SpecialType.System_Int64:
375
            case SpecialType.System_String:
NEW
376
                return true;
×
377
        }
378

379
        // byte[]
NEW
380
        if (type is IArrayTypeSymbol { ElementType.SpecialType: SpecialType.System_Byte })
×
NEW
381
            return true;
×
382

383
        // well-known struct types and FluentCommand.ConcurrencyToken
NEW
384
        var fullName = type.ToDisplayString();
×
NEW
385
        return fullName is
×
NEW
386
            "System.DateTime" or
×
NEW
387
            "System.DateTimeOffset" or
×
NEW
388
            "System.Guid" or
×
NEW
389
            "System.TimeSpan" or
×
NEW
390
            "System.DateOnly" or
×
NEW
391
            "System.TimeOnly" or
×
NEW
392
            "FluentCommand.ConcurrencyToken";
×
393
    }
394

395
    private static bool IsNotMapped(ImmutableArray<AttributeData> attributes)
396
    {
397
        return attributes.Any(
×
398
            a => a.AttributeClass is
×
399
            {
×
400
                Name: "NotMappedAttribute",
×
401
                ContainingNamespace:
×
402
                {
×
403
                    Name: "Schema",
×
404
                    ContainingNamespace:
×
405
                    {
×
406
                        Name: "DataAnnotations",
×
407
                        ContainingNamespace:
×
408
                        {
×
409
                            Name: "ComponentModel",
×
410
                            ContainingNamespace.Name: "System"
×
411
                        }
×
412
                    }
×
413
                }
×
414
            });
×
415
    }
416

417
    private static bool HasIgnorePropertyAttribute(ImmutableArray<AttributeData> attributes)
418
    {
NEW
419
        return attributes.Any(a => a.AttributeClass is
×
NEW
420
        {
×
NEW
421
            Name: "IgnorePropertyAttribute",
×
NEW
422
            ContainingNamespace:
×
NEW
423
            {
×
NEW
424
                Name: "Attributes",
×
NEW
425
                ContainingNamespace.Name: "FluentCommand"
×
NEW
426
            }
×
NEW
427
        });
×
428
    }
429

430
    private static HashSet<string> GetClassIgnoredProperties(ImmutableArray<AttributeData> attributes)
431
    {
NEW
432
        var ignored = new HashSet<string>(StringComparer.Ordinal);
×
433

NEW
434
        foreach (var attr in attributes)
×
435
        {
NEW
436
            if (attr.AttributeClass is not
×
NEW
437
                {
×
NEW
438
                    Name: "IgnorePropertyAttribute",
×
NEW
439
                    ContainingNamespace:
×
NEW
440
                    {
×
NEW
441
                        Name: "Attributes",
×
NEW
442
                        ContainingNamespace.Name: "FluentCommand"
×
NEW
443
                    }
×
NEW
444
                })
×
445
            {
446
                continue;
447
            }
448

449
            // constructor argument: [IgnoreProperty("Name")] or [IgnoreProperty(nameof(T.Name))]
NEW
450
            if (attr.ConstructorArguments.Length > 0 && attr.ConstructorArguments[0].Value is string ctorName)
×
451
            {
NEW
452
                ignored.Add(ctorName);
×
NEW
453
                continue;
×
454
            }
455

456
            // named argument: [IgnoreProperty(PropertyName = "Name")]
NEW
457
            foreach (var namedArg in attr.NamedArguments)
×
458
            {
NEW
459
                if (namedArg.Key == "PropertyName" && namedArg.Value.Value is string namedValue)
×
NEW
460
                    ignored.Add(namedValue);
×
461
            }
462
        }
463

NEW
464
        return ignored;
×
465
    }
466
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc