Skip to content

Commit 73d0484

Browse files
committed
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
1 parent 6c0cb3b commit 73d0484

7 files changed

Lines changed: 167 additions & 27 deletions

File tree

src/DataverseProxyGenerator.Core/Domain/ColumnModel.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,7 @@ public abstract record ColumnModel
1212

1313
public bool IsObsolete { get; init; }
1414

15+
public bool IsReadOnly { get; init; }
16+
1517
public string TypeName => GetType().Name;
1618
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
namespace DataverseProxyGenerator.Core.Domain;
2+
3+
public record UniqueIdentifierColumnModel : ColumnModel
4+
{
5+
}

src/DataverseProxyGenerator.Core/Metadata/DataverseMetadataFetcher.cs

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ private TableModel BuildTableModelFromMetadata(Dictionary<string, EntityMetadata
188188
};
189189

190190
var validAttributes = entityMetadata.Attributes
191-
.Where(x => x.AttributeOf == null)
191+
.Where(x => x.AttributeOf == null && x.LogicalName != entityMetadata.PrimaryIdAttribute)
192192
.ToList();
193193

194194
foreach (var attr in validAttributes)
@@ -225,6 +225,9 @@ private TableModel BuildTableModelFromMetadata(Dictionary<string, EntityMetadata
225225
LookupAttributeMetadata lookupAttr => BuildLookupColumn(lookupAttr),
226226
FileAttributeMetadata fileAttr => BuildFileColumn(fileAttr),
227227
ImageAttributeMetadata imageAttr => BuildImageColumn(imageAttr),
228+
ManagedPropertyAttributeMetadata managedAttr => BuildManagedPropertyColumn(managedAttr),
229+
UniqueIdentifierAttributeMetadata uniqueAttr => BuildUniqueIdentifierColumn(uniqueAttr),
230+
AttributeMetadata attrAttr when attrAttr.AttributeType == AttributeTypeCode.Uniqueidentifier => BuildUniqueIdentifierColumn(attrAttr),
228231
_ => null,
229232
};
230233

@@ -451,6 +454,87 @@ private static Dictionary<int, Dictionary<int, string>> BuildOptionLocalizations
451454
Description = ApplyLabelMapping(attr.Description?.UserLocalizedLabel?.Label ?? string.Empty),
452455
};
453456

457+
private ColumnModel? BuildManagedPropertyColumn(ManagedPropertyAttributeMetadata attr)
458+
{
459+
ColumnModel? column = attr.ValueAttributeTypeCode switch
460+
{
461+
AttributeTypeCode.Boolean => BuildBooleanColumn(new BooleanAttributeMetadata
462+
{
463+
LogicalName = attr.LogicalName,
464+
SchemaName = attr.SchemaName,
465+
}),
466+
AttributeTypeCode.DateTime => BuildDateTimeColumn(new DateTimeAttributeMetadata
467+
{
468+
LogicalName = attr.LogicalName,
469+
SchemaName = attr.SchemaName,
470+
}),
471+
AttributeTypeCode.Decimal => BuildDecimalColumn(new DecimalAttributeMetadata
472+
{
473+
LogicalName = attr.LogicalName,
474+
SchemaName = attr.SchemaName,
475+
}),
476+
AttributeTypeCode.Double => BuildDoubleColumn(new DoubleAttributeMetadata
477+
{
478+
LogicalName = attr.LogicalName,
479+
SchemaName = attr.SchemaName,
480+
}),
481+
AttributeTypeCode.Integer => BuildIntegerColumn(new IntegerAttributeMetadata
482+
{
483+
LogicalName = attr.LogicalName,
484+
SchemaName = attr.SchemaName,
485+
}),
486+
AttributeTypeCode.BigInt => BuildBigIntColumn(new BigIntAttributeMetadata
487+
{
488+
LogicalName = attr.LogicalName,
489+
SchemaName = attr.SchemaName,
490+
}),
491+
AttributeTypeCode.Lookup => BuildLookupColumn(new LookupAttributeMetadata
492+
{
493+
LogicalName = attr.LogicalName,
494+
SchemaName = attr.SchemaName,
495+
}),
496+
AttributeTypeCode.Money => BuildMoneyColumn(new MoneyAttributeMetadata
497+
{
498+
LogicalName = attr.LogicalName,
499+
SchemaName = attr.SchemaName,
500+
}),
501+
AttributeTypeCode.Memo => BuildMemoColumn(new MemoAttributeMetadata
502+
{
503+
LogicalName = attr.LogicalName,
504+
SchemaName = attr.SchemaName,
505+
}),
506+
AttributeTypeCode.PartyList => BuildPartyListColumn(new LookupAttributeMetadata
507+
{
508+
LogicalName = attr.LogicalName,
509+
SchemaName = attr.SchemaName,
510+
}),
511+
AttributeTypeCode.String => BuildStringColumn(new StringAttributeMetadata
512+
{
513+
LogicalName = attr.LogicalName,
514+
SchemaName = attr.SchemaName,
515+
}),
516+
_ => null,
517+
};
518+
519+
if (column is not null)
520+
{
521+
column = column with
522+
{
523+
IsReadOnly = true,
524+
};
525+
}
526+
527+
return column;
528+
}
529+
530+
private UniqueIdentifierColumnModel BuildUniqueIdentifierColumn(AttributeMetadata attr) => new UniqueIdentifierColumnModel
531+
{
532+
LogicalName = attr.LogicalName,
533+
SchemaName = attr.SchemaName,
534+
DisplayName = ApplyLabelMapping(attr.DisplayName?.UserLocalizedLabel?.Label ?? attr.LogicalName),
535+
Description = ApplyLabelMapping(attr.Description?.UserLocalizedLabel?.Label ?? string.Empty),
536+
};
537+
454538
private static void MapRelationships(Dictionary<string, EntityMetadata> logicalNameToMetadata, EntityMetadata entityMetadata, TableModel table)
455539
{
456540
MapManyToOne(logicalNameToMetadata, entityMetadata, table);

src/DataverseProxyGenerator.Core/Templates/Body/EntityClass.scriban-cs

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ public partial class {{table.SchemaName}} : ExtendedEntity{{ if table.Interfaces
4141
{{~ if column.DisplayName ~}}
4242
/// <para>Display Name: {{ column.DisplayName }}</para>
4343
{{~ end ~}}
44+
{{~ if column.IsReadOnly ~}}
45+
/// <para>This column is managed and therefore read-only.</para>
46+
{{~ end ~}}
4447
/// </summary>
4548
{{~ end ~}}
4649
[AttributeLogicalName("{{ column.LogicalName }}")]
@@ -53,82 +56,102 @@ public partial class {{table.SchemaName}} : ExtendedEntity{{ if table.Interfaces
5356
public string {{column.SchemaName}}
5457
{
5558
get => GetAttributeValue<string>("{{column.LogicalName}}");
56-
set => SetAttributeValue("{{column.LogicalName}}", value);
59+
{{~ if !column.IsReadOnly ~}}
60+
set => SetAttributeValue("{{column.LogicalName}}", value);{{~ end}}
5761
}
5862
{{~ else if column.TypeName == "IntegerColumnModel" ~}}
5963
[Range({{ column.Min }}, {{ column.Max }})]
6064
public int? {{column.SchemaName}}
6165
{
6266
get => GetAttributeValue<int?>("{{column.LogicalName}}");
63-
set => SetAttributeValue("{{column.LogicalName}}", value);
67+
{{~ if !column.IsReadOnly ~}}
68+
set => SetAttributeValue("{{column.LogicalName}}", value);{{~ end}}
6469
}
6570
{{~ else if column.TypeName == "BigIntColumnModel" ~}}
6671
public long? {{column.SchemaName}}
6772
{
6873
get => GetAttributeValue<long?>("{{column.LogicalName}}");
69-
set => SetAttributeValue("{{column.LogicalName}}", value);
74+
{{~ if !column.IsReadOnly ~}}
75+
set => SetAttributeValue("{{column.LogicalName}}", value);{{~ end}}
7076
}
7177
{{~ else if column.TypeName == "BooleanColumnModel" ~}}
7278
public bool? {{column.SchemaName}}
7379
{
7480
get => GetAttributeValue<bool?>("{{column.LogicalName}}");
75-
set => SetAttributeValue("{{column.LogicalName}}", value);
81+
{{~ if !column.IsReadOnly ~}}
82+
set => SetAttributeValue("{{column.LogicalName}}", value);{{~ end}}
7683
}
7784
{{~ else if column.TypeName == "DateTimeColumnModel" ~}}
7885
public DateTime? {{column.SchemaName}}
7986
{
8087
get => GetAttributeValue<DateTime?>("{{column.LogicalName}}");
81-
set => SetAttributeValue("{{column.LogicalName}}", value);
88+
{{~ if !column.IsReadOnly ~}}
89+
set => SetAttributeValue("{{column.LogicalName}}", value);{{~ end}}
8290
}
8391
{{~ else if column.TypeName == "DecimalColumnModel" ~}}
8492
public decimal? {{column.SchemaName}}
8593
{
8694
get => GetAttributeValue<decimal?>("{{column.LogicalName}}");
87-
set => SetAttributeValue("{{column.LogicalName}}", value);
95+
{{~ if !column.IsReadOnly ~}}
96+
set => SetAttributeValue("{{column.LogicalName}}", value);{{~ end}}
8897
}
8998
{{~ else if column.TypeName == "DoubleColumnModel" ~}}
9099
public double? {{column.SchemaName}}
91100
{
92101
get => GetAttributeValue<double?>("{{column.LogicalName}}");
93-
set => SetAttributeValue("{{column.LogicalName}}", value);
102+
{{~ if !column.IsReadOnly ~}}
103+
set => SetAttributeValue("{{column.LogicalName}}", value);{{~ end}}
94104
}
95105
{{~ else if column.TypeName == "MoneyColumnModel" ~}}
96106
public decimal? {{column.SchemaName}}
97107
{
98108
get => this.GetMoneyValue("{{column.LogicalName}}");
99-
set => this.SetMoneyValue("{{column.LogicalName}}", value);
109+
{{~ if !column.IsReadOnly ~}}
110+
set => this.SetMoneyValue("{{column.LogicalName}}", value);{{~ end}}
100111
}
101112
{{~ else if column.TypeName == "EnumColumnModel" ~}}
102113
{{~ if column.IsMultiSelect ~}}
103114
public IEnumerable<{{ column.OptionsetName }}> {{column.SchemaName}}
104115
{
105116
get => this.GetOptionSetCollectionValue<{{ column.OptionsetName }}>("{{column.LogicalName}}");
106-
set => this.SetOptionSetCollectionValue("{{column.LogicalName}}", value);
117+
{{~ if !column.IsReadOnly ~}}
118+
set => this.SetOptionSetCollectionValue("{{column.LogicalName}}", value);{{~ end}}
107119
}
108120
{{~ else ~}}
109121
public {{ column.OptionsetName }}? {{column.SchemaName}}
110122
{
111123
get => this.GetOptionSetValue<{{ column.OptionsetName }}>("{{column.LogicalName}}");
112-
set => this.SetOptionSetValue("{{column.LogicalName}}", value);
124+
{{~ if !column.IsReadOnly ~}}
125+
set => this.SetOptionSetValue("{{column.LogicalName}}", value);{{~ end}}
113126
}
114127
{{~ end ~}}
115128
{{~ else if column.TypeName == "LookupColumnModel" ~}}
116129
public EntityReference? {{column.SchemaName}}
117130
{
118131
get => GetAttributeValue<EntityReference?>("{{column.LogicalName}}");
119-
set => SetAttributeValue("{{column.LogicalName}}", value);
132+
{{~ if !column.IsReadOnly ~}}
133+
set => SetAttributeValue("{{column.LogicalName}}", value);{{~ end}}
120134
}
121135
{{~ else if column.TypeName == "PartyListColumnModel" ~}}
122136
public IEnumerable<ActivityParty> {{column.SchemaName}}
123137
{
124138
get => GetEntityCollection<ActivityParty>("{{column.LogicalName}}");
125-
set => SetEntityCollection("{{column.LogicalName}}", value);
139+
{{~ if !column.IsReadOnly ~}}
140+
set => SetEntityCollection("{{column.LogicalName}}", value);{{~ end}}
126141
}
127142
{{~ else if column.TypeName == "FileColumnModel" || column.TypeName == "ImageColumnModel" ~}}
128143
public byte[] {{column.SchemaName}}
129144
{
130145
get => GetAttributeValue<byte[]>("{{column.LogicalName}}");
131-
set => SetAttributeValue("{{column.LogicalName}}", value);
146+
{{~ if !column.IsReadOnly ~}}
147+
set => SetAttributeValue("{{column.LogicalName}}", value);{{~ end}}
148+
}
149+
{{~ else if column.TypeName == "UniqueIdentifierColumnModel" ~}}
150+
public Guid? {{column.SchemaName}}
151+
{
152+
get => GetAttributeValue<Guid?>("{{column.LogicalName}}");
153+
{{~ if !column.IsReadOnly ~}}
154+
set => SetAttributeValue("{{column.LogicalName}}", value);{{~ end}}
132155
}
133156
{{~ else if column.TypeName == "PrimaryIdColumnModel" ~}}
134157
public Guid {{column.SchemaName}}

tests/DataverseProxyGenerator.Tests/AttributeTypeCodeGenTests.Generates_Correct_Code_For_All_AttributeTypes.verified.txt

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,19 @@ public partial class TestEntity : ExtendedEntity
197197
set => SetAttributeValue("ratio", value);
198198
}
199199

200+
/// <summary>
201+
/// <para>Display Name: A ReadOnly Attribute</para>
202+
/// <para>This column is managed and therefore read-only.</para>
203+
/// </summary>
204+
[AttributeLogicalName("readonlyattribute")]
205+
[DisplayName("A ReadOnly Attribute")]
206+
[MaxLength()]
207+
public string ReadOnlyAttribute
208+
{
209+
get => GetAttributeValue<string>("readonlyattribute");
210+
211+
}
212+
200213
/// <summary>
201214
/// <para>Display Name: Revenue</para>
202215
/// </summary>
@@ -231,28 +244,39 @@ public partial class TestEntity : ExtendedEntity
231244
set => this.SetOptionSetValue("status", value);
232245
}
233246

247+
/// <summary>
248+
/// <para>Display Name: Unique Identifier</para>
249+
/// </summary>
250+
[AttributeLogicalName("uniqueid")]
251+
[DisplayName("Unique Identifier")]
252+
public Guid? UniqueId
253+
{
254+
get => GetAttributeValue<Guid?>("uniqueid");
255+
set => SetAttributeValue("uniqueid", value);
256+
}
257+
234258

235259
/// <summary>
236260
/// Gets the logical column name for a property on the TestEntity entity, using the AttributeLogicalNameAttribute if present.
237261
/// </summary>
238-
/// <param name="lambda">Expression to pick the column</param>
262+
/// <param name="column">Expression to pick the column</param>
239263
/// <returns>Name of column</returns>
240264
/// <exception cref="ArgumentNullException">If no expression is provided</exception>
241265
/// <exception cref="ArgumentException">If the expression is not x => x.column</exception>
242-
public static string GetColumnName(Expression<Func<TestEntity, object>> lambda)
266+
public static string GetColumnName(Expression<Func<TestEntity, object>> column)
243267
{
244-
return TableAttributeHelpers.GetColumnName(lambda);
268+
return TableAttributeHelpers.GetColumnName(column);
245269
}
246270

247271
/// <summary>
248-
/// Retrieves a TestEntity with the specified attributes.
272+
/// Retrieves the TestEntity with the specified columns.
249273
/// </summary>
250274
/// <param name="service">Organization service</param>
251275
/// <param name="id">Id of TestEntity to retrieve</param>
252-
/// <param name="attrs">Expressions that specify attributes to retrieve</param>
276+
/// <param name="columns">Expressions that specify columns to retrieve</param>
253277
/// <returns>The retrieved TestEntity</returns>
254-
public static TestEntity Retrieve(IOrganizationService service, Guid id, params Expression<Func<TestEntity, object>>[] attrs)
278+
public static TestEntity Retrieve(IOrganizationService service, Guid id, params Expression<Func<TestEntity, object>>[] columns)
255279
{
256-
return service.Retrieve(id, attrs);
280+
return service.Retrieve(id, columns);
257281
}
258282
}

tests/DataverseProxyGenerator.Tests/AttributeTypeCodeGenTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public async Task Generates_Correct_Code_For_All_AttributeTypes()
1515
DisplayName = "Test Entity",
1616
Columns = new List<ColumnModel>
1717
{
18+
new StringColumnModel { LogicalName = "readonlyattribute", SchemaName = "ReadOnlyAttribute", DisplayName = "A ReadOnly Attribute", IsReadOnly = true },
1819
new StringColumnModel { LogicalName = "obsoleteattribute", SchemaName = "ObsoleteAttribute", DisplayName = "An Obsolete Attribute", IsObsolete = true },
1920
new StringColumnModel { LogicalName = "name", SchemaName = "Name", DisplayName = "Name" },
2021
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()
4950
RelationshipName = "contact_account",
5051
},
5152
new PartyListColumnModel { LogicalName = "participants", SchemaName = "Participants", DisplayName = "Participants" },
53+
new UniqueIdentifierColumnModel { LogicalName = "uniqueid", SchemaName = "UniqueId", DisplayName = "Unique Identifier" },
5254
},
5355
};
5456

tests/DataverseProxyGenerator.Tests/RetrieveMethodTests.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ public void EntityClass_ShouldGenerateStaticRetrieveMethod()
4343
// Assert
4444
file.Should().NotBeNull();
4545
file!.Content.Should().Contain("using System.Linq.Expressions;");
46-
file.Content.Should().Contain("public static Account Retrieve(IOrganizationService service, Guid id, params Expression<Func<Account, object>>[] attrs)");
47-
file.Content.Should().Contain("return service.Retrieve(id, attrs);");
46+
file.Content.Should().Contain("public static Account Retrieve(IOrganizationService service, Guid id, params Expression<Func<Account, object>>[] columns)");
47+
file.Content.Should().Contain("return service.Retrieve(id, columns);");
4848
}
4949

5050
[Fact]
@@ -88,12 +88,12 @@ public void EntityClass_ShouldGenerateStaticGetColumnNameMethod()
8888
file.Content.Should().Contain("/// <summary>");
8989
file.Content.Should().Contain("/// Gets the logical column name for a property on the Account entity, using the AttributeLogicalNameAttribute if present.");
9090
file.Content.Should().Contain("/// </summary>");
91-
file.Content.Should().Contain("/// <param name=\"lambda\">Expression to pick the column</param>");
91+
file.Content.Should().Contain("/// <param name=\"columns\">Expressions that specify columns to retrieve</param>");
9292
file.Content.Should().Contain("/// <returns>Name of column</returns>");
9393
file.Content.Should().Contain("/// <exception cref=\"ArgumentNullException\">If no expression is provided</exception>");
9494
file.Content.Should().Contain("/// <exception cref=\"ArgumentException\">If the expression is not x => x.column</exception>");
95-
file.Content.Should().Contain("public static string GetColumnName(Expression<Func<Account, object>> lambda)");
96-
file.Content.Should().Contain("return TableAttributeHelpers.GetColumnName(lambda);");
95+
file.Content.Should().Contain("public static string GetColumnName(Expression<Func<Account, object>> column)");
96+
file.Content.Should().Contain("return TableAttributeHelpers.GetColumnName(column);");
9797
}
9898

9999
[Fact]
@@ -136,7 +136,7 @@ public void TableAttributeHelpers_ShouldGenerateRetrieveExtensionMethod()
136136
file!.Content.Should().Contain("using Microsoft.Xrm.Sdk.Query;");
137137
file.Content.Should().Contain("public static T Retrieve<T>(this IOrganizationService service, Guid id, params Expression<Func<T, object>>[] attrs)");
138138
file.Content.Should().Contain("where T : Entity, new()");
139-
file.Content.Should().Contain("var columnNames = attrs.Select(attr => entity.GetColumnName(attr)).ToArray();");
139+
file.Content.Should().Contain("var columnNames = attrs.Select(attr => GetColumnName(attr)).ToArray();");
140140
file.Content.Should().Contain("return service.Retrieve(entityLogicalName, id, columnSet).ToEntity<T>();");
141141
}
142142
}

0 commit comments

Comments
 (0)