Wednesday, 5 February 2025

Evaluate Dynamic Formulas in Apex GA

Image created by ChatGPT4o based on a prompt by Bob Buzzard

Introduction

Like zip handling in Apex, the ability to evaluate dynamic formulas in Apex is Generally Available in the Spring '25 release of Salesforce. Unlike zip handling, which we've all been wanting/battling with for years, this might not be such an obvious win. It's definitely something I could have used a few times in my Salesforce career, mostly around rule engines. I've written several of these in my time, to do things like apply a discount to a price based on a number of attributes of a customer, their spend, and whether they have a contract with us. 

In order to avoid everyone having to become an Apex or Flow expert, rules are configured through custom settings or custom metadata types, but defining the criteria to execute rules is a bit more tricky to surface for Administrator configuration, especially if you need to look at the values contained by a specific record. Then it's a toss up between express it in pro/low code and make it less configurable, or write something to validate and parse boolean style expressions including records and fields. Neither are great.

With the ability to evaluate formula fields in Apex, Admins can express criteria in a familiar format and this can easily be evaluated by the engine to decide whether to apply the rule.

The Sample

The examples I'd seen to date were mostly querying a record from the database and evaluating a formula to concatenate some fields against it, but I didn't find that overly exciting. Instead I went a different route - as I'm envisaging an Administrator creating the formula as plain text in a configuration record, so it would be useful to allow them to check it works before settling on it. I guess it could also be used to test someone's ability to create valid formulas without needing to go into Setup and create a field. So many possibilities!

The page itself is a pretty simple:


There's a picklist to choose the sobject type (limited to Account, Opportunity and Contact - this is a demo after all), another picklist to choose the formula return type, and a lookup to select the record to use when evaluating the formula. In this case my test record is in the Technology business and doing pretty well with annual revenue of ten million:


My rule should fire if the candidate account is in the technology business and making less than fifty million a year, so lets try that out:



And it works. But this is hardly an exhaustive test, so lets check what happens if I change the revenue target to less than nine million a year:


Which also works. But why is the result displayed in green regardless of whether it is true or false? Because the formula evaluated successfully of course - if I introduce an error into my formula, and try to compare the website to a numeric value, the result is red :


Show Me The Code!

You can find the code in my Spring '25 samples Github repository. The method to evaluate the formula is as follows:

    @AuraEnabled
    public static String CheckFormula(String formulaStr, String sobjectType, 
                                      String returnTypeStr, Id recordId)
    {
        FormulaEval.FormulaReturnType returnType=
                  FormulaEval.FormulaReturnType.valueof(returnTypeStr);

        FormulaEval.FormulaInstance formulaInstance = Formula.builder()
                                        .withType(Type.forName(sobjectType))
                                        .withReturnType(returnType)
                                        .withFormula(formulaStr)
                                        .build();


        String fieldNameList = String.join(formulaInstance.getReferencedFields(),',');
        String queryStr = 'select ' + fieldNameList + ' from ' + sobjectType +
                          ' where id=:recordId LIMIT 1'; 
        SObject s = Database.query(queryStr);
        
        Object formulaResult=formulaInstance.evaluate(s);
        return formulaResult.toString();
    }

The information populated by the user through the various inputs is passed through verbatim and uses to build the formula instance. One aspect that has changed since I used the original beta is the ability to have the formula instance tell me which fields I need to query from the record via the getReferencedFields method, so I can drop them into my dynamic query with minimal effort:

    String fieldNameList = String.join(formulaInstance.getReferencedFields(),',');
    String queryStr = 'select ' + fieldNameList + ' from ' + sobjectType +
                      ' where id=:recordId LIMIT 1'; 
    SObject s = Database.query(queryStr);

More Information




Sunday, 2 February 2025

Zip Handling in Apex GA


Image generated by ChatGPT 4o based on a prompt from Bob Buzzard

Introduction

Anyone who's developed custom solutions in Salesforce world is likely to have come up against the challenge of processing zip files. You could use a pure Apex solution, such as Zippex, leverage the Metadata API or push it all to the front end and use JavaScript. What these options all had in common was leaving you dissatisfied. Apex isn't the best language for CPU intensive activities like compression, so the maximum file size was relatively small. The metadata API isn't officially supported and introduces asynchronicity, while forcing the user to the front end simply to extract the contents of a file isn't the greatest experience and might introduce security risks.

One year ago, in the Spring '24 release of Salesforce, we got the developer preview of Zip Handling in Apex which looked very cool. I encountered a few teething issues, but the preview was more than enough to convince me this was the route forward once Generally Available. Fast forward a week or so from now (31st Jan) and it will be GA in the Spring '25 release. That being the case, I felt I should revisit.

The Sample

I built the sample in a scratch org before the release preview window opened, and all I had to specify was the ZipSupportInApex feature. The sample is based on my earlier attempts with the developer preview - Lightning Web Component front end which allows you to select a zip file from Salesforce Files, an Apex controller to extract the entries from the zip file and more Lightning Web Component to display the details to the user:


The key differences are (a) I could get the zip entries back successfully this time and (b) I captured how much CPU and heap were consumed processing the zip file. The heap was slightly larger than the size of the zip file in this case, but the CPU was a really nice surprise - just 206 milliseconds consumed.

One expected fun side effect was the ability to breach the heap size limit quite dramatically. As we all know, this limit is enforced through periodic checking rather than a finite heap size, so you can go over as long as it's only for a short while. Processing the entries in a zip file clearly satisfies the "short while" requirement, as I was able to extract the contents of a 24Mb zip file, pushing the heap close to 25Mb:


It was gratifying to see that once again the CPU wasn't too onerous - even when handling a zip file that is theoretically far too large, only 1,375 milliseconds of CPU were consumed. 

Show Me The Code!

You can find the code in my Spring '25 Github repository. The key Apex code is as follows:

   @AuraEnabled(cacheable=true)
    public static ZipResponse GetZipEntries(Id contentVersionId)
    {
        ZipResponse response=new ZipResponse();
        List<Entry> entries=new List<Entry>();
        try 
        {
            ContentVersion contentVer=[select Id, ContentSize, VersionData, Title, Description, PathOnClient
                                        from ContentVersion
                                        where Id=:contentVersionId];

            Blob zipBody = contentVer.VersionData;
            Compression.ZipReader reader = new Compression.ZipReader(zipBody);
            for (Compression.ZipEntry zipEntry : reader.getEntries())
            {
                System.debug('Entry = ' + zipEntry.getName());
                Entry entry=new Entry();
                entry.name=zipEntry.getName();
                entry.method=zipEntry.getMethod().name();
                entry.compressedSize=zipEntry.getCompressedSize();
                entry.uncompressedSize=zipEntry.getUncompressedSize();
                entries.add(entry);
            }

            response.entries=entries;
            response.cpu=Limits.getCpuTime();
            response.size=contentver.ContentSize;
            response.heap=Limits.getHeapSize();
        }
        catch (Exception e)
        {
            System.debug(e);
        }

        return response;
    }

ZipResponse is a custom class to return the information in a friendly format to the front end, including the CPU and Heap consumption, while Entry is another custom class that wraps the information I want to show the user in an AuraEnabled form. The actual amount code required to process a Zip file is very small - once you have the zip file body as a Blob, simple create a new ZipReader on it and iterate the entries:

   Blob zipBody = contentVer.VersionData;
    Compression.ZipReader reader = new Compression.ZipReader(zipBody);
    for (Compression.ZipEntry zipEntry : reader.getEntries())
    {
        // your code here!
    }


Performance

Regular readers of this blog will know that I'm always keen to compare performance, so here's the CPU and heap consumption for the new on platform solution versus Zippex for my test zip files

Zip Size On Platform Zippex
CPU Heap CPU Heap
66,408 30 90,306 19 142,149
123,746 57 143,425 17 253,086
3,992,746 48 4,022,537 208 8,000,354
8,237,397 58 8,256,426 387 16,480,442
24,014,156 973 24,431,186 Heap size exception at 48,028,743

Zippex uses less CPU at the start, but that looks to be falling behind as the zip file size increases. Heap is where the on platform solution really scores though, needing 60-100% less than Zippex and able to process larger files.

More Information




Wednesday, 29 January 2025

Book Review - Salesforce Anti-Patterns


 Disclaimer - I didn't purchase this book, I was given a pre-release copy by Packt Publishing to review

I've been interested in anti-patterns since I read a short article, more years ago than I care to remember, that covered 3-4 of them. When I joined the Salesforce ecosystem, the anti-pattern I saw most of the time was the big ball of mud, which I still reference in talks to this day, "These systems show unmistakable signs of unregulated growth, and repeated expedient repair"- something all of us who have been here for a while are familiar with. I was therefore curious when Packt Publishing reached out and asked me to review the second edition of Lars Malmqvist's book on Salesforce Anti-Patterns - which of my old favourite would I see in there, and what was new in the world of bad solutions to problems that look good at first glance. It's fair to say I wasn't disappointed.

This is a great book for any aspiring architect, as it will teach you the red flags to look out for, which aren't always obvious. Sometimes the problems they indicate take months or even years to appear, but appear they surely will. Recognising this at the outset will save you a lot of angst.

It's also a great book for experienced architects, as it gives you facts rather than feelings with which to argue against a particular approach. Instead of saying that you've seen this kind of thing before and it didn't end well, you can point out the planned (or current) strategy exhibits the classic symptoms of a named anti-pattern, talk knowledgeably about the likely consequences, and advise on alternatives that will lead to better outcomes. I also like that this book isn't entirely focused on programming or technical anti-patterns - it covers the human side of things too, such as communication, collaboration, allocation of work. 

While there are plenty of the old favourites in there, that apply equally to any technology project rather than just Salesforce, there are also a number that you'll only see in a Salesforce project. These are related to specific characteristics of Salesforce like sharing, licensing, the data model, so you are unlikely to encounter them elsewhere.

Each anti-pattern is explained through a scenario, followed by a description of the problem, how it is presented as the solution to a problem, and the likely outcome of using it (rarely good), followed by a description of a better solution. Each chapter concludes with key takeaways and, in a nice touch, advice around the topic area for the CTA review board. A second nice touch is the covering how the introduction of/reliance on AI can bring you into an anti-pattern that you might otherwise have avoided.

One thing I will call out though - the likely real-world experience is that the worst of the anti-pattern is rowed back, but there still isn't the appetite to accept the better solution - life is rarely that neat and tidy. And that's okay - as architects and consultants our job is to advise and make the customer aware of the risks involved. Once that is done, the final decision rests with the customer, and it will be an informed decision, aware of the potential outcome. As long as all stakeholders agree and accept the risk, nobody can point fingers later. Striving for perfection and coming up a bit short is far better than accepting failure at the outset!

The second edition is available at https://packt.link/U7I6R - you won't regret buying it.