Skip to content

Commit d668afb

Browse files
committed
updated summaries to show specific tables on relevant componentTypes
1 parent 7e85d25 commit d668afb

5 files changed

Lines changed: 172 additions & 49 deletions

File tree

Generator/DTO/SolutionComponent.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ public record SolutionComponentData(
6262
string SchemaName,
6363
SolutionComponentType ComponentType,
6464
Guid ObjectId,
65-
bool IsExplicit);
65+
bool IsExplicit,
66+
string? RelatedTable = null);
6667

6768
/// <summary>
6869
/// Collection of solution components grouped by solution.

Generator/DataverseService.cs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,27 @@ public DataverseService(
305305
r => r.MetadataId!.Value,
306306
r => r.SchemaName);
307307

308+
// Build entity lookups for attributes, relationships, and keys (maps component ID to parent entity name)
309+
var attributeEntityLookup = entitiesInSolutionMetadata
310+
.SelectMany(e => e.Attributes.Where(a => a.MetadataId.HasValue)
311+
.Select(a => (AttributeId: a.MetadataId!.Value, EntityName: e.DisplayName.ToLabelString() ?? e.LogicalName)))
312+
.ToDictionary(x => x.AttributeId, x => x.EntityName);
313+
314+
var relationshipEntityLookup = entitiesInSolutionMetadata
315+
.SelectMany(e => e.ManyToManyRelationships.Cast<RelationshipMetadataBase>()
316+
.Concat(e.OneToManyRelationships)
317+
.Concat(e.ManyToOneRelationships)
318+
.Where(r => r.MetadataId.HasValue)
319+
.Select(r => (RelationshipId: r.MetadataId!.Value, EntityName: e.DisplayName.ToLabelString() ?? e.LogicalName)))
320+
.DistinctBy(x => x.RelationshipId)
321+
.ToDictionary(x => x.RelationshipId, x => x.EntityName);
322+
323+
var keyEntityLookup = entitiesInSolutionMetadata
324+
.SelectMany(e => (e.Keys ?? Array.Empty<EntityKeyMetadata>())
325+
.Where(k => k.MetadataId.HasValue)
326+
.Select(k => (KeyId: k.MetadataId!.Value, EntityName: e.DisplayName.ToLabelString() ?? e.LogicalName)))
327+
.ToDictionary(x => x.KeyId, x => x.EntityName);
328+
308329
// Build solution name lookup
309330
var solutionNameLookup = solutionLookup.ToDictionary(
310331
kvp => kvp.Key,
@@ -315,7 +336,10 @@ public DataverseService(
315336
solutionNameLookup,
316337
entityNameLookup,
317338
attributeNameLookup,
318-
relationshipNameLookup);
339+
relationshipNameLookup,
340+
attributeEntityLookup,
341+
relationshipEntityLookup,
342+
keyEntityLookup);
319343

320344
logger.LogInformation($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Extracted components for {solutionComponentCollections.Count} solutions");
321345
}

Generator/Services/SolutionComponentExtractor.cs

Lines changed: 108 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -48,33 +48,39 @@ public class SolutionComponentExtractor
4848
};
4949

5050
/// <summary>
51-
/// Maps component type codes to their Dataverse table, name column, and primary key column for name resolution.
51+
/// Maps component type codes to their Dataverse table, name column, primary key column, and optional entity column for name resolution.
5252
/// Primary key is optional - if null, defaults to tablename + "id".
53+
/// EntityColumn is used to get the related table for components like forms and views.
5354
/// </summary>
54-
private static readonly Dictionary<int, (string TableName, string NameColumn, string? PrimaryKey)> ComponentTableMap = new()
55+
private static readonly Dictionary<int, (string TableName, string NameColumn, string? PrimaryKey, string? EntityColumn)> ComponentTableMap = new()
5556
{
56-
{ 20, ("role", "name", null) },
57-
{ 26, ("savedquery", "name", null) },
58-
{ 29, ("workflow", "name", null) },
59-
{ 50, ("ribboncustomization", "entity", null) },
60-
{ 59, ("savedqueryvisualization", "name", null) },
61-
{ 60, ("systemform", "name", "formid") }, // systemform uses formid, not systemformid
62-
{ 61, ("webresource", "name", null) },
63-
{ 62, ("sitemap", "sitemapname", null) },
64-
{ 63, ("connectionrole", "name", null) },
65-
{ 65, ("hierarchyrule", "name", null) },
66-
{ 66, ("customcontrol", "name", null) },
67-
{ 70, ("fieldsecurityprofile", "name", null) },
68-
{ 80, ("appmodule", "name", "appmoduleid") }, // appmodule uses appmoduleid
69-
{ 91, ("pluginassembly", "name", null) },
70-
{ 92, ("sdkmessageprocessingstep", "name", null) },
71-
{ 300, ("canvasapp", "name", null) },
72-
{ 372, ("connectionreference", "connectionreferencedisplayname", null) },
73-
{ 380, ("environmentvariabledefinition", "displayname", null) },
74-
{ 381, ("environmentvariablevalue", "schemaname", null) },
75-
{ 418, ("workflow", "name", null) }, // Dataflows are stored in workflow table with category=6
57+
{ 20, ("role", "name", null, null) },
58+
{ 26, ("savedquery", "name", null, "returnedtypecode") }, // Views have returnedtypecode for the entity
59+
{ 29, ("workflow", "name", null, null) },
60+
{ 50, ("ribboncustomization", "entity", null, null) },
61+
{ 59, ("savedqueryvisualization", "name", null, null) },
62+
{ 60, ("systemform", "name", "formid", "objecttypecode") }, // Forms have objecttypecode for the entity
63+
{ 61, ("webresource", "name", null, null) },
64+
{ 62, ("sitemap", "sitemapname", null, null) },
65+
{ 63, ("connectionrole", "name", null, null) },
66+
{ 65, ("hierarchyrule", "name", null, null) },
67+
{ 66, ("customcontrol", "name", null, null) },
68+
{ 70, ("fieldsecurityprofile", "name", null, null) },
69+
{ 80, ("appmodule", "name", "appmoduleid", null) }, // appmodule uses appmoduleid
70+
{ 91, ("pluginassembly", "name", null, null) },
71+
{ 92, ("sdkmessageprocessingstep", "name", null, null) },
72+
{ 300, ("canvasapp", "name", null, null) },
73+
{ 372, ("connectionreference", "connectionreferencedisplayname", null, null) },
74+
{ 380, ("environmentvariabledefinition", "displayname", null, null) },
75+
{ 381, ("environmentvariablevalue", "schemaname", null, null) },
76+
{ 418, ("workflow", "name", null, null) }, // Dataflows are stored in workflow table with category=6
7677
};
7778

79+
/// <summary>
80+
/// Component types that should have a related table displayed.
81+
/// </summary>
82+
private static readonly HashSet<int> ComponentTypesWithRelatedTable = new() { 2, 10, 14, 26, 60 }; // Attribute, Relationship, EntityKey, SavedQuery (View), SystemForm
83+
7884
public SolutionComponentExtractor(ServiceClient client, ILogger<SolutionComponentExtractor> logger)
7985
{
8086
_client = client;
@@ -89,7 +95,10 @@ public async Task<List<SolutionComponentCollection>> ExtractSolutionComponentsAs
8995
Dictionary<Guid, string> solutionNameLookup,
9096
Dictionary<Guid, string>? entityNameLookup = null,
9197
Dictionary<Guid, string>? attributeNameLookup = null,
92-
Dictionary<Guid, string>? relationshipNameLookup = null)
98+
Dictionary<Guid, string>? relationshipNameLookup = null,
99+
Dictionary<Guid, string>? attributeEntityLookup = null,
100+
Dictionary<Guid, string>? relationshipEntityLookup = null,
101+
Dictionary<Guid, string>? keyEntityLookup = null)
93102
{
94103
_logger.LogInformation($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Extracting solution components for {solutionIds.Count} solutions");
95104

@@ -109,7 +118,7 @@ public async Task<List<SolutionComponentCollection>> ExtractSolutionComponentsAs
109118
.ToDictionary(g => g.Key, g => g.ToList());
110119

111120
// Resolve names for each component type
112-
var nameCache = await BuildNameCacheAsync(rawComponents, entityNameLookup, attributeNameLookup, relationshipNameLookup);
121+
var nameCache = await BuildNameCacheAsync(rawComponents, entityNameLookup, attributeNameLookup, relationshipNameLookup, attributeEntityLookup, relationshipEntityLookup, keyEntityLookup);
113122

114123
// Build the result collections
115124
var result = new List<SolutionComponentCollection>();
@@ -129,7 +138,8 @@ public async Task<List<SolutionComponentCollection>> ExtractSolutionComponentsAs
129138
SchemaName: ResolveComponentSchemaName(c, nameCache),
130139
ComponentType: (SolutionComponentType)c.ComponentType,
131140
ObjectId: c.ObjectId,
132-
IsExplicit: c.IsExplicit))
141+
IsExplicit: c.IsExplicit,
142+
RelatedTable: ResolveRelatedTable(c, nameCache)))
133143
.OrderBy(c => c.ComponentType)
134144
.ThenBy(c => c.Name)
135145
.ToList();
@@ -184,13 +194,16 @@ private async Task<List<RawComponentInfo>> QuerySolutionComponentsAsync(List<Gui
184194
return results;
185195
}
186196

187-
private async Task<Dictionary<(int ComponentType, Guid ObjectId), (string Name, string SchemaName)>> BuildNameCacheAsync(
197+
private async Task<Dictionary<(int ComponentType, Guid ObjectId), (string Name, string SchemaName, string? RelatedTable)>> BuildNameCacheAsync(
188198
List<RawComponentInfo> components,
189199
Dictionary<Guid, string>? entityNameLookup,
190200
Dictionary<Guid, string>? attributeNameLookup,
191-
Dictionary<Guid, string>? relationshipNameLookup)
201+
Dictionary<Guid, string>? relationshipNameLookup,
202+
Dictionary<Guid, string>? attributeEntityLookup,
203+
Dictionary<Guid, string>? relationshipEntityLookup,
204+
Dictionary<Guid, string>? keyEntityLookup)
192205
{
193-
var cache = new Dictionary<(int, Guid), (string Name, string SchemaName)>();
206+
var cache = new Dictionary<(int, Guid), (string Name, string SchemaName, string? RelatedTable)>();
194207

195208
// Group components by type for batch queries
196209
var componentsByType = components
@@ -206,7 +219,7 @@ private async Task<List<RawComponentInfo>> QuerySolutionComponentsAsync(List<Gui
206219
{
207220
if (entityNameLookup.TryGetValue(objectId, out var name))
208221
{
209-
cache[(componentType, objectId)] = (name, name);
222+
cache[(componentType, objectId)] = (name, name, null);
210223
}
211224
}
212225
continue;
@@ -218,7 +231,8 @@ private async Task<List<RawComponentInfo>> QuerySolutionComponentsAsync(List<Gui
218231
{
219232
if (attributeNameLookup.TryGetValue(objectId, out var name))
220233
{
221-
cache[(componentType, objectId)] = (name, name);
234+
var relatedTable = attributeEntityLookup?.GetValueOrDefault(objectId);
235+
cache[(componentType, objectId)] = (name, name, relatedTable);
222236
}
223237
}
224238
continue;
@@ -230,18 +244,30 @@ private async Task<List<RawComponentInfo>> QuerySolutionComponentsAsync(List<Gui
230244
{
231245
if (relationshipNameLookup.TryGetValue(objectId, out var name))
232246
{
233-
cache[(componentType, objectId)] = (name, name);
247+
var relatedTable = relationshipEntityLookup?.GetValueOrDefault(objectId);
248+
cache[(componentType, objectId)] = (name, name, relatedTable);
234249
}
235250
}
236251
continue;
237252
}
238253

239-
// Skip types that need metadata API (9=OptionSet, 14=EntityKey) - use ObjectId as fallback
240-
if (componentType == 9 || componentType == 14)
254+
// EntityKey - use keyEntityLookup for related table
255+
if (componentType == 14)
241256
{
242257
foreach (var objectId in objectIds)
243258
{
244-
cache[(componentType, objectId)] = (objectId.ToString(), objectId.ToString());
259+
var relatedTable = keyEntityLookup?.GetValueOrDefault(objectId);
260+
cache[(componentType, objectId)] = (objectId.ToString(), objectId.ToString(), relatedTable);
261+
}
262+
continue;
263+
}
264+
265+
// Skip OptionSet - use ObjectId as fallback, no related table
266+
if (componentType == 9)
267+
{
268+
foreach (var objectId in objectIds)
269+
{
270+
cache[(componentType, objectId)] = (objectId.ToString(), objectId.ToString(), null);
245271
}
246272
continue;
247273
}
@@ -250,29 +276,36 @@ private async Task<List<RawComponentInfo>> QuerySolutionComponentsAsync(List<Gui
250276
if (ComponentTableMap.TryGetValue(componentType, out var tableInfo))
251277
{
252278
var primaryKey = tableInfo.PrimaryKey ?? tableInfo.TableName + "id";
253-
var names = await QueryComponentNamesAsync(tableInfo.TableName, tableInfo.NameColumn, primaryKey, objectIds);
254-
foreach (var (objectId, name) in names)
279+
var namesAndEntities = await QueryComponentNamesWithEntityAsync(tableInfo.TableName, tableInfo.NameColumn, primaryKey, tableInfo.EntityColumn, objectIds);
280+
foreach (var (objectId, name, relatedTable) in namesAndEntities)
255281
{
256-
cache[(componentType, objectId)] = (name, name);
282+
cache[(componentType, objectId)] = (name, name, relatedTable);
257283
}
258284
}
259285
}
260286

261287
return cache;
262288
}
263289

264-
private async Task<Dictionary<Guid, string>> QueryComponentNamesAsync(string tableName, string nameColumn, string primaryKey, List<Guid> objectIds)
290+
private async Task<List<(Guid ObjectId, string Name, string? RelatedTable)>> QueryComponentNamesWithEntityAsync(
291+
string tableName, string nameColumn, string primaryKey, string? entityColumn, List<Guid> objectIds)
265292
{
266-
var result = new Dictionary<Guid, string>();
293+
var result = new List<(Guid, string, string?)>();
267294

268295
if (!objectIds.Any())
269296
return result;
270297

271298
try
272299
{
300+
var columns = new List<string> { primaryKey, nameColumn };
301+
if (!string.IsNullOrEmpty(entityColumn))
302+
{
303+
columns.Add(entityColumn);
304+
}
305+
273306
var query = new QueryExpression(tableName)
274307
{
275-
ColumnSet = new ColumnSet(primaryKey, nameColumn),
308+
ColumnSet = new ColumnSet(columns.ToArray()),
276309
Criteria = new FilterExpression(LogicalOperator.And)
277310
{
278311
Conditions =
@@ -287,7 +320,25 @@ private async Task<Dictionary<Guid, string>> QueryComponentNamesAsync(string tab
287320
{
288321
var id = entity.GetAttributeValue<Guid>(primaryKey);
289322
var name = entity.GetAttributeValue<string>(nameColumn) ?? id.ToString();
290-
result[id] = name;
323+
string? relatedTable = null;
324+
325+
if (!string.IsNullOrEmpty(entityColumn) && entity.Contains(entityColumn))
326+
{
327+
// The entity column can be a string (logical name) or an int (object type code)
328+
var entityValue = entity[entityColumn];
329+
if (entityValue is string strValue)
330+
{
331+
relatedTable = strValue;
332+
}
333+
else if (entityValue is int intValue)
334+
{
335+
// Object type code - we'd need entity metadata to resolve this
336+
// For now, just store the numeric value as string
337+
relatedTable = intValue.ToString();
338+
}
339+
}
340+
341+
result.Add((id, name, relatedTable));
291342
}
292343
}
293344
catch (Exception ex)
@@ -299,7 +350,7 @@ private async Task<Dictionary<Guid, string>> QueryComponentNamesAsync(string tab
299350
return result;
300351
}
301352

302-
private string ResolveComponentName(RawComponentInfo component, Dictionary<(int, Guid), (string Name, string SchemaName)> cache)
353+
private string ResolveComponentName(RawComponentInfo component, Dictionary<(int, Guid), (string Name, string SchemaName, string? RelatedTable)> cache)
303354
{
304355
if (cache.TryGetValue((component.ComponentType, component.ObjectId), out var names))
305356
{
@@ -308,7 +359,7 @@ private string ResolveComponentName(RawComponentInfo component, Dictionary<(int,
308359
return component.ObjectId.ToString();
309360
}
310361

311-
private string ResolveComponentSchemaName(RawComponentInfo component, Dictionary<(int, Guid), (string Name, string SchemaName)> cache)
362+
private string ResolveComponentSchemaName(RawComponentInfo component, Dictionary<(int, Guid), (string Name, string SchemaName, string? RelatedTable)> cache)
312363
{
313364
if (cache.TryGetValue((component.ComponentType, component.ObjectId), out var names))
314365
{
@@ -317,5 +368,19 @@ private string ResolveComponentSchemaName(RawComponentInfo component, Dictionary
317368
return component.ObjectId.ToString();
318369
}
319370

371+
private string? ResolveRelatedTable(RawComponentInfo component, Dictionary<(int, Guid), (string Name, string SchemaName, string? RelatedTable)> cache)
372+
{
373+
if (!ComponentTypesWithRelatedTable.Contains(component.ComponentType))
374+
{
375+
return null;
376+
}
377+
378+
if (cache.TryGetValue((component.ComponentType, component.ObjectId), out var names))
379+
{
380+
return names.RelatedTable;
381+
}
382+
return null;
383+
}
384+
320385
private record RawComponentInfo(int ComponentType, Guid ObjectId, Guid SolutionId, bool IsExplicit);
321386
}

0 commit comments

Comments
 (0)