Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 0 additions & 17 deletions appsettings.json

This file was deleted.

133 changes: 124 additions & 9 deletions src/DataverseProxyGenerator.Core/Generation/CSharpProxyGenerator.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -40,22 +41,135 @@ public IEnumerable<GeneratedFile> GenerateCode(IEnumerable<TableModel> tables, X
ArgumentNullException.ThrowIfNull(tables);
ArgumentNullException.ThrowIfNull(config);

var files = new List<GeneratedFile>();
var version = GetAssemblyVersion();
var context = CreateGenerationContext(config);
var tablesList = tables.ToList();
var (interfaceColumns, tableToInterfaces) = PrepareIntersectionData(tablesList, config);

var context = new GenerationContext
if (config.SingleFile)
{
return GenerateSingleFile(tablesList, interfaceColumns, tableToInterfaces, context);
}

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<string, HashSet<ColumnSignature>> InterfaceColumns, Dictionary<string, List<string>> TableToInterfaces)
PrepareIntersectionData(List<TableModel> 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 static IEnumerable<GeneratedFile> GenerateSingleFile(
List<TableModel> tablesList,
Dictionary<string, HashSet<ColumnSignature>> interfaceColumns,
Dictionary<string, List<string>> 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<TableModel> tablesList,
Dictionary<string, HashSet<ColumnSignature>> interfaceColumns,
Dictionary<string, List<string>> tableToInterfaces,
GenerationContext context)
{
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)
var tablesWithInterfaces = tablesList.Select(table =>
{
var tableInterfaces = tableToInterfaces.TryGetValue(table.LogicalName, out var ifaces) ? ifaces : new List<string>();
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<object> CreateInterfaceModels(
Dictionary<string, HashSet<ColumnSignature>> interfaceColumns,
List<TableModel> tablesList)
{
return interfaceColumns.Select(kvp => new
{
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<object>();
}

private static ColumnModel? FindMatchingColumn(ColumnSignature sig, List<TableModel> 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<GeneratedFile> GenerateMultipleFiles(
List<TableModel> tablesList,
Dictionary<string, HashSet<ColumnSignature>> interfaceColumns,
Dictionary<string, List<string>> tableToInterfaces,
GenerationContext context)
{
var files = new List<GeneratedFile>();

// Generate intersection interfaces
foreach (var kvp in interfaceColumns)
Expand All @@ -79,8 +193,8 @@ public IEnumerable<GeneratedFile> GenerateCode(IEnumerable<TableModel> 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));
}
Expand All @@ -94,7 +208,8 @@ public IEnumerable<GeneratedFile> GenerateCode(IEnumerable<TableModel> 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<EnumColumnModel> GetGlobalOptionsets(IEnumerable<TableModel> tables)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using DataverseProxyGenerator.Core.Generation.Utilities;
using Scriban;

namespace DataverseProxyGenerator.Core.Generation.Common;
Expand Down Expand Up @@ -44,25 +43,4 @@ protected static TemplateContext CreateTemplateContext(object model)
templateContext.PushGlobal(Scriban.Runtime.ScriptObject.From(model));
return templateContext;
}

/// <summary>
/// Sanitizes a name using the shared utility.
/// </summary>
/// <param name="name">The name to sanitize.</param>
/// <param name="fallbackPrefix">Optional fallback prefix.</param>
/// <returns>A sanitized name.</returns>
protected static string SanitizeName(string name, string fallbackPrefix = "Item")
{
return NameSanitizer.SanitizeName(name, fallbackPrefix);
}

/// <summary>
/// Gets the type signature for a column using the shared utility.
/// </summary>
/// <param name="column">The column model.</param>
/// <returns>The type signature.</returns>
protected static string GetTypeSignature(Domain.ColumnModel column)
{
return TypeSignatureHelper.GetPropertyTypeSignature(column);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

public class CustomApiGenerator : BaseFileGenerator, IFileGenerator<CustomApiModel>
{
public IEnumerable<GeneratedFile> Generate(CustomApiModel customApi, GenerationContext context)

Check failure on line 9 in src/DataverseProxyGenerator.Core/Generation/Generators/CustomApiGenerator.cs

View workflow job for this annotation

GitHub Actions / build-test-format

In member IEnumerable<GeneratedFile> CustomApiGenerator.Generate(CustomApiModel customApi, GenerationContext context), change parameter name customApi to input in order to match the identifier as it has been declared in IEnumerable<GeneratedFile> IFileGenerator<CustomApiModel>.Generate(CustomApiModel input, GenerationContext context) (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1725)

Check failure on line 9 in src/DataverseProxyGenerator.Core/Generation/Generators/CustomApiGenerator.cs

View workflow job for this annotation

GitHub Actions / build-test-format

In member IEnumerable<GeneratedFile> CustomApiGenerator.Generate(CustomApiModel customApi, GenerationContext context), change parameter name customApi to input in order to match the identifier as it has been declared in IEnumerable<GeneratedFile> IFileGenerator<CustomApiModel>.Generate(CustomApiModel input, GenerationContext context) (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1725)
{
ArgumentNullException.ThrowIfNull(customApi);
ArgumentNullException.ThrowIfNull(context);
Expand All @@ -18,7 +18,7 @@
var requestTemplate = context.Templates.GetTemplate("CustomApiRequest.scriban-cs");
var requestModel = CreateTemplateModel(customApi, context);
var requestContent = requestTemplate.Render(requestModel);
var sanitizedUniqueName = SanitizeName(customApi.UniqueName);
var sanitizedUniqueName = GenerationUtilities.SanitizeName(customApi.UniqueName);
var requestFilename = Path.Combine(FilePathHelper.CustomApiPath, $"{sanitizedUniqueName}Request.cs");

files.Add(new GeneratedFile(requestFilename, requestContent));
Expand All @@ -39,15 +39,15 @@
return new
{
unique_name = customApi.UniqueName, // Original for RequestName/ResponseName
sanitized_unique_name = SanitizeName(customApi.UniqueName), // Sanitized for class names
sanitized_unique_name = GenerationUtilities.SanitizeName(customApi.UniqueName), // Sanitized for class names
display_name = customApi.DisplayName,
description = customApi.Description,
is_function = customApi.IsFunction,
version = context.Version,
@namespace = context.Namespace,
request_parameters = customApi.RequestParameters.Select(p => new
{
name = SanitizeName(p.Name), // Sanitized for C# property names
name = GenerationUtilities.SanitizeName(p.Name), // Sanitized for C# property names
original_name = p.Name, // Original for Parameters collection access
unique_name = p.UniqueName,
display_name = p.DisplayName,
Expand All @@ -59,7 +59,7 @@
}).ToList(),
response_properties = customApi.ResponseProperties.Select(p => new
{
name = SanitizeName(p.Name), // Sanitized for C# property names
name = GenerationUtilities.SanitizeName(p.Name), // Sanitized for C# property names
original_name = p.Name, // Original for Results collection access
unique_name = p.UniqueName,
display_name = p.DisplayName,
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -17,41 +18,11 @@ private static IEnumerable<GeneratedFile> 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");

// Generate unique enum member names to handle duplicate labels using groupBy approach
var sanitizedOptions = input.OptionsetValues
.Select(kvp => new { kvp.Key, kvp.Value, SanitizedName = NameSanitizer.SanitizeEnumOptionName(kvp.Value, kvp.Key) })
.ToList();

var optionsetValuesWithUniqueNames = sanitizedOptions
.GroupBy(item => item.SanitizedName, StringComparer.OrdinalIgnoreCase)
.SelectMany(group =>
{
var items = group.ToList();
return items.Select((item, index) => new
{
Value = item.Key,
Name = index == 0 ? item.SanitizedName : $"{item.SanitizedName}_{index}",
Localizations =
input.OptionLocalizations != null &&
input.OptionLocalizations.TryGetValue(item.Key, out var value)
? value : new Dictionary<int, string>(),
});
})
.ToList();

var enumResult = template.Render(
new
{
optionsetName = sanitizedOptionSetName,
optionsetValues = optionsetValuesWithUniqueNames,
@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);
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
using DataverseProxyGenerator.Core.Generation.Common;
using DataverseProxyGenerator.Core.Generation.Mappers;
using DataverseProxyGenerator.Core.Generation.Utilities;

namespace DataverseProxyGenerator.Core.Generation.Generators;

public class HelperFileGenerator : BaseFileGenerator, IFileGenerator<string>
{
public IEnumerable<GeneratedFile> Generate(string templateName, GenerationContext context)

Check failure on line 9 in src/DataverseProxyGenerator.Core/Generation/Generators/HelperFileGenerator.cs

View workflow job for this annotation

GitHub Actions / build-test-format

In member IEnumerable<GeneratedFile> HelperFileGenerator.Generate(string templateName, GenerationContext context), change parameter name templateName to input in order to match the identifier as it has been declared in IEnumerable<GeneratedFile> IFileGenerator<string>.Generate(string input, GenerationContext context) (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1725)

Check failure on line 9 in src/DataverseProxyGenerator.Core/Generation/Generators/HelperFileGenerator.cs

View workflow job for this annotation

GitHub Actions / build-test-format

In member IEnumerable<GeneratedFile> HelperFileGenerator.Generate(string templateName, GenerationContext context), change parameter name templateName to input in order to match the identifier as it has been declared in IEnumerable<GeneratedFile> IFileGenerator<string>.Generate(string input, GenerationContext context) (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1725)
{
ArgumentNullException.ThrowIfNull(context);
ArgumentNullException.ThrowIfNull(templateName);
Expand All @@ -16,15 +17,9 @@
{
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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -16,28 +17,12 @@ public IEnumerable<GeneratedFile> Generate((string InterfaceName, IEnumerable<Co
private static IEnumerable<GeneratedFile> GenerateInternal((string InterfaceName, IEnumerable<ColumnModel> 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);
}
}
Loading
Loading