Apex & .NET Basics
These are technical notes I compiled while studying using Trailhead, Salesforce's free self-learning portal.
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';
System.debug(msg);
}
}
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 {
Enum1,
Enum2,
Enum3
}
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.
List
- 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>();
myStrings.add('String1');
myStrings.add('String2');
myStrings.add('String3');
- 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;
Set
- 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];
Map
- 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:form>
<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:pageBlockButtons>
<apex:pageBlockSection >
<apex:inputField value="{!contact.lastname}" />
<apex:inputField value="{!contact.accountId}"/>
<apex:inputField value="{!contact.phone}"/>
</apex:pageBlockSection>
</apex:pageBlock>
</apex:form>
</apex:page>
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};
mail.setToAddresses(toAddresses);
mail.setSubject(subject);
mail.setPlainTextBody(body);
// 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.
inspectResults(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 theEmailManager
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;
}
}
- Apex Design Patterns
- Introducing Apex from Apex code developers guide
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:
- Database Trigger: specific event on a custom or standard object.
- Anonymous Apex: Code snippets executed on the fly in Dev Console & other tools.
- Asynchronous Apex: Occurs when executing a future or queueable Apex, running a batch job, or scheduling Apex to run at a specified interval.
- Web Services: Code that is exposed via SOAP or REST web services.
- Email Services: Code that is set up to process inbound email.
- 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:
- Trigger calls a method from a handler. Create that first:
AccountHandler
class- Developer Console > File > New > Apex Class
- Create the Account trigger:
AccountTrigger
trigger. Name:AccountTrigger
, Object:Account
- Developer Console > File > New > Apex Trigger
- Test using Execute Anonymous
- Developer Console > Debug > Open Execute Anonymous Window
- Note that a tab becomes available showing the execution log
- Trigger calls a method from a handler. Create that first:
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) {
AccountHandler.CreateNewOpportunity(Trigger.New);
}
}
Account acct = new Account(
Name='Test Account 2',
Phone='(415)555-8989',
NumberOfEmployees=50,
BillingCity='San Francisco');
insert acct;
- Examining the Execution Log
- Note that in the execution log,
EXECUTION_STARTED
andEXECUTION_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.
- Note that in the execution log,
- 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
- Two limits of primary concern:
- 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.
- Many developers fall into a trap of designing their code to work with a single record:
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);
opps.add(opp);
}
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,
@isTest
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);
accts.add(acct);
}
// Perform Test
Test.startTest();
insert accts;
Test.stopTest();
// 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) {
AccountTriggerHandler.CreateAccounts(Trigger.new);
}
}
@isTest
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,
BillingState='CA');
accts.add(acct);
}
// Perform Test
Test.startTest();
insert accts;
Test.stopTest();
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:
- Batch Apex
- Queueable Apex
- 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:
- Processing a large number of records
- Limits associated with asynchronous processes are higher than those with synchronous processes.
- Large number = thousands or millions
- Making callouts to external web services
- Callouts can take a long time to process. On Lightning Platform, triggers can’t make callouts directly.
- Creating a better and faster user experience
- Offload some processing to asynchronous calls. If it can wait, let it.
- Processing a large number of records
- 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
- Add
- To convert from synchronous to asynchronous, just:
public class MyFutureClass {
// Include callout=true when making callouts
@future(callout=true)
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,
con.FirstName,
con.LastName,
con.Email);
// 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:
- Implements the
Database.Batchable
interface, - Define
start()
,execute()
, andfinish()
methods,
- Implements the
- Then, invoke a batch class using the
Database.executeBatch
method. Example below.
global class MyBatchableClass implements
Database.Batchable<sObject>,
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();
Database.executeBatch(myBatchObject);
- 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,
con.FirstName,
con.LastName,
con.Email);
// 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
- Write to the debug log with code like:
- Using the Log Inspector
- Example of how to run some anonymous code and view the results, below:
- Open Developer Console: Gear Icon (top right) > Developer Console
- Debug > Change Log levels
- Click Add/Change link in General Trace Setting for You
- As an example, select INFO as debug level for all columns
- Click Done, click Done again
- Select Debug > Perspective Manager
- Select All (Predefined) and Set Default
- Select Debug > Open Execute Anonymous Window
- Insert the code snipped below
- Select the Open Log checkbox, then Execute
- Select Debug > Switch Perspective > All (Predefined)
- Review results in the Timeline and Executed Units tabs
- Under Execution Log, select Filter, then enter FINE. Since debug level was INFO for Apex Code, no results will appear.
- Select Debug > Change Log Levels
- Click Add/Change link in General Trace Setting for You.
- Change the Debug Level for Apex Code and Profiling to FINEST
- Click Done, click Done again
- Select Debug > Open Execute Anonymous Window
- Leave code that is currently there, then Execute
- 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
- Example of how to run some anonymous code and view the results, below:
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:
- Open Developer Console
- File > Open
- Select Classes
- Select AccountHandler
- Position cursor over a line in left margin and click once. A red dot appears next to the line number. Click.
- Select Debug > Open Execute Anonymous Window
- Run code snipped below.
- Select the Checkpoints Tab
- On the Symbols Tab, expand the nodes. Note the Key and Value columns.
- On the Heap Tab, note the Count and Total Size columns.
Account acct = new Account(
Name='Test Account 3',
Phone='(415)555-8989',
NumberOfEmployees=30,
BillingCity='San Francisco');
insert acct;
- Resources
- Primary source of debug info on the Lightning Platform is the Debug Log
- Which is true regarding checkpoints: Execution doesn’t stop on the line with the checkpoint