ArthaMarble 0.1.1

dotnet add package ArthaMarble --version 0.1.1
                    
NuGet\Install-Package ArthaMarble -Version 0.1.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="ArthaMarble" Version="0.1.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ArthaMarble" Version="0.1.1" />
                    
Directory.Packages.props
<PackageReference Include="ArthaMarble" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add ArthaMarble --version 0.1.1
                    
#r "nuget: ArthaMarble, 0.1.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package ArthaMarble@0.1.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=ArthaMarble&version=0.1.1
                    
Install as a Cake Addin
#tool nuget:?package=ArthaMarble&version=0.1.1
                    
Install as a Cake Tool

AmlCompliance

A provider-agnostic AML / compliance library for .NET 8. It exposes two operations — decision and ingest — over a pluggable provider interface so you can switch backends without touching call sites.

The first provider is Marble (Checkmarble). Additional providers can be added later and selected at runtime via IAmlProviderFactory.

Projects

Project Output DLL Purpose
src/AmlCompliance.Abstractions AmlCompliance.Abstractions.dll Provider interface, models, DI factory/registry. No provider code.
src/AmlCompliance.Marble AmlCompliance.Marble.dll Marble implementation + typed models (MarbleTransaction, MarbleWallet, MarbleCustomer) + AddMarble(...) DI.
tests/AmlCompliance.Tests — xUnit tests (mocked HTTP, no live API).

Multi-provider by design: the abstractions live in their own DLL so each future provider ships as a separate assembly and your app references only what it uses.

Build

dotnet build -c Release
# DLLs land in src/<project>/bin/Release/net8.0/

The operations

All work for every object type in your Marble data model (transactions, wallet, customer, …) — pass the object type (table name) plus a payload.

Operation Method Marble endpoint
Decision (one scenario) DecideAsync POST /decisions
Decision (all scenarios) DecideAllAsync POST /decisions/all
Ingest (single) IngestAsync POST /ingest/{objectType}
Ingest (many, ≤100) IngestBatchAsync POST /ingest/{objectType}/batch
Update / upsert (single) UpdateAsync PATCH /ingest/{objectType}
Update / upsert (many, ≤100) UpdateBatchAsync PATCH /ingest/{objectType}/batch

Decision results expose which rules triggered and any remarks: decision.TriggeredRules (rules whose Outcome == hit, each with Name + ScoreModifier), decision.ErroredRules (rule evaluation failures with Error), plus Rules, Screenings, ReviewStatus, and Raw.

Setup (ASP.NET / generic host DI)

using AmlCompliance;
using AmlCompliance.DependencyInjection;
using AmlCompliance.Marble;

builder.Services.AddAmlCompliance()
    .AddMarble(o =>
    {
        o.ApiKey  = builder.Configuration["Marble:ApiKey"]!; // sent as X-API-KEY
        o.BaseUrl = "https://api.checkmarble.com";           // or your self-hosted URL
    }, isDefault: true);

// Optional: a second instance (different tenant/region)
//  .AddMarble(o => { o.ApiKey = "..."; o.BaseUrl = "https://marble.eu"; }, name: "marble-eu");

Resolve a provider where you need it:

public sealed class ComplianceService(IAmlProviderFactory providers)
{
    // default provider (first registered, or the one flagged isDefault: true)
    private readonly IAmlProvider _aml = providers.GetDefault();
    // or pick by name at runtime: providers.Get("marble-eu");
}

Decision

using AmlCompliance.Decisions;
using AmlCompliance.Marble.Models;

var txn = new MarbleTransaction
{
    ObjectId  = transaction.Id,            // -> object_id (required)
    UpdatedAt = DateTimeOffset.UtcNow,     // -> updated_at (required)
    Amount    = 1500.50m,
    Asset     = "USD",
    Direction = "outgoing",
    RiskScore = 42m,
};

// Run a single, specific scenario (POST /decisions). One-liner overload:
AmlDecision decision = await _aml.DecideAsync(scenarioId, txn, ct);
// (equivalent: _aml.DecideAsync(DecisionRequest.For(scenarioId, txn), ct))

if (decision.Outcome is DecisionOutcome.Decline or DecisionOutcome.BlockAndReview)
    RejectPayment(decision.Score, decision.Rules);

// Or run every eligible scenario for the object type (0..N decisions + counts):
DecisionBatchResult batch =
    await _aml.DecideAllAsync(DecideAllRequest.For(MarbleTransaction.ObjectType, txn), ct);

foreach (var d in batch.Decisions) { /* fired scenarios */ }
// batch.Metadata?.EligibleScenarios  -> scenarios evaluated for this object type
// batch.Metadata?.Skipped            -> evaluated but trigger condition didn't match
// batch.Metadata?.Total              -> decisions created

AmlDecision exposes normalized fields (Outcome, Score, Scenario, Rules, Screenings, ReviewStatus) plus Raw (the verbatim provider JSON) so you can read provider-specific extras without a library upgrade.

Ingest

using AmlCompliance.Ingestion;

// Single object — for a future compliance run:
IngestResult r = await _aml.IngestAsync(
    IngestRequest.For(MarbleTransaction.ObjectType, txn,
        new IngestOptions { Monitor = true }), ct);
// r.Ingested == true  -> HTTP 201 (new version stored)
// r.Ingested == false -> HTTP 200 (no new object)

// Batch (1–100 of the same type):
await _aml.IngestBatchAsync(
    IngestBatchRequest.For(MarbleTransaction.ObjectType, transactions), ct);

Typed models

Convenience models for the data-model objects (each carries its table name and implements IAmlObject, so object_id/updated_at are filled automatically):

Type ObjectType
MarbleTransaction transactions
MarbleWallet wallet
MarbleCustomer customer

The ObjectType constants match the table names shown in your Marble data model. If a table is renamed in Marble, update the matching const ObjectType.

Payload flexibility — you are not forced to model tables in C#

The typed models are a convenience. Any payload works as long as it serializes to a flat object carrying object_id and updated_at:

// Dictionary
var account = new Dictionary<string, object?>
{
    ["object_id"]  = "acc-9",
    ["updated_at"] = DateTimeOffset.UtcNow.ToString("O"),
    ["full_name"]  = "Jane Doe",
};
await _aml.IngestAsync(IngestRequest.For("accounts", account), ct);

// System.Text.Json.Nodes.JsonObject is also accepted.
// Types implementing IAmlObject get object_id / updated_at filled in automatically.

Errors

  • AmlProviderApiException — provider returned a non-2xx response. Carries StatusCode, ErrorCode (e.g. Marble's invalid_payload), and ResponseBody.
  • AmlProviderNotFoundException — Get(name)/GetDefault() with no matching registration.
  • AmlComplianceException — base type (also thrown for missing object_id/updated_at).

Adding another provider later

  1. New project AmlCompliance.<Provider> referencing AmlCompliance.Abstractions.
  2. Implement IAmlProvider.
  3. Add a DI extension AddXxx(this IAmlComplianceBuilder, ...) that registers a keyed IAmlProvider and calls builder.RegisterProvider(name).

Consumers then do providers.Get("xxx") — no other code changes.

Tests

Unit tests — tests/AmlCompliance.Tests

dotnet test tests/AmlCompliance.Tests -c Release

Use a stubbed HttpMessageHandler; they validate request shaping, response parsing, the nested-error envelope, and DI wiring without calling the live API.

Live integration tests — tests/AmlCompliance.IntegrationTests

Real calls against a Marble instance. Skipped unless MARBLE_API_KEY is set — never fail when unconfigured. Environment variables:

Variable Default Purpose
MARBLE_API_KEY — API key (Settings ▸ API keys). Required to un-skip.
MARBLE_BASE_URL https://api.checkmarble.com Override for self-hosted.
MARBLE_API_VERSION v1 API version segment.
MARBLE_OBJECT_TYPES transactions,wallet,customer Object types to exercise.
MARBLE_SCENARIO_ID — A scenario id (from the app) for the single-decision path.
MARBLE_SCENARIO_OBJECT_TYPE transactions Object type for that scenario.
MARBLE_ALLOW_INGEST false Set true to allow tests that write objects.
MARBLE_API_KEY=*** dotnet test tests/AmlCompliance.IntegrationTests -c Release

What they do:

  • DecideAll_EvaluatesEligibleScenarios_PerObjectType — runs POST /decisions/all per object type and reports Metadata (eligible / created / skipped). This is the discovery signal: the public v1 API has no list-scenarios endpoint, so scenarios are observed via the decision pipeline.
  • Decide_SingleScenario_RoundTrips_WhenScenarioIdProvided — replays one scenario via POST /decisions (needs MARBLE_SCENARIO_ID).
  • ListDecisions_HarvestsScenarioIds_FromHistory — distinct scenario ids from GET /decisions.
  • FiringScenario_ProducesExpectedOutcome — data-driven: reads scenarios.json (copy scenarios.example.json), each entry a firing trigger object + the expected outcome/score from your rules sheet, and asserts the matching decision fires with that outcome.

A scenario only fires through decisions/all when it has a published live version. Draft/inactive scenarios are reported as skipped.

References

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.1 727 7/7/2026
0.1.0 111 7/7/2026