src/Client/Get-XrmRecord.ps1

<#
    .SYNOPSIS
    Search for record with simple query.

    .Description
    Get specific row (Entity record) according to given id, key, attribute, or set of attributes.
    With AttributeName or Attributes, the first matching row is returned ($null when none matches); Unique raises an error when several rows match.

    .PARAMETER XrmClient
    Xrm connector initialized to target instance. Use latest one by default. (Dataverse ServiceClient)

    .PARAMETER LogicalName
    Table / Entity logical name.

    .PARAMETER Key
    Specify alternate key attribute name to search.

    .PARAMETER AttributeName
    Specify attribute name to search.

    .PARAMETER Value
    Specify key or attribute value to search.
    Use Id to specify row (entity record) unique identifier

    .PARAMETER Columns
    Specify row (entity record) columns to return. (array)

    .PARAMETER Attributes
    Hashtable of column = value conditions, all required (AND). A $null value matches an empty column; an EntityReference, OptionSetValue or Money value is compared on its id or value.

    .PARAMETER IfExists
    Return $null when the row does not exist (search by id or key), instead of raising an error.

    .PARAMETER Unique
    With AttributeName or Attributes: raise an error when more than one row matches, instead of returning the first one.

    .PARAMETER AsEntity
    Return the SDK Entity instead of a converted custom object.

    .OUTPUTS
    Custom Object. Row (= Entity record) is converted to custom object to simplify data operations. With AsEntity: Microsoft.Xrm.Sdk.Entity.

    .EXAMPLE
    $xrmClient = New-XrmClient -ConnectionString $connectionString;
    $contosoAccount = Get-XrmRecord -XrmClient $xrmClient -LogicalName "account" -AttributeName "name" -Value "Contoso" -Columns "revenue";
    Write-Host $contosoAccount.revenue;

    .EXAMPLE
    $contact = Get-XrmRecord -XrmClient $xrmClient -LogicalName "contact" -Attributes @{ firstname = "John"; lastname = "Doe"; parentcustomerid = $accountRef } -Unique;

    .EXAMPLE
    $account = Get-XrmRecord -XrmClient $xrmClient -LogicalName "account" -Id $accountId -Columns "name" -IfExists;
    if (-not $account) { Write-Host "Deleted meanwhile"; }

    .LINK
    Samples: https://github.com/AymericM78/PowerDataOps/blob/main/documentation/samples/Working%20with%20data.md
#>

function Get-XrmRecord {
    [CmdletBinding(DefaultParameterSetName = "Value")]
    [OutputType([PSCustomObject], [Microsoft.Xrm.Sdk.Entity])]
    param
    (
        [Parameter(Mandatory = $false, ValueFromPipeline, Position = 0)]
        [Microsoft.PowerPlatform.Dataverse.Client.ServiceClient]
        $XrmClient = $Global:XrmClient,

        [Parameter(Mandatory = $true, Position = 1)]
        [ValidateNotNullOrEmpty()]
        [String]
        $LogicalName,

        [Parameter(Mandatory = $false, ParameterSetName = "Value", Position = 2)]
        [ValidateNotNullOrEmpty()]
        [String]
        $Key,

        [Parameter(Mandatory = $false, ParameterSetName = "Value", Position = 3)]
        [ValidateNotNullOrEmpty()]
        [String]
        $AttributeName,

        [Parameter(Mandatory = $true, ParameterSetName = "Value", Position = 4)]
        [ValidateNotNullOrEmpty()]
        [Alias("Id")]
        [Object]
        $Value,

        [Parameter(Mandatory = $false, Position = 5)]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $Columns,

        [Parameter(Mandatory = $true, ParameterSetName = "Attributes")]
        [ValidateNotNullOrEmpty()]
        [Hashtable]
        $Attributes,

        [Parameter(Mandatory = $false)]
        [switch]
        $IfExists,

        [Parameter(Mandatory = $false)]
        [switch]
        $Unique,

        [Parameter(Mandatory = $false)]
        [switch]
        $AsEntity
    )
    begin {
        $StopWatch = [System.Diagnostics.Stopwatch]::StartNew();
        Trace-XrmFunction -Name $MyInvocation.MyCommand.Name -Stage Start -Parameters ($MyInvocation.MyCommand.Parameters);
    }
    process {

        $columnSet = New-Object -TypeName Microsoft.Xrm.Sdk.Query.ColumnSet;
        if ($PSBoundParameters.Columns) {
            if ($Columns.Contains("*")) {
                $columnSet.AllColumns = $true;
            }
            else {
                $Columns | ForEach-Object {
                    $columnSet.AddColumn($_);
                }
            }
        }

        $record = $null;
        if ($PSCmdlet.ParameterSetName -eq "Attributes" -or $PSBoundParameters.AttributeName) {
            $conditions = if ($PSCmdlet.ParameterSetName -eq "Attributes") { $Attributes } else { @{ $AttributeName = $Value } };

            $query = New-XrmQueryExpression -LogicalName $LogicalName -TopCount $(if ($Unique) { 2 } else { 1 });
            $query.ColumnSet = $columnSet;
            foreach ($field in $conditions.Keys) {
                $conditionValue = $conditions[$field];
                if ($conditionValue -is [Microsoft.Xrm.Sdk.EntityReference]) { $conditionValue = $conditionValue.Id; }
                elseif ($conditionValue -is [Microsoft.Xrm.Sdk.OptionSetValue]) { $conditionValue = $conditionValue.Value; }
                elseif ($conditionValue -is [Microsoft.Xrm.Sdk.Money]) { $conditionValue = $conditionValue.Value; }

                if ($null -eq $conditionValue) {
                    $query | Add-XrmQueryCondition -Field $field -Condition Null | Out-Null;
                }
                else {
                    $query | Add-XrmQueryCondition -Field $field -Condition Equal -Values $conditionValue | Out-Null;
                }
            }
            $rows = $XrmClient | Get-XrmMultipleRecords -Query $query -AsEntity -AsArray;

            if ($Unique -and $rows.Count -gt 1) {
                $criteria = ($conditions.Keys | ForEach-Object { "$_ = '$($conditions[$_])'" }) -join ", ";
                throw "More than one '$LogicalName' row matches $criteria.";
            }
            $record = $rows | Select-Object -First 1;
        }
        elseif ($PSBoundParameters.Key) {
            $request = New-Object -TypeName Microsoft.Xrm.Sdk.Messages.RetrieveRequest;
            $request.Target = New-XrmEntityReference -LogicalName $LogicalName -Key $Key -Value $Value;
            $request.ColumnSet = $columnSet;
            if ($IfExists) {
                try {
                    $response = $XrmClient | Invoke-XrmRequest -Request $request;
                }
                catch {
                    if (Test-XrmNotFoundError -ErrorRecord $_) {
                        return $null;
                    }
                    throw;
                }
            }
            else {
                $response = $XrmClient | Invoke-XrmRequest -Request $request;
            }
            $record = $response.Entity;
        }
        else {
            if ($IfExists) {
                try {
                    $record = Protect-XrmCommand -ScriptBlock { $XrmClient.Retrieve($LogicalName, $Value, $columnSet) };
                }
                catch {
                    if (Test-XrmNotFoundError -ErrorRecord $_) {
                        return $null;
                    }
                    throw;
                }
            }
            else {
                $record = Protect-XrmCommand -ScriptBlock { $XrmClient.Retrieve($LogicalName, $Value, $columnSet) };
            }
        }

        if ($null -eq $record) {
            return $null;
        }
        if ($AsEntity) {
            return $record;
        }
        $record | ConvertTo-XrmObject;
    }
    end {
        $StopWatch.Stop();
        Trace-XrmFunction -Name $MyInvocation.MyCommand.Name -Stage Stop -StopWatch $StopWatch;
    }
}

Export-ModuleMember -Function Get-XrmRecord -Alias *;

Register-ArgumentCompleter -CommandName Get-XrmRecord -ParameterName "LogicalName" -ScriptBlock {

    param($CommandName, $ParameterName, $WordToComplete, $CommandAst, $FakeBoundParameters)

    $validLogicalNames = Get-XrmEntitiesLogicalName;
    return $validLogicalNames | Where-Object { $_ -like "$wordToComplete*" };
}

Register-ArgumentCompleter -CommandName Get-XrmRecord -ParameterName "AttributeName" -ScriptBlock {

    param($CommandName, $ParameterName, $WordToComplete, $CommandAst, $FakeBoundParameters)

    if (-not ($fakeBoundParameters.ContainsKey("LogicalName"))) {
        return @();
    }

    $validAttributeNames = Get-XrmAttributesLogicalName -EntityLogicalName $fakeBoundParameters.LogicalName;
    return $validAttributeNames | Where-Object { $_ -like "$wordToComplete*" };
}

Register-ArgumentCompleter -CommandName Get-XrmRecord -ParameterName "Columns" -ScriptBlock {

    param($CommandName, $ParameterName, $WordToComplete, $CommandAst, $FakeBoundParameters)

    if (-not ($fakeBoundParameters.ContainsKey("LogicalName"))) {
        return @();
    }

    $validAttributeNames = Get-XrmAttributesLogicalName -EntityLogicalName $fakeBoundParameters.LogicalName;
    return $validAttributeNames | Where-Object { $_ -like "$wordToComplete*" };
}