Functional Programming Techniques in Security Dashboard
A guide to the functional and object-oriented patterns in this codebase.
Introduction
This codebase uses a hybrid approach, blending functional programming (FP) techniques within a larger Object-Oriented (OOP) structure. This strategy is often called functional in the small and object-oriented in the large. We use CSharpFunctionalExtensions for robust error handling, records for immutability, and LINQ for data transformation. The result is clean, predictable, and testable code.
Result Pattern
The Result Pattern makes error handling explicit. Methods return a Result<T>, which represents either a success (Ok) or a failure (Fail), eliminating the need for try-catch blocks in business logic.
Composing Result Operations
This example demonstrates composing multiple operations.
.Ensure(predicate, errorMessage): Validates the Result's value. If the predicate is false, it returns a failure.
.Bind(Func<T, Result<K>>): Chains a function that returns a Result. If the input Result is a failure, the function is not called, and the failure is propagated.
.Map(Func<T, K>): Transforms the value inside a successful Result without altering its success/failure state.
From src/CheckmarxTool.Web/Services/Authentication/ServerCredentialProvider.cs:
private static Result<ServerCredential> ReadCredentials(
ServerCredentialOptions opts)
{
return Result.Try(
() => File.ReadAllText(
opts.UsernamePath!).Trim(),
ex => $"Failed to read username file: " +
$"{ex.Message}")
.Ensure(
username => !string.IsNullOrWhiteSpace(username),
"Stored username is empty.")
.Bind(username =>
Result.Try(
() => File.ReadAllText(
opts.PasswordPath!).Trim(),
ex => $"Failed to read password file: " +
$"{ex.Message}")
.Ensure(
encrypted =>
!string.IsNullOrWhiteSpace(encrypted),
"Stored password is empty.")
.Bind(DecryptPassword)
.Map(password =>
new ServerCredential(
username, password)));
}
Async Operations with Result
The pattern works seamlessly with async operations.
From src/CheckmarxTool.Web/Services/Dependabot/GitHubDependabotClient.cs:
private async Task<Result<ImmutableArray<DependabotAlert>>>
FetchAlertsInternal(string owner, string repo)
{
var requestUri =
$"repos/{owner}/{repo}/dependabot/alerts" +
$"?state=open&per_page=100&sort=updated";
return await Result.Success(requestUri)
.Bind(uri => FetchResponse(uri, owner, repo))
.Bind(async response =>
{
using var httpResponse = response;
var (_, isFailure, payload, error) =
await ReadPayload(
httpResponse, owner, repo);
if (isFailure)
return Result.Failure<
ImmutableArray<DependabotAlert>>(error);
return httpResponse.IsSuccessStatusCode
? ParseAlerts(payload, owner, repo)
: Result.Failure<
ImmutableArray<DependabotAlert>>(
BuildErrorMessage(
httpResponse.StatusCode,
owner, repo, payload));
});
}
Fluent API with Result Extensions
A Fluent API creates elegant pipelines for error handling by chaining methods. Failures are propagated automatically.
Procedural (Nested) Approach:
Result<Data> GetData()
{
try
{
var resource = AcquireResource();
try
{
var data = FetchData(resource);
return Result.Success(data);
}
catch (Exception ex)
{
return Result.Failure<Data>(
ex.Message);
}
finally { resource.Dispose(); }
}
catch (Exception ex)
{
return Result.Failure<Data>(ex.Message);
}
}
Fluent (Pipeline) Approach:
async Task<Result<Data>> GetData()
{
return await ResultHelpers.Try(
() => AcquireResource())
.Try(async resource =>
await FetchData(resource))
.Finally(resource => resource.Dispose());
}
Immutability with Records
Immutability means an object's state cannot change after creation. This enhances thread safety and predictability. Instead of modifying objects, you create new ones with the desired changes.
Record Types with init-only Properties
Records are for immutable data. Properties are init-only, so they can only be set during object initialization.
// From: src/CheckmarxTool.Core/Scanning/ScanDetails.cs
public record ScanDetails
{
public long Id { get; init; }
public long ProjectId { get; init; }
public string Status { get; init; } = string.Empty;
}
// scan.Id = 456; // ❌ Compiler error: Init-only property
Creating Copies with with Expressions
The with keyword creates a new record by copying an existing one and applying changes, leaving the original unchanged.
// From: src/CheckmarxTool.Core/ScanResults/ProjectScanResults.cs
public record ProjectScanResults
{
public long ProjectId { get; init; }
public ImmutableArray<ScanResult> Findings { get; init; } = [];
}
// From: src/CheckmarxTool.Core/Scanning/ScanResultsRetriever.cs
return projectResults with { Findings = [..findings] };
LINQ provides a declarative, functional syntax for working with collections.
Common LINQ Operations
Select (map), Where (filter), and OrderBy (sort) are fundamental for data transformation pipelines.
// From: src/CheckmarxTool.Core/Shared/Extensions.cs
return scans
.Where(scan => scan.IsPublic) // Filter
.OrderByDescending(scan => scan.ScanID) // Sort
.Select(scan => scan.ScanID) // Map
.First();
Chained LINQ vs. Imperative Loop
Chaining LINQ operations creates readable pipelines that are often more concise than imperative loops.
LINQ Pipeline:
// From: src/CheckmarxTool.Core/Scanning/CxClientAdapter.cs
var scanInfos = recentScans
.Where(IsScanInProgress)
.Select(scan => CreateScanInfo(scan, projectId))
.OrderByDescending(s => s.QueuedOn ?? DateTime.MinValue)
.ToImmutableArray();
Imperative Equivalent:
var scanInfos = new List<ScanInfo>();
foreach (var scan in recentScans)
{
if (IsScanInProgress(scan))
{
scanInfos.Add(
CreateScanInfo(scan, projectId));
}
}
scanInfos.Sort((a, b) =>
(b.QueuedOn ?? DateTime.MinValue)
.CompareTo(
a.QueuedOn ?? DateTime.MinValue));
return scanInfos.ToImmutableArray();
Handling Exceptions at Boundaries
try-catch is reserved for the boundaries of the system, where it interacts with external code that can throw exceptions (e.g., file system, network APIs).
Our ResultHelpers.Try method is the boundary that converts exceptions into Result failures, keeping business logic clean.
// From: src/CheckmarxTool.Core/Extensions/ResultHelpers.cs
public static Result<T> Try<T>(Func<T> operation)
{
try
{
return Result.Success(operation());
}
catch (Exception ex)
{
return Result.Failure<T>(ex.Message);
}
}
Object-Oriented Structure
This codebase combines OOP and FP. Classes provide structure, while methods use functional techniques internally.
Encapsulation: Objects bundle data (private fields) and behavior (public methods).
Abstraction: Interfaces like ICxClientAdapter decouple business logic from concrete implementations.
Dependency Injection: Injecting dependencies (IConfiguration, ICxClientAdapter) makes classes flexible and testable.
The ScanResultsRetriever class encapsulates state and dependencies. It uses the injected _cxClient to fetch data from the Checkmarx API, demonstrating collaboration between objects.
// File: src/CheckmarxTool.Core/Scanning/
// ScanResultsRetriever.cs
public class ScanResultsRetriever
{
private readonly ICxClientAdapter _cxClient;
// ... other private fields
public ScanResultsRetriever(
IConfiguration configuration,
ICxClientAdapter cxClient)
{
_cxClient = cxClient;
// ...
}
public Result<CheckmarxScanResults>
RetrieveCheckmarxScanResults()
{
return Try(() =>
{
// Using the injected client to get data
var projectScanResults =
_checkmarxProjects
.Select(project =>
BuildProjectScanResults(
project,
_cxClient.GetResultStateList()));
return new CheckmarxScanResults
{
ProjectScanResults = [..projectScanResults]
};
});
}
private ProjectScanResults BuildProjectScanResults(
CheckmarxProject checkmarxProject,
ImmutableDictionary<long, string> availableScanStates)
{
// The client is used to get more data
var (results, scanId) = GetScanResults(
checkmarxProject,
availableScanStates);
var scanDate = _cxClient.GetScanFinishedDate(scanId);
var findings = results.Select(scanResult =>
BuildScanFinding(
scanResult,
checkmarxProject.Id,
scanId,
availableScanStates,
_cxClient.GetQueriesForScan(scanId))
).ToList();
// ... more logic
return new ProjectScanResults();
}
}
Best Practices Summary
Scenario | Recommended Approach | Example |
|---|
Domain operation that can fail | Return Result<T> | Result<Order> ProcessOrder()
|
Calling external, unsafe API | Wrap with ResultHelpers.Try() | Try(() => _client.GetData())
|
Creating a data model | Use record with init properties | public record User { string Name { get; init; } }
|
Storing injected dependencies | Use readonly fields | private readonly IRepository _repo;
|
Transforming a collection | Use LINQ Select | projects.Select(p => p.ToDto())
|
Filtering a collection | Use LINQ Where | items.Where(i => i.IsActive)
|
Chaining failable operations | Use Result extensions (Bind, Map) | result.Bind(...).Map(...)
|
Further Reading
End of document.
29 November 2025