Code Quality Design Help

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 for Data Transformation

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