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

loresoft / FluentCommand / 26620734448

29 May 2026 05:53AM UTC coverage: 54.913% (-0.9%) from 55.778%
26620734448

push

github

pwelter34
Validate JsonColumn and use fully-qualified types

1412 of 3414 branches covered (41.36%)

Branch coverage included in aggregate %.

13 of 102 new or added lines in 4 files covered. (12.75%)

12 existing lines in 3 files now uncovered.

4478 of 7312 relevant lines covered (61.24%)

306.51 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
using Microsoft.CodeAnalysis.CSharp;
7
using Microsoft.CodeAnalysis.CSharp.Syntax;
8

9
namespace FluentCommand.Generators;
10

11
[Generator(LanguageNames.CSharp)]
12
public sealed class DataReaderFactoryGenerator : IIncrementalGenerator
13
{
NEW
14
    private static readonly SymbolDisplayFormat FullyQualifiedNullableFormat = SymbolDisplayFormat.FullyQualifiedFormat
×
NEW
15
        .WithMiscellaneousOptions(SymbolDisplayFormat.FullyQualifiedFormat.MiscellaneousOptions | SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier);
×
16

17
    public void Initialize(IncrementalGeneratorInitializationContext context)
18
    {
19
        // Pipeline for [GenerateReader(typeof(T))] attribute
20
        var generateAttributeClasses = context.SyntaxProvider.ForAttributeWithMetadataName(
×
21
            fullyQualifiedMetadataName: "FluentCommand.Attributes.GenerateReaderAttribute",
×
22
            predicate: static (_, __) => true,
×
23
            transform: static (context, _) =>
×
24
            {
×
25
                if (context.Attributes.Length == 0)
×
26
                    return [];
×
27

×
28
                var classes = new List<EntityClass>();
×
29

×
30
                foreach (var attribute in context.Attributes)
×
31
                {
×
32
                    if (attribute == null)
×
33
                        return [];
×
34

×
35
                    if (attribute.ConstructorArguments.Length != 1)
×
36
                        return [];
×
37

×
38
                    var comparerArgument = attribute.ConstructorArguments[0];
×
39
                    if (comparerArgument.Value is not INamedTypeSymbol targetSymbol)
×
40
                        return [];
×
41

×
42
                    var entityClass = CreateClass(targetSymbol);
×
43
                    if (entityClass != null)
×
44
                        classes.Add(entityClass);
×
45
                }
×
46

×
47
                return new EquatableArray<EntityClass>(classes);
×
48
            }
×
49
        )
×
50
        .Where(static context => context.Count > 0)
×
51
        .SelectMany(static (item, _) => item)
×
52
        .WithTrackingName("GenerateAttributeGenerator");
×
53

54
        context.RegisterSourceOutput(generateAttributeClasses, WriteDataReaderSource);
×
55
        context.RegisterSourceOutput(generateAttributeClasses, WriteTypeAccessorSource);
×
56

57
        // Pipeline for [Table] attribute
58
        var tableAttributeClasses = context.SyntaxProvider.ForAttributeWithMetadataName(
×
59
            fullyQualifiedMetadataName: "System.ComponentModel.DataAnnotations.Schema.TableAttribute",
×
60
            predicate: static (syntaxNode, _) =>
×
61
            {
×
62
                return
×
63
                    (
×
64
                        syntaxNode is ClassDeclarationSyntax { AttributeLists.Count: > 0 } classDeclaration
×
65
                            && !classDeclaration.Modifiers.Any(SyntaxKind.AbstractKeyword)
×
66
                            && !classDeclaration.Modifiers.Any(SyntaxKind.StaticKeyword)
×
67
                    )
×
68
                    ||
×
69
                    (
×
70
                        syntaxNode is RecordDeclarationSyntax { AttributeLists.Count: > 0 } recordDeclaration
×
71
                            && !recordDeclaration.Modifiers.Any(SyntaxKind.AbstractKeyword)
×
72
                            && !recordDeclaration.Modifiers.Any(SyntaxKind.StaticKeyword)
×
73
                    );
×
74
            },
×
75
            transform: static (context, _) =>
×
76
            {
×
77
                if (context.TargetSymbol is not INamedTypeSymbol targetSymbol)
×
78
                    return null;
×
79

×
80
                return CreateClass(targetSymbol);
×
81
            }
×
82
        )
×
83
        .Where(static context => context is not null)
×
84
        .Select(static (context, _) => context!)
×
85
        .WithTrackingName("TableAttributeGenerator");
×
86

87
        context.RegisterSourceOutput(tableAttributeClasses, WriteDataReaderSource);
×
88
        context.RegisterSourceOutput(tableAttributeClasses, WriteTypeAccessorSource);
×
89
    }
×
90

91
    private static void WriteDataReaderSource(SourceProductionContext context, EntityClass entityClass)
92
    {
93
        var qualifiedName = entityClass.EntityNamespace is null
×
94
            ? entityClass.EntityName
×
95
            : $"{entityClass.EntityNamespace}.{entityClass.EntityName}";
×
96

97
        var source = DataReaderFactoryWriter.Generate(entityClass);
×
98

99
        context.AddSource($"{qualifiedName}DataReaderExtensions.g.cs", source);
×
100
    }
×
101

102
    private static void WriteTypeAccessorSource(SourceProductionContext context, EntityClass entityClass)
103
    {
104
        var qualifiedName = entityClass.EntityNamespace is null
×
105
            ? entityClass.EntityName
×
106
            : $"{entityClass.EntityNamespace}.{entityClass.EntityName}";
×
107

108
        var source = TypeAccessorWriter.Generate(entityClass);
×
109

110
        context.AddSource($"{qualifiedName}TypeAccessor.g.cs", source);
×
111
    }
×
112

113

114
    private static EntityClass? CreateClass(INamedTypeSymbol targetSymbol)
115
    {
116
        if (targetSymbol == null)
×
117
            return null;
×
118

119
        var fullyQualified = targetSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
×
120
        var classNamespace = targetSymbol.ContainingNamespace.ToDisplayString();
×
121
        var className = targetSymbol.Name;
×
122

123
        // extract table mapping info
124
        var typeAttributes = targetSymbol.GetAttributes();
×
125
        var tableAttribute = FindSchemaAttribute(typeAttributes, "TableAttribute");
×
126

127
        string? tableName = null;
×
128
        string? tableSchema = null;
×
129

130
        if (tableAttribute != null)
×
131
        {
132
            if (tableAttribute.ConstructorArguments.Length > 0 && tableAttribute.ConstructorArguments[0].Value is string name)
×
133
                tableName = name;
×
134

135
            tableSchema = GetNamedString(tableAttribute, "Schema");
×
136
        }
137

138
        var mode = targetSymbol.Constructors.Any(c => c.Parameters.Length == 0)
×
139
            ? InitializationMode.ObjectInitializer
×
140
            : InitializationMode.Constructor;
×
141

142
        var classIgnored = GetClassIgnoredProperties(typeAttributes);
×
143
        var propertySymbols = GetProperties(targetSymbol);
×
144

145
        if (mode == InitializationMode.ObjectInitializer)
×
146
        {
147
            var propertyArray = propertySymbols
×
148
                .Select(p => CreateProperty(p, classIgnored: classIgnored))
×
149
                .ToArray();
×
150

151
            return new EntityClass
×
152
            {
×
153
                InitializationMode = mode,
×
154
                FullyQualified = fullyQualified,
×
155
                EntityNamespace = classNamespace,
×
156
                EntityName = className,
×
157
                Properties = propertyArray,
×
158
                TableName = tableName,
×
159
                TableSchema = tableSchema
×
160
            };
×
161
        }
162

163
        // constructor initialization
164

165
        // constructor with same number of parameters as mappable properties
166
        var mappableCount = propertySymbols
×
167
            .Count(p => IsMappableProperty(p, classIgnored));
×
168

169
        var constructor = targetSymbol.Constructors.FirstOrDefault(c => c.Parameters.Length == mappableCount);
×
170
        if (constructor == null)
×
171
            return null;
×
172

173
        var properties = new List<EntityProperty>();
×
174
        foreach (var propertySymbol in propertySymbols)
×
175
        {
176
            // find matching constructor name
177
            var parameter = constructor
×
178
                .Parameters
×
179
                .FirstOrDefault(p => string.Equals(p.Name, propertySymbol.Name, StringComparison.InvariantCultureIgnoreCase));
×
180

181
            if (parameter == null)
×
182
                continue;
183

NEW
184
            var property = CreateProperty(
×
NEW
185
                propertySymbol: propertySymbol,
×
NEW
186
                parameterName: parameter.Name,
×
NEW
187
                classIgnored: classIgnored,
×
NEW
188
                parameterAttributes: parameter.GetAttributes());
×
189

UNCOV
190
            properties.Add(property);
×
191
        }
192

193
        return new EntityClass
×
194
        {
×
195
            InitializationMode = mode,
×
196
            FullyQualified = fullyQualified,
×
197
            EntityNamespace = classNamespace,
×
198
            EntityName = className,
×
199
            Properties = properties,
×
200
            TableName = tableName,
×
201
            TableSchema = tableSchema
×
202
        };
×
203
    }
204

205
    private static List<IPropertySymbol> GetProperties(INamedTypeSymbol targetSymbol)
206
    {
207
        var properties = new Dictionary<string, IPropertySymbol>();
×
208

209
        var currentSymbol = targetSymbol;
×
210

211
        // get nested properties
212
        while (currentSymbol != null)
×
213
        {
214
            var propertySymbols = currentSymbol
×
215
                .GetMembers()
×
216
                .Where(m => m.Kind == SymbolKind.Property)
×
217
                .OfType<IPropertySymbol>()
×
218
                .Where(IsIncluded)
×
219
                .Where(p => !properties.ContainsKey(p.Name));
×
220

221
            foreach (var propertySymbol in propertySymbols)
×
222
                properties.Add(propertySymbol.Name, propertySymbol);
×
223

224
            currentSymbol = currentSymbol.BaseType;
×
225
        }
226

227
        return [.. properties.Values];
×
228
    }
229

230
    private static EntityProperty CreateProperty(
231
        IPropertySymbol propertySymbol,
232
        string? parameterName = null,
233
        HashSet<string>? classIgnored = null,
234
        ImmutableArray<AttributeData> parameterAttributes = default)
235
    {
NEW
236
        var propertyType = propertySymbol.Type.ToDisplayString(FullyQualifiedNullableFormat);
×
NEW
237
        var memberTypeName = propertySymbol.Type.WithNullableAnnotation(NullableAnnotation.NotAnnotated).ToDisplayString(FullyQualifiedNullableFormat);
×
238
        var propertyName = propertySymbol.Name;
×
239
        var hasGetter = propertySymbol.GetMethod != null;
×
240
        var hasSetter = propertySymbol.SetMethod?.IsInitOnly == false;
×
241

242
        // Merge property attributes with constructor parameter attributes (parameter attributes take precedence for converters)
NEW
243
        var propertyAttributes = propertySymbol.GetAttributes();
×
NEW
244
        var attributes = (!parameterAttributes.IsDefaultOrEmpty && !propertyAttributes.IsDefaultOrEmpty)
×
NEW
245
            ? propertyAttributes.AddRange(parameterAttributes)
×
NEW
246
            : !parameterAttributes.IsDefaultOrEmpty ? parameterAttributes : propertyAttributes;
×
247

248
        var jsonColumn = GetJsonColumn(attributes);
×
249
        var isJsonColumn = jsonColumn != null;
×
250
        var enumInfo = GetEnumInfo(propertySymbol.Type);
×
251
        var isNullable = IsNullableType(propertySymbol.Type);
×
252
        var isNotMapped = (classIgnored?.Contains(propertyName) == true) || (!isJsonColumn && !IsSupportedType(propertySymbol.Type));
×
253

254
        if (attributes == default || attributes.Length == 0)
×
255
        {
256
            return new EntityProperty
×
257
            {
×
258
                PropertyName = propertyName,
×
259
                ColumnName = propertyName,
×
260
                PropertyType = propertyType,
×
261
                MemberTypeName = memberTypeName,
×
262
                ParameterName = parameterName,
×
263
                IsNotMapped = isNotMapped,
×
264
                HasGetter = hasGetter,
×
265
                HasSetter = hasSetter,
×
266
                IsNullable = isNullable,
×
267
                IsJsonColumn = isJsonColumn,
×
268
                IsEnum = enumInfo.IsEnum,
×
269
                IsNullableEnum = enumInfo.IsNullableEnum,
×
270
                EnumUnderlyingType = enumInfo.UnderlyingType
×
271
            };
×
272
        }
273

274
        var columnName = GetColumnName(attributes) ?? propertyName;
×
275
        var converterName = GetConverterName(attributes);
×
276

277
        var isKey = HasDataAnnotationAttribute(attributes, "KeyAttribute");
×
278

279
        isNotMapped = isNotMapped
×
280
            || IsNotMapped(attributes)
×
281
            || HasIgnorePropertyAttribute(attributes);
×
282

283
        var isDatabaseGenerated = GetIsDatabaseGenerated(attributes);
×
284
        var isConcurrencyCheck = HasDataAnnotationAttribute(attributes, "ConcurrencyCheckAttribute");
×
285
        var foreignKey = GetSchemaAttributeConstructorStringArg(attributes, "ForeignKeyAttribute");
×
286
        var isRequired = HasDataAnnotationAttribute(attributes, "RequiredAttribute");
×
287
        var displayName = GetNamedString(FindDataAnnotationAttribute(attributes, "DisplayAttribute"), "Name");
×
288
        var dataFormatString = GetNamedString(FindDataAnnotationAttribute(attributes, "DisplayFormatAttribute"), "DataFormatString");
×
289
        var columnType = GetNamedString(FindSchemaAttribute(attributes, "ColumnAttribute"), "TypeName");
×
290
        var columnOrder = GetNamedNumber(FindSchemaAttribute(attributes, "ColumnAttribute"), "Order");
×
291
        var jsonOptionsProviderName = GetJsonOptionsProviderName(jsonColumn);
×
292
        var jsonContextName = GetJsonContextName(jsonColumn);
×
293
        var jsonTypeInfoPropertyName = GetJsonTypeInfoPropertyName(jsonColumn);
×
294

295
        return new EntityProperty
×
296
        {
×
297
            PropertyName = propertyName,
×
298
            ColumnName = columnName,
×
299
            PropertyType = propertyType,
×
300
            MemberTypeName = memberTypeName,
×
301
            ParameterName = parameterName,
×
302
            ConverterName = converterName,
×
303
            IsKey = isKey,
×
304
            IsNotMapped = isNotMapped,
×
305
            IsDatabaseGenerated = isDatabaseGenerated,
×
306
            IsConcurrencyCheck = isConcurrencyCheck,
×
307
            ForeignKey = foreignKey,
×
308
            IsRequired = isRequired,
×
309
            IsNullable = isNullable,
×
310
            HasGetter = hasGetter,
×
311
            HasSetter = hasSetter,
×
312
            DisplayName = displayName,
×
313
            DataFormatString = dataFormatString,
×
314
            ColumnType = columnType,
×
315
            ColumnOrder = columnOrder,
×
316
            IsJsonColumn = isJsonColumn,
×
317
            JsonOptionsProviderName = jsonOptionsProviderName,
×
318
            JsonContextName = jsonContextName,
×
319
            JsonTypeInfoPropertyName = jsonTypeInfoPropertyName,
×
320
            IsEnum = enumInfo.IsEnum,
×
321
            IsNullableEnum = enumInfo.IsNullableEnum,
×
322
            EnumUnderlyingType = enumInfo.UnderlyingType
×
323
        };
×
324
    }
325

326
    private static (bool IsEnum, bool IsNullableEnum, string? UnderlyingType) GetEnumInfo(ITypeSymbol type)
327
    {
328
        var isNullableEnum = false;
×
329
        var enumType = type;
×
330

331
        if (type is INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T } namedType)
×
332
        {
333
            isNullableEnum = true;
×
334
            enumType = namedType.TypeArguments[0];
×
335
        }
336

337
        if (enumType is not INamedTypeSymbol { TypeKind: TypeKind.Enum } namedEnum)
×
338
            return (false, false, null);
×
339

340
        return (true, isNullableEnum, namedEnum.EnumUnderlyingType?.ToDisplayString());
×
341
    }
342

343
    private static bool IsNullableType(ITypeSymbol type)
344
    {
345
        return type.NullableAnnotation == NullableAnnotation.Annotated
×
346
            || type is INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T };
×
347
    }
348

349
    private static AttributeData? GetJsonColumn(ImmutableArray<AttributeData> attributes)
350
    {
351
        return attributes.FirstOrDefault(a => a.AttributeClass is
×
352
        {
×
353
            Name: "JsonColumnAttribute",
×
354
            ContainingNamespace:
×
355
            {
×
356
                Name: "Attributes",
×
357
                ContainingNamespace.Name: "FluentCommand"
×
358
            }
×
359
        });
×
360
    }
361

362
    private static string? GetJsonOptionsProviderName(AttributeData? attribute)
363
    {
364
        if (attribute?.ConstructorArguments.Length == 1 && attribute.ConstructorArguments[0].Value is INamedTypeSymbol providerSymbol)
×
365
            return providerSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
×
366

367
        return null;
×
368
    }
369

370
    private static string? GetJsonContextName(AttributeData? attribute)
371
    {
372
        if (attribute?.ConstructorArguments.Length == 2 && attribute.ConstructorArguments[0].Value is INamedTypeSymbol contextSymbol)
×
373
            return contextSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
×
374

375
        return null;
×
376
    }
377

378
    private static string? GetJsonTypeInfoPropertyName(AttributeData? attribute)
379
    {
380
        if (attribute?.ConstructorArguments.Length == 2 && attribute.ConstructorArguments[1].Value is string propertyName)
×
381
            return propertyName;
×
382

383
        return null;
×
384
    }
385

386
    private static string? GetColumnName(ImmutableArray<AttributeData> attributes)
387
    {
388
        var columnAttribute = FindSchemaAttribute(attributes, "ColumnAttribute");
×
389

390
        if (columnAttribute == null)
×
391
            return null;
×
392

393
        // attribute constructor [Column("Name")]
394
        var converterType = columnAttribute.ConstructorArguments.FirstOrDefault();
×
395
        if (converterType.Value is string stringValue)
×
396
            return stringValue;
×
397

398
        return null;
×
399
    }
400

401

402
    private static string? GetConverterName(ImmutableArray<AttributeData> attributes)
403
    {
404
        var converter = attributes
×
405
            .FirstOrDefault(a => a.AttributeClass is
×
406
            {
×
407
                Name: "DataFieldConverterAttribute",
×
408
                ContainingNamespace.Name: "FluentCommand"
×
409
            });
×
410

411
        if (converter == null)
×
412
            return null;
×
413

414
        // attribute constructor
415
        var converterType = converter.ConstructorArguments.FirstOrDefault();
×
416
        if (converterType.Value is INamedTypeSymbol converterSymbol)
×
NEW
417
            return converterSymbol.ToDisplayString(FullyQualifiedNullableFormat);
×
418

419
        // generic attribute
420
        var attributeClass = converter.AttributeClass;
×
421
        if (attributeClass is { IsGenericType: true }
×
422
            && attributeClass.TypeArguments.Length == attributeClass.TypeParameters.Length
×
423
            && attributeClass.TypeArguments.Length == 1)
×
424
        {
425
            var typeArgument = attributeClass.TypeArguments[0];
×
426
            return typeArgument.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
×
427
        }
428

429
        return null;
×
430
    }
431

432
    private static AttributeData? FindDataAnnotationAttribute(ImmutableArray<AttributeData> attributes, string name)
433
    {
434
        return attributes.FirstOrDefault(a => a.AttributeClass is
×
435
        {
×
436
            ContainingNamespace:
×
437
            {
×
438
                Name: "DataAnnotations",
×
439
                ContainingNamespace:
×
440
                {
×
441
                    Name: "ComponentModel",
×
442
                    ContainingNamespace.Name: "System"
×
443
                }
×
444
            }
×
445
        } && a.AttributeClass.Name == name);
×
446
    }
447

448
    private static bool HasDataAnnotationAttribute(ImmutableArray<AttributeData> attributes, string name)
449
    {
450
        return FindDataAnnotationAttribute(attributes, name) != null;
×
451
    }
452

453
    private static AttributeData? FindSchemaAttribute(ImmutableArray<AttributeData> attributes, string name)
454
    {
455
        return attributes.FirstOrDefault(a =>
×
456
            a.AttributeClass is
×
457
            {
×
458
                ContainingNamespace:
×
459
                {
×
460
                    Name: "Schema",
×
461
                    ContainingNamespace:
×
462
                    {
×
463
                        Name: "DataAnnotations",
×
464
                        ContainingNamespace:
×
465
                        {
×
466
                            Name: "ComponentModel",
×
467
                            ContainingNamespace.Name: "System"
×
468
                        }
×
469
                    }
×
470
                }
×
471
            }
×
472
            && a.AttributeClass.Name == name
×
473
        );
×
474
    }
475

476
    private static string? GetSchemaAttributeConstructorStringArg(ImmutableArray<AttributeData> attributes, string name)
477
    {
478
        var attribute = FindSchemaAttribute(attributes, name);
×
479
        if (attribute?.ConstructorArguments.Length > 0 && attribute.ConstructorArguments[0].Value is string value)
×
480
            return value;
×
481

482
        return null;
×
483
    }
484

485
    private static bool GetIsDatabaseGenerated(ImmutableArray<AttributeData> attributes)
486
    {
487
        var attribute = FindSchemaAttribute(attributes, "DatabaseGeneratedAttribute");
×
488
        if (attribute == null)
×
489
            return false;
×
490

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

494
        return false;
×
495
    }
496

497
    private static string? GetNamedString(AttributeData? attribute, string argName)
498
    {
499
        if (attribute == null)
×
500
            return null;
×
501

502
        foreach (var namedArg in attribute.NamedArguments)
×
503
        {
504
            if (namedArg.Key == argName && namedArg.Value.Value is string value)
×
505
                return value;
×
506
        }
507

508
        return null;
×
509
    }
510

511
    private static int? GetNamedNumber(AttributeData? attribute, string argName)
512
    {
513
        if (attribute == null)
×
514
            return null;
×
515

516
        foreach (var namedArg in attribute.NamedArguments)
×
517
        {
518
            if (namedArg.Key == argName && namedArg.Value.Value is int value)
×
519
                return value;
×
520
        }
521

522
        return null;
×
523
    }
524

525
    private static bool IsIncluded(IPropertySymbol propertySymbol)
526
    {
527
        return !propertySymbol.IsIndexer
×
528
            && !propertySymbol.IsAbstract
×
529
            && propertySymbol.DeclaredAccessibility == Accessibility.Public;
×
530
    }
531

532
    private static bool IsSupportedType(ITypeSymbol type)
533
    {
534
        // handle nullable value types
535
        if (type is INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T } namedType)
×
536
            return IsSupportedType(namedType.TypeArguments[0]);
×
537

538
        // enums are stored as their underlying integer type
539
        if (type.TypeKind == TypeKind.Enum)
×
540
            return true;
×
541

542
        // primitives and string
543
        switch (type.SpecialType)
×
544
        {
545
            case SpecialType.System_Boolean:
546
            case SpecialType.System_Byte:
547
            case SpecialType.System_Char:
548
            case SpecialType.System_Decimal:
549
            case SpecialType.System_Double:
550
            case SpecialType.System_Single:
551
            case SpecialType.System_Int16:
552
            case SpecialType.System_Int32:
553
            case SpecialType.System_Int64:
554
            case SpecialType.System_String:
555
                return true;
×
556
        }
557

558
        // byte[]
559
        if (type is IArrayTypeSymbol { ElementType.SpecialType: SpecialType.System_Byte })
×
560
            return true;
×
561

562
        // well-known struct types and FluentCommand.ConcurrencyToken
563
        var fullName = type.ToDisplayString();
×
564
        return fullName is
×
565
            "System.DateTime" or
×
566
            "System.DateTimeOffset" or
×
567
            "System.Guid" or
×
568
            "System.TimeSpan" or
×
569
            "System.DateOnly" or
×
570
            "System.TimeOnly" or
×
571
            "FluentCommand.ConcurrencyToken";
×
572
    }
573

574
    private static bool IsNotMapped(ImmutableArray<AttributeData> attributes)
575
    {
576
        return attributes.Any(
×
577
            a => a.AttributeClass is
×
578
            {
×
579
                Name: "NotMappedAttribute",
×
580
                ContainingNamespace:
×
581
                {
×
582
                    Name: "Schema",
×
583
                    ContainingNamespace:
×
584
                    {
×
585
                        Name: "DataAnnotations",
×
586
                        ContainingNamespace:
×
587
                        {
×
588
                            Name: "ComponentModel",
×
589
                            ContainingNamespace.Name: "System"
×
590
                        }
×
591
                    }
×
592
                }
×
593
            });
×
594
    }
595

596
    private static bool HasIgnorePropertyAttribute(ImmutableArray<AttributeData> attributes)
597
    {
598
        return attributes.Any(a => a.AttributeClass is
×
599
        {
×
600
            Name: "IgnorePropertyAttribute",
×
601
            ContainingNamespace:
×
602
            {
×
603
                Name: "Attributes",
×
604
                ContainingNamespace.Name: "FluentCommand"
×
605
            }
×
606
        });
×
607
    }
608

609
    private static bool IsMappableProperty(IPropertySymbol propertySymbol, HashSet<string> classIgnored)
610
    {
611
        var attributes = propertySymbol.GetAttributes();
×
612
        if (classIgnored.Contains(propertySymbol.Name) || HasIgnorePropertyAttribute(attributes) || IsNotMapped(attributes))
×
613
            return false;
×
614

615
        return GetJsonColumn(attributes) != null || IsSupportedType(propertySymbol.Type);
×
616
    }
617

618
    private static HashSet<string> GetClassIgnoredProperties(ImmutableArray<AttributeData> attributes)
619
    {
620
        var ignored = new HashSet<string>(StringComparer.Ordinal);
×
621

622
        foreach (var attr in attributes)
×
623
        {
624
            if (attr.AttributeClass is not
×
625
                {
×
626
                    Name: "IgnorePropertyAttribute",
×
627
                    ContainingNamespace:
×
628
                    {
×
629
                        Name: "Attributes",
×
630
                        ContainingNamespace.Name: "FluentCommand"
×
631
                    }
×
632
                })
×
633
            {
634
                continue;
635
            }
636

637
            // constructor argument: [IgnoreProperty("Name")] or [IgnoreProperty(nameof(T.Name))]
638
            if (attr.ConstructorArguments.Length > 0 && attr.ConstructorArguments[0].Value is string ctorName)
×
639
            {
640
                ignored.Add(ctorName);
×
641
                continue;
×
642
            }
643

644
            // named argument: [IgnoreProperty(PropertyName = "Name")]
645
            foreach (var namedArg in attr.NamedArguments)
×
646
            {
647
                if (namedArg.Key == "PropertyName" && namedArg.Value.Value is string namedValue)
×
648
                    ignored.Add(namedValue);
×
649
            }
650
        }
651

652
        return ignored;
×
653
    }
654
}
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