•
8 min read
EdiTrack API

EdiTrack is an EDI X12 transaction ingest platform. It accepts raw EDI payloads from integration operators and upstream systems, durably stores each accepted transaction, and returns a structured acknowledgment with a stable transaction ID and correlation ID β€” making every ingest attempt fully traceable through structured logs.

πŸ“‹ The Business Problem

Electronic Data Interchange (EDI) is the language large enterprises use to exchange structured business documents β€” purchase orders, invoices, ship notices, and healthcare claims. The X12 standard, maintained by ASC X12, is the dominant format in North America and sits at the center of almost every large-scale supply chain, healthcare network, and logistics operation on the continent.

A few of the transaction sets you encounter in the wild:

Transaction SetDocument Type
850Purchase Order
856Advance Ship Notice / Manifest
810Invoice
837Healthcare Claim
270 / 271Eligibility Inquiry & Response
997Functional Acknowledgment

Why Ingest Is an Enterprise-Scale Problem

At production scale, EDI X12 is not optional β€” it is a contractual requirement. Large retailers mandate that suppliers transmit an 850 before shipping goods. Healthcare clearinghouses process 837s by the millions per day. Every interchange is wrapped in an ISA* envelope that encodes sender and receiver IDs, interchange control numbers, version identifiers, and delimiters β€” and every one of them has to land somewhere reliably.

The ingest layer is deceptively hard to get right:

  • Payloads arrive from many trading partners with different sender/receiver configurations
  • Each accepted transaction must be durably persisted and acknowledged with a stable, traceable identifier
  • Every ingest attempt β€” accepted or rejected β€” must produce an audit trail
  • Failures must return structured, retryable error responses, not silent drops or generic 500s

At enterprise scale this is the domain of dedicated EDI translation platforms like SPS Commerce, TrueCommerce, and MuleSoft β€” platforms that cost tens of thousands of dollars per year and require months of integration work.

Local Scale, Enterprise Thinking

EdiTrack doesn’t try to replicate a full EDI translation suite. It isolates and solves the hardest sub-problem: durable, traceable ingest with a consistent acknowledgment contract. The goal is to demonstrate that the architectural patterns used at enterprise scale β€” UUID-based tracing, structured logging, discriminated union result types, a uniform error envelope β€” can be applied cleanly in a single-service footprint that anyone can run locally with one docker compose up.

⚑︎ Tech Stack

LayerTechnology
RuntimeC# 13 / .NET 10
Web FrameworkASP.NET Core (Controller APIs)
ORMEntity Framework Core 9 β€” Npgsql.EntityFrameworkCore.PostgreSQL
DatabasePostgreSQL 16
LoggingSerilog β€” structured JSON to stdout
API DocsMicrosoft.AspNetCore.OpenApi + Scalar UI
TestingxUnit + Testcontainers (ephemeral Postgres β€” zero manual setup)
CIGitHub Actions
Dev InfraDocker Compose (Postgres only β€” the API runs natively via dotnet run)

πŸ—οΈ How It Works

The entire public surface area of the API is a single endpoint: POST /api/ingest.

sequenceDiagram
    participant Client as Trading Partner / Operator
    participant CTL as IngestController
    participant SVC as IngestService
    participant DB as PostgreSQL
    participant LOG as Serilog

    Client->>CTL: POST /api/ingest
    Note over Client,CTL: { senderId, receiverId, transactionType, payload }

    CTL->>SVC: IngestAsync(request, ct)
    SVC->>SVC: payload.TrimStart().StartsWith("ISA*")?

    alt Valid EDI envelope
        SVC->>DB: INSERT EdiTransaction (UUID v7)
        DB-->>SVC: OK
        SVC->>LOG: LogInformation β€” Accepted
        SVC-->>CTL: IngestResult.Success
        CTL-->>Client: 200 OK β€” IngestAcknowledgment
    else Invalid payload shape
        SVC-->>CTL: IngestResult.ValidationFailure
        CTL-->>Client: 400 Bad Request β€” ErrorResponse
    else DB write failure
        SVC->>LOG: LogError β€” PersistenceFailure
        SVC-->>CTL: IngestResult.PersistenceFailure
        CTL-->>Client: 503 Service Unavailable β€” ErrorResponse
    end

Discriminated Union Result Type

Rather than throwing exceptions for control flow, the service layer returns a typed discriminated union. The controller maps each case to its appropriate HTTP status code β€” no exceptions cross the service boundary.

// IngestService.cs β€” core ingest logic
public async Task<IngestResult> IngestAsync(IngestRequest request, CancellationToken ct = default)
{
    if (!IsEdiX12Shaped(request.Payload))
        return new IngestResult.ValidationFailure("Payload does not resemble an EDI X12 document.");

    var correlationId = string.IsNullOrWhiteSpace(request.CorrelationId)
        ? Guid.NewGuid().ToString("N")
        : request.CorrelationId;

    var entity = new EdiTransaction
    {
        Id             = Guid.CreateVersion7(),   // time-ordered UUID β€” no index fragmentation
        SenderId       = request.SenderId,
        ReceiverId     = request.ReceiverId,
        TransactionType = request.TransactionType,
        CorrelationId  = correlationId,
        Payload        = request.Payload,
        Status         = TransactionStatus.Received,
        ReceivedAt     = DateTimeOffset.UtcNow
    };

    _context.Transactions.Add(entity);

    try { await _context.SaveChangesAsync(ct); }
    catch (DbUpdateException ex)
    {
        _logger.LogError(ex, "Persistence failure {CorrelationId} {SenderId} {ReceiverId}",
            correlationId, request.SenderId, request.ReceiverId);
        return new IngestResult.PersistenceFailure("A transient database error occurred. Please retry.");
    }

    _logger.LogInformation(
        "Ingest accepted {CorrelationId} {SenderId} {ReceiverId} {TransactionType} {TransactionId} {Outcome}",
        correlationId, request.SenderId, request.ReceiverId,
        request.TransactionType, entity.Id, "Accepted");

    return new IngestResult.Success(new IngestAcknowledgment { ... });
}

UUID v7 for Primary Keys

Guid.CreateVersion7() β€” available in the .NET 10 BCL, no NuGet package required β€” generates time-ordered UUIDs. This means database inserts are naturally sequential, avoiding the B-tree index fragmentation caused by random v4 GUIDs, without introducing a surrogate integer key or sacrificing global uniqueness.

πŸ“„ Data Model

Every accepted ingest persists a single EdiTransaction row:

PropertyC# TypeDB ColumnConstraints
IdGuidid uuidPK β€” UUID v7, time-ordered
SenderIdstringsender_id varchar(50)NOT NULL
ReceiverIdstringreceiver_id varchar(50)NOT NULL
TransactionTypestringtransaction_type varchar(50)NOT NULL
CorrelationIdstringcorrelation_id varchar(100)NOT NULL β€” caller-supplied or server-generated
Payloadstringpayload textNOT NULL β€” raw EDI interchange
StatusTransactionStatusstatus varchar(50)NOT NULL β€” stored as string via EF .HasConversion<string>()
ReceivedAtDateTimeOffsetreceived_at timestamptzNOT NULL β€” UTC

The CorrelationId is always present. If the caller omits it, the service generates one before persisting β€” so every row in the database, and every log line it produces, shares the same trace token from the moment of ingest.

πŸ’» Running Locally

Prerequisites: .NET SDK 10.0 Β· Docker Desktop 24+ Β· dotnet-ef CLI 9.x

# 1. Clone and restore
git clone https://github.com/mehtaz23/editrack-api.git
cd editrack-api && dotnet restore

# 2. Start Postgres
docker compose up -d

# 3. Apply EF Core migrations
dotnet ef database update --project src/EdiTrack.Api

# 4. Run the API
dotnet run --project src/EdiTrack.Api
# β†’ http://localhost:5250
# β†’ Scalar UI: http://localhost:5250/scalar

Test a live ingest against a real ISA envelope:*

curl -s -X POST http://localhost:5250/api/ingest \
  -H "Content-Type: application/json" \
  -d '{
    "senderId": "ACME",
    "receiverId": "GLOBEX",
    "transactionType": "850",
    "payload": "ISA*00*          *00*          *ZZ*ACME           *ZZ*GLOBEX         *250101*0900*^*00501*000000001*0*P*>~"
  }' | jq .

Run the full test suite β€” Testcontainers manages its own ephemeral Postgres instance, so docker compose up is not required, only the Docker daemon:

dotnet test

βœ… Scope

What’s in:

  • βœ… Durable ingest of raw EDI X12 payloads via a single REST endpoint
  • βœ… Structured acknowledgment with stable UUID v7 transaction ID
  • βœ… Correlation ID passthrough β€” caller-supplied or server-generated
  • βœ… Uniform ErrorResponse envelope for 400 and 503 responses
  • βœ… Structured JSON logging via Serilog on every ingest attempt (accepted and rejected)
  • βœ… Full test coverage β€” unit (xUnit + EF Core InMemory) and integration (Testcontainers Postgres)
  • βœ… OpenAPI spec + Scalar UI for local exploration

What’s out of scope by design:

  • ❌ Full EDI X12 segment parsing or interchange validation beyond the ISA* envelope check
  • ❌ Trading partner management or onboarding workflows
  • ❌ Authentication and authorization
  • ❌ Message queuing or async processing pipelines
  • ❌ Support for additional EDI standards (EDIFACT, HL7, etc.)

πŸ›οΈ License

MIT