Discovering Apex Classes that are not in use using the Tooling API and MetadataComponentDependency.

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.