<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Caleb Dickson]]></title><description><![CDATA[Practical writing on enterprise platform engineering, distributed systems, reliability, Dataverse, Azure, and software engineering in the Microsoft ecosystem.]]></description><link>https://caleb-dickson.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6a82592d228f60bfec425cc9/71633cf7-09f0-4f62-bb98-c0733b80d002.png</url><title>Caleb Dickson</title><link>https://caleb-dickson.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Mon, 31 Aug 2026 06:05:13 GMT</lastBuildDate><atom:link href="https://caleb-dickson.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Accumulating Validation Failures in .NET]]></title><description><![CDATA[Validation code often starts with a very simple control flow model:
if (request.SourceId == Guid.Empty)
{
    throw new ArgumentException("Source ID is required.");
}

if (request.BatchSize <= 0)
{
  ]]></description><link>https://caleb-dickson.hashnode.dev/accumulating-validation-failures-in-dotnet</link><guid isPermaLink="true">https://caleb-dickson.hashnode.dev/accumulating-validation-failures-in-dotnet</guid><category><![CDATA[.NET]]></category><category><![CDATA[Validation]]></category><category><![CDATA[Exception Handling]]></category><dc:creator><![CDATA[Caleb Dickson]]></dc:creator><pubDate>Fri, 21 Aug 2026 18:01:38 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a82592d228f60bfec425cc9/ca9977d3-d218-4cb7-80ef-b025d098daeb.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Validation code often starts with a very simple control flow model:</p>
<pre><code class="language-csharp">if (request.SourceId == Guid.Empty)
{
    throw new ArgumentException("Source ID is required.");
}

if (request.BatchSize &lt;= 0)
{
    throw new ArgumentException("Batch size must be greater than zero.");
}

if (request.EffectiveDate == null)
{
    throw new ArgumentException("Effective date is required.");
}
</code></pre>
<p>Each rule is reasonable, and the operation should not continue when any of these conditions is invalid. The problem is
that the first failure prevents us from discovering the others. If all three values are invalid, the caller learns about
<code>SourceId</code> first, corrects it, executes the operation again, then discovers <code>BatchSize</code>. A third execution finally
exposes the missing effective date. The application was capable of identifying all three problems during the first
validation phase, but its control flow deliberately discarded that opportunity.</p>
<p>I started handling validation differently very early in my development career, shortly before entering my first
professional software engineering role. When multiple validation rules are independent and can be evaluated safely, I
prefer to collect the failures before terminating or rejecting the operation. I think of this as accumulating
validation. The idea is closely related to Martin Fowler's Notification pattern, in which validation builds a
description of everything it knows to be invalid rather than allowing the first failed rule to interrupt execution
immediately.</p>
<p>The applications I have in mind are not generic form processing applications. Most of my work is centered on Dynamics
365 Customer Engagement and Azure integrations, particularly Dataverse plugins and Custom APIs, along with HTTP,
Service Bus, and Timer triggered Azure Functions. Those execution models have very different failure semantics. A
synchronous Dataverse plugin may need to cancel a platform operation immediately, an HTTP function may return a
structured validation response, and a Service Bus consumer may need to distinguish a permanently invalid message from a
transient execution failure. The way validation failures leave the application is therefore platform specific, but the
principle of collecting useful validation information does not have to be.</p>
<p>The code examples in this article also intentionally use fairly conservative C# syntax. Many enterprise Dataverse
codebases still contain .NET Framework projects using older language versions, while newer integration workloads may be
targeting current .NET releases. The pattern itself does not depend on modern language features.</p>
<h2>Accumulate failures, not exceptions</h2>
<p>One natural implementation is to collect exceptions and eventually throw an <code>AggregateException</code>:</p>
<pre><code class="language-csharp">var exceptions = new List&lt;Exception&gt;();

if (request.SourceId == Guid.Empty)
{
    exceptions.Add(
        new ArgumentException("Source ID is required."));
}

if (request.BatchSize &lt;= 0)
{
    exceptions.Add(
        new ArgumentException("Batch size must be greater than zero."));
}

if (request.EffectiveDate == null)
{
    exceptions.Add(
        new ArgumentException("Effective date is required."));
}

if (exceptions.Count &gt; 0)
{
    throw new AggregateException(exceptions);
}
</code></pre>
<p>This solves the immediate diagnostic problem because the caller can now see every validation issue identified during the
operation, but I do not think <code>AggregateException</code> is the right abstraction. An exception normally communicates that
execution failed in some way. A validation rule identifying an invalid value is different: the validator successfully
performed its work and produced a result. <code>AggregateException</code> also already carries a well established meaning within
.NET, particularly around multiple failures arising from parallel or asynchronous execution, so using it as a general
validation container blurs two concepts that are better kept separate.</p>
<p>A validation failure is more naturally modeled as data. It does not need to be sophisticated:</p>
<pre><code class="language-csharp">public sealed class ValidationFailure
{
    public ValidationFailure(string code, string message)
    {
        Code = code;
        Message = message;
    }

    public string Code { get; private set; }

    public string Message { get; private set; }
}
</code></pre>
<p>Validation can then collect those failures directly:</p>
<pre><code class="language-csharp">public static IReadOnlyCollection&lt;ValidationFailure&gt; Validate(
    ProcessingRequest request)
{
    var failures = new List&lt;ValidationFailure&gt;();

    if (request.SourceId == Guid.Empty)
    {
        failures.Add(
            new ValidationFailure(
                "SourceId.Required",
                "Source ID is required."));
    }

    if (request.BatchSize &lt;= 0)
    {
        failures.Add(
            new ValidationFailure(
                "BatchSize.Invalid",
                "Batch size must be greater than zero."));
    }

    if (request.EffectiveDate == null)
    {
        failures.Add(
            new ValidationFailure(
                "EffectiveDate.Required",
                "Effective date is required."));
    }

    return failures;
}
</code></pre>
<p>At that point, validation has one responsibility: describe why the operation is invalid. What happens because the
operation is invalid belongs to the execution boundary. This separation is particularly useful in platform applications
because the same conceptual validation result may need to produce very different behavior depending on where the code is
running. A plugin may convert the failures into an exception, an HTTP endpoint may serialize them into a response, and
a background integration may record them as operational diagnostics. The validator itself does not need to know which of
those outcomes will occur.</p>
<h2>Accumulating validation is still compatible with fail fast execution</h2>
<p>Dataverse plugins illustrate an important distinction because most synchronous plugin validation exists specifically
to prevent invalid processing from occurring at all. We generally want to detect invalid state as early as practical and
stop before business processing, external calls, or other consequential behavior occurs. That is fail fast execution,
but fail fast execution does not require fail on first rule validation.</p>
<p>Consider a synchronous plugin that requires several independent pieces of information:</p>
<pre><code class="language-csharp">var failures = new List&lt;ValidationFailure&gt;();

if (target.GetAttributeValue&lt;string&gt;("new_externalid") == null)
{
    failures.Add(
        new ValidationFailure(
            "ExternalId.Required",
            "External ID is required."));
}

if (target.GetAttributeValue&lt;EntityReference&gt;("new_customerid") == null)
{
    failures.Add(
        new ValidationFailure(
            "Customer.Required",
            "Customer is required."));
}

var amount = target.GetAttributeValue&lt;Money&gt;("new_amount");

if (amount == null || amount.Value &lt;= 0)
{
    failures.Add(
        new ValidationFailure(
            "Amount.Invalid",
            "Amount must be greater than zero."));
}

if (failures.Count &gt; 0)
{
    throw new InvalidPluginExecutionException(
        FormatValidationFailures(failures));
}
</code></pre>
<p>The plugin still fails before its business behavior begins. From the Dataverse execution pipeline's perspective, the
operation is being rejected just as early as it would have been if the first validation rule had thrown immediately. The
only difference is that the validation phase is allowed to establish a more complete picture of the invalid request
before terminating execution.</p>
<p>This distinction is useful because fail fast and accumulating validation answer different questions. Fail fast describes
when processing stops; accumulating validation describes how much useful information we gather before reaching that
decision. Treating them as competing strategies makes validation more restrictive than it needs to be. In many platform
scenarios, the better design is to accumulate independent failures and then fail fast once the validation phase is
complete.</p>
<h2>Validation still has dependencies</h2>
<p>Accumulating validation should not be interpreted as executing every validation rule regardless of context. Some rules
depend on others, and once a prerequisite has failed there may be no useful or safe way to evaluate the rest of that
branch.</p>
<p>Suppose a plugin requires a customer reference and then retrieves that customer to evaluate additional business rules:</p>
<pre><code class="language-csharp">if (customerReference == null)
{
    failures.Add(
        new ValidationFailure(
            "Customer.Required",
            "Customer is required."));
}
else
{
    ValidateCustomer(customerReference, failures);
}
</code></pre>
<p>Once the reference is missing, the dependent customer validation branch cannot proceed meaningfully. Independent rules
can still continue:</p>
<pre><code class="language-csharp">ValidateAmount(target, failures);
ValidateEffectiveDate(target, failures);
</code></pre>
<p>The objective is therefore not to execute the largest possible number of validation rules. It is to capture the largest
useful set of validation failures that can be determined safely and efficiently. That distinction becomes increasingly
important in Dataverse and integration workloads because validation itself may involve database reads, Dataverse
retrieves, configuration access, metadata inspection, or calls to external dependencies. A complete validation result is
useful; a validator that performs unnecessary I/O after its prerequisites have already failed is not.</p>
<p>This also means that validation has a shape. Some rules are independent peers, while others form dependency chains. An
effective accumulating validator needs to preserve those relationships rather than treating every rule as an isolated
boolean condition. In practice, that usually means continuing across independent branches while short circuiting only
the branch whose prerequisites are no longer satisfied.</p>
<h2>The application boundary determines what happens next</h2>
<p>Once validation failures are represented independently of exception handling, each application model can translate the
result into the failure semantics appropriate for its host. A conventional synchronous Dataverse plugin will often
convert the result into an <code>InvalidPluginExecutionException</code>:</p>
<pre><code class="language-csharp">var failures = Validate(context);

if (failures.Count &gt; 0)
{
    throw new InvalidPluginExecutionException(
        FormatValidationFailures(failures));
}
</code></pre>
<p>There is usually no validation response object to return from an ordinary event pipeline plugin because the platform
operation itself needs to be rejected. A Custom API gives us more flexibility because it defines an explicit request and
response contract. Some APIs should still fail when validation fails, while others may have legitimate reasons to expose
structured validation information through output parameters. The important point is that the validation mechanism does
not need to decide which model applies; that belongs to the API contract.</p>
<p>HTTP triggered Azure Functions make the separation even clearer. Invalid input is frequently an expected request outcome
rather than an exceptional execution failure. The function can validate the request, detect several independent
problems, and return those failures together in the response. The validator performed successfully; the HTTP request
simply failed to satisfy the operation's contract. There is little value in throwing an exception merely to transfer
control from validation code to the HTTP response layer when invalid input is already part of the expected API behavior.</p>
<p>Service Bus triggered functions introduce a different concern because failure behavior affects message processing. If a
message is permanently invalid, repeatedly throwing may accomplish nothing except consuming delivery attempts before the
message eventually reaches a dead letter or other terminal failure path. Accumulating validation is particularly useful
here because the first processing attempt can establish the full known problem with the message. A message with four
independent structural problems should not require four deliveries to discover them, especially when none of those
problems can be corrected by retrying the same payload.</p>
<p>This distinction also reinforces an important integration principle: deterministic validation failures and transient
execution failures should not automatically be treated the same way. A timeout while reaching a dependency may justify
another attempt because the environmental condition can change. A required identifier missing from an immutable message
will still be missing on the next delivery. Validation can help establish that distinction before retry behavior turns a
permanent input problem into unnecessary operational noise.</p>
<p>Timer triggered functions have no interactive caller at all, so accumulated validation is primarily operational. If
several required configuration values are missing or inconsistent, reporting them together produces a much more useful
diagnostic event than discovering one configuration problem per scheduled execution. The execution models differ
substantially, but the validation principle remains consistent: first determine what is invalid, then translate that
result into the semantics of the host.</p>
<h2>Complete validation improves more than error messages</h2>
<p>The most obvious benefit of accumulated validation is that a developer or user can correct several problems at once, but
for enterprise integration workloads I think the operational benefits are more significant. Consider a scheduled
integration that cannot initialize because several configuration values are invalid. A first error only implementation
may fail on one configuration value, wait for someone to diagnose and correct it, then fail on a second value during the
next run. A third execution may reveal another problem. Nothing about those later failures required the earlier values
to be corrected; they simply were not evaluated.</p>
<p>An accumulating validator can describe the configuration state once:</p>
<pre><code class="language-text">Configuration validation failed:

DataverseConnection.Required
A Dataverse connection must be configured.

BatchSize.OutOfRange
Batch size must be between 1 and 5000.

StorageContainer.Required
A storage container must be configured.

RetryCount.OutOfRange
Retry count cannot be negative.
</code></pre>
<p>This is more than a better exception message because it produces a more complete diagnostic artifact. Structured
validation failures can be logged individually, attached to telemetry, persisted with integration processing results,
returned through APIs, included in dead letter diagnostics, and asserted directly in automated tests. Stable validation
codes also allow application behavior and observability tooling to reason about failures without depending on
human readable message text.</p>
<p>As an application grows, this begins to change the role of validation. It stops looking like a scattered collection of
defensive <code>if</code> statements and starts behaving more like a small diagnostic subsystem: a part of the application
responsible for describing why execution cannot proceed safely, in a form that can be consumed consistently by both
application logic and operational tooling. This is particularly valuable in integration systems, where the immediate
caller is often not a person and the validation output may instead be consumed by telemetry, message infrastructure,
support tooling, or another automated process.</p>
<h2>Some failures should still stop validation immediately</h2>
<p>There are limits to the pattern. If validation itself encounters an unexpected infrastructure failure, continuing may
not provide useful information. If retrieving required Dataverse state fails because the service is unavailable,
producing additional validation failures from rules that depended on that state would be misleading. The same applies
when an invariant has been violated so fundamentally that the application can no longer reason safely about the input.</p>
<p>Those conditions are not ordinary validation failures; they are execution failures. Accumulating validation should
improve the diagnosis of expected invalid states, not conceal actual faults or encourage an application to continue
after it can no longer reason reliably. The practical distinction is whether the validator still understands the state
it is examining. If it does, independent validation can continue. If it does not, execution should generally stop.</p>
<p>That boundary is important because it keeps the pattern from turning into a blanket rule that more errors are always
better. More diagnostic information is useful only when the information is trustworthy. Once the application loses the
prerequisites needed to evaluate a rule correctly, continuing can make the resulting validation report less accurate
rather than more complete.</p>
<p>There is also a cost dimension to consider. A validator should not make three expensive network calls simply because it
can potentially discover three more errors when a local prerequisite has already established that the operation cannot
proceed. Accumulating validation is primarily about avoiding unnecessary information loss, not maximizing the number of
rules executed. The same engineering judgment that governs ordinary control flow still applies.</p>
<h2>Validation as an explicit phase</h2>
<p>The implementation does not need to be elaborate. It may be a dedicated validation framework, a reusable result type, or
simply a collection of strongly typed failures. The more important change is conceptual: instead of treating validation
as a scattered series of opportunities to throw, treat it as an explicit phase of execution.</p>
<p>During that phase, evaluate the independent rules that can be evaluated meaningfully, preserve the failures that are
discovered, respect dependencies between rules, and avoid unnecessary I/O. Once validation is complete, allow the
application boundary to decide what the result means. In a synchronous Dataverse plugin, that may mean throwing one
<code>InvalidPluginExecutionException</code>. In a Custom API, it may mean either rejecting execution or populating a defined
response. In an HTTP triggered Azure Function, it may mean returning a structured client error. In a Service Bus
consumer, it may contribute to a dead letter decision. In a Timer triggered integration, it may become an operational
diagnostic before processing is allowed to begin.</p>
<p>Those are platform concerns. The validation concern is simpler: when the application can identify multiple independent
reasons that an operation is invalid, there is usually little value in deliberately reporting only the first one.</p>
]]></content:encoded></item></channel></rss>