Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,16 @@ The package orchestrates a MSBuild pipeline with these stages:

### Core Capabilities

- **🔄 Incremental Builds** - Only regenerates when database schema or configuration changes
- **🔄 Incremental Builds** - Smart fingerprinting detects when regeneration is needed based on:
- Library or tool version changes
- Database schema modifications
- Configuration file changes
- MSBuild property overrides (`EfcptConfig*`)
- Template file changes
- Generated file changes (optional)
- **🎨 T4 Template Support** - Customize code generation with your own templates
- **📁 Smart File Organization** - Schema-based folders and namespaces
- **🔧 Highly Configurable** - Override namespaces, output paths, and generation options
- **🔧 Highly Configurable** - Override namespaces, output paths, and generation options via MSBuild properties
- **🌐 Multi-Schema Support** - Generate models across multiple database schemas
- **📦 NuGet Ready** - Enterprise-ready package for production use

Expand Down
7 changes: 6 additions & 1 deletion docs/user-guide/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,13 +135,17 @@ Computes a composite fingerprint to detect when regeneration is needed.
| `RenamingPath` | Yes | Path to renaming file |
| `TemplateDir` | Yes | Path to templates |
| `FingerprintFile` | Yes | Path to fingerprint cache file |
| `ToolVersion` | No | EF Core Power Tools CLI version |
| `GeneratedDir` | No | Directory containing generated files |
| `DetectGeneratedFileChanges` | No | Whether to detect changes to generated files (default: false) |
| `ConfigPropertyOverrides` | No | JSON string of MSBuild property overrides |
| `LogVerbosity` | No | Logging level |

**Outputs:**

| Output | Description |
|--------|-------------|
| `Fingerprint` | Computed XxHash64 hash |
| `Fingerprint` | Computed XxHash64 hash including library version, tool version, schema, config, overrides, templates, and optionally generated files |
| `HasChanged` | Whether fingerprint changed |

### RunEfcpt
Expand Down Expand Up @@ -315,6 +319,7 @@ Applies MSBuild property overrides to the staged `efcpt-config.json` file. This
| `EfcptDumpResolvedInputs` | `false` | Write resolved inputs to JSON |
| `EfcptFingerprintFile` | `$(EfcptOutput)fingerprint.txt` | Fingerprint cache location |
| `EfcptStampFile` | `$(EfcptOutput).efcpt.stamp` | Generation stamp file |
| `EfcptDetectGeneratedFileChanges` | `false` | Detect changes to generated `.g.cs` files and trigger regeneration. **Warning**: When enabled, manual edits to generated files will be overwritten. |

### Config Override Properties

Expand Down
48 changes: 43 additions & 5 deletions docs/user-guide/core-concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,36 +134,74 @@ Fingerprinting is a key optimization that prevents unnecessary code regeneration

### What's Included in the Fingerprint

The fingerprint includes multiple sources to ensure regeneration when any relevant input changes:

- **Library version** - Version of JD.Efcpt.Build.Tasks assembly
- **Tool version** - EF Core Power Tools CLI version (`EfcptToolVersion`)
- **DACPAC content** (in .sqlproj mode) or **schema metadata** (in connection string mode)
- **efcpt-config.json** - Generation options, namespaces, table selection (including MSBuild overrides)
- **efcpt-config.json** - Generation options, namespaces, table selection
- **MSBuild property overrides** - All `EfcptConfig*` properties set in the .csproj
- **efcpt.renaming.json** - Custom naming rules
- **T4 templates** - All template files and their contents
- **Generated files** (optional) - When `EfcptDetectGeneratedFileChanges=true`, includes fingerprints of generated `.g.cs` files

Note: The fingerprint is computed after MSBuild property overrides are applied, so changing an override property (like `EfcptConfigRootNamespace`) will trigger regeneration.
**Important**: The fingerprint is computed after MSBuild property overrides are applied, so changing any `EfcptConfig*` property (like `EfcptConfigRootNamespace`) will automatically trigger regeneration.

All hashing uses XxHash64, a fast non-cryptographic hash algorithm.

### How Fingerprinting Works

```
Build 1 (first run):
Fingerprint = Hash(DACPAC/Schema + config + renaming + templates)
Fingerprint = Hash(library + tool + DACPAC/Schema + config + overrides + renaming + templates)
→ No previous fingerprint exists
→ Generate models
→ Store fingerprint

Build 2 (no changes):
Fingerprint = Hash(DACPAC/Schema + config + renaming + templates)
Fingerprint = Hash(library + tool + DACPAC/Schema + config + overrides + renaming + templates)
→ Same as stored fingerprint
→ Skip generation (fast build)

Build 3 (schema changed):
Fingerprint = Hash(new DACPAC/Schema + config + renaming + templates)
Fingerprint = Hash(library + tool + new DACPAC/Schema + config + overrides + renaming + templates)
→ Different from stored fingerprint
→ Regenerate models
→ Store new fingerprint

Build 4 (config property changed):
Fingerprint = Hash(library + tool + DACPAC/Schema + config + new overrides + renaming + templates)
→ Different from stored fingerprint (overrides changed)
→ Regenerate models
→ Store new fingerprint
```

### Regeneration Triggers

The following changes will automatically trigger model regeneration:

1. **Library upgrade** - When you update the JD.Efcpt.Build NuGet package
2. **Tool version change** - When you change `<EfcptToolVersion>` in your .csproj
3. **Database schema change** - Tables, columns, or relationships modified
4. **Config file change** - efcpt-config.json or efcpt.renaming.json modified
5. **MSBuild property change** - Any `<EfcptConfig*>` property changed in .csproj
6. **Template change** - T4 template files added, removed, or modified
7. **Generated file change** (optional) - When `<EfcptDetectGeneratedFileChanges>true</EfcptDetectGeneratedFileChanges>` is set

### Detecting Manual Edits (Optional)

By default, the system **does not** detect changes to generated files. This prevents accidentally overwriting manual edits you might make to generated code.

To enable detection of changes to generated files (useful in some workflows):

```xml
<PropertyGroup>
<EfcptDetectGeneratedFileChanges>true</EfcptDetectGeneratedFileChanges>
</PropertyGroup>
```

**Warning**: When enabled, any manual edits to `.g.cs` files will trigger regeneration, overwriting your changes. Only enable this if your workflow never involves manual edits to generated code.

### Forcing Regeneration

To force regeneration regardless of fingerprint:
Expand Down
93 changes: 89 additions & 4 deletions src/JD.Efcpt.Build.Tasks/ComputeFingerprint.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Reflection;
using System.Text;
using JD.Efcpt.Build.Tasks.Decorators;
using JD.Efcpt.Build.Tasks.Extensions;
Expand All @@ -11,10 +12,19 @@ namespace JD.Efcpt.Build.Tasks;
/// </summary>
/// <remarks>
/// <para>
/// The fingerprint is derived from the contents of the DACPAC, configuration JSON, renaming JSON, and
/// every file under the template directory. For each input, an XxHash64 hash is computed and written into
/// an internal manifest string, which is itself hashed using XxHash64 to produce the final
/// <see cref="Fingerprint"/>.
/// The fingerprint is derived from multiple sources to ensure regeneration when any relevant input changes:
/// <list type="bullet">
/// <item><description>Library version (JD.Efcpt.Build.Tasks assembly)</description></item>
/// <item><description>Tool version (EF Core Power Tools CLI version)</description></item>
/// <item><description>Database schema (DACPAC or connection string schema fingerprint)</description></item>
/// <item><description>Configuration JSON file contents</description></item>
/// <item><description>Renaming JSON file contents</description></item>
/// <item><description>MSBuild config property overrides (EfcptConfig* properties)</description></item>
/// <item><description>All template files under the template directory</description></item>
/// <item><description>Generated files (optional, via <c>EfcptDetectGeneratedFileChanges</c>)</description></item>
/// </list>
/// For each input, an XxHash64 hash is computed and written into an internal manifest string,
/// which is itself hashed using XxHash64 to produce the final <see cref="Fingerprint"/>.
/// </para>
/// <para>
/// The computed fingerprint is compared to the existing value stored in <see cref="FingerprintFile"/>.
Expand Down Expand Up @@ -70,6 +80,26 @@ public sealed class ComputeFingerprint : Task
/// </summary>
public string LogVerbosity { get; set; } = "minimal";

/// <summary>
/// Version of the EF Core Power Tools CLI tool package being used.
/// </summary>
public string ToolVersion { get; set; } = "";

/// <summary>
/// Directory containing generated files to optionally include in the fingerprint.
/// </summary>
public string GeneratedDir { get; set; } = "";

/// <summary>
/// Indicates whether to detect changes to generated files (default: false to avoid overwriting manual edits).
/// </summary>
public string DetectGeneratedFileChanges { get; set; } = "false";

/// <summary>
/// Serialized JSON string containing MSBuild config property overrides.
/// </summary>
public string ConfigPropertyOverrides { get; set; } = "";

/// <summary>
/// Newly computed fingerprint value for the current inputs.
/// </summary>
Expand Down Expand Up @@ -99,6 +129,21 @@ private bool ExecuteCore(TaskExecutionContext ctx)
var log = new BuildLog(ctx.Logger, LogVerbosity);
var manifest = new StringBuilder();

// Library version (JD.Efcpt.Build.Tasks assembly)
var libraryVersion = GetLibraryVersion();
if (!string.IsNullOrWhiteSpace(libraryVersion))
{
manifest.Append("library\0").Append(libraryVersion).Append('\n');
log.Detail($"Library version: {libraryVersion}");
}

// Tool version (EF Core Power Tools CLI)
if (!string.IsNullOrWhiteSpace(ToolVersion))
{
manifest.Append("tool\0").Append(ToolVersion).Append('\n');
log.Detail($"Tool version: {ToolVersion}");
}

// Source fingerprint (DACPAC OR schema fingerprint)
if (UseConnectionStringMode.IsTrue())
{
Expand All @@ -124,6 +169,13 @@ private bool ExecuteCore(TaskExecutionContext ctx)
Append(manifest, ConfigPath, "config");
Append(manifest, RenamingPath, "renaming");

// Config property overrides (MSBuild properties that override efcpt-config.json)
if (!string.IsNullOrWhiteSpace(ConfigPropertyOverrides))
{
manifest.Append("config-overrides\0").Append(ConfigPropertyOverrides).Append('\n');
log.Detail("Including MSBuild config property overrides in fingerprint");
}

manifest = Directory
.EnumerateFiles(TemplateDir, "*", SearchOption.AllDirectories)
.Select(p => p.Replace('\u005C', '/'))
Expand All @@ -136,6 +188,23 @@ private bool ExecuteCore(TaskExecutionContext ctx)
.Append(data.rel).Append('\0')
.Append(data.h).Append('\n'));

// Generated files (optional, off by default to avoid overwriting manual edits)
if (!string.IsNullOrWhiteSpace(GeneratedDir) && Directory.Exists(GeneratedDir) && DetectGeneratedFileChanges.IsTrue())
{
log.Detail("Detecting generated file changes (EfcptDetectGeneratedFileChanges=true)");
manifest = Directory
.EnumerateFiles(GeneratedDir, "*.g.cs", SearchOption.AllDirectories)
.Select(p => p.Replace('\u005C', '/'))
.OrderBy(p => p, StringComparer.Ordinal)
.Select(file => (
rel: Path.GetRelativePath(GeneratedDir, file).Replace('\u005C', '/'),
h: FileHash.HashFile(file)))
.Aggregate(manifest, (builder, data)
=> builder.Append("generated/")
.Append(data.rel).Append('\0')
.Append(data.h).Append('\n'));
}

Fingerprint = FileHash.HashString(manifest.ToString());

var prior = File.Exists(FingerprintFile) ? File.ReadAllText(FingerprintFile).Trim() : "";
Expand All @@ -155,6 +224,22 @@ private bool ExecuteCore(TaskExecutionContext ctx)
return true;
}

private static string GetLibraryVersion()
{
try
{
var assembly = typeof(ComputeFingerprint).Assembly;
var version = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion
?? assembly.GetName().Version?.ToString()
?? "";
return version;
}
catch
{
return "";
}
}

private static void Append(StringBuilder manifest, string path, string label)
{
var full = Path.GetFullPath(path);
Expand Down
Loading
Loading