Functions/Get-DacPacHash.ps1

<#
.SYNOPSIS
    Calculates a hash for a DACPAC file and its dependencies.
 
.DESCRIPTION
    The Get-DacPacHash function generates a SHA256 hash for a DACPAC file by analyzing its model.xml content,
    referenced DACPACs, and pre/post deployment scripts. This hash can be used to determine if the DACPAC
    has changed since a previous deployment.
 
.PARAMETER dacpacPath
    Specifies the path to the DACPAC file. Can be a relative path if rootPath is provided, or an absolute path.
 
.PARAMETER rootPath
    Optional. Specifies the root path to use when dacpacPath is relative. If not provided, the function will
    use the directory containing the DACPAC file as the root path.
 
.OUTPUTS
    String
    Returns a SHA256 hash string representing the DACPAC content and its dependencies.
 
.EXAMPLE
    Get-DacPacHash -dacpacPath "C:\MyProject\bin\MyDatabase.dacpac"
     
    Calculates the hash for the specified DACPAC file.
 
.EXAMPLE
    Get-DacPacHash -dacpacPath "MyDatabase.dacpac" -rootPath "C:\MyProject\bin"
     
    Calculates the hash using a relative path and specified root directory.
 
.NOTES
    This function will recursively calculate hashes for any referenced DACPACs.
    Pre-deployment and post-deployment scripts are included in the hash calculation.
#>

function Get-DacPacHash {
    [CmdletBinding()]
    [OutputType([String])]
    param (
        [Parameter(Mandatory = $true, HelpMessage = "Path to the DACPAC file")]
        [string]$dacpacPath,
        
        [Parameter(HelpMessage = "Root path for relative dacpacPath resolution")]
        $rootPath
    )
    [xml]$dacpacXml = New-Object xml
    $dacPacZipModelStream = $null
    $IsRootDacPac = $null -eq $rootPath
    try {
        if ($null -eq $rootpath){
            $dacpacitem = (Get-Item $dacpacPath)
            $FulldacPacPath = $dacpacitem.FullName
            $rootpath = $dacpacitem.Directory
        }
        else{
            $FulldacPacPath = Join-Path $rootPath $dacpacPath
        }
        Write-Verbose "getting DacPac hash for $FulldacPacPath"
        $Zip = [io.compression.zipfile]::OpenRead($FulldacPacPath)
    }
    catch [System.IO.FileNotFoundException], [System.Management.Automation.ItemNotFoundException] {
        throw "Can't open dacpac file $dacpacPath doesn't exist"
    }
    catch {
        $Ex = New-Object System.Exception ("Error reading dacpac $dacpacPath probably not a valid dacpac", $_.Exception)
        throw $ex
    }
    try {
        if (-not ($Zip.Entries.Name -eq "model.xml")) {
            Throw "Can't find the model.xml file in the dacpac, would guess this isn't a dacpac"
        }
        $dacPacZipModelStream = $Zip.GetEntry("model.xml").Open()
        $dacpacXml.Load($dacPacZipModelStream)        
        $checksum = Get-ModelChecksum $dacpacXml;

        foreach ($dacpac in (Get-ReferencedDacpacsFromModel -modelxml $dacpacXml)) {
            $checksum += Get-DacPacHash -dacpacPath $dacpac -rootPath $rootPath
        }

        if ($IsRootDacPac) {
            $Zip.Entries | Where-Object { $_.Name -in ("predeploy.sql", "postdeploy.sql")} | ForEach-Object {
                
                $stream = $Zip.GetEntry($_.Name).open()
                $checksum += (Get-FileHash -InputStream $stream -Algorithm SHA256).Hash;
                $stream.Close();
                $stream.Dispose();             
            }
        }
    }
    catch { Throw }
    finally {
        if ($null -ne $dacPacZipModelStream) {
            $dacPacZipModelStream.Close()
            $dacPacZipModelStream.Dispose()
        }
        if ($null -ne $Zip) { $Zip.Dispose() }
    }
    return $checksum
}