Session M2-07 trainer guide: Migrate a legacy Java service to modern .NET¶
Delivery objective¶
Teach learners to preserve behavior during migration. Have them inspect a runnable Java service and define the pricing contract before moving one complete HTTP slice to .NET 10. Then compare both implementations using the same JSON cases.
Keep the slice narrow. Aim for one reviewed route and a backlog for the rest of the service. Do not build a rewrite plan on untested assumptions.
Important
Check GitHub Copilot access before delivery. If a learner cannot use Copilot in the training environment, stop the lab for that learner. Do not replace the session with an unreviewed public tool or personal account.
One-hour plan¶
| Time | Segment |
|---|---|
| 0:00 to 0:08 | Define the migration boundary |
| 0:08 to 0:18 | Read the Java request path |
| 0:18 to 0:30 | Extract behavior and contract cases |
| 0:30 to 0:42 | Map the slice to .NET 10 |
| 0:42 to 0:52 | Use Copilot without surrendering decisions |
| 0:52 to 1:00 | Prepared demo and lab handoff |
Trainer preflight¶
Prefer the repository Dev Container or Codespaces.
Run:
java -version
dotnet --version
cd sessions/session-m2-07-java-to-dotnet-modernization/lab/starter/legacy-java-order-service
./mvnw test
./mvnw package
Expected Java test output:
The container should report the .NET 10 SDK. The Java runtime may be newer than
Java 8 because Maven compiles the baseline with source and target set to 8.
Prepare two terminals:
Also prepare the completed .NET solution:
cd ../../solution/modern-dotnet-order-service
dotnet build
dotnet run --project OrderPricing.ContractTests
Do not rely on a network connection during the teaching segment after the Maven Wrapper has downloaded Maven and its plugins. The code itself uses only JDK and .NET platform APIs.
Teaching model¶
1. Migrate a route, not a codebase¶
Use the HTTP route as the migration boundary:
This is a vertical slice because it includes the entry point, contract, domain logic, failure behavior, and verification. Moving only the calculation class would leave the risky edges behind.
Keep these Java routes in place:
GET /healthGET /orders/samples
Keep them as backlog candidates. Leaving them in Java makes the migration boundary clear and keeps the lab inside two hours.
2. Separate contract from structure¶
The Java service contains classes and JDK HTTP plumbing. Those are implementation choices. The contract is smaller:
- method and path;
- request JSON fields;
- accepted values;
- calculation order;
- rounding rule;
- response JSON fields;
- status codes and error shape.
Ask learners to mark each finding as one of:
| Classification | Example | Migration treatment |
|---|---|---|
| External behavior | GOLD gives a 10% tier discount |
Preserve |
| Calculation rule | coupon runs after tier discount | Preserve and test |
| Transport choice | JDK HttpServer |
Replace with Minimal API |
| Language structure | getters and nested item class | Redesign in C# |
| Unknown behavior | concurrent request limit | Record, do not invent |
Use this classification to show which Java structures can change. Copilot may reproduce them in C#. Check behavior to establish parity.
3. Use integer money and explicit rounding¶
The service stores money in cents. Percentage calculations use basis points:
This is integer round-half-up for non-negative amounts.
The operation order is fixed:
- sum line items;
- apply the customer-tier discount;
- apply
SAVE10to the remaining merchandise amount; - decide shipping from the discounted merchandise amount;
- calculate tax on discounted merchandise;
- add discounted merchandise, shipping, and tax.
Changing the order changes the result. Floating-point arithmetic can also change
the result. The round-half-up fixture exists to catch both mistakes.
4. Test both implementations with the same fixtures¶
Open lab/starter/contracts/order-pricing-cases.jsonl.
Each line contains:
- a case name;
- one request;
- the expected response fields.
JSON Lines keeps each case readable and lets the Java 8 baseline consume the file
without adding a JSON package. The .NET test executable uses System.Text.Json.
The five cases cover:
| Case | Main risk |
|---|---|
standard-domestic |
baseline shipping and tax |
silver-free-domestic-shipping |
threshold after discount |
gold-save10-domestic |
discount order |
gold-international |
international shipping |
round-half-up |
integer rounding |
Contract fixtures check only the selected shared behavior. Validation errors and HTTP behavior still need focused checks.
5. Map responsibilities to .NET¶
Use a small mapping:
| Java baseline | .NET 10 target | Review question |
|---|---|---|
App and HttpHandler |
Minimal API route | Are method, path, status, and JSON stable? |
OrderRequest |
C# records | Do names and null rules match? |
OrderPricingService |
PricingEngine |
Are operation order and overflow checks preserved? |
PriceQuote |
response record | Are all fields present and in cents? |
ContractFixtureTest |
console contract test project | Does it read the same fixture file? |
The target has one API project and one package-free executable test project.
Minimal APIs and System.Text.Json come with ASP.NET Core and .NET.
Do not add:
- an object mapper;
- a validation framework;
- a mediator package;
- a database abstraction;
- a repository interface for fixture data.
Those additions do not support this pricing slice.
Copilot working agreement¶
Give Copilot a specific route and contract. Do not ask it to infer the business contract.
Good prompt sequence¶
Start with inspection:
Trace POST /orders/price through the Java starter. Return a table with the entry
point, parsing, validation, pricing rules, response mapping, and tests. Cite file
and symbol names. Do not propose .NET code yet.
Then check behavior:
Compare the Java pricing code with contracts/order-pricing-cases.jsonl. List the
calculation order, rounding rule, shipping threshold, accepted values, and any
behavior not covered by a fixture. Do not change files.
Only then implement:
Implement PricingEngine.Price in the .NET 10 scaffold. Preserve the shared JSON
contract exactly. Use integer cents, checked arithmetic, and platform APIs only.
Do not add packages or change the endpoint shape.
Finish with review:
Review the Java and .NET pricing implementations for behavior drift. Focus on
operation order, rounding, validation, overflow, shipping threshold, tax base,
and response fields. Return findings before editing.
Reject these patterns¶
- "Convert this project to .NET."
- generated packages without a stated need;
- a new contract because the C# model looks cleaner;
- decimal or floating-point calculations without a parity decision;
- tests generated from the .NET implementation instead of the accepted fixtures;
- deletion of the Java path before tests and review support the replacement.
Prepared demonstration¶
Use the tested starter and solution.
Step 1: establish the Java behavior¶
Run:
cd sessions/session-m2-07-java-to-dotnet-modernization/lab/starter/legacy-java-order-service
./mvnw test
./mvnw package
java -jar target/legacy-order-service-1.0.0.jar
Send the gold-save10-domestic request. Point out the response:
{
"subtotalCents": 5097,
"tierDiscountCents": 510,
"couponDiscountCents": 459,
"shippingCents": 795,
"taxCents": 341,
"totalCents": 5264
}
Step 2: trace the request¶
Open these files in order:
App.javaJsonCodec.javaOrderRequest.javaOrderPricingService.javaPriceQuote.javaContractFixtureTest.java
Show that the HTTP handler is thin. Most behavior sits in the pricing service and the fixture-backed test.
Step 3: show a plausible wrong migration¶
In a temporary demo copy, change the .NET coupon calculation to use subtotal
instead of afterTier.
Run:
The gold-save10-domestic case should fail. Use the failure to show that clean C#
can still produce the wrong result.
Restore the reference code. Run the test again and show all five cases passing.
Step 4: compare HTTP results¶
Run the .NET API on port 5080:
Send the same request to ports 8080 and 5080. Compare the JSON field values.
Do not claim full service parity. The demo checks one HTTP request and five accepted pricing cases.
Review points¶
Pause the lab at these points.
Review point 1: architecture notes¶
Learners should identify:
- one HTTP entry point;
- one parsing boundary;
- one domain calculation;
- one response mapping;
- one existing contract test.
If the notes describe only classes, ask which observable behavior each class supports.
Review point 2: contract before code¶
Learners should explain the round-half-up case without reading the expected
value from the fixture. Ask them to calculate the tier and coupon discounts in
order.
Review point 3: generated change¶
Before learners accept Copilot output, ask:
- Did it add packages?
- Did it change JSON names?
- Did it use floating point?
- Did it preserve checked arithmetic?
- Did it move the shipping threshold?
- Did it calculate tax on shipping?
Review point 4: compare test and HTTP results¶
Require these results at the final review:
- Java contract test output;
- .NET contract test output;
- one matching HTTP response;
- a migration backlog with checks required for each next slice.
Migration backlog model¶
Use a slice-first backlog:
| Priority | Slice | Checks before cutover |
|---|---|---|
| P0 | Order pricing | Shared contract cases, validation checks, HTTP comparison |
| P1 | Sample-order query | Response contract and synthetic catalog checks |
| P1 | Health and diagnostics | Agreed status contract and operational owner |
| P2 | Error consistency | Status and JSON error matrix |
| P2 | Load and concurrency | Measured target and repeatable test |
Have learners state what remains in Java, what they do not yet know, and when they can remove each legacy slice.
Common mistakes¶
The learner translates every Java class. Bring them back to the route and contract. A class-by-class rewrite preserves structure that may not matter.
The .NET output is off by one cent. Check the operation order and the + 5000
round-half-up step.
Free shipping triggers too early. The threshold uses merchandise after both discounts.
Tax includes shipping. The accepted Java behavior taxes discounted merchandise only.
The generated API adds packages. Remove them unless the learner can name a requirement that platform APIs cannot meet.
Tests pass because expected values were copied from new code. The shared fixture is the accepted source. Do not regenerate it from the migration.
Lab handoff¶
Learners:
- run and inspect the Java baseline;
- complete the architecture notes;
- review the shared contract cases;
- implement the .NET pricing engine;
- add package-free contract tests;
- compare Java and .NET HTTP responses;
- write the next migration backlog.
One route must pass the contract and HTTP checks. The rest of the Java service stays in place until its own slices have contracts and review.