From 2451749f667c28ee34b9b0a64ddb7e285ed4a5ba Mon Sep 17 00:00:00 2001 From: skovlund Date: Fri, 15 Aug 2025 12:51:39 +0200 Subject: [PATCH 1/4] Refactoring to support single file as output. --- .../Generation/CSharpProxyGenerator.cs | 122 +++- .../Generation/Common/BaseFileGenerator.cs | 22 - .../Generation/Generators/EnumGenerator.cs | 23 +- .../Generators/HelperFileGenerator.cs | 11 +- .../IntersectionInterfaceGenerator.cs | 25 +- .../Generators/ProxyClassGenerator.cs | 41 +- .../Generators/XrmContextGenerator.cs | 13 +- .../Generation/Mappers/EnumMapper.cs | 31 + .../Generation/Mappers/HelperFileMapper.cs | 15 + .../Mappers/IntersectionInterfaceMapper.cs | 31 + .../Generation/Mappers/ProxyClassMapper.cs | 48 ++ .../Generation/Mappers/XrmContextMapper.cs | 20 + .../Utilities/GenerationUtilities.cs | 27 + .../Generation/XrmGenerationConfig.cs | 4 +- .../Templates/ProxyClass.scriban-cs | 7 +- .../Templates/SingleFile.scriban-cs | 529 ++++++++++++++++++ .../Templates/XrmClass.scriban-cs | 4 +- .../CommandLineParser.cs | 11 +- .../SimpleXrmContextConfigBuilder.cs | 4 +- src/DataverseProxyGenerator.Tool/Program.cs | 6 +- 20 files changed, 860 insertions(+), 134 deletions(-) create mode 100644 src/DataverseProxyGenerator.Core/Generation/Mappers/EnumMapper.cs create mode 100644 src/DataverseProxyGenerator.Core/Generation/Mappers/HelperFileMapper.cs create mode 100644 src/DataverseProxyGenerator.Core/Generation/Mappers/IntersectionInterfaceMapper.cs create mode 100644 src/DataverseProxyGenerator.Core/Generation/Mappers/ProxyClassMapper.cs create mode 100644 src/DataverseProxyGenerator.Core/Generation/Mappers/XrmContextMapper.cs create mode 100644 src/DataverseProxyGenerator.Core/Generation/Utilities/GenerationUtilities.cs create mode 100644 src/DataverseProxyGenerator.Core/Templates/SingleFile.scriban-cs diff --git a/src/DataverseProxyGenerator.Core/Generation/CSharpProxyGenerator.cs b/src/DataverseProxyGenerator.Core/Generation/CSharpProxyGenerator.cs index 569a490..f731dea 100644 --- a/src/DataverseProxyGenerator.Core/Generation/CSharpProxyGenerator.cs +++ b/src/DataverseProxyGenerator.Core/Generation/CSharpProxyGenerator.cs @@ -38,22 +38,125 @@ public IEnumerable GenerateCode(IEnumerable tables, X ArgumentNullException.ThrowIfNull(tables); ArgumentNullException.ThrowIfNull(config); - var files = new List(); - var version = GetAssemblyVersion(); + var context = CreateGenerationContext(config); + var tablesList = tables.ToList(); + var (interfaceColumns, tableToInterfaces) = PrepareIntersectionData(tablesList, config); + + if (config.SingleFile) + { + return GenerateSingleFile(tablesList, interfaceColumns, tableToInterfaces, context); + } - var context = new GenerationContext + return GenerateMultipleFiles(tablesList, interfaceColumns, tableToInterfaces, context); + } + + private GenerationContext CreateGenerationContext(XrmGenerationConfig config) + { + return new GenerationContext { Namespace = config.NamespaceSetting ?? "DataverseContext", - Version = version, + Version = GetAssemblyVersion(), Templates = templateProvider, ServiceContextName = config.ServiceContextName, IntersectMapping = config.IntersectMapping, }; + } - var tablesList = tables.ToList(); + private static (Dictionary> InterfaceColumns, Dictionary> TableToInterfaces) + PrepareIntersectionData(List tablesList, XrmGenerationConfig config) + { var tableDict = tablesList.ToDictionary(t => t.LogicalName, t => t, StringComparer.InvariantCulture); var tableColumns = BuildTableColumns(tablesList); - var (interfaceColumns, tableToInterfaces) = BuildIntersectionData(config.IntersectMapping, tableDict, tableColumns); + return BuildIntersectionData(config.IntersectMapping, tableDict, tableColumns); + } + + private IEnumerable GenerateSingleFile( + List tablesList, + Dictionary> interfaceColumns, + Dictionary> tableToInterfaces, + GenerationContext context) + { + var templateModel = CreateSingleFileTemplateModel(tablesList, interfaceColumns, tableToInterfaces, context); + + var templateName = "SingleFile.scriban-cs"; + var template = context.Templates.GetTemplate(templateName); + var content = template.Render(templateModel, member => member.Name); + + yield return new GeneratedFile($"{context.ServiceContextName}.cs", content); + } + + private static object CreateSingleFileTemplateModel( + List tablesList, + Dictionary> interfaceColumns, + Dictionary> tableToInterfaces, + GenerationContext context) + { + var globalOptionsets = GetGlobalOptionsets(tablesList).ToList(); + var interfaces = CreateInterfaceModels(interfaceColumns, tablesList); + + // Add interface lists to tables (without modifying TableModel structure) + var tablesWithInterfaces = tablesList.Select(table => + { + var tableInterfaces = tableToInterfaces.TryGetValue(table.LogicalName, out var ifaces) ? ifaces : new List(); + return new + { + table, + InterfacesList = tableInterfaces, + }; + }).ToList(); + + // Prepare the template model with correct property names + return new + { + @namespace = context.Namespace, + version = context.Version, + serviceContextName = context.ServiceContextName, + tables = tablesWithInterfaces.Select(t => new + { + t.table.SchemaName, + t.table.LogicalName, + t.table.DisplayName, + t.table.EntityTypeCode, + t.table.PrimaryNameAttribute, + t.table.PrimaryIdAttribute, + t.table.IsIntersect, + t.table.Columns, + t.table.Relationships, + InterfacesList = t.InterfacesList, + }).ToList(), + optionsets = globalOptionsets, + interfaces = interfaces, + }; + } + + private static List CreateInterfaceModels( + Dictionary> interfaceColumns, + List tablesList) + { + return interfaceColumns.Select(kvp => new + { + Name = kvp.Key, + Columns = kvp.Value.Select(sig => FindMatchingColumn(sig, tablesList)) + .Where(c => c != null) + .ToList(), + }).ToList(); + } + + private static ColumnModel? FindMatchingColumn(ColumnSignature sig, List tablesList) + { + return tablesList.SelectMany(t => t.Columns) + .FirstOrDefault(c => c.SchemaName == sig.SchemaName && + (c.TypeName == sig.TypeName || + (c is EnumColumnModel enumCol && sig.TypeName == $"EnumColumnModel:{enumCol.OptionsetName}"))); + } + + private IEnumerable GenerateMultipleFiles( + List tablesList, + Dictionary> interfaceColumns, + Dictionary> tableToInterfaces, + GenerationContext context) + { + var files = new List(); // Generate intersection interfaces foreach (var kvp in interfaceColumns) @@ -77,8 +180,8 @@ public IEnumerable GenerateCode(IEnumerable tables, X } // Generate enums - var globalOptionsets = GetGlobalOptionsets(tablesList); - foreach (var optionset in globalOptionsets) + var globalOptionsetsMulti = GetGlobalOptionsets(tablesList); + foreach (var optionset in globalOptionsetsMulti) { files.AddRange(enumGenerator.Generate(optionset, context)); } @@ -92,7 +195,8 @@ public IEnumerable GenerateCode(IEnumerable tables, X files.AddRange(helperFileGenerator.Generate("TableAttributeHelpers", context)); files.AddRange(helperFileGenerator.Generate("ExtendedEntity", context)); - return files; + foreach (var file in files) + yield return file; } private static IEnumerable GetGlobalOptionsets(IEnumerable tables) diff --git a/src/DataverseProxyGenerator.Core/Generation/Common/BaseFileGenerator.cs b/src/DataverseProxyGenerator.Core/Generation/Common/BaseFileGenerator.cs index 2fe19fb..23d5902 100644 --- a/src/DataverseProxyGenerator.Core/Generation/Common/BaseFileGenerator.cs +++ b/src/DataverseProxyGenerator.Core/Generation/Common/BaseFileGenerator.cs @@ -1,4 +1,3 @@ -using DataverseProxyGenerator.Core.Generation.Utilities; using Scriban; namespace DataverseProxyGenerator.Core.Generation.Common; @@ -44,25 +43,4 @@ protected static TemplateContext CreateTemplateContext(object model) templateContext.PushGlobal(Scriban.Runtime.ScriptObject.From(model)); return templateContext; } - - /// - /// Sanitizes a name using the shared utility. - /// - /// The name to sanitize. - /// Optional fallback prefix. - /// A sanitized name. - protected static string SanitizeName(string name, string fallbackPrefix = "Item") - { - return NameSanitizer.SanitizeName(name, fallbackPrefix); - } - - /// - /// Gets the type signature for a column using the shared utility. - /// - /// The column model. - /// The type signature. - protected static string GetTypeSignature(Domain.ColumnModel column) - { - return TypeSignatureHelper.GetPropertyTypeSignature(column); - } } \ No newline at end of file diff --git a/src/DataverseProxyGenerator.Core/Generation/Generators/EnumGenerator.cs b/src/DataverseProxyGenerator.Core/Generation/Generators/EnumGenerator.cs index 2eb2fb6..701f12e 100644 --- a/src/DataverseProxyGenerator.Core/Generation/Generators/EnumGenerator.cs +++ b/src/DataverseProxyGenerator.Core/Generation/Generators/EnumGenerator.cs @@ -1,5 +1,6 @@ using DataverseProxyGenerator.Core.Domain; using DataverseProxyGenerator.Core.Generation.Common; +using DataverseProxyGenerator.Core.Generation.Mappers; using DataverseProxyGenerator.Core.Generation.Utilities; namespace DataverseProxyGenerator.Core.Generation.Generators; @@ -17,27 +18,11 @@ private static IEnumerable GenerateInternal(EnumColumnModel input { ValidateContext(context); + var templateModel = EnumMapper.MapToTemplateModel(input, context); var template = context.Templates.GetTemplate("EnumOptionset.scriban-cs"); - var sanitizedOptionSetName = SanitizeName(input.OptionsetName, "UnknownOptionSet"); - - var enumResult = template.Render( - new - { - optionsetName = sanitizedOptionSetName, - optionsetValues = input.OptionsetValues.Select(kvp => new - { - Value = kvp.Key, - Name = NameSanitizer.SanitizeEnumOptionName(kvp.Value, kvp.Key), - Localizations = - input.OptionLocalizations != null && - input.OptionLocalizations.TryGetValue(kvp.Key, out var value) - ? value : new Dictionary(), - }), - @namespace = context.Namespace, - version = context.Version, - }, - member => member.Name); + var enumResult = template.Render(templateModel, member => member.Name); + var sanitizedOptionSetName = DataverseProxyGenerator.Core.Generation.Utilities.GenerationUtilities.SanitizeName(input.OptionsetName, "UnknownOptionSet"); yield return new GeneratedFile(FilePathHelper.GetOptionSetFilePath(sanitizedOptionSetName), enumResult); } } \ No newline at end of file diff --git a/src/DataverseProxyGenerator.Core/Generation/Generators/HelperFileGenerator.cs b/src/DataverseProxyGenerator.Core/Generation/Generators/HelperFileGenerator.cs index 0d04da7..27fdd3f 100644 --- a/src/DataverseProxyGenerator.Core/Generation/Generators/HelperFileGenerator.cs +++ b/src/DataverseProxyGenerator.Core/Generation/Generators/HelperFileGenerator.cs @@ -1,4 +1,5 @@ using DataverseProxyGenerator.Core.Generation.Common; +using DataverseProxyGenerator.Core.Generation.Mappers; using DataverseProxyGenerator.Core.Generation.Utilities; namespace DataverseProxyGenerator.Core.Generation.Generators; @@ -16,15 +17,9 @@ private static IEnumerable GenerateInternal(string templateName, { ValidateContext(context); + var templateModel = HelperFileMapper.MapToTemplateModel(templateName, context); var template = context.Templates.GetTemplate($"{templateName}.scriban-cs"); - - var result = template.Render( - new - { - @namespace = context.Namespace, - version = context.Version, - }, - member => member.Name); + var result = template.Render(templateModel, member => member.Name); yield return new GeneratedFile(FilePathHelper.GetHelperFilePath(templateName), result); } diff --git a/src/DataverseProxyGenerator.Core/Generation/Generators/IntersectionInterfaceGenerator.cs b/src/DataverseProxyGenerator.Core/Generation/Generators/IntersectionInterfaceGenerator.cs index 9b26117..86664ab 100644 --- a/src/DataverseProxyGenerator.Core/Generation/Generators/IntersectionInterfaceGenerator.cs +++ b/src/DataverseProxyGenerator.Core/Generation/Generators/IntersectionInterfaceGenerator.cs @@ -1,5 +1,6 @@ using DataverseProxyGenerator.Core.Domain; using DataverseProxyGenerator.Core.Generation.Common; +using DataverseProxyGenerator.Core.Generation.Mappers; using DataverseProxyGenerator.Core.Generation.Utilities; namespace DataverseProxyGenerator.Core.Generation.Generators; @@ -16,28 +17,12 @@ public IEnumerable Generate((string InterfaceName, IEnumerable GenerateInternal((string InterfaceName, IEnumerable Columns) input, GenerationContext context) { ValidateContext(context); - var (interfaceName, columns) = input; - var template = context.Templates.GetTemplate("IntersectionInterface.scriban-cs"); - var sanitizedInterfaceName = SanitizeName(interfaceName); - - var columnData = columns.Select(col => new - { - SchemaName = SanitizeName(col.SchemaName), - col.DisplayName, - col.Description, - TypeSignature = GetTypeSignature(col), - }); - var interfaceResult = template.Render( - new - { - interfaceName = sanitizedInterfaceName, - @namespace = context.Namespace, - columns = columnData, - version = context.Version, - }, - member => member.Name); + var templateModel = IntersectionInterfaceMapper.MapToTemplateModel(input, context); + var template = context.Templates.GetTemplate("IntersectionInterface.scriban-cs"); + var interfaceResult = template.Render(templateModel, member => member.Name); + var sanitizedInterfaceName = DataverseProxyGenerator.Core.Generation.Utilities.GenerationUtilities.SanitizeName(input.InterfaceName); yield return new GeneratedFile(FilePathHelper.GetIntersectionInterfaceFilePath(sanitizedInterfaceName), interfaceResult); } } \ No newline at end of file diff --git a/src/DataverseProxyGenerator.Core/Generation/Generators/ProxyClassGenerator.cs b/src/DataverseProxyGenerator.Core/Generation/Generators/ProxyClassGenerator.cs index 751c4b0..2e056f5 100644 --- a/src/DataverseProxyGenerator.Core/Generation/Generators/ProxyClassGenerator.cs +++ b/src/DataverseProxyGenerator.Core/Generation/Generators/ProxyClassGenerator.cs @@ -1,5 +1,6 @@ using DataverseProxyGenerator.Core.Domain; using DataverseProxyGenerator.Core.Generation.Common; +using DataverseProxyGenerator.Core.Generation.Mappers; using DataverseProxyGenerator.Core.Generation.Utilities; namespace DataverseProxyGenerator.Core.Generation.Generators; @@ -16,46 +17,14 @@ public IEnumerable Generate((TableModel Table, IReadOnlyList GenerateInternal((TableModel Table, IReadOnlyList Interfaces) input, GenerationContext context) { ValidateContext(context); - var (table, interfaces) = input; - var template = context.Templates.GetTemplate("ProxyClass.scriban-cs"); - var sanitizedSchemaName = SanitizeName(table.SchemaName); - var model = new - { - table = new - { - SchemaName = table.SchemaName, - Columns = table.Columns.Select(c => - c switch - { - EnumColumnModel enumCol => enumCol with - { - SchemaName = SanitizeName(enumCol.SchemaName), - OptionsetName = SanitizeName(enumCol.OptionsetName), - }, - _ => c with - { - SchemaName = SanitizeName(c.SchemaName), - }, - }), - Relationships = table.Relationships.Select(r => r with - { - SchemaName = SanitizeName(r.SchemaName), - }), - LogicalName = table.LogicalName, - DisplayName = table.DisplayName, - EntityTypeCode = table.EntityTypeCode, - PrimaryNameAttribute = table.PrimaryNameAttribute, - PrimaryIdAttribute = table.PrimaryIdAttribute, - IsIntersect = table.IsIntersect, - InterfacesList = interfaces ?? new List(), - }, - @namespace = context.Namespace, - version = context.Version, - }; + var model = ProxyClassMapper.MapToTemplateModel(input, context); + var template = context.Templates.GetTemplate("ProxyClass.scriban-cs"); var templateContext = CreateTemplateContext(model); var result = template.Render(templateContext); + + var sanitizedSchemaName = DataverseProxyGenerator.Core.Generation.Utilities.GenerationUtilities.SanitizeName(input.Table.SchemaName); yield return new GeneratedFile(FilePathHelper.GetTableFilePath(sanitizedSchemaName), result); } } \ No newline at end of file diff --git a/src/DataverseProxyGenerator.Core/Generation/Generators/XrmContextGenerator.cs b/src/DataverseProxyGenerator.Core/Generation/Generators/XrmContextGenerator.cs index 203687b..260f3f9 100644 --- a/src/DataverseProxyGenerator.Core/Generation/Generators/XrmContextGenerator.cs +++ b/src/DataverseProxyGenerator.Core/Generation/Generators/XrmContextGenerator.cs @@ -1,5 +1,6 @@ using DataverseProxyGenerator.Core.Domain; using DataverseProxyGenerator.Core.Generation.Common; +using DataverseProxyGenerator.Core.Generation.Mappers; using DataverseProxyGenerator.Core.Generation.Utilities; namespace DataverseProxyGenerator.Core.Generation.Generators; @@ -17,17 +18,9 @@ private static IEnumerable GenerateInternal(IEnumerable member.Name); + var xrmClassResult = template.Render(templateModel, member => member.Name); yield return new GeneratedFile(FilePathHelper.GetXrmContextFilePath(), xrmClassResult); } diff --git a/src/DataverseProxyGenerator.Core/Generation/Mappers/EnumMapper.cs b/src/DataverseProxyGenerator.Core/Generation/Mappers/EnumMapper.cs new file mode 100644 index 0000000..6e80ecb --- /dev/null +++ b/src/DataverseProxyGenerator.Core/Generation/Mappers/EnumMapper.cs @@ -0,0 +1,31 @@ +using DataverseProxyGenerator.Core.Domain; +using DataverseProxyGenerator.Core.Generation.Utilities; + +namespace DataverseProxyGenerator.Core.Generation.Mappers; + +public static class EnumMapper +{ + public static object MapToTemplateModel(EnumColumnModel input, GenerationContext context) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(input); + + var sanitizedOptionSetName = DataverseProxyGenerator.Core.Generation.Utilities.GenerationUtilities.SanitizeName(input.OptionsetName, "UnknownOptionSet"); + + return new + { + optionsetName = sanitizedOptionSetName, + optionsetValues = input.OptionsetValues.Select(kvp => new + { + Value = kvp.Key, + Name = NameSanitizer.SanitizeEnumOptionName(kvp.Value, kvp.Key), + Localizations = + input.OptionLocalizations != null && + input.OptionLocalizations.TryGetValue(kvp.Key, out var value) + ? value : new Dictionary(), + }), + @namespace = context.Namespace, + version = context.Version, + }; + } +} diff --git a/src/DataverseProxyGenerator.Core/Generation/Mappers/HelperFileMapper.cs b/src/DataverseProxyGenerator.Core/Generation/Mappers/HelperFileMapper.cs new file mode 100644 index 0000000..1fe6ff0 --- /dev/null +++ b/src/DataverseProxyGenerator.Core/Generation/Mappers/HelperFileMapper.cs @@ -0,0 +1,15 @@ +namespace DataverseProxyGenerator.Core.Generation.Mappers; + +public static class HelperFileMapper +{ + public static object MapToTemplateModel(string templateName, GenerationContext context) + { + ArgumentNullException.ThrowIfNull(context); + + return new + { + @namespace = context.Namespace, + version = context.Version, + }; + } +} diff --git a/src/DataverseProxyGenerator.Core/Generation/Mappers/IntersectionInterfaceMapper.cs b/src/DataverseProxyGenerator.Core/Generation/Mappers/IntersectionInterfaceMapper.cs new file mode 100644 index 0000000..a7aa8f7 --- /dev/null +++ b/src/DataverseProxyGenerator.Core/Generation/Mappers/IntersectionInterfaceMapper.cs @@ -0,0 +1,31 @@ +using DataverseProxyGenerator.Core.Domain; + +namespace DataverseProxyGenerator.Core.Generation.Mappers; + +public static class IntersectionInterfaceMapper +{ + public static object MapToTemplateModel((string InterfaceName, IEnumerable Columns) input, GenerationContext context) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(input.InterfaceName); + + var (interfaceName, columns) = input; + var sanitizedInterfaceName = DataverseProxyGenerator.Core.Generation.Utilities.GenerationUtilities.SanitizeName(interfaceName); + + var columnData = columns.Select(col => new + { + SchemaName = DataverseProxyGenerator.Core.Generation.Utilities.GenerationUtilities.SanitizeName(col.SchemaName), + col.DisplayName, + col.Description, + TypeSignature = DataverseProxyGenerator.Core.Generation.Utilities.GenerationUtilities.GetTypeSignature(col), + }); + + return new + { + interfaceName = sanitizedInterfaceName, + @namespace = context.Namespace, + columns = columnData, + version = context.Version, + }; + } +} diff --git a/src/DataverseProxyGenerator.Core/Generation/Mappers/ProxyClassMapper.cs b/src/DataverseProxyGenerator.Core/Generation/Mappers/ProxyClassMapper.cs new file mode 100644 index 0000000..9ce4a79 --- /dev/null +++ b/src/DataverseProxyGenerator.Core/Generation/Mappers/ProxyClassMapper.cs @@ -0,0 +1,48 @@ +using DataverseProxyGenerator.Core.Domain; + +namespace DataverseProxyGenerator.Core.Generation.Mappers; + +public static class ProxyClassMapper +{ + public static object MapToTemplateModel((TableModel Table, IReadOnlyList Interfaces) input, GenerationContext context) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(input.Table); + + var (table, interfaces) = input; + + return new + { + table = new + { + SchemaName = table.SchemaName, + Columns = table.Columns.Select(c => + c switch + { + EnumColumnModel enumCol => enumCol with + { + SchemaName = DataverseProxyGenerator.Core.Generation.Utilities.GenerationUtilities.SanitizeName(enumCol.SchemaName), + OptionsetName = DataverseProxyGenerator.Core.Generation.Utilities.GenerationUtilities.SanitizeName(enumCol.OptionsetName), + }, + _ => c with + { + SchemaName = DataverseProxyGenerator.Core.Generation.Utilities.GenerationUtilities.SanitizeName(c.SchemaName), + }, + }), + Relationships = table.Relationships.Select(r => r with + { + SchemaName = DataverseProxyGenerator.Core.Generation.Utilities.GenerationUtilities.SanitizeName(r.SchemaName), + }), + LogicalName = table.LogicalName, + DisplayName = table.DisplayName, + EntityTypeCode = table.EntityTypeCode, + PrimaryNameAttribute = table.PrimaryNameAttribute, + PrimaryIdAttribute = table.PrimaryIdAttribute, + IsIntersect = table.IsIntersect, + InterfacesList = interfaces ?? new List(), + }, + @namespace = context.Namespace, + version = context.Version, + }; + } +} diff --git a/src/DataverseProxyGenerator.Core/Generation/Mappers/XrmContextMapper.cs b/src/DataverseProxyGenerator.Core/Generation/Mappers/XrmContextMapper.cs new file mode 100644 index 0000000..9c530cd --- /dev/null +++ b/src/DataverseProxyGenerator.Core/Generation/Mappers/XrmContextMapper.cs @@ -0,0 +1,20 @@ +using DataverseProxyGenerator.Core.Domain; + +namespace DataverseProxyGenerator.Core.Generation.Mappers; + +public static class XrmContextMapper +{ + public static object MapToTemplateModel(IEnumerable input, GenerationContext context) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(input); + + return new + { + tables = input, + @namespace = context.Namespace, + serviceContextName = context.ServiceContextName ?? "Xrm", + version = context.Version, + }; + } +} diff --git a/src/DataverseProxyGenerator.Core/Generation/Utilities/GenerationUtilities.cs b/src/DataverseProxyGenerator.Core/Generation/Utilities/GenerationUtilities.cs new file mode 100644 index 0000000..d16c664 --- /dev/null +++ b/src/DataverseProxyGenerator.Core/Generation/Utilities/GenerationUtilities.cs @@ -0,0 +1,27 @@ +using DataverseProxyGenerator.Core.Domain; + +namespace DataverseProxyGenerator.Core.Generation.Utilities; + +public static class GenerationUtilities +{ + /// + /// Sanitizes a name to make it a valid C# identifier. + /// + /// The name to sanitize. + /// Optional fallback prefix. + /// A sanitized name. + public static string SanitizeName(string name, string fallbackPrefix = "Item") + { + return NameSanitizer.SanitizeName(name, fallbackPrefix); + } + + /// + /// Gets the type signature for a column using the shared utility. + /// + /// The column model. + /// The type signature. + public static string GetTypeSignature(ColumnModel column) + { + return TypeSignatureHelper.GetPropertyTypeSignature(column); + } +} diff --git a/src/DataverseProxyGenerator.Core/Generation/XrmGenerationConfig.cs b/src/DataverseProxyGenerator.Core/Generation/XrmGenerationConfig.cs index b859f66..0cc6fe9 100644 --- a/src/DataverseProxyGenerator.Core/Generation/XrmGenerationConfig.cs +++ b/src/DataverseProxyGenerator.Core/Generation/XrmGenerationConfig.cs @@ -4,4 +4,6 @@ public record XrmGenerationConfig( string OutputDirectory, string NamespaceSetting, string ServiceContextName, - IReadOnlyDictionary> IntersectMapping); \ No newline at end of file + IReadOnlyDictionary> IntersectMapping, + bool SingleFile = false +); \ No newline at end of file diff --git a/src/DataverseProxyGenerator.Core/Templates/ProxyClass.scriban-cs b/src/DataverseProxyGenerator.Core/Templates/ProxyClass.scriban-cs index d4965c7..1349068 100644 --- a/src/DataverseProxyGenerator.Core/Templates/ProxyClass.scriban-cs +++ b/src/DataverseProxyGenerator.Core/Templates/ProxyClass.scriban-cs @@ -158,12 +158,15 @@ public partial class {{table.SchemaName}} : ExtendedEntity{{ if table.Interfaces get => GetRelatedEntities<{{ rel.RelatedEntitySchemaName }}>("{{ rel.SchemaName }}", null); set => SetRelatedEntities("{{ rel.SchemaName }}", null, value); } - {{ else if rel.RelationshipType == "ManyToOne" }}{{ rel.RelatedEntitySchemaName }} {{ rel.SchemaName }} + {{~ else if rel.RelationshipType == "ManyToOne" }}{{ rel.RelatedEntitySchemaName }} {{ rel.SchemaName }} { get => GetRelatedEntity<{{ rel.RelatedEntitySchemaName }}>("{{ rel.SchemaName }}", null); set => SetRelatedEntity("{{ rel.SchemaName }}", null, value); } - {{ end }} + {{~ end ~}} + {{~ if !for.last ~}} + + {{~ end ~}} {{~ end ~}} {{~ end ~}} } \ No newline at end of file diff --git a/src/DataverseProxyGenerator.Core/Templates/SingleFile.scriban-cs b/src/DataverseProxyGenerator.Core/Templates/SingleFile.scriban-cs new file mode 100644 index 0000000..25e7120 --- /dev/null +++ b/src/DataverseProxyGenerator.Core/Templates/SingleFile.scriban-cs @@ -0,0 +1,529 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics; +using System.Linq; +using System.Linq.Expressions; +using System.Runtime.Serialization; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Client; + +namespace {{namespace}}; + +// ============================================================================ +// ENTITY PROXY CLASSES +// ============================================================================ + +{{~ for table in tables | array.sort "SchemaName" ~}} +/// +/// Display Name: {{ table.DisplayName }} +/// +[System.CodeDom.Compiler.GeneratedCode("DataverseProxyGenerator", "{{version}}")] +[EntityLogicalName("{{table.LogicalName}}")] +[DebuggerDisplay("{DebuggerDisplay,nq}")] +[DataContract] +public partial class {{table.SchemaName}} : ExtendedEntity{{ if table.InterfacesList.size > 0 }}, {{ table.InterfacesList | array.join ", " }}{{ end }} +{ + public const string EntityLogicalName = "{{table.LogicalName}}"; + public const int EntityTypeCode = {{table.EntityTypeCode}}; + + public {{table.SchemaName}}() : base(EntityLogicalName) { } + public {{table.SchemaName}}(Guid id) : base(EntityLogicalName, id) { } + + private string DebuggerDisplay => GetDebuggerDisplay("{{ table.PrimaryNameAttribute }}"); + + [AttributeLogicalName("{{ table.PrimaryIdAttribute }}")] + public override Guid Id { + get { + return base.Id; + } + set { + SetId("{{ table.PrimaryIdAttribute }}", value); + } + } + + {{~ for column in table.Columns | array.sort "LogicalName" ~}} + {{~ if column.Description || column.DisplayName ~}} + /// + {{~ if column.Description && column.Description != "" && column.Description != " " && column.Description != " " && column.Description != " " && column.Description != "\t" && column.Description != "\n" && column.Description != "\r\n" ~}} + {{~ for line in column.Description | string.split '\n' ~}} + /// {{ line | string.strip }} + {{~ end ~}} + {{~ end ~}} + {{~ if column.DisplayName ~}} + /// Display Name: {{ column.DisplayName }} + {{~ end ~}} + /// + {{~ end ~}} + [AttributeLogicalName("{{ column.LogicalName }}")] + [DisplayName("{{ column.DisplayName }}")] + {{~ if column.IsObsolete ~}} + [ObsoleteAttribute()] + {{~ end ~}} + {{~ if column.TypeName == "StringColumnModel" || column.TypeName == "MemoColumnModel" ~}} + [MaxLength({{ column.MaxLength }})] + public string? {{column.SchemaName}} + { + get => GetAttributeValue("{{column.LogicalName}}"); + set => SetAttributeValue("{{column.LogicalName}}", value); + } + {{~ else if column.TypeName == "IntegerColumnModel" ~}} + [Range({{ column.Min }}, {{ column.Max }})] + public int? {{column.SchemaName}} + { + get => GetAttributeValue("{{column.LogicalName}}"); + set => SetAttributeValue("{{column.LogicalName}}", value); + } + {{~ else if column.TypeName == "BigIntColumnModel" ~}} + public long? {{column.SchemaName}} + { + get => GetAttributeValue("{{column.LogicalName}}"); + set => SetAttributeValue("{{column.LogicalName}}", value); + } + {{~ else if column.TypeName == "BooleanColumnModel" ~}} + public bool? {{column.SchemaName}} + { + get => GetAttributeValue("{{column.LogicalName}}"); + set => SetAttributeValue("{{column.LogicalName}}", value); + } + {{~ else if column.TypeName == "DateTimeColumnModel" ~}} + public DateTime? {{column.SchemaName}} + { + get => GetAttributeValue("{{column.LogicalName}}"); + set => SetAttributeValue("{{column.LogicalName}}", value); + } + {{~ else if column.TypeName == "DecimalColumnModel" ~}} + public decimal? {{column.SchemaName}} + { + get => GetAttributeValue("{{column.LogicalName}}"); + set => SetAttributeValue("{{column.LogicalName}}", value); + } + {{~ else if column.TypeName == "DoubleColumnModel" ~}} + public double? {{column.SchemaName}} + { + get => GetAttributeValue("{{column.LogicalName}}"); + set => SetAttributeValue("{{column.LogicalName}}", value); + } + {{~ else if column.TypeName == "MoneyColumnModel" ~}} + public decimal? {{column.SchemaName}} + { + get => this.GetMoneyValue("{{column.LogicalName}}"); + set => this.SetMoneyValue("{{column.LogicalName}}", value); + } + {{~ else if column.TypeName == "EnumColumnModel" ~}} + {{~ if column.IsMultiSelect ~}} + public IEnumerable<{{ column.OptionsetName | string.capitalize }}> {{column.SchemaName}} + { + get => this.GetOptionSetCollectionValue<{{ column.OptionsetName | string.capitalize }}>("{{column.LogicalName}}"); + set => this.SetOptionSetCollectionValue("{{column.LogicalName}}", value); + } + {{~ else ~}} + public {{ column.OptionsetName | string.capitalize }}? {{column.SchemaName}} + { + get => this.GetOptionSetValue<{{ column.OptionsetName | string.capitalize }}>("{{column.LogicalName}}"); + set => this.SetOptionSetValue("{{column.LogicalName}}", value); + } + {{~ end ~}} + {{~ else if column.TypeName == "LookupColumnModel" ~}} + public EntityReference? {{column.SchemaName}} + { + get => GetAttributeValue("{{column.LogicalName}}"); + set => SetAttributeValue("{{column.LogicalName}}", value); + } + {{~ else if column.TypeName == "PartyListColumnModel" ~}} + public IEnumerable {{column.SchemaName}} + { + get => GetEntityCollection("{{column.LogicalName}}"); + set => SetEntityCollection("{{column.LogicalName}}", value); + } + {{~ else if column.TypeName == "FileColumnModel" || column.TypeName == "ImageColumnModel" ~}} + public byte[] {{column.SchemaName}} + { + get => GetAttributeValue("{{column.LogicalName}}"); + set => SetAttributeValue("{{column.LogicalName}}", value); + } + {{~ else if column.TypeName == "PrimaryIdColumnModel" ~}} + public Guid {{column.SchemaName}} + { + get => GetAttributeValue("{{column.LogicalName}}"); + set => SetId("{{column.LogicalName}}", value); + } + {{~ end ~}} + {{~ if !for.last ~}} + + {{~ end ~}} + {{~ end ~}} + + {{~ if ! table.IsIntersect ~}} + {{~ for rel in table.Relationships ~}} + {{~ if rel.RelationshipType == "ManyToOne" ~}} + [AttributeLogicalName("{{ rel.ThisEntityAttribute }}")] + {{~ end ~}} + [RelationshipSchemaName("{{ rel.SchemaName }}")] + [RelationshipMetadata("{{ rel.RelationshipType }}", "{{ rel.ThisEntityAttribute }}", "{{ rel.RelatedEntity }}", "{{ rel.RelatedEntityAttribute }}", "{{ rel.ThisEntityRole }}")] + public {{ if rel.RelationshipType == "OneToMany" || rel.RelationshipType == "ManyToMany" }}IEnumerable<{{ rel.RelatedEntitySchemaName }}> {{ rel.SchemaName }} + { + get => GetRelatedEntities<{{ rel.RelatedEntitySchemaName }}>("{{ rel.SchemaName }}", null); + set => SetRelatedEntities("{{ rel.SchemaName }}", null, value); + } + {{~ else if rel.RelationshipType == "ManyToOne" }}{{ rel.RelatedEntitySchemaName }} {{ rel.SchemaName }} + { + get => GetRelatedEntity<{{ rel.RelatedEntitySchemaName }}>("{{ rel.SchemaName }}", null); + set => SetRelatedEntity("{{ rel.SchemaName }}", null, value); + } + {{~ end ~}} + {{~ if !for.last ~}} + + {{~ end ~}} + {{~ end ~}} + {{~ end ~}} +} + +{{ end }} + +// ============================================================================ +// INTERSECTION INTERFACES +// ============================================================================ + +{{~ for interface in interfaces | array.sort "Name" ~}} +[System.CodeDom.Compiler.GeneratedCode("DataverseProxyGenerator", "{{version}}")] +public interface {{interface.Name}} +{ + {{~ for column in interface.Columns | array.sort "LogicalName" ~}} + {{~ if column.Description || column.DisplayName ~}} + /// + {{~ if column.Description && column.Description != "" && column.Description != " " && column.Description != " " && column.Description != "\t" && column.Description != "\n" && column.Description != "\r\n" ~}} + {{~ for line in column.Description | string.split '\n' ~}} + /// {{ line | string.strip }} + {{~ end ~}} + {{~ end ~}} + {{~ if column.DisplayName ~}} + /// Display Name: {{ column.DisplayName }} + {{~ end ~}} + /// + {{~ end ~}} + {{ column.TypeSignature }} {{column.SchemaName}} { get; set; } + {{~ if !for.last ~}} + {{~ end ~}} + {{~ end ~}} +} + +{{ end }} + +// ============================================================================ +// ENUM OPTION SETS +// ============================================================================ + +{{~ for optionset in optionsets | array.sort "Name" ~}} +[System.CodeDom.Compiler.GeneratedCode("DataverseProxyGenerator", "{{version}}")] +[DataContract] +public enum {{optionset.Name | string.capitalize}} +{ + {{~ for pair in optionset.Values | array.sort "Value" ~}} + [EnumMember] + {{~ for loc in pair.Localizations ~}} + [OptionSetMetadata("{{ loc.Value }}", {{ loc.Key }})] + {{~ end ~}} + {{ pair.Name }} = {{ pair.Value }}, + {{~ end ~}} +} + +{{ end }} + +// ============================================================================ +// ATTRIBUTE CLASSES +// ============================================================================ + +[System.CodeDom.Compiler.GeneratedCode("DataverseProxyGenerator", "{{version}}")] +[AttributeUsage(AttributeTargets.Field, AllowMultiple = true)] +public sealed class OptionSetMetadataAttribute : Attribute +{ + public string Label { get; } + + public int Lcid { get; } + + public OptionSetMetadataAttribute(string label, int lcid) + { + Label = label; + Lcid = lcid; + } +} + +[System.CodeDom.Compiler.GeneratedCode("DataverseProxyGenerator", "{{version}}")] +[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] +public sealed class RelationshipMetadataAttribute : Attribute +{ + public string RelationshipType { get; } + public string? ThisEntityAttribute { get; } + public string? RelatedEntity { get; } + public string? RelatedEntityAttribute { get; } + public string? ThisEntityRole { get; } + + public RelationshipMetadataAttribute( + string relationshipType, + string? thisEntityAttribute = null, + string? relatedEntity = null, + string? relatedEntityAttribute = null, + string? thisEntityRole = null) + { + RelationshipType = relationshipType; + ThisEntityAttribute = thisEntityAttribute; + RelatedEntity = relatedEntity; + RelatedEntityAttribute = relatedEntityAttribute; + ThisEntityRole = thisEntityRole; + } +} + +// ============================================================================ +// EXTENDED ENTITY BASE CLASS +// ============================================================================ + +[System.CodeDom.Compiler.GeneratedCode("DataverseProxyGenerator", "{{version}}")] +public class ExtendedEntity : Entity +{ + public ExtendedEntity(string logicalName) + : base(logicalName) + { + } + + public ExtendedEntity(string logicalName, Guid id) + : base(logicalName, id) + { + } + + public new T? GetAttributeValue(string attributeLogicalName) + { + return base.GetAttributeValue(attributeLogicalName); + } + + protected string GetDebuggerDisplay(string primaryNameAttribute) + { + string display = GetType().Name; + + var name = this.GetAttributeValue(primaryNameAttribute); + if (!string.IsNullOrEmpty(name)) display += $" ({name})"; + if (this.Id != Guid.Empty) display += $" [{this.Id}]"; + + return display; + } + + protected void SetId(string primaryIdAttribute, Guid? id) + { + this.Id = id.GetValueOrDefault(); + this.SetAttributeValue(primaryIdAttribute, id); + } + + protected IEnumerable GetEntityCollection(string attributeName) + where T : Entity + { + var collection = this.GetAttributeValue(attributeName); + if (collection != null && collection.Entities != null) + { + return collection.Entities.Select(x => x.ToEntity()); + } + else + { + return Enumerable.Empty(); + } + } + + protected void SetEntityCollection(string attributeName, IEnumerable entities) + where T : Entity + { + var list = entities?.Cast().ToList(); + if (list == null || !list.Any()) + { + this.SetAttributeValue(attributeName, null); + return; + } + + this.SetAttributeValue(attributeName, new EntityCollection(list)); + } + + protected decimal? GetMoneyValue(string attributeName) + { + var money = this.GetAttributeValue(attributeName); + if (money != null) + { + return money.Value; + } + else + { + return null; + } + } + + protected void SetMoneyValue(string attributeName, decimal? value) + { + if (value.HasValue) + { + this.SetAttributeValue(attributeName, new Money(value.Value)); + } + else + { + this.SetAttributeValue(attributeName, null); + } + } + + protected IEnumerable GetOptionSetCollectionValue(string attributeName) + where T : struct, IComparable, IConvertible, IFormattable + { + var optionSetCollection = this.GetAttributeValue(attributeName); + if (optionSetCollection != null && optionSetCollection.Count != 0) + { + return optionSetCollection + .Select(osv => (T)Enum.ToObject(typeof(T), osv.Value)) + .ToArray(); + } + + return Enumerable.Empty(); + } + + protected void SetOptionSetCollectionValue(string attributeName, IEnumerable values) + { + var list = values?.Cast().Cast().ToList(); + if (list == null || !list.Any()) + { + this.SetAttributeValue(attributeName, null); + return; + } + + var arr = list + .Select(v => new OptionSetValue(v)) + .ToArray(); + this.SetAttributeValue(attributeName, new OptionSetValueCollection(arr)); + } + + protected T? GetOptionSetValue(string attributeName) + where T : struct, IComparable, IConvertible, IFormattable + { + var optionSet = this.GetAttributeValue(attributeName); + if (optionSet != null) + { + return (T)Enum.ToObject(typeof(T), optionSet.Value); + } + + return null; + } + + protected void SetOptionSetValue(string attributeName, T? value) + { + if (value != null) + { + this.SetAttributeValue(attributeName, new OptionSetValue((int)(object)value)); + } + else + { + this.SetAttributeValue(attributeName, null); + } + } +} + +// ============================================================================ +// TABLE ATTRIBUTE HELPERS +// ============================================================================ + +[System.CodeDom.Compiler.GeneratedCode("DataverseProxyGenerator", "{{version}}")] +public static class TableAttributeHelpers +{ + /// + /// Gets the logical column name for a property on the entity, using the AttributeLogicalNameAttribute if present. + /// + /// Type of Entity + /// Entity to get the column from + /// Expression to pick the column + /// Name of column + /// If no expression is provided + /// If the expression is not x => x.column + public static string GetColumnName(this T entity, Expression> lambda) + where T : Entity + { + if (lambda == null) throw new ArgumentNullException(nameof(lambda)); + + MemberExpression? body = lambda.Body as MemberExpression; + if (body == null) + { + var ubody = lambda.Body as UnaryExpression; + if (ubody != null && ubody.Operand is MemberExpression) + { + body = ubody.Operand as MemberExpression; + } + } + + if (body == null) + throw new ArgumentException("Invalid lambda expression. Expression should point to a property.", nameof(lambda)); + + var member = body.Member; + var customAttributes = member.GetCustomAttributesData(); + var neededAttribute = customAttributes.FirstOrDefault(x => x.AttributeType == typeof(AttributeLogicalNameAttribute)); + if (neededAttribute != null) + { + var arg = neededAttribute.ConstructorArguments.FirstOrDefault().Value; + if (arg != null) + return arg.ToString()!; + } + + return member.Name; + } + + /// + /// Checks if all specified attributes are present on the entity. + /// + /// Type of Entity + /// Entity to check if contains attribute + /// Expression that specify attributes to check + /// Bool for whether all attributes are on the entity + /// If Entity is null + public static bool ContainsAttributes(this T entity, params Expression>[] attrGetters) + where T : Entity + { + if (entity == null) throw new ArgumentNullException(nameof(entity)); + if (attrGetters == null || attrGetters.Length == 0) return false; + return attrGetters + .Select(a => entity.GetColumnName(a).ToLowerInvariant()) + .All(a => entity.Contains(a)); + } + + /// + /// Removes all specified attributes from the entity. Returns true if any attribute was removed. + /// + /// Type of Entity + /// Entity to remove attributes from + /// Expression that specify attributes to remove + /// Return true if any attributes were removed + /// If entity is null + public static bool RemoveAttributes(this T entity, params Expression>[] attrGetters) + where T : Entity + { + if (entity == null) throw new ArgumentNullException(nameof(entity)); + if (attrGetters == null || attrGetters.Length == 0) return false; + bool anyRemoved = false; + foreach (var a in attrGetters) + { + var name = entity.GetColumnName(a).ToLowerInvariant(); + anyRemoved |= entity.Attributes.Remove(name); + } + + return anyRemoved; + } +} + +// ============================================================================ +// XRM SERVICE CONTEXT +// ============================================================================ + +[System.CodeDom.Compiler.GeneratedCode("DataverseProxyGenerator", "{{version}}")] +public class {{serviceContextName}} : OrganizationServiceContext +{ + public {{serviceContextName}}(IOrganizationService service) + : base(service) + { + } + +{{~ for table in tables | array.sort "SchemaName" ~}} + public IQueryable<{{ table.SchemaName }}> {{ table.SchemaName }}Set + { + get { return CreateQuery<{{ table.SchemaName }}>(); } + } +{{ end }} +} diff --git a/src/DataverseProxyGenerator.Core/Templates/XrmClass.scriban-cs b/src/DataverseProxyGenerator.Core/Templates/XrmClass.scriban-cs index 7082b10..47582aa 100644 --- a/src/DataverseProxyGenerator.Core/Templates/XrmClass.scriban-cs +++ b/src/DataverseProxyGenerator.Core/Templates/XrmClass.scriban-cs @@ -10,11 +10,11 @@ public class Xrm : OrganizationServiceContext : base(service) { } - {{~ for table in tables | array.sort "SchemaName" ~}} + public IQueryable<{{ table.SchemaName }}> {{ table.SchemaName }}Set { get { return CreateQuery<{{ table.SchemaName }}>(); } } -{{ end }} +{{~ end ~}} } \ No newline at end of file diff --git a/src/DataverseProxyGenerator.Tool/CommandLineParser.cs b/src/DataverseProxyGenerator.Tool/CommandLineParser.cs index a11b526..78267fe 100644 --- a/src/DataverseProxyGenerator.Tool/CommandLineParser.cs +++ b/src/DataverseProxyGenerator.Tool/CommandLineParser.cs @@ -12,7 +12,8 @@ public static (string OutputDirectory, string ServiceContextName, string DeprecatedPrefix, IReadOnlyDictionary> IntersectMapping, - IReadOnlyDictionary LabelMapping) + IReadOnlyDictionary LabelMapping, + bool SingleFile) #pragma warning disable MA0051 // Method is too long Parse(string[] args) #pragma warning restore MA0051 // Method is too long @@ -97,6 +98,10 @@ public static (string OutputDirectory, Arity = ArgumentArity.ZeroOrMore, }; + var singleFileOption = new Option( + aliases: ["--singlefile"], + description: "If set, all output will be written to a single file named XrmContext.cs"); + var rootCommand = new RootCommand("Dataverse Proxy Generator CLI") { outputDirectoryOption, @@ -107,6 +112,7 @@ public static (string OutputDirectory, deprecatedPrefixOption, intersectOption, labelMappingsOption, + singleFileOption, }; var parsedResult = rootCommand.Parse(args); @@ -119,7 +125,8 @@ public static (string OutputDirectory, parsedResult.GetValueForOption(serviceContextNameOption) ?? string.Empty, parsedResult.GetValueForOption(deprecatedPrefixOption) ?? string.Empty, (parsedResult.GetValueForOption(intersectOption) ?? []).AsReadOnly(), - (parsedResult.GetValueForOption(labelMappingsOption) ?? []).AsReadOnly() + (parsedResult.GetValueForOption(labelMappingsOption) ?? []).AsReadOnly(), + parsedResult.GetValueForOption(singleFileOption) ); } diff --git a/src/DataverseProxyGenerator.Tool/Configuration/SimpleXrmContextConfigBuilder.cs b/src/DataverseProxyGenerator.Tool/Configuration/SimpleXrmContextConfigBuilder.cs index 34f2820..97a07a2 100644 --- a/src/DataverseProxyGenerator.Tool/Configuration/SimpleXrmContextConfigBuilder.cs +++ b/src/DataverseProxyGenerator.Tool/Configuration/SimpleXrmContextConfigBuilder.cs @@ -27,6 +27,8 @@ public static XrmContextConfig BuildFromConfiguration() configSection.GetValue("OutputDirectory") ?? string.Empty, configSection.GetValue("NamespaceSetting") ?? "DataverseContext", configSection.GetValue("ServiceContextName") ?? "Xrm", - configSection.GetSection("IntersectMapping").Get>>() ?? new Dictionary>(StringComparer.InvariantCulture))); + configSection.GetSection("IntersectMapping").Get>>() ?? new Dictionary>(StringComparer.InvariantCulture), + configSection.GetValue("SingleFile") + )); } } \ No newline at end of file diff --git a/src/DataverseProxyGenerator.Tool/Program.cs b/src/DataverseProxyGenerator.Tool/Program.cs index f3be43c..638a868 100644 --- a/src/DataverseProxyGenerator.Tool/Program.cs +++ b/src/DataverseProxyGenerator.Tool/Program.cs @@ -28,7 +28,7 @@ private static async Task RunApplication(string[] args) var baseConfig = SimpleXrmContextConfigBuilder.BuildFromConfiguration(); // Parse command line args and merge - var (outputDirectory, solutions, entities, namespaceSetting, serviceContextName, deprecatedPrefix, intersectMapping, labelMapping) = CommandLineParser.Parse(args); + var (outputDirectory, solutions, entities, namespaceSetting, serviceContextName, deprecatedPrefix, intersectMapping, labelMapping, singleFile) = CommandLineParser.Parse(args); var config = new XrmContextConfig( new XrmFetchConfig( @@ -40,7 +40,9 @@ private static async Task RunApplication(string[] args) !string.IsNullOrWhiteSpace(outputDirectory) ? outputDirectory : baseConfig.Generation.OutputDirectory, !string.IsNullOrWhiteSpace(namespaceSetting) ? namespaceSetting : baseConfig.Generation.NamespaceSetting ?? "DataverseContext", !string.IsNullOrWhiteSpace(serviceContextName) ? serviceContextName : baseConfig.Generation.ServiceContextName ?? "Xrm", - (intersectMapping.Count > 0) ? intersectMapping : baseConfig.Generation.IntersectMapping)); + (intersectMapping.Count > 0) ? intersectMapping : baseConfig.Generation.IntersectMapping, + singleFile + )); if (string.IsNullOrWhiteSpace(config.Generation.OutputDirectory)) { From 6576d380a4e1507d7a6708325bb570aee5b77df9 Mon Sep 17 00:00:00 2001 From: skovlund Date: Fri, 15 Aug 2025 14:12:46 +0200 Subject: [PATCH 2/4] Template formatting --- .../Templates/CustomApiRequest.scriban-cs | 4 ++-- .../Templates/CustomApiResponse.scriban-cs | 4 ++-- .../Templates/SingleFile.scriban-cs | 19 +++++++++---------- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/DataverseProxyGenerator.Core/Templates/CustomApiRequest.scriban-cs b/src/DataverseProxyGenerator.Core/Templates/CustomApiRequest.scriban-cs index ac3371b..e49e6fe 100644 --- a/src/DataverseProxyGenerator.Core/Templates/CustomApiRequest.scriban-cs +++ b/src/DataverseProxyGenerator.Core/Templates/CustomApiRequest.scriban-cs @@ -5,7 +5,7 @@ public partial class {{ sanitized_unique_name }}Request : Microsoft.Xrm.Sdk.OrganizationRequest { {{~ for param in request_parameters ~}} - + {{ param.xml_doc_comment }} public {{ param.csharp_type }}{{ if param.is_optional }}?{{ end }} {{ param.name }} { @@ -26,7 +26,7 @@ public partial class {{ sanitized_unique_name }}Request : Microsoft.Xrm.Sdk.Orga } } {{~ end ~}} - + public {{ sanitized_unique_name }}Request() { this.RequestName = "{{ unique_name }}"; diff --git a/src/DataverseProxyGenerator.Core/Templates/CustomApiResponse.scriban-cs b/src/DataverseProxyGenerator.Core/Templates/CustomApiResponse.scriban-cs index 74ba45a..c629b66 100644 --- a/src/DataverseProxyGenerator.Core/Templates/CustomApiResponse.scriban-cs +++ b/src/DataverseProxyGenerator.Core/Templates/CustomApiResponse.scriban-cs @@ -4,11 +4,11 @@ [System.CodeDom.Compiler.GeneratedCodeAttribute("DataverseProxyGenerator", "{{ version }}")] public partial class {{ sanitized_unique_name }}Response : Microsoft.Xrm.Sdk.OrganizationResponse { - + public {{ sanitized_unique_name }}Response() { } - + {{~ for prop in response_properties ~}} {{ prop.xml_doc_comment }} public {{ prop.csharp_type }} {{ prop.name }} diff --git a/src/DataverseProxyGenerator.Core/Templates/SingleFile.scriban-cs b/src/DataverseProxyGenerator.Core/Templates/SingleFile.scriban-cs index 25e7120..6d1d27f 100644 --- a/src/DataverseProxyGenerator.Core/Templates/SingleFile.scriban-cs +++ b/src/DataverseProxyGenerator.Core/Templates/SingleFile.scriban-cs @@ -178,8 +178,7 @@ public partial class {{table.SchemaName}} : ExtendedEntity{{ if table.Interfaces {{~ end ~}} {{~ end ~}} } - -{{ end }} +{{~ end ~}} // ============================================================================ // INTERSECTION INTERFACES @@ -207,8 +206,7 @@ public interface {{interface.Name}} {{~ end ~}} {{~ end ~}} } - -{{ end }} +{{~ end ~}} // ============================================================================ // ENUM OPTION SETS @@ -217,7 +215,9 @@ public interface {{interface.Name}} {{~ for optionset in optionsets | array.sort "Name" ~}} [System.CodeDom.Compiler.GeneratedCode("DataverseProxyGenerator", "{{version}}")] [DataContract] -public enum {{optionset.Name | string.capitalize}} +#pragma warning disable CS8981 +public enum {{optionsetName}} +#pragma warning restore CS8981 { {{~ for pair in optionset.Values | array.sort "Value" ~}} [EnumMember] @@ -227,8 +227,7 @@ public enum {{optionset.Name | string.capitalize}} {{ pair.Name }} = {{ pair.Value }}, {{~ end ~}} } - -{{ end }} +{{~ end ~}} // ============================================================================ // ATTRIBUTE CLASSES @@ -519,11 +518,11 @@ public class {{serviceContextName}} : OrganizationServiceContext : base(service) { } - {{~ for table in tables | array.sort "SchemaName" ~}} + public IQueryable<{{ table.SchemaName }}> {{ table.SchemaName }}Set { get { return CreateQuery<{{ table.SchemaName }}>(); } } -{{ end }} -} +{{~ end ~}} +} \ No newline at end of file From 2d5753da11bb912bc17aafd3b4b0501a25487c38 Mon Sep 17 00:00:00 2001 From: skovlund Date: Fri, 15 Aug 2025 15:24:40 +0200 Subject: [PATCH 3/4] Fixes to single-file enums and intersections --- .../Generation/CSharpProxyGenerator.cs | 15 +++++++++++++-- .../Generation/Mappers/EnumMapper.cs | 4 ++-- .../Generation/Mappers/HelperFileMapper.cs | 2 +- .../Mappers/IntersectionInterfaceMapper.cs | 2 +- .../Generation/Mappers/ProxyClassMapper.cs | 2 +- .../Generation/Mappers/XrmContextMapper.cs | 2 +- .../Generation/Utilities/FilePathHelper.cs | 1 + .../Generation/Utilities/GenerationUtilities.cs | 2 +- .../Templates/SingleFile.scriban-cs | 9 ++++++--- 9 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/DataverseProxyGenerator.Core/Generation/CSharpProxyGenerator.cs b/src/DataverseProxyGenerator.Core/Generation/CSharpProxyGenerator.cs index 01d4a36..d917846 100644 --- a/src/DataverseProxyGenerator.Core/Generation/CSharpProxyGenerator.cs +++ b/src/DataverseProxyGenerator.Core/Generation/CSharpProxyGenerator.cs @@ -1,5 +1,6 @@ using DataverseProxyGenerator.Core.Domain; using DataverseProxyGenerator.Core.Generation.Generators; +using DataverseProxyGenerator.Core.Generation.Mappers; using DataverseProxyGenerator.Core.Templates; namespace DataverseProxyGenerator.Core.Generation; @@ -72,7 +73,7 @@ private static (Dictionary> InterfaceColumns, D return BuildIntersectionData(config.IntersectMapping, tableDict, tableColumns); } - private IEnumerable GenerateSingleFile( + private static IEnumerable GenerateSingleFile( List tablesList, Dictionary> interfaceColumns, Dictionary> tableToInterfaces, @@ -93,7 +94,9 @@ private static object CreateSingleFileTemplateModel( Dictionary> tableToInterfaces, GenerationContext context) { - var globalOptionsets = GetGlobalOptionsets(tablesList).ToList(); + var globalOptionsets = GetGlobalOptionsets(tablesList) + .Select(enumCol => EnumMapper.MapToTemplateModel(enumCol, context)) + .ToList(); var interfaces = CreateInterfaceModels(interfaceColumns, tablesList); // Add interface lists to tables (without modifying TableModel structure) @@ -140,6 +143,14 @@ private static List CreateInterfaceModels( Name = kvp.Key, Columns = kvp.Value.Select(sig => FindMatchingColumn(sig, tablesList)) .Where(c => c != null) + .Select(col => new + { + SchemaName = Utilities.GenerationUtilities.SanitizeName(col!.SchemaName), + col!.LogicalName, + col.DisplayName, + col.Description, + TypeSignature = Utilities.GenerationUtilities.GetTypeSignature(col), + }) .ToList(), }).ToList(); } diff --git a/src/DataverseProxyGenerator.Core/Generation/Mappers/EnumMapper.cs b/src/DataverseProxyGenerator.Core/Generation/Mappers/EnumMapper.cs index fe74a06..94cfa56 100644 --- a/src/DataverseProxyGenerator.Core/Generation/Mappers/EnumMapper.cs +++ b/src/DataverseProxyGenerator.Core/Generation/Mappers/EnumMapper.cs @@ -10,7 +10,7 @@ public static object MapToTemplateModel(EnumColumnModel input, GenerationContext ArgumentNullException.ThrowIfNull(context); ArgumentNullException.ThrowIfNull(input); - var sanitizedOptionSetName = DataverseProxyGenerator.Core.Generation.Utilities.GenerationUtilities.SanitizeName(input.OptionsetName, "UnknownOptionSet"); + var sanitizedOptionSetName = GenerationUtilities.SanitizeName(input.OptionsetName, "UnknownOptionSet"); // Generate unique enum member names to handle duplicate labels using groupBy approach var sanitizedOptions = input.OptionsetValues @@ -42,4 +42,4 @@ public static object MapToTemplateModel(EnumColumnModel input, GenerationContext version = context.Version, }; } -} +} \ No newline at end of file diff --git a/src/DataverseProxyGenerator.Core/Generation/Mappers/HelperFileMapper.cs b/src/DataverseProxyGenerator.Core/Generation/Mappers/HelperFileMapper.cs index 1fe6ff0..88f734d 100644 --- a/src/DataverseProxyGenerator.Core/Generation/Mappers/HelperFileMapper.cs +++ b/src/DataverseProxyGenerator.Core/Generation/Mappers/HelperFileMapper.cs @@ -12,4 +12,4 @@ public static object MapToTemplateModel(string templateName, GenerationContext c version = context.Version, }; } -} +} \ No newline at end of file diff --git a/src/DataverseProxyGenerator.Core/Generation/Mappers/IntersectionInterfaceMapper.cs b/src/DataverseProxyGenerator.Core/Generation/Mappers/IntersectionInterfaceMapper.cs index a7aa8f7..7dd6a8a 100644 --- a/src/DataverseProxyGenerator.Core/Generation/Mappers/IntersectionInterfaceMapper.cs +++ b/src/DataverseProxyGenerator.Core/Generation/Mappers/IntersectionInterfaceMapper.cs @@ -28,4 +28,4 @@ public static object MapToTemplateModel((string InterfaceName, IEnumerable version = context.Version, }; } -} +} \ No newline at end of file diff --git a/src/DataverseProxyGenerator.Core/Generation/Mappers/XrmContextMapper.cs b/src/DataverseProxyGenerator.Core/Generation/Mappers/XrmContextMapper.cs index 9c530cd..43c51f6 100644 --- a/src/DataverseProxyGenerator.Core/Generation/Mappers/XrmContextMapper.cs +++ b/src/DataverseProxyGenerator.Core/Generation/Mappers/XrmContextMapper.cs @@ -17,4 +17,4 @@ public static object MapToTemplateModel(IEnumerable input, Generatio version = context.Version, }; } -} +} \ No newline at end of file diff --git a/src/DataverseProxyGenerator.Core/Generation/Utilities/FilePathHelper.cs b/src/DataverseProxyGenerator.Core/Generation/Utilities/FilePathHelper.cs index 0d9267d..58bac42 100644 --- a/src/DataverseProxyGenerator.Core/Generation/Utilities/FilePathHelper.cs +++ b/src/DataverseProxyGenerator.Core/Generation/Utilities/FilePathHelper.cs @@ -35,6 +35,7 @@ public static string GetIntersectionInterfaceFilePath(string sanitizedName) /// /// Gets the output path for the Xrm context class file. /// + /// The sanitized name for the context file. /// The relative file path. public static string GetXrmContextFilePath(string sanitizedName) { diff --git a/src/DataverseProxyGenerator.Core/Generation/Utilities/GenerationUtilities.cs b/src/DataverseProxyGenerator.Core/Generation/Utilities/GenerationUtilities.cs index d16c664..9c68381 100644 --- a/src/DataverseProxyGenerator.Core/Generation/Utilities/GenerationUtilities.cs +++ b/src/DataverseProxyGenerator.Core/Generation/Utilities/GenerationUtilities.cs @@ -24,4 +24,4 @@ public static string GetTypeSignature(ColumnModel column) { return TypeSignatureHelper.GetPropertyTypeSignature(column); } -} +} \ No newline at end of file diff --git a/src/DataverseProxyGenerator.Core/Templates/SingleFile.scriban-cs b/src/DataverseProxyGenerator.Core/Templates/SingleFile.scriban-cs index 6d1d27f..6d801c2 100644 --- a/src/DataverseProxyGenerator.Core/Templates/SingleFile.scriban-cs +++ b/src/DataverseProxyGenerator.Core/Templates/SingleFile.scriban-cs @@ -216,19 +216,22 @@ public interface {{interface.Name}} [System.CodeDom.Compiler.GeneratedCode("DataverseProxyGenerator", "{{version}}")] [DataContract] #pragma warning disable CS8981 -public enum {{optionsetName}} +public enum {{optionset.optionsetName}} #pragma warning restore CS8981 { - {{~ for pair in optionset.Values | array.sort "Value" ~}} + {{~ for pair in optionset.optionsetValues | array.sort "Value" ~}} [EnumMember] {{~ for loc in pair.Localizations ~}} [OptionSetMetadata("{{ loc.Value }}", {{ loc.Key }})] {{~ end ~}} {{ pair.Name }} = {{ pair.Value }}, + {{~ if !for.last ~}} + + {{~ end ~}} {{~ end ~}} } -{{~ end ~}} +{{~ end ~}} // ============================================================================ // ATTRIBUTE CLASSES // ============================================================================ From ab10cb88859a68f724d0337fb81df0cc1daaae37 Mon Sep 17 00:00:00 2001 From: skovlund Date: Fri, 15 Aug 2025 15:34:08 +0200 Subject: [PATCH 4/4] Deleted appsettings.json --- appsettings.json | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 appsettings.json diff --git a/appsettings.json b/appsettings.json deleted file mode 100644 index 70cac1e..0000000 --- a/appsettings.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "DATAVERSE_URL": "https://yourenv.crm4.dynamics.com", - "XrmContext": { - "OutputDirectory": "out", - "Solutions": [ "solutionName" ], - "Entities": [ "account", "contact" ], - "NamespaceSetting": "MyNamespace", - "ServiceContextName": "Xrm", - "DeprecatedPrefix": "ZZ", - "IntersectMapping": { - "ICustomer": [ "account", "contact" ] - }, - "LabelMapping": { - "\\u2714\\uFE0F": "checkmark" - } - } -}