Apex & .NET Basics

Map .NET Concepts to the Lightning Platform

Understand which key features make up the Lightning Platform and the Apex programming language. Identify similarities and differences between .NET and the Lightning Platform. Use Developer Console to create your first Apex class. Use Anonymous Apex to invoke a method from an Apex class.
  • Platform basics
    • .NET developers are familiar with cloud applications that run on Microsoft Azure.
    • Lightning Platform works differently as it is tightly integrated with the database - UI, Security, reporting, etc, are built right in with the platform.
    • Since Lightning Platform is so tightly integrated and relies on a metadata architecture, a lot can be accomplished declaratively (“point-and-click”). Code is not always needed.
  • Apex Basics
    • Apex programming language is similar to one that is commonly used by .NET developers: C#
    • Apex is saved, compiled, and executed directly on the Lightning Platform
  • Object-oriented Design
    • Apex supports many object-oriented principles: encapsulation, abstraction, inheritance, polymorphism.
    • Apex supports many language constructs: classes, interfaces, properties, collections.

Example Apex class named HelloWorld:

public with sharing class HelloWorld {
    public void printMessage() {
        String msg = 'Hello World';

Basic syntax for defining classes:

private | public | global
[virtual | abstract | with sharing | without sharing]
class ClassName [implements InterfaceNameList] [extends ClassName]
    // The body of the class
  • Data Types
    • The usual primitives: Integer, Double, Long, Date, Datetime, String, Boolean
    • ID datatype for any valid 18-character Lightning Platform record identifier assigned by the system
    • All variables are initialized to null by default
      • In .NET, strings are references b/c they’re immutable. In Apex, strings are treated like primitives.
    • Other data types include sObjects (generic or specific, like an Account or Contact)
    • Data type can also be a typed list of values, known as an enum. Enums can be used with numbers but we can’t define what the number values are.
      • Ordinal assignment starts at zero.
public enum myEnums {
 Integer enumOrd = myEnums.Enum3.ordinal(); // Value of the enumOrd variable would be 2.
  • Working with Collections
    • List: an ordered collection of elements that works much the same as a traditional array.
    • Set: an unordered collection of elements that does not contain duplicates.
    • Map: collection of key-value pairs. Each key maps to a single value.


  • Declare a list of strings:
    • List<String> myStrings = new List<String>();
  • Declare the myStrings variable as an array but assign it to a list instead of an array:
    • String[] myStrings = new List<String>();
  • Declare the list and initialize its values in a single step:
    • List<String> myStrings = new List<String> {'String1', 'String2', 'String3' };
  • Add values to the list after it has been created:
List<String> myStrings = new List<String>();
  • Lists are common, because the output of every SOQL query is a list. The following creates a list of Accounts:
    • List<Account> myAccounts = [SELECT Id, Name FROM Account];
  • Like arrays, lists have indexes that start at zero. Access the name of the first account in the list like this:
List<Account> myAccounts = [SELECT Id, Name FROM Account];
String firstAccount = myAccounts[0].Name;


  • Sets are commonly used to store ID values because the values are always unique
  • They are commonly used as part of a WHERE clause in a SOQL query
Set<ID> accountIds = new Set<ID>{'001d000000BOaHSAA1','001d000000BOaHTAA1'};  
List<Account> accounts = [SELECT Name FROM Account WHERE Id IN :accountIds];    


  • Collection of key-value pairs. Each key maps to a single value.
  • Useful when you need to quickly find something by key.
  • Key values must be unique.
  • For example, use the following code to declare a map variable named accountMap that contains all Accounts mapped to their IDs.
Map<Id, Account> accountMap = new Map<Id, Account>([SELECT Id, Name FROM Account]);
  • Access a specific Account record using the get method:
Id accId = '001d000000BOaHSAA1';
Account account = accountMap.get(accId);

ASP.NET to Visualforce

  • Visualforce is a framework for rendering HTML pages using an MVC paradigm.
    • Both render web pages and both separate the application logic from the markup and the database model.
    • Visualforce Basics module
    • Example code below uses a “standard controller.” Can also create custom controllers to add more complex functioning.
<apex:page standardController="Contact">
    <apex:pageBlock title="Edit Contact" mode="Edit">
       <apex:pageBlockButtons >
         <apex:commandButton action="{!edit}" id="editButton" value="Edit"/>
         <apex:commandButton action="{!save}" id="saveButton" value="Save"/>
         <apex:commandButton action="{!cancel}" id="cancelButton" value="Cancel"/>
       <apex:pageBlockSection >
          <apex:inputField value="{!contact.lastname}" />
          <apex:inputField value="{!contact.accountId}"/>
          <apex:inputField value="{!contact.phone}"/>

Differences between Apex and .NET

  • Apex is not case sensitive, C# is
  • Apex and Database are Tightly Coupled
    • Each standard or custom object in the database has an Apex class representation that provides functionality to make interacting with the database easy.
    • Class and its underlying object are a mirror image that’s always in sync
  • Different Design Pattern
    • .NET design patterns generally don’t work on Lightning Platform
  • Unit Tests are Required
    • 75% code coverage is required to deploy Apex code to a production org
    • Unit tests promote development of robust, error-free code
    • All tests are run before every major release
  • No Solution, Project, or Config Files
    • An application on the Lightning Platform is just a loose collection of components, like tabs, reports, dashboards, pages
    • All code resides and executes in the cloud
  • Much Smaller Class Library
    • Apex class library is much smaller than the .NET Framework class library - easier and faster to come up to speed with Apex
    • Lightning Platform is built with idea of providing rapid application development.
    • If you’re looking to build pixel-perfect, custom-coded applications, the Heroku Enterprise platform provides the power and features you need.
  • Development Tool
    • You can use Developer Console to edit and navigate source code
    • You can use Developer Console to execute SOQL and SOSL queries and view query plans
    • Salesforce Extensions for VS Code are available
      • Tied closely to Salesforce DX, providing a modern source-driven development experience
    • Salesforce CLI provides a powerful command-line interface to the Lightning Platform
    • Check out Salesforce Tools and Toolkits for more community-contributed tools (some free, some paid)
  • Handling Security
    • Do not need to worry about authentication, storing passwords, database connection strings, etc
    • Identity is handled by the platform - can control access to data at many levels, including object level, record level, field level
    • Security is often defined and set up by a Salesforce administrator
  • Integration
    • Many ways to integrate with Salesforce, but SOAP and REST are most common
    • Can create and expose web services using Apex programming language
    • Can invoke external web services from Apex
    • Can react to incoming email messages and have automated outbound messages sent when certain events occur
    • Salesforce offers SOAP and REST APIs that provide direct access to the data in your org. Various toolkits that wrap the APIs are available, so you can use whatever language you prefer: .NET, Java, PHP, Objective C, Ruby, Javascript, etc.
    • Many third-part integration applications are avalable on the AppExchange - anything is possible.

Create an Apex Class

  • Create an Apex Class via Setup > Developer Console
  • Then, File > New > Apex Class
  • Enter a class name, like EmailManager
  • Add code as required, like such as that below from Trailhead
public with sharing class EmailManager {
    // Public method
    public static void sendMail(String address, String subject, String body) {
        // Create an email message object
        Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
        String[] toAddresses = new String[] {address};
        // Pass this email message to the built-in sendEmail method
        // of the Messaging class
        Messaging.SendEmailResult[] results = Messaging.sendEmail(
            new Messaging.SingleEmailMessage[] { mail });
        // Call a helper method to inspect the returned results.
    // Helper method
    private static Boolean inspectResults(Messaging.SendEmailResult[] results) {
        Boolean sendResult = true;
        // sendEmail returns an array of result objects.
        // Iterate through the list to inspect results.
        // In this class, the methods send only one email,
        // so we should have only one result.
        for (Messaging.SendEmailResult res : results) {
            if (res.isSuccess()) {
                System.debug('Email sent successfully');
            } else {
                sendResult = false;
                System.debug('The following errors occurred: ' + res.getErrors());                 
        return sendResult;

Invoke a Method

  • Invoke a method via: Setup > Developer Console
  • Debug > Open Execute Anonymous Window
  • Delete any existing code, and add the following from Trailhead (as an example)
EmailManager.sendMail('Your email address', 'Trailhead Tutorial', '123 body');
  • Example above uses the sendMail method of the EmailManager class above

An Example Apex class that returns Accounts

public class AccountUtils {
    public static Account[] accountsByState (String state) {
        List<Account> accounts = [SELECT ID, Name FROM Account WHERE BillingState = :state];
        return accounts;

Understand Execution Context

Know which methods to use to invoke Apex. Write a trigger for a Salesforce object. Observe how execution context works by executing code in Developer Console. Understand how governor limits impact design patterns. Understand the importance of working with bulk operations.
  • What Is Execution Context?
    • ASP.NET applications execute within context of an application domain
    • Lightning Platform applications execute within an execution context
      • Execution Context represents the time between when code is executed and when it ends
      • Your Apex code is usually not the only code that is executing
    • Ways to invoke Apex:
      1. Database Trigger: specific event on a custom or standard object.
      2. Anonymous Apex: Code snippets executed on the fly in Dev Console & other tools.
      3. Asynchronous Apex: Occurs when executing a future or queueable Apex, running a batch job, or scheduling Apex to run at a specified interval.
      4. Web Services: Code that is exposed via SOAP or REST web services.
      5. Email Services: Code that is set up to process inbound email.
      6. Visualforce or Lightning Pages: Visualforce controllers and Lightning components can execute Apex code automatically or when a user initiates an action, such as clicking a button. Lightning components can also be executed by Lightning processes and flows.
    • By default, Apex executes in system context, which means it has access to all objects and fields. Object permissions, field-level security, and sharing rules aren’t applied for the current user.
      • You can use the with/without sharing keyword to specify that the sharing rules for the current user be taken into account. Reference this sharing keyword help
  • Trigger Essentials
    • Apex database triggers execute programming logic before or after events related to records in Salesforce.
    • When defining trigger, can specify more than one of the following events:
      • before insert, before update, before delete, after insert, after update, after delete, after undelete
    • Productivity tip: Only resort to using a trigger when you are absolutely sure that the same thing cannot be accomplished using point-and-click automation (Flows, Process Builder)
      • This will help avoid introducing a lot of unnecessary technical overhead to a Salesforce org.
    • Basic syntax for a trigger:
trigger TriggerName on ObjectName (trigger_events) {
   // code_block
  • Mark Execution Context
    • Example below walks through creating an Apex database trigger that creates an opportunity when a new account is entered.
    • Best practice: write one trigger per object, then use context-specific handler methods within triggers to create logic-less triggers
    • Steps:
      1. Trigger calls a method from a handler. Create that first: AccountHandler class
        • Developer Console > File > New > Apex Class
      2. Create the Account trigger: AccountTrigger trigger. Name: AccountTrigger, Object: Account
        • Developer Console > File > New > Apex Trigger
      3. Test using Execute Anonymous
        • Developer Console > Debug > Open Execute Anonymous Window
        • Note that a tab becomes available showing the execution log
public with sharing class AccountHandler {
    public static void CreateNewOpportunity(List<Account> accts) {
        for (Account a : accts) {
            Opportunity opp = new Opportunity();
            opp.Name = a.Name + ' Opportunity';
            opp.AccountId = a.Id;
            opp.StageName = 'Prospecting';
            opp.CloseDate = System.Today().addMonths(1);
            insert opp;
trigger AccountTrigger on Account (before insert, before update, before
    delete, after insert, after update, after delete,  after undelete) {
    if (Trigger.isAfter && Trigger.isInsert) {
Account acct = new Account(
    Name='Test Account 2',
    BillingCity='San Francisco');
insert acct;
  • Examining the Execution Log
    • Note that in the execution log, EXECUTION_STARTED and EXECUTION_ENDED events delineate the execution context for the Execute Anonymous and BeforeInstert events
    • All the code below operates in the same execution environment and is therefore subject to the same set of governor limits.
    • Since Salesforce is a multi-tenant environment, governor limits keep each instance of a Salesforce org from consuming too many resources.

  • Working with Limits
    • Two limits of primary concern:
      • # of SOQL Queries, # of DML Statements
    • Lots of other limits too, and they change with each major release.
    • Commonly, they get looser, not tighter.
    • Resource: Executing Governors and Limit
  • Working in Bulk
    • Many developers fall into a trap of designing their code to work with a single record:
      • Apex triggers can retrieve up to 200 objects at once.
      • Synchronous limit for the total number of SOQL queries is 100, and 150 for the total number of DML statements issued.
      • If you have a trigger that performs a SOQL query or DML statement inside of a loop and the trigger was fired for a bulk operation, likely to get a limits error.
      • It then becomes necessary to “bulkify” the code. Reference the Bulk Apex Triggers Module.
    • Note that the AccountHandler code above has an insert DML operation inside a loop. Can fix by changing it to write to a list variable and then insert the contents in one step.

Before bulkify:

public with sharing class AccountHandler {
    public static void CreateNewOpportunity(List<Account> accts) {
        for (Account a : accts) {
            Opportunity opp = new Opportunity();
            opp.Name = a.Name + ' Opportunity';
            opp.AccountId = a.Id;
            opp.StageName = 'Prospecting';
            opp.CloseDate = System.Today().addMonths(1);
            insert opp;

After bulkify:

public with sharing class AccountHandler {
    public static void CreateNewOpportunity(List<Account> accts) {
        List<Opportunity> opps = new List<Opportunity>();
        for (Account a : accts) {
            Opportunity opp = new Opportunity();
            opp.Name = a.Name + ' Opportunity';
            opp.AccountId = a.Id;
            opp.StageName = 'Prospecting';
            opp.CloseDate = System.Today().addMonths(1);
        if (opps.size() > 0) {
            insert opps;
  • Test code below is used to make sure the trigger can handle a load of 200 records. Writing unit tests like this ensure the code works is a best practice.
    • Developer Console > File > New > Apex Class > add code below and save
    • Test > New Run > Select AccountTrigger_Test as TestClass, TestCreateNewAccounntInBulk as test method
    • Check Test tab and verify the test runs to completion without failures
  • Unit tests work in the same way on the Lightning Platform as they do in .NET,
private class AccountTrigger_Test {
    @isTest static void TestCreateNewAccountInBulk() {
        // Test Setup data
        // Create 200 new Accounts
        List<Account> accts = new List<Account>();
        for(Integer i=0; i < 200; i++) {
            Account acct = new Account(Name='Test Account ' + i);
        // Perform Test
        insert accts;                               
        // Verify that 200 new Accounts were inserted
        List<Account> verifyAccts = [SELECT Id FROM Account];
        System.assertEquals(200, verifyAccts.size());    
        // Also verify that 200 new Opportunities were inserted
        List<Opportunity> verifyOpps = [SELECT Id FROM Opportunity];
        System.assertEquals(200, verifyOpps.size());                             
  • Other notes:
    • Apex uses familiar try-catch-finally blocks to handle exceptions.
    • No such thing as application/session variables in Lightning Platform. If data needs to persist between classes, need to use static variables.
      • Static variables only persist information within a single execution context.
    • There are many tradeoffs to consider when working with limits.

  • An example Apex trigger handler, trigger, and test class that modifies Account fields before inserting records:
public class AccountTriggerHandler {
    public static void CreateAccounts(List<Account> accts) {
        for (Account a : accts) {
            a.ShippingState = a.BillingState;
trigger AccountTrigger on Account (before insert) {
    if (Trigger.isBefore && Trigger.isInsert) {
private class AccountTriggerTest {
    @isTest static void TestCreateNewAccountInBulk () {
		// Test Setup data
        // Create 200 new Accounts
        List<Account> accts = new List<Account>();
        for(Integer i=0; i < 200; i++){
            Account acct = new Account(Name='Test Account ' + i,
        // Perform Test
        insert accts;
        List<Account> verifyAccts = [SELECT ShippingState FROM Account];
        for(Account a : verifyAccts){
            System.assertEquals(a.ShippingState, 'CA');

Use Asynchronous Apex

Know when to use Asynchronous Apex. Use future methods to handle a web callout. Work with the batchable interface to process a large number of records. Understand the advantages of using the queueable interface when you need to meet in the middle.
  • Three types of asynchronous processing:
    1. Batch Apex
    2. Queueable Apex
    3. Future Methods

  • When to Go Asynchronous
    • Benefits of asynchronous programming are well-understood to .NET developers
    • 3 reasons to use asynchronous programming on the Lightning Platform:
      1. Processing a large number of records
        • Limits associated with asynchronous processes are higher than those with synchronous processes.
        • Large number = thousands or millions
      2. Making callouts to external web services
        • Callouts can take a long time to process. On Lightning Platform, triggers can’t make callouts directly.
      3. Creating a better and faster user experience
        • Offload some processing to asynchronous calls. If it can wait, let it.
  • Future Methods
    • To convert from synchronous to asynchronous, just:
      • Add @future annotation to the method
      • Make sure method is static
      • Make sure method returns a void type
public class MyFutureClass {
    // Include callout=true when making callouts
    static void myFutureMethod(Set<Id> ids) {
        // Get the list of contacts in the future method since
        // you cannot pass objects as arguments to future methods
        List<Contact> contacts = [SELECT Id, LastName, FirstName, Email
            FROM Contact WHERE Id IN :ids];
        // Loop through the results and call a method
        // which contains the code to do the actual callout
        for (Contact con: contacts) {
            String response = anotherClass.calloutMethod(con.Id,
            // May want to add some code here to log
            // the response to a custom object
  • Future Limitations
    • Can’t track execution. No Apex job ID is returned.
    • Future methods can’t take objects as arguments. Parameters must be primitive data types, arrays of primitive data types, or collections of primitive data types.
    • Can’t chain future methods. One cannot call another.
  • Batch or Scheduled Apex
    • Long-used asynchronous tool is the batchable interface. Use it if you need to process a large number of records. Ex: clean up or archive 50 million records.
    • To use it, your class:
      1. Implements the Database.Batchable interface,
      2. Define start(), execute(), and finish() methods,
    • Then, invoke a batch class using the Database.executeBatch method. Example below.
global class MyBatchableClass implements
            Database.Stateful {  
    // Used to record the total number of Accounts processed
    global Integer numOfRecs = 0;
    // Used to gather the records that will be passed to the interface method
    // This method will only be called once and will return either a
    // Database.QueryLocator object or an Iterable that contains the records
    // or objects passed to the job.
    global Database.QueryLocator start(Database.BatchableContext bc) {
        return Database.getQueryLocator('SELECT Id, Name FROM Account');
    // This is where the actual processing occurs as data is chunked into
    // batches and the default batch size is 200.
    global void execute(Database.BatchableContext bc, List<Account> scope) {
        for (Account acc : scope) {
            // Do some processing here
            // and then increment the counter variable
            numOfRecs = numOfRecs + 1;
    // Used to execute any post-processing that may need to happen. This
    // is called only once and after all the batches have finished.
    global void finish(Database.BatchableContext bc) {
        EmailManager.sendMail('[email protected]',
                              numOfRecs + ' Accounts were processed!',
                              'Meet me at the bar for drinks to celebrate');
  • Invoke the batch class using anonymous code like the following.
MyBatchableClass myBatchObject = new MyBatchableClass();
  • Note that Scheduled Apex is another option. Reference the Anonymous Apex Trailhead.
  • Batchable Limitations
    • Troubleshooting is difficult
    • Jobs are queued and subject to server availability
    • Limits
  • Queueable Apex
    • Queueable Apex was the answer to the issue of limits.
    • Brings to gether the best of future methods and the batchable interface, rolled into a single asynchronous tool. Benefits:
      • Non-primitive types: Can accept parameter variables of non-primitive data types, like sObjects or custom Apex types
      • Monitoring: When you submit your job, a jobId is returned, enabling you to identify the job and its progress
      • Chaining jobs: Chain one job to another by starting a second job from a running job. This is useful for sequential processing.
    • Easier to implement than Batch Apex. Same code above that used a future method to do a web callout, reworked with Queueable Apex.
public class MyQueueableClass implements Queueable {
    private List<Contact> contacts;
    // Constructor for the class, where we pass
    // in the list of contacts that we want to process
    public MyQueueableClass(List<Contact> myContacts) {
        contacts = myContacts;
    public void execute(QueueableContext context) {
        // Loop through the contacts passed in through
        // the constructor and call a method
        // which contains the code to do the actual callout
        for (Contact con: contacts) {
            String response = anotherClass.calloutMethod(con.Id,
            // May still want to add some code here to log
            // the response to a custom object
  • Invoke Queueable Apex with the following.
List<Contact> contacts = [SELECT Id, LastName, FirstName, Email
    FROM Contact WHERE Is_Active__c = true];
Id jobId = System.enqueueJob(new MyQueueableClass(contacts));
  • Choose asynchronous processing when:
    • You need to make callouts to external web services
    • You want to create a better and faster user experience
    • You are processing a large number of records

Debug and Run Diagnostics

Understand which debugging features are available on the Lightning Platform. Use the Log Inspector in Developer Console to examine debug logs.
  • Debugging on Lightning Platform versus in Visual Studio
    • Debugging on Lightning is not as easy as it is in Visual Studio, due to challenges associated with working in a multi-tenant cloud environment.
    • The ease of debugging is improving with new releases.
  • Debug Log
    • Write to the debug log with code like: System.debug('My Debug Message');
    • Specify one of the following logging levels (lowest to highest and cumulative)
      • NONE
      • ERROR
      • WARN
      • INFO
      • DEBUG
      • FINE
      • FINER
      • FINEST
    • Limits to debug logs:
      • Each debug log must be 20 MB or smaller. If larger, you won’t see everything you need.
      • Each org can retaain up to 1,000 MB of debug logs. The oldest logs are overwritten.
    • Debug logs are primaary way of getting debug info, so make sure not to exceed these limits.
    • Resources from Apex Code Developer’s Guide
  • Using the Log Inspector
    • Example of how to run some anonymous code and view the results, below:
      1. Open Developer Console: Gear Icon (top right) > Developer Console
      2. Debug > Change Log levels
      3. Click Add/Change link in General Trace Setting for You
      4. As an example, select INFO as debug level for all columns
      5. Click Done, click Done again
      6. Select Debug > Perspective Manager
      7. Select All (Predefined) and Set Default
      8. Select Debug > Open Execute Anonymous Window
      9. Insert the code snipped below
      10. Select the Open Log checkbox, then Execute
      11. Select Debug > Switch Perspective > All (Predefined)
      12. Review results in the Timeline and Executed Units tabs
      13. Under Execution Log, select Filter, then enter FINE. Since debug level was INFO for Apex Code, no results will appear.
      14. Select Debug > Change Log Levels
      15. Click Add/Change link in General Trace Setting for You.
      16. Change the Debug Level for Apex Code and Profiling to FINEST
      17. Click Done, click Done again
      18. Select Debug > Open Execute Anonymous Window
      19. Leave code that is currently there, then Execute
      20. Under Execution Log, select Filter, then FINE. The ‘My Fine Debbug Message’ is now displayed. There will also be a size difference between the two laatest logs in the Logs tab.
        • INFO: 1.78 KB
        • FINEST: 14.78 KB
System.debug(LoggingLevel.INFO, 'My Info Debug Message');
System.debug(LoggingLevel.FINE, 'My Fine Debug Message');
List<Account> accts = [SELECT Id, Name FROM Account];
for(Account a : accts) {
    System.debug('Account Name: ' + a.name);
    System.debug('Account Id: ' + a.Id);
  • Set Checkpoints
    • .NET developers are used to setting breakpoints in applications. This isn’t possible in a cloud-based multi-tenanted environment.
    • Checkpoints are similar to breakpoints - they reveal a lot of detailed execution info about a line of code.
    • Instructions to setting a breakpoint:
      1. Open Developer Console
      2. File > Open
      3. Select Classes
      4. Select AccountHandler
      5. Position cursor over a line in left margin and click once. A red dot appears next to the line number. Click.
      6. Select Debug > Open Execute Anonymous Window
      7. Run code snipped below.
      8. Select the Checkpoints Tab
      9. On the Symbols Tab, expand the nodes. Note the Key and Value columns.
      10. On the Heap Tab, note the Count and Total Size columns.
Account acct = new Account(
    Name='Test Account 3',
    BillingCity='San Francisco');
insert acct;