During release preparation, package maintenance, or technical documentation audits, development teams need a fast way to map all components associated with a specific project or managed namespace. By combining Dynamic Apex Schema Describe with SOQL queries against system metadata tables (ApexClass, ApexPage, EntityDefinition, and ObjectPermissions), you can render a real-time inventory dashboard styled with the Salesforce Lightning Design System (SLDS).
1. Architecture: Inspecting Metadata & Schema via Apex
The dashboard controller aggregates information from several platform layers:
- Schema Describing:
Schema.getGlobalDescribe()andSchema.describeSObjects()identify custom objects and extract relationship details (Lookup vs. Master-Detail). - Metadata Tables: Queries against
ApexClass,ApexTrigger,ApexComponent, andPermissionSetfiltered by project prefixes or namespaces. - Visualforce Page Markup Parsing: Inspects
ApexPage.Markupto dynamically extract standard controllers, custom controllers, and extensions. - Entity Definition & Setup URLs: Queries
EntityDefinitionto generate direct links into the Salesforce Object Manager setup menu.
2. Step 1: Apex Controller Implementation
The controller organizes each component category into wrapper classes and uses the transient keyword on large collections to keep the Visualforce View State lean.
ProjectComponentsController.cls)
public with sharing class ProjectComponentsController {
public transient List<DisplayListWrapper> displayList { get; set; }
public transient List<ApexClass> classList { get; set; }
public transient List<PageClassWrapper> pageClassList { get; set; }
public transient List<PermissionSet> permissionSetList { get; set; }
public transient List<Group> publicGroupList { get; set; }
public transient List<ApexTrigger> triggerList { get; set; }
public transient List<ApexComponent> apexComponentList { get; set; }
public transient List<ObjectRelationshipWrapper> objectRelationList { get; set; }
// Target project prefix or managed namespace (e.g., 'c2g', 'MyProject')
private static final String PROJECT_PREFIX = 'c2g';
public ProjectComponentsController() {
loadPermissionSetsAndObjects();
loadApexClasses();
loadVisualforcePages();
loadTriggersAndComponents();
loadObjectRelationships();
}
private void loadPermissionSetsAndObjects() {
displayList = new List<DisplayListWrapper>();
// 1. Fetch project permission sets
String pSetFilter = '%' + PROJECT_PREFIX + '%';
permissionSetList = [
SELECT Id, Name, Label
FROM PermissionSet
WHERE Name LIKE :pSetFilter OR Label LIKE :pSetFilter
WITH USER_MODE
ORDER BY Label ASC
];
Map<Id, String> pMap = new Map<Id, String>();
for (PermissionSet ps : permissionSetList) {
pMap.put(ps.Id, ps.Label);
}
// 2. Identify Matching Custom Objects
Map<String, Schema.SObjectType> allObjects = Schema.getGlobalDescribe();
List<String> matchedObjectNames = new List<String>();
for (String key : allObjects.keySet()) {
if (key.containsIgnoreCase(PROJECT_PREFIX) && key.endsWithIgnoreCase('__c')) {
matchedObjectNames.add(key);
}
}
if (matchedObjectNames.isEmpty()) {
return;
}
// 3. Describe Object Details
Schema.DescribeSObjectResult[] describes = Schema.describeSObjects(matchedObjectNames);
List<String> cleanNames = new List<String>();
for (Schema.DescribeSObjectResult dr : describes) {
cleanNames.add(dr.getName().replace('__c', ''));
}
// 4. Retrieve Setup Durable IDs for direct linking
Map<String, String> setupUrlMap = new Map<String, String>();
for (EntityDefinition entity : [
SELECT DeveloperName, DurableId
FROM EntityDefinition
WHERE DeveloperName IN :cleanNames
WITH USER_MODE
]) {
setupUrlMap.put(entity.DeveloperName, entity.DurableId);
}
// 5. Query Object Permissions
List<ObjectPermissions> perms = [
SELECT ParentId, SobjectType
FROM ObjectPermissions
WHERE SobjectType IN :matchedObjectNames
WITH USER_MODE
];
Map<String, Set<String>> objToPermSetNames = new Map<String, Set<String>>();
for (ObjectPermissions op : perms) {
if (!objToPermSetNames.containsKey(op.SobjectType)) {
objToPermSetNames.put(op.SobjectType, new Set<String>());
}
if (pMap.containsKey(op.ParentId)) {
objToPermSetNames.get(op.SobjectType).add(pMap.get(op.ParentId));
}
}
for (Schema.DescribeSObjectResult dr : describes) {
String clean = dr.getName().replace('__c', '');
String durableId = setupUrlMap.containsKey(clean) ? setupUrlMap.get(clean) : dr.getName();
Set<String> permLabels = objToPermSetNames.get(dr.getName());
String permString = permLabels != null ? String.join(new List<String>(permLabels), ', ') : '--';
displayList.add(new DisplayListWrapper(dr.getLabel(), dr.getName(), durableId, permString));
}
displayList.sort();
}
private void loadApexClasses() {
String filter = '%' + PROJECT_PREFIX + '%';
classList = [
SELECT Id, Name, ApiVersion, Status
FROM ApexClass
WHERE Name LIKE :filter
WITH USER_MODE
ORDER BY Name ASC
];
}
private void loadVisualforcePages() {
pageClassList = new List<PageClassWrapper>();
String filter = '%' + PROJECT_PREFIX + '%';
for (ApexPage page : [
SELECT Id, Name, Markup
FROM ApexPage
WHERE Name LIKE :filter
WITH USER_MODE
ORDER BY Name ASC
]) {
String markup = page.Markup;
String stdCtrl = extractAttribute(markup, 'standardController');
String ctrl = extractAttribute(markup, 'controller');
String ext = extractAttribute(markup, 'extensions');
pageClassList.add(new PageClassWrapper(page.Id, page.Name, stdCtrl, ctrl, ext));
}
}
private void loadTriggersAndComponents() {
String filter = '%' + PROJECT_PREFIX + '%';
triggerList = [SELECT Id, Name, TableEnumOrId FROM ApexTrigger WHERE Name LIKE :filter WITH USER_MODE ORDER BY Name ASC];
apexComponentList = [SELECT Id, Name FROM ApexComponent WHERE Name LIKE :filter WITH USER_MODE ORDER BY Name ASC];
publicGroupList = [SELECT Id, Name, DeveloperName FROM Group WHERE Name LIKE :filter WITH USER_MODE ORDER BY Name ASC];
}
private void loadObjectRelationships() {
objectRelationList = new List<ObjectRelationshipWrapper>();
Map<String, Schema.SObjectType> allObjMap = Schema.getGlobalDescribe();
List<String> targetObjects = new List<String>();
for (String name : allObjMap.keySet()) {
if (name.containsIgnoreCase(PROJECT_PREFIX) && !name.containsIgnoreCase('history') && !name.containsIgnoreCase('share')) {
targetObjects.add(name);
}
}
if (targetObjects.isEmpty()) return;
Schema.DescribeSObjectResult[] describeResults = Schema.describeSObjects(targetObjects);
for (Schema.DescribeSObjectResult dr : describeResults) {
List<FieldDetailWrapper> fList = new List<FieldDetailWrapper>();
for (Schema.SObjectField fieldToken : dr.fields.getMap().values()) {
Schema.DescribeFieldResult fr = fieldToken.getDescribe();
if (fr.getRelationshipName() != null && !fr.getReferenceTo().isEmpty()) {
String relType = fr.getRelationshipOrder() == null ? 'Lookup' : 'Master-Detail';
String relCategory = fr.getRelationshipOrder() == 0 ? 'Primary' : (fr.getRelationshipOrder() == 1 ? 'Secondary' : '--');
fList.add(new FieldDetailWrapper(String.valueOf(fr.getReferenceTo()[0]), fr.getLabel(), relType, relCategory, fr.getRelationshipName()));
}
}
if (!fList.isEmpty()) {
objectRelationList.add(new ObjectRelationshipWrapper(dr.getLabel(), fList));
}
}
}
private String extractAttribute(String markup, String attributeName) {
if (String.isBlank(markup) || !markup.contains(attributeName + '="')) {
return '--';
}
Integer startIdx = markup.indexOf(attributeName + '="') + (attributeName + '="').length();
Integer endIdx = markup.indexOf('"', startIdx);
return (startIdx > 0 && endIdx > startIdx) ? markup.substring(startIdx, endIdx) : '--';
}
// Wrapper Classes
public class DisplayListWrapper implements Comparable {
public String objectLabel { get; set; }
public String objectApiName { get; set; }
public String setupUrl { get; set; }
public String permissionSets { get; set; }
public DisplayListWrapper(String label, String apiName, String url, String ps) {
this.objectLabel = label;
this.objectApiName = apiName;
this.setupUrl = url;
this.permissionSets = ps;
}
public Integer compareTo(Object compareTo) {
DisplayListWrapper other = (DisplayListWrapper) compareTo;
return this.objectLabel.compareTo(other.objectLabel);
}
}
public class PageClassWrapper {
public Id pageId { get; set; }
public String pageName { get; set; }
public String stdCtrlName { get; set; }
public String ctrlName { get; set; }
public String extName { get; set; }
public PageClassWrapper(Id i, String name, String std, String ctrl, String ext) {
this.pageId = i;
this.pageName = name;
this.stdCtrlName = std;
this.ctrlName = ctrl;
this.extName = ext;
}
}
public class ObjectRelationshipWrapper {
public String objectName { get; set; }
public List<FieldDetailWrapper> fieldRelations { get; set; }
public ObjectRelationshipWrapper(String obName, List<FieldDetailWrapper> fList) {
this.objectName = obName;
this.fieldRelations = fList;
}
}
public class FieldDetailWrapper {
public String relatedObjectName { get; set; }
public String relatedFieldName { get; set; }
public String relationType { get; set; }
public String relationCategory { get; set; }
public String relationshipName { get; set; }
public FieldDetailWrapper(String robName, String rFldName, String rType, String rCat, String rName) {
this.relatedObjectName = robName;
this.relatedFieldName = rFldName;
this.relationType = rType;
this.relationCategory = rCat;
this.relationshipName = rName;
}
}
}
3. Step 2: Visualforce Dashboard Markup
The Visualforce page incorporates the Salesforce Lightning Design System (<apex:slds />) and renders responsive tables with direct links to metadata records in Setup.
ProjectComponentsDashboard.page)
<apex:page controller="ProjectComponentsController" showHeader="false" sidebar="false" standardStylesheets="false" lightningStylesheets="true">
<apex:slds />
<div class="slds-scope slds-p-around_medium">
<!-- SLDS Header Banner -->
<div class="slds-page-header slds-m-bottom_medium">
<div class="slds-page-header__row">
<div class="slds-page-header__col-title">
<div class="slds-media">
<div class="slds-media__body">
<h1 class="slds-page-header__title slds-truncate" title="Project Inventory">
Project Component & Metadata Inventory
</h1>
<p class="slds-page-header__name-meta">
Centralized Audit of Objects, Code, Pages, and Security
</p>
</div>
</div>
</div>
</div>
</div>
<!-- Permission Sets Summary -->
<div class="slds-card slds-m-bottom_medium">
<div class="slds-card__header slds-grid">
<header class="slds-media slds-media_center slds-has-flexi-truncate">
<div class="slds-media__body">
<h2 class="slds-card__header-title">Permission Sets ({!permissionSetList.size})</h2>
</div>
</header>
</div>
<div class="slds-card__body slds-card__body_inner">
<div class="slds-grid slds-wrap">
<apex:repeat value="{!permissionSetList}" var="ps">
<div class="slds-col slds-size_1-of-3 slds-p-around_xx-small">
<a href="/{!ps.Id}" target="_blank" class="slds-text-link">{!ps.Label}</a>
</div>
</apex:repeat>
</div>
</div>
</div>
<!-- Custom Objects & Permissions Table -->
<div class="slds-card slds-m-bottom_medium">
<div class="slds-card__header slds-grid">
<header class="slds-media slds-media_center slds-has-flexi-truncate">
<div class="slds-media__body">
<h2 class="slds-card__header-title">Custom Objects ({!displayList.size})</h2>
</div>
</header>
</div>
<div class="slds-card__body">
<table class="slds-table slds-table_cell-buffer slds-table_bordered slds-table_striped">
<thead>
<tr class="slds-line-height_reset">
<th scope="col">Object Label</th>
<th scope="col">API Name</th>
<th scope="col">Associated Permission Sets</th>
</tr>
</thead>
<tbody>
<apex:repeat value="{!displayList}" var="obj">
<tr>
<td><a href="/lightning/setup/ObjectManager/{!obj.setupUrl}/view" target="_blank">{!obj.objectLabel}</a></td>
<td><code class="ap-code">{!obj.objectApiName}</code></td>
<td>{!obj.permissionSets}</td>
</tr>
</apex:repeat>
</tbody>
</table>
</div>
</div>
<!-- Visualforce Pages and Controllers -->
<div class="slds-card slds-m-bottom_medium">
<div class="slds-card__header slds-grid">
<header class="slds-media slds-media_center slds-has-flexi-truncate">
<div class="slds-media__body">
<h2 class="slds-card__header-title">Visualforce Pages ({!pageClassList.size})</h2>
</div>
</header>
</div>
<div class="slds-card__body">
<table class="slds-table slds-table_cell-buffer slds-table_bordered">
<thead>
<tr class="slds-line-height_reset">
<th scope="col">Page Name</th>
<th scope="col">Standard Controller</th>
<th scope="col">Custom Controller</th>
<th scope="col">Extensions</th>
</tr>
</thead>
<tbody>
<apex:repeat value="{!pageClassList}" var="p">
<tr>
<td><a href="/{!p.pageId}" target="_blank">{!p.pageName}</a></td>
<td><code class="ap-code">{!p.stdCtrlName}</code></td>
<td><code class="ap-code">{!p.ctrlName}</code></td>
<td><code class="ap-code">{!p.extName}</code></td>
</tr>
</apex:repeat>
</tbody>
</table>
</div>
</div>
</div>
</apex:page>
- View State Optimization: Declared collections as
transientto prevent bloated 135 KB View State limits. - Schema Sizing: Used
Schema.describeSObjects(List<String>)in bulk rather than individual describe calls in loops to avoid CPU time timeouts. - Deep Linking: Linked object labels directly to Lightning Object Manager using
EntityDefinition.DurableId. - Security Enforcement: Enforced User Mode on all metadata queries using
WITH USER_MODE.
4. Common Traps & Best Practices
Calling
Schema.getGlobalDescribe() instantiates metadata tokens for every standard and custom object in the org, consuming roughly 1 to 3 MB of heap space. Never execute global describes multiple times within the same transaction. Cache the result in a local map variable.
transient variables in Visualforce controllers when displaying large read-only metadata tables, and describe sObjects in bulk batches using Schema.describeSObjects().
- Filter Unnecessary Suffixes: Always filter out history (
__History), share (__Share), and feed (__Feed) tables when mapping data model relationships. - Regex Attribute Parsing: When reading controller attributes from page markup, ensure string indices safely check for closing quotes to avoid substring index out of bounds exceptions.
- Modern Alternative: For full-scale enterprise audits, combine this approach with the Tooling API or Salesforce CLI (
sf project deploy validate) to generate JSON-formatted dependency graphs.
Summary
Building a dynamic project component dashboard in Visualforce provides an instant architectural overview of your application metadata. By querying system tables, evaluating object relationships with Schema describes, and applying clean SLDS styling, you streamline release tracking, onboarding, and compliance reviews across your entire Salesforce organization.