< Summary

Information
Class: ArturRios.Data.Export.Abstractions.Column
Assembly: ArturRios.Data.Export
File(s): D:\Repositories\dotnet-data\src\ArturRios.Data.Export\Abstractions\ColumnMap.cs
Line coverage
100%
Covered lines: 3
Uncovered lines: 0
Coverable lines: 3
Total lines: 63
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage
100%
Covered methods: 3
Fully covered methods: 3
Total methods: 3
Method coverage: 100%
Full method coverage: 100%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_Header()100%11100%
get_Getter()100%11100%

File(s)

D:\Repositories\dotnet-data\src\ArturRios.Data.Export\Abstractions\ColumnMap.cs

#LineLine coverage
 1using System.Collections.Concurrent;
 2using System.Linq.Expressions;
 3using System.Reflection;
 4using ArturRios.Data.Export.Attributes;
 5
 6namespace ArturRios.Data.Export.Abstractions;
 7
 8/// <summary>A single export column: a header and a compiled getter over the record.</summary>
 119public sealed class Column(string header, Func<object, object?> getter)
 10{
 11    /// <summary>The column header.</summary>
 4512    public string Header { get; } = header;
 13
 14    /// <summary>Reads the column value from a record instance.</summary>
 3315    public Func<object, object?> Getter { get; } = getter;
 16}
 17
 18/// <summary>Builds and caches the ordered column plan for a record type.</summary>
 19public static class ColumnMap
 20{
 21    private static readonly ConcurrentDictionary<Type, IReadOnlyList<Column>> Cache = new();
 22
 23    /// <summary>Returns the cached column plan for <typeparamref name="T" />.</summary>
 24    public static IReadOnlyList<Column> For<T>() => For(typeof(T));
 25
 26    /// <summary>Returns the cached column plan for <paramref name="type" />.</summary>
 27    public static IReadOnlyList<Column> For(Type type) => Cache.GetOrAdd(type, Build);
 28
 29    private static IReadOnlyList<Column> Build(Type type)
 30    {
 31        var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
 32            .Where(p => p.CanRead && p.GetIndexParameters().Length == 0
 33                        && p.GetCustomAttribute<ExportIgnoreAttribute>() is null);
 34
 35        var planned = properties.Select(p =>
 36        {
 37            var attribute = p.GetCustomAttribute<ExportColumnAttribute>();
 38            return new
 39            {
 40                Header = attribute?.Name ?? p.Name,
 41                Order = attribute?.Order ?? int.MaxValue,
 42                Token = p.MetadataToken,
 43                Getter = BuildGetter(p)
 44            };
 45        });
 46
 47        return planned
 48            .OrderBy(x => x.Order).ThenBy(x => x.Token).ThenBy(x => x.Header, StringComparer.Ordinal)
 49            .Select(x => new Column(x.Header, x.Getter))
 50            .ToArray();
 51    }
 52
 53    // Compiles a delegate getter once per property (cached with the column plan) to avoid per-row reflection.
 54    private static Func<object, object?> BuildGetter(PropertyInfo property)
 55    {
 56        var instance = Expression.Parameter(typeof(object), "instance");
 57        var typed = Expression.Convert(instance, property.DeclaringType!);
 58        var access = Expression.Property(typed, property);
 59        var boxed = Expression.Convert(access, typeof(object));
 60
 61        return Expression.Lambda<Func<object, object?>>(boxed, instance).Compile();
 62    }
 63}