-
Notifications
You must be signed in to change notification settings - Fork 1
samples: FactoryDemo.Api.FSharp — JSON API (v0, in-memory, stack-independent) #146
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk.Web"> | ||
| <PropertyGroup> | ||
| <OutputType>Exe</OutputType> | ||
| <RootNamespace>Zeta.Samples.FactoryDemo.Api</RootNamespace> | ||
| <TreatWarningsAsErrors>true</TreatWarningsAsErrors> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <Compile Include="Seed.fs" /> | ||
| <Compile Include="Program.fs" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <PackageReference Include="FSharp.Core" /> | ||
| </ItemGroup> | ||
| </Project> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| module Zeta.Samples.FactoryDemo.Api.Program | ||
|
|
||
| open System | ||
| open Microsoft.AspNetCore.Builder | ||
| open Microsoft.AspNetCore.Http | ||
| open Microsoft.Extensions.DependencyInjection | ||
|
|
||
| // Minimal F# ASP.NET Core Web API serving the seed data for the | ||
| // factory-demo. Stack-independent — any frontend choice (Blazor / | ||
| // React / Vue / Svelte / curl) consumes the same JSON. | ||
| // | ||
| // V0 scope: in-memory seed only, no DB wiring. A Postgres backing | ||
| // with schema.sql + seed-data.sql is a planned follow-up PR; until | ||
| // then this API carries its own in-memory seed so the sample runs | ||
| // with zero external dependencies. | ||
|
|
||
| let private pipelineFunnel () = | ||
| Seed.opportunities | ||
| |> List.groupBy (fun o -> o.Stage) | ||
| |> List.map (fun (stage, opps) -> | ||
| {| Stage = stage | ||
| Count = opps |> List.length | ||
| TotalCents = opps |> List.sumBy (fun o -> o.AmountCents) |}) | ||
|
|
||
| let private duplicateEmails () = | ||
| Seed.customers | ||
| |> List.groupBy (fun c -> c.Email) | ||
| |> List.filter (fun (_, xs) -> xs |> List.length > 1) | ||
| |> List.map (fun (email, xs) -> | ||
| {| Email = email | ||
| CustomerIds = xs |> List.map (fun c -> c.Id) |}) | ||
|
|
||
| [<EntryPoint>] | ||
| let main args = | ||
| let builder = WebApplication.CreateBuilder(args) | ||
| // Emit consistent JSON for an external consumer — default | ||
| // System.Text.Json options are fine; documented for future tuning. | ||
| builder.Services.AddEndpointsApiExplorer() |> ignore | ||
| let app = builder.Build() | ||
|
|
||
| // Trivial root — lets a browser confirm the API is up. The | ||
| // `endpoints` list is the full contract surface (all 9 paths | ||
| // listed, including parameterised routes). | ||
| app.MapGet("/", Func<_>(fun () -> | ||
| {| name = "Factory-demo API (F#)" | ||
| version = "0.0.1" | ||
| endpoints = | ||
| [ "/" | ||
| "/api/customers" | ||
| "/api/customers/{id}" | ||
| "/api/customers/{id}/activities" | ||
| "/api/opportunities" | ||
| "/api/opportunities/{id}" | ||
| "/api/activities" | ||
| "/api/pipeline/funnel" | ||
| "/api/pipeline/duplicates" ] |} :> obj)) | ||
| |> ignore | ||
|
|
||
| app.MapGet("/api/customers", Func<_>(fun () -> Seed.customers :> obj)) |> ignore | ||
| app.MapGet("/api/customers/{id:long}", Func<int64, IResult>(fun id -> | ||
| Seed.customers | ||
| |> List.tryFind (fun c -> c.Id = id) | ||
| |> function | ||
| | Some c -> Results.Ok c | ||
| | None -> Results.NotFound())) | ||
| |> ignore | ||
|
|
||
| app.MapGet("/api/opportunities", Func<_>(fun () -> Seed.opportunities :> obj)) |> ignore | ||
| app.MapGet("/api/opportunities/{id:long}", Func<int64, IResult>(fun id -> | ||
| Seed.opportunities | ||
| |> List.tryFind (fun o -> o.Id = id) | ||
| |> function | ||
| | Some o -> Results.Ok o | ||
| | None -> Results.NotFound())) | ||
| |> ignore | ||
|
|
||
| app.MapGet("/api/activities", Func<_>(fun () -> Seed.activities :> obj)) |> ignore | ||
| app.MapGet("/api/customers/{id:long}/activities", Func<int64, obj>(fun id -> | ||
| Seed.activities | ||
| |> List.filter (fun a -> a.CustomerId = id) | ||
| :> obj)) | ||
| |> ignore | ||
|
|
||
| // Derived views that a frontend would otherwise compute client- | ||
| // side. Landing them server-side keeps the API contract tight. | ||
| app.MapGet("/api/pipeline/funnel", Func<_>(fun () -> pipelineFunnel () :> obj)) |> ignore | ||
| app.MapGet("/api/pipeline/duplicates", Func<_>(fun () -> duplicateEmails () :> obj)) |> ignore | ||
|
|
||
| app.Run() | ||
| 0 | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,101 @@ | ||||||||||||||||||||||||
| # Factory-demo — JSON API (F#) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| **What this is:** A minimal F# ASP.NET Core Web API that serves | ||||||||||||||||||||||||
| the demo's seed data as JSON. Stack-independent — any frontend | ||||||||||||||||||||||||
| (Blazor / React / Vue / curl) consumes the same endpoints. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| **What this is NOT:** A pitch for Zeta as a data store. The | ||||||||||||||||||||||||
| demo sells the **software factory**, not the database layer. | ||||||||||||||||||||||||
| Backend is in-memory (v0) and will be swapped to Postgres (v1) | ||||||||||||||||||||||||
| without changing the public API contract. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| ## Why this sample exists | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| The factory-demo needs a JSON API that any frontend choice can | ||||||||||||||||||||||||
| consume. This API ships now so the backend is not on the | ||||||||||||||||||||||||
| critical path when the frontend stack is chosen. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| This is the F# reference implementation. A C# companion sample | ||||||||||||||||||||||||
| (`samples/FactoryDemo.Api.CSharp/`, sibling PR #147) is planned | ||||||||||||||||||||||||
| with the same 9 endpoints, matching JSON shapes, and identical | ||||||||||||||||||||||||
| seed. C# is the more popular .NET language, so it is the natural | ||||||||||||||||||||||||
| primary demo path; F# stays the reference because F# looks | ||||||||||||||||||||||||
| closer to math, which makes theorems over the algebra easier | ||||||||||||||||||||||||
| to express. | ||||||||||||||||||||||||
|
Comment on lines
+19
to
+24
|
||||||||||||||||||||||||
| (`samples/FactoryDemo.Api.CSharp/`, sibling PR #147) is planned | |
| with the same 9 endpoints, matching JSON shapes, and identical | |
| seed. C# is the more popular .NET language, so it is the natural | |
| primary demo path; F# stays the reference because F# looks | |
| closer to math, which makes theorems over the algebra easier | |
| to express. | |
| at `samples/FactoryDemo.Api.CSharp/` is planned with the same 9 | |
| endpoints, matching JSON shapes, and identical seed. C# is the | |
| more popular .NET language, so it is the natural primary demo | |
| path; F# stays the reference because F# looks closer to math, | |
| which makes theorems over the algebra easier to express. |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,151 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| module Zeta.Samples.FactoryDemo.Api.Seed | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| open System | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Deterministic in-memory seed data for the API sample. Keep these | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // values stable so the same demo scenarios work whether the backing | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // store is Postgres (production-shape, planned follow-up) or this | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // in-memory fallback (zero-dependency, current). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type Customer = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { Id: int64 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Name: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Email: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Phone: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Address: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| CreatedAt: DateTimeOffset | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| UpdatedAt: DateTimeOffset } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type Opportunity = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { Id: int64 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| CustomerId: int64 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Stage: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AmountCents: int64 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| CreatedAt: DateTimeOffset | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| UpdatedAt: DateTimeOffset } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type Activity = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { Id: int64 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| CustomerId: int64 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| OpportunityId: int64 option | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Kind: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Notes: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| OccurredAt: DateTimeOffset } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Fixed clock so the seed is deterministic. Any timestamp computed | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // from this is reproducible across restarts. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let private now = DateTimeOffset(2026, 4, 23, 0, 0, 0, TimeSpan.Zero) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let private ago (days: int) = now.AddDays(float -days) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let customers : Customer list = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let mk id name email phone address = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { Id = id | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Name = name | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Email = email | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Phone = phone | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Address = address | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| CreatedAt = ago 120 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| UpdatedAt = ago 1 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Email collision #1: Alice Plumbing (1) and Aaron Smith (13) share alice@acme.example. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Email collision #2: Bob HVAC (5) and Quincy Assistant (19) share bob@trades.example. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [ mk 1L "Alice Plumbing LLC" "alice@acme.example" "555-0101" "123 Elm St, Portland OR" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mk 2L "Benson Roofing" "benson@roof.example" "555-0102" "45 Oak Ave, Seattle WA" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mk 3L "Crystal Electric" "crystal@sparks.example" "555-0103" "9 Pine Rd, Boise ID" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mk 4L "Delta HVAC & Mechanical" "delta@hvac.example" "555-0104" "700 Main St, Spokane WA" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mk 5L "Bob HVAC Services" "bob@trades.example" "555-0105" "12 Bay Blvd, Tacoma WA" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mk 6L "Evergreen Landscaping" "info@evergreen.example" "555-0106" "88 Forest Ln, Eugene OR" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mk 7L "Fairbanks Plumbing" "contact@fairbanks.example" "555-0107" "5 River Rd, Anchorage AK" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mk 8L "Granite Pest Control" "hello@granite.example" "555-0108" "301 Stone Way, Boise ID" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mk 9L "Highland Roofing Co" "highland@roof.example" "555-0109" "22 Hill Dr, Bend OR" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mk 10L "Iron Tree Electric" "iron@tree.example" "555-0110" "17 Spruce St, Salem OR" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mk 11L "Jackson Pool Services" "jackson@pools.example" "555-0111" "600 Lake Rd, Reno NV" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mk 12L "Klein Garage Doors" "klein@doors.example" "555-0112" "44 4th Ave, Medford OR" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mk 13L "Aaron Smith (new contact)" "alice@acme.example" "555-0113" "123 Elm St, Portland OR" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mk 14L "Lakeview Solar" "lakeview@solar.example" "555-0114" "250 Shore Dr, Bellevue WA" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mk 15L "Mountain Well Drilling" "mountain@wells.example" "555-0115" "12 Ridge Rd, Coeur dAlene ID" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mk 16L "Nightingale Security" "ngale@secure.example" "555-0116" "88 Watch Way, Vancouver WA" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mk 17L "Oak Hill Septic" "oak@septic.example" "555-0117" "14 Rural Rt 3, Gresham OR" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mk 18L "Prairie Window Cleaning" "prairie@windows.example" "555-0118" "66 Glass Rd, Kennewick WA" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mk 19L "Quincy Assistant (Bob HVAC)" "bob@trades.example" "555-0119" "12 Bay Blvd, Tacoma WA" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mk 20L "Redwood Tree Service" "redwood@trees.example" "555-0120" "3 Canopy Ct, Hillsboro OR" ] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+49
to
+70
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Email collision #1: Alice Plumbing (1) and Aaron Smith (13) share alice@acme.example. | |
| // Email collision #2: Bob HVAC (5) and Quincy Assistant (19) share bob@trades.example. | |
| [ mk 1L "Alice Plumbing LLC" "alice@acme.example" "555-0101" "123 Elm St, Portland OR" | |
| mk 2L "Benson Roofing" "benson@roof.example" "555-0102" "45 Oak Ave, Seattle WA" | |
| mk 3L "Crystal Electric" "crystal@sparks.example" "555-0103" "9 Pine Rd, Boise ID" | |
| mk 4L "Delta HVAC & Mechanical" "delta@hvac.example" "555-0104" "700 Main St, Spokane WA" | |
| mk 5L "Bob HVAC Services" "bob@trades.example" "555-0105" "12 Bay Blvd, Tacoma WA" | |
| mk 6L "Evergreen Landscaping" "info@evergreen.example" "555-0106" "88 Forest Ln, Eugene OR" | |
| mk 7L "Fairbanks Plumbing" "contact@fairbanks.example" "555-0107" "5 River Rd, Anchorage AK" | |
| mk 8L "Granite Pest Control" "hello@granite.example" "555-0108" "301 Stone Way, Boise ID" | |
| mk 9L "Highland Roofing Co" "highland@roof.example" "555-0109" "22 Hill Dr, Bend OR" | |
| mk 10L "Iron Tree Electric" "iron@tree.example" "555-0110" "17 Spruce St, Salem OR" | |
| mk 11L "Jackson Pool Services" "jackson@pools.example" "555-0111" "600 Lake Rd, Reno NV" | |
| mk 12L "Klein Garage Doors" "klein@doors.example" "555-0112" "44 4th Ave, Medford OR" | |
| mk 13L "Aaron Smith (new contact)" "alice@acme.example" "555-0113" "123 Elm St, Portland OR" | |
| mk 14L "Lakeview Solar" "lakeview@solar.example" "555-0114" "250 Shore Dr, Bellevue WA" | |
| mk 15L "Mountain Well Drilling" "mountain@wells.example" "555-0115" "12 Ridge Rd, Coeur dAlene ID" | |
| mk 16L "Nightingale Security" "ngale@secure.example" "555-0116" "88 Watch Way, Vancouver WA" | |
| mk 17L "Oak Hill Septic" "oak@septic.example" "555-0117" "14 Rural Rt 3, Gresham OR" | |
| mk 18L "Prairie Window Cleaning" "prairie@windows.example" "555-0118" "66 Glass Rd, Kennewick WA" | |
| mk 19L "Quincy Assistant (Bob HVAC)" "bob@trades.example" "555-0119" "12 Bay Blvd, Tacoma WA" | |
| mk 20L "Redwood Tree Service" "redwood@trees.example" "555-0120" "3 Canopy Ct, Hillsboro OR" ] | |
| // Email collision #1: Alice Plumbing (1) and Alice Plumbing Intake Contact (13) share alice@acme.example. | |
| // Email collision #2: Bob HVAC (5) and Quincy Assistant (19) share bob@trades.example. | |
| [ mk 1L "Alice Plumbing LLC" "alice@acme.example" "555-0101" "123 Elm St, Portland OR" | |
| mk 2L "Benson Roofing" "benson@roof.example" "555-0102" "45 Oak Ave, Seattle WA" | |
| mk 3L "Crystal Electric" "crystal@sparks.example" "555-0103" "9 Pine Rd, Boise ID" | |
| mk 4L "Delta HVAC & Mechanical" "delta@hvac.example" "555-0104" "700 Main St, Spokane WA" | |
| mk 5L "Bob HVAC Services" "bob@trades.example" "555-0105" "12 Bay Blvd, Tacoma WA" | |
| mk 6L "Evergreen Landscaping" "info@evergreen.example" "555-0106" "88 Forest Ln, Eugene OR" | |
| mk 7L "Fairbanks Plumbing" "contact@fairbanks.example" "555-0107" "5 River Rd, Anchorage AK" | |
| mk 8L "Granite Pest Control" "hello@granite.example" "555-0108" "301 Stone Way, Boise ID" | |
| mk 9L "Highland Roofing Co" "highland@roof.example" "555-0109" "22 Hill Dr, Bend OR" | |
| mk 10L "Iron Tree Electric" "iron@tree.example" "555-0110" "17 Spruce St, Salem OR" | |
| mk 11L "Jackson Pool Services" "jackson@pools.example" "555-0111" "600 Lake Rd, Reno NV" | |
| mk 12L "Klein Garage Doors" "klein@doors.example" "555-0112" "44 4th Ave, Medford OR" | |
| mk 13L "Alice Plumbing Intake Contact" "alice@acme.example" "555-0113" "123 Elm St, Portland OR" | |
| mk 14L "Lakeview Solar" "lakeview@solar.example" "555-0114" "250 Shore Dr, Bellevue WA" | |
| mk 15L "Mountain Well Drilling" "mountain@wells.example" "555-0115" "12 Ridge Rd, Coeur dAlene ID" | |
| mk 16L "Nightingale Security" "ngale@secure.example" "555-0116" "88 Watch Way, Vancouver WA" | |
| mk 17L "Oak Hill Septic" "oak@septic.example" "555-0117" "14 Rural Rt 3, Gresham OR" | |
| mk 18L "Prairie Window Cleaning" "prairie@windows.example" "555-0118" "66 Glass Rd, Kennewick WA" | |
| mk 19L "Quincy Assistant (Bob HVAC)" "bob@trades.example" "555-0119" "12 Bay Blvd, Tacoma WA" | |
| mk 20L "Redwood Tree Service" "redwood@trees.example" "555-0120" "3 Canopy Ct, Hillsboro OR" ] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P1: JSON property casing is inconsistent across endpoints: the root endpoint returns lowercase fields (name/version/endpoints) while the record/anonymous-record responses from other endpoints will serialize as PascalCase by default. This creates a confusing contract for API consumers. Consider either configuring System.Text.Json to enforce a single naming policy (e.g., camelCase) for all responses or aligning the root payload field names with the rest of the API.