├── .github ├── CODEOWNERS ├── policies │ └── csdl-diagrams-branch-protection.yml └── workflows │ └── codeql-analysis.yml ├── README.md ├── Microsoft.OData.Diagram ├── Microsoft.OData.Diagram.snk ├── Properties │ └── launchSettings.json ├── README.md ├── ProgramOptions.cs ├── Microsoft.OData.Diagram.csproj ├── Program.cs └── RenderSvg.cs ├── Microsoft.OData.UML ├── Microsoft.CsdlDiagrams.Net.snk ├── GeneratorOptions.cs ├── PlantConverter.cs ├── Microsoft.OData.UML.csproj ├── GenerationError.cs ├── CodeGeneratorBase.cs ├── Range.cs └── CsdlToPlantGenerator.cs ├── CODE_OF_CONDUCT.md ├── Microsoft.OData.UML.Tests ├── Microsoft.OData.UML.Tests.csproj ├── StringAssertExtensions.cs ├── CsdlTestHelper.cs └── BasicStructureTests.cs ├── LICENSE ├── Microsoft.OData.Diagram.sln ├── SECURITY.md ├── .gitignore └── .azure-pipelines └── ci-build.yml /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @microsoftgraph/msgraph-devx-agora-write 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # csdl-diagrams 2 | Libraries and tools to process OData CSDL files and visualize them as diagrams. 3 | -------------------------------------------------------------------------------- /Microsoft.OData.Diagram/Microsoft.OData.Diagram.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftgraph/csdl-diagrams/main/Microsoft.OData.Diagram/Microsoft.OData.Diagram.snk -------------------------------------------------------------------------------- /Microsoft.OData.UML/Microsoft.CsdlDiagrams.Net.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftgraph/csdl-diagrams/main/Microsoft.OData.UML/Microsoft.CsdlDiagrams.Net.snk -------------------------------------------------------------------------------- /Microsoft.OData.Diagram/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "CsdlToDiagram": { 4 | "commandName": "Project", 5 | "commandLineArgs": "C:\\Users\\Wamae\\source\\schema.csdl" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | - Employees can reach out at [aka.ms/opensource/moderation-support](https://aka.ms/opensource/moderation-support) 11 | -------------------------------------------------------------------------------- /Microsoft.OData.Diagram/README.md: -------------------------------------------------------------------------------- 1 | # CsdlToDiagram 2 | Simple console app to generate a PlantUml diagram from an OData CSDL file. 3 | Available on nuget as a tool: `CsdlToDiagram` 4 | 5 | ## Generate a PlantUML diagram to the console. 6 | ``` 7 | CsdlToDiagram 8 | ``` 9 | 10 | ## Generate an SVG diagram to the console. 11 | ``` 12 | CsdlToDiagram -s 13 | ``` 14 | 15 | ## Generate an SVG diagram to a file 16 | ``` 17 | CsdlToDiagram -s -o 18 | ``` 19 | 20 | ## Generate an SVG diagram to a file using a specific PlantUML rendering server 21 | ``` 22 | CsdlToDiagram -s -o -u https://www.plantuml.com/plantuml 23 | ``` 24 | -------------------------------------------------------------------------------- /Microsoft.OData.UML/GeneratorOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.OData.UML 2 | { 3 | using System.Collections.Generic; 4 | 5 | /// 6 | /// Options to configure generation 7 | /// 8 | public class GeneratorOptions 9 | { 10 | /// 11 | /// Generator options used if no custom value specified. 12 | /// 13 | public static GeneratorOptions DefaultGeneratorOptions = new GeneratorOptions {SkipList = new[] {"Entity"}}; 14 | 15 | /// 16 | /// List of names of types to skip over when generating. 17 | /// 18 | public IEnumerable SkipList { get; set; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Microsoft.OData.UML.Tests/Microsoft.OData.UML.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Microsoft.OData.Diagram/ProgramOptions.cs: -------------------------------------------------------------------------------- 1 | // 2 | // © Microsoft. All rights reserved. 3 | // 4 | 5 | using CommandLine; 6 | 7 | namespace Microsoft.OData.Diagram 8 | { 9 | public class ProgramOptions 10 | { 11 | [Option('s', "svgMode", Required = false, HelpText = "Flag to emit svg files rather than raw PlantUML.")] 12 | public bool SvgModel { get; set; } 13 | 14 | [Option('o', "out", Required = false, HelpText = "Option to specify a filename to output.")] 15 | public string? Output { get; set; } 16 | 17 | [Option('u', "serverUrl", Required = false, HelpText = "Option to specify a PlantUML rendering server to use.")] 18 | public string? ServerUrl { get; set; } 19 | 20 | [Value(0, HelpText = "The CSDL file to render.", Required = true)] 21 | public string? CsdlFile { get; set; } 22 | } 23 | } -------------------------------------------------------------------------------- /Microsoft.OData.UML/PlantConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | 3 | namespace Microsoft.OData.UML 4 | { 5 | public class PlantConverter 6 | { 7 | private const string GenerationErrorsMessage = "There were errors generating the PlantUML file."; 8 | private readonly Generator generator = new Generator(); 9 | 10 | public string EmitPlantDiagram(string csdlContent, string csdlFilename, GeneratorOptions options = null) 11 | { 12 | options ??= GeneratorOptions.DefaultGeneratorOptions; 13 | 14 | this.generator.EmitPlantDiagram(csdlContent, csdlFilename, options); 15 | if (this.generator.Errors.Any(e => !e.IsWarning)) 16 | { 17 | return GenerationErrorsMessage; 18 | } 19 | else 20 | { 21 | return this.generator.GetText(); 22 | } 23 | } 24 | 25 | private class Generator : CsdlToPlantGenerator 26 | { 27 | public string GetText() 28 | { 29 | return this.GenerationEnvironment.ToString(); 30 | } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Microsoft Graph 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Microsoft.OData.UML.Tests/StringAssertExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.RegularExpressions; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | namespace Microsoft.OData.UML.Tests 6 | { 7 | public static class StringAssertExtensions 8 | { 9 | #pragma warning disable IDE0060 // Remove unused parameter - stadard StringAssert extension mechanism. 10 | public static void ContainsCountOf(this StringAssert assert, string value, int count, string substring) 11 | { 12 | string processedSubstring = Regex.Escape(substring); 13 | int found = Regex.Matches(value, processedSubstring).Count; 14 | if (found != count) 15 | { 16 | throw new AssertFailedException($"Unexpected number of instances of '{substring}' found in '{value}'{Environment.NewLine}Expected {count}, actual {found}."); 17 | } 18 | } 19 | 20 | public static void MatchesLines(this StringAssert assert, string value, params string[] patterns) 21 | { 22 | string pattern = string.Join(Environment.NewLine, patterns); 23 | StringAssert.Matches(value, new Regex(pattern, RegexOptions.Multiline)); 24 | } 25 | #pragma warning restore IDE0060 // Remove unused parameter 26 | } 27 | } -------------------------------------------------------------------------------- /Microsoft.OData.Diagram/Microsoft.OData.Diagram.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | 9.0 6 | net6.0 7 | http://go.microsoft.com/fwlink/?LinkID=288890 8 | https://github.com/microsoftgraph/csdl-diagrams 9 | Microsoft.OData.Diagram.Program 10 | 1.0.0 11 | Microsoft 12 | Microsoft 13 | Microsoft.OData.Diagram 14 | true 15 | Microsoft.OData.Diagram 16 | Generates UML diagrams for OData CSDL files. 17 | © Microsoft Corporation. All rights reserved. 18 | true 19 | MIT 20 | ./../../artifacts 21 | https://github.com/microsoftgraph/csdl-diagrams 22 | enable 23 | Microsoft.OData.Diagram 24 | Microsoft.OData.Diagram 25 | true 26 | true 27 | Microsoft.OData.Diagram.snk 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | all 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /Microsoft.OData.UML/Microsoft.OData.UML.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 1.0.0 6 | Microsoft 7 | Microsoft 8 | Csdl Diagrams 9 | Microsoft.OData.UML 10 | .Net standard 2.0 library to generate diagram source files (such as PlantUml) from OData CSDL files. 11 | © Microsoft Corporation. All rights reserved. 12 | MIT 13 | true 14 | Microsoft.OData.UML 15 | http://go.microsoft.com/fwlink/?LinkID=288890 16 | https://github.com/microsoftgraph/csdl-diagrams 17 | https://github.com/microsoftgraph/csdl-diagrams 18 | git 19 | true 20 | 8.0 21 | Microsoft.OData.UML 22 | Microsoft.OData.UML 23 | true 24 | true 25 | Microsoft.CsdlDiagrams.Net.snk 26 | 27 | 28 | 29 | 30 | 31 | all 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /Microsoft.OData.UML.Tests/CsdlTestHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Microsoft.OData.UML.Tests 6 | { 7 | public class CsdlTestHelper 8 | { 9 | private const string CsdlHeader = @" 10 | 15 | 16 | 17 | 18 | "; 19 | 20 | private const string CsdlFooter = @""; 21 | private const string CsdlStartSchema = @""; 22 | private const string CsdlEndSchema = @""; 23 | 24 | public static string CreateEntity(string entityName, IEnumerable> attributes, string content = null) 25 | { 26 | content ??= String.Empty; 27 | string attributeString = String.Join(' ', attributes.Select(a => $@"{a.Key}=""{a.Value}""")); 28 | return $@"{content}"; 29 | } 30 | 31 | public static string CreateEntity(string entityName, string content = null) 32 | { 33 | return CreateEntity(entityName, Array.Empty>(), content); 34 | } 35 | 36 | public static string FormCsdl(string theNamespace, string content) 37 | { 38 | return $"{CsdlHeader}{string.Format(CsdlStartSchema, theNamespace)}{content}{CsdlEndSchema}{CsdlFooter}"; 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /.github/policies/csdl-diagrams-branch-protection.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | # File initially created using https://github.com/MIchaelMainer/policyservicetoolkit/blob/main/branch_protection_export.ps1. 5 | 6 | name: csdl-diagrams-branch-protection 7 | description: Branch protection policy for the csdl-diagrams repository 8 | resource: repository 9 | configuration: 10 | branchProtectionRules: 11 | 12 | - branchNamePattern: main 13 | # This branch pattern applies to the following branches as of 06/12/2023 10:31:16: 14 | # main 15 | 16 | # Specifies whether this branch can be deleted. boolean 17 | allowsDeletions: false 18 | # Specifies whether forced pushes are allowed on this branch. boolean 19 | allowsForcePushes: false 20 | # Specifies whether new commits pushed to the matching branches dismiss pull request review approvals. boolean 21 | dismissStaleReviews: true 22 | # Specifies whether admins can overwrite branch protection. boolean 23 | isAdminEnforced: false 24 | # Indicates whether "Require a pull request before merging" is enabled. boolean 25 | requiresPullRequestBeforeMerging: true 26 | # Specifies the number of pull request reviews before merging. int (0-6). Should be null/empty if PRs are not required 27 | requiredApprovingReviewsCount: 1 28 | # Require review from Code Owners. Requires requiredApprovingReviewsCount. boolean 29 | requireCodeOwnersReview: true 30 | # Are commits required to be signed. boolean. TODO: all contributors must have commit signing on local machines. 31 | requiresCommitSignatures: false 32 | # Are conversations required to be resolved before merging? boolean 33 | requiresConversationResolution: true 34 | # Are merge commits prohibited from being pushed to this branch. boolean 35 | requiresLinearHistory: false 36 | # Required status checks to pass before merging. Values can be any string, but if the value does not correspond to any existing status check, the status check will be stuck on pending for status since nothing exists to push an actual status 37 | requiredStatusChecks: 38 | # Require branches to be up to date before merging. Requires requiredStatusChecks. boolean 39 | requiresStrictStatusChecks: false 40 | # Indicates whether there are restrictions on who can push. boolean. Should be set with whoCanPush. 41 | restrictsPushes: false 42 | # Restrict who can dismiss pull request reviews. boolean 43 | restrictsReviewDismissals: false -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '18 16 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v1 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v1 71 | -------------------------------------------------------------------------------- /Microsoft.OData.Diagram.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31903.59 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.OData.Diagram", "Microsoft.OData.Diagram\Microsoft.OData.Diagram.csproj", "{35979C97-3ECF-45B7-87D0-043B1AF1CDDA}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.OData.UML", "Microsoft.OData.UML\Microsoft.OData.UML.csproj", "{F30F8D61-4ACB-4973-91D1-A3F855BF78E7}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionItems", "{6D6ED9E6-A9B1-4B72-A85C-C27CBE7892C0}" 11 | ProjectSection(SolutionItems) = preProject 12 | .github\workflows\dotnet-core.yml = .github\workflows\dotnet-core.yml 13 | License.txt = License.txt 14 | README.md = README.md 15 | EndProjectSection 16 | EndProject 17 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.OData.UML.Tests", "Microsoft.OData.UML.Tests\Microsoft.OData.UML.Tests.csproj", "{7F40FD29-84D4-4B01-917E-69FBC8D5B51E}" 18 | EndProject 19 | Global 20 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 21 | Debug|Any CPU = Debug|Any CPU 22 | Release|Any CPU = Release|Any CPU 23 | EndGlobalSection 24 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 25 | {35979C97-3ECF-45B7-87D0-043B1AF1CDDA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {35979C97-3ECF-45B7-87D0-043B1AF1CDDA}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {35979C97-3ECF-45B7-87D0-043B1AF1CDDA}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {35979C97-3ECF-45B7-87D0-043B1AF1CDDA}.Release|Any CPU.Build.0 = Release|Any CPU 29 | {F30F8D61-4ACB-4973-91D1-A3F855BF78E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {F30F8D61-4ACB-4973-91D1-A3F855BF78E7}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {F30F8D61-4ACB-4973-91D1-A3F855BF78E7}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {F30F8D61-4ACB-4973-91D1-A3F855BF78E7}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {7F40FD29-84D4-4B01-917E-69FBC8D5B51E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {7F40FD29-84D4-4B01-917E-69FBC8D5B51E}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {7F40FD29-84D4-4B01-917E-69FBC8D5B51E}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {7F40FD29-84D4-4B01-917E-69FBC8D5B51E}.Release|Any CPU.Build.0 = Release|Any CPU 37 | EndGlobalSection 38 | GlobalSection(SolutionProperties) = preSolution 39 | HideSolutionNode = FALSE 40 | EndGlobalSection 41 | GlobalSection(ExtensibilityGlobals) = postSolution 42 | SolutionGuid = {22D41408-9F62-4A9F-B207-3D618533FD0E} 43 | EndGlobalSection 44 | EndGlobal 45 | -------------------------------------------------------------------------------- /Microsoft.OData.UML.Tests/BasicStructureTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text.RegularExpressions; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using static Microsoft.OData.UML.Tests.CsdlTestHelper; 5 | 6 | namespace Microsoft.OData.UML.Tests 7 | { 8 | [TestClass] 9 | public class BasicStructureTests 10 | { 11 | [TestMethod] 12 | public void EntityProjectsClass() 13 | { 14 | string csdl = FormCsdl("myNamespace", CreateEntity(@"entityName")); 15 | var convertor = new PlantConverter(); 16 | 17 | string plant = convertor.EmitPlantDiagram(csdl, @"c:\model.csdl"); 18 | 19 | StringAssert.Matches(plant, new Regex(@"^class entityName", RegexOptions.Multiline)); 20 | } 21 | 22 | [TestMethod] 23 | public void AbstractEntityProjectsAbstractClass() 24 | { 25 | string csdl = FormCsdl("myNamespace", CreateEntity(@"entityName", new[] {new KeyValuePair("Abstract", "true")})); 26 | var convertor = new PlantConverter(); 27 | 28 | string plant = convertor.EmitPlantDiagram(csdl, @"c:\model.csdl"); 29 | 30 | StringAssert.Matches(plant, new Regex(@"^abstract class entityName", RegexOptions.Multiline)); 31 | } 32 | 33 | [TestMethod] 34 | public void AnnotatedEntityProjectsClassAndNote() 35 | { 36 | string csdl = FormCsdl("myNamespace", CreateEntity(@"entityName", @"")); 37 | var convertor = new PlantConverter(); 38 | 39 | string plant = convertor.EmitPlantDiagram(csdl, @"c:\model.csdl"); 40 | 41 | StringAssert.Matches(plant, new Regex(@"^class entityName", RegexOptions.Multiline)); 42 | StringAssert.That.MatchesLines(plant, @"note top of entityName", @"Note: First note.", @"end note"); 43 | StringAssert.That.ContainsCountOf(plant, 1, "note top of"); 44 | StringAssert.That.ContainsCountOf(plant, 1, "First note."); 45 | } 46 | 47 | [TestMethod] 48 | public void RootCommentProjectsFloatingNote() 49 | { 50 | string csdl = FormCsdl("myNamespace", 51 | CreateEntity(@"entityName") + 52 | @""); 53 | var convertor = new PlantConverter(); 54 | 55 | string plant = convertor.EmitPlantDiagram(csdl, @"c:\model.csdl"); 56 | 57 | StringAssert.Contains(plant, @"class entityName"); 58 | StringAssert.That.MatchesLines(plant, @"note as RootNoteR1", @"Note: Root note.", @"end note"); 59 | } 60 | 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /Microsoft.OData.Diagram/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Xml.Linq; 7 | using CommandLine; 8 | using Microsoft.OData.UML; 9 | 10 | namespace Microsoft.OData.Diagram 11 | { 12 | static class Program 13 | { 14 | static async Task Main(string[] args) 15 | { 16 | ParserResult result = (await Parser.Default.ParseArguments(args).WithParsedAsync(RunCommandAsync)); 17 | if (Debugger.IsAttached) 18 | { 19 | Console.ReadLine(); 20 | } 21 | return result.Tag == ParserResultType.NotParsed ? 1 : 0; 22 | } 23 | 24 | private static async Task RunCommandAsync(ProgramOptions args) 25 | { 26 | try 27 | { 28 | var csdlFile = args.CsdlFile!; 29 | string csdl = File.ReadAllText(csdlFile); 30 | var root = XElement.Parse(csdl); 31 | if (root.Name.LocalName.Equals("schema", StringComparison.OrdinalIgnoreCase)) 32 | { 33 | // This is an unwrapped CSDL file - user needs to top and tail it with standard EDMX nodes for the CSDL reader. 34 | Console.WriteLine("CSDL file is missing standard Edmx and Edmx:DataServices wrapper nodes."); 35 | return 1; 36 | } 37 | 38 | var convertor = new PlantConverter(); 39 | string plantUml = convertor.EmitPlantDiagram(csdl, csdlFile); 40 | if (!args.SvgModel) 41 | { 42 | if (args.Output == null) 43 | { 44 | Console.WriteLine(plantUml); 45 | } 46 | else 47 | { 48 | await File.WriteAllTextAsync(args.Output, plantUml, System.Text.Encoding.UTF8); 49 | } 50 | } 51 | else 52 | { 53 | byte[] svgBytesInUtf8; 54 | if (args.ServerUrl == null) 55 | { 56 | svgBytesInUtf8 = await RenderSvg.RenderSvgDiagram(plantUml); 57 | } 58 | else 59 | { 60 | svgBytesInUtf8 = await RenderSvg.RenderSvgDiagram(plantUml, args.ServerUrl); 61 | } 62 | 63 | if (args.Output == null) 64 | { 65 | Console.WriteLine(UTF8Encoding.UTF8.GetString(svgBytesInUtf8)); 66 | } 67 | else 68 | { 69 | await File.WriteAllBytesAsync(args.Output, svgBytesInUtf8); 70 | } 71 | } 72 | } 73 | catch (Exception ex) 74 | { 75 | Console.WriteLine(ex.Message); 76 | } 77 | 78 | if (Debugger.IsAttached) 79 | { 80 | Console.ReadLine(); 81 | 82 | } 83 | return 0; 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Microsoft.OData.UML/GenerationError.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.OData.UML 2 | { 3 | /// 4 | /// POCO for errors during generation. 5 | /// 6 | public class GenerationError 7 | { 8 | private const string Warning = nameof(Warning); 9 | private const string Error = nameof(Error); 10 | 11 | /// Initializes a new instance of the class. 12 | public GenerationError() 13 | { 14 | } 15 | 16 | /// Initializes a new instance of the class using the specified file name, line, column, error number, and error text. 17 | /// The file name of the file that the compiler was compiling when it encountered the error. 18 | /// The line of the source of the error. 19 | /// The column of the source of the error. 20 | /// The error number of the error. 21 | /// The error message text. 22 | public GenerationError( 23 | string fileName, 24 | int line, 25 | int column, 26 | string errorNumber, 27 | string errorText) 28 | { 29 | this.FileName = fileName; 30 | this.Line = line; 31 | this.Column = column; 32 | this.ErrorNumber = errorNumber; 33 | this.ErrorText = errorText; 34 | } 35 | 36 | /// Gets or sets the column number where the source of the error occurs. 37 | /// The column number of the source file where the compiler encountered the error. 38 | public int Column { get; set; } 39 | 40 | /// Gets or sets the error number. 41 | /// The error number as a string. 42 | public string ErrorNumber { get; set; } 43 | 44 | /// Gets or sets the text of the error message. 45 | /// The text of the error message. 46 | public string ErrorText { get; set; } 47 | 48 | /// Gets or sets the file name of the source file that contains the code which caused the error. 49 | /// The file name of the source file that contains the code which caused the error. 50 | public string FileName { get; set; } 51 | 52 | /// Gets or sets a value that indicates whether the error is a warning. 53 | /// 54 | /// if the error is a warning; otherwise, . 55 | public bool IsWarning { get; set; } 56 | 57 | /// Gets or sets the line number where the source of the error occurs. 58 | /// The line number of the source file where the compiler encountered the error. 59 | public int Line { get; set; } 60 | 61 | /// Provides an implementation of Object's method. 62 | /// A string representation of the compiler error. 63 | public override string ToString() 64 | { 65 | return $"{this.FileName}:{this.Line},{this.Column} {(this.IsWarning ? Warning : Error)} {this.ErrorNumber} {this.ErrorText}"; 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /Microsoft.OData.Diagram/RenderSvg.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO.Compression; 3 | using System.IO; 4 | using System.Net.Http; 5 | using System.Net; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace Microsoft.OData.Diagram 10 | { 11 | internal static class RenderSvg 12 | { 13 | private const string defaultServerBase = "https://www.plantuml.com/plantuml"; 14 | 15 | /// 16 | /// Render an Svg diagram using public PlantUML server by default. 17 | /// 18 | /// The plantuml code to render. 19 | /// Base url of platuml renderer to use. 20 | /// 21 | /// 22 | public static async ValueTask RenderSvgDiagram(string plantUml, string? urlBase = null) 23 | { 24 | using var client = new HttpClient(); 25 | string url = $"{urlBase ?? defaultServerBase}/svg"; 26 | HttpResponseMessage response; 27 | 28 | // The default public server does not support POSTing, so we have to fall back to encoded GET 29 | if (urlBase == null) 30 | { 31 | string encodedDiagram = CreateEncodedDiagram(plantUml); 32 | url += $"/{encodedDiagram}"; 33 | response = await SendWithRetry(() => client.GetAsync(url)); 34 | } 35 | else // Assumption is that explicitly-specified server has been configured to accept POST. 36 | { 37 | var content = new StringContent(plantUml); 38 | response = await SendWithRetry(() => client.PostAsync(url, content)); 39 | } 40 | 41 | if (response.StatusCode != HttpStatusCode.OK) 42 | { 43 | string errorMessage = $"Error rendering SVG file: status code: {response.StatusCode}"; 44 | throw new InvalidOperationException(errorMessage); 45 | } 46 | else 47 | { 48 | return await response.Content.ReadAsByteArrayAsync(); 49 | } 50 | 51 | static async Task SendWithRetry(Func> task) 52 | { 53 | HttpResponseMessage response = await task(); 54 | if (response.StatusCode == HttpStatusCode.Forbidden) 55 | { 56 | // Retry once after delay. 57 | await Task.Delay(2000); 58 | response = await task(); 59 | } 60 | 61 | return response; 62 | } 63 | } 64 | 65 | private static string CreateEncodedDiagram(string s) 66 | { 67 | var utf8 = Encoding.UTF8.GetBytes(s); 68 | using var deflatedMemStream = new MemoryStream(); 69 | using (var deflatedStream = new DeflateStream(deflatedMemStream, CompressionLevel.Optimal)) 70 | { 71 | deflatedStream.Write(utf8, 0, utf8.Length); 72 | } 73 | byte[] bytes = deflatedMemStream.ToArray(); 74 | string encodedBody = Encode64(bytes); 75 | return encodedBody; 76 | } 77 | 78 | // Custom Base64 encoding algorithm needed by PlantUML server. :-( 79 | private static string Encode64(byte[] data) 80 | { 81 | StringBuilder sb = new StringBuilder(); 82 | for (int i = 0; i < data.Length; i += 3) 83 | { 84 | if (i + 2 == data.Length) 85 | { 86 | sb.Append(Append3Bytes(Convert.ToUInt16(data[i]), Convert.ToUInt16(data[i + 1]), 0)); 87 | } 88 | else if (i + 1 == data.Length) 89 | { 90 | sb.Append(Append3Bytes(Convert.ToUInt16(data[i]), 0, 0)); 91 | } 92 | else 93 | { 94 | sb.Append(Append3Bytes(Convert.ToUInt16(data[i]), 95 | Convert.ToUInt16(data[i + 1]), 96 | Convert.ToUInt16(data[i + 2]))); 97 | } 98 | } 99 | return sb.ToString(); 100 | } 101 | 102 | private static string Append3Bytes(UInt16 b1, UInt16 b2, UInt16 b3) 103 | { 104 | UInt16 c1 = (UInt16)(b1 >> 2); 105 | UInt16 c2 = (UInt16)(((b1 & 0x3) << 4) | (b2 >> 4)); 106 | UInt16 c3 = (UInt16)(((b2 & 0xF) << 2) | (b3 >> 6)); 107 | UInt16 c4 = (UInt16)(b3 & 0x3F); 108 | var r = ""; 109 | r += Encode6Bit((UInt16)(c1 & 0x3F)); 110 | r += Encode6Bit((UInt16)(c2 & 0x3F)); 111 | r += Encode6Bit((UInt16)(c3 & 0x3F)); 112 | r += Encode6Bit((UInt16)(c4 & 0x3F)); 113 | return r; 114 | } 115 | 116 | private static char Encode6Bit(UInt16 b) 117 | { 118 | if (b < 10) 119 | { 120 | return Convert.ToChar(48 + b); 121 | } 122 | b -= 10; 123 | if (b < 26) 124 | { 125 | return Convert.ToChar(65 + b); 126 | } 127 | b -= 26; 128 | if (b < 26) 129 | { 130 | return Convert.ToChar(97 + b); 131 | } 132 | b -= 26; 133 | if (b == 0) 134 | { 135 | return '-'; 136 | } 137 | if (b == 1) 138 | { 139 | return '_'; 140 | } 141 | return '?'; 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /Microsoft.OData.UML/CodeGeneratorBase.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.OData.UML 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | 7 | /// 8 | /// Base class for generating well-formatted code. 9 | /// Borrowed from T4's generated base class. 10 | /// 11 | internal class CodeGeneratorBase 12 | { 13 | private bool endsWithNewline; 14 | 15 | /// 16 | /// The string builder that generation-time code uses to assemble generated output. 17 | /// 18 | protected StringBuilder GenerationEnvironment { get; set; } = new StringBuilder(); 19 | 20 | /// 21 | /// The error collection for the generation process. 22 | /// 23 | public IList Errors { get; } = new List(); 24 | 25 | /// 26 | /// A list of the lengths of each indent that were added with PushIndent. 27 | /// 28 | private List IndentLengths { get; } = new List(); 29 | 30 | /// 31 | /// Gets the current indent to use when adding lines to the output. 32 | /// 33 | public string CurrentIndent { get; private set; } = string.Empty; 34 | 35 | /// 36 | /// Write text directly into the generated output. 37 | /// 38 | public void Write(string textToAppend) 39 | { 40 | if (string.IsNullOrEmpty(textToAppend)) 41 | { 42 | return; 43 | } 44 | 45 | // If we're starting off, or if the previous text ended with a newline, 46 | // we have to append the current indent first. 47 | if (this.GenerationEnvironment.Length == 0 48 | || this.endsWithNewline) 49 | { 50 | this.GenerationEnvironment.Append(this.CurrentIndent); 51 | this.endsWithNewline = false; 52 | } 53 | 54 | // Check if the current text ends with a newline 55 | if (textToAppend.EndsWith(global::System.Environment.NewLine, 56 | global::System.StringComparison.CurrentCulture)) 57 | { 58 | this.endsWithNewline = true; 59 | } 60 | 61 | // This is an optimization. If the current indent is "", then we don't have to do any 62 | // of the more complex stuff further down. 63 | if (this.CurrentIndent.Length == 0) 64 | { 65 | this.GenerationEnvironment.Append(textToAppend); 66 | return; 67 | } 68 | 69 | // Everywhere there is a newline in the text, add an indent after it 70 | textToAppend = textToAppend.Replace(global::System.Environment.NewLine, 71 | global::System.Environment.NewLine + this.CurrentIndent); 72 | // If the text ends with a newline, then we should strip off the indent added at the very end 73 | // because the appropriate indent will be added when the next time Write() is called 74 | if (this.endsWithNewline) 75 | { 76 | this.GenerationEnvironment.Append(textToAppend, 0, textToAppend.Length - this.CurrentIndent.Length); 77 | } 78 | else 79 | { 80 | this.GenerationEnvironment.Append(textToAppend); 81 | } 82 | } 83 | 84 | /// 85 | /// Write text directly into the generated output. 86 | /// 87 | public void WriteLine(string textToAppend) 88 | { 89 | this.Write(textToAppend); 90 | this.GenerationEnvironment.AppendLine(); 91 | this.endsWithNewline = true; 92 | } 93 | 94 | /// 95 | /// Write formatted text directly into the generated output. 96 | /// 97 | public void Write(string format, params object[] args) 98 | { 99 | this.Write(string.Format(System.Globalization.CultureInfo.CurrentCulture, format, args)); 100 | } 101 | 102 | /// 103 | /// Write formatted text directly into the generated output. 104 | /// 105 | public void WriteLine(string format, params object[] args) 106 | { 107 | this.WriteLine(string.Format(System.Globalization.CultureInfo.CurrentCulture, format, args)); 108 | } 109 | 110 | /// 111 | /// Raise an error. 112 | /// 113 | public void Error(string message) 114 | { 115 | var error = new GenerationError {ErrorText = message}; 116 | this.Errors.Add(error); 117 | } 118 | 119 | /// 120 | /// Raise a warning. 121 | /// 122 | public void Warning(string message) 123 | { 124 | var error = new GenerationError 125 | { 126 | ErrorText = message, 127 | IsWarning = true 128 | }; 129 | this.Errors.Add(error); 130 | } 131 | 132 | /// 133 | /// Increase the indent. 134 | /// 135 | public void PushIndent(string indent) 136 | { 137 | this.CurrentIndent += indent ?? throw new ArgumentNullException(nameof(indent)); 138 | this.IndentLengths.Add(indent.Length); 139 | } 140 | 141 | /// 142 | /// Remove the last indent that was added with PushIndent. 143 | /// 144 | public string PopIndent() 145 | { 146 | string returnValue = string.Empty; 147 | if (this.IndentLengths.Count > 0) 148 | { 149 | int indentLength = this.IndentLengths[^1]; 150 | this.IndentLengths.RemoveAt(this.IndentLengths.Count - 1); 151 | if (indentLength > 0) 152 | { 153 | returnValue = this.CurrentIndent[^indentLength..]; 154 | this.CurrentIndent = this.CurrentIndent.Remove(this.CurrentIndent.Length - indentLength); 155 | } 156 | } 157 | 158 | return returnValue; 159 | } 160 | 161 | /// 162 | /// Remove all indentation. 163 | /// 164 | public void ClearIndent() 165 | { 166 | this.IndentLengths.Clear(); 167 | this.CurrentIndent = string.Empty; 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | -------------------------------------------------------------------------------- /Microsoft.OData.UML/Range.cs: -------------------------------------------------------------------------------- 1 | namespace System 2 | { 3 | using System.Runtime.CompilerServices; 4 | 5 | // From https://gist.github.com/meziantou/177600eab9961f3296060d1b8bcd5f40 6 | // Reimplementation from src/Common/src/CoreLib/System/Index.cs and src/Common/src/CoreLib/System/Range.cs 7 | 8 | /// Represent a type can be used to index a collection either from the start or the end. 9 | /// 10 | /// Index is used by the C# compiler to support the new index syntax 11 | /// 12 | /// int[] someArray = new int[5] { 1, 2, 3, 4, 5 } ; 13 | /// int lastElement = someArray[^1]; // lastElement = 5 14 | /// 15 | /// 16 | internal readonly struct Index : IEquatable 17 | { 18 | private readonly int _value; 19 | 20 | /// Construct an Index using a value and indicating if the index is from the start or from the end. 21 | /// The index value. it has to be zero or positive number. 22 | /// Indicating if the index is from the start or from the end. 23 | /// 24 | /// If the Index constructed from the end, index value 1 means pointing at the last element and index value 0 means pointing at beyond last element. 25 | /// 26 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 27 | public Index(int value, bool fromEnd = false) 28 | { 29 | if (value < 0) 30 | { 31 | throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); 32 | } 33 | 34 | if (fromEnd) 35 | _value = ~value; 36 | else 37 | _value = value; 38 | } 39 | 40 | // The following private constructors mainly created for perf reason to avoid the checks 41 | private Index(int value) 42 | { 43 | _value = value; 44 | } 45 | 46 | /// Create an Index pointing at first element. 47 | public static Index Start => new Index(0); 48 | 49 | /// Create an Index pointing at beyond last element. 50 | public static Index End => new Index(~0); 51 | 52 | /// Create an Index from the start at the position indicated by the value. 53 | /// The index value from the start. 54 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 55 | public static Index FromStart(int value) 56 | { 57 | if (value < 0) 58 | { 59 | throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); 60 | } 61 | 62 | return new Index(value); 63 | } 64 | 65 | /// Create an Index from the end at the position indicated by the value. 66 | /// The index value from the end. 67 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 68 | public static Index FromEnd(int value) 69 | { 70 | if (value < 0) 71 | { 72 | throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); 73 | } 74 | 75 | return new Index(~value); 76 | } 77 | 78 | /// Returns the index value. 79 | public int Value 80 | { 81 | get 82 | { 83 | if (_value < 0) 84 | { 85 | return ~_value; 86 | } 87 | else 88 | { 89 | return _value; 90 | } 91 | } 92 | } 93 | 94 | /// Indicates whether the index is from the start or the end. 95 | public bool IsFromEnd => _value < 0; 96 | 97 | /// Calculate the offset from the start using the giving collection length. 98 | /// The length of the collection that the Index will be used with. length has to be a positive value 99 | /// 100 | /// For performance reason, we don't validate the input length parameter and the returned offset value against negative values. 101 | /// we don't validate either the returned offset is greater than the input length. 102 | /// It is expected Index will be used with collections which always have non negative length/count. If the returned offset is negative and 103 | /// then used to index a collection will get out of range exception which will be same affect as the validation. 104 | /// 105 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 106 | public int GetOffset(int length) 107 | { 108 | var offset = _value; 109 | if (IsFromEnd) 110 | { 111 | // offset = length - (~value) 112 | // offset = length + (~(~value) + 1) 113 | // offset = length + value + 1 114 | 115 | offset += length + 1; 116 | } 117 | return offset; 118 | } 119 | 120 | /// Indicates whether the current Index object is equal to another object of the same type. 121 | /// An object to compare with this object 122 | public override bool Equals(object value) => value is Index index && _value == index._value; 123 | 124 | /// Indicates whether the current Index object is equal to another Index object. 125 | /// An object to compare with this object 126 | public bool Equals(Index other) => _value == other._value; 127 | 128 | /// Returns the hash code for this instance. 129 | public override int GetHashCode() => _value; 130 | 131 | /// Converts integer number to an Index. 132 | public static implicit operator Index(int value) => FromStart(value); 133 | 134 | /// Converts the value of the current Index object to its equivalent string representation. 135 | public override string ToString() 136 | { 137 | if (IsFromEnd) 138 | return "^" + ((uint)Value).ToString(); 139 | 140 | return ((uint)Value).ToString(); 141 | } 142 | } 143 | 144 | /// Represent a range has start and end indexes. 145 | /// 146 | /// Range is used by the C# compiler to support the range syntax. 147 | /// 148 | /// int[] someArray = new int[5] { 1, 2, 3, 4, 5 }; 149 | /// int[] subArray1 = someArray[0..2]; // { 1, 2 } 150 | /// int[] subArray2 = someArray[1..^0]; // { 2, 3, 4, 5 } 151 | /// 152 | /// 153 | internal readonly struct Range : IEquatable 154 | { 155 | /// Represent the inclusive start index of the Range. 156 | public Index Start { get; } 157 | 158 | /// Represent the exclusive end index of the Range. 159 | public Index End { get; } 160 | 161 | /// Construct a Range object using the start and end indexes. 162 | /// Represent the inclusive start index of the range. 163 | /// Represent the exclusive end index of the range. 164 | public Range(Index start, Index end) 165 | { 166 | Start = start; 167 | End = end; 168 | } 169 | 170 | /// Indicates whether the current Range object is equal to another object of the same type. 171 | /// An object to compare with this object 172 | public override bool Equals(object value) => 173 | value is Range r && 174 | r.Start.Equals(Start) && 175 | r.End.Equals(End); 176 | 177 | /// Indicates whether the current Range object is equal to another Range object. 178 | /// An object to compare with this object 179 | public bool Equals(Range other) => other.Start.Equals(Start) && other.End.Equals(End); 180 | 181 | /// Returns the hash code for this instance. 182 | public override int GetHashCode() 183 | { 184 | return Start.GetHashCode() * 31 + End.GetHashCode(); 185 | } 186 | 187 | /// Converts the value of the current Range object to its equivalent string representation. 188 | public override string ToString() 189 | { 190 | return Start + ".." + End; 191 | } 192 | 193 | /// Create a Range object starting from start index to the end of the collection. 194 | public static Range StartAt(Index start) => new Range(start, Index.End); 195 | 196 | /// Create a Range object starting from first element in the collection to the end Index. 197 | public static Range EndAt(Index end) => new Range(Index.Start, end); 198 | 199 | /// Create a Range object starting from first element to the end. 200 | public static Range All => new Range(Index.Start, Index.End); 201 | 202 | /// Calculate the start offset and length of range object using a collection length. 203 | /// The length of the collection that the range will be used with. length has to be a positive value. 204 | /// 205 | /// For performance reason, we don't validate the input length parameter against negative values. 206 | /// It is expected Range will be used with collections which always have non negative length/count. 207 | /// We validate the range is inside the length scope though. 208 | /// 209 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 210 | public (int Offset, int Length) GetOffsetAndLength(int length) 211 | { 212 | int start; 213 | var startIndex = Start; 214 | if (startIndex.IsFromEnd) 215 | start = length - startIndex.Value; 216 | else 217 | start = startIndex.Value; 218 | 219 | int end; 220 | var endIndex = End; 221 | if (endIndex.IsFromEnd) 222 | end = length - endIndex.Value; 223 | else 224 | end = endIndex.Value; 225 | 226 | if ((uint)end > (uint)length || (uint)start > (uint)end) 227 | { 228 | throw new ArgumentOutOfRangeException(nameof(length)); 229 | } 230 | 231 | return (start, end - start); 232 | } 233 | } 234 | } 235 | 236 | namespace System.Runtime.CompilerServices 237 | { 238 | internal static class RuntimeHelpers 239 | { 240 | /// 241 | /// Slices the specified array using the specified range. 242 | /// 243 | public static T[] GetSubArray(T[] array, Range range) 244 | { 245 | if (array == null) 246 | { 247 | throw new ArgumentNullException(nameof(array)); 248 | } 249 | 250 | (int offset, int length) = range.GetOffsetAndLength(array.Length); 251 | 252 | if (default(T) != null || typeof(T[]) == array.GetType()) 253 | { 254 | // We know the type of the array to be exactly T[]. 255 | 256 | if (length == 0) 257 | { 258 | return Array.Empty(); 259 | } 260 | 261 | var dest = new T[length]; 262 | Array.Copy(array, offset, dest, 0, length); 263 | return dest; 264 | } 265 | else 266 | { 267 | // The array is actually a U[] where U:T. 268 | var dest = (T[])Array.CreateInstance(array.GetType().GetElementType(), length); 269 | Array.Copy(array, offset, dest, 0, length); 270 | return dest; 271 | } 272 | } 273 | } 274 | } -------------------------------------------------------------------------------- /.azure-pipelines/ci-build.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | 4 | name: $(BuildDefinitionName)_$(SourceBranchName)_$(Date:yyyyMMdd)$(Rev:.r) 5 | 6 | trigger: 7 | branches: 8 | include: 9 | - main 10 | pr: 11 | branches: 12 | include: 13 | - main 14 | 15 | pool: 16 | name: Azure Pipelines 17 | vmImage: windows-latest 18 | 19 | variables: 20 | buildPlatform: 'Any CPU' 21 | buildConfiguration: 'Release' 22 | ProductBinPath: '$(Build.SourcesDirectory)\Microsoft.OData.UML\bin\$(BuildConfiguration)' 23 | 24 | 25 | stages: 26 | 27 | - stage: build 28 | jobs: 29 | - job: build 30 | steps: 31 | - task: UseDotNet@2 32 | displayName: 'Use .NET 6' 33 | inputs: 34 | version: 6.x 35 | 36 | - task: PoliCheck@1 37 | displayName: 'Run PoliCheck' 38 | inputs: 39 | inputType: CmdLine 40 | cmdLineArgs: '/F:$(Build.SourcesDirectory) /T:9 /Sev:"1|2" /PE:2 /O:poli_result.xml' 41 | 42 | # Install the nuget tool. 43 | - task: NuGetToolInstaller@0 44 | displayName: 'Use NuGet >=5.2.0' 45 | inputs: 46 | versionSpec: '>=5.2.0' 47 | checkLatest: true 48 | 49 | # Build the Product project 50 | - task: DotNetCoreCLI@2 51 | displayName: 'build' 52 | inputs: 53 | projects: '$(Build.SourcesDirectory)\Microsoft.OData.Diagram.sln' 54 | arguments: '--configuration $(BuildConfiguration) --no-incremental' 55 | 56 | # Run the Unit test 57 | - task: DotNetCoreCLI@2 58 | displayName: 'test' 59 | inputs: 60 | command: test 61 | projects: '$(Build.SourcesDirectory)\Microsoft.OData.Diagram.sln' 62 | arguments: '--configuration $(BuildConfiguration) --no-build' 63 | 64 | # CredScan 65 | - task: securedevelopmentteam.vss-secure-development-tools.build-task-credscan.CredScan@2 66 | displayName: 'Run CredScan' 67 | inputs: 68 | toolMajorVersion: 'V2' 69 | scanFolder: '$(Build.SourcesDirectory)' 70 | debugMode: false 71 | 72 | - task: AntiMalware@3 73 | displayName: 'Run MpCmdRun.exe - ProductBinPath' 74 | inputs: 75 | FileDirPath: '$(ProductBinPath)' 76 | enabled: false 77 | 78 | - task: BinSkim@3 79 | displayName: 'Run BinSkim - Product Binaries' 80 | inputs: 81 | InputType: Basic 82 | AnalyzeTarget: '$(ProductBinPath)\**\Microsoft.OData.UML.dll' 83 | AnalyzeSymPath: '$(ProductBinPath)' 84 | AnalyzeVerbose: true 85 | AnalyzeHashes: true 86 | AnalyzeEnvironment: true 87 | 88 | - task: PublishSecurityAnalysisLogs@2 89 | displayName: 'Publish Security Analysis Logs' 90 | inputs: 91 | ArtifactName: SecurityLogs 92 | 93 | - task: PostAnalysis@1 94 | displayName: 'Post Analysis' 95 | inputs: 96 | BinSkim: true 97 | CredScan: true 98 | PoliCheck: true 99 | 100 | - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@5 101 | displayName: 'ESRP CodeSigning' 102 | inputs: 103 | ConnectedServiceName: 'Federated DevX ESRP Managed Identity Connection' 104 | AppRegistrationClientId: '65035b7f-7357-4f29-bf25-c5ee5c3949f8' 105 | AppRegistrationTenantId: 'cdc5aeea-15c5-4db6-b079-fcadd2505dc2' 106 | AuthAKVName: 'akv-prod-eastus' 107 | AuthCertName: 'ReferenceLibraryPrivateCert' 108 | AuthSignCertName: 'ReferencePackagePublisherCertificate' 109 | FolderPath: $(Build.SourcesDirectory) 110 | signConfigType: inlineSignParams 111 | inlineOperation: | 112 | [ 113 | { 114 | "keyCode": "CP-230012", 115 | "operationSetCode": "SigntoolSign", 116 | "parameters": [ 117 | { 118 | "parameterName": "OpusName", 119 | "parameterValue": "Microsoft" 120 | }, 121 | { 122 | "parameterName": "OpusInfo", 123 | "parameterValue": "http://www.microsoft.com" 124 | }, 125 | { 126 | "parameterName": "FileDigest", 127 | "parameterValue": "/fd \"SHA256\"" 128 | }, 129 | { 130 | "parameterName": "PageHash", 131 | "parameterValue": "/NPH" 132 | }, 133 | { 134 | "parameterName": "TimeStamp", 135 | "parameterValue": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" 136 | } 137 | ], 138 | "toolName": "sign", 139 | "toolVersion": "1.0" 140 | }, 141 | { 142 | "keyCode": "CP-230012", 143 | "operationSetCode": "SigntoolVerify", 144 | "parameters": [ ], 145 | "toolName": "sign", 146 | "toolVersion": "1.0" 147 | } 148 | ] 149 | SessionTimeout: 20 150 | 151 | # Pack 152 | - task: DotNetCoreCLI@2 153 | displayName: 'pack Csdl to UML Library' 154 | inputs: 155 | command: pack 156 | projects: Microsoft.OData.UML/Microsoft.OData.UML.csproj 157 | arguments: '-o $(Build.ArtifactStagingDirectory) --configuration $(BuildConfiguration) --no-build --include-symbols --include-source /p:SymbolPackageFormat=snupkg' 158 | 159 | # Pack 160 | - task: DotNetCoreCLI@2 161 | displayName: 'pack CLI tool' 162 | inputs: 163 | command: pack 164 | projects: Microsoft.OData.Diagram/Microsoft.OData.Diagram.csproj 165 | arguments: '-o $(Build.ArtifactStagingDirectory) --configuration $(BuildConfiguration) --no-build --include-symbols --include-source /p:SymbolPackageFormat=snupkg' 166 | 167 | - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@5 168 | displayName: 'ESRP CodeSigning Nuget Packages' 169 | inputs: 170 | ConnectedServiceName: 'Federated DevX ESRP Managed Identity Connection' 171 | AppRegistrationClientId: '65035b7f-7357-4f29-bf25-c5ee5c3949f8' 172 | AppRegistrationTenantId: 'cdc5aeea-15c5-4db6-b079-fcadd2505dc2' 173 | AuthAKVName: 'akv-prod-eastus' 174 | AuthCertName: 'ReferenceLibraryPrivateCert' 175 | AuthSignCertName: 'ReferencePackagePublisherCertificate' 176 | FolderPath: '$(Build.ArtifactStagingDirectory)' 177 | Pattern: '*.nupkg' 178 | signConfigType: inlineSignParams 179 | inlineOperation: | 180 | [ 181 | { 182 | "keyCode": "CP-401405", 183 | "operationSetCode": "NuGetSign", 184 | "parameters": [ ], 185 | "toolName": "sign", 186 | "toolVersion": "1.0" 187 | }, 188 | { 189 | "keyCode": "CP-401405", 190 | "operationSetCode": "NuGetVerify", 191 | "parameters": [ ], 192 | "toolName": "sign", 193 | "toolVersion": "1.0" 194 | } 195 | ] 196 | SessionTimeout: 20 197 | 198 | - task: PowerShell@2 199 | displayName: "Get CLI tool version-number from .csproj" 200 | inputs: 201 | targetType: 'inline' 202 | script: | 203 | $xml = [Xml] (Get-Content .\Microsoft.OData.Diagram\Microsoft.OData.Diagram.csproj) 204 | $version = $xml.Project.PropertyGroup.Version 205 | echo $version 206 | echo "##vso[task.setvariable variable=version]$version" 207 | 208 | # publish cli tool as an .exe 209 | - task: DotNetCoreCLI@2 210 | displayName: publish CLI tool as executable 211 | inputs: 212 | command: 'publish' 213 | arguments: -c Release --runtime win-x64 -p:PublishSingleFile=true --self-contained true --output $(Build.ArtifactStagingDirectory)/Microsoft.OData.Diagram-v$(version) -p:PublishTrimmed=true 214 | projects: 'Microsoft.OData.Diagram/Microsoft.OData.Diagram.csproj' 215 | publishWebProjects: False 216 | zipAfterPublish: false 217 | 218 | - task: CopyFiles@2 219 | displayName: Prepare staging folder for upload 220 | inputs: 221 | targetFolder: $(Build.ArtifactStagingDirectory)/Nugets 222 | sourceFolder: $(Build.ArtifactStagingDirectory) 223 | content: '*.nupkg' 224 | 225 | - task: PublishBuildArtifacts@1 226 | displayName: 'Publish Artifact: Nugets' 227 | inputs: 228 | ArtifactName: Nugets 229 | PathtoPublish: '$(Build.ArtifactStagingDirectory)/Nugets' 230 | 231 | - task: PublishBuildArtifacts@1 232 | displayName: 'Publish Artifact: CLI tool' 233 | inputs: 234 | ArtifactName: Microsoft.OData.Diagram-v$(version) 235 | PathtoPublish: '$(Build.ArtifactStagingDirectory)/Microsoft.OData.Diagram-v$(version)' 236 | 237 | - stage: deploy 238 | condition: and(contains(variables['build.sourceBranch'], 'refs/heads/main'), succeeded()) 239 | dependsOn: build 240 | jobs: 241 | - deployment: deploy_cli 242 | dependsOn: [] 243 | environment: nuget-org 244 | strategy: 245 | runOnce: 246 | deploy: 247 | pool: 248 | vmImage: ubuntu-latest 249 | steps: 250 | - task: DownloadPipelineArtifact@2 251 | displayName: Download nupkg from artifacts 252 | inputs: 253 | artifact: Nugets 254 | source: current 255 | - task: DownloadPipelineArtifact@2 256 | displayName: Download cli tool executable from artifacts 257 | inputs: 258 | source: current 259 | - pwsh: | 260 | $artifactMainDirectory = Get-ChildItem -Filter Microsoft.OData.Diagram-* -Directory -Recurse | select -First 1 261 | $artifactName = $artifactMainDirectory.Name -replace "Microsoft.OData.Diagram-", "" 262 | #Set Variable $artifactName 263 | Write-Host "##vso[task.setvariable variable=artifactName; isSecret=false; isOutput=true;]$artifactName" 264 | Write-Host "##vso[task.setvariable variable=artifactMainDirectory; isSecret=false; isOutput=true;]$artifactMainDirectory" 265 | displayName: 'Fetch Artifact Name' 266 | 267 | - task: NuGetCommand@2 268 | displayName: 'NuGet push' 269 | inputs: 270 | command: push 271 | packagesToPush: '$(Pipeline.Workspace)/Nugets/Microsoft.OData.Diagram.*.nupkg' 272 | nuGetFeedType: external 273 | publishFeedCredentials: 'microsoftgraph NuGet connection' 274 | - task: GitHubRelease@1 275 | displayName: 'GitHub release (create)' 276 | inputs: 277 | gitHubConnection: 'Michael-Wamae' 278 | tagSource: userSpecifiedTag 279 | tag: '$(artifactName)' 280 | title: '$(artifactName)' 281 | releaseNotesSource: inline 282 | assets: '$(Pipeline.Workspace)\**\*.exe' 283 | changeLogType: issueBased 284 | 285 | - deployment: deploy_lib 286 | dependsOn: [] 287 | environment: nuget-org 288 | strategy: 289 | runOnce: 290 | deploy: 291 | pool: 292 | vmImage: ubuntu-latest 293 | steps: 294 | - task: DownloadPipelineArtifact@2 295 | displayName: Download nupkg from artifacts 296 | inputs: 297 | artifact: Nugets 298 | source: current 299 | - powershell: | 300 | $fileNames = "$(Pipeline.Workspace)/Nugets/Microsoft.OData.Diagram.*.nupkg" 301 | foreach($fileName in $fileNames) { 302 | if(Test-Path $fileName) { 303 | rm $fileName -Verbose 304 | } 305 | } 306 | displayName: remove other nupkgs to avoid duplication 307 | - task: NuGetCommand@2 308 | displayName: 'NuGet push' 309 | inputs: 310 | command: push 311 | packagesToPush: '$(Pipeline.Workspace)/Nugets/Microsoft.OData.UML.*.nupkg' 312 | nuGetFeedType: external 313 | publishFeedCredentials: 'microsoftgraph NuGet connection' 314 | 315 | -------------------------------------------------------------------------------- /Microsoft.OData.UML/CsdlToPlantGenerator.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Xml; 3 | 4 | namespace Microsoft.OData.UML 5 | { 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Xml.Linq; 10 | using Microsoft.OData.Edm; 11 | using Microsoft.OData.Edm.Csdl; 12 | using Microsoft.OData.Edm.Validation; 13 | 14 | internal class CsdlToPlantGenerator : CodeGeneratorBase 15 | { 16 | private const string CollectionPrefix = "Collection("; 17 | private IEdmModel model; 18 | private string theFilename; 19 | private bool usesNamespaces; 20 | 21 | /// 22 | /// Dictionary of (namespace, name) to list of notes, sorted on the namespace. 23 | /// 24 | private readonly SortedDictionary<(string theNamespace, string theName), IEnumerable> 25 | noteMap = new SortedDictionary<(string theNamespace, string theName), IEnumerable>(); 26 | 27 | private readonly Dictionary> boundOperations = 28 | new Dictionary>(); 29 | 30 | private GeneratorOptions options; 31 | 32 | private readonly HashSet entitiesToEmit = new HashSet(); 33 | private readonly HashSet complexToEmit = new HashSet(); 34 | private readonly HashSet enumsToEmit = new HashSet(); 35 | 36 | private readonly HashSet emittedEntities = new HashSet(); 37 | private readonly HashSet emittedComplex = new HashSet(); 38 | private readonly HashSet emittedEnums = new HashSet(); 39 | 40 | public void EmitPlantDiagram(string csdl, string filename, GeneratorOptions options) 41 | { 42 | this.options = options; 43 | var parsed = XElement.Parse(csdl); 44 | this.ConstructNotesLookaside(parsed); 45 | this.model = this.LoadModel(filename, parsed); 46 | if (this.model == null) 47 | { 48 | return; 49 | } 50 | 51 | this.CalculateNamespaceUsage(); 52 | this.theFilename = Path.GetFileName(filename); 53 | this.WriteLine(@"@startuml"); 54 | this.WriteLine(@"skinparam classAttributeIconSize 0"); 55 | this.WriteLine($@"title API Entity Diagram for {this.theFilename}"); 56 | this.WriteLine(""); 57 | 58 | // This may populate the sets with types referenced in the container. 59 | this.EmitEntityContainer(); 60 | 61 | this.WriteLine(""); 62 | this.CollateBoundOperations(); 63 | 64 | // Put top-level elements onto the processing sets. 65 | this.entitiesToEmit.UnionWith(this.FilterSkipped(this.model.SchemaElements.OfType())); 66 | this.complexToEmit.UnionWith(this.FilterSkipped(this.model.SchemaElements.OfType())); 67 | this.enumsToEmit.UnionWith(this.FilterSkipped(this.model.SchemaElements.OfType())); 68 | 69 | // Keep spitting out types until nothing new has been introduced. 70 | bool anyEmitted; 71 | do 72 | { 73 | // Any types emitted on this iteration. 74 | anyEmitted = false; 75 | 76 | // Now walk the sets - these could dynamically get more added during processing so use a RemoveFirst rather than an enumerator. 77 | for (var entity = this.RemoveFirst(this.entitiesToEmit); 78 | entity != null; 79 | entity = this.RemoveFirst(this.entitiesToEmit)) 80 | { 81 | this.EmitStructuralType(entity, "entity"); 82 | this.EmitNavigationProperties(entity); 83 | this.WriteLine(""); 84 | this.emittedEntities.Add(entity); 85 | anyEmitted = true; 86 | 87 | // Also need to emit any types derived from this type. 88 | this.AddDerivedStructuredTypes(entity); 89 | } 90 | 91 | for (var complex = this.RemoveFirst(this.complexToEmit); 92 | complex != null; 93 | complex = this.RemoveFirst(this.complexToEmit)) 94 | { 95 | this.EmitStructuralType(complex, "complexType"); 96 | this.WriteLine(""); 97 | this.emittedComplex.Add(complex); 98 | anyEmitted = true; 99 | 100 | // Also need to emit any types derived from this type. 101 | this.AddDerivedStructuredTypes(complex); 102 | } 103 | 104 | for (var enumeration = this.RemoveFirst(this.enumsToEmit); 105 | enumeration != null; 106 | enumeration = this.RemoveFirst(this.enumsToEmit)) 107 | { 108 | this.EmitEnumType(enumeration); 109 | this.WriteLine(""); 110 | this.emittedEnums.Add(enumeration); 111 | anyEmitted = true; 112 | } 113 | 114 | } while (anyEmitted); 115 | 116 | this.EmitNotes(); 117 | this.WriteLine(@"@enduml"); 118 | } 119 | 120 | private IEdmModel LoadModel(string filename, XElement parsed) 121 | { 122 | IEdmModel theModel = null; 123 | try 124 | { 125 | var directory = Path.GetDirectoryName(filename); 126 | using XmlReader mainReader = parsed.CreateReader(); 127 | theModel = CsdlReader.Parse(mainReader, u => 128 | { 129 | if (string.IsNullOrEmpty(directory)) 130 | { 131 | this.Error($"No directory to resolve referenced model."); 132 | return null; 133 | } 134 | 135 | // Currently only support relative paths 136 | if (u.IsAbsoluteUri) 137 | { 138 | this.Error($"Referenced model must use relative URIs."); 139 | return null; 140 | } 141 | 142 | var file = Path.Combine(directory, u.OriginalString); 143 | string referenceText = File.ReadAllText(file); 144 | var referenceParsed = XElement.Parse(referenceText); 145 | this.ConstructNotesLookaside(referenceParsed); 146 | XmlReader referenceReader = referenceParsed.CreateReader(); 147 | return referenceReader; 148 | }); 149 | } 150 | catch (EdmParseException parseException) 151 | { 152 | this.Error("Failed to parse the CSDL file."); 153 | this.Error(string.Join(Environment.NewLine, parseException.Errors.Select(e => e.ToString()))); 154 | throw; 155 | } 156 | 157 | if (theModel == null) 158 | { 159 | this.Error("Failed to load the CSDL file."); 160 | } 161 | 162 | return theModel; 163 | } 164 | 165 | private void AddDerivedStructuredTypes(IEdmStructuredType structured) 166 | { 167 | foreach (var derived in this.model.FindAllDerivedTypes(structured)) 168 | { 169 | if (derived is IEdmEntityType derivedEntity) 170 | { 171 | this.AddEntityToEmit(derivedEntity); 172 | } 173 | else 174 | { 175 | this.AddComplexToEmit(derived as IEdmComplexType); 176 | } 177 | } 178 | } 179 | 180 | private void AddEntityToEmit(IEdmEntityType type) 181 | { 182 | this.AddTypeToEmit(type, this.entitiesToEmit, this.emittedEntities); 183 | } 184 | 185 | private void AddComplexToEmit(IEdmComplexType type) 186 | { 187 | this.AddTypeToEmit(type, this.complexToEmit, this.emittedComplex); 188 | } 189 | 190 | private void AddEnumToEmit(IEdmEnumType type) 191 | { 192 | this.AddTypeToEmit(type, this.enumsToEmit, this.emittedEnums); 193 | } 194 | 195 | private void AddTypeToEmit(T type, HashSet toEmitCollection, HashSet emittedCollection) 196 | where T : IEdmSchemaType 197 | { 198 | var positive = this.FilterSkipped(Enumerable.Repeat(type, 1)); 199 | type = positive.SingleOrDefault(); 200 | if (type != null 201 | && !emittedCollection.Contains(type) 202 | && !IsUnresolved(type)) 203 | { 204 | toEmitCollection.UnionWith(positive); 205 | } 206 | } 207 | 208 | /// 209 | /// Take the first item out of a HashSet 210 | /// 211 | private T RemoveFirst(HashSet collection) where T : IEdmType 212 | { 213 | var first = collection.FirstOrDefault(); 214 | if (first != null) 215 | { 216 | collection.Remove(first); 217 | } 218 | return first; 219 | } 220 | 221 | private void CalculateNamespaceUsage() 222 | { 223 | // If this model has non-normative references that will be chased down, 224 | // we have to use namespaces, as we can't calculate in advance. 225 | if (this.model.GetEdmReferences().Any(r => !r.Uri.IsAbsoluteUri)) 226 | { 227 | this.usesNamespaces = true; 228 | return; 229 | } 230 | 231 | int count = this.model.DeclaredNamespaces.Count(); 232 | string firstNamespace = this.model.DeclaredNamespaces.First(); 233 | this.usesNamespaces = count > 1 || 234 | !firstNamespace.Equals("microsoft.graph", StringComparison.OrdinalIgnoreCase) && 235 | firstNamespace.StartsWith("microsoft.graph.", StringComparison.OrdinalIgnoreCase); 236 | } 237 | 238 | /// 239 | /// Return elements that are not in the options SkipList. 240 | /// 241 | private IEnumerable FilterSkipped(IEnumerable unfilteredList) 242 | where T: IEdmNamedElement 243 | { 244 | return unfilteredList.Where(t => !this.options.SkipList.Contains(t.Name, StringComparer.OrdinalIgnoreCase)); 245 | } 246 | 247 | private void EmitStructuralType( 248 | IEdmStructuredType theType, 249 | string prototype) 250 | { 251 | var props = new List(); 252 | var complexUsages = new List(); 253 | IEdmStructuredType baseType = theType.BaseType; 254 | if (this.options.SkipList.Contains("entity", StringComparer.OrdinalIgnoreCase)) 255 | { 256 | if (theType is IEdmEntityType && 257 | (baseType == null || 258 | ((IEdmNamedElement)baseType).Name.Equals("entity", StringComparison.OrdinalIgnoreCase))) 259 | { 260 | // Add fake id property because everything is originally derived from Graph's 'Entity' base type which would clutter the diagram. 261 | props.Add("+id: String"); 262 | } 263 | } 264 | 265 | foreach (var property in theType.DeclaredProperties) 266 | { 267 | var typeName = this.GetTypeName(property.Type); 268 | var fullTypeName = typeName; 269 | if (GetNamespace(typeName).Equals(GetNamespace(this.GetTypeName(theType)), 270 | StringComparison.OrdinalIgnoreCase)) 271 | { 272 | typeName = GetSimpleName(typeName); 273 | } 274 | 275 | // Prefix properties with parentheses in them to avoid them being interpreted as methods. 276 | var isCollection = property.Type.Definition is IEdmCollectionType; 277 | var prefix = isCollection ? "{field} " : string.Empty; 278 | 279 | var optionality = CalculatePropertyCardinalityText(property, isCollection, out var cardinalityMin, out var cardinalityMax); 280 | if (!string.IsNullOrWhiteSpace(optionality)) 281 | { 282 | optionality = $" [{optionality}]"; 283 | } 284 | 285 | var navType = string.Empty; 286 | var exposure = "+"; 287 | if (property is IEdmNavigationProperty navProp) 288 | { 289 | navType = navProp.ContainsTarget ? "*" : navType; 290 | // Nav props don't show up by default. 291 | exposure = "-"; 292 | } 293 | 294 | props.Add($"{prefix}{exposure}{property.Name}: {typeName}{optionality}{navType}"); 295 | 296 | IEdmType propFundamental = property.Type.Definition.AsElementType(); 297 | if (propFundamental.TypeKind == EdmTypeKind.Complex || 298 | propFundamental.TypeKind == EdmTypeKind.Enum) 299 | { 300 | if (propFundamental.TypeKind == EdmTypeKind.Complex) 301 | { 302 | this.AddComplexToEmit(propFundamental as IEdmComplexType); 303 | } 304 | else 305 | { 306 | this.AddEnumToEmit(propFundamental as IEdmEnumType); 307 | } 308 | 309 | string basePropType = StripCollection(fullTypeName); 310 | complexUsages.Add( 311 | $@"{this.GetTypeName(theType)} +--> ""[{cardinalityMin}..{cardinalityMax}]"" {basePropType}: {property.Name}"); 312 | } 313 | } 314 | 315 | var isAbstract = theType.IsAbstract ? "abstract " : string.Empty; 316 | if (prototype.Equals("entity") && !theType.IsAbstract) 317 | { 318 | prototype = $"(N,white){prototype}"; 319 | } 320 | 321 | var extends = string.Empty; 322 | if (baseType != null && !this.options.SkipList.Contains(((IEdmNamedElement)baseType).Name, StringComparer.OrdinalIgnoreCase)) 323 | { 324 | extends = $" extends {this.GetTypeName(baseType)}"; 325 | if (baseType is IEdmEntityType baseEntity) 326 | { 327 | this.AddEntityToEmit(baseEntity); 328 | } 329 | else 330 | { 331 | this.AddComplexToEmit(baseType as IEdmComplexType); 332 | } 333 | } 334 | 335 | this.WriteLine( 336 | $"{isAbstract}class {this.GetTypeName(theType)} <<{prototype}>> {GetTypeColor(theType)}{extends} {{"); 337 | 338 | foreach (var prop in props) 339 | { 340 | this.WriteLine(prop); 341 | } 342 | 343 | if (this.boundOperations.TryGetValue(theType, out IList list)) 344 | { 345 | foreach (var boundOperation in list) 346 | { 347 | string returnType = string.Empty; 348 | if (boundOperation is IEdmFunction function) 349 | { 350 | returnType = $": {this.GetTypeName(function.ReturnType)}"; 351 | } 352 | 353 | this.WriteLine($"+{boundOperation.Name}(){returnType}"); 354 | } 355 | } 356 | 357 | this.WriteLine("}"); 358 | 359 | foreach (var usage in complexUsages) 360 | { 361 | this.WriteLine(usage); 362 | } 363 | } 364 | 365 | private static string CalculatePropertyCardinalityText( 366 | IEdmProperty property, 367 | bool isCollection, 368 | out string cardinalityMin, 369 | out string cardinalityMax) 370 | { 371 | var optionality = string.Empty; 372 | cardinalityMin = "1"; 373 | cardinalityMax = isCollection ? "*" : "1"; 374 | if (property.Type.IsNullable) 375 | { 376 | // Optionality only specified on property names if they are optional. 377 | cardinalityMin = "0"; 378 | optionality = $"{cardinalityMin}..{cardinalityMax}"; 379 | } 380 | 381 | return optionality; 382 | } 383 | 384 | private void ConstructNotesLookaside(XElement root) 385 | { 386 | static IEnumerable extractComments(IEnumerable childNodes) 387 | { 388 | return from c in childNodes.OfType() 389 | where c.Value.Trim().StartsWith("Note:", StringComparison.OrdinalIgnoreCase) 390 | select c.Value.Trim(); 391 | } 392 | 393 | var commentedEntities = from s in Enumerable.Repeat(root, 1).DescendantsAndSelf() 394 | where s.Name.LocalName == "Schema" 395 | let n = s.Attributes().First(a => a.Name == "Namespace").Value 396 | from e in Enumerable.Repeat(s, 1).DescendantsAndSelf() 397 | where e.Name.LocalName == "EntityType" || 398 | e.Name.LocalName == "ComplexType" || 399 | e.Name.LocalName == "EnumType" 400 | let comments = extractComments(e.DescendantNodes()) 401 | where comments.Any() 402 | select new {Namespace = n, Entity = e, Comments = comments}; 403 | 404 | foreach (var commentedEntity in commentedEntities) 405 | { 406 | this.noteMap[(commentedEntity.Namespace, commentedEntity.Entity.Attributes().First(a => a.Name == "Name").Value)] = 407 | commentedEntity.Comments; 408 | } 409 | 410 | var rootComments = from s in Enumerable.Repeat(root, 1).DescendantsAndSelf() 411 | where s.Name.LocalName == "Schema" 412 | let n = s.Attributes().First(a => a.Name == "Namespace").Value 413 | let comments = extractComments(s.Nodes()) 414 | from comment in comments 415 | select new {Namespace = n, Comments = comments}; 416 | 417 | foreach (var rootComment in rootComments) 418 | { 419 | this.noteMap[(rootComment.Namespace, string.Empty)] = rootComment.Comments; 420 | } 421 | } 422 | 423 | private void EmitEnumType(IEdmEnumType theType) 424 | { 425 | this.WriteLine($"enum {this.GetTypeName(theType)} <> #GoldenRod {{"); 426 | foreach (IEdmEnumMember member in theType.Members) 427 | { 428 | this.WriteLine($"{member.Name}"); 429 | } 430 | this.WriteLine("}"); 431 | } 432 | 433 | private void EmitNotes() 434 | { 435 | string theNamespace = string.Empty; 436 | foreach (var note in this.noteMap) 437 | { 438 | if (this.usesNamespaces && !theNamespace.Equals(note.Key.theNamespace)) 439 | { 440 | if (theNamespace != string.Empty) 441 | { 442 | this.WriteLine("}"); 443 | } 444 | 445 | theNamespace = note.Key.theNamespace; 446 | this.WriteLine($"namespace {theNamespace} {{"); 447 | } 448 | 449 | this.EmitNote(note.Key.theName, note.Value); 450 | } 451 | 452 | if (this.usesNamespaces && this.noteMap.Any()) 453 | { 454 | this.WriteLine("}"); 455 | } 456 | } 457 | 458 | private void EmitNote(string noteTarget, IEnumerable notes) 459 | { 460 | this.WriteLine(string.IsNullOrWhiteSpace(noteTarget) ? "note as RootNoteR1" : $"note top of {noteTarget}"); 461 | 462 | foreach (string note in notes) 463 | { 464 | this.WriteLine(note); 465 | } 466 | this.WriteLine("end note"); 467 | } 468 | 469 | private void EmitNavigationProperties(IEdmEntityType entity) 470 | { 471 | foreach (IEdmNavigationProperty navProp in entity.DeclaredProperties.OfType()) 472 | { 473 | 474 | var target = navProp.Type.Definition; 475 | if (target is IEdmNamedElement namedTarget && 476 | this.options.SkipList.Contains(namedTarget.Name, StringComparer.OrdinalIgnoreCase)) 477 | { 478 | continue; 479 | } 480 | 481 | if (IsUnresolved(navProp.Type.Definition)) 482 | { 483 | continue; 484 | } 485 | 486 | var entityTarget = target as IEdmEntityType; 487 | bool isCollection = false; 488 | if (target is IEdmCollectionType collectionTarget) 489 | { 490 | isCollection = true; 491 | entityTarget = collectionTarget.ElementType.Definition as IEdmEntityType; 492 | } 493 | this.AddEntityToEmit(entityTarget); 494 | 495 | var navType = navProp.ContainsTarget ? "*" : string.Empty; 496 | CalculatePropertyCardinalityText(navProp, isCollection, out var cardinalityMin, out var cardinalityMax); 497 | this.WriteLine( 498 | $@"{this.GetTypeName(entity)} {navType}--> ""{cardinalityMin}..{cardinalityMax}"" {this.GetTypeName(entityTarget)}: {navProp.Name}"); 499 | } 500 | 501 | // TODO: Emit a (custom?) nav line for types refered to in operation parameters. 502 | } 503 | 504 | private void EmitEntityContainer() 505 | { 506 | if (this.model.EntityContainer == null) 507 | { 508 | return; 509 | } 510 | 511 | var members = new List(); 512 | string ecName = this.StripNamespace(this.model.EntityContainer.FullName()); 513 | foreach (var singleton in this.model.EntityContainer.Elements.OfType()) 514 | { 515 | IEdmEntityType entityType = singleton.EntityType(); 516 | var singletonTypeName = this.GetTypeName(entityType); 517 | members.Add($"+{singleton.Name}: {singletonTypeName}"); 518 | this.WriteLine($@"{ecName} .. ""1..1"" {singletonTypeName}: {singleton.Name}"); 519 | this.AddEntityToEmit(entityType); 520 | } 521 | 522 | foreach (var entitySet in this.model.EntityContainer.Elements.OfType()) 523 | { 524 | IEdmEntityType entityType = entitySet.EntityType(); 525 | var entitySetTypeName = this.GetTypeName(entityType); 526 | members.Add($"+{entitySet.Name}: {entitySetTypeName}"); 527 | this.WriteLine( 528 | $@"{ecName} .. ""0..*"" {StripCollection(entitySetTypeName)}: {entitySet.Name}"); 529 | this.AddEntityToEmit(entityType); 530 | } 531 | 532 | this.WriteLine($"class {ecName} <<(S,white)entityContainer>> #LightPink {{"); 533 | foreach (var member in members) 534 | { 535 | this.WriteLine(member); 536 | } 537 | this.WriteLine("}"); 538 | } 539 | 540 | private void CollateBoundOperations() 541 | { 542 | List allOperations = new List(); 543 | allOperations.AddRange(this.model.SchemaElements.OfType().Where(o => o.IsBound)); 544 | foreach (IEdmModel refModel in this.model.ReferencedModels) 545 | { 546 | allOperations.AddRange(refModel.SchemaElements.OfType().Where(o => o.IsBound)); 547 | } 548 | 549 | // Collate the bound actions and functions against their bound elements. 550 | foreach (var operation in allOperations) 551 | { 552 | // By spec definition, first parameter is the binding parameter. 553 | if ((operation?.Parameters?.FirstOrDefault()?.Type?.Definition) is IEdmStructuredType 554 | bindingParameterType) 555 | { 556 | if (!this.boundOperations.TryGetValue(bindingParameterType, out var list)) 557 | { 558 | list = new List(); 559 | this.boundOperations[bindingParameterType] = list; 560 | } 561 | 562 | list.Add(operation); 563 | } 564 | } 565 | } 566 | private static bool IsUnresolved(IEdmType type) 567 | { 568 | type = type.AsElementType(); 569 | if (!(type is IEdmNamedElement named)) 570 | { 571 | return true; 572 | } 573 | 574 | string name = named.Name; 575 | return type.Errors().Any(e => 576 | (e.ErrorCode == EdmErrorCode.BadUnresolvedEntityType 577 | || e.ErrorCode == EdmErrorCode.BadUnresolvedComplexType 578 | || e.ErrorCode == EdmErrorCode.BadUnresolvedEnumType) && 579 | e.ErrorMessage.Contains(name)); 580 | } 581 | 582 | private static string StripCollection(string name) 583 | { 584 | if (name.Contains(CollectionPrefix)) 585 | { 586 | name = name.Replace(CollectionPrefix, string.Empty); 587 | name = name[0..^1]; 588 | } 589 | 590 | return name; 591 | } 592 | 593 | private string StripNamespace(string name) 594 | { 595 | if (name == null) return null; 596 | 597 | return this.usesNamespaces ? name : GetSimpleName(name); 598 | } 599 | 600 | private static bool IsCollection(string name) 601 | { 602 | return name.Contains(CollectionPrefix); 603 | } 604 | 605 | private static string GetNamespace(string name) 606 | { 607 | if (name == null) 608 | { 609 | return null; 610 | } 611 | else 612 | { 613 | name = StripCollection(name); 614 | return name.Contains('.') ? string.Join(".", name.Split('.')[..^2]) : string.Empty; 615 | } 616 | } 617 | 618 | private static string GetSimpleName(string name) 619 | { 620 | if (name == null) 621 | { 622 | return null; 623 | } 624 | else 625 | { 626 | bool isColl = IsCollection(name); 627 | name = StripCollection(name); 628 | name = name.Contains('.') ? name.Split('.')[^1] : name; 629 | name = isColl ? $"{CollectionPrefix}{name})" : name; 630 | return name; 631 | } 632 | } 633 | 634 | private string GetTypeName(IEdmTypeReference theType) 635 | { 636 | var name = theType.ShortQualifiedName() ?? theType.FullName(); 637 | 638 | name = this.StripNamespace(name); 639 | return name; 640 | } 641 | 642 | private string GetTypeName(IEdmType theType) 643 | { 644 | var typeName = string.Empty; 645 | switch (theType) 646 | { 647 | case IEdmComplexType complex: 648 | typeName = complex.FullTypeName(); 649 | break; 650 | case IEdmEntityType entity: 651 | typeName = entity.FullTypeName(); 652 | break; 653 | case IEdmCollectionType collection: 654 | typeName = this.GetTypeName(collection.ElementType); 655 | break; 656 | case IEdmEnumType enumeration: 657 | typeName = enumeration.FullTypeName(); 658 | break; 659 | } 660 | 661 | return this.StripNamespace(typeName); 662 | } 663 | 664 | private static string GetTypeColor(IEdmType theType) 665 | { 666 | return theType switch 667 | { 668 | IEdmComplexType _ => "#Skyblue", 669 | IEdmEntityType _ => "#PaleGreen", 670 | _ => string.Empty, 671 | }; 672 | } 673 | } 674 | } 675 | --------------------------------------------------------------------------------