ArthaMarble 0.1.1
dotnet add package ArthaMarble --version 0.1.1
NuGet\Install-Package ArthaMarble -Version 0.1.1
<PackageReference Include="ArthaMarble" Version="0.1.1" />
<PackageVersion Include="ArthaMarble" Version="0.1.1" />
<PackageReference Include="ArthaMarble" />
paket add ArthaMarble --version 0.1.1
#r "nuget: ArthaMarble, 0.1.1"
#:package ArthaMarble@0.1.1
#addin nuget:?package=ArthaMarble&version=0.1.1
#tool nuget:?package=ArthaMarble&version=0.1.1
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
ObjectTypeconstants match the table names shown in your Marble data model. If a table is renamed in Marble, update the matchingconst 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. CarriesStatusCode,ErrorCode(e.g. Marble'sinvalid_payload), andResponseBody.AmlProviderNotFoundException—Get(name)/GetDefault()with no matching registration.AmlComplianceException— base type (also thrown for missingobject_id/updated_at).
Adding another provider later
- New project
AmlCompliance.<Provider>referencingAmlCompliance.Abstractions. - Implement
IAmlProvider. - Add a DI extension
AddXxx(this IAmlComplianceBuilder, ...)that registers a keyedIAmlProviderand callsbuilder.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— runsPOST /decisions/allper object type and reportsMetadata(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 viaPOST /decisions(needsMARBLE_SCENARIO_ID).ListDecisions_HarvestsScenarioIds_FromHistory— distinct scenario ids fromGET /decisions.FiringScenario_ProducesExpectedOutcome— data-driven: readsscenarios.json(copyscenarios.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/allwhen it has a published live version. Draft/inactive scenarios are reported asskipped.
References
- Marble docs: https://docs.checkmarble.com/docs/what-is-marble
- API reference: https://docs.checkmarble.com/reference/
| Product | Versions 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. |
-
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
- Microsoft.Extensions.Http (>= 8.0.1)
- Microsoft.Extensions.Options (>= 8.0.2)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 8.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.