From 73d04849c62b8c296096e386ae24d7922ecfdda2 Mon Sep 17 00:00:00 2001 From: Morten Holt Date: Wed, 29 Oct 2025 10:25:02 +0100 Subject: [PATCH 1/4] ADD: Support for Managed properties (generating a read-only column on the table) ADD: Support for UniqueIdentifier properties (generating a Guid? column on the table) FIX: Tests weren't aligned with actual output --- .../Domain/ColumnModel.cs | 2 + .../Domain/UniqueIdentifierColumnModel.cs | 5 ++ .../Metadata/DataverseMetadataFetcher.cs | 86 ++++++++++++++++++- .../Templates/Body/EntityClass.scriban-cs | 49 ++++++++--- ...t_Code_For_All_AttributeTypes.verified.txt | 38 ++++++-- .../AttributeTypeCodeGenTests.cs | 2 + .../RetrieveMethodTests.cs | 12 +-- 7 files changed, 167 insertions(+), 27 deletions(-) create mode 100644 src/DataverseProxyGenerator.Core/Domain/UniqueIdentifierColumnModel.cs diff --git a/src/DataverseProxyGenerator.Core/Domain/ColumnModel.cs b/src/DataverseProxyGenerator.Core/Domain/ColumnModel.cs index ea64bbc..fe4e2d6 100644 --- a/src/DataverseProxyGenerator.Core/Domain/ColumnModel.cs +++ b/src/DataverseProxyGenerator.Core/Domain/ColumnModel.cs @@ -12,5 +12,7 @@ public abstract record ColumnModel public bool IsObsolete { get; init; } + public bool IsReadOnly { get; init; } + public string TypeName => GetType().Name; } \ No newline at end of file diff --git a/src/DataverseProxyGenerator.Core/Domain/UniqueIdentifierColumnModel.cs b/src/DataverseProxyGenerator.Core/Domain/UniqueIdentifierColumnModel.cs new file mode 100644 index 0000000..5c1957a --- /dev/null +++ b/src/DataverseProxyGenerator.Core/Domain/UniqueIdentifierColumnModel.cs @@ -0,0 +1,5 @@ +namespace DataverseProxyGenerator.Core.Domain; + +public record UniqueIdentifierColumnModel : ColumnModel +{ +} diff --git a/src/DataverseProxyGenerator.Core/Metadata/DataverseMetadataFetcher.cs b/src/DataverseProxyGenerator.Core/Metadata/DataverseMetadataFetcher.cs index e851be6..14af4f8 100644 --- a/src/DataverseProxyGenerator.Core/Metadata/DataverseMetadataFetcher.cs +++ b/src/DataverseProxyGenerator.Core/Metadata/DataverseMetadataFetcher.cs @@ -188,7 +188,7 @@ private TableModel BuildTableModelFromMetadata(Dictionary x.AttributeOf == null) + .Where(x => x.AttributeOf == null && x.LogicalName != entityMetadata.PrimaryIdAttribute) .ToList(); foreach (var attr in validAttributes) @@ -225,6 +225,9 @@ private TableModel BuildTableModelFromMetadata(Dictionary BuildLookupColumn(lookupAttr), FileAttributeMetadata fileAttr => BuildFileColumn(fileAttr), ImageAttributeMetadata imageAttr => BuildImageColumn(imageAttr), + ManagedPropertyAttributeMetadata managedAttr => BuildManagedPropertyColumn(managedAttr), + UniqueIdentifierAttributeMetadata uniqueAttr => BuildUniqueIdentifierColumn(uniqueAttr), + AttributeMetadata attrAttr when attrAttr.AttributeType == AttributeTypeCode.Uniqueidentifier => BuildUniqueIdentifierColumn(attrAttr), _ => null, }; @@ -451,6 +454,87 @@ private static Dictionary> BuildOptionLocalizations Description = ApplyLabelMapping(attr.Description?.UserLocalizedLabel?.Label ?? string.Empty), }; + private ColumnModel? BuildManagedPropertyColumn(ManagedPropertyAttributeMetadata attr) + { + ColumnModel? column = attr.ValueAttributeTypeCode switch + { + AttributeTypeCode.Boolean => BuildBooleanColumn(new BooleanAttributeMetadata + { + LogicalName = attr.LogicalName, + SchemaName = attr.SchemaName, + }), + AttributeTypeCode.DateTime => BuildDateTimeColumn(new DateTimeAttributeMetadata + { + LogicalName = attr.LogicalName, + SchemaName = attr.SchemaName, + }), + AttributeTypeCode.Decimal => BuildDecimalColumn(new DecimalAttributeMetadata + { + LogicalName = attr.LogicalName, + SchemaName = attr.SchemaName, + }), + AttributeTypeCode.Double => BuildDoubleColumn(new DoubleAttributeMetadata + { + LogicalName = attr.LogicalName, + SchemaName = attr.SchemaName, + }), + AttributeTypeCode.Integer => BuildIntegerColumn(new IntegerAttributeMetadata + { + LogicalName = attr.LogicalName, + SchemaName = attr.SchemaName, + }), + AttributeTypeCode.BigInt => BuildBigIntColumn(new BigIntAttributeMetadata + { + LogicalName = attr.LogicalName, + SchemaName = attr.SchemaName, + }), + AttributeTypeCode.Lookup => BuildLookupColumn(new LookupAttributeMetadata + { + LogicalName = attr.LogicalName, + SchemaName = attr.SchemaName, + }), + AttributeTypeCode.Money => BuildMoneyColumn(new MoneyAttributeMetadata + { + LogicalName = attr.LogicalName, + SchemaName = attr.SchemaName, + }), + AttributeTypeCode.Memo => BuildMemoColumn(new MemoAttributeMetadata + { + LogicalName = attr.LogicalName, + SchemaName = attr.SchemaName, + }), + AttributeTypeCode.PartyList => BuildPartyListColumn(new LookupAttributeMetadata + { + LogicalName = attr.LogicalName, + SchemaName = attr.SchemaName, + }), + AttributeTypeCode.String => BuildStringColumn(new StringAttributeMetadata + { + LogicalName = attr.LogicalName, + SchemaName = attr.SchemaName, + }), + _ => null, + }; + + if (column is not null) + { + column = column with + { + IsReadOnly = true, + }; + } + + return column; + } + + private UniqueIdentifierColumnModel BuildUniqueIdentifierColumn(AttributeMetadata attr) => new UniqueIdentifierColumnModel + { + LogicalName = attr.LogicalName, + SchemaName = attr.SchemaName, + DisplayName = ApplyLabelMapping(attr.DisplayName?.UserLocalizedLabel?.Label ?? attr.LogicalName), + Description = ApplyLabelMapping(attr.Description?.UserLocalizedLabel?.Label ?? string.Empty), + }; + private static void MapRelationships(Dictionary logicalNameToMetadata, EntityMetadata entityMetadata, TableModel table) { MapManyToOne(logicalNameToMetadata, entityMetadata, table); diff --git a/src/DataverseProxyGenerator.Core/Templates/Body/EntityClass.scriban-cs b/src/DataverseProxyGenerator.Core/Templates/Body/EntityClass.scriban-cs index abca4ee..864dabe 100644 --- a/src/DataverseProxyGenerator.Core/Templates/Body/EntityClass.scriban-cs +++ b/src/DataverseProxyGenerator.Core/Templates/Body/EntityClass.scriban-cs @@ -41,6 +41,9 @@ public partial class {{table.SchemaName}} : ExtendedEntity{{ if table.Interfaces {{~ if column.DisplayName ~}} /// Display Name: {{ column.DisplayName }} {{~ end ~}} + {{~ if column.IsReadOnly ~}} + /// This column is managed and therefore read-only. + {{~ end ~}} /// {{~ end ~}} [AttributeLogicalName("{{ column.LogicalName }}")] @@ -53,82 +56,102 @@ public partial class {{table.SchemaName}} : ExtendedEntity{{ if table.Interfaces public string {{column.SchemaName}} { get => GetAttributeValue("{{column.LogicalName}}"); - set => SetAttributeValue("{{column.LogicalName}}", value); + {{~ if !column.IsReadOnly ~}} + set => SetAttributeValue("{{column.LogicalName}}", value);{{~ end}} } {{~ else if column.TypeName == "IntegerColumnModel" ~}} [Range({{ column.Min }}, {{ column.Max }})] public int? {{column.SchemaName}} { get => GetAttributeValue("{{column.LogicalName}}"); - set => SetAttributeValue("{{column.LogicalName}}", value); + {{~ if !column.IsReadOnly ~}} + set => SetAttributeValue("{{column.LogicalName}}", value);{{~ end}} } {{~ else if column.TypeName == "BigIntColumnModel" ~}} public long? {{column.SchemaName}} { get => GetAttributeValue("{{column.LogicalName}}"); - set => SetAttributeValue("{{column.LogicalName}}", value); + {{~ if !column.IsReadOnly ~}} + set => SetAttributeValue("{{column.LogicalName}}", value);{{~ end}} } {{~ else if column.TypeName == "BooleanColumnModel" ~}} public bool? {{column.SchemaName}} { get => GetAttributeValue("{{column.LogicalName}}"); - set => SetAttributeValue("{{column.LogicalName}}", value); + {{~ if !column.IsReadOnly ~}} + set => SetAttributeValue("{{column.LogicalName}}", value);{{~ end}} } {{~ else if column.TypeName == "DateTimeColumnModel" ~}} public DateTime? {{column.SchemaName}} { get => GetAttributeValue("{{column.LogicalName}}"); - set => SetAttributeValue("{{column.LogicalName}}", value); + {{~ if !column.IsReadOnly ~}} + set => SetAttributeValue("{{column.LogicalName}}", value);{{~ end}} } {{~ else if column.TypeName == "DecimalColumnModel" ~}} public decimal? {{column.SchemaName}} { get => GetAttributeValue("{{column.LogicalName}}"); - set => SetAttributeValue("{{column.LogicalName}}", value); + {{~ if !column.IsReadOnly ~}} + set => SetAttributeValue("{{column.LogicalName}}", value);{{~ end}} } {{~ else if column.TypeName == "DoubleColumnModel" ~}} public double? {{column.SchemaName}} { get => GetAttributeValue("{{column.LogicalName}}"); - set => SetAttributeValue("{{column.LogicalName}}", value); + {{~ if !column.IsReadOnly ~}} + set => SetAttributeValue("{{column.LogicalName}}", value);{{~ end}} } {{~ else if column.TypeName == "MoneyColumnModel" ~}} public decimal? {{column.SchemaName}} { get => this.GetMoneyValue("{{column.LogicalName}}"); - set => this.SetMoneyValue("{{column.LogicalName}}", value); + {{~ if !column.IsReadOnly ~}} + set => this.SetMoneyValue("{{column.LogicalName}}", value);{{~ end}} } {{~ else if column.TypeName == "EnumColumnModel" ~}} {{~ if column.IsMultiSelect ~}} public IEnumerable<{{ column.OptionsetName }}> {{column.SchemaName}} { get => this.GetOptionSetCollectionValue<{{ column.OptionsetName }}>("{{column.LogicalName}}"); - set => this.SetOptionSetCollectionValue("{{column.LogicalName}}", value); + {{~ if !column.IsReadOnly ~}} + set => this.SetOptionSetCollectionValue("{{column.LogicalName}}", value);{{~ end}} } {{~ else ~}} public {{ column.OptionsetName }}? {{column.SchemaName}} { get => this.GetOptionSetValue<{{ column.OptionsetName }}>("{{column.LogicalName}}"); - set => this.SetOptionSetValue("{{column.LogicalName}}", value); + {{~ if !column.IsReadOnly ~}} + set => this.SetOptionSetValue("{{column.LogicalName}}", value);{{~ end}} } {{~ end ~}} {{~ else if column.TypeName == "LookupColumnModel" ~}} public EntityReference? {{column.SchemaName}} { get => GetAttributeValue("{{column.LogicalName}}"); - set => SetAttributeValue("{{column.LogicalName}}", value); + {{~ if !column.IsReadOnly ~}} + set => SetAttributeValue("{{column.LogicalName}}", value);{{~ end}} } {{~ else if column.TypeName == "PartyListColumnModel" ~}} public IEnumerable {{column.SchemaName}} { get => GetEntityCollection("{{column.LogicalName}}"); - set => SetEntityCollection("{{column.LogicalName}}", value); + {{~ if !column.IsReadOnly ~}} + set => SetEntityCollection("{{column.LogicalName}}", value);{{~ end}} } {{~ else if column.TypeName == "FileColumnModel" || column.TypeName == "ImageColumnModel" ~}} public byte[] {{column.SchemaName}} { get => GetAttributeValue("{{column.LogicalName}}"); - set => SetAttributeValue("{{column.LogicalName}}", value); + {{~ if !column.IsReadOnly ~}} + set => SetAttributeValue("{{column.LogicalName}}", value);{{~ end}} + } + {{~ else if column.TypeName == "UniqueIdentifierColumnModel" ~}} + public Guid? {{column.SchemaName}} + { + get => GetAttributeValue("{{column.LogicalName}}"); + {{~ if !column.IsReadOnly ~}} + set => SetAttributeValue("{{column.LogicalName}}", value);{{~ end}} } {{~ else if column.TypeName == "PrimaryIdColumnModel" ~}} public Guid {{column.SchemaName}} diff --git a/tests/DataverseProxyGenerator.Tests/AttributeTypeCodeGenTests.Generates_Correct_Code_For_All_AttributeTypes.verified.txt b/tests/DataverseProxyGenerator.Tests/AttributeTypeCodeGenTests.Generates_Correct_Code_For_All_AttributeTypes.verified.txt index a36ffcb..3aa3237 100644 --- a/tests/DataverseProxyGenerator.Tests/AttributeTypeCodeGenTests.Generates_Correct_Code_For_All_AttributeTypes.verified.txt +++ b/tests/DataverseProxyGenerator.Tests/AttributeTypeCodeGenTests.Generates_Correct_Code_For_All_AttributeTypes.verified.txt @@ -197,6 +197,19 @@ public partial class TestEntity : ExtendedEntity set => SetAttributeValue("ratio", value); } + /// + /// Display Name: A ReadOnly Attribute + /// This column is managed and therefore read-only. + /// + [AttributeLogicalName("readonlyattribute")] + [DisplayName("A ReadOnly Attribute")] + [MaxLength()] + public string ReadOnlyAttribute + { + get => GetAttributeValue("readonlyattribute"); + + } + /// /// Display Name: Revenue /// @@ -231,28 +244,39 @@ public partial class TestEntity : ExtendedEntity set => this.SetOptionSetValue("status", value); } + /// + /// Display Name: Unique Identifier + /// + [AttributeLogicalName("uniqueid")] + [DisplayName("Unique Identifier")] + public Guid? UniqueId + { + get => GetAttributeValue("uniqueid"); + set => SetAttributeValue("uniqueid", value); + } + /// /// Gets the logical column name for a property on the TestEntity entity, using the AttributeLogicalNameAttribute if present. /// - /// Expression to pick the column + /// 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(Expression> lambda) + public static string GetColumnName(Expression> column) { - return TableAttributeHelpers.GetColumnName(lambda); + return TableAttributeHelpers.GetColumnName(column); } /// - /// Retrieves a TestEntity with the specified attributes. + /// Retrieves the TestEntity with the specified columns. /// /// Organization service /// Id of TestEntity to retrieve - /// Expressions that specify attributes to retrieve + /// Expressions that specify columns to retrieve /// The retrieved TestEntity - public static TestEntity Retrieve(IOrganizationService service, Guid id, params Expression>[] attrs) + public static TestEntity Retrieve(IOrganizationService service, Guid id, params Expression>[] columns) { - return service.Retrieve(id, attrs); + return service.Retrieve(id, columns); } } \ No newline at end of file diff --git a/tests/DataverseProxyGenerator.Tests/AttributeTypeCodeGenTests.cs b/tests/DataverseProxyGenerator.Tests/AttributeTypeCodeGenTests.cs index be625db..6541f7f 100644 --- a/tests/DataverseProxyGenerator.Tests/AttributeTypeCodeGenTests.cs +++ b/tests/DataverseProxyGenerator.Tests/AttributeTypeCodeGenTests.cs @@ -15,6 +15,7 @@ public async Task Generates_Correct_Code_For_All_AttributeTypes() DisplayName = "Test Entity", Columns = new List { + new StringColumnModel { LogicalName = "readonlyattribute", SchemaName = "ReadOnlyAttribute", DisplayName = "A ReadOnly Attribute", IsReadOnly = true }, new StringColumnModel { LogicalName = "obsoleteattribute", SchemaName = "ObsoleteAttribute", DisplayName = "An Obsolete Attribute", IsObsolete = true }, new StringColumnModel { LogicalName = "name", SchemaName = "Name", DisplayName = "Name" }, new StringColumnModel { LogicalName = "prefix_pascalcasetest_withname", SchemaName = "prefix_pascalCaseTest_withName", DisplayName = "Pascal Test" }, @@ -49,6 +50,7 @@ public async Task Generates_Correct_Code_For_All_AttributeTypes() RelationshipName = "contact_account", }, new PartyListColumnModel { LogicalName = "participants", SchemaName = "Participants", DisplayName = "Participants" }, + new UniqueIdentifierColumnModel { LogicalName = "uniqueid", SchemaName = "UniqueId", DisplayName = "Unique Identifier" }, }, }; diff --git a/tests/DataverseProxyGenerator.Tests/RetrieveMethodTests.cs b/tests/DataverseProxyGenerator.Tests/RetrieveMethodTests.cs index 62e3052..ebc96a9 100644 --- a/tests/DataverseProxyGenerator.Tests/RetrieveMethodTests.cs +++ b/tests/DataverseProxyGenerator.Tests/RetrieveMethodTests.cs @@ -43,8 +43,8 @@ public void EntityClass_ShouldGenerateStaticRetrieveMethod() // Assert file.Should().NotBeNull(); file!.Content.Should().Contain("using System.Linq.Expressions;"); - file.Content.Should().Contain("public static Account Retrieve(IOrganizationService service, Guid id, params Expression>[] attrs)"); - file.Content.Should().Contain("return service.Retrieve(id, attrs);"); + file.Content.Should().Contain("public static Account Retrieve(IOrganizationService service, Guid id, params Expression>[] columns)"); + file.Content.Should().Contain("return service.Retrieve(id, columns);"); } [Fact] @@ -88,12 +88,12 @@ public void EntityClass_ShouldGenerateStaticGetColumnNameMethod() file.Content.Should().Contain("/// "); file.Content.Should().Contain("/// Gets the logical column name for a property on the Account entity, using the AttributeLogicalNameAttribute if present."); file.Content.Should().Contain("/// "); - file.Content.Should().Contain("/// Expression to pick the column"); + file.Content.Should().Contain("/// Expressions that specify columns to retrieve"); file.Content.Should().Contain("/// Name of column"); file.Content.Should().Contain("/// If no expression is provided"); file.Content.Should().Contain("/// If the expression is not x => x.column"); - file.Content.Should().Contain("public static string GetColumnName(Expression> lambda)"); - file.Content.Should().Contain("return TableAttributeHelpers.GetColumnName(lambda);"); + file.Content.Should().Contain("public static string GetColumnName(Expression> column)"); + file.Content.Should().Contain("return TableAttributeHelpers.GetColumnName(column);"); } [Fact] @@ -136,7 +136,7 @@ public void TableAttributeHelpers_ShouldGenerateRetrieveExtensionMethod() file!.Content.Should().Contain("using Microsoft.Xrm.Sdk.Query;"); file.Content.Should().Contain("public static T Retrieve(this IOrganizationService service, Guid id, params Expression>[] attrs)"); file.Content.Should().Contain("where T : Entity, new()"); - file.Content.Should().Contain("var columnNames = attrs.Select(attr => entity.GetColumnName(attr)).ToArray();"); + file.Content.Should().Contain("var columnNames = attrs.Select(attr => GetColumnName(attr)).ToArray();"); file.Content.Should().Contain("return service.Retrieve(entityLogicalName, id, columnSet).ToEntity();"); } } \ No newline at end of file From 6df0259d4a63cad46d0473ac501b2d25ba9263a9 Mon Sep 17 00:00:00 2001 From: Morten Holt Date: Wed, 29 Oct 2025 10:31:03 +0100 Subject: [PATCH 2/4] Make the method shorter --- .../Metadata/DataverseMetadataFetcher.cs | 41 +++++++------------ 1 file changed, 15 insertions(+), 26 deletions(-) diff --git a/src/DataverseProxyGenerator.Core/Metadata/DataverseMetadataFetcher.cs b/src/DataverseProxyGenerator.Core/Metadata/DataverseMetadataFetcher.cs index 14af4f8..9d99351 100644 --- a/src/DataverseProxyGenerator.Core/Metadata/DataverseMetadataFetcher.cs +++ b/src/DataverseProxyGenerator.Core/Metadata/DataverseMetadataFetcher.cs @@ -458,73 +458,62 @@ private static Dictionary> BuildOptionLocalizations { ColumnModel? column = attr.ValueAttributeTypeCode switch { - AttributeTypeCode.Boolean => BuildBooleanColumn(new BooleanAttributeMetadata + AttributeTypeCode.Boolean => BuildBooleanColumn(new BooleanAttributeMetadata(attr.SchemaName) { LogicalName = attr.LogicalName, - SchemaName = attr.SchemaName, }), - AttributeTypeCode.DateTime => BuildDateTimeColumn(new DateTimeAttributeMetadata + AttributeTypeCode.DateTime => BuildDateTimeColumn(new DateTimeAttributeMetadata(null, attr.SchemaName) { LogicalName = attr.LogicalName, - SchemaName = attr.SchemaName, }), - AttributeTypeCode.Decimal => BuildDecimalColumn(new DecimalAttributeMetadata + AttributeTypeCode.Decimal => BuildDecimalColumn(new DecimalAttributeMetadata(attr.SchemaName) { LogicalName = attr.LogicalName, - SchemaName = attr.SchemaName, }), - AttributeTypeCode.Double => BuildDoubleColumn(new DoubleAttributeMetadata + AttributeTypeCode.Double => BuildDoubleColumn(new DoubleAttributeMetadata(attr.SchemaName) { LogicalName = attr.LogicalName, - SchemaName = attr.SchemaName, }), - AttributeTypeCode.Integer => BuildIntegerColumn(new IntegerAttributeMetadata + AttributeTypeCode.Integer => BuildIntegerColumn(new IntegerAttributeMetadata(attr.SchemaName) { LogicalName = attr.LogicalName, - SchemaName = attr.SchemaName, }), - AttributeTypeCode.BigInt => BuildBigIntColumn(new BigIntAttributeMetadata + AttributeTypeCode.BigInt => BuildBigIntColumn(new BigIntAttributeMetadata(attr.SchemaName) { LogicalName = attr.LogicalName, - SchemaName = attr.SchemaName, }), AttributeTypeCode.Lookup => BuildLookupColumn(new LookupAttributeMetadata { LogicalName = attr.LogicalName, SchemaName = attr.SchemaName, }), - AttributeTypeCode.Money => BuildMoneyColumn(new MoneyAttributeMetadata + AttributeTypeCode.Money => BuildMoneyColumn(new MoneyAttributeMetadata(attr.SchemaName) { LogicalName = attr.LogicalName, - SchemaName = attr.SchemaName, }), - AttributeTypeCode.Memo => BuildMemoColumn(new MemoAttributeMetadata + AttributeTypeCode.Memo => BuildMemoColumn(new MemoAttributeMetadata(attr.SchemaName) { LogicalName = attr.LogicalName, - SchemaName = attr.SchemaName, }), AttributeTypeCode.PartyList => BuildPartyListColumn(new LookupAttributeMetadata { LogicalName = attr.LogicalName, SchemaName = attr.SchemaName, }), - AttributeTypeCode.String => BuildStringColumn(new StringAttributeMetadata + AttributeTypeCode.String => BuildStringColumn(new StringAttributeMetadata(attr.SchemaName) { LogicalName = attr.LogicalName, - SchemaName = attr.SchemaName, }), _ => null, }; - if (column is not null) - { - column = column with - { - IsReadOnly = true, - }; - } + if (column is null) + return column; - return column; + return column with + { + IsReadOnly = true, + }; } private UniqueIdentifierColumnModel BuildUniqueIdentifierColumn(AttributeMetadata attr) => new UniqueIdentifierColumnModel From 760a0d94e9aefc0e1877bab2dd2f43741bc0ecf5 Mon Sep 17 00:00:00 2001 From: Morten Holt Date: Wed, 29 Oct 2025 10:33:37 +0100 Subject: [PATCH 3/4] FIX: Remove newline --- .../Domain/UniqueIdentifierColumnModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DataverseProxyGenerator.Core/Domain/UniqueIdentifierColumnModel.cs b/src/DataverseProxyGenerator.Core/Domain/UniqueIdentifierColumnModel.cs index 5c1957a..bdfc0ae 100644 --- a/src/DataverseProxyGenerator.Core/Domain/UniqueIdentifierColumnModel.cs +++ b/src/DataverseProxyGenerator.Core/Domain/UniqueIdentifierColumnModel.cs @@ -2,4 +2,4 @@ namespace DataverseProxyGenerator.Core.Domain; public record UniqueIdentifierColumnModel : ColumnModel { -} +} \ No newline at end of file From 2de4ef8e791662345da43d8660a25fd7f7a218b2 Mon Sep 17 00:00:00 2001 From: Morten Holt Date: Wed, 29 Oct 2025 14:49:28 +0100 Subject: [PATCH 4/4] FIX: Managed properties are returned as ManagedProperty (or BooleanManagedProperty) --- .../Domain/BooleanManagedColumnModel.cs | 5 ++ .../Domain/ColumnModel.cs | 2 - .../Domain/ManagedColumnModel.cs | 6 ++ .../Metadata/DataverseMetadataFetcher.cs | 83 +++++++------------ .../Templates/Body/EntityClass.scriban-cs | 57 ++++++------- ...t_Code_For_All_AttributeTypes.verified.txt | 30 +++++-- .../AttributeTypeCodeGenTests.cs | 4 +- 7 files changed, 93 insertions(+), 94 deletions(-) create mode 100644 src/DataverseProxyGenerator.Core/Domain/BooleanManagedColumnModel.cs create mode 100644 src/DataverseProxyGenerator.Core/Domain/ManagedColumnModel.cs diff --git a/src/DataverseProxyGenerator.Core/Domain/BooleanManagedColumnModel.cs b/src/DataverseProxyGenerator.Core/Domain/BooleanManagedColumnModel.cs new file mode 100644 index 0000000..fceefbe --- /dev/null +++ b/src/DataverseProxyGenerator.Core/Domain/BooleanManagedColumnModel.cs @@ -0,0 +1,5 @@ +namespace DataverseProxyGenerator.Core.Domain; + +public record BooleanManagedColumnModel() : ManagedColumnModel("bool", IsNullable: false) +{ +} \ No newline at end of file diff --git a/src/DataverseProxyGenerator.Core/Domain/ColumnModel.cs b/src/DataverseProxyGenerator.Core/Domain/ColumnModel.cs index fe4e2d6..ea64bbc 100644 --- a/src/DataverseProxyGenerator.Core/Domain/ColumnModel.cs +++ b/src/DataverseProxyGenerator.Core/Domain/ColumnModel.cs @@ -12,7 +12,5 @@ public abstract record ColumnModel public bool IsObsolete { get; init; } - public bool IsReadOnly { get; init; } - public string TypeName => GetType().Name; } \ No newline at end of file diff --git a/src/DataverseProxyGenerator.Core/Domain/ManagedColumnModel.cs b/src/DataverseProxyGenerator.Core/Domain/ManagedColumnModel.cs new file mode 100644 index 0000000..21696b3 --- /dev/null +++ b/src/DataverseProxyGenerator.Core/Domain/ManagedColumnModel.cs @@ -0,0 +1,6 @@ +namespace DataverseProxyGenerator.Core.Domain; + +public record ManagedColumnModel(string ReturnType, bool IsNullable) : ColumnModel +{ + public string FullReturnType => IsNullable && ReturnType != "string" ? ReturnType + "?" : ReturnType; +} \ No newline at end of file diff --git a/src/DataverseProxyGenerator.Core/Metadata/DataverseMetadataFetcher.cs b/src/DataverseProxyGenerator.Core/Metadata/DataverseMetadataFetcher.cs index 9d99351..1c7757e 100644 --- a/src/DataverseProxyGenerator.Core/Metadata/DataverseMetadataFetcher.cs +++ b/src/DataverseProxyGenerator.Core/Metadata/DataverseMetadataFetcher.cs @@ -454,67 +454,40 @@ private static Dictionary> BuildOptionLocalizations Description = ApplyLabelMapping(attr.Description?.UserLocalizedLabel?.Label ?? string.Empty), }; - private ColumnModel? BuildManagedPropertyColumn(ManagedPropertyAttributeMetadata attr) + private ManagedColumnModel? BuildManagedPropertyColumn(ManagedPropertyAttributeMetadata attr) { - ColumnModel? column = attr.ValueAttributeTypeCode switch + return attr.ValueAttributeTypeCode switch { - AttributeTypeCode.Boolean => BuildBooleanColumn(new BooleanAttributeMetadata(attr.SchemaName) - { - LogicalName = attr.LogicalName, - }), - AttributeTypeCode.DateTime => BuildDateTimeColumn(new DateTimeAttributeMetadata(null, attr.SchemaName) - { - LogicalName = attr.LogicalName, - }), - AttributeTypeCode.Decimal => BuildDecimalColumn(new DecimalAttributeMetadata(attr.SchemaName) - { - LogicalName = attr.LogicalName, - }), - AttributeTypeCode.Double => BuildDoubleColumn(new DoubleAttributeMetadata(attr.SchemaName) - { - LogicalName = attr.LogicalName, - }), - AttributeTypeCode.Integer => BuildIntegerColumn(new IntegerAttributeMetadata(attr.SchemaName) - { - LogicalName = attr.LogicalName, - }), - AttributeTypeCode.BigInt => BuildBigIntColumn(new BigIntAttributeMetadata(attr.SchemaName) - { - LogicalName = attr.LogicalName, - }), - AttributeTypeCode.Lookup => BuildLookupColumn(new LookupAttributeMetadata - { - LogicalName = attr.LogicalName, - SchemaName = attr.SchemaName, - }), - AttributeTypeCode.Money => BuildMoneyColumn(new MoneyAttributeMetadata(attr.SchemaName) - { - LogicalName = attr.LogicalName, - }), - AttributeTypeCode.Memo => BuildMemoColumn(new MemoAttributeMetadata(attr.SchemaName) - { - LogicalName = attr.LogicalName, - }), - AttributeTypeCode.PartyList => BuildPartyListColumn(new LookupAttributeMetadata - { - LogicalName = attr.LogicalName, - SchemaName = attr.SchemaName, - }), - AttributeTypeCode.String => BuildStringColumn(new StringAttributeMetadata(attr.SchemaName) - { - LogicalName = attr.LogicalName, - }), + AttributeTypeCode.Boolean => BuildBooleanManagedColumnModel(attr), + AttributeTypeCode.DateTime => BuildManagedColumnModel(attr, "DateTime", nullable: true), + AttributeTypeCode.Decimal => BuildManagedColumnModel(attr, "decimal", nullable: true), + AttributeTypeCode.Double => BuildManagedColumnModel(attr, "double", nullable: true), + AttributeTypeCode.Integer => BuildManagedColumnModel(attr, "int", nullable: true), + AttributeTypeCode.BigInt => BuildManagedColumnModel(attr, "long", nullable: true), + AttributeTypeCode.Lookup => BuildManagedColumnModel(attr, "EntityReference", nullable: true), + AttributeTypeCode.Money => BuildManagedColumnModel(attr, "decimal", nullable: true), + AttributeTypeCode.Memo => BuildManagedColumnModel(attr, "string"), + AttributeTypeCode.PartyList => BuildManagedColumnModel(attr, "IEnumerable"), + AttributeTypeCode.String => BuildManagedColumnModel(attr, "string"), _ => null, }; + } - if (column is null) - return column; + private ManagedColumnModel BuildManagedColumnModel(AttributeMetadata attr, string returnType, bool nullable = false) => new ManagedColumnModel(returnType, nullable) + { + LogicalName = attr.LogicalName, + SchemaName = attr.SchemaName, + DisplayName = ApplyLabelMapping(attr.DisplayName?.UserLocalizedLabel?.Label ?? attr.LogicalName), + Description = ApplyLabelMapping(attr.Description?.UserLocalizedLabel?.Label ?? string.Empty), + }; - return column with - { - IsReadOnly = true, - }; - } + private BooleanManagedColumnModel BuildBooleanManagedColumnModel(AttributeMetadata attr) => new BooleanManagedColumnModel + { + LogicalName = attr.LogicalName, + SchemaName = attr.SchemaName, + DisplayName = ApplyLabelMapping(attr.DisplayName?.UserLocalizedLabel?.Label ?? attr.LogicalName), + Description = ApplyLabelMapping(attr.Description?.UserLocalizedLabel?.Label ?? string.Empty), + }; private UniqueIdentifierColumnModel BuildUniqueIdentifierColumn(AttributeMetadata attr) => new UniqueIdentifierColumnModel { diff --git a/src/DataverseProxyGenerator.Core/Templates/Body/EntityClass.scriban-cs b/src/DataverseProxyGenerator.Core/Templates/Body/EntityClass.scriban-cs index 864dabe..6c8c8de 100644 --- a/src/DataverseProxyGenerator.Core/Templates/Body/EntityClass.scriban-cs +++ b/src/DataverseProxyGenerator.Core/Templates/Body/EntityClass.scriban-cs @@ -41,9 +41,6 @@ public partial class {{table.SchemaName}} : ExtendedEntity{{ if table.Interfaces {{~ if column.DisplayName ~}} /// Display Name: {{ column.DisplayName }} {{~ end ~}} - {{~ if column.IsReadOnly ~}} - /// This column is managed and therefore read-only. - {{~ end ~}} /// {{~ end ~}} [AttributeLogicalName("{{ column.LogicalName }}")] @@ -56,102 +53,100 @@ public partial class {{table.SchemaName}} : ExtendedEntity{{ if table.Interfaces public string {{column.SchemaName}} { get => GetAttributeValue("{{column.LogicalName}}"); - {{~ if !column.IsReadOnly ~}} - set => SetAttributeValue("{{column.LogicalName}}", value);{{~ end}} + set => SetAttributeValue("{{column.LogicalName}}", value); } {{~ else if column.TypeName == "IntegerColumnModel" ~}} [Range({{ column.Min }}, {{ column.Max }})] public int? {{column.SchemaName}} { get => GetAttributeValue("{{column.LogicalName}}"); - {{~ if !column.IsReadOnly ~}} - set => SetAttributeValue("{{column.LogicalName}}", value);{{~ end}} + set => SetAttributeValue("{{column.LogicalName}}", value); } {{~ else if column.TypeName == "BigIntColumnModel" ~}} public long? {{column.SchemaName}} { get => GetAttributeValue("{{column.LogicalName}}"); - {{~ if !column.IsReadOnly ~}} - set => SetAttributeValue("{{column.LogicalName}}", value);{{~ end}} + set => SetAttributeValue("{{column.LogicalName}}", value); } {{~ else if column.TypeName == "BooleanColumnModel" ~}} public bool? {{column.SchemaName}} { get => GetAttributeValue("{{column.LogicalName}}"); - {{~ if !column.IsReadOnly ~}} - set => SetAttributeValue("{{column.LogicalName}}", value);{{~ end}} + set => SetAttributeValue("{{column.LogicalName}}", value); } {{~ else if column.TypeName == "DateTimeColumnModel" ~}} public DateTime? {{column.SchemaName}} { get => GetAttributeValue("{{column.LogicalName}}"); - {{~ if !column.IsReadOnly ~}} - set => SetAttributeValue("{{column.LogicalName}}", value);{{~ end}} + set => SetAttributeValue("{{column.LogicalName}}", value); } {{~ else if column.TypeName == "DecimalColumnModel" ~}} public decimal? {{column.SchemaName}} { get => GetAttributeValue("{{column.LogicalName}}"); - {{~ if !column.IsReadOnly ~}} - set => SetAttributeValue("{{column.LogicalName}}", value);{{~ end}} + set => SetAttributeValue("{{column.LogicalName}}", value); } {{~ else if column.TypeName == "DoubleColumnModel" ~}} public double? {{column.SchemaName}} { get => GetAttributeValue("{{column.LogicalName}}"); - {{~ if !column.IsReadOnly ~}} - set => SetAttributeValue("{{column.LogicalName}}", value);{{~ end}} + set => SetAttributeValue("{{column.LogicalName}}", value); } {{~ else if column.TypeName == "MoneyColumnModel" ~}} public decimal? {{column.SchemaName}} { get => this.GetMoneyValue("{{column.LogicalName}}"); - {{~ if !column.IsReadOnly ~}} - set => this.SetMoneyValue("{{column.LogicalName}}", value);{{~ end}} + set => this.SetMoneyValue("{{column.LogicalName}}", value); } {{~ else if column.TypeName == "EnumColumnModel" ~}} {{~ if column.IsMultiSelect ~}} public IEnumerable<{{ column.OptionsetName }}> {{column.SchemaName}} { get => this.GetOptionSetCollectionValue<{{ column.OptionsetName }}>("{{column.LogicalName}}"); - {{~ if !column.IsReadOnly ~}} - set => this.SetOptionSetCollectionValue("{{column.LogicalName}}", value);{{~ end}} + set => this.SetOptionSetCollectionValue("{{column.LogicalName}}", value); } {{~ else ~}} public {{ column.OptionsetName }}? {{column.SchemaName}} { get => this.GetOptionSetValue<{{ column.OptionsetName }}>("{{column.LogicalName}}"); - {{~ if !column.IsReadOnly ~}} - set => this.SetOptionSetValue("{{column.LogicalName}}", value);{{~ end}} + set => this.SetOptionSetValue("{{column.LogicalName}}", value); } {{~ end ~}} {{~ else if column.TypeName == "LookupColumnModel" ~}} public EntityReference? {{column.SchemaName}} { get => GetAttributeValue("{{column.LogicalName}}"); - {{~ if !column.IsReadOnly ~}} - set => SetAttributeValue("{{column.LogicalName}}", value);{{~ end}} + set => SetAttributeValue("{{column.LogicalName}}", value); } {{~ else if column.TypeName == "PartyListColumnModel" ~}} public IEnumerable {{column.SchemaName}} { get => GetEntityCollection("{{column.LogicalName}}"); - {{~ if !column.IsReadOnly ~}} - set => SetEntityCollection("{{column.LogicalName}}", value);{{~ end}} + set => SetEntityCollection("{{column.LogicalName}}", value); } {{~ else if column.TypeName == "FileColumnModel" || column.TypeName == "ImageColumnModel" ~}} public byte[] {{column.SchemaName}} { get => GetAttributeValue("{{column.LogicalName}}"); - {{~ if !column.IsReadOnly ~}} - set => SetAttributeValue("{{column.LogicalName}}", value);{{~ end}} + set => SetAttributeValue("{{column.LogicalName}}", value); } {{~ else if column.TypeName == "UniqueIdentifierColumnModel" ~}} public Guid? {{column.SchemaName}} { get => GetAttributeValue("{{column.LogicalName}}"); - {{~ if !column.IsReadOnly ~}} - set => SetAttributeValue("{{column.LogicalName}}", value);{{~ end}} + set => SetAttributeValue("{{column.LogicalName}}", value); + } + {{~ else if column.TypeName == "BooleanManagedColumnModel" ~}} + public BooleanManagedProperty {{column.SchemaName}} + { + get => GetAttributeValue("{{column.LogicalName}}"); + set => SetAttributeValue("{{column.LogicalName}}", value); + } + {{~ else if column.TypeName == "ManagedColumnModel" ~}} + public ManagedProperty<{{column.FullReturnType}}> {{column.SchemaName}} + { + get => GetAttributeValue>("{{column.LogicalName}}"); + set => SetAttributeValue("{{column.LogicalName}}", value); } {{~ else if column.TypeName == "PrimaryIdColumnModel" ~}} public Guid {{column.SchemaName}} diff --git a/tests/DataverseProxyGenerator.Tests/AttributeTypeCodeGenTests.Generates_Correct_Code_For_All_AttributeTypes.verified.txt b/tests/DataverseProxyGenerator.Tests/AttributeTypeCodeGenTests.Generates_Correct_Code_For_All_AttributeTypes.verified.txt index 3aa3237..9b5f4f8 100644 --- a/tests/DataverseProxyGenerator.Tests/AttributeTypeCodeGenTests.Generates_Correct_Code_For_All_AttributeTypes.verified.txt +++ b/tests/DataverseProxyGenerator.Tests/AttributeTypeCodeGenTests.Generates_Correct_Code_For_All_AttributeTypes.verified.txt @@ -115,6 +115,28 @@ public partial class TestEntity : ExtendedEntity set => SetAttributeValue("isactive", value); } + /// + /// Display Name: A Managed Boolean Attribute + /// + [AttributeLogicalName("managedbooleanattribute")] + [DisplayName("A Managed Boolean Attribute")] + public BooleanManagedProperty ManagedBooleanAttribute + { + get => GetAttributeValue("managedbooleanattribute"); + set => SetAttributeValue("managedbooleanattribute", value); + } + + /// + /// Display Name: A Managed DateTime Attribute + /// + [AttributeLogicalName("manageddatetimeattribute")] + [DisplayName("A Managed DateTime Attribute")] + public ManagedProperty ManagedDateTimeAttribute + { + get => GetAttributeValue>("manageddatetimeattribute"); + set => SetAttributeValue("manageddatetimeattribute", value); + } + /// /// Display Name: Name /// @@ -199,15 +221,13 @@ public partial class TestEntity : ExtendedEntity /// /// Display Name: A ReadOnly Attribute - /// This column is managed and therefore read-only. /// [AttributeLogicalName("readonlyattribute")] [DisplayName("A ReadOnly Attribute")] - [MaxLength()] - public string ReadOnlyAttribute + public ManagedProperty ReadOnlyAttribute { - get => GetAttributeValue("readonlyattribute"); - + get => GetAttributeValue>("readonlyattribute"); + set => SetAttributeValue("readonlyattribute", value); } /// diff --git a/tests/DataverseProxyGenerator.Tests/AttributeTypeCodeGenTests.cs b/tests/DataverseProxyGenerator.Tests/AttributeTypeCodeGenTests.cs index 6541f7f..e90d2b4 100644 --- a/tests/DataverseProxyGenerator.Tests/AttributeTypeCodeGenTests.cs +++ b/tests/DataverseProxyGenerator.Tests/AttributeTypeCodeGenTests.cs @@ -15,7 +15,9 @@ public async Task Generates_Correct_Code_For_All_AttributeTypes() DisplayName = "Test Entity", Columns = new List { - new StringColumnModel { LogicalName = "readonlyattribute", SchemaName = "ReadOnlyAttribute", DisplayName = "A ReadOnly Attribute", IsReadOnly = true }, + new ManagedColumnModel("string", IsNullable: false) { LogicalName = "readonlyattribute", SchemaName = "ReadOnlyAttribute", DisplayName = "A ReadOnly Attribute" }, + new BooleanManagedColumnModel { LogicalName = "managedbooleanattribute", SchemaName = "ManagedBooleanAttribute", DisplayName = "A Managed Boolean Attribute" }, + new ManagedColumnModel("DateTime", IsNullable: true) { LogicalName = "manageddatetimeattribute", SchemaName = "ManagedDateTimeAttribute", DisplayName = "A Managed DateTime Attribute" }, new StringColumnModel { LogicalName = "obsoleteattribute", SchemaName = "ObsoleteAttribute", DisplayName = "An Obsolete Attribute", IsObsolete = true }, new StringColumnModel { LogicalName = "name", SchemaName = "Name", DisplayName = "Name" }, new StringColumnModel { LogicalName = "prefix_pascalcasetest_withname", SchemaName = "prefix_pascalCaseTest_withName", DisplayName = "Pascal Test" },