22using Azure . Identity ;
33using Generator . DTO ;
44using Generator . DTO . Attributes ;
5+ using Generator . DTO . Warnings ;
56using Generator . Queries ;
67using Generator . Services ;
78using Generator . Services . Plugins ;
9+ using Generator . Services . WebResources ;
810using Microsoft . Crm . Sdk . Messages ;
911using Microsoft . Extensions . Caching . Memory ;
1012using Microsoft . Extensions . Configuration ;
1416using Microsoft . Xrm . Sdk . Metadata ;
1517using Microsoft . Xrm . Sdk . Query ;
1618using System . Collections . Concurrent ;
19+ using System . Diagnostics ;
1720using System . Reflection ;
1821using Attribute = Generator . DTO . Attributes . Attribute ;
1922
@@ -27,6 +30,7 @@ internal class DataverseService
2730
2831 private readonly PluginAnalyzer pluginAnalyzer ;
2932 private readonly PowerAutomateFlowAnalyzer flowAnalyzer ;
33+ private readonly WebResourceAnalyzer webResourceAnalyzer ;
3034
3135 public DataverseService ( IConfiguration configuration , ILogger < DataverseService > logger )
3236 {
@@ -47,10 +51,12 @@ public DataverseService(IConfiguration configuration, ILogger<DataverseService>
4751
4852 pluginAnalyzer = new PluginAnalyzer ( client ) ;
4953 flowAnalyzer = new PowerAutomateFlowAnalyzer ( client ) ;
54+ webResourceAnalyzer = new WebResourceAnalyzer ( client , configuration ) ;
5055 }
5156
52- public async Task < IEnumerable < Record > > GetFilteredMetadata ( )
57+ public async Task < ( IEnumerable < Record > , IEnumerable < SolutionWarning > ) > GetFilteredMetadata ( )
5358 {
59+ var warnings = new List < SolutionWarning > ( ) ; // used to collect warnings for the insights dashboard
5460 var ( publisherPrefix , solutionIds ) = await GetSolutionIds ( ) ;
5561 var solutionComponents = await GetSolutionComponents ( solutionIds ) ; // (id, type, rootcomponentbehavior)
5662
@@ -96,15 +102,32 @@ public async Task<IEnumerable<Record>> GetFilteredMetadata()
96102 // Processes analysis
97103 var attributeUsages = new Dictionary < string , Dictionary < string , List < AttributeUsage > > > ( ) ;
98104 // Plugins
105+ var pluginStopWatch = new Stopwatch ( ) ;
106+ pluginStopWatch . Start ( ) ;
99107 var pluginCollection = await client . GetSDKMessageProcessingStepsAsync ( solutionIds ) ;
100108 logger . LogInformation ( $ "There are { pluginCollection . Count ( ) } plugin sdk steps in the environment.") ;
101109 foreach ( var plugin in pluginCollection )
102110 await pluginAnalyzer . AnalyzeComponentAsync ( plugin , attributeUsages ) ;
111+ pluginStopWatch . Stop ( ) ;
112+ logger . LogInformation ( $ "Plugin analysis took { pluginStopWatch . ElapsedMilliseconds } ms.") ;
103113 // Flows
114+ var flowStopWatch = new Stopwatch ( ) ;
115+ flowStopWatch . Start ( ) ;
104116 var flowCollection = await client . GetPowerAutomateFlowsAsync ( solutionIds ) ;
105117 logger . LogInformation ( $ "There are { flowCollection . Count ( ) } Power Automate flows in the environment.") ;
106118 foreach ( var flow in flowCollection )
107119 await flowAnalyzer . AnalyzeComponentAsync ( flow , attributeUsages ) ;
120+ flowStopWatch . Stop ( ) ;
121+ logger . LogInformation ( $ "Power Automate flow analysis took { flowStopWatch . ElapsedMilliseconds } ms.") ;
122+ // WebResources
123+ var resourceStopWatch = new Stopwatch ( ) ;
124+ resourceStopWatch . Start ( ) ;
125+ var webresourceCollection = await client . GetWebResourcesAsync ( solutionIds ) ;
126+ logger . LogInformation ( $ "There are { webresourceCollection . Count ( ) } WebResources in the environment.") ;
127+ foreach ( var resource in webresourceCollection )
128+ await webResourceAnalyzer . AnalyzeComponentAsync ( resource , attributeUsages ) ;
129+ resourceStopWatch . Stop ( ) ;
130+ logger . LogInformation ( $ "WebResource analysis took { resourceStopWatch . ElapsedMilliseconds } ms.") ;
108131
109132 var records =
110133 entitiesInSolutionMetadata
@@ -123,8 +146,17 @@ public async Task<IEnumerable<Record>> GetFilteredMetadata()
123146 . Where ( x => x . EntityMetadata . DisplayName . UserLocalizedLabel ? . Label != null )
124147 . ToList ( ) ;
125148
149+ // Warn about attributes that were used in processes, but the entity could not be resolved from e.g. JavaScript file name or similar
150+ var hash = entitiesInSolutionMetadata . SelectMany < EntityMetadata , string > ( r => [ r . LogicalCollectionName ? . ToLower ( ) ?? "" , r . LogicalName . ToLower ( ) ] ) . ToHashSet ( ) ;
151+ warnings . AddRange ( attributeUsages . Keys
152+ . Where ( k => ! hash . Contains ( k . ToLower ( ) ) )
153+ . SelectMany ( entityKey => attributeUsages . GetValueOrDefault ( entityKey ) !
154+ . SelectMany ( attributeDict => attributeDict . Value
155+ . Select ( usage =>
156+ 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.") ) ) ) ) ;
126157
127- return records
158+
159+ return ( records
128160 . Select ( x =>
129161 {
130162 logicalNameToSecurityRoles . TryGetValue ( x . EntityMetadata . LogicalName , out var securityRoles ) ;
@@ -142,7 +174,7 @@ public async Task<IEnumerable<Record>> GetFilteredMetadata()
142174 entityIconMap ,
143175 attributeUsages ,
144176 configuration ) ;
145- } ) ;
177+ } ) , warnings ) ;
146178 }
147179
148180 private static Record MakeRecord (
0 commit comments