Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ icp new <project-name> --subfolder <template-name>
| [bitcoin-starter](./bitcoin-starter/) | Bitcoin integration with balance reading (Rust or Motoko) |
| [proxy](./proxy/) | A pre-built proxy canister for use with `icp canister call --proxy` |
| [static-website](./static-website/) | A static website deployed to an asset canister |
| [blazor-hello-world](./blazor-hello-world/) | Full-stack Blazor WebAssembly (.NET 10) frontend with a Motoko backend |

## Contributing

Expand Down
Binary file added blazor-hello-world/ICP-BLAZOR-HELLO-WORLD.bmp
Binary file not shown.
169 changes: 169 additions & 0 deletions blazor-hello-world/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# ICP Blazor Hello World
full medium article :
https://medium.com/@mitnick2012/your-net-blazor-skills-just-became-blockchain-skills-c41c14d87b44

A hello world template combining **Blazor WebAssembly (.NET 10)** on the frontend and **Motoko** on the backend, deployed fully on-chain on the **Internet Computer (ICP)**.

Hello World Blazor Demo App:

![Alt text](ICP-BLAZOR-HELLO-WORLD.bmp)

> Scaffold a new project with [`icp new`](https://cli.internetcomputer.org/1.0/guides/creating-templates/):
>
> ```bash
> # from the team template collection
> icp new my-app --git https://github.com/mitnick2012/icp-cli-templates --subfolder blazor-hello-world
>
> # from a published git repo containing only this template
> icp new my-app --git https://github.com/mitnick2012/icp-blazor-wasm-template
>
> # or from a local checkout (the folder with cargo-generate.toml)
> icp new my-app --path ./icp-blazor-hello
> ```
>
> `my-app` becomes the canister names `my-app-backend` / `my-app-frontend` and the
> .NET project name `MyApp`.

## Stack

| Layer | Technology |
|----------|-------------------------------------|
| Frontend | Blazor WebAssembly (.NET 10) |
| Backend | Motoko canister |
| Bridge | @icp-sdk/core (webpack bundle) |
| Platform | Internet Computer (ICP) |
| CLI | icp-cli |

## Project Structure

```
icp-blazor-hello/
├── cargo-generate.toml # icp-cli template config (cargo-generate)
├── icp.yaml # icp-cli project config
├── .gitignore
├── scripts/
│ └── fix-wsl-keyring.sh # headless identity helper for WSL
├── backend/
│ ├── canister.yaml # backend canister config (name: {{ project-name }}-backend)
│ ├── backend.did # Candid interface (semicolons required!)
│ └── src/
│ └── main.mo # Motoko canister
└── frontend/
├── canister.yaml # frontend canister config (name: {{ project-name }}-frontend)
├── vendor/
│ └── assetstorage.wasm.gz # pinned asset canister WASM (no build-time download)
└── {{ project-name }}/
├── {{ project-name }}.csproj # .NET 10 Blazor WASM (AssemblyName = {{ project-name | pascal_case }})
├── Program.cs
├── App.razor
├── _Imports.razor # required — Blazor namespace imports
├── package.json # webpack + @icp-sdk/core
├── webpack.config.js # bundles icpAgent.ts → wwwroot/icpAgent.js
├── tsconfig.json
├── src/
│ └── icpAgent.ts # TypeScript ICP agent bridge
├── Layout/
│ └── MainLayout.razor
├── Pages/
│ └── Home.razor # main UI with canister calls
├── Services/
│ └── IcpAgentService.cs # C# → JS interop service
└── wwwroot/
├── index.html
└── app.css
```

## How it works

```
Home.razor (C#)
→ IcpAgentService.cs (IJSRuntime)
→ window.IcpAgent.* (webpack bundle, defer loaded)
→ @icp-sdk/core (Agent module: https://js.icp.build/core/latest/libs/agent)
→ Motoko backend canister on ICP
```

The key insight: use **webpack** to bundle `@icp-sdk/core` into a plain JS file
loaded with `defer`, not as an ES module. This avoids the race condition between
the module loader and Blazor's JS interop system.

## Prerequisites

```bash
# icp-cli
npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm

# Motoko toolchain
npm install -g ic-mops
```

### .NET 10 SDK

**Method 1: Ubuntu 24.04 LTS or newer (APT)**
```bash
sudo apt update
sudo apt install -y dotnet-sdk-10.0
```

> **Ubuntu 22.04 LTS?** Register the backports PPA first:
> ```bash
> sudo add-apt-repository ppa:dotnet/backports
> sudo apt update
> sudo apt install -y dotnet-sdk-10.0
> ```

**Method 2: Official Microsoft script (recommended for non-Ubuntu or if APT fails)**
```bash
wget https://dot.net/v1/dotnet-install.sh -O dotnet-install.sh
chmod +x dotnet-install.sh
./dotnet-install.sh --channel 10.0
```

After installing, add .NET to your PATH:
```bash
echo 'export PATH="$HOME/.dotnet:$PATH"' >> ~/.bashrc
source ~/.bashrc
```

### Blazor WASM workload (required)
```bash
dotnet workload install wasm-tools
```

### Remove conflicting `icp` binary (Ubuntu/Debian)
Ubuntu ships a package called `renameutils` that installs its own unrelated `icp` binary.
Remove it before installing icp-cli:
```bash
sudo apt remove renameutils -y
```

Verify the correct `icp` is active:
```bash
icp --version # should show icp-cli, not renameutils
```

## Run locally

```bash
# 1. Start the local ICP network (gateway on the standard port 8000,
# see icp.yaml networks.gateway.port)
icp network start -d
icp network status # Gateway Url: http://localhost:8000/

# 2. Deploy both canisters
icp deploy

# 3. Open the frontend URL printed by icp deploy (friendly domain), e.g.
# {{ project-name }}-frontend: http://{{ project-name }}-frontend.local.localhost:8000/
# The canister-id form works too: http://<frontend-canister-id>.localhost:8000/
```

## Deploy to mainnet

```bash
icp deploy --network ic
```

## License

MIT
5 changes: 5 additions & 0 deletions blazor-hello-world/backend/backend.did
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
service : {
getGreeting : () -> (text) query;
setGreeting : (text) -> (text);
hello : (text) -> (text) query;
}
7 changes: 7 additions & 0 deletions blazor-hello-world/backend/canister.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# yaml-language-server: $schema=https://github.com/dfinity/icp-cli/raw/refs/tags/v1.3.0/docs/schemas/canister-yaml-schema.json
name: "{{ project-name }}-backend"
recipe:
type: "@dfinity/motoko@v4.1.0"
configuration:
main: src/main.mo
candid: backend.did
2 changes: 2 additions & 0 deletions blazor-hello-world/backend/mops.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[toolchain]
moc = "1.3.0"
18 changes: 18 additions & 0 deletions blazor-hello-world/backend/src/main.mo
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
persistent actor {

stable var greeting : Text = "Hello from {{ project-name }}!";

public query func getGreeting() : async Text {
return greeting;
};

public func setGreeting(name : Text) : async Text {
greeting := "Hello, " # name # "! Welcome to ICP + Blazor!";
return greeting;
};

public query func hello(name : Text) : async Text {
return "Hello, " # name # "!";
};

};
26 changes: 26 additions & 0 deletions blazor-hello-world/cargo-generate.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[template]
name = "Hello World ICP app"
description = "Full-stack ICP app: Blazor WASM (.NET 10) frontend + Motoko backend, deployed fully on-chain"

# Never run Liquid over binary assets (non-UTF8 files are copied verbatim anyway).
exclude = ["frontend/vendor/**"]

# Never copy local build/test litter into generated projects. (cargo-generate
# does not read .gitignore, so anything git ignores is NOT automatically
# excluded here.) `ignore` matches whole files/folders, no wildcards, so every
# path below is spelled out. Missing paths are ignored.
ignore = [
# ICP CLI runtime cache (can hold filenames with raw `{{ project-name }}`
# placeholders, e.g. cache/artifacts/{{ project-name }}-backend, which would
# otherwise make scaffolding fail with "invalid syntax").
".icp",
"frontend/{{project-name}}/bin",
"frontend/{{project-name}}/obj",
"frontend/{{project-name}}/node_modules",
Comment on lines +17 to +19
"frontend/{{project-name}}/package-lock.json",
# Log files from manual test runs.
"frontend/{{project-name}}/npm-install.log",
# Webpack output is regenerated by `npm run build` during deploy.
"frontend/{{project-name}}/wwwroot/icpAgent.js",
"frontend/{{project-name}}/wwwroot/icpAgent.js.LICENSE.txt",
]
28 changes: 28 additions & 0 deletions blazor-hello-world/frontend/canister.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# yaml-language-server: $schema=https://github.com/dfinity/icp-cli/raw/refs/tags/v1.3.0/docs/schemas/canister-yaml-schema.json
name: "{{ project-name }}-frontend"
build:
steps:
# 1. Build the TypeScript ICP agent bundle, then the Blazor WASM app.
- type: script
commands:
- cd {{project-name}} && npm install && npm run build
- dotnet publish {{project-name}}/{{project-name}}.csproj -c Release
# 2. Use the asset canister WASM vendored next to this file instead of
# fetching it from GitHub on every build. The `@dfinity/asset-canister`
# recipe downloaded it from
# https://github.com/dfinity/sdk/releases/latest/download/assetstorage.wasm.gz
# with no sha256, so icp-cli re-downloaded it on every deploy and any
# network hiccup failed the build ("failed to fetch wasm file").
# See vendor/README.md for how to refresh this file.
- type: pre-built
path: vendor/assetstorage.wasm.gz
sha256: 04e565b3425fe7510ee16b02adcfe3f01abc9a2725c82a21cb08969241debd62
sync:
steps:
# Upload the published wwwroot to the asset canister (same plugin step the
# @dfinity/asset-canister recipe emits).
- type: plugin
url: https://github.com/dfinity/certified-assets/releases/download/migration-v2.2.1-6b48585/sync_plugin.wasm
sha256: ca7cb5666c30d2875f8d5e10535f8a53f97a86c79c263f7d5bdac2fdd1bbf83c
dirs:
- "{{project-name}}/bin/Release/net10.0/publish/wwwroot"
45 changes: 45 additions & 0 deletions blazor-hello-world/frontend/vendor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Vendored asset canister

`assetstorage.wasm.gz` is the prebuilt **asset canister** module that serves the
Blazor frontend. It is checked in so `icp build` / `icp deploy` never has to
download it:

| | |
|---|---|
| Source | `https://github.com/dfinity/sdk/releases/download/0.32.0/assetstorage.wasm.gz` (dfx release 0.32.0) |
| Size | 465,172 bytes |
| sha256 | `04e565b3425fe7510ee16b02adcfe3f01abc9a2725c82a21cb08969241debd62` |

## Why it is vendored

`frontend/canister.yaml` used to use the `@dfinity/asset-canister@v2.2.1` recipe.
That recipe's build step fetches the module from
`https://github.com/dfinity/sdk/releases/latest/download/assetstorage.wasm.gz`
and does **not** supply a `sha256`, and icp-cli only caches a remote WASM when a
checksum is given (`Remote without sha256: always downloads`). Every deploy
therefore re-downloaded ~465 KB through GitHub's `releases/latest` redirect. When
that request failed at the transport level (reset/TLS/DNS/timeout) the build
aborted with:

```
[frontend] ✘ Failed to build canister: failed to fetch wasm file
```

Keeping the module in the repository makes the build hermetic: the
`pre-built` step reads it from disk, verifies the checksum above, and no network
access is needed for the canister WASM.

## Refreshing the module

Pick a dfx release tag from <https://github.com/dfinity/sdk/releases>, download
it and update `sha256` in `frontend/canister.yaml`:

```bash
cd frontend
curl -sL -o vendor/assetstorage.wasm.gz \
https://github.com/dfinity/sdk/releases/download/<DFX_VERSION>/assetstorage.wasm.gz
sha256sum vendor/assetstorage.wasm.gz # copy this into canister.yaml
```

If the checksum in `canister.yaml` does not match the file, the build fails with
`checksum mismatch, expected: ..., actual: ...`.
Binary file not shown.
12 changes: 12 additions & 0 deletions blazor-hello-world/frontend/{{project-name}}/App.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@using Microsoft.AspNetCore.Components.Routing
@using {{ project-name | pascal_case }}.Layout

<Router AppAssembly="typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
<NotFound>
<p>Page not found.</p>
</NotFound>
</Router>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@inherits LayoutComponentBase

<main>
@Body
</main>
Loading
Loading