Skip to content

Commit 9e4099b

Browse files
mkholtclaude
andauthored
Fail loud on duplicate workflow ids in Workflows folder (#337)
* Fail loud on duplicate workflow ids in Workflows folder When the Workflows folder contains multiple XML files referencing the same workflow id (typically leftover files from older metadata generations whose tooling did not clear the folder before writing renamed workflows back to disk), initialization used to throw a generic FaultException out of XrmDb ("record already exists with that Id"). GetWorkflows now detects the collision at load time and throws a MockupException listing the conflicting filenames and instructing the user to re-generate metadata. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 10fc380 commit 9e4099b

3 files changed

Lines changed: 117 additions & 1 deletion

File tree

RELEASE_NOTES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
### 1.18.3 - 26 June 2026
2+
* Fix: When the Workflows folder contained multiple XML files referencing the same workflow id (typically leftover files from older metadata generations), initialization threw a confusing internal "record already exists" FaultException. It now throws a `MockupException` at load time listing the conflicting files and instructing the user to re-generate metadata
3+
14
### 1.18.2 - 25 June 2026
25
* Fix: Top level link criteria were unioned instead of cross joined
36

src/XrmMockup365/Internal/Utility.cs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -725,7 +725,27 @@ internal static List<Entity> GetWorkflows(string folderLocation)
725725
}
726726

727727
var files = Directory.GetFiles(pathToWorkflows, "*.xml");
728-
return files.Select(GetWorkflow).ToList();
728+
var workflowsByFile = files.ToDictionary(f => f, GetWorkflow);
729+
730+
// Detect duplicate workflow ids across files. This typically indicates leftover files
731+
// from older metadata generations whose tooling did not clear the Workflows folder
732+
// before writing renamed workflows back to disk. Picking a winner silently would risk
733+
// running a stale version, so we fail loud with an actionable message.
734+
var duplicates = workflowsByFile
735+
.GroupBy(kvp => kvp.Value.Id)
736+
.Where(g => g.Count() > 1)
737+
.ToList();
738+
if (duplicates.Count > 0)
739+
{
740+
var details = string.Join("; ", duplicates.Select(g =>
741+
$"workflow id {g.Key} appears in: {string.Join(", ", g.Select(kvp => Path.GetFileName(kvp.Key)))}"));
742+
throw new MockupException(
743+
$"Duplicate workflow ids found in '{pathToWorkflows}'. {details}. " +
744+
"This usually means leftover XML files from a previous metadata generation. " +
745+
"Delete the Workflows folder and re-generate metadata.");
746+
}
747+
748+
return workflowsByFile.Values.ToList();
729749
}
730750

731751
internal static Entity GetWorkflow(string path)
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
using System;
2+
using System.IO;
3+
using System.Linq;
4+
using DG.Tools.XrmMockup;
5+
using Xunit;
6+
7+
namespace DG.XrmMockupTest
8+
{
9+
public class TestInitialization
10+
{
11+
// Regression for #138: initialization threw a generic FaultException ("a record already
12+
// exists with that Id") when the Workflows folder contained multiple XML files referring to
13+
// the same workflow id — typically leftover files from older metadata generations whose
14+
// tooling did not clear the folder before writing renamed workflows back to disk. The fix
15+
// surfaces the duplicate as a MockupException at load time so the user gets actionable
16+
// guidance instead of a confusing database error.
17+
[Fact]
18+
public void InitializationThrowsMockupExceptionOnDuplicateWorkflowIds()
19+
{
20+
var sourceMetadata = ResolveSourceMetadataPath();
21+
var tempMetadata = Path.Combine(Path.GetTempPath(), "XrmMockupDupeWorkflowTest-" + Guid.NewGuid().ToString("N"));
22+
23+
try
24+
{
25+
CopyDirectory(sourceMetadata, tempMetadata);
26+
27+
var workflowsDir = Path.Combine(tempMetadata, "Workflows");
28+
var workflowFiles = Directory.GetFiles(workflowsDir, "*.xml");
29+
Assert.NotEmpty(workflowFiles);
30+
31+
// Drop a copy of an existing workflow XML under a new filename — same workflow id,
32+
// different file. This is the shape produced by older metadata generators that did
33+
// not clear the Workflows folder before writing renamed workflows back to disk.
34+
var duplicatePath = Path.Combine(workflowsDir, "DuplicateOfFirstWorkflow.xml");
35+
File.Copy(workflowFiles[0], duplicatePath);
36+
37+
var settings = new XrmMockupSettings
38+
{
39+
MetadataDirectoryPath = tempMetadata,
40+
IncludeAllWorkflows = false,
41+
BasePluginTypes = Array.Empty<Type>(),
42+
CodeActivityInstanceTypes = Array.Empty<Type>()
43+
};
44+
45+
var ex = Assert.Throws<MockupException>(() => XrmMockup365.GetInstance(settings));
46+
47+
// Message must surface the actual conflicting files and tell the user how to fix it.
48+
Assert.Contains("Duplicate workflow ids", ex.Message);
49+
Assert.Contains(Path.GetFileName(workflowFiles[0]), ex.Message);
50+
Assert.Contains("DuplicateOfFirstWorkflow.xml", ex.Message);
51+
Assert.Contains("re-generate metadata", ex.Message);
52+
}
53+
finally
54+
{
55+
if (Directory.Exists(tempMetadata))
56+
{
57+
Directory.Delete(tempMetadata, recursive: true);
58+
}
59+
}
60+
}
61+
62+
private static string ResolveSourceMetadataPath()
63+
{
64+
var currentDir = Directory.GetCurrentDirectory();
65+
var candidates = new[]
66+
{
67+
Path.Combine(currentDir, "Metadata"),
68+
Path.Combine(currentDir, "..", "..", "..", "Metadata"),
69+
};
70+
71+
foreach (var candidate in candidates)
72+
{
73+
var full = Path.GetFullPath(candidate);
74+
if (Directory.Exists(full)) return full;
75+
}
76+
77+
throw new DirectoryNotFoundException("Could not locate the test Metadata directory.");
78+
}
79+
80+
private static void CopyDirectory(string source, string destination)
81+
{
82+
Directory.CreateDirectory(destination);
83+
foreach (var file in Directory.GetFiles(source))
84+
{
85+
File.Copy(file, Path.Combine(destination, Path.GetFileName(file)));
86+
}
87+
foreach (var directory in Directory.GetDirectories(source))
88+
{
89+
CopyDirectory(directory, Path.Combine(destination, Path.GetFileName(directory)));
90+
}
91+
}
92+
}
93+
}

0 commit comments

Comments
 (0)