Migrate and optimize a .NET nopCommerce application on Microsoft Azure
Introduction
Set up an Arm-based environment
Create a nopCommerce endpoint benchmark baseline on Arm
Generate an SBOM and run .NET dependency discovery before migration
Choose a containerization path for the nopCommerce application
Tune the performance of the .NET application on Azure
Use the Arm MCP Server to optimize .NET performance
Next Steps
Migrate and optimize a .NET nopCommerce application on Microsoft Azure
Introduction
Set up an Arm-based environment
Create a nopCommerce endpoint benchmark baseline on Arm
Generate an SBOM and run .NET dependency discovery before migration
Choose a containerization path for the nopCommerce application
Tune the performance of the .NET application on Azure
Use the Arm MCP Server to optimize .NET performance
Next Steps
Build nopCommerce and create an endpoint baseline
Create a reproducible Arm baseline before optimization work.
You’ll pin the source, verify a clean build, and capture a representative endpoint baseline so you can measure later changes against a known control.
Clone and pin the source
Pinning the exact tag and commit avoids silent drift in dependencies and behavior.
Run the following commands on the Arm-based virtual machine (VM):
git clone https://github.com/nopSolutions/nopCommerce.git
cd nopCommerce
git fetch --tags --prune
git checkout release-4.90.3
git rev-parse --short=10 HEAD # expect 9beda11c42
Later commands in the section assume you’re in the nopCommerce repository root unless stated otherwise.
If you open a new terminal later, return to the same repository root before running commands:
cd ~/nopCommerce # replace with your clone path if different
Restore and build on Arm
Run restore and build on the Arm-based VM to establish a native Arm baseline:
# Restore dependencies first so build failures are easier to triage.
dotnet restore src/Presentation/Nop.Web/Nop.Web.csproj
# Build release binaries without re-restoring packages.
dotnet build src/Presentation/Nop.Web/Nop.Web.csproj -c Release --no-restore
Restoring first separates package resolution problems from compilation problems, and --no-restore makes the build validate the already restored dependency graph.
Start and install nopCommerce
Start the app locally and complete installer setup with PostgreSQL.
Run the following command in one terminal and leave it running:
cd src/Presentation/Nop.Web
dotnet run -c Release --no-build --urls http://0.0.0.0:5000
dotnet run hosts the web application and keeps the terminal attached to the server logs.
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:
- Server:
127.0.0.1 - Port:
5432 - Database:
nopcommerce - User:
nop - Password: the value of
NOP_POSTGRES_PASSWORD
Use sample data if you want a stable set of storefront pages for repeatable endpoint tests.
Open a second terminal, return to the repository root, and verify that the app is installed and serving storefront traffic:
cd ~/nopCommerce # replace with your clone path if different
# Root should return storefront content.
curl -s -o /dev/null -w 'root=%{http_code}\n' http://127.0.0.1:5000/
# Install route should redirect once installation is complete.
curl -s -o /dev/null -w 'install=%{http_code}\n' http://127.0.0.1:5000/install
The outputs after a successful install should be: root=200, install=302.
Run the endpoint baseline benchmark
Don’t 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.
//search//catalog/searchtermautocomplete/product/search- Product and category pages from your sample data
- Cart or checkout endpoints from a recorded production-like workflow
Create the endpoint tester in the repository root:
#!/usr/bin/env python3
import argparse
import concurrent.futures
import json
import statistics
import time
import urllib.error
import urllib.request
DEFAULT_ROUTES = [
"/",
"/search/",
"/product/search",
"/catalog/searchtermautocomplete?term=a",
]
def percentile(values, percent):
if not values:
return None
data = sorted(values)
index = round((percent / 100) * (len(data) - 1))
return data[index]
def fetch(base_url, route, timeout):
url = base_url.rstrip("/") + route
start = time.perf_counter()
status = None
error = None
try:
with urllib.request.urlopen(url, timeout=timeout) as response:
response.read()
status = response.getcode()
except urllib.error.HTTPError as exc:
exc.read()
status = exc.code
error = f"HTTPError:{exc.code}"
except Exception as exc:
error = type(exc).__name__
elapsed_ms = (time.perf_counter() - start) * 1000
ok = status is not None and 200 <= status < 400
return {
"route": route,
"status": status,
"ok": ok,
"elapsed_ms": round(elapsed_ms, 2),
"error": error,
}
def summarize(samples):
summary = {}
for route in sorted({sample["route"] for sample in samples}):
route_samples = [sample for sample in samples if sample["route"] == route]
successful = [sample["elapsed_ms"] for sample in route_samples if sample["ok"]]
status_counts = {}
for sample in route_samples:
key = str(sample["status"]) if sample["status"] is not None else "no-status"
status_counts[key] = status_counts.get(key, 0) + 1
summary[route] = {
"requests": len(route_samples),
"errors": sum(1 for sample in route_samples if not sample["ok"]),
"status_counts": status_counts,
"p50_ms": round(statistics.median(successful), 2) if successful else None,
"p95_ms": round(percentile(successful, 95), 2) if successful else None,
"max_ms": round(max(successful), 2) if successful else None,
}
return summary
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--base-url", required=True)
parser.add_argument("--concurrency", type=int, default=8)
parser.add_argument("--iterations", type=int, default=20)
parser.add_argument("--json-out", required=True)
parser.add_argument("--route", action="append", default=[])
parser.add_argument("--timeout", type=float, default=10)
args = parser.parse_args()
routes = args.route or DEFAULT_ROUTES
work = [route for route in routes for _ in range(args.iterations)]
with concurrent.futures.ThreadPoolExecutor(max_workers=args.concurrency) as pool:
samples = list(pool.map(lambda route: fetch(args.base_url, route, args.timeout), work))
result = {
"base_url": args.base_url,
"concurrency": args.concurrency,
"iterations_per_route": args.iterations,
"routes": routes,
"summary": summarize(samples),
"samples": samples,
}
with open(args.json_out, "w", encoding="utf-8") as output:
json.dump(result, output, indent=2)
output.write("\n")
print(json.dumps(result["summary"], indent=2))
raise SystemExit(1 if any(not sample["ok"] for sample in samples) else 0)
if __name__ == "__main__":
main()
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.
Run the endpoint tester from the repository root while the app is still running in the first terminal:
# Use fixed concurrency and iterations for a repeatable starting point.
python3 test_nopcommerce_endpoints.py \
--base-url http://127.0.0.1:5000 \
--concurrency 8 \
--iterations 20 \
--json-out arm_before.json
The command prints the per-route summary and writes the raw measurements to arm_before.json. Keep that JSON file unchanged because it’s the baseline artifact used in the tuning step.
Baseline quality rules
Use these rules before you compare any optimization result:
- Run at least three baseline trials with identical parameters.
- Keep endpoint order fixed per run (not randomized) when comparing before and after.
- Keep database state and seeded data identical across runs.
- Capture raw JSON for every run and compare medians, not single outliers.
This baseline process becomes the control for every later tuning or code-change decision.
What you’ve accomplished and what’s next
You’ve now pinned the nopCommerce source, verified the build, installed the app, and captured arm_before.json as your endpoint baseline.
Next, you’ll audit dependencies before migration.