| | | 1 | | using System.Collections.Concurrent; |
| | | 2 | | using System.Linq.Expressions; |
| | | 3 | | using System.Reflection; |
| | | 4 | | using ArturRios.Data.Export.Attributes; |
| | | 5 | | |
| | | 6 | | namespace ArturRios.Data.Export.Abstractions; |
| | | 7 | | |
| | | 8 | | /// <summary>A single export column: a header and a compiled getter over the record.</summary> |
| | 11 | 9 | | public sealed class Column(string header, Func<object, object?> getter) |
| | | 10 | | { |
| | | 11 | | /// <summary>The column header.</summary> |
| | 45 | 12 | | public string Header { get; } = header; |
| | | 13 | | |
| | | 14 | | /// <summary>Reads the column value from a record instance.</summary> |
| | 33 | 15 | | public Func<object, object?> Getter { get; } = getter; |
| | | 16 | | } |
| | | 17 | | |
| | | 18 | | /// <summary>Builds and caches the ordered column plan for a record type.</summary> |
| | | 19 | | public 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 | | } |