Skip to content

Commit 05452ed

Browse files
committed
Added support for Retrieve by alternate keys
1 parent 8e6e5b9 commit 05452ed

8 files changed

Lines changed: 210 additions & 4 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace DataverseProxyGenerator.Core.Domain;
2+
3+
public record AlternateKeyModel
4+
{
5+
public string SchemaName { get; init; } = string.Empty;
6+
7+
public string DisplayName { get; init; } = string.Empty;
8+
9+
public IList<ColumnModel> KeyAttributes { get; init; } = [];
10+
}

src/DataverseProxyGenerator.Core/Domain/TableModel.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,6 @@ public record TableModel
2121
public IList<ColumnModel> Columns { get; init; } = [];
2222

2323
public IList<RelationshipModel> Relationships { get; init; } = [];
24+
25+
public IList<AlternateKeyModel> Keys { get; init; } = [];
2426
}

src/DataverseProxyGenerator.Core/Generation/Mappers/ProxyClassMapper.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@ public static object MapToTemplateModel((TableModel Table, IReadOnlyList<string>
1414

1515
var processedColumns = ProcessColumnsWithClassNameConflictResolution(table.Columns, table.SchemaName);
1616

17+
if (table.SchemaName == "EnvironmentVariableDefinition") {
18+
foreach (var key in table.Keys) {
19+
Console.WriteLine(key.SchemaName);
20+
foreach (var attr in key.KeyAttributes) {
21+
Console.WriteLine($" {attr.SchemaName} : {attr.TypeName}");
22+
}
23+
}
24+
}
25+
1726
return new
1827
{
1928
SchemaName = table.SchemaName,
@@ -22,6 +31,7 @@ public static object MapToTemplateModel((TableModel Table, IReadOnlyList<string>
2231
{
2332
SchemaName = GenerationUtilities.SanitizeName(r.SchemaName),
2433
}),
34+
Keys = table.Keys,
2535
LogicalName = table.LogicalName,
2636
DisplayName = table.DisplayName,
2737
Description = table.Description,

src/DataverseProxyGenerator.Core/Metadata/DataverseMetadataFetcher.cs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ private TableModel BuildTableModelFromMetadata(Dictionary<string, EntityMetadata
185185
IsIntersect = entityMetadata.IsIntersect ?? false,
186186
Columns = new List<ColumnModel>(),
187187
Relationships = new List<RelationshipModel>(),
188+
Keys = new List<AlternateKeyModel>(),
188189
};
189190

190191
var validAttributes = entityMetadata.Attributes
@@ -204,6 +205,8 @@ private TableModel BuildTableModelFromMetadata(Dictionary<string, EntityMetadata
204205

205206
MapRelationships(logicalNameToMetadata, entityMetadata, table);
206207

208+
MapAlternateKeys(entityMetadata, table);
209+
207210
return table;
208211
}
209212

@@ -581,6 +584,57 @@ private static void MapManyToMany(Dictionary<string, EntityMetadata> logicalName
581584
}
582585
}
583586

587+
private static void MapAlternateKeys(EntityMetadata entityMetadata, TableModel table)
588+
{
589+
// Try to access Keys property through reflection if it exists
590+
// This approach allows us to work even if the SDK version doesn't expose Keys directly
591+
var keysProperty = entityMetadata.GetType().GetProperty("Keys");
592+
if (keysProperty == null)
593+
return;
594+
595+
var keys = keysProperty.GetValue(entityMetadata) as IEnumerable<object>;
596+
if (keys == null)
597+
return;
598+
599+
foreach (var key in keys)
600+
{
601+
var keyType = key.GetType();
602+
var schemaNameProperty = keyType.GetProperty("SchemaName");
603+
var displayNameProperty = keyType.GetProperty("DisplayName");
604+
var keyAttributesProperty = keyType.GetProperty("KeyAttributes");
605+
606+
if (schemaNameProperty == null || keyAttributesProperty == null)
607+
continue;
608+
609+
var schemaName = schemaNameProperty.GetValue(key) as string ?? string.Empty;
610+
var displayName = (displayNameProperty?.GetValue(key) as Label)?.UserLocalizedLabel?.Label ?? string.Empty;
611+
var keyAttributes = keyAttributesProperty.GetValue(key) as IEnumerable<string>;
612+
613+
if (keyAttributes == null)
614+
continue;
615+
616+
var alternateKeyAttributes = new List<ColumnModel>();
617+
foreach (var attrLogicalName in keyAttributes)
618+
{
619+
var attr = table.Columns.FirstOrDefault(a => a.LogicalName == attrLogicalName);
620+
if (attr != null)
621+
{
622+
alternateKeyAttributes.Add(attr);
623+
}
624+
}
625+
626+
if (alternateKeyAttributes.Count > 0)
627+
{
628+
table.Keys.Add(new AlternateKeyModel
629+
{
630+
SchemaName = schemaName,
631+
DisplayName = displayName,
632+
KeyAttributes = alternateKeyAttributes,
633+
});
634+
}
635+
}
636+
}
637+
584638
public async Task<IEnumerable<CustomApiModel>> FetchCustomApisAsync()
585639
{
586640
var customApis = new List<CustomApiModel>();

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

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -178,12 +178,9 @@ public partial class {{table.SchemaName}} : ExtendedEntity{{ if table.Interfaces
178178
set => SetRelatedEntity("{{ rel.SchemaName }}", null, value);
179179
}
180180
{{~ end ~}}
181-
{{~ if !for.last ~}}
182181

183182
{{~ end ~}}
184183
{{~ end ~}}
185-
{{~ end ~}}
186-
187184
/// <summary>
188185
/// Gets the logical column name for a property on the {{table.SchemaName}} entity, using the AttributeLogicalNameAttribute if present.
189186
/// </summary>
@@ -207,4 +204,30 @@ public partial class {{table.SchemaName}} : ExtendedEntity{{ if table.Interfaces
207204
{
208205
return service.Retrieve(id, columns);
209206
}
207+
{{~ for key in table.Keys ~}}
208+
209+
/// <summary>
210+
/// Retrieves the {{table.SchemaName}} using the {{key.DisplayName}} alternate key.
211+
/// </summary>
212+
/// <param name="service">Organization service</param>
213+
{{~ for attr in key.KeyAttributes ~}}
214+
/// <param name="{{attr.SchemaName}}">{{attr.SchemaName}} key value</param>
215+
{{~ end ~}}
216+
/// <param name="columns">Expressions that specify columns to retrieve</param>
217+
/// <returns>The retrieved {{table.SchemaName}}</returns>
218+
public static {{table.SchemaName}} Retrieve_{{key.SchemaName}}(IOrganizationService service{{ for attr in key.KeyAttributes }}, {{ if attr.TypeName == "StringColumnModel" || attr.TypeName == "MemoColumnModel" }}string{{ else if attr.TypeName == "IntegerColumnModel" }}int{{ else if attr.TypeName == "BigIntColumnModel" }}long{{ else if attr.TypeName == "BooleanColumnModel" }}bool{{ else if attr.TypeName == "DateTimeColumnModel" }}DateTime{{ else if attr.TypeName == "DecimalColumnModel" }}decimal{{ else if attr.TypeName == "DoubleColumnModel" }}double{{ else if attr.TypeName == "MoneyColumnModel" }}decimal{{ else if attr.TypeName == "EnumColumnModel" }}{{ attr.OptionsetName }}{{ else if attr.TypeName == "LookupColumnModel" }}Guid{{ else if attr.TypeName == "FileColumnModel" || attr.TypeName == "ImageColumnModel" }}byte[]{{ else if attr.TypeName == "PrimaryIdColumnModel" }}Guid{{ else if attr.TypeName == "UniqueIdentifierColumnModel" }}Guid{{ else if attr.TypeName == "BooleanManagedColumnModel" }}Bool{{ else if attr.TypeName == "ManagedColumnModel" }}{{attr.FullReturnType}}{{ else }}object{{ end }} {{attr.SchemaName}}{{ end }}, params Expression<Func<{{table.SchemaName}}, object>>[] columns)
219+
{
220+
var keyedEntityReference = new EntityReference(EntityLogicalName, new KeyAttributeCollection
221+
{
222+
{{~ for attr in key.KeyAttributes ~}}
223+
["{{attr.LogicalName}}"] = {{attr.SchemaName}},
224+
{{~ end ~}}
225+
});
226+
227+
return service.Retrieve(keyedEntityReference, columns);
228+
}
229+
{{~ if !for.last ~}}
230+
231+
{{~ end ~}}
232+
{{~ end ~}}
210233
}

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,4 +96,34 @@ public static class TableAttributeHelpers
9696

9797
return service.Retrieve(entityLogicalName, id, columnSet).ToEntity<T>();
9898
}
99+
100+
/// <summary>
101+
/// Retrieves an entity of type T using an alternate key with the specified attributes.
102+
/// </summary>
103+
/// <typeparam name="T">Type of Entity to retrieve</typeparam>
104+
/// <param name="service">Organization service</param>
105+
/// <param name="keyedEntityReference">EntityReference with alternate key values</param>
106+
/// <param name="attrs">Expressions that specify attributes to retrieve</param>
107+
/// <returns>The retrieved entity</returns>
108+
public static T Retrieve<T>(this IOrganizationService service, EntityReference keyedEntityReference, params Expression<Func<T, object>>[] attrs)
109+
where T : Entity, new()
110+
{
111+
if (service == null) throw new ArgumentNullException(nameof(service));
112+
if (keyedEntityReference == null) throw new ArgumentNullException(nameof(keyedEntityReference));
113+
114+
var req = new Microsoft.Xrm.Sdk.Messages.RetrieveRequest();
115+
req.Target = keyedEntityReference;
116+
117+
if (attrs == null || attrs.Length == 0)
118+
{
119+
req.ColumnSet = new ColumnSet(true);
120+
}
121+
else
122+
{
123+
var columnNames = attrs.Select(attr => GetColumnName(attr)).ToArray();
124+
req.ColumnSet = new ColumnSet(columnNames);
125+
}
126+
127+
return (service.Execute(req) as Microsoft.Xrm.Sdk.Messages.RetrieveResponse)?.Entity?.ToEntity<T>();
128+
}
99129
}

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,6 @@ public partial class TestEntity : ExtendedEntity
275275
set => SetAttributeValue("uniqueid", value);
276276
}
277277

278-
279278
/// <summary>
280279
/// Gets the logical column name for a property on the TestEntity entity, using the AttributeLogicalNameAttribute if present.
281280
/// </summary>

tests/DataverseProxyGenerator.Tests/RetrieveMethodTests.cs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,4 +139,82 @@ public void TableAttributeHelpers_ShouldGenerateRetrieveExtensionMethod()
139139
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
}
142+
143+
[Fact]
144+
public void EntityClass_ShouldGenerateAlternateKeyRetrieveMethods()
145+
{
146+
// Arrange
147+
var table = new TableModel
148+
{
149+
SchemaName = "Account",
150+
LogicalName = "account",
151+
DisplayName = "Account",
152+
Description = "Business account",
153+
EntityTypeCode = 1,
154+
PrimaryNameAttribute = "name",
155+
PrimaryIdAttribute = "accountid",
156+
IsIntersect = false,
157+
Columns = new ColumnModel[]
158+
{
159+
new StringColumnModel
160+
{
161+
SchemaName = "Name",
162+
LogicalName = "name",
163+
DisplayName = "Name",
164+
MaxLength = 100,
165+
},
166+
new IntegerColumnModel
167+
{
168+
SchemaName = "AccountNumber",
169+
LogicalName = "new_accountnumber",
170+
DisplayName = "Account Number",
171+
Min = 0,
172+
Max = 999999,
173+
},
174+
},
175+
Relationships = new List<RelationshipModel>(),
176+
Keys = new List<AlternateKeyModel>
177+
{
178+
new AlternateKeyModel
179+
{
180+
SchemaName = "ThisKey",
181+
DisplayName = "This Key",
182+
KeyAttributes = new List<ColumnModel>
183+
{
184+
new StringColumnModel
185+
{
186+
SchemaName = "Name",
187+
LogicalName = "name",
188+
DisplayName = "Name",
189+
MaxLength = 100,
190+
},
191+
new IntegerColumnModel
192+
{
193+
SchemaName = "AccountNumber",
194+
LogicalName = "new_accountnumber",
195+
DisplayName = "Account Number",
196+
Min = 0,
197+
Max = 999999,
198+
},
199+
},
200+
},
201+
},
202+
};
203+
204+
var generator = new CSharpProxyGenerator();
205+
206+
// Act
207+
var files = generator.GenerateCode(
208+
new[] { table },
209+
new XrmGenerationConfig("Output", "TestNamespace", "TestContextName", new Dictionary<string, IReadOnlyList<string>>(StringComparer.InvariantCulture).AsReadOnly()));
210+
var file = files.FirstOrDefault(f => f.Filename.EndsWith("Account.cs", StringComparison.InvariantCulture));
211+
212+
// Assert
213+
file.Should().NotBeNull();
214+
file!.Content.Should().Contain("public static Account Retrieve_ThisKey(IOrganizationService service, string Name, int? AccountNumber, params Expression<Func<Account, object>>[] columns)");
215+
file.Content.Should().Contain("var keyedEntityReference = new EntityReference(EntityLogicalName, \"ThisKey\", new KeyAttributeCollection");
216+
file.Content.Should().Contain("[\"name\"] = Name,");
217+
file.Content.Should().Contain("[\"new_accountnumber\"] = AccountNumber");
218+
file.Content.Should().Contain("return service.Retrieve(keyedEntityReference, columns);");
219+
}
142220
}

0 commit comments

Comments
 (0)