@@ -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