diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index e1698c5..e79a84e 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -14,6 +14,9 @@ jobs:
dotnet-version: |
8.0.x
9.0.x
+ 10.0.x
+ - name: Installl WASM workloads
+ run: dotnet workload install wasm-tools --skip-manifest-update
- name: Install dependencies
run: dotnet restore
- name: Locate MSBuild with vswhere
@@ -36,6 +39,14 @@ jobs:
& $env:MSBUILD AspNetCore.sln /p:Configuration=Debug /p:Platform="Any CPU"
- name: Test Zapto.AspNetCore.NetFx
run: dotnet test --no-restore --no-build --verbosity normal tests/Zapto.AspNetCore.NetFx.Tests/Zapto.AspNetCore.NetFx.Tests.csproj
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '22'
+ - name: Install wrangler
+ run: npm install -g wrangler
+ - name: Test Zapto.AspNetCore.Wasm
+ run: dotnet test --verbosity normal tests/Zapto.AspNetCore.Wasm.Tests/Zapto.AspNetCore.Wasm.Tests.csproj
- name: Publish
uses: GerardSmit/publish-nuget@v4.0.2
if: github.ref == 'refs/heads/main'
@@ -43,6 +54,9 @@ jobs:
NUGET_KEY: ${{secrets.NUGET_API_KEY}}
VERSION_FILE_PATH: src/Directory.Build.props
PROJECT_FILE_PATH: |
+ src/Zapto.AspNetCore.Wasm.Interop/Zapto.AspNetCore.Wasm.Interop.csproj
+ src/Zapto.AspNetCore.Wasm/Zapto.AspNetCore.Wasm.csproj
src/Zapto.AspNetCore.Polyfill/Zapto.AspNetCore.Polyfill.csproj
src/Zapto.AspNetCore.NetFx/Zapto.AspNetCore.NetFx.csproj
+ sdk/Zapto.AspNetCore.CloudFlare.SDK/Zapto.AspNetCore.CloudFlare.SDK.csproj
diff --git a/AspNetCore.sln b/AspNetCore.sln
index 9f10d9f..88efd2a 100644
--- a/AspNetCore.sln
+++ b/AspNetCore.sln
@@ -1,46 +1,242 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AspNetCore", "AspNetCore", "{7F209FD2-1190-4721-9A08-9569AB4AA8D5}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Zapto.AspNetCore.NetFx", "src\Zapto.AspNetCore.NetFx\Zapto.AspNetCore.NetFx.csproj", "{C3CEF02D-43F9-4C28-BC1D-18ED3D1FC0E4}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sandbox", "Sandbox", "{DD6845EA-4012-4E0A-8B78-8BAE5FF6967B}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebFormsApp", "sandbox\WebFormsApp\WebFormsApp.csproj", "{77FA9DA3-FB50-46EC-B9B8-905121594EA1}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{B3DA0F06-4511-4791-BB12-A84655F02BFA}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Zapto.AspNetCore.NetFx.Tests", "tests\Zapto.AspNetCore.NetFx.Tests\Zapto.AspNetCore.NetFx.Tests.csproj", "{305C7374-1A54-4409-B527-968D49B258DF}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Zapto.AspNetCore.Polyfill", "src\Zapto.AspNetCore.Polyfill\Zapto.AspNetCore.Polyfill.csproj", "{507A2467-52E9-4181-8414-426AC45E01B3}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Release|Any CPU = Release|Any CPU
- EndGlobalSection
- GlobalSection(NestedProjects) = preSolution
- {C3CEF02D-43F9-4C28-BC1D-18ED3D1FC0E4} = {7F209FD2-1190-4721-9A08-9569AB4AA8D5}
- {77FA9DA3-FB50-46EC-B9B8-905121594EA1} = {DD6845EA-4012-4E0A-8B78-8BAE5FF6967B}
- {305C7374-1A54-4409-B527-968D49B258DF} = {B3DA0F06-4511-4791-BB12-A84655F02BFA}
- {507A2467-52E9-4181-8414-426AC45E01B3} = {7F209FD2-1190-4721-9A08-9569AB4AA8D5}
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {C3CEF02D-43F9-4C28-BC1D-18ED3D1FC0E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {C3CEF02D-43F9-4C28-BC1D-18ED3D1FC0E4}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {C3CEF02D-43F9-4C28-BC1D-18ED3D1FC0E4}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {C3CEF02D-43F9-4C28-BC1D-18ED3D1FC0E4}.Release|Any CPU.Build.0 = Release|Any CPU
- {77FA9DA3-FB50-46EC-B9B8-905121594EA1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {77FA9DA3-FB50-46EC-B9B8-905121594EA1}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {77FA9DA3-FB50-46EC-B9B8-905121594EA1}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {77FA9DA3-FB50-46EC-B9B8-905121594EA1}.Release|Any CPU.Build.0 = Release|Any CPU
- {305C7374-1A54-4409-B527-968D49B258DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {305C7374-1A54-4409-B527-968D49B258DF}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {305C7374-1A54-4409-B527-968D49B258DF}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {305C7374-1A54-4409-B527-968D49B258DF}.Release|Any CPU.Build.0 = Release|Any CPU
- {507A2467-52E9-4181-8414-426AC45E01B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {507A2467-52E9-4181-8414-426AC45E01B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {507A2467-52E9-4181-8414-426AC45E01B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {507A2467-52E9-4181-8414-426AC45E01B3}.Release|Any CPU.Build.0 = Release|Any CPU
- EndGlobalSection
-EndGlobal
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AspNetCore", "AspNetCore", "{7F209FD2-1190-4721-9A08-9569AB4AA8D5}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Zapto.AspNetCore.NetFx", "src\Zapto.AspNetCore.NetFx\Zapto.AspNetCore.NetFx.csproj", "{C3CEF02D-43F9-4C28-BC1D-18ED3D1FC0E4}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sandbox", "Sandbox", "{DD6845EA-4012-4E0A-8B78-8BAE5FF6967B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebFormsApp", "sandbox\WebFormsApp\WebFormsApp.csproj", "{77FA9DA3-FB50-46EC-B9B8-905121594EA1}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{B3DA0F06-4511-4791-BB12-A84655F02BFA}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Zapto.AspNetCore.NetFx.Tests", "tests\Zapto.AspNetCore.NetFx.Tests\Zapto.AspNetCore.NetFx.Tests.csproj", "{305C7374-1A54-4409-B527-968D49B258DF}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Zapto.AspNetCore.Polyfill", "src\Zapto.AspNetCore.Polyfill\Zapto.AspNetCore.Polyfill.csproj", "{507A2467-52E9-4181-8414-426AC45E01B3}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Zapto.AspNetCore.Wasm", "src\Zapto.AspNetCore.Wasm\Zapto.AspNetCore.Wasm.csproj", "{A8E7D4F1-2B3C-4D5E-9F6A-1B2C3D4E5F6A}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SDK", "SDK", "{E1F2A3B4-C5D6-4E7F-8A9B-0C1D2E3F4A5B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Zapto.AspNetCore.CloudFlare.SDK", "sdk\Zapto.AspNetCore.CloudFlare.SDK\Zapto.AspNetCore.CloudFlare.SDK.csproj", "{B1C2D3E4-F5A6-4B7C-8D9E-0A1B2C3D4E5F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WasmApp", "sandbox\WasmApp\WasmApp.csproj", "{C2D3E4F5-A6B7-4C8D-9E0F-1A2B3C4D5E6F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Zapto.AspNetCore.Wasm.Tests", "tests\Zapto.AspNetCore.Wasm.Tests\Zapto.AspNetCore.Wasm.Tests.csproj", "{D3E4F5A6-B7C8-4D9E-0F1A-2B3C4D5E6F7A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Zapto.AspNetCore.Wasm.SourceGenerator", "src\Zapto.AspNetCore.Wasm.SourceGenerator\Zapto.AspNetCore.Wasm.SourceGenerator.csproj", "{7BEC1F6E-3E97-489C-A134-ECCEA93FC93F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Zapto.AspNetCore.Wasm.SourceGenerator.Tests", "tests\Zapto.AspNetCore.Wasm.SourceGenerator.Tests\Zapto.AspNetCore.Wasm.SourceGenerator.Tests.csproj", "{1333E8B4-070C-4C70-A9F2-070CAA66CDAD}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Zapto.AspNetCore.Wasm.Interop", "src\Zapto.AspNetCore.Wasm.Interop\Zapto.AspNetCore.Wasm.Interop.csproj", "{3827F320-31F0-4AE3-A048-8157C66DBEC8}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Wasm", "Wasm", "{36B2818B-95AB-47ED-96AC-563A115C16A8}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CloudFlare", "CloudFlare", "{75661D3A-C7D4-4032-8E88-5949C22550EC}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Zapto.AspNetCore.CloudFlare", "src\Zapto.AspNetCore.CloudFlare\Zapto.AspNetCore.CloudFlare.csproj", "{B42E9FA8-1B6D-4CF0-BAFB-9CF8E86DF414}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WasmApp.Library", "sandbox\WasmApp.Library\WasmApp.Library.csproj", "{3A4FB731-98D3-4006-A63C-5DA356FEF674}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Zapto.AspNetCore.CloudFlare.SDK.Tasks", "sdk\Zapto.AspNetCore.CloudFlare.SDK.Tasks\Zapto.AspNetCore.CloudFlare.SDK.Tasks.csproj", "{20818784-C3EC-4554-95EB-B92CE9E58C7A}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
+ Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {C3CEF02D-43F9-4C28-BC1D-18ED3D1FC0E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C3CEF02D-43F9-4C28-BC1D-18ED3D1FC0E4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C3CEF02D-43F9-4C28-BC1D-18ED3D1FC0E4}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {C3CEF02D-43F9-4C28-BC1D-18ED3D1FC0E4}.Debug|x64.Build.0 = Debug|Any CPU
+ {C3CEF02D-43F9-4C28-BC1D-18ED3D1FC0E4}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C3CEF02D-43F9-4C28-BC1D-18ED3D1FC0E4}.Debug|x86.Build.0 = Debug|Any CPU
+ {C3CEF02D-43F9-4C28-BC1D-18ED3D1FC0E4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C3CEF02D-43F9-4C28-BC1D-18ED3D1FC0E4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C3CEF02D-43F9-4C28-BC1D-18ED3D1FC0E4}.Release|x64.ActiveCfg = Release|Any CPU
+ {C3CEF02D-43F9-4C28-BC1D-18ED3D1FC0E4}.Release|x64.Build.0 = Release|Any CPU
+ {C3CEF02D-43F9-4C28-BC1D-18ED3D1FC0E4}.Release|x86.ActiveCfg = Release|Any CPU
+ {C3CEF02D-43F9-4C28-BC1D-18ED3D1FC0E4}.Release|x86.Build.0 = Release|Any CPU
+ {77FA9DA3-FB50-46EC-B9B8-905121594EA1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {77FA9DA3-FB50-46EC-B9B8-905121594EA1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {77FA9DA3-FB50-46EC-B9B8-905121594EA1}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {77FA9DA3-FB50-46EC-B9B8-905121594EA1}.Debug|x64.Build.0 = Debug|Any CPU
+ {77FA9DA3-FB50-46EC-B9B8-905121594EA1}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {77FA9DA3-FB50-46EC-B9B8-905121594EA1}.Debug|x86.Build.0 = Debug|Any CPU
+ {77FA9DA3-FB50-46EC-B9B8-905121594EA1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {77FA9DA3-FB50-46EC-B9B8-905121594EA1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {77FA9DA3-FB50-46EC-B9B8-905121594EA1}.Release|x64.ActiveCfg = Release|Any CPU
+ {77FA9DA3-FB50-46EC-B9B8-905121594EA1}.Release|x64.Build.0 = Release|Any CPU
+ {77FA9DA3-FB50-46EC-B9B8-905121594EA1}.Release|x86.ActiveCfg = Release|Any CPU
+ {77FA9DA3-FB50-46EC-B9B8-905121594EA1}.Release|x86.Build.0 = Release|Any CPU
+ {305C7374-1A54-4409-B527-968D49B258DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {305C7374-1A54-4409-B527-968D49B258DF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {305C7374-1A54-4409-B527-968D49B258DF}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {305C7374-1A54-4409-B527-968D49B258DF}.Debug|x64.Build.0 = Debug|Any CPU
+ {305C7374-1A54-4409-B527-968D49B258DF}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {305C7374-1A54-4409-B527-968D49B258DF}.Debug|x86.Build.0 = Debug|Any CPU
+ {305C7374-1A54-4409-B527-968D49B258DF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {305C7374-1A54-4409-B527-968D49B258DF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {305C7374-1A54-4409-B527-968D49B258DF}.Release|x64.ActiveCfg = Release|Any CPU
+ {305C7374-1A54-4409-B527-968D49B258DF}.Release|x64.Build.0 = Release|Any CPU
+ {305C7374-1A54-4409-B527-968D49B258DF}.Release|x86.ActiveCfg = Release|Any CPU
+ {305C7374-1A54-4409-B527-968D49B258DF}.Release|x86.Build.0 = Release|Any CPU
+ {507A2467-52E9-4181-8414-426AC45E01B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {507A2467-52E9-4181-8414-426AC45E01B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {507A2467-52E9-4181-8414-426AC45E01B3}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {507A2467-52E9-4181-8414-426AC45E01B3}.Debug|x64.Build.0 = Debug|Any CPU
+ {507A2467-52E9-4181-8414-426AC45E01B3}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {507A2467-52E9-4181-8414-426AC45E01B3}.Debug|x86.Build.0 = Debug|Any CPU
+ {507A2467-52E9-4181-8414-426AC45E01B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {507A2467-52E9-4181-8414-426AC45E01B3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {507A2467-52E9-4181-8414-426AC45E01B3}.Release|x64.ActiveCfg = Release|Any CPU
+ {507A2467-52E9-4181-8414-426AC45E01B3}.Release|x64.Build.0 = Release|Any CPU
+ {507A2467-52E9-4181-8414-426AC45E01B3}.Release|x86.ActiveCfg = Release|Any CPU
+ {507A2467-52E9-4181-8414-426AC45E01B3}.Release|x86.Build.0 = Release|Any CPU
+ {A8E7D4F1-2B3C-4D5E-9F6A-1B2C3D4E5F6A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A8E7D4F1-2B3C-4D5E-9F6A-1B2C3D4E5F6A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A8E7D4F1-2B3C-4D5E-9F6A-1B2C3D4E5F6A}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {A8E7D4F1-2B3C-4D5E-9F6A-1B2C3D4E5F6A}.Debug|x64.Build.0 = Debug|Any CPU
+ {A8E7D4F1-2B3C-4D5E-9F6A-1B2C3D4E5F6A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {A8E7D4F1-2B3C-4D5E-9F6A-1B2C3D4E5F6A}.Debug|x86.Build.0 = Debug|Any CPU
+ {A8E7D4F1-2B3C-4D5E-9F6A-1B2C3D4E5F6A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A8E7D4F1-2B3C-4D5E-9F6A-1B2C3D4E5F6A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A8E7D4F1-2B3C-4D5E-9F6A-1B2C3D4E5F6A}.Release|x64.ActiveCfg = Release|Any CPU
+ {A8E7D4F1-2B3C-4D5E-9F6A-1B2C3D4E5F6A}.Release|x64.Build.0 = Release|Any CPU
+ {A8E7D4F1-2B3C-4D5E-9F6A-1B2C3D4E5F6A}.Release|x86.ActiveCfg = Release|Any CPU
+ {A8E7D4F1-2B3C-4D5E-9F6A-1B2C3D4E5F6A}.Release|x86.Build.0 = Release|Any CPU
+ {B1C2D3E4-F5A6-4B7C-8D9E-0A1B2C3D4E5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B1C2D3E4-F5A6-4B7C-8D9E-0A1B2C3D4E5F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B1C2D3E4-F5A6-4B7C-8D9E-0A1B2C3D4E5F}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {B1C2D3E4-F5A6-4B7C-8D9E-0A1B2C3D4E5F}.Debug|x64.Build.0 = Debug|Any CPU
+ {B1C2D3E4-F5A6-4B7C-8D9E-0A1B2C3D4E5F}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {B1C2D3E4-F5A6-4B7C-8D9E-0A1B2C3D4E5F}.Debug|x86.Build.0 = Debug|Any CPU
+ {B1C2D3E4-F5A6-4B7C-8D9E-0A1B2C3D4E5F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B1C2D3E4-F5A6-4B7C-8D9E-0A1B2C3D4E5F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B1C2D3E4-F5A6-4B7C-8D9E-0A1B2C3D4E5F}.Release|x64.ActiveCfg = Release|Any CPU
+ {B1C2D3E4-F5A6-4B7C-8D9E-0A1B2C3D4E5F}.Release|x64.Build.0 = Release|Any CPU
+ {B1C2D3E4-F5A6-4B7C-8D9E-0A1B2C3D4E5F}.Release|x86.ActiveCfg = Release|Any CPU
+ {B1C2D3E4-F5A6-4B7C-8D9E-0A1B2C3D4E5F}.Release|x86.Build.0 = Release|Any CPU
+ {C2D3E4F5-A6B7-4C8D-9E0F-1A2B3C4D5E6F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C2D3E4F5-A6B7-4C8D-9E0F-1A2B3C4D5E6F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C2D3E4F5-A6B7-4C8D-9E0F-1A2B3C4D5E6F}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {C2D3E4F5-A6B7-4C8D-9E0F-1A2B3C4D5E6F}.Debug|x64.Build.0 = Debug|Any CPU
+ {C2D3E4F5-A6B7-4C8D-9E0F-1A2B3C4D5E6F}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C2D3E4F5-A6B7-4C8D-9E0F-1A2B3C4D5E6F}.Debug|x86.Build.0 = Debug|Any CPU
+ {C2D3E4F5-A6B7-4C8D-9E0F-1A2B3C4D5E6F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C2D3E4F5-A6B7-4C8D-9E0F-1A2B3C4D5E6F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C2D3E4F5-A6B7-4C8D-9E0F-1A2B3C4D5E6F}.Release|x64.ActiveCfg = Release|Any CPU
+ {C2D3E4F5-A6B7-4C8D-9E0F-1A2B3C4D5E6F}.Release|x64.Build.0 = Release|Any CPU
+ {C2D3E4F5-A6B7-4C8D-9E0F-1A2B3C4D5E6F}.Release|x86.ActiveCfg = Release|Any CPU
+ {C2D3E4F5-A6B7-4C8D-9E0F-1A2B3C4D5E6F}.Release|x86.Build.0 = Release|Any CPU
+ {D3E4F5A6-B7C8-4D9E-0F1A-2B3C4D5E6F7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D3E4F5A6-B7C8-4D9E-0F1A-2B3C4D5E6F7A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D3E4F5A6-B7C8-4D9E-0F1A-2B3C4D5E6F7A}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {D3E4F5A6-B7C8-4D9E-0F1A-2B3C4D5E6F7A}.Debug|x64.Build.0 = Debug|Any CPU
+ {D3E4F5A6-B7C8-4D9E-0F1A-2B3C4D5E6F7A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {D3E4F5A6-B7C8-4D9E-0F1A-2B3C4D5E6F7A}.Debug|x86.Build.0 = Debug|Any CPU
+ {D3E4F5A6-B7C8-4D9E-0F1A-2B3C4D5E6F7A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D3E4F5A6-B7C8-4D9E-0F1A-2B3C4D5E6F7A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D3E4F5A6-B7C8-4D9E-0F1A-2B3C4D5E6F7A}.Release|x64.ActiveCfg = Release|Any CPU
+ {D3E4F5A6-B7C8-4D9E-0F1A-2B3C4D5E6F7A}.Release|x64.Build.0 = Release|Any CPU
+ {D3E4F5A6-B7C8-4D9E-0F1A-2B3C4D5E6F7A}.Release|x86.ActiveCfg = Release|Any CPU
+ {D3E4F5A6-B7C8-4D9E-0F1A-2B3C4D5E6F7A}.Release|x86.Build.0 = Release|Any CPU
+ {7BEC1F6E-3E97-489C-A134-ECCEA93FC93F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7BEC1F6E-3E97-489C-A134-ECCEA93FC93F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7BEC1F6E-3E97-489C-A134-ECCEA93FC93F}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {7BEC1F6E-3E97-489C-A134-ECCEA93FC93F}.Debug|x64.Build.0 = Debug|Any CPU
+ {7BEC1F6E-3E97-489C-A134-ECCEA93FC93F}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {7BEC1F6E-3E97-489C-A134-ECCEA93FC93F}.Debug|x86.Build.0 = Debug|Any CPU
+ {7BEC1F6E-3E97-489C-A134-ECCEA93FC93F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7BEC1F6E-3E97-489C-A134-ECCEA93FC93F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7BEC1F6E-3E97-489C-A134-ECCEA93FC93F}.Release|x64.ActiveCfg = Release|Any CPU
+ {7BEC1F6E-3E97-489C-A134-ECCEA93FC93F}.Release|x64.Build.0 = Release|Any CPU
+ {7BEC1F6E-3E97-489C-A134-ECCEA93FC93F}.Release|x86.ActiveCfg = Release|Any CPU
+ {7BEC1F6E-3E97-489C-A134-ECCEA93FC93F}.Release|x86.Build.0 = Release|Any CPU
+ {1333E8B4-070C-4C70-A9F2-070CAA66CDAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1333E8B4-070C-4C70-A9F2-070CAA66CDAD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1333E8B4-070C-4C70-A9F2-070CAA66CDAD}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {1333E8B4-070C-4C70-A9F2-070CAA66CDAD}.Debug|x64.Build.0 = Debug|Any CPU
+ {1333E8B4-070C-4C70-A9F2-070CAA66CDAD}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {1333E8B4-070C-4C70-A9F2-070CAA66CDAD}.Debug|x86.Build.0 = Debug|Any CPU
+ {1333E8B4-070C-4C70-A9F2-070CAA66CDAD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1333E8B4-070C-4C70-A9F2-070CAA66CDAD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1333E8B4-070C-4C70-A9F2-070CAA66CDAD}.Release|x64.ActiveCfg = Release|Any CPU
+ {1333E8B4-070C-4C70-A9F2-070CAA66CDAD}.Release|x64.Build.0 = Release|Any CPU
+ {1333E8B4-070C-4C70-A9F2-070CAA66CDAD}.Release|x86.ActiveCfg = Release|Any CPU
+ {1333E8B4-070C-4C70-A9F2-070CAA66CDAD}.Release|x86.Build.0 = Release|Any CPU
+ {3827F320-31F0-4AE3-A048-8157C66DBEC8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3827F320-31F0-4AE3-A048-8157C66DBEC8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3827F320-31F0-4AE3-A048-8157C66DBEC8}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {3827F320-31F0-4AE3-A048-8157C66DBEC8}.Debug|x64.Build.0 = Debug|Any CPU
+ {3827F320-31F0-4AE3-A048-8157C66DBEC8}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {3827F320-31F0-4AE3-A048-8157C66DBEC8}.Debug|x86.Build.0 = Debug|Any CPU
+ {3827F320-31F0-4AE3-A048-8157C66DBEC8}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3827F320-31F0-4AE3-A048-8157C66DBEC8}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3827F320-31F0-4AE3-A048-8157C66DBEC8}.Release|x64.ActiveCfg = Release|Any CPU
+ {3827F320-31F0-4AE3-A048-8157C66DBEC8}.Release|x64.Build.0 = Release|Any CPU
+ {3827F320-31F0-4AE3-A048-8157C66DBEC8}.Release|x86.ActiveCfg = Release|Any CPU
+ {3827F320-31F0-4AE3-A048-8157C66DBEC8}.Release|x86.Build.0 = Release|Any CPU
+ {B42E9FA8-1B6D-4CF0-BAFB-9CF8E86DF414}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B42E9FA8-1B6D-4CF0-BAFB-9CF8E86DF414}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B42E9FA8-1B6D-4CF0-BAFB-9CF8E86DF414}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {B42E9FA8-1B6D-4CF0-BAFB-9CF8E86DF414}.Debug|x64.Build.0 = Debug|Any CPU
+ {B42E9FA8-1B6D-4CF0-BAFB-9CF8E86DF414}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {B42E9FA8-1B6D-4CF0-BAFB-9CF8E86DF414}.Debug|x86.Build.0 = Debug|Any CPU
+ {B42E9FA8-1B6D-4CF0-BAFB-9CF8E86DF414}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B42E9FA8-1B6D-4CF0-BAFB-9CF8E86DF414}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B42E9FA8-1B6D-4CF0-BAFB-9CF8E86DF414}.Release|x64.ActiveCfg = Release|Any CPU
+ {B42E9FA8-1B6D-4CF0-BAFB-9CF8E86DF414}.Release|x64.Build.0 = Release|Any CPU
+ {B42E9FA8-1B6D-4CF0-BAFB-9CF8E86DF414}.Release|x86.ActiveCfg = Release|Any CPU
+ {B42E9FA8-1B6D-4CF0-BAFB-9CF8E86DF414}.Release|x86.Build.0 = Release|Any CPU
+ {3A4FB731-98D3-4006-A63C-5DA356FEF674}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3A4FB731-98D3-4006-A63C-5DA356FEF674}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3A4FB731-98D3-4006-A63C-5DA356FEF674}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {3A4FB731-98D3-4006-A63C-5DA356FEF674}.Debug|x64.Build.0 = Debug|Any CPU
+ {3A4FB731-98D3-4006-A63C-5DA356FEF674}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {3A4FB731-98D3-4006-A63C-5DA356FEF674}.Debug|x86.Build.0 = Debug|Any CPU
+ {3A4FB731-98D3-4006-A63C-5DA356FEF674}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3A4FB731-98D3-4006-A63C-5DA356FEF674}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3A4FB731-98D3-4006-A63C-5DA356FEF674}.Release|x64.ActiveCfg = Release|Any CPU
+ {3A4FB731-98D3-4006-A63C-5DA356FEF674}.Release|x64.Build.0 = Release|Any CPU
+ {3A4FB731-98D3-4006-A63C-5DA356FEF674}.Release|x86.ActiveCfg = Release|Any CPU
+ {3A4FB731-98D3-4006-A63C-5DA356FEF674}.Release|x86.Build.0 = Release|Any CPU
+ {20818784-C3EC-4554-95EB-B92CE9E58C7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {20818784-C3EC-4554-95EB-B92CE9E58C7A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {20818784-C3EC-4554-95EB-B92CE9E58C7A}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {20818784-C3EC-4554-95EB-B92CE9E58C7A}.Debug|x64.Build.0 = Debug|Any CPU
+ {20818784-C3EC-4554-95EB-B92CE9E58C7A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {20818784-C3EC-4554-95EB-B92CE9E58C7A}.Debug|x86.Build.0 = Debug|Any CPU
+ {20818784-C3EC-4554-95EB-B92CE9E58C7A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {20818784-C3EC-4554-95EB-B92CE9E58C7A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {20818784-C3EC-4554-95EB-B92CE9E58C7A}.Release|x64.ActiveCfg = Release|Any CPU
+ {20818784-C3EC-4554-95EB-B92CE9E58C7A}.Release|x64.Build.0 = Release|Any CPU
+ {20818784-C3EC-4554-95EB-B92CE9E58C7A}.Release|x86.ActiveCfg = Release|Any CPU
+ {20818784-C3EC-4554-95EB-B92CE9E58C7A}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {C3CEF02D-43F9-4C28-BC1D-18ED3D1FC0E4} = {7F209FD2-1190-4721-9A08-9569AB4AA8D5}
+ {77FA9DA3-FB50-46EC-B9B8-905121594EA1} = {DD6845EA-4012-4E0A-8B78-8BAE5FF6967B}
+ {305C7374-1A54-4409-B527-968D49B258DF} = {B3DA0F06-4511-4791-BB12-A84655F02BFA}
+ {507A2467-52E9-4181-8414-426AC45E01B3} = {7F209FD2-1190-4721-9A08-9569AB4AA8D5}
+ {A8E7D4F1-2B3C-4D5E-9F6A-1B2C3D4E5F6A} = {36B2818B-95AB-47ED-96AC-563A115C16A8}
+ {B1C2D3E4-F5A6-4B7C-8D9E-0A1B2C3D4E5F} = {E1F2A3B4-C5D6-4E7F-8A9B-0C1D2E3F4A5B}
+ {C2D3E4F5-A6B7-4C8D-9E0F-1A2B3C4D5E6F} = {DD6845EA-4012-4E0A-8B78-8BAE5FF6967B}
+ {D3E4F5A6-B7C8-4D9E-0F1A-2B3C4D5E6F7A} = {B3DA0F06-4511-4791-BB12-A84655F02BFA}
+ {7BEC1F6E-3E97-489C-A134-ECCEA93FC93F} = {36B2818B-95AB-47ED-96AC-563A115C16A8}
+ {1333E8B4-070C-4C70-A9F2-070CAA66CDAD} = {B3DA0F06-4511-4791-BB12-A84655F02BFA}
+ {3827F320-31F0-4AE3-A048-8157C66DBEC8} = {36B2818B-95AB-47ED-96AC-563A115C16A8}
+ {36B2818B-95AB-47ED-96AC-563A115C16A8} = {7F209FD2-1190-4721-9A08-9569AB4AA8D5}
+ {B42E9FA8-1B6D-4CF0-BAFB-9CF8E86DF414} = {75661D3A-C7D4-4032-8E88-5949C22550EC}
+ {3A4FB731-98D3-4006-A63C-5DA356FEF674} = {DD6845EA-4012-4E0A-8B78-8BAE5FF6967B}
+ {20818784-C3EC-4554-95EB-B92CE9E58C7A} = {E1F2A3B4-C5D6-4E7F-8A9B-0C1D2E3F4A5B}
+ EndGlobalSection
+EndGlobal
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000..0c9276f
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,14 @@
+
+
+
+ 1.0.0-alpha.2
+ 1.0.0-alpha.2
+ Zapto
+ https://github.com/zapto-dev/AspNetCore
+ Copyright © 2025 Zapto
+ zapto, aspnetcore
+ https://github.com/zapto-dev/AspNetCore
+ MIT
+
+
+
diff --git a/nuget.config b/nuget.config
new file mode 100644
index 0000000..8f6583e
--- /dev/null
+++ b/nuget.config
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/sandbox/WasmApp.Library/Example.wasmlib.js b/sandbox/WasmApp.Library/Example.wasmlib.js
new file mode 100644
index 0000000..b67b76d
--- /dev/null
+++ b/sandbox/WasmApp.Library/Example.wasmlib.js
@@ -0,0 +1,8 @@
+/**
+ * Greets a person by name.
+ * @param {string} name - The name of the person to greet.
+ * @returns {Promise} A greeting message.
+ */
+export function greet(name) {
+ return Promise.resolve(`Greetings from Example, ${name}!`);
+}
diff --git a/sandbox/WasmApp.Library/NativeMethods.cs b/sandbox/WasmApp.Library/NativeMethods.cs
new file mode 100644
index 0000000..f47bbae
--- /dev/null
+++ b/sandbox/WasmApp.Library/NativeMethods.cs
@@ -0,0 +1,13 @@
+using Zapto.AspNetCore.Wasm.Interop;
+
+namespace WasmApp.Library;
+
+public static partial class NativeMethods
+{
+ ///
+ /// Calls the JavaScript greet function from the Example module.
+ /// WASM import: Example_greet(byte* namePtr, int nameLen, int callbackId)
+ ///
+ [WasmLibraryImport("greet", "Example")]
+ public static partial Task GreetAsync(string name);
+}
diff --git a/sandbox/WasmApp.Library/WasmApp.Library.csproj b/sandbox/WasmApp.Library/WasmApp.Library.csproj
new file mode 100644
index 0000000..7c7705a
--- /dev/null
+++ b/sandbox/WasmApp.Library/WasmApp.Library.csproj
@@ -0,0 +1,23 @@
+
+
+
+ net10.0
+ browser-wasm
+ enable
+ true
+ enable
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+ PreserveNewest
+
+
+
+
diff --git a/sandbox/WasmApp/Program.cs b/sandbox/WasmApp/Program.cs
new file mode 100644
index 0000000..efbe476
--- /dev/null
+++ b/sandbox/WasmApp/Program.cs
@@ -0,0 +1,44 @@
+using System.Text.Json.Serialization;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+using WasmApp.Library;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.Services.AddRouting();
+
+builder.Services.ConfigureHttpJsonOptions(options =>
+{
+ options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default);
+});
+
+var app = builder.Build();
+
+app.UseRouting();
+
+app.MapGet("/", () => "Hello from ASP.NET Core on Cloudflare Workers!");
+
+app.MapGet("/api/time", () =>
+{
+ return Results.Json(new TimeResponse(DateTime.UtcNow.ToString("O")), AppJsonContext.Default.TimeResponse);
+});
+
+app.MapGet("/api/greet/{name}", (string name) => $"Hello, {name}!");
+
+app.MapGet("/library/greet/{name}", async (string name) => await NativeMethods.GreetAsync(name));
+
+app.MapPost("/api/echo", async context =>
+{
+ using var reader = new StreamReader(context.Request.Body);
+ var body = await reader.ReadToEndAsync(context.RequestAborted);
+ await context.Response.WriteAsync($"Echo: {body}");
+});
+
+await app.RunAsync();
+
+internal record TimeResponse([property: JsonPropertyName("time")] string Time);
+
+[JsonSerializable(typeof(TimeResponse))]
+internal partial class AppJsonContext : JsonSerializerContext;
diff --git a/sandbox/WasmApp/WasmApp.csproj b/sandbox/WasmApp/WasmApp.csproj
new file mode 100644
index 0000000..27cb29a
--- /dev/null
+++ b/sandbox/WasmApp/WasmApp.csproj
@@ -0,0 +1,18 @@
+
+
+
+
+
+ net10.0-browser;net10.0
+
+
+ true
+
+
+
+
+
+
+
+
+
diff --git a/sdk/Zapto.AspNetCore.CloudFlare.SDK.Tasks/Configuration/WranglerBindings.cs b/sdk/Zapto.AspNetCore.CloudFlare.SDK.Tasks/Configuration/WranglerBindings.cs
new file mode 100644
index 0000000..c197516
--- /dev/null
+++ b/sdk/Zapto.AspNetCore.CloudFlare.SDK.Tasks/Configuration/WranglerBindings.cs
@@ -0,0 +1,655 @@
+namespace Zapto.AspNetCore.CloudFlare.SDK.Tasks;
+
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+///
+/// Configures a custom build step to be run by Wrangler when building your Worker.
+///
+public sealed class WranglerBuildConfig
+{
+ ///
+ /// The command used to build your Worker.
+ ///
+ [JsonPropertyName("command")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? Command { get; set; }
+
+ ///
+ /// The directory in which the command is executed.
+ ///
+ [JsonPropertyName("cwd")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? Cwd { get; set; }
+
+ ///
+ /// The directory to watch for changes while using wrangler dev.
+ ///
+ [JsonPropertyName("watch_dir")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? WatchDir { get; set; }
+}
+
+///
+/// Represents a route for the Worker.
+///
+public sealed class WranglerRoute
+{
+ ///
+ /// The route pattern.
+ ///
+ [JsonPropertyName("pattern")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? Pattern { get; set; }
+
+ ///
+ /// The zone ID for the route.
+ ///
+ [JsonPropertyName("zone_id")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? ZoneId { get; set; }
+
+ ///
+ /// The zone name for the route.
+ ///
+ [JsonPropertyName("zone_name")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? ZoneName { get; set; }
+
+ ///
+ /// Whether to use a custom domain.
+ ///
+ [JsonPropertyName("custom_domain")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public bool? CustomDomain { get; set; }
+}
+
+///
+/// Cron trigger definitions.
+///
+public sealed class WranglerTriggers
+{
+ ///
+ /// Cron expressions for scheduled triggers.
+ ///
+ [JsonPropertyName("crons")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public List? Crons { get; set; }
+}
+
+///
+/// Runtime limits for the Worker.
+///
+public sealed class WranglerLimits
+{
+ ///
+ /// Maximum CPU time in milliseconds.
+ ///
+ [JsonPropertyName("cpu_ms")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public int? CpuMs { get; set; }
+}
+
+///
+/// Smart placement configuration.
+///
+public sealed class WranglerPlacement
+{
+ ///
+ /// Placement mode: "off", "smart", or "targeted".
+ ///
+ [JsonPropertyName("mode")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? Mode { get; set; }
+
+ ///
+ /// Hint for placement (when mode is "smart").
+ ///
+ [JsonPropertyName("hint")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? Hint { get; set; }
+}
+
+///
+/// Static assets configuration.
+///
+public sealed class WranglerAssets
+{
+ ///
+ /// The directory containing static assets.
+ ///
+ [JsonPropertyName("directory")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? Directory { get; set; }
+
+ ///
+ /// Binding name for the assets.
+ ///
+ [JsonPropertyName("binding")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? Binding { get; set; }
+}
+
+///
+/// KV Namespace binding configuration.
+///
+public sealed class WranglerKvNamespace
+{
+ ///
+ /// The binding name used to refer to the KV Namespace.
+ ///
+ [JsonPropertyName("binding")]
+ public string? Binding { get; set; }
+
+ ///
+ /// The ID of the KV namespace.
+ ///
+ [JsonPropertyName("id")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? Id { get; set; }
+
+ ///
+ /// The ID of the KV namespace used during wrangler dev.
+ ///
+ [JsonPropertyName("preview_id")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? PreviewId { get; set; }
+}
+
+///
+/// R2 Bucket binding configuration.
+///
+public sealed class WranglerR2Bucket
+{
+ ///
+ /// The binding name used to refer to the R2 bucket.
+ ///
+ [JsonPropertyName("binding")]
+ public string? Binding { get; set; }
+
+ ///
+ /// The name of this R2 bucket at the edge.
+ ///
+ [JsonPropertyName("bucket_name")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? BucketName { get; set; }
+
+ ///
+ /// The preview name of this R2 bucket at the edge.
+ ///
+ [JsonPropertyName("preview_bucket_name")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? PreviewBucketName { get; set; }
+
+ ///
+ /// The jurisdiction that the bucket exists in.
+ ///
+ [JsonPropertyName("jurisdiction")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? Jurisdiction { get; set; }
+}
+
+///
+/// D1 Database binding configuration.
+///
+public sealed class WranglerD1Database
+{
+ ///
+ /// The binding name used to refer to the D1 database.
+ ///
+ [JsonPropertyName("binding")]
+ public string? Binding { get; set; }
+
+ ///
+ /// The name of this D1 database.
+ ///
+ [JsonPropertyName("database_name")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? DatabaseName { get; set; }
+
+ ///
+ /// The UUID of this D1 database.
+ ///
+ [JsonPropertyName("database_id")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? DatabaseId { get; set; }
+
+ ///
+ /// The UUID of this D1 database for Wrangler Dev.
+ ///
+ [JsonPropertyName("preview_database_id")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? PreviewDatabaseId { get; set; }
+
+ ///
+ /// The name of the migrations table.
+ ///
+ [JsonPropertyName("migrations_table")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? MigrationsTable { get; set; }
+
+ ///
+ /// The path to the directory of migrations.
+ ///
+ [JsonPropertyName("migrations_dir")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? MigrationsDir { get; set; }
+}
+
+///
+/// Durable Objects configuration.
+///
+public sealed class WranglerDurableObjects
+{
+ ///
+ /// List of Durable Object bindings.
+ ///
+ [JsonPropertyName("bindings")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public List? Bindings { get; set; }
+}
+
+///
+/// Durable Object binding configuration.
+///
+public sealed class WranglerDurableObjectBinding
+{
+ ///
+ /// The binding name for the Durable Object.
+ ///
+ [JsonPropertyName("name")]
+ public string? Name { get; set; }
+
+ ///
+ /// The class name of the Durable Object.
+ ///
+ [JsonPropertyName("class_name")]
+ public string? ClassName { get; set; }
+
+ ///
+ /// The script name where the Durable Object class is defined.
+ ///
+ [JsonPropertyName("script_name")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? ScriptName { get; set; }
+
+ ///
+ /// The environment where the Durable Object is defined.
+ ///
+ [JsonPropertyName("environment")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? Environment { get; set; }
+}
+
+///
+/// Service binding (Worker-to-Worker) configuration.
+///
+public sealed class WranglerService
+{
+ ///
+ /// The binding name used to refer to the bound service.
+ ///
+ [JsonPropertyName("binding")]
+ public string? Binding { get; set; }
+
+ ///
+ /// The name of the service.
+ ///
+ [JsonPropertyName("service")]
+ public string? Service { get; set; }
+
+ ///
+ /// The entrypoint (named export) of the service to bind to.
+ ///
+ [JsonPropertyName("entrypoint")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? Entrypoint { get; set; }
+}
+
+///
+/// Queues configuration.
+///
+public sealed class WranglerQueues
+{
+ ///
+ /// Producer bindings.
+ ///
+ [JsonPropertyName("producers")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public List? Producers { get; set; }
+
+ ///
+ /// Consumer configuration.
+ ///
+ [JsonPropertyName("consumers")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public List? Consumers { get; set; }
+}
+
+///
+/// Queue producer binding.
+///
+public sealed class WranglerQueueProducer
+{
+ ///
+ /// The binding name used to refer to the Queue.
+ ///
+ [JsonPropertyName("binding")]
+ public string? Binding { get; set; }
+
+ ///
+ /// The name of this Queue.
+ ///
+ [JsonPropertyName("queue")]
+ public string? Queue { get; set; }
+
+ ///
+ /// The number of seconds to wait before delivering a message.
+ ///
+ [JsonPropertyName("delivery_delay")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public int? DeliveryDelay { get; set; }
+}
+
+///
+/// Queue consumer configuration.
+///
+public sealed class WranglerQueueConsumer
+{
+ ///
+ /// The name of the queue from which this consumer should consume.
+ ///
+ [JsonPropertyName("queue")]
+ public string? Queue { get; set; }
+
+ ///
+ /// The maximum number of messages per batch.
+ ///
+ [JsonPropertyName("max_batch_size")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public int? MaxBatchSize { get; set; }
+
+ ///
+ /// The maximum number of seconds to wait to fill a batch.
+ ///
+ [JsonPropertyName("max_batch_timeout")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public int? MaxBatchTimeout { get; set; }
+
+ ///
+ /// The maximum number of retries for each message.
+ ///
+ [JsonPropertyName("max_retries")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public int? MaxRetries { get; set; }
+
+ ///
+ /// The queue to send messages that failed to be consumed.
+ ///
+ [JsonPropertyName("dead_letter_queue")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? DeadLetterQueue { get; set; }
+
+ ///
+ /// The maximum number of concurrent consumer Worker invocations.
+ ///
+ [JsonPropertyName("max_concurrency")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public int? MaxConcurrency { get; set; }
+}
+
+///
+/// Analytics Engine Dataset binding.
+///
+public sealed class WranglerAnalyticsEngineDataset
+{
+ ///
+ /// The binding name used to refer to the dataset.
+ ///
+ [JsonPropertyName("binding")]
+ public string? Binding { get; set; }
+
+ ///
+ /// The name of this dataset to write to.
+ ///
+ [JsonPropertyName("dataset")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? Dataset { get; set; }
+}
+
+///
+/// AI binding configuration.
+///
+public sealed class WranglerAiBinding
+{
+ ///
+ /// The binding name.
+ ///
+ [JsonPropertyName("binding")]
+ public string? Binding { get; set; }
+}
+
+///
+/// Browser binding configuration.
+///
+public sealed class WranglerBrowserBinding
+{
+ ///
+ /// The binding name.
+ ///
+ [JsonPropertyName("binding")]
+ public string? Binding { get; set; }
+}
+
+///
+/// Hyperdrive binding configuration.
+///
+public sealed class WranglerHyperdrive
+{
+ ///
+ /// The binding name used to refer to the project.
+ ///
+ [JsonPropertyName("binding")]
+ public string? Binding { get; set; }
+
+ ///
+ /// The id of the database.
+ ///
+ [JsonPropertyName("id")]
+ public string? Id { get; set; }
+
+ ///
+ /// The local database connection string for wrangler dev.
+ ///
+ [JsonPropertyName("localConnectionString")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? LocalConnectionString { get; set; }
+}
+
+///
+/// Vectorize index binding.
+///
+public sealed class WranglerVectorize
+{
+ ///
+ /// The binding name used to refer to the Vectorize index.
+ ///
+ [JsonPropertyName("binding")]
+ public string? Binding { get; set; }
+
+ ///
+ /// The name of the index.
+ ///
+ [JsonPropertyName("index_name")]
+ public string? IndexName { get; set; }
+}
+
+///
+/// mTLS certificate binding.
+///
+public sealed class WranglerMtlsCertificate
+{
+ ///
+ /// The binding name used to refer to the certificate.
+ ///
+ [JsonPropertyName("binding")]
+ public string? Binding { get; set; }
+
+ ///
+ /// The uuid of the uploaded mTLS certificate.
+ ///
+ [JsonPropertyName("certificate_id")]
+ public string? CertificateId { get; set; }
+}
+
+///
+/// Tail consumer binding.
+///
+public sealed class WranglerTailConsumer
+{
+ ///
+ /// The name of the tail Worker.
+ ///
+ [JsonPropertyName("service")]
+ public string? Service { get; set; }
+
+ ///
+ /// The environment of the tail Worker.
+ ///
+ [JsonPropertyName("environment")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? Environment { get; set; }
+}
+
+///
+/// Send Email binding configuration.
+///
+public sealed class WranglerSendEmailBinding
+{
+ ///
+ /// The binding name.
+ ///
+ [JsonPropertyName("name")]
+ public string? Name { get; set; }
+
+ ///
+ /// If this binding should be restricted to a specific verified address.
+ ///
+ [JsonPropertyName("destination_address")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? DestinationAddress { get; set; }
+
+ ///
+ /// If this binding should be restricted to a set of verified addresses.
+ ///
+ [JsonPropertyName("allowed_destination_addresses")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public List? AllowedDestinationAddresses { get; set; }
+}
+
+///
+/// Pipeline binding configuration.
+///
+public sealed class WranglerPipeline
+{
+ ///
+ /// The binding name used to refer to the bound service.
+ ///
+ [JsonPropertyName("binding")]
+ public string? Binding { get; set; }
+
+ ///
+ /// Name of the Pipeline to bind.
+ ///
+ [JsonPropertyName("pipeline")]
+ public string? Pipeline { get; set; }
+}
+
+///
+/// Observability configuration.
+///
+public sealed class WranglerObservability
+{
+ ///
+ /// Whether observability is enabled.
+ ///
+ [JsonPropertyName("enabled")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public bool? Enabled { get; set; }
+
+ ///
+ /// The sampling rate for head-based sampling (0.0 to 1.0).
+ ///
+ [JsonPropertyName("head_sampling_rate")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public double? HeadSamplingRate { get; set; }
+}
+
+///
+/// Environment-specific configuration overrides.
+///
+public sealed class WranglerEnvironment
+{
+ ///
+ /// The name of the environment.
+ ///
+ [JsonPropertyName("name")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? Name { get; set; }
+
+ ///
+ /// Account ID for this environment.
+ ///
+ [JsonPropertyName("account_id")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? AccountId { get; set; }
+
+ ///
+ /// Compatibility date for this environment.
+ ///
+ [JsonPropertyName("compatibility_date")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? CompatibilityDate { get; set; }
+
+ ///
+ /// Compatibility flags for this environment.
+ ///
+ [JsonPropertyName("compatibility_flags")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public List? CompatibilityFlags { get; set; }
+
+ ///
+ /// Routes for this environment.
+ ///
+ [JsonPropertyName("routes")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public List? Routes { get; set; }
+
+ ///
+ /// Environment variables for this environment.
+ ///
+ [JsonPropertyName("vars")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public Dictionary? Vars { get; set; }
+
+ ///
+ /// KV namespaces for this environment.
+ ///
+ [JsonPropertyName("kv_namespaces")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public List? KvNamespaces { get; set; }
+
+ ///
+ /// R2 buckets for this environment.
+ ///
+ [JsonPropertyName("r2_buckets")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public List? R2Buckets { get; set; }
+
+ ///
+ /// D1 databases for this environment.
+ ///
+ [JsonPropertyName("d1_databases")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public List? D1Databases { get; set; }
+}
diff --git a/sdk/Zapto.AspNetCore.CloudFlare.SDK.Tasks/Configuration/WranglerConfig.cs b/sdk/Zapto.AspNetCore.CloudFlare.SDK.Tasks/Configuration/WranglerConfig.cs
new file mode 100644
index 0000000..4c39772
--- /dev/null
+++ b/sdk/Zapto.AspNetCore.CloudFlare.SDK.Tasks/Configuration/WranglerConfig.cs
@@ -0,0 +1,273 @@
+namespace Zapto.AspNetCore.CloudFlare.SDK.Tasks;
+
+using System;
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+///
+/// Represents the root wrangler configuration.
+/// This corresponds to the wrangler.jsonc configuration file format.
+///
+///
+/// Based on the Wrangler config schema.
+/// See: https://developers.cloudflare.com/workers/wrangler/configuration/
+///
+public sealed class WranglerConfig
+{
+ ///
+ /// JSON Schema reference.
+ ///
+ [JsonPropertyName("$schema")]
+ public string? Schema { get; set; }
+
+ ///
+ /// The name of your Worker. Alphanumeric + dashes only.
+ ///
+ [JsonPropertyName("name")]
+ public string? Name { get; set; }
+
+ ///
+ /// The entrypoint/path to the JavaScript file that will be executed.
+ ///
+ [JsonPropertyName("main")]
+ public string? Main { get; set; }
+
+ ///
+ /// This is the ID of the account associated with your zone.
+ /// It can also be specified through the CLOUDFLARE_ACCOUNT_ID environment variable.
+ ///
+ [JsonPropertyName("account_id")]
+ public string? AccountId { get; set; }
+
+ ///
+ /// A date in the form yyyy-mm-dd, which will be used to determine which
+ /// version of the Workers runtime is used.
+ ///
+ [JsonPropertyName("compatibility_date")]
+ public string? CompatibilityDate { get; set; }
+
+ ///
+ /// A list of flags that enable features from upcoming features of the Workers runtime.
+ ///
+ [JsonPropertyName("compatibility_flags")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public List? CompatibilityFlags { get; set; }
+
+ ///
+ /// Whether we use <name>.<subdomain>.workers.dev to test and deploy your Worker.
+ ///
+ [JsonPropertyName("workers_dev")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public bool? WorkersDev { get; set; }
+
+ ///
+ /// Options to configure the development server.
+ ///
+ [JsonPropertyName("dev")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public WranglerDevConfig? Dev { get; set; }
+
+ ///
+ /// Configures a custom build step to be run by Wrangler when building your Worker.
+ ///
+ [JsonPropertyName("build")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public WranglerBuildConfig? Build { get; set; }
+
+ ///
+ /// Skip internal build steps and directly deploy script.
+ ///
+ [JsonPropertyName("no_bundle")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public bool? NoBundle { get; set; }
+
+ ///
+ /// Minify the script before uploading.
+ ///
+ [JsonPropertyName("minify")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public bool? Minify { get; set; }
+
+ ///
+ /// A list of routes that your Worker should be published to.
+ ///
+ [JsonPropertyName("routes")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public List? Routes { get; set; }
+
+ ///
+ /// Cron definitions to trigger a Worker's scheduled function.
+ ///
+ [JsonPropertyName("triggers")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public WranglerTriggers? Triggers { get; set; }
+
+ ///
+ /// Specify limits for runtime behavior.
+ ///
+ [JsonPropertyName("limits")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public WranglerLimits? Limits { get; set; }
+
+ ///
+ /// Specify how the Worker should be located to minimize round-trip time.
+ ///
+ [JsonPropertyName("placement")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public WranglerPlacement? Placement { get; set; }
+
+ ///
+ /// Specify the directory of static assets to deploy/serve.
+ ///
+ [JsonPropertyName("assets")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public WranglerAssets? Assets { get; set; }
+
+ ///
+ /// A map of environment variables to set when deploying your Worker.
+ ///
+ [JsonPropertyName("vars")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public Dictionary? Vars { get; set; }
+
+ ///
+ /// A map of values to substitute when deploying your Worker.
+ ///
+ [JsonPropertyName("define")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public Dictionary? Define { get; set; }
+
+ ///
+ /// Workers KV Namespaces you want to access from inside your Worker.
+ ///
+ [JsonPropertyName("kv_namespaces")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public List? KvNamespaces { get; set; }
+
+ ///
+ /// Specifies R2 buckets that are bound to this Worker environment.
+ ///
+ [JsonPropertyName("r2_buckets")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public List? R2Buckets { get; set; }
+
+ ///
+ /// Specifies D1 databases that are bound to this Worker environment.
+ ///
+ [JsonPropertyName("d1_databases")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public List? D1Databases { get; set; }
+
+ ///
+ /// A list of durable objects that your Worker should be bound to.
+ ///
+ [JsonPropertyName("durable_objects")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public WranglerDurableObjects? DurableObjects { get; set; }
+
+ ///
+ /// Specifies service bindings (Worker-to-Worker) that are bound to this Worker.
+ ///
+ [JsonPropertyName("services")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public List? Services { get; set; }
+
+ ///
+ /// Specifies Queues that are bound to this Worker environment.
+ ///
+ [JsonPropertyName("queues")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public WranglerQueues? Queues { get; set; }
+
+ ///
+ /// Specifies analytics engine datasets that are bound to this Worker.
+ ///
+ [JsonPropertyName("analytics_engine_datasets")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public List? AnalyticsEngineDatasets { get; set; }
+
+ ///
+ /// Binding to the AI project.
+ ///
+ [JsonPropertyName("ai")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public WranglerAiBinding? Ai { get; set; }
+
+ ///
+ /// A browser that will be usable from the Worker.
+ ///
+ [JsonPropertyName("browser")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public WranglerBrowserBinding? Browser { get; set; }
+
+ ///
+ /// Specifies Hyperdrive configs that are bound to this Worker environment.
+ ///
+ [JsonPropertyName("hyperdrive")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public List? Hyperdrive { get; set; }
+
+ ///
+ /// Specifies Vectorize indexes that are bound to this Worker environment.
+ ///
+ [JsonPropertyName("vectorize")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public List? Vectorize { get; set; }
+
+ ///
+ /// Specifies a list of mTLS certificates that are bound to this Worker.
+ ///
+ [JsonPropertyName("mtls_certificates")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public List? MtlsCertificates { get; set; }
+
+ ///
+ /// Specifies a list of Tail Workers that are bound to this Worker.
+ ///
+ [JsonPropertyName("tail_consumers")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public List? TailConsumers { get; set; }
+
+ ///
+ /// Send bindings to send email from inside your Worker.
+ ///
+ [JsonPropertyName("send_email")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public List? SendEmail { get; set; }
+
+ ///
+ /// Specifies list of Pipelines bound to this Worker environment.
+ ///
+ [JsonPropertyName("pipelines")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public List? Pipelines { get; set; }
+
+ ///
+ /// Specify the observability behavior of the Worker.
+ ///
+ [JsonPropertyName("observability")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public WranglerObservability? Observability { get; set; }
+
+ ///
+ /// Upload source maps when deploying this worker.
+ ///
+ [JsonPropertyName("upload_source_maps")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public bool? UploadSourceMaps { get; set; }
+
+ ///
+ /// Send Trace Events from this Worker to Workers Logpush.
+ ///
+ [JsonPropertyName("logpush")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public bool? Logpush { get; set; }
+
+ ///
+ /// Environment overrides for different deployment environments.
+ ///
+ [JsonPropertyName("env")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public Dictionary? Env { get; set; }
+}
diff --git a/sdk/Zapto.AspNetCore.CloudFlare.SDK.Tasks/Configuration/WranglerDevConfig.cs b/sdk/Zapto.AspNetCore.CloudFlare.SDK.Tasks/Configuration/WranglerDevConfig.cs
new file mode 100644
index 0000000..7de9dc7
--- /dev/null
+++ b/sdk/Zapto.AspNetCore.CloudFlare.SDK.Tasks/Configuration/WranglerDevConfig.cs
@@ -0,0 +1,53 @@
+namespace Zapto.AspNetCore.CloudFlare.SDK.Tasks;
+
+using System.Text.Json.Serialization;
+
+///
+/// Options to configure the development server.
+///
+public sealed class WranglerDevConfig
+{
+ ///
+ /// IP address for the local dev server to listen on.
+ ///
+ [JsonPropertyName("ip")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? Ip { get; set; }
+
+ ///
+ /// Port for the local dev server to listen on.
+ ///
+ [JsonPropertyName("port")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public int? Port { get; set; }
+
+ ///
+ /// Port for the local dev server's inspector to listen on.
+ ///
+ [JsonPropertyName("inspector_port")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public int? InspectorPort { get; set; }
+
+ ///
+ /// Protocol that local wrangler dev server listens to requests on.
+ /// Valid values: "http" or "https".
+ ///
+ [JsonPropertyName("local_protocol")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? LocalProtocol { get; set; }
+
+ ///
+ /// Protocol that wrangler dev forwards requests on.
+ /// Valid values: "http" or "https".
+ ///
+ [JsonPropertyName("upstream_protocol")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? UpstreamProtocol { get; set; }
+
+ ///
+ /// Host to forward requests to, defaults to the host of the first route.
+ ///
+ [JsonPropertyName("host")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? Host { get; set; }
+}
diff --git a/sdk/Zapto.AspNetCore.CloudFlare.SDK.Tasks/GenerateWranglerConfig.cs b/sdk/Zapto.AspNetCore.CloudFlare.SDK.Tasks/GenerateWranglerConfig.cs
new file mode 100644
index 0000000..22e8062
--- /dev/null
+++ b/sdk/Zapto.AspNetCore.CloudFlare.SDK.Tasks/GenerateWranglerConfig.cs
@@ -0,0 +1,570 @@
+namespace Zapto.AspNetCore.CloudFlare.SDK.Tasks;
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+///
+/// MSBuild task that generates a wrangler.jsonc configuration file for Cloudflare Workers.
+///
+public sealed class GenerateWranglerConfig : Task
+{
+ private static readonly JsonSerializerOptions s_jsonOptions = new()
+ {
+ WriteIndented = true,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
+ };
+
+ ///
+ /// The output path for the generated wrangler.jsonc file.
+ ///
+ [Required]
+ public string OutputPath { get; set; } = string.Empty;
+
+ ///
+ /// The name of the Worker. Alphanumeric + dashes only.
+ ///
+ [Required]
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// The entrypoint/path to the JavaScript file that will be executed.
+ ///
+ [Required]
+ public string Main { get; set; } = string.Empty;
+
+ ///
+ /// A date in the form yyyy-mm-dd for the Workers runtime compatibility.
+ ///
+ [Required]
+ public string CompatibilityDate { get; set; } = string.Empty;
+
+ ///
+ /// Comma-separated list of compatibility flags.
+ ///
+ public string? CompatibilityFlags { get; set; }
+
+ ///
+ /// Port for the local dev server to listen on.
+ ///
+ public int DevPort { get; set; }
+
+ ///
+ /// Protocol that local wrangler dev server listens to requests on.
+ ///
+ public string? DevProtocol { get; set; }
+
+ ///
+ /// IP address for the local dev server to listen on.
+ ///
+ public string? DevIp { get; set; }
+
+ ///
+ /// Account ID for Cloudflare.
+ ///
+ public string? AccountId { get; set; }
+
+ ///
+ /// Whether to deploy to workers.dev subdomain.
+ ///
+ public bool? WorkersDev { get; set; }
+
+ ///
+ /// Whether to upload source maps.
+ ///
+ public bool? UploadSourceMaps { get; set; }
+
+ ///
+ /// Whether to enable logpush.
+ ///
+ public bool? Logpush { get; set; }
+
+ ///
+ /// Whether to minify the script.
+ ///
+ public bool? Minify { get; set; }
+
+ ///
+ /// Skip internal build steps and directly deploy script.
+ ///
+ public bool? NoBundle { get; set; }
+
+ ///
+ /// KV namespaces as semicolon-separated bindings in format "binding=id" or "binding=id;preview_id=preview".
+ ///
+ public ITaskItem[]? KvNamespaces { get; set; }
+
+ ///
+ /// R2 buckets as semicolon-separated bindings in format "binding=bucket_name".
+ ///
+ public ITaskItem[]? R2Buckets { get; set; }
+
+ ///
+ /// D1 databases as semicolon-separated bindings in format "binding=database_name;database_id=id".
+ ///
+ public ITaskItem[]? D1Databases { get; set; }
+
+ ///
+ /// Service bindings in format "binding=service_name".
+ ///
+ public ITaskItem[]? Services { get; set; }
+
+ ///
+ /// Environment variables in format "key=value".
+ ///
+ public ITaskItem[]? Vars { get; set; }
+
+ ///
+ /// Define substitutions in format "key=value".
+ ///
+ public ITaskItem[]? Define { get; set; }
+
+ ///
+ /// Routes in format "pattern" or "pattern;zone_id=id".
+ ///
+ public ITaskItem[]? Routes { get; set; }
+
+ ///
+ /// Cron triggers as comma-separated expressions.
+ ///
+ public string? CronTriggers { get; set; }
+
+ ///
+ /// Smart placement mode: "off", "smart", or "targeted".
+ ///
+ public string? PlacementMode { get; set; }
+
+ ///
+ /// CPU time limit in milliseconds.
+ ///
+ public int CpuMs { get; set; }
+
+ ///
+ /// Optional path to a custom wrangler.jsonc file to merge with the generated config.
+ /// Properties from the custom config will override generated properties.
+ ///
+ public string? CustomConfigPath { get; set; }
+
+ ///
+ public override bool Execute()
+ {
+ try
+ {
+ var config = BuildConfiguration();
+ var generatedJson = JsonSerializer.Serialize(config, s_jsonOptions);
+
+ // Merge with custom config if provided
+ string finalJson;
+ if (!string.IsNullOrWhiteSpace(CustomConfigPath) && File.Exists(CustomConfigPath))
+ {
+ Log.LogMessage(MessageImportance.Normal, $"Merging custom wrangler config: {CustomConfigPath}");
+ finalJson = MergeWithCustomConfig(generatedJson, CustomConfigPath!);
+ }
+ else
+ {
+ finalJson = generatedJson;
+ }
+
+ var output = SerializeWithComments(finalJson);
+
+ var directory = Path.GetDirectoryName(OutputPath);
+ if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
+ {
+ Directory.CreateDirectory(directory);
+ }
+
+ // Only write if content has changed
+ if (File.Exists(OutputPath))
+ {
+ var existingContent = File.ReadAllText(OutputPath);
+ if (existingContent == output)
+ {
+ Log.LogMessage(MessageImportance.Low, "Wrangler config unchanged, skipping write.");
+ return true;
+ }
+ }
+
+ File.WriteAllText(OutputPath, output, Encoding.UTF8);
+ Log.LogMessage(MessageImportance.Normal, $"Generated wrangler config: {OutputPath}");
+ return true;
+ }
+ catch (Exception ex)
+ {
+ Log.LogErrorFromException(ex, showStackTrace: true);
+ return false;
+ }
+ }
+
+ private WranglerConfig BuildConfiguration()
+ {
+ var config = new WranglerConfig
+ {
+ Schema = "https://unpkg.com/wrangler@latest/config-schema.json",
+ Name = Name,
+ Main = Main,
+ CompatibilityDate = CompatibilityDate,
+ AccountId = string.IsNullOrWhiteSpace(AccountId) ? null : AccountId,
+ WorkersDev = WorkersDev,
+ UploadSourceMaps = UploadSourceMaps,
+ Logpush = Logpush,
+ Minify = Minify,
+ NoBundle = NoBundle
+ };
+
+ // Parse compatibility flags
+ if (!string.IsNullOrWhiteSpace(CompatibilityFlags))
+ {
+ config.CompatibilityFlags = ParseCommaSeparatedList(CompatibilityFlags!);
+ }
+
+ // Dev settings
+ if (DevPort > 0 || !string.IsNullOrWhiteSpace(DevProtocol) || !string.IsNullOrWhiteSpace(DevIp))
+ {
+ config.Dev = new WranglerDevConfig
+ {
+ Port = DevPort > 0 ? DevPort : (int?)null,
+ LocalProtocol = string.IsNullOrWhiteSpace(DevProtocol) ? null : DevProtocol,
+ Ip = string.IsNullOrWhiteSpace(DevIp) ? null : DevIp
+ };
+ }
+
+ // KV Namespaces
+ if (KvNamespaces is { Length: > 0 })
+ {
+ config.KvNamespaces = new List();
+ foreach (var item in KvNamespaces)
+ {
+ config.KvNamespaces.Add(new WranglerKvNamespace
+ {
+ Binding = item.ItemSpec,
+ Id = GetMetadataOrNull(item, "Id"),
+ PreviewId = GetMetadataOrNull(item, "PreviewId")
+ });
+ }
+ }
+
+ // R2 Buckets
+ if (R2Buckets is { Length: > 0 })
+ {
+ config.R2Buckets = new List();
+ foreach (var item in R2Buckets)
+ {
+ config.R2Buckets.Add(new WranglerR2Bucket
+ {
+ Binding = item.ItemSpec,
+ BucketName = GetMetadataOrNull(item, "BucketName"),
+ PreviewBucketName = GetMetadataOrNull(item, "PreviewBucketName"),
+ Jurisdiction = GetMetadataOrNull(item, "Jurisdiction")
+ });
+ }
+ }
+
+ // D1 Databases
+ if (D1Databases is { Length: > 0 })
+ {
+ config.D1Databases = new List();
+ foreach (var item in D1Databases)
+ {
+ config.D1Databases.Add(new WranglerD1Database
+ {
+ Binding = item.ItemSpec,
+ DatabaseName = GetMetadataOrNull(item, "DatabaseName"),
+ DatabaseId = GetMetadataOrNull(item, "DatabaseId"),
+ PreviewDatabaseId = GetMetadataOrNull(item, "PreviewDatabaseId"),
+ MigrationsTable = GetMetadataOrNull(item, "MigrationsTable"),
+ MigrationsDir = GetMetadataOrNull(item, "MigrationsDir")
+ });
+ }
+ }
+
+ // Services
+ if (Services is { Length: > 0 })
+ {
+ config.Services = new List();
+ foreach (var item in Services)
+ {
+ config.Services.Add(new WranglerService
+ {
+ Binding = item.ItemSpec,
+ Service = GetMetadataOrNull(item, "Service"),
+ Entrypoint = GetMetadataOrNull(item, "Entrypoint")
+ });
+ }
+ }
+
+ // Environment variables
+ if (Vars is { Length: > 0 })
+ {
+ config.Vars = new Dictionary();
+ foreach (var item in Vars)
+ {
+ var value = GetMetadataOrNull(item, "Value");
+ if (value != null)
+ {
+ config.Vars[item.ItemSpec] = value;
+ }
+ }
+ }
+
+ // Define substitutions
+ if (Define is { Length: > 0 })
+ {
+ config.Define = new Dictionary();
+ foreach (var item in Define)
+ {
+ var value = GetMetadataOrNull(item, "Value");
+ if (value != null)
+ {
+ config.Define[item.ItemSpec] = value;
+ }
+ }
+ }
+
+ // Routes
+ if (Routes is { Length: > 0 })
+ {
+ config.Routes = new List();
+ foreach (var item in Routes)
+ {
+ config.Routes.Add(new WranglerRoute
+ {
+ Pattern = item.ItemSpec,
+ ZoneId = GetMetadataOrNull(item, "ZoneId"),
+ ZoneName = GetMetadataOrNull(item, "ZoneName"),
+ CustomDomain = GetBoolMetadata(item, "CustomDomain")
+ });
+ }
+ }
+
+ // Cron triggers
+ if (!string.IsNullOrWhiteSpace(CronTriggers))
+ {
+ config.Triggers = new WranglerTriggers
+ {
+ Crons = ParseCommaSeparatedList(CronTriggers!)
+ };
+ }
+
+ // Placement
+ if (!string.IsNullOrWhiteSpace(PlacementMode))
+ {
+ config.Placement = new WranglerPlacement
+ {
+ Mode = PlacementMode
+ };
+ }
+
+ // Limits
+ if (CpuMs > 0)
+ {
+ config.Limits = new WranglerLimits
+ {
+ CpuMs = CpuMs
+ };
+ }
+
+ return config;
+ }
+
+ private static string SerializeWithComments(string json)
+ {
+ // Add a header comment for JSONC
+ var sb = new StringBuilder();
+ sb.AppendLine("// Wrangler configuration file");
+ sb.AppendLine("// Generated by Zapto.AspNetCore.CloudFlare.SDK");
+ sb.AppendLine("// See: https://developers.cloudflare.com/workers/wrangler/configuration/");
+ sb.Append(json);
+
+ return sb.ToString();
+ }
+
+ private static List ParseCommaSeparatedList(string value)
+ {
+ var result = new List();
+ foreach (var item in value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
+ {
+ var trimmed = item.Trim();
+ if (!string.IsNullOrEmpty(trimmed))
+ {
+ result.Add(trimmed);
+ }
+ }
+ return result;
+ }
+
+ private static string? GetMetadataOrNull(ITaskItem item, string metadataName)
+ {
+ var value = item.GetMetadata(metadataName);
+ return string.IsNullOrWhiteSpace(value) ? null : value;
+ }
+
+ private static bool? GetBoolMetadata(ITaskItem item, string metadataName)
+ {
+ var value = item.GetMetadata(metadataName);
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ return null;
+ }
+ return bool.TryParse(value, out var result) && result ? true : null;
+ }
+
+ ///
+ /// Merges a custom wrangler.jsonc file with the generated configuration.
+ /// Custom config properties take precedence over generated ones.
+ /// Uses JsonDocument to preserve unknown properties not in WranglerConfig.
+ ///
+ private string MergeWithCustomConfig(string generatedJson, string customConfigPath)
+ {
+ try
+ {
+ var customJson = File.ReadAllText(customConfigPath);
+
+ // Strip JSONC comments (single-line // and multi-line /* */)
+ customJson = StripJsonComments(customJson);
+
+ // Parse both as JsonDocument to merge properties
+ using var generatedDoc = JsonDocument.Parse(generatedJson);
+ using var customDoc = JsonDocument.Parse(customJson);
+
+ // Merge the two JSON objects (custom takes precedence)
+ return MergeJsonObjects(generatedDoc.RootElement, customDoc.RootElement);
+ }
+ catch (Exception ex)
+ {
+ Log.LogWarning($"Failed to merge custom wrangler config: {ex.Message}");
+ return generatedJson;
+ }
+ }
+
+ ///
+ /// Strips JSONC comments from JSON content.
+ ///
+ private static string StripJsonComments(string json)
+ {
+ var sb = new StringBuilder();
+ var i = 0;
+ var inString = false;
+
+ while (i < json.Length)
+ {
+ // Handle string literals
+ if (json[i] == '"' && (i == 0 || json[i - 1] != '\\'))
+ {
+ inString = !inString;
+ sb.Append(json[i]);
+ i++;
+ continue;
+ }
+
+ if (!inString)
+ {
+ // Single-line comment
+ if (i + 1 < json.Length && json[i] == '/' && json[i + 1] == '/')
+ {
+ while (i < json.Length && json[i] != '\n' && json[i] != '\r')
+ {
+ i++;
+ }
+ continue;
+ }
+
+ // Multi-line comment
+ if (i + 1 < json.Length && json[i] == '/' && json[i + 1] == '*')
+ {
+ i += 2;
+ while (i + 1 < json.Length && !(json[i] == '*' && json[i + 1] == '/'))
+ {
+ i++;
+ }
+ i += 2; // Skip */
+ continue;
+ }
+ }
+
+ sb.Append(json[i]);
+ i++;
+ }
+
+ return sb.ToString();
+ }
+
+ ///
+ /// Merges two JSON objects, with the second object's properties taking precedence.
+ ///
+ private static string MergeJsonObjects(JsonElement baseElement, JsonElement overrideElement)
+ {
+ using var stream = new MemoryStream();
+ using (var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true }))
+ {
+ MergeJsonElements(writer, baseElement, overrideElement);
+ }
+
+ return Encoding.UTF8.GetString(stream.ToArray());
+ }
+
+ private static void MergeJsonElements(Utf8JsonWriter writer, JsonElement baseElement, JsonElement overrideElement)
+ {
+ if (baseElement.ValueKind != JsonValueKind.Object || overrideElement.ValueKind != JsonValueKind.Object)
+ {
+ // If either is not an object, use override
+ overrideElement.WriteTo(writer);
+ return;
+ }
+
+ writer.WriteStartObject();
+
+ // Collect all property names from both objects
+ var baseProperties = new Dictionary();
+ foreach (var prop in baseElement.EnumerateObject())
+ {
+ baseProperties[prop.Name] = prop.Value;
+ }
+
+ var overrideProperties = new Dictionary();
+ foreach (var prop in overrideElement.EnumerateObject())
+ {
+ overrideProperties[prop.Name] = prop.Value;
+ }
+
+ // Write base properties, merging with override where applicable
+ foreach (var kvp in baseProperties)
+ {
+ writer.WritePropertyName(kvp.Key);
+
+ if (overrideProperties.TryGetValue(kvp.Key, out var overrideValue))
+ {
+ // Both have this property - merge if both are objects, otherwise use override
+ if (kvp.Value.ValueKind == JsonValueKind.Object && overrideValue.ValueKind == JsonValueKind.Object)
+ {
+ MergeJsonElements(writer, kvp.Value, overrideValue);
+ }
+ else
+ {
+ overrideValue.WriteTo(writer);
+ }
+
+ overrideProperties.Remove(kvp.Key);
+ }
+ else
+ {
+ // Only in base
+ kvp.Value.WriteTo(writer);
+ }
+ }
+
+ // Write remaining override-only properties
+ foreach (var kvp in overrideProperties)
+ {
+ writer.WritePropertyName(kvp.Key);
+ kvp.Value.WriteTo(writer);
+ }
+
+ writer.WriteEndObject();
+ }
+}
diff --git a/sdk/Zapto.AspNetCore.CloudFlare.SDK.Tasks/Zapto.AspNetCore.CloudFlare.SDK.Tasks.csproj b/sdk/Zapto.AspNetCore.CloudFlare.SDK.Tasks/Zapto.AspNetCore.CloudFlare.SDK.Tasks.csproj
new file mode 100644
index 0000000..c23ecb4
--- /dev/null
+++ b/sdk/Zapto.AspNetCore.CloudFlare.SDK.Tasks/Zapto.AspNetCore.CloudFlare.SDK.Tasks.csproj
@@ -0,0 +1,29 @@
+
+
+
+ netstandard2.0
+ latest
+ enable
+ Zapto.AspNetCore.CloudFlare.SDK.Tasks
+ MSBuild tasks for generating Cloudflare Workers configuration
+ aspnetcore, cloudflare, workers, msbuild, tasks
+
+
+ true
+ true
+ false
+
+
+ $(NoWarn);NU1903
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sdk/Zapto.AspNetCore.CloudFlare.SDK/Sdk/Sdk.props b/sdk/Zapto.AspNetCore.CloudFlare.SDK/Sdk/Sdk.props
new file mode 100644
index 0000000..216aa52
--- /dev/null
+++ b/sdk/Zapto.AspNetCore.CloudFlare.SDK/Sdk/Sdk.props
@@ -0,0 +1,125 @@
+
+
+
+
+ net10.0-browser
+
+
+ <_IsBrowserTfm Condition="'$(TargetPlatformIdentifier)' == 'browser'">true
+ <_IsBrowserTfm Condition="'$(_IsBrowserTfm)' == '' and $([System.String]::Copy('$(TargetFramework)').EndsWith('-browser'))">true
+ <_IsBrowserTfm Condition="'$(_IsBrowserTfm)' == ''">false
+
+
+ Exe
+
+
+ true
+
+
+
+
+
+ <_WranglerNameIsDefault Condition="'$(WranglerName)' == ''">true
+ $(AssemblyName)
+
+ index.js
+
+ 2026-01-01
+
+ nodejs_compat
+
+ 8787
+
+ http
+
+
+
+ false
+ true
+ false
+ $(EmccFlags) -O3
+ $(InterceptorsNamespaces);Zapto.AspNetCore.Wasm.Generated
+
+
+ true
+ true
+ Shared
+
+
+ true
+ true
+
+
+ browser-wasm
+
+
+ wasm
+ AnyCPU
+
+
+ true
+ true
+ true
+
+
+ true
+ true
+
+
+ full
+ true
+ <_AggressiveAttributeTrimming>true
+ false
+ false
+ false
+ false
+ false
+ false
+ <_EnableConsumingManagedCodeFromNativeHosting>false
+ false
+ false
+ false
+ true
+ true
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+ true
+ false
+ false
+ false
+ false
+ none
+
+
+ -s EXPORTED_RUNTIME_METHODS=cwrap -s ENVIRONMENT=webview -s EXPORT_ES6=1 -s ASSERTIONS=0 -s WARN_ON_UNDEFINED_SYMBOLS=0 -Wno-unused-command-line-argument -Wno-js-compiler
+
+
+ CS8784
+ $(NoWarn);CS8784
+
+
+ CLOUDFLARE
+ $(DefineConstants);CLOUDFLARE
+
+
+
+
+ enable
+ enable
+
+
+
+ https://api.nuget.org/v3/index.json;
+ https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-experimental/nuget/v3/index.json;
+
+
+
+
+
+
+
diff --git a/sdk/Zapto.AspNetCore.CloudFlare.SDK/Sdk/Sdk.targets b/sdk/Zapto.AspNetCore.CloudFlare.SDK/Sdk/Sdk.targets
new file mode 100644
index 0000000..a8acbba
--- /dev/null
+++ b/sdk/Zapto.AspNetCore.CloudFlare.SDK/Sdk/Sdk.targets
@@ -0,0 +1,371 @@
+
+
+
+
+ 10.0.1
+ $(NetCoreRoot)shared\Microsoft.AspNetCore.App\$(AspNetCoreAppRuntimeVersion)\
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 3.1.56
+ 10.0.2
+ $(NetCoreRoot)packs\Microsoft.NET.Runtime.Emscripten.$(EmscriptenVersion).Sdk.win-x64\$(EmscriptenPackVersion)\tools\
+ $(NetCoreRoot)packs\Microsoft.NET.Runtime.Emscripten.$(EmscriptenVersion).Node.win-x64\$(EmscriptenPackVersion)\tools\bin\
+
+ $(NetCoreRoot)packs\Microsoft.NET.Runtime.Emscripten.$(EmscriptenVersion).Cache.win-x64\$(EmscriptenPackVersion)\tools\emscripten\cache\
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+ PreserveNewest
+
+
+ PreserveNewest
+ PreserveNewest
+
+
+
+
+
+ <_WranglerTaskAssemblyPath Condition="'$(ZaptoWasmUseProjectReference)' == 'true'">$(MSBuildThisFileDirectory)..\..\Zapto.AspNetCore.CloudFlare.SDK.Tasks\bin\$(Configuration)\netstandard2.0\Zapto.AspNetCore.CloudFlare.SDK.Tasks.dll
+ <_WranglerTaskProjectPath Condition="'$(ZaptoWasmUseProjectReference)' == 'true'">$(MSBuildThisFileDirectory)..\..\Zapto.AspNetCore.CloudFlare.SDK.Tasks\Zapto.AspNetCore.CloudFlare.SDK.Tasks.csproj
+ <_WranglerTaskAssemblyPath Condition="'$(ZaptoWasmUseProjectReference)' != 'true'">$(MSBuildThisFileDirectory)..\tasks\netstandard2.0\Zapto.AspNetCore.CloudFlare.SDK.Tasks.dll
+
+
+
+
+
+
+
+
+
+ Release
+ <_CloudflareBaseOutputPath Condition="'$(BaseOutputPath)' != ''">$(BaseOutputPath)
+ <_CloudflareBaseOutputPath Condition="'$(BaseOutputPath)' == ''">bin\
+ <_PublishOutputPath Condition="'$(PublishDir)' != ''">$(PublishDir)
+ <_PublishOutputPath Condition="'$(PublishDir)' == ''">$(_CloudflareBaseOutputPath)$(CloudflarePublishConfiguration)\$(TargetFramework)\browser-wasm\publish\
+ <_PublishOutputPathAbsolute Condition="!$([System.IO.Path]::IsPathRooted('$(_PublishOutputPath)'))">$(MSBuildProjectDirectory)\$(_PublishOutputPath)
+ <_PublishOutputPathAbsolute Condition="$([System.IO.Path]::IsPathRooted('$(_PublishOutputPath)'))">$(_PublishOutputPath)
+ $([System.IO.Path]::GetFullPath('$(_PublishOutputPathAbsolute)..\cloudflare\'))
+ $(MSBuildThisFileDirectory)index.js
+ $(BaseIntermediateOutputPath)generated\
+ $(CloudflareIndexGeneratedDir)index.js
+ $(CloudflareIndexGeneratedDir)wrangler.jsonc
+ $(MSBuildProjectDirectory)\wrangler.jsonc
+
+
+
+
+
+
+ <_DiscoveredWasmLibraryScripts Include="$(PublishDir)*.wasmlib.js" />
+
+
+
+
+
+
+ <_LibraryImportLines Include="import __STAR__ as _lib_$([System.String]::Copy('%(_DiscoveredWasmLibraryScripts.Filename)').Replace('.wasmlib', '').Replace('.', '_').Replace('-', '_')) from __QUOTE__./%(_DiscoveredWasmLibraryScripts.Filename)%(_DiscoveredWasmLibraryScripts.Extension)__QUOTE____SEMI__"
+ Condition="'@(_DiscoveredWasmLibraryScripts)' != ''" />
+ <_LibraryModuleLines Include=" __QUOTE__$([System.String]::Copy('%(_DiscoveredWasmLibraryScripts.Filename)').Replace('.wasmlib', ''))__QUOTE__: _lib_$([System.String]::Copy('%(_DiscoveredWasmLibraryScripts.Filename)').Replace('.wasmlib', '').Replace('.', '_').Replace('-', '_')),"
+ Condition="'@(_DiscoveredWasmLibraryScripts)' != ''" />
+
+
+
+ <_LibraryImportStatements>@(_LibraryImportLines, '%0a')
+ <_LibraryImportStatements>$(_LibraryImportStatements.Replace('__STAR__', '*'))
+ <_LibraryImportStatements>$(_LibraryImportStatements.Replace('__QUOTE__', "'"))
+ <_LibraryImportStatements>$(_LibraryImportStatements.Replace('__SEMI__', ';'))
+ <_LibraryModuleRegistrations>@(_LibraryModuleLines, '%0a')
+ <_LibraryModuleRegistrations>$(_LibraryModuleRegistrations.Replace('__QUOTE__', "'"))
+
+
+
+ <_CloudflareIndexTemplate>$([System.IO.File]::ReadAllText('$(CloudflareIndexTemplatePath)'))
+ <_CloudflareIndexContent>$(_CloudflareIndexTemplate.Replace('__MAIN_ASSEMBLY__', '$(AssemblyName)'))
+ <_CloudflareIndexContent>$(_CloudflareIndexContent.Replace('// __LIBRARY_IMPORTS__', '$(_LibraryImportStatements)'))
+ <_CloudflareIndexContent>$(_CloudflareIndexContent.Replace(' // __LIBRARY_MODULES__', '$(_LibraryModuleRegistrations)'))
+
+
+
+
+
+
+
+
+
+
+ <_WranglerNameKebab Condition="'$(_WranglerNameIsDefault)' == 'true'">$(WranglerName.Replace('.', '-').Replace('_', '-').ToLowerInvariant())
+ <_WranglerNameKebab Condition="'$(_WranglerNameIsDefault)' != 'true'">$(WranglerName)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <_WasmLibScriptsToCopy Include="$(PublishDir)*.wasmlib.js" />
+
+
+
+
+
+ <_WasmFilesToCopy Include="$(PublishDir)*.wasm" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ EnsureWranglerAvailable;EnsureCloudflareOutput;Publish;PrepareCloudflareOutput;$(RunDependsOn)
+ $(MSBuildProjectDirectory)
+
+
+
+ powershell.exe
+ -NoProfile -Command "$ErrorActionPreference='Stop'; if (-not (Get-Command wrangler -ErrorAction SilentlyContinue)) { Write-Error 'Wrangler is not available on PATH. Install it with ''npm i -g wrangler''.'; exit 1 }; Write-Host '[Zapto.CloudFlare.SDK] Starting NativeAOT compilation...'; dotnet publish -f $(TargetFramework) -c $(CloudflarePublishConfiguration) -v q -tl:false; if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }; Write-Host '[Zapto.CloudFlare.SDK] Preparing Cloudflare output...'; dotnet msbuild /t:PrepareCloudflareOutput -p:Configuration=$(CloudflarePublishConfiguration) -p:TargetFramework=$(TargetFramework) -p:RuntimeIdentifier=browser-wasm; if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }; Write-Host '[Zapto.CloudFlare.SDK] Launching Wrangler dev server...'; wrangler dev --config '$(CloudflareOutputPath)wrangler.jsonc' --cwd '$(CloudflareOutputPath)' --local --ip 127.0.0.1 --port 8787"
+
+
+
+ /bin/sh
+ -c "echo '[Zapto.CloudFlare.SDK] Starting NativeAOT compilation...' && dotnet publish -f $(TargetFramework) -c $(CloudflarePublishConfiguration) -v q -tl:false && echo '[Zapto.CloudFlare.SDK] Preparing Cloudflare output...' && dotnet msbuild /t:PrepareCloudflareOutput -p:Configuration=$(CloudflarePublishConfiguration) -p:TargetFramework=$(TargetFramework) -p:RuntimeIdentifier=browser-wasm && command -v wrangler >/dev/null 2>&1 && echo '[Zapto.CloudFlare.SDK] Launching Wrangler dev server...' && wrangler dev --config '$(CloudflareOutputPath)wrangler.jsonc' --cwd '$(CloudflareOutputPath)' --local --ip 127.0.0.1 --port 8787 || { echo 'Wrangler is not available on PATH. Install it with \"npm i -g wrangler\".' 1>&2; exit 1; }"
+
+
+
+ powershell.exe -NoProfile -Command "if (Get-Command wrangler -ErrorAction SilentlyContinue) { exit 0 } else { exit 1 }"
+ /bin/sh -c "command -v wrangler"
+
+
+
+
+
+
+
+
+
+ @(_WranglerPathLines)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sdk/Zapto.AspNetCore.CloudFlare.SDK/Sdk/context.js b/sdk/Zapto.AspNetCore.CloudFlare.SDK/Sdk/context.js
new file mode 100644
index 0000000..f1269ab
--- /dev/null
+++ b/sdk/Zapto.AspNetCore.CloudFlare.SDK/Sdk/context.js
@@ -0,0 +1,3 @@
+const context = {}
+
+export default context
\ No newline at end of file
diff --git a/sdk/Zapto.AspNetCore.CloudFlare.SDK/Sdk/index.js b/sdk/Zapto.AspNetCore.CloudFlare.SDK/Sdk/index.js
new file mode 100644
index 0000000..91ea247
--- /dev/null
+++ b/sdk/Zapto.AspNetCore.CloudFlare.SDK/Sdk/index.js
@@ -0,0 +1,375 @@
+import worker from './dotnet.native.wasm';
+import context from './context.js';
+import createDotnetRuntime from './dotnet.native.js';
+import {
+ initializeExports,
+ initializeReplacements,
+ configureRuntimeStartup,
+ configureEmscriptenStartup,
+ configureWorkerStartup,
+ passEmscriptenInternals,
+ setRuntimeGlobals,
+} from './dotnet.runtime.js';
+
+// __LIBRARY_IMPORTS__
+
+const mainAssemblyName = '__MAIN_ASSEMBLY__';
+
+const libraryModules = {
+ // __LIBRARY_MODULES__
+};
+
+const textEncoder = new TextEncoder();
+const textDecoder = new TextDecoder('utf-8');
+const pendingCallbacks = new Map();
+
+function createFeatureProbe(value) {
+ return async () => value;
+}
+
+function readString(ptr, length) {
+ if (ptr === 0 || length <= 0) return null;
+ const bytes = context.runtime.HEAPU8.subarray(ptr, ptr + length);
+ return textDecoder.decode(bytes);
+}
+
+function allocateString(str) {
+ const bytes = textEncoder.encode(str);
+ const ptr = context.runtime._malloc(bytes.length);
+ context.runtime.HEAPU8.set(bytes, ptr);
+ return { ptr, length: bytes.length };
+}
+
+function completeCallback(callbackId, resultStr) {
+ const fn = context.runtime._WasmLibrary_CompleteCallback;
+ if (!fn) return;
+
+ if (resultStr == null) {
+ fn(callbackId, 0, 0);
+ } else {
+ const { ptr, length } = allocateString(resultStr);
+ fn(callbackId, ptr, length);
+ }
+}
+
+function completeCallbackError(callbackId, errorMessage) {
+ const fn = context.runtime._WasmLibrary_CompleteCallbackError;
+ if (!fn) return;
+
+ const { ptr, length } = allocateString(errorMessage || 'Unknown error');
+ fn(callbackId, ptr, length);
+}
+
+function createImportFunction(moduleName, funcName, func) {
+ return (arg0Ptr, arg0Len, callbackId) => {
+ const arg0 = readString(arg0Ptr, arg0Len);
+
+ try {
+ const result = func(arg0);
+
+ if (result && typeof result.then === 'function') {
+ const pendingPromise = result
+ .then(value => {
+ completeCallback(callbackId, value == null ? null : String(value));
+ })
+ .catch(err => {
+ completeCallbackError(callbackId, err?.message || String(err));
+ })
+ .finally(() => {
+ pendingCallbacks.delete(callbackId);
+ });
+ pendingCallbacks.set(callbackId, pendingPromise);
+ } else {
+ completeCallback(callbackId, result == null ? null : String(result));
+ }
+ } catch (err) {
+ completeCallbackError(callbackId, err?.message || String(err));
+ }
+ };
+}
+
+function createWasmImports() {
+ const imports = {};
+
+ for (const [moduleName, moduleExports] of Object.entries(libraryModules)) {
+ for (const [funcName, func] of Object.entries(moduleExports)) {
+ if (typeof func !== 'function') continue;
+ imports[`_${moduleName}_${funcName}`] = createImportFunction(moduleName, funcName, func);
+ }
+ }
+
+ return imports;
+}
+
+let cachedModule;
+
+function load() {
+ return cachedModule ??= (async () => {
+ let scriptDirectory = './';
+ try {
+ if (typeof import.meta !== 'undefined' && import.meta.url) {
+ scriptDirectory = new URL('./', import.meta.url).toString();
+ }
+ } catch {
+ scriptDirectory = './';
+ }
+
+ const moduleConfig = {
+ config: {
+ mainAssemblyName,
+ applicationArguments: [],
+ environmentVariables: {},
+ },
+ addRunDependency: () => {},
+ removeRunDependency: () => {},
+ monitorRunDependencies: () => {},
+ };
+
+ const runtimeGlobals = {
+ mono: {},
+ binding: {},
+ internal: {
+ require: globalThis.require ?? (() => {
+ throw new Error('require is not supported in this environment');
+ }),
+ },
+ module: moduleConfig,
+ loaderHelpers: {
+ scriptDirectory,
+ locateFile: (path) => {
+ if (scriptDirectory.startsWith('http://') || scriptDirectory.startsWith('https://') || scriptDirectory.startsWith('file:')) {
+ return new URL(path, scriptDirectory).toString();
+ }
+ return `${scriptDirectory}${path}`;
+ },
+ fetch_like: (url, options) => fetch(url, options),
+ simd: createFeatureProbe(true),
+ relaxedSimd: createFeatureProbe(true),
+ exceptions: createFeatureProbe(true),
+ createPromiseController: (afterResolve, afterReject) => {
+ let promiseControl = null;
+ const promise = new Promise((resolve, reject) => {
+ promiseControl = {
+ isDone: false,
+ promise: null,
+ resolve: (value) => {
+ if (!promiseControl.isDone) {
+ promiseControl.isDone = true;
+ resolve(value);
+ afterResolve?.();
+ }
+ },
+ reject: (reason) => {
+ if (!promiseControl.isDone) {
+ promiseControl.isDone = true;
+ reject(reason);
+ afterReject?.();
+ }
+ },
+ };
+ });
+ promiseControl.promise = promise;
+ return { promise, promise_control: promiseControl };
+ },
+ },
+ runtimeHelpers: {},
+ api: {},
+ };
+
+ setRuntimeGlobals(runtimeGlobals);
+ const dotnetApi = initializeExports(runtimeGlobals);
+ await configureRuntimeStartup(moduleConfig);
+
+ runtimeGlobals.runtimeHelpers?.coreAssetsInMemory?.promise_control?.resolve?.();
+ runtimeGlobals.runtimeHelpers?.allAssetsInMemory?.promise_control?.resolve?.();
+
+ const customImports = createWasmImports();
+
+ const moduleFactory = (module) => {
+ const base = {};
+ if (module && (typeof module === 'object' || typeof module === 'function')) {
+ for (const key of Object.keys(module)) {
+ if (key !== 'caller' && key !== 'callee' && key !== 'arguments') {
+ base[key] = module[key];
+ }
+ }
+ }
+
+ const merged = Object.assign(base, moduleConfig, {
+ locateFile: () => './dotnet.native.wasm',
+ instantiateWasm: (info, receiveInstance) => {
+ const imports = info.a || info.env || {};
+ for (const [key, fn] of Object.entries(imports)) {
+ if (typeof fn === 'function' && fn.stub) {
+ const fnString = fn.toString();
+ for (const [importName, importFn] of Object.entries(customImports)) {
+ const funcName = importName.slice(1);
+ if (fnString.includes(funcName)) {
+ imports[key] = importFn;
+ break;
+ }
+ }
+ }
+ }
+
+ WebAssembly.instantiate(worker, info).then(instance => {
+ receiveInstance(instance, worker);
+ }).catch(err => {
+ console.error('WebAssembly.instantiate failed:', err);
+ });
+ return [];
+ },
+ addRunDependency: () => {},
+ removeRunDependency: () => {},
+ monitorRunDependencies: () => {},
+ __dotnet_runtime: {
+ initializeReplacements,
+ configureEmscriptenStartup,
+ configureWorkerStartup,
+ passEmscriptenInternals,
+ },
+ });
+
+ if (!merged.ready) {
+ merged.ready = Promise.resolve(merged);
+ }
+ return merged;
+ };
+
+ const runtime = await createDotnetRuntime(moduleFactory);
+
+ context.runtime = runtime;
+
+ Object.assign(runtimeGlobals.module, runtime);
+
+ runtimeGlobals.runtimeHelpers.mono_wasm_bindings_is_ready = true;
+
+ const setModuleImports = dotnetApi?.setModuleImports || runtime?.setModuleImports;
+ if (setModuleImports) {
+ for (const [moduleName, moduleExports] of Object.entries(libraryModules)) {
+ setModuleImports(moduleName, moduleExports);
+ }
+ }
+
+ runtime._WasmApp__GeneratedInitializer__Register_?.();
+
+ const mono_wasm_assembly_load = runtime.cwrap?.('mono_wasm_assembly_load', 'number', ['string'])
+ || runtime._mono_wasm_assembly_load;
+ if (mono_wasm_assembly_load) {
+ try {
+ mono_wasm_assembly_load('System.Runtime.InteropServices.JavaScript');
+ } catch {
+ }
+ }
+
+ const callEntrypoint = runtime._System_Runtime_InteropServices_JavaScript_JavaScriptExports_CallEntrypoint;
+ if (typeof callEntrypoint === 'function') {
+ const frameSize = 5 * 32;
+ const sp = runtime.stackSave();
+ const args = runtime.stackAlloc(frameSize);
+ runtime.HEAPU8.fill(0, args, args + frameSize);
+ callEntrypoint(args);
+ runtime.stackRestore(sp);
+ }
+
+ const beginRequest = runtime.cwrap('BeginRequest', 'number', ['number', 'array', 'number', 'array']);
+ const endRequest = runtime.cwrap('EndRequest', null, ['number']);
+ const getRequestResult = runtime._GetRequestResult;
+ const isRequestComplete = runtime._IsRequestComplete;
+
+ const encoder = new TextEncoder();
+ const decoder = new TextDecoder('utf-8');
+
+ const getInt32 = (ptr) => {
+ const memory = runtime['HEAP8'];
+ return ((memory[ptr + 3] & 0xFF) << 24) |
+ ((memory[ptr + 2] & 0xFF) << 16) |
+ ((memory[ptr + 1] & 0xFF) << 8) |
+ (memory[ptr] & 0xFF);
+ };
+
+ const parseResponse = (ptr) => {
+ const memory = runtime['HEAP8'];
+ let offset = ptr;
+ const responseBodyLength = getInt32(offset);
+ offset += 4;
+ const responseLength = getInt32(offset);
+ offset += 4;
+
+ let body = null;
+ if (responseBodyLength > 0) {
+ const responseBodyCopy = new Uint8Array(responseBodyLength);
+ responseBodyCopy.set(memory.subarray(offset, offset + responseBodyLength));
+ offset += responseBodyLength;
+
+ let done = false;
+ body = new ReadableStream({
+ start(controller) {
+ controller.enqueue(responseBodyCopy);
+ },
+ pull(controller) {
+ if (!done) {
+ done = true;
+ controller.close();
+ endRequest(ptr);
+ }
+ },
+ cancel() {
+ endRequest(ptr);
+ }
+ });
+ } else {
+ endRequest(ptr);
+ }
+
+ const responseContext = JSON.parse(decoder.decode(memory.subarray(offset, offset + responseLength)));
+ return { body, response: responseContext };
+ };
+
+ return async (requestContext, requestBody) => {
+ const json = encoder.encode(JSON.stringify(requestContext));
+ let ptr = beginRequest(json.length, json, requestBody.length, requestBody);
+
+ if (ptr === 0) {
+ while (pendingCallbacks.size > 0) {
+ const pendingPromises = Array.from(pendingCallbacks.values());
+ await Promise.all(pendingPromises);
+ }
+
+ if (isRequestComplete && isRequestComplete() !== 0) {
+ ptr = getRequestResult ? getRequestResult() : 0;
+ }
+
+ if (ptr === 0) {
+ throw new Error('Request did not complete after all callbacks finished');
+ }
+ }
+
+ return parseResponse(ptr);
+ };
+ })();
+}
+
+export default {
+ async fetch(request, env, ctx) {
+ context.request = request;
+ context.env = env;
+ context.ctx = ctx;
+
+ const processRequest = await load();
+ const arrayBuffer = await request.arrayBuffer();
+ const requestBody = new Uint8Array(arrayBuffer);
+ const headers = {};
+
+ for (const [key, value] of request.headers) {
+ headers[key] = value;
+ }
+
+ const { body, response } = await processRequest(
+ { method: request.method, url: request.url, headers },
+ requestBody
+ );
+
+ return new Response(body, response);
+ },
+};
\ No newline at end of file
diff --git a/sdk/Zapto.AspNetCore.CloudFlare.SDK/Zapto.AspNetCore.CloudFlare.SDK.csproj b/sdk/Zapto.AspNetCore.CloudFlare.SDK/Zapto.AspNetCore.CloudFlare.SDK.csproj
new file mode 100644
index 0000000..27073c2
--- /dev/null
+++ b/sdk/Zapto.AspNetCore.CloudFlare.SDK/Zapto.AspNetCore.CloudFlare.SDK.csproj
@@ -0,0 +1,96 @@
+
+
+
+ netstandard2.0
+ SDK package for hosting ASP.NET Core on Cloudflare Workers using WebAssembly LLVM
+ aspnetcore, webassembly, wasm, llvm, cloudflare, workers, sdk
+ true
+ MSBuildSdk
+
+ $(NoWarn);NU5128;NU5100
+ Zapto.AspNetCore.CloudFlare.SDK
+ $(BaseIntermediateOutputPath)Sdk\
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <_PackageFiles Include="..\..\src\Zapto.AspNetCore.Wasm.SourceGenerator\bin\$(Configuration)\netstandard2.0\Zapto.AspNetCore.Wasm.SourceGenerator.dll"
+ PackagePath="analyzers/dotnet/cs" />
+
+
+
+
+
+
+ <_PackageFiles Include="..\Zapto.AspNetCore.CloudFlare.SDK.Tasks\bin\$(Configuration)\netstandard2.0\Zapto.AspNetCore.CloudFlare.SDK.Tasks.dll"
+ PackagePath="tasks/netstandard2.0" />
+ <_PackageFiles Include="..\Zapto.AspNetCore.CloudFlare.SDK.Tasks\bin\$(Configuration)\netstandard2.0\System.Text.Json.dll"
+ PackagePath="tasks/netstandard2.0" />
+ <_PackageFiles Include="..\Zapto.AspNetCore.CloudFlare.SDK.Tasks\bin\$(Configuration)\netstandard2.0\System.Text.Encodings.Web.dll"
+ PackagePath="tasks/netstandard2.0" />
+ <_PackageFiles Include="..\Zapto.AspNetCore.CloudFlare.SDK.Tasks\bin\$(Configuration)\netstandard2.0\System.Buffers.dll"
+ PackagePath="tasks/netstandard2.0"
+ Condition="Exists('..\Zapto.AspNetCore.CloudFlare.SDK.Tasks\bin\$(Configuration)\netstandard2.0\System.Buffers.dll')" />
+ <_PackageFiles Include="..\Zapto.AspNetCore.CloudFlare.SDK.Tasks\bin\$(Configuration)\netstandard2.0\System.Memory.dll"
+ PackagePath="tasks/netstandard2.0"
+ Condition="Exists('..\Zapto.AspNetCore.CloudFlare.SDK.Tasks\bin\$(Configuration)\netstandard2.0\System.Memory.dll')" />
+ <_PackageFiles Include="..\Zapto.AspNetCore.CloudFlare.SDK.Tasks\bin\$(Configuration)\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll"
+ PackagePath="tasks/netstandard2.0"
+ Condition="Exists('..\Zapto.AspNetCore.CloudFlare.SDK.Tasks\bin\$(Configuration)\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll')" />
+
+
+
+
+
+
+
+
+ <_SdkTargetsTemplate>$([System.IO.File]::ReadAllText('$(MSBuildProjectDirectory)\Sdk\Sdk.targets'))
+ <_SdkTargetsContent>$(_SdkTargetsTemplate.Replace('__ZAPTO_WASM_VERSION__', '$(PackageVersion)'))
+
+
+
+
+
+
+ <_PackageFiles Include="$(GeneratedSdkDir)Sdk.targets" PackagePath="Sdk" />
+
+
+
+
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index 9ae51fa..bb91393 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -1,15 +1,7 @@
-
- 1.0.0-alpha.2
- 1.0.0-alpha.2
- Zapto
- https://github.com/zapto-dev/AspNetCore
- Copyright © 2025 Zapto
- zapto, aspnetcore
- https://github.com/zapto-dev/AspNetCore
- https://github.com/zapto-dev/AspNetCore/blob/main/LICENSE
-
+
+
8.0.0
diff --git a/src/Zapto.AspNetCore.CloudFlare/Caching/KeyValueDistributedCache.cs b/src/Zapto.AspNetCore.CloudFlare/Caching/KeyValueDistributedCache.cs
new file mode 100644
index 0000000..81f089e
--- /dev/null
+++ b/src/Zapto.AspNetCore.CloudFlare/Caching/KeyValueDistributedCache.cs
@@ -0,0 +1,46 @@
+using Microsoft.Extensions.Caching.Distributed;
+
+namespace WasmApp.Library.Caching;
+
+public class KeyValueDistributedCache : IDistributedCache
+{
+ public byte[]? Get(string key)
+ {
+ return GetAsync(key).GetAwaiter().GetResult();
+ }
+
+ public Task GetAsync(string key, CancellationToken token = default)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void Set(string key, byte[] value, DistributedCacheEntryOptions options)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void Refresh(string key)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task RefreshAsync(string key, CancellationToken token = default)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void Remove(string key)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task RemoveAsync(string key, CancellationToken token = default)
+ {
+ throw new NotImplementedException();
+ }
+}
diff --git a/src/Zapto.AspNetCore.CloudFlare/CloudFlareKeyValue.wasmlib.js b/src/Zapto.AspNetCore.CloudFlare/CloudFlareKeyValue.wasmlib.js
new file mode 100644
index 0000000..7dfeade
--- /dev/null
+++ b/src/Zapto.AspNetCore.CloudFlare/CloudFlareKeyValue.wasmlib.js
@@ -0,0 +1,17 @@
+import context from "./context.js";
+
+export function get(key) {
+ return context.env.KV.get(key);
+}
+
+export function put(key, value) {
+ return context.env.KV.put(key, value);
+}
+
+export function delete(key) {
+ return context.env.KV.delete(key);
+}
+
+export function list() {
+ return context.env.KV.list();
+}
\ No newline at end of file
diff --git a/src/Zapto.AspNetCore.CloudFlare/NativeMethods.cs b/src/Zapto.AspNetCore.CloudFlare/NativeMethods.cs
new file mode 100644
index 0000000..1fb0d80
--- /dev/null
+++ b/src/Zapto.AspNetCore.CloudFlare/NativeMethods.cs
@@ -0,0 +1,9 @@
+using Zapto.AspNetCore.Wasm.Interop;
+
+namespace WasmApp.Library;
+
+public static partial class NativeMethods
+{
+ [WasmLibraryImport("get", "CloudFlareKeyValue.wasmlib")]
+ public static partial Task GetAsync(string name);
+}
diff --git a/src/Zapto.AspNetCore.CloudFlare/Zapto.AspNetCore.CloudFlare.csproj b/src/Zapto.AspNetCore.CloudFlare/Zapto.AspNetCore.CloudFlare.csproj
new file mode 100644
index 0000000..8effced
--- /dev/null
+++ b/src/Zapto.AspNetCore.CloudFlare/Zapto.AspNetCore.CloudFlare.csproj
@@ -0,0 +1,27 @@
+
+
+
+ net10.0
+ browser-wasm
+ enable
+ true
+ enable
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+ PreserveNewest
+
+
+
+
+
+
+
+
diff --git a/src/Zapto.AspNetCore.NetFx/Http/Features/FeatureCollectionImpl.cs b/src/Zapto.AspNetCore.NetFx/Http/Features/FeatureCollectionImpl.cs
index e873037..b54de61 100644
--- a/src/Zapto.AspNetCore.NetFx/Http/Features/FeatureCollectionImpl.cs
+++ b/src/Zapto.AspNetCore.NetFx/Http/Features/FeatureCollectionImpl.cs
@@ -48,7 +48,7 @@ public void Reset()
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)Features).GetEnumerator();
- public TFeature Get() => (TFeature) this[typeof (TFeature)];
+ public TFeature? Get() => (TFeature?)this[typeof(TFeature)];
public void Set(TFeature instance) => this[typeof (TFeature)] = instance!;
diff --git a/src/Zapto.AspNetCore.Wasm.Interop/ModuleInitializer.cs b/src/Zapto.AspNetCore.Wasm.Interop/ModuleInitializer.cs
new file mode 100644
index 0000000..5a4fc07
--- /dev/null
+++ b/src/Zapto.AspNetCore.Wasm.Interop/ModuleInitializer.cs
@@ -0,0 +1,18 @@
+using System.Runtime.CompilerServices;
+
+namespace Zapto.AspNetCore.Wasm.Interop;
+
+///
+/// Module initializer to ensure WasmLibraryRuntime exports are preserved by the linker.
+///
+internal static class ModuleInitializer
+{
+ ///
+ /// Forces the runtime type to be referenced, preventing tree-shaking of exports.
+ ///
+ [ModuleInitializer]
+ public static void Initialize()
+ {
+ _ = typeof(WasmLibraryRuntime);
+ }
+}
diff --git a/src/Zapto.AspNetCore.Wasm.Interop/WasmExportAttribute.cs b/src/Zapto.AspNetCore.Wasm.Interop/WasmExportAttribute.cs
new file mode 100644
index 0000000..abe899a
--- /dev/null
+++ b/src/Zapto.AspNetCore.Wasm.Interop/WasmExportAttribute.cs
@@ -0,0 +1,28 @@
+namespace Zapto.AspNetCore.Wasm.Interop;
+
+///
+/// Marks a static method as a WASM export that should be forwarded from the main assembly.
+/// The source generator will create a forwarding method with [UnmanagedCallersOnly] in the main assembly.
+///
+///
+/// This attribute is used to mark methods that need to be callable from JavaScript.
+/// Since NativeAOT only exports [UnmanagedCallersOnly] methods from the main assembly,
+/// the source generator will generate forwarding methods in the consuming assembly.
+///
+[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
+public sealed class WasmExportAttribute : Attribute
+{
+ ///
+ /// Creates a new instance of .
+ ///
+ /// The entry point name for the WASM export.
+ public WasmExportAttribute(string entryPoint)
+ {
+ EntryPoint = entryPoint;
+ }
+
+ ///
+ /// Gets the entry point name for the WASM export.
+ ///
+ public string EntryPoint { get; }
+}
diff --git a/src/Zapto.AspNetCore.Wasm.Interop/WasmLibraryImportAttribute.cs b/src/Zapto.AspNetCore.Wasm.Interop/WasmLibraryImportAttribute.cs
new file mode 100644
index 0000000..7eb6edc
--- /dev/null
+++ b/src/Zapto.AspNetCore.Wasm.Interop/WasmLibraryImportAttribute.cs
@@ -0,0 +1,31 @@
+namespace Zapto.AspNetCore.Wasm.Interop;
+
+///
+/// Marks a partial method as a WASM library import.
+/// The source generator will generate the implementation that marshals
+/// strings to/from WASM memory and handles async Task completion via callbacks.
+///
+[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
+public sealed class WasmLibraryImportAttribute : Attribute
+{
+ ///
+ /// The name of the JavaScript function to call.
+ ///
+ public string FunctionName { get; }
+
+ ///
+ /// The module name prefix. The WASM import will be named "{ModuleName}_{FunctionName}".
+ ///
+ public string ModuleName { get; }
+
+ ///
+ /// Creates a new WasmLibraryImportAttribute.
+ ///
+ /// The JavaScript function name.
+ /// The module name prefix (default: "lib").
+ public WasmLibraryImportAttribute(string functionName, string moduleName = "lib")
+ {
+ FunctionName = functionName;
+ ModuleName = moduleName;
+ }
+}
diff --git a/src/Zapto.AspNetCore.Wasm.Interop/WasmLibraryRuntime.cs b/src/Zapto.AspNetCore.Wasm.Interop/WasmLibraryRuntime.cs
new file mode 100644
index 0000000..830c88f
--- /dev/null
+++ b/src/Zapto.AspNetCore.Wasm.Interop/WasmLibraryRuntime.cs
@@ -0,0 +1,92 @@
+using System.Collections.Concurrent;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.InteropServices;
+
+namespace Zapto.AspNetCore.Wasm.Interop;
+
+///
+/// Represents the result of a WASM library callback.
+///
+public readonly struct WasmCallbackResult
+{
+ public readonly nint Ptr;
+ public readonly int Length;
+
+ public WasmCallbackResult(nint ptr, int length)
+ {
+ Ptr = ptr;
+ Length = length;
+ }
+}
+
+///
+/// Runtime support for WASM library imports.
+/// Handles callback registration for async operations.
+///
+public static class WasmLibraryRuntime
+{
+ private static int _nextCallbackId = 1;
+ private static readonly ConcurrentDictionary> _pendingCallbacks = new();
+
+ ///
+ /// Gets the number of pending callbacks waiting for JavaScript completion.
+ ///
+ public static int PendingCallbackCount => _pendingCallbacks.Count;
+
+ ///
+ /// Registers a callback and returns the callback ID.
+ /// This method also ensures the callback methods are preserved by the linker.
+ ///
+ [DynamicDependency(nameof(CompleteCallback), typeof(WasmLibraryRuntime))]
+ [DynamicDependency(nameof(CompleteCallbackError), typeof(WasmLibraryRuntime))]
+ public static int RegisterCallback(TaskCompletionSource tcs)
+ {
+ var id = Interlocked.Increment(ref _nextCallbackId);
+ _pendingCallbacks[id] = tcs;
+ return id;
+ }
+
+ ///
+ /// Called from JavaScript when an async operation completes successfully.
+ /// This method completes the callback and immediately runs all pending continuations.
+ ///
+ /// The callback ID returned from RegisterCallback.
+ /// Pointer to the UTF-8 result string (0 if null).
+ /// Length of the result in bytes.
+ [WasmExport("WasmLibrary_CompleteCallback")]
+ public static unsafe void CompleteCallback(int callbackId, byte* resultPtr, int resultLen)
+ {
+ if (!_pendingCallbacks.TryRemove(callbackId, out var tcs))
+ return;
+
+ tcs.TrySetResult(new WasmCallbackResult((nint)resultPtr, resultLen));
+
+ WasmSynchronizationContext.Instance.PumpAll();
+ }
+
+ ///
+ /// Called from JavaScript when an async operation fails.
+ /// This method completes the callback with an error and immediately runs all pending continuations.
+ ///
+ /// The callback ID returned from RegisterCallback.
+ /// Pointer to the UTF-8 error message.
+ /// Length of the error message in bytes.
+ [WasmExport("WasmLibrary_CompleteCallbackError")]
+ public static unsafe void CompleteCallbackError(int callbackId, byte* errorPtr, int errorLen)
+ {
+ if (!_pendingCallbacks.TryRemove(callbackId, out var tcs))
+ {
+ return;
+ }
+
+ string errorMessage = "Unknown error";
+ if (errorPtr != null && errorLen > 0)
+ {
+ errorMessage = System.Text.Encoding.UTF8.GetString(errorPtr, errorLen);
+ }
+
+ tcs.TrySetException(new InvalidOperationException(errorMessage));
+
+ WasmSynchronizationContext.Instance.PumpAll();
+ }
+}
diff --git a/src/Zapto.AspNetCore.Wasm.Interop/WasmLibraryRuntime.rd.xml b/src/Zapto.AspNetCore.Wasm.Interop/WasmLibraryRuntime.rd.xml
new file mode 100644
index 0000000..1db2642
--- /dev/null
+++ b/src/Zapto.AspNetCore.Wasm.Interop/WasmLibraryRuntime.rd.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Zapto.AspNetCore.Wasm.Interop/WasmSynchronizationContext.cs b/src/Zapto.AspNetCore.Wasm.Interop/WasmSynchronizationContext.cs
new file mode 100644
index 0000000..19dfe14
--- /dev/null
+++ b/src/Zapto.AspNetCore.Wasm.Interop/WasmSynchronizationContext.cs
@@ -0,0 +1,93 @@
+using System.Collections.Concurrent;
+
+namespace Zapto.AspNetCore.Wasm.Interop;
+
+///
+/// A SynchronizationContext for single-threaded WASM that queues work items
+/// and allows them to be pumped from JavaScript.
+///
+public sealed class WasmSynchronizationContext : SynchronizationContext
+{
+ private static WasmSynchronizationContext? _current;
+ private readonly ConcurrentQueue<(SendOrPostCallback Callback, object? State)> _workQueue = new();
+
+ ///
+ /// Gets the current WASM synchronization context, creating one if necessary.
+ ///
+ public static WasmSynchronizationContext Instance => _current ??= new WasmSynchronizationContext();
+
+ ///
+ /// Installs this synchronization context as the current context.
+ ///
+ public static void Install()
+ {
+ SetSynchronizationContext(Instance);
+ }
+
+ ///
+ /// Gets whether there is pending work in the queue.
+ ///
+ public bool HasPendingWork => !_workQueue.IsEmpty;
+
+ ///
+ /// Processes all pending work items in the queue.
+ ///
+ /// The number of work items processed.
+ public int PumpAll()
+ {
+ int count = 0;
+ while (_workQueue.TryDequeue(out var item))
+ {
+ try
+ {
+ item.Callback(item.State);
+ }
+ catch
+ {
+ // Swallow exceptions to prevent breaking the pump loop
+ // In production, you might want to log these
+ }
+ count++;
+ }
+ return count;
+ }
+
+ ///
+ /// Processes a single pending work item.
+ ///
+ /// True if a work item was processed, false if the queue was empty.
+ public bool PumpOne()
+ {
+ if (_workQueue.TryDequeue(out var item))
+ {
+ try
+ {
+ item.Callback(item.State);
+ }
+ catch
+ {
+ }
+ return true;
+ }
+ return false;
+ }
+
+ ///
+ public override void Post(SendOrPostCallback d, object? state)
+ {
+ _workQueue.Enqueue((d, state));
+ }
+
+ ///
+ public override void Send(SendOrPostCallback d, object? state)
+ {
+ // In single-threaded WASM, we can execute synchronously
+ d(state);
+ }
+
+ ///
+ public override SynchronizationContext CreateCopy()
+ {
+ return this; // Single instance for WASM
+ }
+}
diff --git a/src/Zapto.AspNetCore.Wasm.Interop/Zapto.AspNetCore.Wasm.Interop.csproj b/src/Zapto.AspNetCore.Wasm.Interop/Zapto.AspNetCore.Wasm.Interop.csproj
new file mode 100644
index 0000000..deec16d
--- /dev/null
+++ b/src/Zapto.AspNetCore.Wasm.Interop/Zapto.AspNetCore.Wasm.Interop.csproj
@@ -0,0 +1,16 @@
+
+
+
+ net10.0
+ enable
+ enable
+ true
+ $(NoWarn);CA2255
+
+
+
+
+
+
+
+
diff --git a/src/Zapto.AspNetCore.Wasm.SourceGenerator/IsExternalInit.cs b/src/Zapto.AspNetCore.Wasm.SourceGenerator/IsExternalInit.cs
new file mode 100644
index 0000000..0700cdb
--- /dev/null
+++ b/src/Zapto.AspNetCore.Wasm.SourceGenerator/IsExternalInit.cs
@@ -0,0 +1,7 @@
+// ReSharper disable once CheckNamespace
+namespace System.Runtime.CompilerServices;
+
+///
+/// Polyfill for init-only properties in netstandard2.0.
+///
+internal static class IsExternalInit;
diff --git a/src/Zapto.AspNetCore.Wasm.SourceGenerator/WasmExportsGenerator.cs b/src/Zapto.AspNetCore.Wasm.SourceGenerator/WasmExportsGenerator.cs
new file mode 100644
index 0000000..9607b77
--- /dev/null
+++ b/src/Zapto.AspNetCore.Wasm.SourceGenerator/WasmExportsGenerator.cs
@@ -0,0 +1,357 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Text;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Text;
+
+namespace Zapto.AspNetCore.Wasm.SourceGenerator;
+
+///
+/// Incremental source generator that finds all methods marked with [WasmExport]
+/// in referenced assemblies and generates forwarding exports with [UnmanagedCallersOnly] in the main assembly.
+///
+[Generator(LanguageNames.CSharp)]
+public sealed class WasmExportsGenerator : IIncrementalGenerator
+{
+ ///
+ /// WasmExport attribute from the main Wasm assembly.
+ ///
+ private const string WasmExportAttribute = "Zapto.AspNetCore.Wasm.WasmExportAttribute";
+
+ ///
+ /// WasmExport attribute from the Interop assembly (for runtime exports).
+ ///
+ private const string WasmExportInteropAttribute = "Zapto.AspNetCore.Wasm.Interop.WasmExportAttribute";
+
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ var assemblyNameProvider = context.CompilationProvider
+ .Select(static (compilation, _) => GetRootNamespace(compilation));
+
+ var exportsFromReferences = context.CompilationProvider
+ .Select(static (compilation, ct) => GetExportsFromReferences(compilation, ct));
+
+ var combined = assemblyNameProvider.Combine(exportsFromReferences);
+
+ context.RegisterSourceOutput(combined, static (spc, source) =>
+ {
+ var (rootNamespace, exports) = source;
+
+ if (exports.IsEmpty)
+ {
+ return;
+ }
+
+ var code = GenerateWasmExports(rootNamespace, exports);
+ spc.AddSource("WasmExports.g.cs", SourceText.From(code, Encoding.UTF8));
+ });
+ }
+
+ private static string GetRootNamespace(Compilation compilation)
+ {
+ // Try to get from assembly attributes or fall back to assembly name
+ return compilation.AssemblyName ?? "GeneratedExports";
+ }
+
+ private static ImmutableArray GetExportsFromReferences(
+ Compilation compilation,
+ System.Threading.CancellationToken cancellationToken)
+ {
+ var exports = ImmutableArray.CreateBuilder();
+ var processedMethods = new HashSet();
+
+ // Look through all referenced assemblies
+ foreach (var reference in compilation.References)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (compilation.GetAssemblyOrModuleSymbol(reference) is not IAssemblySymbol assemblySymbol)
+ {
+ continue;
+ }
+
+ // Skip system assemblies and known non-relevant assemblies
+ var assemblyName = assemblySymbol.Name;
+ if (ShouldSkipAssembly(assemblyName))
+ {
+ continue;
+ }
+
+ FindExportsInNamespace(assemblySymbol.GlobalNamespace, exports, processedMethods, cancellationToken);
+ }
+
+ return exports.ToImmutable();
+ }
+
+ private static bool ShouldSkipAssembly(string assemblyName)
+ {
+ return assemblyName.StartsWith("System", StringComparison.Ordinal)
+ || assemblyName.StartsWith("Microsoft", StringComparison.Ordinal)
+ || assemblyName.StartsWith("netstandard", StringComparison.Ordinal)
+ || assemblyName.StartsWith("mscorlib", StringComparison.Ordinal)
+ || assemblyName.StartsWith("WindowsBase", StringComparison.Ordinal)
+ || assemblyName.StartsWith("PresentationCore", StringComparison.Ordinal);
+ }
+
+ private static void FindExportsInNamespace(
+ INamespaceSymbol namespaceSymbol,
+ ImmutableArray.Builder exports,
+ HashSet processedMethods,
+ System.Threading.CancellationToken cancellationToken)
+ {
+ foreach (var member in namespaceSymbol.GetMembers())
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (member is INamespaceSymbol nestedNamespace)
+ {
+ FindExportsInNamespace(nestedNamespace, exports, processedMethods, cancellationToken);
+ }
+ else if (member is INamedTypeSymbol typeSymbol)
+ {
+ FindExportsInType(typeSymbol, exports, processedMethods, cancellationToken);
+ }
+ }
+ }
+
+ private static void FindExportsInType(
+ INamedTypeSymbol typeSymbol,
+ ImmutableArray.Builder exports,
+ HashSet processedMethods,
+ System.Threading.CancellationToken cancellationToken)
+ {
+ foreach (var nestedType in typeSymbol.GetTypeMembers())
+ {
+ FindExportsInType(nestedType, exports, processedMethods, cancellationToken);
+ }
+
+ foreach (var member in typeSymbol.GetMembers())
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (member is not IMethodSymbol methodSymbol)
+ {
+ continue;
+ }
+
+ if (!methodSymbol.IsStatic)
+ {
+ continue;
+ }
+
+ var wasmExportAttr = methodSymbol.GetAttributes()
+ .FirstOrDefault(attr =>
+ {
+ var attrName = attr.AttributeClass?.ToDisplayString();
+ return attrName == WasmExportAttribute || attrName == WasmExportInteropAttribute;
+ });
+
+ if (wasmExportAttr == null)
+ {
+ continue;
+ }
+
+ var entryPoint = GetEntryPointFromAttribute(wasmExportAttr);
+ if (string.IsNullOrEmpty(entryPoint))
+ {
+ entryPoint = methodSymbol.Name;
+ }
+
+ var key = $"{entryPoint}";
+ if (!processedMethods.Add(key))
+ {
+ continue;
+ }
+
+ var exportInfo = new WasmExportInfo(
+ EntryPoint: entryPoint!,
+ ContainingType: typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
+ MethodName: methodSymbol.Name,
+ ReturnType: methodSymbol.ReturnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
+ Parameters: GetParameters(methodSymbol),
+ IsUnsafe: RequiresUnsafe(methodSymbol));
+
+ exports.Add(exportInfo);
+ }
+ }
+
+ private static string? GetEntryPointFromAttribute(AttributeData attribute)
+ {
+ // First check constructor arguments (WasmExport uses constructor parameter)
+ if (attribute.ConstructorArguments.Length > 0 && attribute.ConstructorArguments[0].Value is string constructorEntryPoint)
+ {
+ return constructorEntryPoint;
+ }
+
+ // Fall back to named arguments
+ foreach (var namedArg in attribute.NamedArguments)
+ {
+ if (namedArg.Key == "EntryPoint" && namedArg.Value.Value is string entryPoint)
+ {
+ return entryPoint;
+ }
+ }
+ return null;
+ }
+
+ private static ImmutableArray GetParameters(IMethodSymbol method)
+ {
+ var builder = ImmutableArray.CreateBuilder(method.Parameters.Length);
+
+ foreach (var param in method.Parameters)
+ {
+ builder.Add(new ParameterInfo(
+ Name: param.Name,
+ Type: param.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
+ RefKind: param.RefKind));
+ }
+
+ return builder.ToImmutable();
+ }
+
+ private static bool RequiresUnsafe(IMethodSymbol method)
+ {
+ // Check if return type is pointer
+ if (method.ReturnType is IPointerTypeSymbol)
+ {
+ return true;
+ }
+
+ // Check if any parameter is a pointer
+ foreach (var param in method.Parameters)
+ {
+ if (param.Type is IPointerTypeSymbol)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static string GenerateWasmExports(string rootNamespace, ImmutableArray exports)
+ {
+ var sb = new StringBuilder();
+
+ sb.AppendLine("// ");
+ sb.AppendLine("// This file is generated by Zapto.AspNetCore.Wasm.SourceGenerator.");
+ sb.AppendLine("// Do not modify this file directly.");
+ sb.AppendLine("// ");
+ sb.AppendLine();
+ sb.AppendLine("#nullable enable");
+ sb.AppendLine();
+ sb.AppendLine("using System;");
+ sb.AppendLine("using System.Runtime.InteropServices;");
+ sb.AppendLine();
+ sb.AppendLine($"namespace {rootNamespace};");
+ sb.AppendLine();
+ sb.AppendLine("/// ");
+ sb.AppendLine("/// WASM exports that forward to referenced assemblies.");
+ sb.AppendLine("/// These must be in the main assembly for ILC to export them.");
+ sb.AppendLine("/// ");
+ sb.AppendLine("internal static class WasmExports");
+ sb.AppendLine("{");
+
+ for (var i = 0; i < exports.Length; i++)
+ {
+ var export = exports[i];
+
+ if (i > 0)
+ {
+ sb.AppendLine();
+ }
+
+ sb.AppendLine($" [UnmanagedCallersOnly(EntryPoint = \"{export.EntryPoint}\")]");
+
+ var unsafeModifier = export.IsUnsafe ? "unsafe " : "";
+ var parameters = FormatParameters(export.Parameters);
+ var arguments = FormatArguments(export.Parameters);
+
+ sb.AppendLine($" public static {unsafeModifier}{export.ReturnType} {export.EntryPoint}({parameters})");
+
+ if (export.ReturnType == "void")
+ {
+ sb.AppendLine($" => {export.ContainingType}.{export.MethodName}({arguments});");
+ }
+ else
+ {
+ sb.AppendLine($" => {export.ContainingType}.{export.MethodName}({arguments});");
+ }
+ }
+
+ sb.AppendLine("}");
+
+ return sb.ToString();
+ }
+
+ private static string FormatParameters(ImmutableArray parameters)
+ {
+ if (parameters.IsEmpty)
+ {
+ return "";
+ }
+
+ var parts = new string[parameters.Length];
+ for (var i = 0; i < parameters.Length; i++)
+ {
+ var param = parameters[i];
+ var refKindPrefix = param.RefKind switch
+ {
+ RefKind.Ref => "ref ",
+ RefKind.Out => "out ",
+ RefKind.In => "in ",
+ _ => ""
+ };
+ parts[i] = $"{refKindPrefix}{param.Type} {param.Name}";
+ }
+
+ return string.Join(", ", parts);
+ }
+
+ private static string FormatArguments(ImmutableArray parameters)
+ {
+ if (parameters.IsEmpty)
+ {
+ return "";
+ }
+
+ var parts = new string[parameters.Length];
+ for (var i = 0; i < parameters.Length; i++)
+ {
+ var param = parameters[i];
+ var refKindPrefix = param.RefKind switch
+ {
+ RefKind.Ref => "ref ",
+ RefKind.Out => "out ",
+ RefKind.In => "in ",
+ _ => ""
+ };
+ parts[i] = $"{refKindPrefix}{param.Name}";
+ }
+
+ return string.Join(", ", parts);
+ }
+}
+
+///
+/// Information about a WASM export method.
+///
+internal sealed record WasmExportInfo(
+ string EntryPoint,
+ string ContainingType,
+ string MethodName,
+ string ReturnType,
+ ImmutableArray Parameters,
+ bool IsUnsafe);
+
+///
+/// Information about a method parameter.
+///
+internal sealed record ParameterInfo(
+ string Name,
+ string Type,
+ RefKind RefKind);
diff --git a/src/Zapto.AspNetCore.Wasm.SourceGenerator/WasmLibraryImportGenerator.cs b/src/Zapto.AspNetCore.Wasm.SourceGenerator/WasmLibraryImportGenerator.cs
new file mode 100644
index 0000000..04ebac3
--- /dev/null
+++ b/src/Zapto.AspNetCore.Wasm.SourceGenerator/WasmLibraryImportGenerator.cs
@@ -0,0 +1,271 @@
+using System.Collections.Immutable;
+using System.Linq;
+using System.Text;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Text;
+
+namespace Zapto.AspNetCore.Wasm.SourceGenerator;
+
+///
+/// Incremental source generator that generates implementations for methods marked with [WasmLibraryImport].
+///
+[Generator(LanguageNames.CSharp)]
+public sealed class WasmLibraryImportGenerator : IIncrementalGenerator
+{
+ private const string WasmLibraryImportAttribute = "Zapto.AspNetCore.Wasm.Interop.WasmLibraryImportAttribute";
+
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ // Find all partial methods with [WasmLibraryImport] attribute
+ var methodsWithAttribute = context.SyntaxProvider
+ .ForAttributeWithMetadataName(
+ WasmLibraryImportAttribute,
+ predicate: static (node, _) => node is MethodDeclarationSyntax,
+ transform: static (context, ct) => GetMethodInfo(context, ct))
+ .Where(static m => m is not null)
+ .Select(static (m, _) => m!.Value);
+
+ // Collect all methods and generate
+ var collectedMethods = methodsWithAttribute.Collect();
+
+ context.RegisterSourceOutput(collectedMethods, GenerateSource);
+ }
+
+ private static void GenerateSource(SourceProductionContext spc, ImmutableArray methods)
+ {
+ if (methods.IsEmpty)
+ {
+ return;
+ }
+
+ var groupedByType = methods.GroupBy(m => (m.Namespace, m.TypeName));
+
+ foreach (var group in groupedByType)
+ {
+ var code = GenerateImplementation(group.Key.Namespace, group.Key.TypeName, group.ToArray());
+ var fileName = $"{group.Key.Namespace}.{group.Key.TypeName}.WasmLibraryImports.g.cs";
+ spc.AddSource(fileName, SourceText.From(code, Encoding.UTF8));
+ }
+ }
+
+ private static WasmLibraryImportInfo? GetMethodInfo(GeneratorAttributeSyntaxContext context, System.Threading.CancellationToken ct)
+ {
+ if (context.TargetSymbol is not IMethodSymbol methodSymbol)
+ {
+ return null;
+ }
+
+ if (!methodSymbol.IsPartialDefinition)
+ {
+ return null;
+ }
+
+ var attributeData = context.Attributes.FirstOrDefault();
+ if (attributeData is null)
+ {
+ return null;
+ }
+
+ string? functionName = null;
+ string? moduleName = null;
+
+ if (attributeData.ConstructorArguments.Length >= 1)
+ {
+ functionName = attributeData.ConstructorArguments[0].Value as string;
+ }
+ if (attributeData.ConstructorArguments.Length >= 2)
+ {
+ moduleName = attributeData.ConstructorArguments[1].Value as string;
+ }
+
+ if (string.IsNullOrEmpty(functionName))
+ {
+ return null;
+ }
+
+ var containingType = methodSymbol.ContainingType;
+ var namespaceName = containingType.ContainingNamespace.ToDisplayString();
+ var typeName = containingType.Name;
+
+ var returnType = methodSymbol.ReturnType;
+ var isAsync = returnType.OriginalDefinition.ToDisplayString() == "System.Threading.Tasks.Task"
+ || returnType.ToDisplayString() == "System.Threading.Tasks.Task";
+
+ string? asyncResultType = null;
+ if (isAsync && returnType is INamedTypeSymbol namedType && namedType.TypeArguments.Length == 1)
+ {
+ asyncResultType = namedType.TypeArguments[0].ToDisplayString();
+ }
+
+ var parameters = methodSymbol.Parameters
+ .Select(p => new WasmLibraryImportParameterInfo(p.Name, p.Type.ToDisplayString()))
+ .ToImmutableArray();
+
+ return new WasmLibraryImportInfo(
+ namespaceName,
+ typeName,
+ methodSymbol.Name,
+ functionName!,
+ moduleName ?? "default",
+ returnType.ToDisplayString(),
+ isAsync,
+ asyncResultType,
+ parameters);
+ }
+
+ private static string GenerateImplementation(string namespaceName, string typeName, WasmLibraryImportInfo[] methods)
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine("// ");
+ sb.AppendLine("#nullable enable");
+ sb.AppendLine();
+ sb.AppendLine("using System.Runtime.InteropServices;");
+ sb.AppendLine("using System.Text;");
+ sb.AppendLine("using Zapto.AspNetCore.Wasm.Interop;");
+ sb.AppendLine();
+ sb.AppendLine($"namespace {namespaceName};");
+ sb.AppendLine();
+ sb.AppendLine($"partial class {typeName}");
+ sb.AppendLine("{");
+
+ foreach (var method in methods)
+ {
+ GenerateMethodImplementation(sb, method);
+ }
+
+ // Generate the DllImport declarations
+ sb.AppendLine(" // Native WASM imports");
+ foreach (var method in methods)
+ {
+ GenerateDllImport(sb, method);
+ }
+
+ sb.AppendLine("}");
+ return sb.ToString();
+ }
+
+ private static void GenerateDllImport(StringBuilder sb, WasmLibraryImportInfo method)
+ {
+ var entryPoint = $"{method.ModuleName}_{method.FunctionName}";
+
+ if (method.IsAsync)
+ {
+ // Async: (byte* arg0Ptr, int arg0Len, int callbackId) -> void
+ sb.AppendLine($" [DllImport(\"*\", EntryPoint = \"{entryPoint}\")]");
+ sb.AppendLine($" private static extern unsafe void {method.MethodName}_Native(byte* arg0Ptr, int arg0Len, int callbackId);");
+ }
+ else
+ {
+ // Sync: (byte* arg0Ptr, int arg0Len, int* resultLenPtr) -> byte*
+ sb.AppendLine($" [DllImport(\"*\", EntryPoint = \"{entryPoint}\")]");
+ sb.AppendLine($" private static extern unsafe byte* {method.MethodName}_Native(byte* arg0Ptr, int arg0Len, int* resultLenPtr);");
+ }
+ sb.AppendLine();
+ }
+
+ private static void GenerateMethodImplementation(StringBuilder sb, WasmLibraryImportInfo method)
+ {
+ // Build parameter list
+ var paramList = string.Join(", ", method.Parameters.Select(p => $"{p.Type} {p.Name}"));
+ var firstParam = method.Parameters.Length > 0 ? method.Parameters[0].Name : null;
+
+ sb.AppendLine($" public static partial {method.ReturnType} {method.MethodName}({paramList})");
+ sb.AppendLine(" {");
+
+ if (method.IsAsync)
+ {
+ GenerateAsyncMethodBody(sb, method, firstParam);
+ }
+ else
+ {
+ GenerateSyncMethodBody(sb, method, firstParam);
+ }
+
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ }
+
+ private static void GenerateSyncMethodBody(StringBuilder sb, WasmLibraryImportInfo method, string? firstParam)
+ {
+ sb.AppendLine(" unsafe");
+ sb.AppendLine(" {");
+
+ if (firstParam != null)
+ {
+ sb.AppendLine($" var argBytes = Encoding.UTF8.GetBytes({firstParam} ?? \"\");");
+ sb.AppendLine(" int resultLen = 0;");
+ sb.AppendLine(" fixed (byte* argPtr = argBytes)");
+ sb.AppendLine(" {");
+ sb.AppendLine($" var resultPtr = {method.MethodName}_Native(argPtr, argBytes.Length, &resultLen);");
+ sb.AppendLine(" if (resultPtr == null || resultLen <= 0)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" return null;");
+ sb.AppendLine(" }");
+ sb.AppendLine(" return Encoding.UTF8.GetString(resultPtr, resultLen);");
+ sb.AppendLine(" }");
+ }
+ else
+ {
+ sb.AppendLine(" int resultLen = 0;");
+ sb.AppendLine($" var resultPtr = {method.MethodName}_Native(null, 0, &resultLen);");
+ sb.AppendLine(" if (resultPtr == null || resultLen <= 0)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" return null;");
+ sb.AppendLine(" }");
+ sb.AppendLine(" return Encoding.UTF8.GetString(resultPtr, resultLen);");
+ }
+
+ sb.AppendLine(" }");
+ }
+
+ private static void GenerateAsyncMethodBody(StringBuilder sb, WasmLibraryImportInfo method, string? firstParam)
+ {
+ sb.AppendLine(" var tcs = new TaskCompletionSource();");
+ sb.AppendLine(" var callbackId = WasmLibraryRuntime.RegisterCallback(tcs);");
+ sb.AppendLine();
+ sb.AppendLine(" unsafe");
+ sb.AppendLine(" {");
+
+ if (firstParam != null)
+ {
+ sb.AppendLine($" var argBytes = Encoding.UTF8.GetBytes({firstParam} ?? \"\");");
+ sb.AppendLine(" fixed (byte* argPtr = argBytes)");
+ sb.AppendLine(" {");
+ sb.AppendLine($" {method.MethodName}_Native(argPtr, argBytes.Length, callbackId);");
+ sb.AppendLine(" }");
+ }
+ else
+ {
+ sb.AppendLine($" {method.MethodName}_Native(null, 0, callbackId);");
+ }
+
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ sb.AppendLine(" return tcs.Task.ContinueWith(t =>");
+ sb.AppendLine(" {");
+ sb.AppendLine(" var result = t.Result;");
+ sb.AppendLine(" if (result.Ptr == 0 || result.Length <= 0)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" return null;");
+ sb.AppendLine(" }");
+ sb.AppendLine(" unsafe");
+ sb.AppendLine(" {");
+ sb.AppendLine(" return Encoding.UTF8.GetString((byte*)result.Ptr, result.Length);");
+ sb.AppendLine(" }");
+ sb.AppendLine(" }, TaskContinuationOptions.ExecuteSynchronously);");
+ }
+}
+
+internal readonly record struct WasmLibraryImportInfo(
+ string Namespace,
+ string TypeName,
+ string MethodName,
+ string FunctionName,
+ string ModuleName,
+ string ReturnType,
+ bool IsAsync,
+ string? AsyncResultType,
+ ImmutableArray Parameters);
+
+internal readonly record struct WasmLibraryImportParameterInfo(string Name, string Type);
diff --git a/src/Zapto.AspNetCore.Wasm.SourceGenerator/WebApplicationRunAsyncInterceptorGenerator.cs b/src/Zapto.AspNetCore.Wasm.SourceGenerator/WebApplicationRunAsyncInterceptorGenerator.cs
new file mode 100644
index 0000000..0ed4110
--- /dev/null
+++ b/src/Zapto.AspNetCore.Wasm.SourceGenerator/WebApplicationRunAsyncInterceptorGenerator.cs
@@ -0,0 +1,268 @@
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Text;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Text;
+
+namespace Zapto.AspNetCore.Wasm.SourceGenerator;
+
+[Generator(LanguageNames.CSharp)]
+public sealed class WebApplicationRunAsyncInterceptorGenerator : IIncrementalGenerator
+{
+ private const string GeneratedNamespace = "Zapto.AspNetCore.Wasm.Generated";
+
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ var interceptedInvocations = context.SyntaxProvider
+ .CreateSyntaxProvider(
+ predicate: static (node, _) => node is InvocationExpressionSyntax,
+ transform: static (syntaxContext, cancellationToken) => TryGetIntercept(syntaxContext, cancellationToken))
+ .Where(static interceptor => interceptor.HasValue)
+ .Select(static (interceptor, _) => interceptor!.Value)
+ .Collect();
+
+ context.RegisterSourceOutput(interceptedInvocations, static (sourceProductionContext, interceptors) =>
+ {
+ EmitInterceptors(sourceProductionContext, interceptors);
+ });
+ }
+
+ private static InterceptInfo? TryGetIntercept(GeneratorSyntaxContext syntaxContext, System.Threading.CancellationToken cancellationToken)
+ {
+ if (syntaxContext.Node is not InvocationExpressionSyntax invocation)
+ {
+ return null;
+ }
+
+ var symbolInfo = syntaxContext.SemanticModel.GetSymbolInfo(invocation, cancellationToken);
+ if (symbolInfo.Symbol is not IMethodSymbol methodSymbol)
+ {
+ return null;
+ }
+
+ var targetType = GetInterceptTargetType(methodSymbol, invocation.ArgumentList.Arguments.Count);
+ if (targetType is null)
+ {
+ return null;
+ }
+
+ var interceptableLocation = syntaxContext.SemanticModel.GetInterceptableLocation(invocation, cancellationToken);
+ if (interceptableLocation is null)
+ {
+ return null;
+ }
+
+ return new InterceptInfo(
+ TargetType: targetType.Value,
+ AttributeSyntax: interceptableLocation.GetInterceptsLocationAttributeSyntax());
+ }
+
+ private static InterceptTargetType? GetInterceptTargetType(IMethodSymbol methodSymbol, int argumentCount)
+ {
+ var methodToCheck = methodSymbol.ReducedFrom ?? methodSymbol;
+ var containingTypeName = methodToCheck.ContainingType.ToDisplayString();
+
+ if (containingTypeName == "Microsoft.AspNetCore.Builder.WebApplication")
+ {
+ if (methodToCheck.Name == "RunAsync" && argumentCount == 0)
+ {
+ return InterceptTargetType.WebApplicationRunAsync;
+ }
+
+ if (methodToCheck.Name == "CreateBuilder")
+ {
+ if (methodToCheck.Parameters.Length == 0)
+ {
+ return InterceptTargetType.CreateBuilderNoArgs;
+ }
+
+ if (methodToCheck.Parameters.Length == 1)
+ {
+ var parameterType = methodToCheck.Parameters[0].Type.ToDisplayString();
+ if (parameterType == "string[]")
+ {
+ return InterceptTargetType.CreateBuilderArgs;
+ }
+
+ if (parameterType == "Microsoft.AspNetCore.Builder.WebApplicationOptions")
+ {
+ return InterceptTargetType.CreateBuilderOptions;
+ }
+ }
+ }
+
+ if (methodToCheck.Name == "CreateSlimBuilder")
+ {
+ if (methodToCheck.Parameters.Length == 0)
+ {
+ return InterceptTargetType.CreateSlimBuilderNoArgs;
+ }
+
+ if (methodToCheck.Parameters.Length == 1
+ && methodToCheck.Parameters[0].Type.ToDisplayString() == "Microsoft.AspNetCore.Builder.WebApplicationOptions")
+ {
+ return InterceptTargetType.CreateSlimBuilderOptions;
+ }
+ }
+
+ return null;
+ }
+
+ if (containingTypeName == "Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions"
+ && methodToCheck.Name == "RunAsync"
+ && methodToCheck.Parameters.Length > 0
+ && methodToCheck.Parameters[0].Type.ToDisplayString() == "Microsoft.Extensions.Hosting.IHost")
+ {
+ return InterceptTargetType.HostRunAsync;
+ }
+
+ return null;
+ }
+
+ private static void EmitInterceptors(
+ SourceProductionContext sourceProductionContext,
+ ImmutableArray interceptors)
+ {
+ if (interceptors.IsDefaultOrEmpty)
+ {
+ return;
+ }
+
+ var attributesByTarget = new Dictionary>();
+
+ foreach (var interceptor in interceptors)
+ {
+ if (!attributesByTarget.TryGetValue(interceptor.TargetType, out var attributes))
+ {
+ attributes = new HashSet();
+ attributesByTarget[interceptor.TargetType] = attributes;
+ }
+
+ attributes.Add(interceptor.AttributeSyntax);
+ }
+
+ if (attributesByTarget.Count == 0)
+ {
+ return;
+ }
+
+ var source = GenerateInterceptorSource(attributesByTarget);
+ sourceProductionContext.AddSource("WebApplicationRunAsyncInterceptors.g.cs", SourceText.From(source, Encoding.UTF8));
+ }
+
+ private static string GenerateInterceptorSource(Dictionary> attributesByTarget)
+ {
+ var builder = new StringBuilder();
+
+ builder.AppendLine("// ");
+ builder.AppendLine("// This file is generated by Zapto.AspNetCore.Wasm.SourceGenerator.");
+ builder.AppendLine("// Do not modify this file directly.");
+ builder.AppendLine("// ");
+ builder.AppendLine("#nullable enable");
+ builder.AppendLine("#if CLOUDFLARE");
+ builder.AppendLine("namespace System.Runtime.CompilerServices");
+ builder.AppendLine("{");
+ builder.AppendLine(" [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true, Inherited = false)]");
+ builder.AppendLine(" internal sealed class InterceptsLocationAttribute : global::System.Attribute");
+ builder.AppendLine(" {");
+ builder.AppendLine(" public InterceptsLocationAttribute(int version, string data)");
+ builder.AppendLine(" {");
+ builder.AppendLine(" }");
+ builder.AppendLine(" }");
+ builder.AppendLine("}");
+ builder.AppendLine();
+ builder.AppendLine($"namespace {GeneratedNamespace}");
+ builder.AppendLine("{");
+ builder.AppendLine("internal static class WebApplicationRunAsyncInterceptors");
+ builder.AppendLine("{");
+
+ if (attributesByTarget.TryGetValue(InterceptTargetType.WebApplicationRunAsync, out var webApplicationRunAsyncAttributes)
+ && webApplicationRunAsyncAttributes.Count > 0)
+ {
+ foreach (var attribute in webApplicationRunAsyncAttributes.OrderBy(static attribute => attribute))
+ {
+ builder.AppendLine($" {attribute}");
+ }
+
+ builder.AppendLine(" internal static global::System.Threading.Tasks.Task InterceptRunAsync(this global::Microsoft.AspNetCore.Builder.WebApplication app, string? url)");
+ builder.AppendLine(" => app.StartAsync();");
+ builder.AppendLine();
+ }
+
+ if (attributesByTarget.TryGetValue(InterceptTargetType.HostRunAsync, out var hostRunAsyncAttributes)
+ && hostRunAsyncAttributes.Count > 0)
+ {
+ foreach (var attribute in hostRunAsyncAttributes.OrderBy(static attribute => attribute))
+ {
+ builder.AppendLine($" {attribute}");
+ }
+
+ builder.AppendLine(" internal static global::System.Threading.Tasks.Task InterceptHostRunAsync(this global::Microsoft.Extensions.Hosting.IHost host, global::System.Threading.CancellationToken cancellationToken)");
+ builder.AppendLine(" => host.StartAsync(cancellationToken);");
+ builder.AppendLine();
+ }
+
+ AppendCreateBuilderIntercept(builder, attributesByTarget, InterceptTargetType.CreateBuilderNoArgs,
+ "InterceptCreateBuilder", "()", "global::Microsoft.AspNetCore.Builder.WebApplicationWasmExtensions.CreateWasmBuilder()");
+
+ AppendCreateBuilderIntercept(builder, attributesByTarget, InterceptTargetType.CreateBuilderArgs,
+ "InterceptCreateBuilderWithArgs", "(string[] args)",
+ "global::Microsoft.AspNetCore.Builder.WebApplicationWasmExtensions.CreateWasmBuilder(new global::Microsoft.AspNetCore.Builder.WebApplicationOptions { Args = args })");
+
+ AppendCreateBuilderIntercept(builder, attributesByTarget, InterceptTargetType.CreateBuilderOptions,
+ "InterceptCreateBuilderWithOptions", "(global::Microsoft.AspNetCore.Builder.WebApplicationOptions options)",
+ "global::Microsoft.AspNetCore.Builder.WebApplicationWasmExtensions.CreateWasmBuilder(options)");
+
+ AppendCreateBuilderIntercept(builder, attributesByTarget, InterceptTargetType.CreateSlimBuilderNoArgs,
+ "InterceptCreateSlimBuilder", "()", "global::Microsoft.AspNetCore.Builder.WebApplicationWasmExtensions.CreateWasmBuilder()");
+
+ AppendCreateBuilderIntercept(builder, attributesByTarget, InterceptTargetType.CreateSlimBuilderOptions,
+ "InterceptCreateSlimBuilderWithOptions", "(global::Microsoft.AspNetCore.Builder.WebApplicationOptions options)",
+ "global::Microsoft.AspNetCore.Builder.WebApplicationWasmExtensions.CreateWasmBuilder(options)");
+
+ builder.AppendLine("}");
+ builder.AppendLine("}");
+ builder.AppendLine("#endif");
+
+ return builder.ToString();
+ }
+
+ private static void AppendCreateBuilderIntercept(
+ StringBuilder builder,
+ Dictionary> attributesByTarget,
+ InterceptTargetType targetType,
+ string methodName,
+ string parameterList,
+ string expression)
+ {
+ if (!attributesByTarget.TryGetValue(targetType, out var attributes) || attributes.Count == 0)
+ {
+ return;
+ }
+
+ foreach (var attribute in attributes.OrderBy(static attribute => attribute))
+ {
+ builder.AppendLine($" {attribute}");
+ }
+
+ builder.AppendLine($" internal static global::Microsoft.AspNetCore.Builder.WebApplicationBuilder {methodName}{parameterList}");
+ builder.AppendLine($" => {expression};");
+ builder.AppendLine();
+ }
+}
+
+internal readonly record struct InterceptInfo(InterceptTargetType TargetType, string AttributeSyntax);
+
+internal enum InterceptTargetType
+{
+ WebApplicationRunAsync,
+ HostRunAsync,
+ CreateBuilderNoArgs,
+ CreateBuilderArgs,
+ CreateBuilderOptions,
+ CreateSlimBuilderNoArgs,
+ CreateSlimBuilderOptions
+}
diff --git a/src/Zapto.AspNetCore.Wasm.SourceGenerator/Zapto.AspNetCore.Wasm.SourceGenerator.csproj b/src/Zapto.AspNetCore.Wasm.SourceGenerator/Zapto.AspNetCore.Wasm.SourceGenerator.csproj
new file mode 100644
index 0000000..6c05254
--- /dev/null
+++ b/src/Zapto.AspNetCore.Wasm.SourceGenerator/Zapto.AspNetCore.Wasm.SourceGenerator.csproj
@@ -0,0 +1,30 @@
+
+
+
+ netstandard2.0
+ 12
+ enable
+ disable
+ Zapto.AspNetCore.Wasm.SourceGenerator
+ Source generator for WASM exports in ASP.NET Core
+ aspnetcore, webassembly, wasm, sourcegenerator
+ true
+
+
+ false
+ true
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
diff --git a/src/Zapto.AspNetCore.Wasm/GlobalUsings.cs b/src/Zapto.AspNetCore.Wasm/GlobalUsings.cs
new file mode 100644
index 0000000..7e78320
--- /dev/null
+++ b/src/Zapto.AspNetCore.Wasm/GlobalUsings.cs
@@ -0,0 +1,16 @@
+global using System.Buffers;
+global using System.Buffers.Binary;
+global using System.IO.Pipelines;
+global using System.Runtime.CompilerServices;
+global using System.Runtime.InteropServices;
+global using System.Text;
+global using System.Text.Json;
+global using System.Text.Json.Serialization;
+global using Microsoft.AspNetCore.Hosting.Server;
+global using Microsoft.AspNetCore.Hosting.Server.Features;
+global using Microsoft.AspNetCore.Http;
+global using Microsoft.AspNetCore.Http.Features;
+global using Microsoft.Extensions.DependencyInjection;
+global using Microsoft.Extensions.Hosting;
+global using Microsoft.Extensions.Logging;
+global using Microsoft.Extensions.Options;
diff --git a/src/Zapto.AspNetCore.Wasm/HostBuilderWasmExtensions.cs b/src/Zapto.AspNetCore.Wasm/HostBuilderWasmExtensions.cs
new file mode 100644
index 0000000..2da0134
--- /dev/null
+++ b/src/Zapto.AspNetCore.Wasm/HostBuilderWasmExtensions.cs
@@ -0,0 +1,37 @@
+using Microsoft.Extensions.Hosting;
+using Zapto.AspNetCore.Wasm;
+
+namespace Microsoft.Extensions.Hosting;
+
+///
+/// Provides extension methods for configuring WebAssembly server on IHostBuilder.
+///
+public static class HostBuilderWasmExtensions
+{
+ ///
+ /// Configures the host to use the WebAssembly server.
+ ///
+ /// The IHostBuilder to configure.
+ /// A reference to the IHostBuilder.
+ public static IHostBuilder UseWasmServer(this IHostBuilder hostBuilder)
+ {
+ return hostBuilder.ConfigureServices(services =>
+ {
+ services.AddSingleton();
+ });
+ }
+
+ ///
+ /// Configures the host to use the WebAssembly server with options.
+ ///
+ /// The IHostBuilder to configure.
+ /// A callback to configure WasmServerOptions.
+ /// A reference to the IHostBuilder.
+ public static IHostBuilder UseWasmServer(this IHostBuilder hostBuilder, Action options)
+ {
+ return hostBuilder.UseWasmServer().ConfigureServices(services =>
+ {
+ services.Configure(options);
+ });
+ }
+}
diff --git a/src/Zapto.AspNetCore.Wasm/Internal/RequestContext.cs b/src/Zapto.AspNetCore.Wasm/Internal/RequestContext.cs
new file mode 100644
index 0000000..80c2082
--- /dev/null
+++ b/src/Zapto.AspNetCore.Wasm/Internal/RequestContext.cs
@@ -0,0 +1,18 @@
+using System.Text.Json.Serialization;
+
+namespace Zapto.AspNetCore.Wasm.Internal;
+
+///
+/// Represents the request context received from the JavaScript runtime.
+///
+internal sealed class RequestContext
+{
+ [JsonPropertyName("url")]
+ public required string Url { get; set; }
+
+ [JsonPropertyName("method")]
+ public required string Method { get; set; }
+
+ [JsonPropertyName("headers")]
+ public Dictionary Headers { get; set; } = new();
+}
diff --git a/src/Zapto.AspNetCore.Wasm/Internal/ResponseContext.cs b/src/Zapto.AspNetCore.Wasm/Internal/ResponseContext.cs
new file mode 100644
index 0000000..46b4425
--- /dev/null
+++ b/src/Zapto.AspNetCore.Wasm/Internal/ResponseContext.cs
@@ -0,0 +1,15 @@
+using System.Text.Json.Serialization;
+
+namespace Zapto.AspNetCore.Wasm.Internal;
+
+///
+/// Represents the response context to send back to the JavaScript runtime.
+///
+internal sealed class ResponseContext
+{
+ [JsonPropertyName("status")]
+ public int Status { get; set; } = 200;
+
+ [JsonPropertyName("headers")]
+ public Dictionary Headers { get; set; } = new();
+}
diff --git a/src/Zapto.AspNetCore.Wasm/Internal/WasmHttpConnectionFeature.cs b/src/Zapto.AspNetCore.Wasm/Internal/WasmHttpConnectionFeature.cs
new file mode 100644
index 0000000..b375452
--- /dev/null
+++ b/src/Zapto.AspNetCore.Wasm/Internal/WasmHttpConnectionFeature.cs
@@ -0,0 +1,15 @@
+namespace Zapto.AspNetCore.Wasm.Internal;
+
+///
+/// Implementation of IHttpConnectionFeature for WebAssembly.
+///
+internal sealed class WasmHttpConnectionFeature : IHttpConnectionFeature
+{
+ private static readonly System.Net.IPAddress LocalAddress = System.Net.IPAddress.Parse("127.0.0.1");
+
+ public string ConnectionId { get; set; } = Guid.NewGuid().ToString("N");
+ public System.Net.IPAddress? LocalIpAddress { get; set; } = LocalAddress;
+ public int LocalPort { get; set; } = 443;
+ public System.Net.IPAddress? RemoteIpAddress { get; set; } = LocalAddress;
+ public int RemotePort { get; set; } = 0;
+}
diff --git a/src/Zapto.AspNetCore.Wasm/Internal/WasmHttpRequestFeature.cs b/src/Zapto.AspNetCore.Wasm/Internal/WasmHttpRequestFeature.cs
new file mode 100644
index 0000000..4d80bbb
--- /dev/null
+++ b/src/Zapto.AspNetCore.Wasm/Internal/WasmHttpRequestFeature.cs
@@ -0,0 +1,17 @@
+namespace Zapto.AspNetCore.Wasm.Internal;
+
+///
+/// Default implementation of IHttpRequestFeature for WebAssembly.
+///
+internal sealed class WasmHttpRequestFeature : IHttpRequestFeature
+{
+ public string Protocol { get; set; } = "HTTP/1.1";
+ public string Scheme { get; set; } = "https";
+ public string Method { get; set; } = "GET";
+ public string PathBase { get; set; } = string.Empty;
+ public string Path { get; set; } = "/";
+ public string QueryString { get; set; } = string.Empty;
+ public string RawTarget { get; set; } = string.Empty;
+ public IHeaderDictionary Headers { get; set; } = new HeaderDictionary();
+ public Stream Body { get; set; } = Stream.Null;
+}
diff --git a/src/Zapto.AspNetCore.Wasm/Internal/WasmHttpRequestIdentifierFeature.cs b/src/Zapto.AspNetCore.Wasm/Internal/WasmHttpRequestIdentifierFeature.cs
new file mode 100644
index 0000000..0b7f715
--- /dev/null
+++ b/src/Zapto.AspNetCore.Wasm/Internal/WasmHttpRequestIdentifierFeature.cs
@@ -0,0 +1,9 @@
+namespace Zapto.AspNetCore.Wasm.Internal;
+
+///
+/// Implementation of IHttpRequestIdentifierFeature for WebAssembly.
+///
+internal sealed class WasmHttpRequestIdentifierFeature : IHttpRequestIdentifierFeature
+{
+ public string TraceIdentifier { get; set; } = Guid.NewGuid().ToString("N");
+}
diff --git a/src/Zapto.AspNetCore.Wasm/Internal/WasmHttpResponseBodyFeature.cs b/src/Zapto.AspNetCore.Wasm/Internal/WasmHttpResponseBodyFeature.cs
new file mode 100644
index 0000000..d83020b
--- /dev/null
+++ b/src/Zapto.AspNetCore.Wasm/Internal/WasmHttpResponseBodyFeature.cs
@@ -0,0 +1,41 @@
+namespace Zapto.AspNetCore.Wasm.Internal;
+
+///
+/// Implementation of IHttpResponseBodyFeature for WebAssembly.
+///
+internal sealed class WasmHttpResponseBodyFeature : IHttpResponseBodyFeature
+{
+ private readonly WasmHttpResponseFeature _responseFeature;
+ private PipeWriter? _pipeWriter;
+
+ public WasmHttpResponseBodyFeature(MemoryStream stream, WasmHttpResponseFeature responseFeature)
+ {
+ Stream = stream;
+ _responseFeature = responseFeature;
+ }
+
+ public Stream Stream { get; }
+
+ public PipeWriter Writer => _pipeWriter ??= PipeWriter.Create(Stream, new StreamPipeWriterOptions(leaveOpen: true));
+
+ public Task CompleteAsync()
+ {
+ return Task.CompletedTask;
+ }
+
+ public void DisableBuffering()
+ {
+ // WASM doesn't support disabling buffering - everything is buffered
+ }
+
+ public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellationToken = default)
+ {
+ throw new NotSupportedException("SendFileAsync is not supported in WebAssembly.");
+ }
+
+ public async Task StartAsync(CancellationToken cancellationToken = default)
+ {
+ _responseFeature.MarkAsStarted();
+ await Task.CompletedTask;
+ }
+}
diff --git a/src/Zapto.AspNetCore.Wasm/Internal/WasmHttpResponseFeature.cs b/src/Zapto.AspNetCore.Wasm/Internal/WasmHttpResponseFeature.cs
new file mode 100644
index 0000000..94d7611
--- /dev/null
+++ b/src/Zapto.AspNetCore.Wasm/Internal/WasmHttpResponseFeature.cs
@@ -0,0 +1,28 @@
+namespace Zapto.AspNetCore.Wasm.Internal;
+
+///
+/// Default implementation of IHttpResponseFeature for WebAssembly.
+///
+internal sealed class WasmHttpResponseFeature : IHttpResponseFeature
+{
+ public int StatusCode { get; set; } = 200;
+ public string? ReasonPhrase { get; set; }
+ public IHeaderDictionary Headers { get; set; } = new HeaderDictionary();
+ public Stream Body { get; set; } = Stream.Null;
+ public bool HasStarted { get; private set; }
+
+ public void OnStarting(Func