@@ -54,16 +54,24 @@ public DataverseService(IConfiguration configuration, ILogger<DataverseService>
5454 webResourceAnalyzer = new WebResourceAnalyzer ( client , configuration ) ;
5555 }
5656
57- public async Task < ( IEnumerable < Record > , IEnumerable < SolutionWarning > ) > GetFilteredMetadata ( )
57+ public async Task < ( IEnumerable < Record > , IEnumerable < SolutionWarning > , IEnumerable < Solution > ) > GetFilteredMetadata ( )
5858 {
5959 var warnings = new List < SolutionWarning > ( ) ; // used to collect warnings for the insights dashboard
60- var ( publisherPrefix , solutionIds ) = await GetSolutionIds ( ) ;
61- var solutionComponents = await GetSolutionComponents ( solutionIds ) ; // (id, type, rootcomponentbehavior)
62-
63- var entitiesInSolution = solutionComponents . Where ( x => x . ComponentType == 1 ) . Select ( x => x . ObjectId ) . ToList ( ) ;
64- var entityRootBehaviour = solutionComponents . Where ( x => x . ComponentType == 1 ) . ToDictionary ( x => x . ObjectId , x => x . RootComponentBehavior ) ;
60+ var ( publisherPrefix , solutionIds , solutionEntities ) = await GetSolutionIds ( ) ;
61+ var solutionComponents = await GetSolutionComponents ( solutionIds ) ; // (id, type, rootcomponentbehavior, solutionid)
62+
63+ var entitiesInSolution = solutionComponents . Where ( x => x . ComponentType == 1 ) . Select ( x => x . ObjectId ) . Distinct ( ) . ToList ( ) ;
64+ var entityRootBehaviour = solutionComponents
65+ . Where ( x => x . ComponentType == 1 )
66+ . GroupBy ( x => x . ObjectId )
67+ . ToDictionary ( g => g . Key , g =>
68+ {
69+ // If any solution includes all attributes (0), use that, otherwise use the first occurrence
70+ var behaviors = g . Select ( x => x . RootComponentBehavior ) . ToList ( ) ;
71+ return behaviors . Contains ( 0 ) ? 0 : behaviors . First ( ) ;
72+ } ) ;
6573 var attributesInSolution = solutionComponents . Where ( x => x . ComponentType == 2 ) . Select ( x => x . ObjectId ) . ToHashSet ( ) ;
66- var rolesInSolution = solutionComponents . Where ( x => x . ComponentType == 20 ) . Select ( x => x . ObjectId ) . ToList ( ) ;
74+ var rolesInSolution = solutionComponents . Where ( x => x . ComponentType == 20 ) . Select ( x => x . ObjectId ) . Distinct ( ) . ToList ( ) ;
6775
6876 var entitiesInSolutionMetadata = await GetEntityMetadata ( entitiesInSolution ) ;
6977
@@ -154,6 +162,8 @@ public DataverseService(IConfiguration configuration, ILogger<DataverseService>
154162 . Select ( usage =>
155163 new AttributeWarning ( $ "{ attributeDict . Key } was used inside a { usage . ComponentType } component [{ usage . Name } ]. However, the entity { entityKey } could not be resolved in the provided solutions.") ) ) ) ) ;
156164
165+ // Create solutions with their components
166+ var solutions = await CreateSolutions ( solutionEntities , solutionComponents , allEntityMetadata ) ;
157167
158168 return ( records
159169 . Select ( x =>
@@ -173,7 +183,142 @@ public DataverseService(IConfiguration configuration, ILogger<DataverseService>
173183 entityIconMap ,
174184 attributeUsages ,
175185 configuration ) ;
176- } ) , warnings ) ;
186+ } ) ,
187+ warnings ,
188+ solutions ) ;
189+ }
190+
191+ private Task < IEnumerable < Solution > > CreateSolutions (
192+ List < Entity > solutionEntities ,
193+ IEnumerable < ( Guid ObjectId , int ComponentType , int RootComponentBehavior , EntityReference SolutionId ) > solutionComponents ,
194+ List < EntityMetadata > allEntityMetadata )
195+ {
196+ var solutions = new List < Solution > ( ) ;
197+
198+ // Create lookup dictionaries for faster access
199+ var entityLookup = allEntityMetadata . ToDictionary ( e => e . MetadataId ?? Guid . Empty , e => e ) ;
200+
201+ // Group components by solution
202+ var componentsBySolution = solutionComponents . GroupBy ( c => c . SolutionId ) ;
203+
204+ foreach ( var solutionGroup in componentsBySolution )
205+ {
206+ var solutionId = solutionGroup . Key ;
207+ var solutionEntity = solutionEntities . FirstOrDefault ( s => s . GetAttributeValue < Guid > ( "solutionid" ) == solutionId . Id ) ;
208+
209+ if ( solutionEntity == null ) continue ;
210+
211+ var solutionName = solutionEntity . GetAttributeValue < string > ( "friendlyname" ) ??
212+ solutionEntity . GetAttributeValue < string > ( "uniquename" ) ??
213+ "Unknown Solution" ;
214+
215+ var components = new List < SolutionComponent > ( ) ;
216+
217+ foreach ( var component in solutionGroup )
218+ {
219+ var solutionComponent = CreateSolutionComponent ( component , entityLookup , allEntityMetadata ) ;
220+ if ( solutionComponent != null )
221+ {
222+ components . Add ( solutionComponent ) ;
223+ }
224+ }
225+
226+ solutions . Add ( new Solution ( solutionName , components ) ) ;
227+ }
228+
229+ return Task . FromResult ( solutions . AsEnumerable ( ) ) ;
230+ }
231+
232+ private SolutionComponent ? CreateSolutionComponent (
233+ ( Guid ObjectId , int ComponentType , int RootComponentBehavior , EntityReference SolutionId ) component ,
234+ Dictionary < Guid , EntityMetadata > entityLookup ,
235+ List < EntityMetadata > allEntityMetadata )
236+ {
237+ try
238+ {
239+ switch ( component . ComponentType )
240+ {
241+ case 1 : // Entity
242+ // Try to find entity by MetadataId first, then by searching all entities
243+ if ( entityLookup . TryGetValue ( component . ObjectId , out var entityMetadata ) )
244+ {
245+ return new SolutionComponent (
246+ entityMetadata . DisplayName ? . UserLocalizedLabel ? . Label ?? entityMetadata . SchemaName ,
247+ entityMetadata . SchemaName ,
248+ entityMetadata . Description ? . UserLocalizedLabel ? . Label ?? string . Empty ,
249+ SolutionComponentType . Entity ) ;
250+ }
251+
252+ // Entity lookup by ObjectId is complex in Dataverse, so we'll skip the fallback for now
253+ // The primary lookup by MetadataId should handle most cases
254+ break ;
255+
256+ case 2 : // Attribute
257+ // Search for attribute across all entities
258+ foreach ( var entity in allEntityMetadata )
259+ {
260+ var attribute = entity . Attributes ? . FirstOrDefault ( a => a . MetadataId == component . ObjectId ) ;
261+ if ( attribute != null )
262+ {
263+ return new SolutionComponent (
264+ attribute . DisplayName ? . UserLocalizedLabel ? . Label ?? attribute . SchemaName ,
265+ attribute . SchemaName ,
266+ attribute . Description ? . UserLocalizedLabel ? . Label ?? string . Empty ,
267+ SolutionComponentType . Attribute ) ;
268+ }
269+ }
270+ break ;
271+
272+ case 3 : // Relationship (if you want to add this to the enum later)
273+ // Search for relationships across all entities
274+ foreach ( var entity in allEntityMetadata )
275+ {
276+ // Check one-to-many relationships
277+ var oneToMany = entity . OneToManyRelationships ? . FirstOrDefault ( r => r . MetadataId == component . ObjectId ) ;
278+ if ( oneToMany != null )
279+ {
280+ return new SolutionComponent (
281+ oneToMany . SchemaName ,
282+ oneToMany . SchemaName ,
283+ $ "One-to-Many: { entity . SchemaName } -> { oneToMany . ReferencingEntity } ",
284+ SolutionComponentType . Relationship ) ;
285+ }
286+
287+ // Check many-to-one relationships
288+ var manyToOne = entity . ManyToOneRelationships ? . FirstOrDefault ( r => r . MetadataId == component . ObjectId ) ;
289+ if ( manyToOne != null )
290+ {
291+ return new SolutionComponent (
292+ manyToOne . SchemaName ,
293+ manyToOne . SchemaName ,
294+ $ "Many-to-One: { entity . SchemaName } -> { manyToOne . ReferencedEntity } ",
295+ SolutionComponentType . Relationship ) ;
296+ }
297+
298+ // Check many-to-many relationships
299+ var manyToMany = entity . ManyToManyRelationships ? . FirstOrDefault ( r => r . MetadataId == component . ObjectId ) ;
300+ if ( manyToMany != null )
301+ {
302+ return new SolutionComponent (
303+ manyToMany . SchemaName ,
304+ manyToMany . SchemaName ,
305+ $ "Many-to-Many: { manyToMany . Entity1LogicalName } <-> { manyToMany . Entity2LogicalName } ",
306+ SolutionComponentType . Relationship ) ;
307+ }
308+ }
309+ break ;
310+
311+ case 20 : // Security Role - skip for now as not in enum
312+ case 92 : // SDK Message Processing Step (Plugin) - skip for now as not in enum
313+ break ;
314+ }
315+ }
316+ catch ( Exception ex )
317+ {
318+ logger . LogWarning ( $ "Failed to create solution component for ObjectId { component . ObjectId } , ComponentType { component . ComponentType } : { ex . Message } ") ;
319+ }
320+
321+ return null ;
177322 }
178323
179324 private static Record MakeRecord (
@@ -268,6 +413,7 @@ private static Record MakeRecord(
268413 description ? . PrettyDescription ( ) ,
269414 entity . IsAuditEnabled . Value ,
270415 entity . IsActivity ?? false ,
416+ entity . IsCustomEntity ?? false ,
271417 entity . OwnershipType ?? OwnershipTypes . UserOwned ,
272418 entity . HasNotes ?? false ,
273419 attributes ,
@@ -376,7 +522,7 @@ await Parallel.ForEachAsync(
376522 return metadata ;
377523 }
378524
379- private async Task < ( string PublisherPrefix , List < Guid > SolutionIds ) > GetSolutionIds ( )
525+ private async Task < ( string PublisherPrefix , List < Guid > SolutionIds , List < Entity > SolutionEntities ) > GetSolutionIds ( )
380526 {
381527 var solutionNameArg = configuration [ "DataverseSolutionNames" ] ;
382528 if ( solutionNameArg == null )
@@ -387,7 +533,7 @@ await Parallel.ForEachAsync(
387533
388534 var resp = await client . RetrieveMultipleAsync ( new QueryExpression ( "solution" )
389535 {
390- ColumnSet = new ColumnSet ( "publisherid" ) ,
536+ ColumnSet = new ColumnSet ( "publisherid" , "friendlyname" , "uniquename" , "solutionid" ) ,
391537 Criteria = new FilterExpression ( LogicalOperator . And )
392538 {
393539 Conditions =
@@ -406,14 +552,14 @@ await Parallel.ForEachAsync(
406552
407553 var publisher = await client . RetrieveAsync ( "publisher" , publisherIds [ 0 ] , new ColumnSet ( "customizationprefix" ) ) ;
408554
409- return ( publisher . GetAttributeValue < string > ( "customizationprefix" ) , resp . Entities . Select ( e => e . GetAttributeValue < Guid > ( "solutionid" ) ) . ToList ( ) ) ;
555+ return ( publisher . GetAttributeValue < string > ( "customizationprefix" ) , resp . Entities . Select ( e => e . GetAttributeValue < Guid > ( "solutionid" ) ) . ToList ( ) , resp . Entities . ToList ( ) ) ;
410556 }
411557
412- public async Task < IEnumerable < ( Guid ObjectId , int ComponentType , int RootComponentBehavior ) > > GetSolutionComponents ( List < Guid > solutionIds )
558+ public async Task < IEnumerable < ( Guid ObjectId , int ComponentType , int RootComponentBehavior , EntityReference SolutionId ) > > GetSolutionComponents ( List < Guid > solutionIds )
413559 {
414560 var entityQuery = new QueryExpression ( "solutioncomponent" )
415561 {
416- ColumnSet = new ColumnSet ( "objectid" , "componenttype" , "rootcomponentbehavior" ) ,
562+ ColumnSet = new ColumnSet ( "objectid" , "componenttype" , "rootcomponentbehavior" , "solutionid" ) ,
417563 Criteria = new FilterExpression ( LogicalOperator . And )
418564 {
419565 Conditions =
@@ -427,7 +573,7 @@ await Parallel.ForEachAsync(
427573 return
428574 ( await client . RetrieveMultipleAsync ( entityQuery ) )
429575 . Entities
430- . Select ( e => ( e . GetAttributeValue < Guid > ( "objectid" ) , e . GetAttributeValue < OptionSetValue > ( "componenttype" ) . Value , e . Contains ( "rootcomponentbehavior" ) ? e . GetAttributeValue < OptionSetValue > ( "rootcomponentbehavior" ) . Value : - 1 ) )
576+ . Select ( e => ( e . GetAttributeValue < Guid > ( "objectid" ) , e . GetAttributeValue < OptionSetValue > ( "componenttype" ) . Value , e . Contains ( "rootcomponentbehavior" ) ? e . GetAttributeValue < OptionSetValue > ( "rootcomponentbehavior" ) . Value : - 1 , e . GetAttributeValue < EntityReference > ( "solutionid" ) ) )
431577 . ToList ( ) ;
432578 }
433579
0 commit comments