In the ever-evolving world of business applications, speed, flexibility, and efficiency are key. For years, C# plugins have been the go-to solution for extending Dataverse functionality, offering unparalleled power and control to developers. However, with the rise of low-code platforms, a new generation of Dataverse plugins is emerging, designed to empower citizen developers and streamline development.
But how do these low-code plugins measure up against their tried-and-true C# counterparts? In this post, we'll dive deep into a performance comparison between traditional C# plugins and the new low-code approach, uncovering the strengths, limitations, and practical use cases of each.
Setting the expectations
There are a lot of really interesting articles out there describing the internals of Low Code plugins. One key point is they are built on top of old C# plugins, the infrastructure down below it's the same, so it's a reasonable expectation that
Performance(Low Code Plugin) = Performance(C# Plugin) + overhead
Where the overhead
may be generated by:
- Language parsing latency
- Sub-optimal command execution strategies
The second bullet is releted to what Richard Anderson stated in his article:
Performance
While basic operations on the same function are relatively performant, Power Fx expressions that utilise a Dataverse connection to interact with other entities are not. Operations such as LookUp, CountIf, SumIf, etc. utilising a Dataverse connection with a condition inefficiently retrieve records and loop over the results, rather than letting Dataverse query out the ineligible records.Power Fx low-code functions also complete another submission back to Dataverse to update or create data where configured, instead of using the target object to avoid the extra call, this could be concern for anyone on licenses where users are limited to the number of API operations per day.
That said, my intention here is to determine the size of this overhead, with basic, simple scenarios.
📐 Let's set up the benchmark
To perform a comparison i created 3 custom tables:
- ava_test01
- ava_test02
- ava_test03
All of them with primary field called ava_name
, and another custom field called ava_description
. The test harness would be a console application built with BenchmarkDotNet, a really powerful library for this kind of scenarios.
ava_test01
will be used as baseline for our benchmark. I'll run against it basic create operation, no plugins attached.
ava_test02
will have a plain old C# plugin attached doing some stuff.
ava_test03
will have a low code plugin attached doing the same stuff of the C# plugin, but in PowerFX.
I'm gonna make 2 different tests:
- In pre create, set the description of the newly created record
- In post create, *create a contact record *
Each test will then be run in 3 batches:
- First batch will measure 1 single create operation
- Second batch will measure 10 create operations executed sequentially
- Third batch will measure 100 create operations executed sequentially
Let's see how it behaves!
🧪 1st test - Setting the "Description" of the record in Pre Operation
This is the plain old plugin code that will run on ava_test02
:
public class PreOperationPlugin : IPlugin
{
public void Execute(IServiceProvider serviceProvider)
{
var context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
var target = context.InputParameterOrDefault<Entity>("Target");
target["ava_description"] = "Description added by PreOperationPlugin";
}
}
No overdesigned structures, no BasePlugin, just the bare minimum.
While the Automated Plugin of ava_test03
is:
Quite simple. Down below the benchmark results.
Benchmark results
BenchmarkDotNet v0.14.0, Windows 11 (10.0.22631.4169/23H2/2023Update/SunValley3)
12th Gen Intel Core i7-1270P, 1 CPU, 16 logical and 12 physical cores
.NET SDK 8.0.400
[Host] : .NET 8.0.8 (8.0.824.36612), X64 RyuJIT AVX2 [AttachedDebugger]
.NET 8.0 : .NET 8.0.8 (8.0.824.36612), X64 RyuJIT AVX2
Job=.NET 8.0 Runtime=.NET 8.0
Method | RecordCount | Mean | Error | StdDev | Median | Max | Min | Ratio | RatioSD |
---|---|---|---|---|---|---|---|---|---|
NoPlugin | 1 | 378.2 ms | 8.08 ms | 23.07 ms | 380.0 ms | 434.1 ms | 330.6 ms | 1.00 | 0.09 |
OldPlugin | 1 | 372.5 ms | 8.61 ms | 23.99 ms | 372.6 ms | 432.5 ms | 320.0 ms | 0.99 | 0.09 |
PowerFXPlugin | 1 | 380.0 ms | 8.03 ms | 22.12 ms | 377.3 ms | 452.2 ms | 335.7 ms | 1.01 | 0.08 |
NoPlugin | 10 | 1,095.2 ms | 26.17 ms | 71.65 ms | 1,087.1 ms | 1,340.7 ms | 975.7 ms | 1.00 | 0.09 |
OldPlugin | 10 | 1,250.6 ms | 24.89 ms | 65.56 ms | 1,236.9 ms | 1,431.0 ms | 1,128.3 ms | 1.15 | 0.09 |
PowerFXPlugin | 10 | 1,449.3 ms | 37.27 ms | 106.94 ms | 1,414.4 ms | 1,757.8 ms | 1,287.9 ms | 1.33 | 0.13 |
NoPlugin | 100 | 8,367.7 ms | 167.33 ms | 330.30 ms | 8,340.6 ms | 9,099.4 ms | 7,641.4 ms | 1.00 | 0.06 |
OldPlugin | 100 | 10,054.3 ms | 199.27 ms | 581.27 ms | 9,958.3 ms | 11,442.1 ms | 8,854.3 ms | 1.20 | 0.08 |
PowerFXPlugin | 100 | 11,888.0 ms | 221.80 ms | 477.45 ms | 11,958.6 ms | 12,926.1 ms | 10,773.7 ms | 1.42 | 0.08 |
The Ratio column in the table above shows the overhead of both plugins compared to the baseline execution without plugins. The presence of plugins accounts for a +20% (for the C# plugin) or +42% (for the PowerFX plugin) of the average execution time.
The computed overhead
in the formula above can be calculated as 1.33/1.15 ≃ 1.42/1.20 ≃ 20%.
Let's try a slightly more complex scenario.
🧪 2nd test - Creating a contact in Post Operation
This is the plain old plugin code that will run on ava_test02
:
public class PostOperationPlugin : IPlugin
{
public void Execute(IServiceProvider serviceProvider)
{
var context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
var serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
var service = serviceFactory.CreateOrganizationService(context.UserId);
var clone = new Entity("contact");
clone["firstname"] = "Luca";
clone["lastname"] = "Gregori";
service.Create(clone);
}
}
While the matching Automated Plugin looks like this:
Please note: To run this test properly I had to remove the previous plugin steps... but while you can simply disable a classic Plugin step, you cannot simply disable an Automated Plugin... I had to delete it physically 😒.
Who works with Power Platform in enterprise scenarios knows that physical deletion of solution components when you have ALM in place, and lots of customization in your solution, can be really painful...
The capability is still in preview, so I hope that in the final release there will be a capability to simply deactivate the automated plugin without having to delete it.
Benchmark results
BenchmarkDotNet v0.14.0, Windows 11 (10.0.22631.4169/23H2/2023Update/SunValley3)
12th Gen Intel Core i7-1270P, 1 CPU, 16 logical and 12 physical cores
.NET SDK 8.0.400
[Host] : .NET 8.0.8 (8.0.824.36612), X64 RyuJIT AVX2 [AttachedDebugger]
.NET 8.0 : .NET 8.0.8 (8.0.824.36612), X64 RyuJIT AVX2
Job=.NET 8.0 Runtime=.NET 8.0
Method | RecordCount | Mean | Error | StdDev | Median | Max | Min | Ratio | RatioSD |
---|---|---|---|---|---|---|---|---|---|
NoPlugin | 1 | 75.25 ms | 1.457 ms | 2.514 ms | 74.58 ms | 81.34 ms | 71.08 ms | 1.00 | 0.05 |
OldPlugin | 1 | 219.48 ms | 7.239 ms | 19.817 ms | 216.34 ms | 279.94 ms | 181.40 ms | 2.92 | 0.28 |
PowerFXPlugin | 1 | 272.77 ms | 11.900 ms | 34.524 ms | 268.49 ms | 365.67 ms | 218.04 ms | 3.63 | 0.47 |
NoPlugin | 10 | 819.59 ms | 29.180 ms | 83.723 ms | 788.89 ms | 1,053.18 ms | 729.54 ms | 1.01 | 0.14 |
OldPlugin | 10 | 2,189.04 ms | 58.002 ms | 169.194 ms | 2,151.62 ms | 2,639.01 ms | 1,881.12 ms | 2.70 | 0.32 |
PowerFXPlugin | 10 | 2,562.66 ms | 82.962 ms | 239.364 ms | 2,531.42 ms | 3,196.66 ms | 2,193.59 ms | 3.16 | 0.41 |
NoPlugin | 100 | 7,720.34 ms | 152.514 ms | 314.969 ms | 7,729.09 ms | 8,519.10 ms | 7,110.76 ms | 1.00 | 0.06 |
OldPlugin | 100 | 21,934.29 ms | 437.219 ms | 1,136.390 ms | 21,786.87 ms | 24,703.31 ms | 19,979.68 ms | 2.85 | 0.19 |
PowerFXPlugin | 100 | 29,148.69 ms | 1,102.034 ms | 3,108.306 ms | 28,196.23 ms | 37,090.00 ms | 23,872.81 ms | 3.78 | 0.43 |
First of all we should notice that the execution with 1 single record is quite meaningless, because it can be affected by transient external factors we don't have control on (it's quite stunning that the comparison between the first line of the two tests is really different, even if the operation performed in the "No Plugin" scenario is the same).
Let's compare again the Ratios in the different scenarios.
The computed overhead
in the formula above can be calculated as 3.16/2.70 ≃ 2.85/3.78 ≃ 20% - 25%.
🎯 Conclusions
As expected, ease of use comes with a price: the PowerFX version is slower than the old C#, and the slowness can be accounted for a ⁓20% of the overall execution. A factor that may not be relevant in many scenarios, but must be carefully took into account while designing enterprise scale solutions.
One key aspect is that I've not included in the benchmark scenarios where the plugin should perform queries against the Dataverse. My guess is that, considering the limitations highlighted in Richard's article, the overhead in those cases may be higher.
That being said, performance isn't everything. Despite the lag, the new Automated Plugins powered by PowerFX are truly a game changer. What sets them apart is their ability to leverage Power Platform connectors, unlocking an entire ecosystem of data sources and services that traditional C# plugins hardly tap into. This feature alone is a killer advantage, allowing both citizen developers and seasoned pros to create powerful, integrated solutions with ease.
For citizen developers, it opens the door to automation without needing deep coding expertise. For pro developers, it accelerates prototyping and enhances integration capabilities, empowering them to focus on the bigger picture. In short, while you might trade off some performance, the flexibility of the PowerFX-powered approach can supercharge your solution-building process, making it a powerful tool in any developer's arsenal.
What do you think about it? Drop a comment below if you want to learn more about this topic!
Top comments (1)
Thanks for this investigation! Not surprisingly, there are some overhead, but putting a number to it is great!