src/Mapping/DataMapper.psm1

using namespace System.Collections
using namespace System.ComponentModel.DataAnnotations.Schema
using namespace System.Diagnostics.CodeAnalysis
using namespace System.Reflection

<#
.SYNOPSIS
    Maps data records to entity objects.
#>

class DataMapper {

    <#
    .SYNOPSIS
        The property maps, keyed by type.
    #>

    hidden static [hashtable] $PropertyMaps = @{}

    <#
    .SYNOPSIS
        Converts the specified data reader into an array of objects of the specified type.
    .PARAMETER Reader
        The data reader to be converted.
    .PARAMETER Type
        The type of objects to return.
    .OUTPUTS
        The array of objects corresponding to the specified data reader.
    #>

    [object[]] ConvertReader([System.Data.IDataReader] $Reader, [type] $Type) {
        $list = [ArrayList]::new()
        while ($Reader.Read()) { $list.Add($this.ConvertRecord($Reader, $Type)) }
        $Reader.Close()
        return $list.ToArray()
    }

    <#
    .SYNOPSIS
        Converts the specified data record to the specified type.
    .PARAMETER Record
        The data record to be converted.
    .PARAMETER Type
        The type of object to return.
    .OUTPUTS
        The object corresponding to the specified data record.
    #>

    [SuppressMessage("PSUseDeclaredVarsMoreThanAssignments", "")]
    [object] ConvertRecord([System.Data.IDataRecord] $Record, [type] $Type) {
        $properties = [ordered]@{}
        for ($index = 0; $index -lt $Record.FieldCount; $index++) {
            $key = $Record.GetName($index)
            $properties.$key = $Record.IsDBNull($index) ? $null : $Record.GetValue($index)
        }

        return $discard = switch ($Type) {
            ([hashtable]) { [hashtable] $properties; break }
            ([ordered]) { $properties; break }
            ([psobject]) { [pscustomobject] $properties; break }
            default { $this.CreateInstance($Type, $properties) }
        }
    }

    <#
    .SYNOPSIS
        Creates a new entity of the specified type using that type's parameterless constructor.
    .PARAMETER Type
        The entity type.
    .PARAMETER Properties
        The properties to be set on the newly created object.
    .OUTPUTS
        The newly created object.
    #>

    [object] CreateInstance([type] $Type, [hashtable] $Properties) {
        $culture = [cultureinfo]::InvariantCulture
        $object = $Type::new()
        $propertyMap = $this.GetPropertyMap($Type)

        foreach ($key in $Properties.Keys.Where{ $_ -in $propertyMap.Keys }) {
            $propertyInfo = $propertyMap.$key
            if ($propertyInfo.CanWrite -and (-not [Attribute]::IsDefined($propertyInfo, ([NotMappedAttribute])))) {
                $object.$($propertyInfo.Name) = [Convert]::ChangeType($Properties.$key, $propertyInfo.PropertyType, $culture)
            }
        }

        return $object
    }

    <#
    .SYNOPSIS
        Gets the property map associated with the specified entity type.
    .PARAMETER Type
        The entity type.
    .OUTPUTS
        The property map associated with the specified entity type.
    #>

    [hashtable] GetPropertyMap([type] $Type) {
        if ($Type -in [DataMapper]::PropertyMaps.Keys) { return [DataMapper]::PropertyMaps.$Type }

        $propertyMap = @{}
        $propertyInfos = $Type.GetProperties([BindingFlags]::Instance -bor [BindingFlags]::Public)
        foreach ($propertyInfo in $propertyInfos) {
            $column = [Attribute]::GetCustomAttribute($propertyInfo, ([ColumnAttribute]))
            $key = $column ? $column.Name : $propertyInfo.Name
            $propertyMap.$key = $propertyInfo
        }

        return [DataMapper]::PropertyMaps.$Type = $propertyMap
    }
}