2

Is there a way to SOQL find all Apex Classes (or fields or any other metadata) that are not used/referenced in an org by doing a smart Tooling API SOQL on MetadataComponentDependency?

When I run

SELECT MetadataComponentId, MetadataComponentName, MetadataComponentType, RefMetadataComponentId, RefMetadataComponentName, RefMetadataComponentType 
FROM MetadataComponentDependency 
WHERE RefMetadataComponentType = 'ApexClass'

I expected to find some null values for independant classes but there are none. So i tried this

SELECT Name 
FROM ApexClass 
WHERE Id IN (SELECT RefMetadataComponentId 
             FROM MetadataComponentDependency 
             WHERE RefMetadataComponentType = 'ApexClass')

which also didnt work.

5
  • 2
    Is there something you need to explicitly enable to have access to MetadataComponentDependency via SOQL? If I try to run your first query this object is reported as being an invalid SObject type. (I'm running this on a scratch org using current release version.)
    – Phil W
    Mar 12, 2020 at 21:59
  • 1
    Do you want to change the second query to ... WHERE Id NOT IN (SELECT...?
    – Phil W
    Mar 12, 2020 at 22:05
  • 2
    Try the query in the dev console with Tooling API checkbox activated. Mar 12, 2020 at 22:05
  • 1
    I believe the doc mentions this is to " list the relationships between the metadata components in your org". If there is no relationship/unused (as you're looking for) then it simply wouldn't exist or be returned at all. Doesn't seem like it'd meet your need. Though you could compare what's returned vs. what exists in terms of all apex classes to find the "non-returned" ones so that's something. Apex Tests throw a wrench in this since they may be only reference. Apr 16, 2020 at 14:14
  • @RobertSösemann try using the NPM package sfdc-soup npmjs.com/package/sfdc-soup Oct 1, 2020 at 19:50

3 Answers 3

4

I think the doc is clear that it will only list the relationships between components so the lack of one would not be returned.

Usage

Use SOQL queries to list the relationships between the metadata components in your org. The query results include one row for each relationship. Each relationship is a directional dependency between two metadata components.

I think that answers the question of whether you could do a single SOQL query on this to get components without a relationship - probably not. It'd be cool to use an anti-join in your 2nd example, but I get an error saying

semi join sub selects can only query id fields, cannot use: 'RefMetadataComponentId'

If they open that up (as this is in beta), that could be a good option. However, you're not too far off from something without much extra effort (extra query and two for loops). I did the below in anonymous apex (and deployed the wrapper class to dev environment first) just as a quick POC.

//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 added the 'test' filter because we use Test in the names of our apex tests and that was basically all the results (since no one references them).

Edit: made a quick github repo that makes them both classes and easier to use if you want to fork/check it out.

1

When using the api tool, there is a limit for 2000 rows. This can be a problem on large projecs. The result is too many Apex clases are set as not used.

I altered the solution to use Bulk api instead, this will also work on large projects where there is more than 2000 apex dependencies

//Used in anonymous apex
// -------------- Run this first ----------------------
Http httpProtocol = new Http();
HttpRequest req = new HttpRequest();
req.setEndpoint(URL.getSalesforceBaseUrl().toExternalForm()+
    '/services/data/v58.0/tooling/jobs/query/');
req.setBody('{"operation": "query",' +
    '"query": "SELECT RefMetadataComponentName FROM MetadataComponentDependency WHERE RefMetadataComponentType=\'ApexClass\'"' +
    '} ');

//setting method and header
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setHeader('Accept', 'application/json');
req.setHeader('Authorization', 'OAuth ' + UserInfo.getSessionId());
HttpResponse resp = httpProtocol.send(req);
System.debug(resp.getBody());

Copy the job id from the above result and paste it into the folowing:

// ------------------- Run second ---------------
// Copy the ID console from the above job into the query request
Http httpProtocol = new Http();
HttpRequest req = new HttpRequest();
req.setEndpoint(URL.getSalesforceBaseUrl().toExternalForm() + '/services/data/v59.0/tooling/jobs/query/7502500000FgCnEAAV/results/');

//setting method and header
req.setMethod('GET');
req.setHeader('Authorization', 'OAuth ' + UserInfo.getSessionId());
HttpResponse resp = httpProtocol.send(req);
System.debug(resp.getBody());

Set<String> classesWithDependency = new Set<String>();
List<String> resultList = resp.getBody().replace('"', '').split('\n');
System.debug('resultList size=' + resultList.size());
classesWithDependency.addAll(resultList);
resultList=null;
System.debug('classesWithDependency size=' + classesWithDependency.size());

//only want custom classes, not from managed packages
List<ApexClass> allClasses = [SELECT Name FROM ApexClass WHERE NamespacePrefix = NULL];
//find which classes are not referenced in org
List<String> classesNotReferenced = new List<String>();
//not including test classes
for(ApexClass apexName : allClasses){
    if(!classesWithDependency.contains(apexName.Name) && !apexName.Name.containsIgnoreCase('test')) {
        classesNotReferenced.add(apexName.Name);
        System.debug('No references ' + apexName.Name);
    }
}
System.debug('Classes with no references ' + classesNotReferenced);
New contributor
Jens Henrik is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.
0

Ran into a similar task recently where we needed to optimize code and filter out all the unreferenced classes in the Org. Have modified Kris Goncalves' logic and executed it in the Anonymous Window and it did the trick.

/**********************************************Code Starts***********************************************************/

/* ----------------------------------------------------------------
* @Description
* The wrapper classes ApexDependencyWrapper and DependencyRecords are used to deserialize the response from Tooling API query.
* Used in method findClsWithDependency() 
*-----------------------------------------------------------------------
*/
private class ApexDependencyWrapper { 
    private list<DependencyRecords> records = new list<DependencyRecords>(); 
}

private class DependencyRecords{
    private String RefMetadataComponentName; 
    private String MetadataComponentName; 
}

/* ----------------------------------------------------------------
* @Description
* The method findClsWithDependency() is used for calling the Tooling API which in turn returns a list of classes that are referenced elsewhere.
* Used in method findReferencedClasses() 
*-----------------------------------------------------------------------
*/
private ApexDependencyWrapper findClsWithDependency(){
    ApexDependencyWrapper classesWithDependency = new ApexDependencyWrapper(); 
    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\'');
    req.setMethod('GET');
    req.setHeader('Authorization', 'OAuth ' + UserInfo.getSessionId());
    HttpResponse resp = httpProtocol.send(req);
    classesWithDependency  = (ApexDependencyWrapper) JSON.deserialize(resp.getBody(), ApexDependencyWrapper.Class);
    return classesWithDependency; 
}  

/* ----------------------------------------------------------------
* @Description
* The method findReferencedClasses() finds all classes which are referenced by another class (excluding test classes because they do not count against org char limit) 
* we are assuming here that the test classes in your org contain the 'test' keyword in their name.
* Used in method findUnreferencedClasses()
*-----------------------------------------------------------------------
*/
private set<String> findReferencedClasses(){   
    ApexDependencyWrapper classesWithDependency = new ApexDependencyWrapper(); 
    classesWithDependency = findClsWithDependency(); 
    set<String> classesReferenced = new set<String>();
    for(DependencyRecords apexDepRec : classesWithDependency.records){      
        if(apexDepRec.RefMetadataComponentName != null && !apexDepRec.RefMetadataComponentName.containsIgnoreCase('test') ){ 
            classesReferenced.add(apexDepRec.RefMetadataComponentName);
        }
    }
    return classesReferenced; 
}

/* ----------------------------------------------------------------
* @Description
* The method findUnreferencedClasses() finds all classes that are not referenced in the org  
* It is used to check if all classes apart from the classes containing any of the keywords supplied by the parameter are present in classesReferenced set 
* if not then add the class inside classesNotReferenced set.
*-----------------------------------------------------------------------
*/
private Void findUnreferencedClasses(String ignoreUnrefClsParam){    
    set<String> classesReferenced = findReferencedClasses();  
    //fetch all the custom classes (not from managed packages)
    list<ApexClass> allClasses = [select Name from ApexClass where NamespacePrefix = null limit 10000];
    set<String> classesNotReferenced = new set<String>();
    for(ApexClass apexName : allClasses){
        if(!classesReferenced.contains(apexName.Name) && ignoreUnrefClsTypes(apexName.Name, ignoreUnrefClsParam) ){ 
            system.debug('Class with no references--- '+ apexName.Name);        
            classesNotReferenced.add(apexName.Name);
        }
    }
    system.debug('Classes with no references rows ' + classesNotReferenced.size());
}   

/* ----------------------------------------------------------------
* @Description
* The helper method ignoreUnrefClsTypes() is used to check if any of the keywords supplied by the parameter in findUnreferencedClasses()
* is present in the class name - if not then return true.
* Used in method findUnreferencedClasses()
*-----------------------------------------------------------------------
*/
private Boolean ignoreUnrefClsTypes(String clsName, String ignoreUnrefClsParam){    
    list<String> ignoreUnrefCls = ignoreUnrefClsParam.split(',');    
    list<String> result = new list<String>(); 
    for(String clsTypes : ignoreUnrefCls){
        if(!clsName.containsIgnoreCase(clsTypes) ){
            result.add('true');
        }else{
            result.add('false');
        }
    }      
    Boolean output = result.contains('false') ? false : true;
    return output; 
}

/**********************************************Code Ends***********************************************************/

/* ----------------------------------------------------------------
* @Description
* Call following method in Execution Window to find all the unreferenced classes in debug logs. 
* If you want to ignore the unreferenced classes containing specific keywords then add the same inside
* the string parameter as shown below.
*-----------------------------------------------------------------------
*/
findUnreferencedClasses('test,trigger,batch'); 

Hope this helps,

Parikshit

2
  • What did you fix or add to the previously accepted answer by Kris? Sep 25, 2022 at 9:47
  • 1
    Hi robert, I have made some corrections in the code namely the separate wrapper class definitions (which in earlier code by kris threw an error in the execution window), I also separated the logic in different methods for maintaining singularity and added a new method for ignoring certain types of unreferenced classes (e.g. test/batch etc. ) based on input parameters. Sep 30, 2022 at 5:42

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .