diff --git a/readme.md b/readme.md index afcac020..26fe90fd 100644 --- a/readme.md +++ b/readme.md @@ -658,6 +658,89 @@ 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) + +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 00000000..e2f6b004 Binary files /dev/null and b/src/Verify.EntityFramework.Tests/ModelDiagramTests.Formats.verified.png differ 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