zachtbeer.SqlSchemaHasher 1.0.0

dotnet add package zachtbeer.SqlSchemaHasher --version 1.0.0
                    
NuGet\Install-Package zachtbeer.SqlSchemaHasher -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="zachtbeer.SqlSchemaHasher" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="zachtbeer.SqlSchemaHasher" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="zachtbeer.SqlSchemaHasher" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add zachtbeer.SqlSchemaHasher --version 1.0.0
                    
#r "nuget: zachtbeer.SqlSchemaHasher, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package zachtbeer.SqlSchemaHasher@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=zachtbeer.SqlSchemaHasher&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=zachtbeer.SqlSchemaHasher&version=1.0.0
                    
Install as a Cake Tool

zachtbeer.SqlSchemaHasher

CI NuGet NuGet Downloads License: MIT

A lightweight .NET library for computing deterministic SHA256 hashes of SQL Server database schemas. Use it to detect schema changes, group databases by schema signature, or verify schema consistency across environments.

Why Schema Hashing?

The Problem

When you manage many SQL Server databases, schemas drift. Someone modifies a table directly. A backup gets restored from the wrong date. A migration partially applies and nobody notices. Without a fast way to answer "what schema is this database actually running?", drift is invisible until something breaks.

Why Hashing Instead of Version Numbers

Version numbers require discipline: someone has to bump them, and they have to be applied everywhere consistently. More importantly, version numbers tell you what should be deployed, not what actually is deployed. They can lie. Migration journals track what scripts ran, not what the schema looks like after manual changes.

A content-based hash is computed from the schema itself -- it's ground truth. Two databases with the same hash are structurally identical, period. No trust required, no process dependency, no "well it should be on v4.2."

Hashes also surface unexpected groupings. You might discover that 300 databases are on schema A, 50 are on schema B, and 3 are on something nobody recognizes. That visibility alone saves you from a class of problems that have historically been expensive to diagnose.

How You Might Use It

  • Define the golden schema -- Hash a reference database and compare everything against it. Any mismatch is immediately actionable.
  • Continuous drift monitoring -- Compute schema hashes on a schedule and report them centrally. Deviations surface in dashboards instead of in support tickets.
  • Migration tooling -- Compare a database's hash against the target schema before generating diff SQL. If hashes match, skip the diff entirely.
  • Fleet-wide grouping -- Group all databases by hash to get the full picture of what's actually deployed.

Installation

dotnet add package zachtbeer.SqlSchemaHasher

Quick Start

using zachtbeer.SqlSchemaHasher;

// Simple one-liner to get a schema hash
var hash = await SqlSchemaHash.GetHashAsync("Server=localhost;Database=MyDb;Trusted_Connection=true");
// Returns: "dGhpcyBpcyBhIGJhc2U2NCBoYXNo..."

API Reference

SqlSchemaHash.GetHashAsync(connectionString)

Returns a base64-encoded SHA256 hash of the database schema.

var hash = await SqlSchemaHash.GetHashAsync(connectionString);

SqlSchemaHash.GetHashAsync(connectionString, options)

Returns a hash with custom normalization options.

var options = new SchemaHashOptions
{
    NormalizeAutoGeneratedIndexNames = true,  // Strip GUID suffixes from index names
    NormalizeClusteringType = true            // Treat clustered/nonclustered as equivalent
};

var hash = await SqlSchemaHash.GetHashAsync(connectionString, options);

SqlSchemaHash.ExtractSchemaAsync(connectionString)

Returns detailed schema metadata if you need to inspect the schema structure.

var schema = await SqlSchemaHash.ExtractSchemaAsync(connectionString);

Console.WriteLine($"Tables: {schema.Tables.Count}");
Console.WriteLine($"Stored Procedures: {schema.StoredProcedures.Count}");
Console.WriteLine($"User-Defined Types: {schema.UserDefinedTableTypes.Count}");

SqlSchemaHash.ComputeHash(schema, options)

Computes a hash from pre-extracted schema metadata. Useful when comparing the same schema with different normalization options.

var schema = await SqlSchemaHash.ExtractSchemaAsync(connectionString);

var strictHash = SqlSchemaHash.ComputeHash(schema);
var normalizedHash = SqlSchemaHash.ComputeHash(schema, new SchemaHashOptions
{
    NormalizeAutoGeneratedIndexNames = true
});

What Gets Hashed

The hash includes:

  • Tables: Schema name, table name, columns (name, type, precision, nullability), indexes, constraints, identity columns
  • Stored Procedures: Schema name, procedure name, parameters, and a hash of the procedure body (detects logic changes)
  • User-Defined Table Types: Schema name, type name, columns

Excluded Objects

The following objects are automatically excluded from hashing (via SchemaHashOptions.Default):

  • sysdiagrams table and related diagram helper procedures (fn_diagramobjects, sp_alterdiagram, sp_creatediagram, sp_dropdiagram, sp_helpdiagramdefinition, sp_helpdiagrams, sp_renamediagram)

Options

Option Default Description
SchemaFilter null Filter to specific schema (e.g., "dbo", "sales"). When null, all schemas are included.
NormalizeAutoGeneratedIndexNames false Strips GUID suffixes from auto-generated index names (e.g., nci_wi_Table_ABC123nci_wi_Table)
NormalizeClusteringType false Normalizes clustered/nonclustered to a common value
IncludeStoredProcedureText true When false, only procedure name and parameters are hashed (body changes ignored)

Use Cases

Hash Only Specific Schema

// Only hash objects in the dbo schema, ignoring other schemas like reporting or staging
var options = new SchemaHashOptions { SchemaFilter = "dbo" };
var hash = await SqlSchemaHash.GetHashAsync(connectionString, options);

Schema Change Detection

var beforeHash = await SqlSchemaHash.GetHashAsync(connectionString);
// ... apply migration ...
var afterHash = await SqlSchemaHash.GetHashAsync(connectionString);

if (beforeHash != afterHash)
{
    Console.WriteLine("Schema changed!");
}

Group Databases by Schema

var databases = new[] { "Db1", "Db2", "Db3", "Db4" };
var groups = new Dictionary<string, List<string>>();

foreach (var db in databases)
{
    var connStr = $"Server=localhost;Database={db};Trusted_Connection=true";
    var hash = await SqlSchemaHash.GetHashAsync(connStr);

    if (!groups.ContainsKey(hash))
        groups[hash] = new List<string>();
    groups[hash].Add(db);
}

// Databases with identical schemas are grouped together

Environment Verification

var prodHash = await SqlSchemaHash.GetHashAsync(prodConnectionString);
var stagingHash = await SqlSchemaHash.GetHashAsync(stagingConnectionString);

if (prodHash != stagingHash)
{
    throw new Exception("Staging schema does not match production!");
}

Dependencies

  • Microsoft.Data.SqlClient - SQL Server connectivity
  • Dapper - Efficient database queries

License

MIT

Product Compatible and additional computed target framework versions.
.NET net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 is compatible.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 1,396 2/6/2026
0.0.1 136 2/6/2026