References Speedup For Kinetic V2! - Case insensitive AND blazing fast 🤣

Ok guys, my buds and I were bitching about the case sensitivity in the references dialog in the new BPM and Function Apps.

We don’t like it, and some of us want all the references returned with no paging.

So I solved it here:

And then @Olga gave me some light ribbing because mine is slower:

Well, I couldn’t stand it, so I made another one.

I wrote it, then had AI clean it, then I cleaned up the AI :rofl: , and eventually I ended up with this:

Edit: File lol →

ReferencesSpeedupKineticWithCache.bpm (15.3 KB)

This is a pre-processing BPM on Ice.BO.BpMethod.GetAvailableReferencesWithFilter

Reference Cache

This BPM implements a cached version of GetAvailableReferences(referenceGroup) using XXXDef as a backing store.

Cache Behavior

The cache is stored in XXXDef with the following keys:

  • referenceGroup is Assemblies or Externals
Field Value
ProductID EP
TypeCode Custom
Key1 References
Key2 referenceGroup
Key3 not set

Cache metadata is stored as:

Field Purpose
SysCharacter01 Last refresh timestamp (ISO-8601 format)
Content Serialized JSON payload containing the reference list

The cache automatically refreshes when:

  • No cache record exists
  • The cached timestamp cannot be parsed
  • The cache is older than cacheTimeOutHours
  • A force refresh is requested

Special Filter Commands

The filter parameter supports special commands.

Force Refresh

(Can be set in forceRefreshString)

^ 

Forces the cache to refresh before returning results.

Return All Results

#

Returns all cached references without paging.
@josecgomez :rofl:

Return All Matching Results

#Microsoft

Returns all references matching the filter without paging.

Equivalent to:

  • Filter by "Microsoft"
  • Case-Insensitive
  • Disable paging

Normal Filtering

Any other filter value performs a case-insensitive contains search against ReferenceInfo.Name.

Example:

Microsoft

Returns paged results where:

reference.Name.Contains("Microsoft", StringComparison.OrdinalIgnoreCase)

Paging

Paging is applied after filtering.

.Skip((pageNum - 1) * pageSize)
.Take(pageSize)

Paging is skipped when the filter begins with #.

Fallback Behavior

This BPM executes in Pre-Processing.

If the cache path succeeds:

MarkCallCompleted();

is called and the base BO method is skipped.

If any exception occurs:

  • The error is logged
  • The BPM does not call MarkCallCompleted()
  • Processing falls through to the base BO implementation

Code

/*
 * Author: Kevin Lincecum
 *
 * Reference Cache
 *
 * Caches GetAvailableReferences(referenceGroup) results in XXXDef to reduce repeated BO calls and improve response time.
 *
 * Cache Storage:
 *   ProductID      = EP
 *   TypeCode       = Custom
 *   Key1           = References
 *   Key2           = referenceGroup
 *   Key3           = not set
 *   SysCharacter01 = Last refresh timestamp (ISO-8601)
 *   Content        = Serialized JSON payload
 *
 * Cache Refresh Conditions:
 *   - Force refresh requested (forceRefreshString defined below)
 *   - Cache record does not exist
 *   - Cached timestamp is invalid
 *   - Cache age exceeds cacheTimeOutHours
 *
 * Filter Commands:
 *   ^              Force cache refresh
 *   #              Return all records (no paging)
 *   #<text>        Filter by text and return all matches (no paging)
 *   <text>         Filter by text and apply paging
 *
 * Filtering:
 *   Case-insensitive contains match against ReferenceInfo.Name.
 *
 * Paging:
 *   Applied after filtering using pageNum/pageSize.
 *   Disabled when filter begins with (returnAllString defined below).
 *
 * Execution:
 *   Runs in Pre-Processing. On success, MarkCallCompleted() is called
 *   and the base BO method is skipped. On failure, the exception is logged
 *   and processing falls through to the base implementation.
 */

//using Newtonsoft.Json;
//ref: Newtonsoft.Json;


var cacheTimeOutHours  = 24;
var forceRefreshString = "^";
var returnAllString    = "#";

// Special filter values
bool forceRefresh = filter == forceRefreshString;
bool returnAll = !string.IsNullOrWhiteSpace(filter) && filter.StartsWith(returnAllString);

//Put it back lol
if (forceRefresh)
{
    filter = "";
}
else if (returnAll)
{
    filter = filter.Substring(1);
}

try
{
    // JSON payload of the cached reference data.
    var cachedDataJson = "";
    
    // Look for an existing cache entry for this reference group.
    var cachedRow = Db.XXXDef.FirstOrDefault(x =>
        x.Company == CompanyID &&
        x.ProductID == "EP" &&
        x.TypeCode == "Custom" &&
        x.Key1 == "References" &&
        x.Key2 == referenceGroup);
    
    DateTime cachedDate;
    
    
    
    // Refresh the cache when:
    // 1. Forced by the caller
    // 2. No cache row exists
    // 3. Cached timestamp is invalid
    // 4. Cache is older than 4 hours
    if (forceRefresh || cachedRow == null || !DateTime.TryParse(cachedRow.SysCharacter01, out cachedDate) || DateTime.Now - cachedDate > TimeSpan.FromHours(cacheTimeOutHours))
    {
        // Get fresh reference data from the BO.
        CallService<Ice.Contracts.BpMethodSvcContract>(bpm =>
        {
            var refs = bpm.GetAvailableReferences(referenceGroup);
            cachedDataJson = JsonConvert.SerializeObject( refs, Formatting.Indented);
        });
    
        // Save/update the cache.
        CallService<Ice.Contracts.GenXDataSvcContract>(gx =>
        {
            // Load existing cache row or create a new tableset.
            var ts = cachedRow == null
                ? new GenXDataTableset()
                : gx.GetByID(CompanyID, "EP", "Custom", "", "References", referenceGroup, "");
    
            // Create a new cache record if one doesn't exist.
            if (cachedRow == null)
                gx.GetNewXXXDef(ref ts, CompanyID, "EP", "Custom", "", "References", referenceGroup);
    
            var row = ts.XXXDef.First();
    
            // Existing rows need a before-image and RowMod for update processing.
            if (cachedRow != null)
            {
                var bi = new XXXDefRow();
                
                BufferCopy.Copy(row, bi);
                
                ts.XXXDef.Add(bi);
                
                row.RowMod = "U";
            }
    
            // Store refresh timestamp and serialized data.
            row.SysCharacter01 = DateTime.Now.ToString("o");
            row.Content = cachedDataJson;
    
            gx.Update(ref ts);
        });
    }
    else
    {
        // Use the cached JSON payload.
        cachedDataJson = cachedRow.Content;
    }
    
    // Deserialize cached JSON into the reference list.
    var cachedList = JsonConvert.DeserializeObject<List<Ice.Contracts.BO.BpMethod.ReferenceInfo>>(cachedDataJson);
    
    // Apply case-insensitive name filtering when a filter is provided.
    var filteredList = string.IsNullOrWhiteSpace(filter)
        ? cachedList
        : cachedList
            .Where(x => !string.IsNullOrEmpty(x.Name) && x.Name.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0)
            .ToList();
    
    // Return the requested page.
    result = (returnAll ? filteredList : filteredList
      .Skip((pageNum - 1) * pageSize)
      .Take(pageSize))
      .ToList();

    // This is pre-processing. If cache handling succeeds, skip the base method.
    MarkCallCompleted();        
}
catch(Exception ex)
{
    // Cache failed; allow base method to run.
    Ice.Diagnostics.Log.WriteEntry($"Reference cache failed; falling back to base method. {ex}");
}

It’s blazing fast now!

  • It’s a little slow on a cache refresh, but the interval is configurable.
  • If you’re cloud, how often do they update those?.. Ever :rofl:

Older Simpler Version

Click Me Lol

Pre-Processing on Ice.BO.BpMethod.GetAvailableReferencesWithFilter

CallService<Ice.Contracts.BpMethodSvcContract>(bpm =>
{
    var refs = bpm.GetAvailableReferences(referenceGroup);

    result = refs.Where(x => x.Name.Contains(filter, StringComparison.OrdinalIgnoreCase)).ToList();

});

MarkCallCompleted();

What references are needed? Specifically Ice.Contracts.GenXDataSvcContract ?

TIL about the MarkCallCompleted method. Great work Kevin

Oops, I should probably just attach the file.

and just add as using Newtonsoft.Json;
and that assembly as well.

Attached at top now. :party_parrot: