Can I use the Tooling API and SOQL to identify Apex Classes (or fields or any other metadata) within the org that are not being utilized or referenced?
Upon executing
SELECT MetadataComponentId, MetadataComponentName, MetadataComponentType, RefMetadataComponentId, RefMetadataComponentName, RefMetadataComponentType FROM MetadataComponentDependency WHERE RefMetadataComponentType = 'ApexClass'
I anticipated discovering null values for independent classes, but I couldn’t find any. As a result, I attempted the following approach.
SELECT Name FROM ApexClass WHERE Id IN (SELECT RefMetadataComponentId FROM MetadataComponentDependency WHERE RefMetadataComponentType = 'ApexClass')
This approach also proved ineffective.
SOLUTION
I believe the documentation clearly indicates that it only lists the connections or relationships between components, so the absence of a relationship would not be included in the results.
I believe this clarifies whether it’s possible to use a single SOQL query to obtain components without a relationship, which is likely not the case. It would be interesting to implement an anti-join in your second example, but I encountered an error message stating that.
Sub-selects with semi-join conditions are limited to querying the ‘Id’ fields and cannot utilize the ‘RefMetadataComponentId’ field.
If they decide to expand on this feature, given that it’s currently in beta, it could offer a valuable solution. Nevertheless, with minimal additional effort, you can achieve a similar outcome with an extra query and two for loops. As a quick proof of concept, I executed the following code in anonymous Apex, deploying the wrapper class to the development environment beforehand.
//Class to deserialize the response from Tooling API query public class ApexDependencyWrapper { public List<DependencyRecords> records {get; set;} public class DependencyRecords{ //class being referred to public String RefMetadataComponentName {get; set;} //class relying on the above public String MetadataComponentName {get; set;} } }
//Used in anonymous apex Http httpProtocol = new Http(); HttpRequest req = new HttpRequest(); req.setEndpoint(URL.getSalesforceBaseUrl().toExternalForm()+ '/services/data/v47.0/tooling/query/?q='+ 'SELECT+RefMetadataComponentName'+ '+FROM+MetadataComponentDependency'+ '+WHERE+RefMetadataComponentType=\'ApexClass\''); //setting method and header req.setMethod('GET'); req.setHeader('Authorization', 'OAuth ' + UserInfo.getSessionId()); HttpResponse resp = httpProtocol.send(req); ApexDependencyWrapper classesWithDependency = (ApexDependencyWrapper) System.JSON.deserialize(resp.getBody(), ApexDependencyWrapper.Class); //only want custom classes, not from managed packages List<ApexClass> allClasses = [SELECT Name FROM ApexClass WHERE NamespacePrefix = null]; //get all class names that are referenced List<String> classesReferenced = new List<String>(); for(ApexDependencyWrapper.DependencyRecords apexDepRec : classesWithDependency.records){ //ignore test classes that rely on apex classes if(apexDepRec.MetadataComponentName != null && !apexDepRec.MetadataComponentName.containsIgnoreCase('test')){ classesReferenced.add(apexDepRec.RefMetadataComponentName); } } //find which classes are not referenced in org List<String> classesNotReferenced = new List<String>(); //not including test classes for(ApexClass apexName : allClasses){ if(!classesReferenced.contains(apexName.Name) && !apexName.Name.containsIgnoreCase('test')){ classesNotReferenced.add(apexName.Name); } } System.debug('Classes with no references ' + classesNotReferenced);
I included the ‘test’ filter because our apex test names typically contain the word ‘Test,’ and as a result, it accounted for the majority of the results, as they are not commonly referred to.