From b3c954b73425c39f1c661688293264d92348d6b2 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Fri, 9 Oct 2026 21:45:08 +1100 Subject: [PATCH 1/2] Verify an IModel as a Mermaid diagram in markdown, svg or png --- readme.md | 81 ++++++++++++++++++ src/Directory.Build.props | 4 +- src/Directory.Packages.props | 1 + .../ModelDiagramTests.Formats.verified.md | 14 +++ .../ModelDiagramTests.Formats.verified.png | Bin 0 -> 17718 bytes .../ModelDiagramTests.Formats.verified.svg | 1 + .../ModelDiagramTests.Markdown.verified.md | 14 +++ .../ModelDiagramTests.SvgOnly.verified.svg | 1 + .../ModelDiagramTests.cs | 55 ++++++++++++ .../ModuleInitializer.cs | 6 ++ src/Verify.EntityFramework/ModelDiagram.cs | 65 ++++++++++++++ .../ModelDiagramFormat.cs | 25 ++++++ .../Verify.EntityFramework.csproj | 1 + .../VerifyEntityFramework.cs | 1 + src/mdsnippets.json | 5 +- 15 files changed, 271 insertions(+), 3 deletions(-) create mode 100644 src/Verify.EntityFramework.Tests/ModelDiagramTests.Formats.verified.md create mode 100644 src/Verify.EntityFramework.Tests/ModelDiagramTests.Formats.verified.png create mode 100644 src/Verify.EntityFramework.Tests/ModelDiagramTests.Formats.verified.svg create mode 100644 src/Verify.EntityFramework.Tests/ModelDiagramTests.Markdown.verified.md create mode 100644 src/Verify.EntityFramework.Tests/ModelDiagramTests.SvgOnly.verified.svg create mode 100644 src/Verify.EntityFramework.Tests/ModelDiagramTests.cs create mode 100644 src/Verify.EntityFramework/ModelDiagram.cs create mode 100644 src/Verify.EntityFramework/ModelDiagramFormat.cs diff --git a/readme.md b/readme.md index afcac020..e16aa905 100644 --- a/readme.md +++ b/readme.md @@ -658,6 +658,87 @@ Will result in the following verified file with all data in the database: +## Model diagram + +An `IModel` can be verified as a [Mermaid ER diagram](https://mermaid.js.org/syntax/entityRelationshipDiagram.html), using [DbToMermaid](https://github.com/SimonCropp/DbToMermaid). EF Core only. + + + +```cs +await Verify(data.Model); +``` +snippet source | anchor + + +Result: [ModelDiagramTests.Markdown.verified.md](/src/Verify.EntityFramework.Tests/ModelDiagramTests.Markdown.verified.md) + +The diagram can be verified as markdown, svg, png, or any combination. One file is verified for each format. + + + +```cs +await Verify(data.Model) + .ModelAsDiagram( + ModelDiagramFormat.Markdown | + ModelDiagramFormat.Svg | + ModelDiagramFormat.Png); +``` +snippet source | anchor + + +Result: + + * [ModelDiagramTests.Formats.verified.md](/src/Verify.EntityFramework.Tests/ModelDiagramTests.Formats.verified.md) + * [ModelDiagramTests.Formats.verified.svg](/src/Verify.EntityFramework.Tests/ModelDiagramTests.Formats.verified.svg) + * [ModelDiagramTests.Formats.verified.png](/src/Verify.EntityFramework.Tests/ModelDiagramTests.Formats.verified.png) + +Available values: + + + +```cs +namespace VerifyTests.EntityFramework; + +/// +/// The files produced when a model is verified as a diagram. Can be combined. +/// +[Flags] +public enum ModelDiagramFormat +{ + /// + /// A Mermaid ER diagram in a markdown code block. Uses the `md` extension. + /// + Markdown = 1, + + /// + /// The diagram rendered to an image. Uses the `svg` extension. + /// + Svg = 2, + + /// + /// The diagram rendered to an image. Uses the `png` extension. + /// + Png = 4, + + All = Markdown | Svg | Png +} +``` +snippet source | anchor + + +A rendered png may not be pixel identical across machines, so compare using [ssim](https://github.com/VerifyTests/Verify/blob/main/docs/comparer.md): + + + +```cs +VerifierSettings.UseSsimForPng(); +``` +snippet source | anchor + + +The svg and png are rendered by [Naiad](https://github.com/Papyrine/Naiad). Its build check requires a sponsorship, or exemption, property to be set in the consuming project. The build error lists the options. + + ## IgnoreNavigationProperties `IgnoreNavigationProperties` extends `SerializationSettings` to exclude all navigation properties from serialization: diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 16801d16..9bcb0592 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,7 +2,7 @@ CS1591;CS0649;CS8632;EF1001;NU1608;NU1109 - 16.0.0 + 16.1.0 preview 1.0.0 EntityFramework, Verify @@ -13,5 +13,7 @@ true OpenSource 2027-08 + OpenSource + 2027-08 diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 25f242a4..1c2cf93d 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -6,6 +6,7 @@ + diff --git a/src/Verify.EntityFramework.Tests/ModelDiagramTests.Formats.verified.md b/src/Verify.EntityFramework.Tests/ModelDiagramTests.Formats.verified.md new file mode 100644 index 00000000..421a1171 --- /dev/null +++ b/src/Verify.EntityFramework.Tests/ModelDiagramTests.Formats.verified.md @@ -0,0 +1,14 @@ +```mermaid +erDiagram + Companies["**Companies**"] { + int Id pk + nvarchar(max) Name + } + Employees["**Employees**"] { + int Id pk + int Age + int CompanyId + nvarchar(max) Name + } + Companies ||--o{ Employees : "FK_Employees_Companies_CompanyId" +``` \ No newline at end of file diff --git a/src/Verify.EntityFramework.Tests/ModelDiagramTests.Formats.verified.png b/src/Verify.EntityFramework.Tests/ModelDiagramTests.Formats.verified.png new file mode 100644 index 0000000000000000000000000000000000000000..e2f6b004e7d6ed71593a915ebfb6351d02ec82ee GIT binary patch literal 17718 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 literal 0 HcmV?d00001 diff --git a/src/Verify.EntityFramework.Tests/ModelDiagramTests.Formats.verified.svg b/src/Verify.EntityFramework.Tests/ModelDiagramTests.Formats.verified.svg new file mode 100644 index 00000000..880097f2 --- /dev/null +++ b/src/Verify.EntityFramework.Tests/ModelDiagramTests.Formats.verified.svg @@ -0,0 +1 @@ +FK_Employees_Companies_CompanyIdCompaniesPKint Idnvarchar(max) NameEmployeesPKint Idint Ageint CompanyIdnvarchar(max) Name \ No newline at end of file diff --git a/src/Verify.EntityFramework.Tests/ModelDiagramTests.Markdown.verified.md b/src/Verify.EntityFramework.Tests/ModelDiagramTests.Markdown.verified.md new file mode 100644 index 00000000..421a1171 --- /dev/null +++ b/src/Verify.EntityFramework.Tests/ModelDiagramTests.Markdown.verified.md @@ -0,0 +1,14 @@ +```mermaid +erDiagram + Companies["**Companies**"] { + int Id pk + nvarchar(max) Name + } + Employees["**Employees**"] { + int Id pk + int Age + int CompanyId + nvarchar(max) Name + } + Companies ||--o{ Employees : "FK_Employees_Companies_CompanyId" +``` \ No newline at end of file diff --git a/src/Verify.EntityFramework.Tests/ModelDiagramTests.SvgOnly.verified.svg b/src/Verify.EntityFramework.Tests/ModelDiagramTests.SvgOnly.verified.svg new file mode 100644 index 00000000..880097f2 --- /dev/null +++ b/src/Verify.EntityFramework.Tests/ModelDiagramTests.SvgOnly.verified.svg @@ -0,0 +1 @@ +FK_Employees_Companies_CompanyIdCompaniesPKint Idnvarchar(max) NameEmployeesPKint Idint Ageint CompanyIdnvarchar(max) Name \ No newline at end of file diff --git a/src/Verify.EntityFramework.Tests/ModelDiagramTests.cs b/src/Verify.EntityFramework.Tests/ModelDiagramTests.cs new file mode 100644 index 00000000..e8124cd0 --- /dev/null +++ b/src/Verify.EntityFramework.Tests/ModelDiagramTests.cs @@ -0,0 +1,55 @@ +using VerifyTests.EntityFramework; + +public class ModelDiagramTests +{ + [Test] + public async Task Markdown() + { + await using var data = BuildData(); + + #region ModelDiagram + + await Verify(data.Model); + + #endregion + } + + [Test] + public async Task Formats() + { + await using var data = BuildData(); + + #region ModelDiagramFormats + + await Verify(data.Model) + .ModelAsDiagram( + ModelDiagramFormat.Markdown | + ModelDiagramFormat.Svg | + ModelDiagramFormat.Png); + + #endregion + } + + [Test] + public async Task SvgOnly() + { + await using var data = BuildData(); + await Verify(data.Model) + .ModelAsDiagram(ModelDiagramFormat.Svg); + } + + [Test] + public async Task NoFormat() + { + var settings = new VerifySettings(); + await Assert.That(() => settings.ModelAsDiagram(0)) + .Throws(); + } + + static SampleDbContext BuildData() + { + var options = new DbContextOptionsBuilder(); + options.UseSqlServer("fake"); + return new(options.Options); + } +} diff --git a/src/Verify.EntityFramework.Tests/ModuleInitializer.cs b/src/Verify.EntityFramework.Tests/ModuleInitializer.cs index f2c5a964..794ca7aa 100644 --- a/src/Verify.EntityFramework.Tests/ModuleInitializer.cs +++ b/src/Verify.EntityFramework.Tests/ModuleInitializer.cs @@ -24,6 +24,12 @@ public static void Init() [ModuleInitializer] public static void InitOther() { + #region UseSsimForPng + + VerifierSettings.UseSsimForPng(); + + #endregion + VerifierSettings.InitializePlugins(); Recording.IgnoreNames("sql"); } diff --git a/src/Verify.EntityFramework/ModelDiagram.cs b/src/Verify.EntityFramework/ModelDiagram.cs new file mode 100644 index 00000000..e00a64de --- /dev/null +++ b/src/Verify.EntityFramework/ModelDiagram.cs @@ -0,0 +1,65 @@ +using DbToMermaid; + +namespace VerifyTests; + +public static class VerifyEntityFrameworkModelDiagram +{ + const string contextKey = "EntityFramework.ModelDiagram"; + + /// + /// Select the files produced when an is verified as a Mermaid ER diagram. + /// + /// The settings to apply to. + /// The files to produce. Can be combined, with one file verified per format. + public static SettingsTask ModelAsDiagram( + this SettingsTask settings, + ModelDiagramFormat formats = ModelDiagramFormat.Markdown) + { + settings.CurrentSettings.ModelAsDiagram(formats); + return settings; + } + + /// + public static void ModelAsDiagram( + this VerifySettings settings, + ModelDiagramFormat formats = ModelDiagramFormat.Markdown) + { + if ((formats & ModelDiagramFormat.All) == 0) + { + throw new ArgumentException("At least one format is required.", nameof(formats)); + } + + settings.Context[contextKey] = formats; + } + + internal static async Task Convert(IModel model, IReadOnlyDictionary context) + { + var formats = ModelDiagramFormat.Markdown; + if (context.TryGetValue(contextKey, out var value)) + { + formats = (ModelDiagramFormat) value; + } + + var targets = new List(); + + if (formats.HasFlag(ModelDiagramFormat.Markdown)) + { + var markdown = await EfToMermaid.RenderMarkdown(model); + targets.Add(new("md", markdown)); + } + + if (formats.HasFlag(ModelDiagramFormat.Svg)) + { + var svg = await EfToMermaid.RenderSvg(model); + targets.Add(new("svg", svg)); + } + + if (formats.HasFlag(ModelDiagramFormat.Png)) + { + var png = await EfToMermaid.RenderPng(model); + targets.Add(new("png", new MemoryStream(png))); + } + + return new(null, targets); + } +} diff --git a/src/Verify.EntityFramework/ModelDiagramFormat.cs b/src/Verify.EntityFramework/ModelDiagramFormat.cs new file mode 100644 index 00000000..154f1b0e --- /dev/null +++ b/src/Verify.EntityFramework/ModelDiagramFormat.cs @@ -0,0 +1,25 @@ +namespace VerifyTests.EntityFramework; + +/// +/// The files produced when a model is verified as a diagram. Can be combined. +/// +[Flags] +public enum ModelDiagramFormat +{ + /// + /// A Mermaid ER diagram in a markdown code block. Uses the `md` extension. + /// + Markdown = 1, + + /// + /// The diagram rendered to an image. Uses the `svg` extension. + /// + Svg = 2, + + /// + /// The diagram rendered to an image. Uses the `png` extension. + /// + Png = 4, + + All = Markdown | Svg | Png +} diff --git a/src/Verify.EntityFramework/Verify.EntityFramework.csproj b/src/Verify.EntityFramework/Verify.EntityFramework.csproj index d6e12053..ed1640c2 100644 --- a/src/Verify.EntityFramework/Verify.EntityFramework.csproj +++ b/src/Verify.EntityFramework/Verify.EntityFramework.csproj @@ -3,6 +3,7 @@ net10.0 + diff --git a/src/Verify.EntityFramework/VerifyEntityFramework.cs b/src/Verify.EntityFramework/VerifyEntityFramework.cs index 45909e76..0fd4fa65 100644 --- a/src/Verify.EntityFramework/VerifyEntityFramework.cs +++ b/src/Verify.EntityFramework/VerifyEntityFramework.cs @@ -199,6 +199,7 @@ public static void Initialize(IModel? model, bool recordCommands) VerifierSettings.RegisterFileConverter( QueryableToSql, (target, _) => QueryableConverter.IsQueryable(target)); + VerifierSettings.RegisterFileConverter(VerifyEntityFrameworkModelDiagram.Convert); VerifierSettings.IgnoreMembersWithType(typeof(IDbContextFactory<>)); VerifierSettings.IgnoreMembersWithType(); diff --git a/src/mdsnippets.json b/src/mdsnippets.json index 331566e9..6f67fb44 100644 --- a/src/mdsnippets.json +++ b/src/mdsnippets.json @@ -1,7 +1,8 @@ -{ +{ "$schema": "https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/schema.json", "TocExcludes": [ "NuGet package", "Release Notes", "Icon" ], "MaxWidth": 90, "ValidateContent": true, - "Convention": "InPlaceOverwrite" + "Convention": "InPlaceOverwrite", + "ExcludeMarkdownFiles": [ "*.verified.md", "*.received.md" ] } \ No newline at end of file From 5f5817171c135b87b61b41d4d5846790f60252bb Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Fri, 9 Oct 2026 21:47:43 +1100 Subject: [PATCH 2/2] Show the png diagram in the readme --- readme.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/readme.md b/readme.md index e16aa905..26fe90fd 100644 --- a/readme.md +++ b/readme.md @@ -692,6 +692,8 @@ Result: * [ModelDiagramTests.Formats.verified.svg](/src/Verify.EntityFramework.Tests/ModelDiagramTests.Formats.verified.svg) * [ModelDiagramTests.Formats.verified.png](/src/Verify.EntityFramework.Tests/ModelDiagramTests.Formats.verified.png) +ModelDiagramTests.Formats.verified.png + Available values: