Skip to content

Commit ea1f8c8

Browse files
authored
Merge pull request #3465 from pareenaverma/content_review
Tech review of nopcommerce dotnet LP
2 parents d144970 + 8a2462b commit ea1f8c8

6 files changed

Lines changed: 234 additions & 42 deletions

File tree

content/learning-paths/servers-and-cloud-computing/dotnet-migration-nopcommerce/2-environment-setup.md

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,47 +16,57 @@ This path was validated with Ubuntu 24.04 LTS on Azure Cobalt in `westus2` (exam
1616

1717
## Install tools on Cobalt VM
1818

19-
Install the toolchain on the Cobalt VM before building and testing.
19+
Install the toolchain on the Cobalt VM before building and testing. The commands install the Linux packages used throughout this Learning Path: source control, HTTP validation, JSON inspection, Python for the endpoint tester, package archive inspection, Docker, and the .NET 9 SDK from the Ubuntu backports feed.
2020

2121
```bash
2222
sudo apt-get update -y
23-
sudo apt-get install -y git curl jq ca-certificates gnupg docker.io
23+
sudo apt-get install -y git curl jq python3 ripgrep unzip ca-certificates gnupg software-properties-common docker.io
2424
sudo usermod -aG docker "$USER"
2525

2626
sudo add-apt-repository ppa:dotnet/backports -y
2727
sudo apt-get update -y
2828
sudo apt-get install -y dotnet-sdk-9.0
2929
```
3030

31-
Verify:
31+
The Docker group change applies to new login sessions. Sign out and sign back in before running Docker without `sudo`, or run `newgrp docker` in the current terminal before continuing.
3232

33-
Confirm architecture and tool versions before proceeding.
33+
Confirm architecture and tool versions before proceeding. On Azure Cobalt, the Linux machine architecture should be `aarch64`, and the .NET SDK should be a 9.0.x release.
3434

3535
```bash
3636
uname -m
3737
dotnet --version
3838
docker --version
3939
```
4040

41-
Expected output: `Arm` (kernel strings may still show architecture-specific values). Ensure the .NET SDK version is 9.0.x.
41+
The output is similar to:
42+
43+
```output
44+
aarch64
45+
9.0.x
46+
Docker version ...
47+
```
4248

4349
If you are upgrading from an older .NET version before migrating to Cobalt, you can use [GitHub Copilot modernization for .NET](https://learn.microsoft.com/dotnet/core/porting/github-copilot-app-modernization/overview) to assess the project and guide the upgrade. In Visual Studio Code, open Copilot Chat and use `@modernize-dotnet`; in Visual Studio, use the Modernize action from Solution Explorer.
4450

4551
## PostgreSQL prerequisite for nopCommerce install
4652

4753
nopCommerce defaults to SQL Server, but this learning path uses PostgreSQL for Arm validation. For PostgreSQL installs, `citext` is required before migration/installation. Without it, installer migrations fail with `type "citext" does not exist` (captured in local test artifacts).
4854

49-
Create PostgreSQL and enable `citext` before running the installer:
55+
Create PostgreSQL and enable `citext` before running the installer. The database runs in Docker on the Cobalt VM so the nopCommerce app can connect to `127.0.0.1:5432` during the local validation phase. Replace the password value before running the command.
5056

5157
```bash
58+
export NOP_POSTGRES_PASSWORD='replace-with-a-strong-password'
59+
5260
# Start PostgreSQL for local validation.
5361
docker run -d --name nop-postgres \
5462
-e POSTGRES_USER=nop \
55-
-e POSTGRES_PASSWORD=<password> \
63+
-e POSTGRES_PASSWORD="$NOP_POSTGRES_PASSWORD" \
5664
-e POSTGRES_DB=nopcommerce \
5765
-p 5432:5432 postgres:16
5866

5967
# Enable the extension required by nopCommerce migrations.
6068
docker exec nop-postgres psql -U nop -d nopcommerce \
6169
-c "CREATE EXTENSION IF NOT EXISTS citext;"
6270
```
71+
72+
The `docker exec` command should print `CREATE EXTENSION`. If it prints `NOTICE: extension "citext" already exists, skipping`, the database is already prepared and you can continue.

content/learning-paths/servers-and-cloud-computing/dotnet-migration-nopcommerce/3-checkout-build-baseline.md

Lines changed: 142 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Create a reproducible Arm baseline before optimization work. This page pins sour
1212

1313
## Clone and pin the source
1414

15-
Pinning the exact tag and commit avoids silent drift in dependencies and behavior.
15+
Pinning the exact tag and commit avoids silent drift in dependencies and behavior. Run these commands on the Cobalt VM. The later commands assume you are in the `nopCommerce` repository root unless a section says otherwise.
1616

1717
```bash
1818
git clone https://github.com/nopSolutions/nopCommerce.git
@@ -22,9 +22,15 @@ git checkout release-4.90.3
2222
git rev-parse --short=10 HEAD # expect 9beda11c42
2323
```
2424

25+
If you open a new terminal later, return to the same repository root before running commands:
26+
27+
```bash
28+
cd ~/nopCommerce # replace with your clone path if different
29+
```
30+
2531
## Restore and build on Arm
2632

27-
Run on the Arm VM to establish a native Arm baseline.
33+
Run the restore and build on the Arm-based VM to establish a native Arm baseline. Restoring first separates package resolution problems from compilation problems, and `--no-restore` makes the build validate the already restored dependency graph.
2834

2935
```bash
3036
# Restore dependencies first so build failures are easier to triage.
@@ -36,16 +42,28 @@ dotnet build src/Presentation/Nop.Web/Nop.Web.csproj -c Release --no-restore
3642

3743
## Start and install nopCommerce
3844

39-
Start the app locally and complete installer setup with PostgreSQL.
45+
Start the app locally and complete installer setup with PostgreSQL. Run this command in one terminal and leave it running; `dotnet run` hosts the web application and keeps the terminal attached to the server logs.
4046

4147
```bash
4248
cd src/Presentation/Nop.Web
4349
dotnet run -c Release --no-build --urls http://0.0.0.0:5000
4450
```
4551

46-
Complete installation with PostgreSQL (`citext` enabled), then verify:
52+
In a browser, open `http://<cobalt-public-ip>:5000` or use an SSH tunnel to reach `http://127.0.0.1:5000`. On the nopCommerce install page, select PostgreSQL and use the database settings from the previous section:
53+
54+
- Server: `127.0.0.1`
55+
- Port: `5432`
56+
- Database: `nopcommerce`
57+
- User: `nop`
58+
- Password: the value of `NOP_POSTGRES_PASSWORD`
59+
60+
Use sample data if you want a stable set of storefront pages for repeatable endpoint tests.
61+
62+
Open a second terminal, return to the repository root, and verify that the app is installed and serving storefront traffic:
4763

4864
```bash
65+
cd ~/nopCommerce # replace with your clone path if different
66+
4967
# Root should return storefront content.
5068
curl -s -o /dev/null -w 'root=%{http_code}\n' http://127.0.0.1:5000/
5169

@@ -57,21 +75,130 @@ Expected after successful install: `root=200`, `install=302`.
5775

5876
## Baseline methodology
5977

60-
Do not benchmark `/install`. Baseline real storefront paths:
78+
Do not benchmark `/install`. Baseline stable storefront paths first, then add cart and attribute-change routes only after you know the valid product IDs, attribute IDs, request method, and anti-forgery token behavior for your installed store.
6179

6280
- `/`
6381
- `/search/`
6482
- `/catalog/searchtermautocomplete`
6583
- `/product/search`
66-
- `/category/products/`
67-
- `/addproducttocart/catalog/...`
68-
- `/addproducttocart/details/...`
69-
- `/shoppingcart/productdetails_attributechange/...`
70-
- `/product/combinations`
71-
- `/cart/estimateshipping`
72-
- `/cart/selectshippingoption`
84+
- Product and category pages from your sample data
85+
- Cart or checkout endpoints from a recorded production-like workflow
86+
87+
Create the endpoint tester in the repository root. The script uses only the Python standard library, submits requests with fixed concurrency, treats non-2xx and non-3xx responses as errors, and writes raw samples plus per-route summaries to JSON.
88+
89+
```python {file_name="test_nopcommerce_endpoints.py"}
90+
#!/usr/bin/env python3
91+
import argparse
92+
import concurrent.futures
93+
import json
94+
import statistics
95+
import time
96+
import urllib.error
97+
import urllib.request
98+
99+
DEFAULT_ROUTES = [
100+
"/",
101+
"/search/",
102+
"/product/search",
103+
"/catalog/searchtermautocomplete?term=a",
104+
]
105+
106+
107+
def percentile(values, percent):
108+
if not values:
109+
return None
110+
data = sorted(values)
111+
index = round((percent / 100) * (len(data) - 1))
112+
return data[index]
113+
114+
115+
def fetch(base_url, route, timeout):
116+
url = base_url.rstrip("/") + route
117+
start = time.perf_counter()
118+
status = None
119+
error = None
120+
121+
try:
122+
with urllib.request.urlopen(url, timeout=timeout) as response:
123+
response.read()
124+
status = response.getcode()
125+
except urllib.error.HTTPError as exc:
126+
exc.read()
127+
status = exc.code
128+
error = f"HTTPError:{exc.code}"
129+
except Exception as exc:
130+
error = type(exc).__name__
131+
132+
elapsed_ms = (time.perf_counter() - start) * 1000
133+
ok = status is not None and 200 <= status < 400
134+
return {
135+
"route": route,
136+
"status": status,
137+
"ok": ok,
138+
"elapsed_ms": round(elapsed_ms, 2),
139+
"error": error,
140+
}
141+
142+
143+
def summarize(samples):
144+
summary = {}
145+
for route in sorted({sample["route"] for sample in samples}):
146+
route_samples = [sample for sample in samples if sample["route"] == route]
147+
successful = [sample["elapsed_ms"] for sample in route_samples if sample["ok"]]
148+
status_counts = {}
149+
for sample in route_samples:
150+
key = str(sample["status"]) if sample["status"] is not None else "no-status"
151+
status_counts[key] = status_counts.get(key, 0) + 1
152+
153+
summary[route] = {
154+
"requests": len(route_samples),
155+
"errors": sum(1 for sample in route_samples if not sample["ok"]),
156+
"status_counts": status_counts,
157+
"p50_ms": round(statistics.median(successful), 2) if successful else None,
158+
"p95_ms": round(percentile(successful, 95), 2) if successful else None,
159+
"max_ms": round(max(successful), 2) if successful else None,
160+
}
161+
return summary
162+
163+
164+
def main():
165+
parser = argparse.ArgumentParser()
166+
parser.add_argument("--base-url", required=True)
167+
parser.add_argument("--concurrency", type=int, default=8)
168+
parser.add_argument("--iterations", type=int, default=20)
169+
parser.add_argument("--json-out", required=True)
170+
parser.add_argument("--route", action="append", default=[])
171+
parser.add_argument("--timeout", type=float, default=10)
172+
args = parser.parse_args()
173+
174+
routes = args.route or DEFAULT_ROUTES
175+
work = [route for route in routes for _ in range(args.iterations)]
176+
177+
with concurrent.futures.ThreadPoolExecutor(max_workers=args.concurrency) as pool:
178+
samples = list(pool.map(lambda route: fetch(args.base_url, route, args.timeout), work))
179+
180+
result = {
181+
"base_url": args.base_url,
182+
"concurrency": args.concurrency,
183+
"iterations_per_route": args.iterations,
184+
"routes": routes,
185+
"summary": summarize(samples),
186+
"samples": samples,
187+
}
188+
189+
with open(args.json_out, "w", encoding="utf-8") as output:
190+
json.dump(result, output, indent=2)
191+
output.write("\n")
192+
193+
print(json.dumps(result["summary"], indent=2))
194+
raise SystemExit(1 if any(not sample["ok"] for sample in samples) else 0)
195+
196+
197+
if __name__ == "__main__":
198+
main()
199+
```
73200

74-
Use the endpoint tester:
201+
Run the endpoint tester from the repository root while the app is still running in the first terminal:
75202

76203
```bash
77204
# Use fixed concurrency and iterations for a repeatable starting point.
@@ -82,6 +209,8 @@ python3 test_nopcommerce_endpoints.py \
82209
--json-out arm_before.json
83210
```
84211

212+
The command prints the per-route summary and writes the raw measurements to `arm_before.json`. Keep that JSON file unchanged; it is the baseline artifact used by the tuning step.
213+
85214
## Baseline quality rules
86215

87216
Use these rules before you compare any optimization result:

content/learning-paths/servers-and-cloud-computing/dotnet-migration-nopcommerce/4-dependency-mapping-manifests.md

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,15 @@ Run dependency discovery before migration changes so you understand direct refer
1212

1313
### 1. Map direct references
1414

15-
Start with project-level references to see what your app explicitly depends on.
15+
Start from the `nopCommerce` repository root. Project-level references show what the app explicitly depends on before NuGet expands the graph. The command searches project files under `src` and prints file names and line numbers so you can jump directly to the owning project.
1616

1717
```bash
1818
rg -n "<PackageReference|<ProjectReference" src
1919
```
2020

2121
### 2. List transitive dependencies
2222

23-
Enumerate the full dependency graph; transitive packages often carry architecture-sensitive constraints.
23+
Enumerate the full dependency graph for the web entry point. Transitive packages often carry architecture-sensitive constraints, so review the output for runtime-specific package names, native library packages, image processing libraries, database providers, and platform-specific assets.
2424

2525
```bash
2626
dotnet list src/Presentation/Nop.Web/Nop.Web.csproj package --include-transitive
@@ -30,25 +30,32 @@ dotnet list src/Presentation/Nop.Web/Nop.Web.csproj package --include-transitive
3030

3131
Generate an SBOM so you can track all components, versions, and exposure surface as a first-class migration artifact. While not strictly necessary for migration purposes, this is a best practice that will save your team time down the road. You can also give this SBOM to an LLM to extract insights about your codebase for you.
3232

33+
The CycloneDX tool is installed as a .NET global tool. Add `~/.dotnet/tools` to `PATH` in the current shell so the `dotnet-CycloneDX` command is available immediately after installation.
34+
3335
```bash
3436
dotnet tool install --global CycloneDX
35-
dotnet CycloneDX src/Presentation/Nop.Web/Nop.Web.csproj -o sbom/
37+
export PATH="$PATH:$HOME/.dotnet/tools"
38+
dotnet-CycloneDX src/Presentation/Nop.Web/Nop.Web.csproj -o sbom/ -rs -F Json
3639
```
3740

38-
If tool installation is blocked, treat `dotnet list --include-transitive` plus `*.deps.json` evidence as a temporary fallback, not the final state.
41+
The `-rs` option includes project references, and `-F Json` writes JSON output that is easier to diff in CI. If tool installation is blocked, treat `dotnet list --include-transitive` plus `*.deps.json` evidence as a temporary fallback, not the final state.
3942

4043
### 4. Inspect package internals for native payloads
4144

42-
Inspect package contents directly to find architecture-specific native binaries.
45+
Inspect package contents directly when a dependency looks architecture-sensitive. NuGet package folders are usually lower case on Linux, so set `PACKAGE_ID` to the folder name under `~/.nuget/packages` and `PACKAGE_VERSION` to the version you want to inspect.
4346

4447
```bash
48+
PACKAGE_ID="replace-with-package-id"
49+
PACKAGE_VERSION="replace-with-version"
50+
NUPKG="$HOME/.nuget/packages/$PACKAGE_ID/$PACKAGE_VERSION/$PACKAGE_ID.$PACKAGE_VERSION.nupkg"
51+
4552
mkdir -p /tmp/nupkg-audit
46-
cp ~/.nuget/packages/<package>/<version>/<package>.<version>.nupkg /tmp/nupkg-audit/
53+
cp "$NUPKG" /tmp/nupkg-audit/
4754
cd /tmp/nupkg-audit
48-
unzip -l <package>.<version>.nupkg | rg "runtimes/|native/"
55+
unzip -l "$(basename "$NUPKG")" | rg "runtimes/|native/"
4956
```
5057

51-
This is how you catch hidden architecture-specific binaries.
58+
If the command prints `runtimes/linux-arm64`, the package already carries an Arm Linux asset. If it prints only `linux-x64`, `win-x64`, or another non-Arm runtime, trace whether that asset is used by nopCommerce before you treat the dependency as portable. If there is no output, the package does not advertise runtime or native payload directories in the `.nupkg`.
5259

5360
## Dependency cascade rule
5461

0 commit comments

Comments
 (0)