├── .editorconfig ├── .gitignore ├── Directory.Build.props ├── LICENSE ├── Microsoft.JSInterop.sln ├── README.md ├── build └── Key.snk ├── src ├── Microsoft.JSInterop.JS │ ├── .gitignore │ ├── Microsoft.JSInterop.JS.csproj │ ├── package-lock.json │ ├── package.json │ ├── src │ │ └── Microsoft.JSInterop.ts │ └── tsconfig.json ├── Microsoft.JSInterop │ ├── DotNetDispatcher.cs │ ├── DotNetObjectRef.cs │ ├── ICustomArgSerializer.cs │ ├── IJSInProcessRuntime.cs │ ├── IJSRuntime.cs │ ├── InteropArgSerializerStrategy.cs │ ├── JSAsyncCallResult.cs │ ├── JSException.cs │ ├── JSInProcessRuntimeBase.cs │ ├── JSInvokableAttribute.cs │ ├── JSRuntime.cs │ ├── JSRuntimeBase.cs │ ├── Json │ │ ├── CamelCase.cs │ │ ├── Json.cs │ │ └── SimpleJson │ │ │ ├── README.txt │ │ │ └── SimpleJson.cs │ ├── Microsoft.JSInterop.csproj │ ├── Properties │ │ └── AssemblyInfo.cs │ └── TaskGenericsUtil.cs └── Mono.WebAssembly.Interop │ ├── InternalCalls.cs │ ├── Mono.WebAssembly.Interop.csproj │ └── MonoWebAssemblyJSRuntime.cs └── test └── Microsoft.JSInterop.Test ├── DotNetDispatcherTest.cs ├── DotNetObjectRefTest.cs ├── JSInProcessRuntimeBaseTest.cs ├── JSRuntimeBaseTest.cs ├── JSRuntimeTest.cs ├── JsonUtilTest.cs └── Microsoft.JSInterop.Test.csproj /.editorconfig: -------------------------------------------------------------------------------- 1 | # All Files 2 | [*] 3 | charset = utf-8 4 | end_of_line = crlf 5 | indent_style = space 6 | indent_size = 4 7 | insert_final_newline = false 8 | trim_trailing_whitespace = true 9 | 10 | # Solution Files 11 | [*.sln] 12 | indent_style = tab 13 | 14 | # Markdown Files 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | 18 | # Web Files 19 | [*.{htm,html,js,ts,css,scss,less}] 20 | insert_final_newline = true 21 | indent_size = 2 22 | 23 | [*.{yml,json}] 24 | indent_size = 2 25 | 26 | [*.{xml,csproj,config,*proj,targets,props}] 27 | indent_size = 2 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | # tools/** 303 | # !tools/packages.config 304 | 305 | # Tabs Studio 306 | *.tss 307 | 308 | # Telerik's JustMock configuration file 309 | *.jmconfig 310 | 311 | # BizTalk build output 312 | *.btp.cs 313 | *.btm.cs 314 | *.odx.cs 315 | *.xsd.cs 316 | 317 | # OpenCover UI analysis results 318 | OpenCover/ 319 | 320 | # Azure Stream Analytics local run output 321 | ASALocalRun/ 322 | 323 | # MSBuild Binary and Structured Log 324 | *.binlog 325 | 326 | # NVidia Nsight GPU debugger configuration file 327 | *.nvuser 328 | 329 | # MFractors (Xamarin productivity tool) working folder 330 | .mfractor/ 331 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | $(MSBuildThisFileDirectory)build\Key.snk 6 | true 7 | true 8 | Microsoft 9 | 7.3 10 | 11 | 12 | 13 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Microsoft.JSInterop.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27703.2042 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{1290437E-A890-419E-A317-D0F7FEE185A5}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{B98D4F51-88FB-471C-B56F-752E8EE502E7}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.JSInterop", "src\Microsoft.JSInterop\Microsoft.JSInterop.csproj", "{CB4CD4A6-9BAA-46D1-944F-CE56DEC2663C}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.JSInterop.Test", "test\Microsoft.JSInterop.Test\Microsoft.JSInterop.Test.csproj", "{7FF8B199-52C0-4DFE-A73B-0C9E18220C0E}" 13 | EndProject 14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.JSInterop.JS", "src\Microsoft.JSInterop.JS\Microsoft.JSInterop.JS.csproj", "{60BA5AAD-264A-437E-8319-577841C66CC6}" 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{BE4CBB33-5C40-4A07-B6FC-1D7C3AE13024}" 17 | ProjectSection(SolutionItems) = preProject 18 | README.md = README.md 19 | EndProjectSection 20 | EndProject 21 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mono.WebAssembly.Interop", "src\Mono.WebAssembly.Interop\Mono.WebAssembly.Interop.csproj", "{10145E99-1B2D-40C5-9595-582BDAF3E024}" 22 | EndProject 23 | Global 24 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 25 | Debug|Any CPU = Debug|Any CPU 26 | Release|Any CPU = Release|Any CPU 27 | EndGlobalSection 28 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 29 | {CB4CD4A6-9BAA-46D1-944F-CE56DEC2663C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {CB4CD4A6-9BAA-46D1-944F-CE56DEC2663C}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {CB4CD4A6-9BAA-46D1-944F-CE56DEC2663C}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {CB4CD4A6-9BAA-46D1-944F-CE56DEC2663C}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {7FF8B199-52C0-4DFE-A73B-0C9E18220C0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {7FF8B199-52C0-4DFE-A73B-0C9E18220C0E}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {7FF8B199-52C0-4DFE-A73B-0C9E18220C0E}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {7FF8B199-52C0-4DFE-A73B-0C9E18220C0E}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {60BA5AAD-264A-437E-8319-577841C66CC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {60BA5AAD-264A-437E-8319-577841C66CC6}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {60BA5AAD-264A-437E-8319-577841C66CC6}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {60BA5AAD-264A-437E-8319-577841C66CC6}.Release|Any CPU.Build.0 = Release|Any CPU 41 | {10145E99-1B2D-40C5-9595-582BDAF3E024}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {10145E99-1B2D-40C5-9595-582BDAF3E024}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {10145E99-1B2D-40C5-9595-582BDAF3E024}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {10145E99-1B2D-40C5-9595-582BDAF3E024}.Release|Any CPU.Build.0 = Release|Any CPU 45 | EndGlobalSection 46 | GlobalSection(SolutionProperties) = preSolution 47 | HideSolutionNode = FALSE 48 | EndGlobalSection 49 | GlobalSection(NestedProjects) = preSolution 50 | {CB4CD4A6-9BAA-46D1-944F-CE56DEC2663C} = {1290437E-A890-419E-A317-D0F7FEE185A5} 51 | {7FF8B199-52C0-4DFE-A73B-0C9E18220C0E} = {B98D4F51-88FB-471C-B56F-752E8EE502E7} 52 | {60BA5AAD-264A-437E-8319-577841C66CC6} = {1290437E-A890-419E-A317-D0F7FEE185A5} 53 | {10145E99-1B2D-40C5-9595-582BDAF3E024} = {1290437E-A890-419E-A317-D0F7FEE185A5} 54 | EndGlobalSection 55 | GlobalSection(ExtensibilityGlobals) = postSolution 56 | SolutionGuid = {7E07ABF2-427A-43FA-A6A4-82B21B96ACAF} 57 | EndGlobalSection 58 | EndGlobal 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jsinterop [ARCHIVED] 2 | 3 | ## Please go to https://github.com/aspnet/Extensions/tree/master/src/JSInterop to see the latest JSInterop code base and to file issues 4 | 5 | This repo is for `Microsoft.JSInterop`, a package that provides abstractions and features for interop between .NET and JavaScript code. 6 | 7 | ## Usage 8 | 9 | The primary use case is for applications built with Mono WebAssembly or Blazor. It's not expected that developers will typically use these libraries separately from Mono WebAssembly, Blazor, or a similar technology. 10 | 11 | ## How to build and test 12 | 13 | To build: 14 | 15 | 1. Ensure you have installed an up-to-date version of the [.NET Core SDK](https://www.microsoft.com/net/download). To verify, run `dotnet --version` and be sure that it returns `2.1.300` (i.e., .NET Core 2.1) or later. 16 | 2. Run `dotnet build` 17 | 18 | To run tests: 19 | 20 | 1. Run `dotnet test test/Microsoft.JSInterop.Test` 21 | -------------------------------------------------------------------------------- /build/Key.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet/jsinterop/8495c8bb132d3c92a5e4a3a9d4bbf1ad06fb9992/build/Key.snk -------------------------------------------------------------------------------- /src/Microsoft.JSInterop.JS/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | -------------------------------------------------------------------------------- /src/Microsoft.JSInterop.JS/Microsoft.JSInterop.JS.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | Latest 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Microsoft.JSInterop.JS/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dotnet/jsinterop", 3 | "version": "0.1.1", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "balanced-match": { 8 | "version": "1.0.0", 9 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 10 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 11 | "dev": true 12 | }, 13 | "brace-expansion": { 14 | "version": "1.1.11", 15 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 16 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 17 | "dev": true, 18 | "requires": { 19 | "balanced-match": "1.0.0", 20 | "concat-map": "0.0.1" 21 | } 22 | }, 23 | "concat-map": { 24 | "version": "0.0.1", 25 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 26 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 27 | "dev": true 28 | }, 29 | "fs.realpath": { 30 | "version": "1.0.0", 31 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 32 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 33 | "dev": true 34 | }, 35 | "glob": { 36 | "version": "7.1.3", 37 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", 38 | "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", 39 | "dev": true, 40 | "requires": { 41 | "fs.realpath": "1.0.0", 42 | "inflight": "1.0.6", 43 | "inherits": "2.0.3", 44 | "minimatch": "3.0.4", 45 | "once": "1.4.0", 46 | "path-is-absolute": "1.0.1" 47 | } 48 | }, 49 | "inflight": { 50 | "version": "1.0.6", 51 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 52 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 53 | "dev": true, 54 | "requires": { 55 | "once": "1.4.0", 56 | "wrappy": "1.0.2" 57 | } 58 | }, 59 | "inherits": { 60 | "version": "2.0.3", 61 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 62 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", 63 | "dev": true 64 | }, 65 | "minimatch": { 66 | "version": "3.0.4", 67 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 68 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 69 | "dev": true, 70 | "requires": { 71 | "brace-expansion": "1.1.11" 72 | } 73 | }, 74 | "once": { 75 | "version": "1.4.0", 76 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 77 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 78 | "dev": true, 79 | "requires": { 80 | "wrappy": "1.0.2" 81 | } 82 | }, 83 | "path-is-absolute": { 84 | "version": "1.0.1", 85 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 86 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 87 | "dev": true 88 | }, 89 | "rimraf": { 90 | "version": "2.6.2", 91 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", 92 | "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", 93 | "dev": true, 94 | "requires": { 95 | "glob": "7.1.3" 96 | } 97 | }, 98 | "wrappy": { 99 | "version": "1.0.2", 100 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 101 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 102 | "dev": true 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Microsoft.JSInterop.JS/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dotnet/jsinterop", 3 | "version": "0.1.1", 4 | "description": "Provides abstractions and features for interop between .NET and JavaScript code.", 5 | "main": "dist/Microsoft.JSInterop.js", 6 | "types": "dist/Microsoft.JSInterop.d.js", 7 | "scripts": { 8 | "prepublish": "rimraf dist && dotnet build && echo 'Finished building NPM package \"@dotnet/jsinterop\"'" 9 | }, 10 | "files": [ 11 | "dist/**" 12 | ], 13 | "author": "Microsoft", 14 | "license": "Apache-2.0", 15 | "bugs": { 16 | "url": "https://github.com/dotnet/jsinterop/issues" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/dotnet/jsinterop.git" 21 | }, 22 | "devDependencies": { 23 | "rimraf": "^2.5.4" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Microsoft.JSInterop.JS/src/Microsoft.JSInterop.ts: -------------------------------------------------------------------------------- 1 | // This is a single-file self-contained module to avoid the need for a Webpack build 2 | 3 | module DotNet { 4 | (window as any).DotNet = DotNet; // Ensure reachable from anywhere 5 | 6 | export type JsonReviver = ((key: any, value: any) => any); 7 | const jsonRevivers: JsonReviver[] = []; 8 | 9 | const pendingAsyncCalls: { [id: number]: PendingAsyncCall } = {}; 10 | const cachedJSFunctions: { [identifier: string]: Function } = {}; 11 | let nextAsyncCallId = 1; // Start at 1 because zero signals "no response needed" 12 | 13 | let dotNetDispatcher: DotNetCallDispatcher | null = null; 14 | 15 | /** 16 | * Sets the specified .NET call dispatcher as the current instance so that it will be used 17 | * for future invocations. 18 | * 19 | * @param dispatcher An object that can dispatch calls from JavaScript to a .NET runtime. 20 | */ 21 | export function attachDispatcher(dispatcher: DotNetCallDispatcher) { 22 | dotNetDispatcher = dispatcher; 23 | } 24 | 25 | /** 26 | * Adds a JSON reviver callback that will be used when parsing arguments received from .NET. 27 | * @param reviver The reviver to add. 28 | */ 29 | export function attachReviver(reviver: JsonReviver) { 30 | jsonRevivers.push(reviver); 31 | } 32 | 33 | /** 34 | * Invokes the specified .NET public method synchronously. Not all hosting scenarios support 35 | * synchronous invocation, so if possible use invokeMethodAsync instead. 36 | * 37 | * @param assemblyName The short name (without key/version or .dll extension) of the .NET assembly containing the method. 38 | * @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier. 39 | * @param args Arguments to pass to the method, each of which must be JSON-serializable. 40 | * @returns The result of the operation. 41 | */ 42 | export function invokeMethod(assemblyName: string, methodIdentifier: string, ...args: any[]): T { 43 | return invokePossibleInstanceMethod(assemblyName, methodIdentifier, null, args); 44 | } 45 | 46 | /** 47 | * Invokes the specified .NET public method asynchronously. 48 | * 49 | * @param assemblyName The short name (without key/version or .dll extension) of the .NET assembly containing the method. 50 | * @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier. 51 | * @param args Arguments to pass to the method, each of which must be JSON-serializable. 52 | * @returns A promise representing the result of the operation. 53 | */ 54 | export function invokeMethodAsync(assemblyName: string, methodIdentifier: string, ...args: any[]): Promise { 55 | return invokePossibleInstanceMethodAsync(assemblyName, methodIdentifier, null, args); 56 | } 57 | 58 | function invokePossibleInstanceMethod(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, args: any[]): T { 59 | const dispatcher = getRequiredDispatcher(); 60 | if (dispatcher.invokeDotNetFromJS) { 61 | const argsJson = JSON.stringify(args, argReplacer); 62 | const resultJson = dispatcher.invokeDotNetFromJS(assemblyName, methodIdentifier, dotNetObjectId, argsJson); 63 | return resultJson ? parseJsonWithRevivers(resultJson) : null; 64 | } else { 65 | throw new Error('The current dispatcher does not support synchronous calls from JS to .NET. Use invokeMethodAsync instead.'); 66 | } 67 | } 68 | 69 | function invokePossibleInstanceMethodAsync(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, args: any[]): Promise { 70 | const asyncCallId = nextAsyncCallId++; 71 | const resultPromise = new Promise((resolve, reject) => { 72 | pendingAsyncCalls[asyncCallId] = { resolve, reject }; 73 | }); 74 | 75 | try { 76 | const argsJson = JSON.stringify(args, argReplacer); 77 | getRequiredDispatcher().beginInvokeDotNetFromJS(asyncCallId, assemblyName, methodIdentifier, dotNetObjectId, argsJson); 78 | } catch(ex) { 79 | // Synchronous failure 80 | completePendingCall(asyncCallId, false, ex); 81 | } 82 | 83 | return resultPromise; 84 | } 85 | 86 | function getRequiredDispatcher(): DotNetCallDispatcher { 87 | if (dotNetDispatcher !== null) { 88 | return dotNetDispatcher; 89 | } 90 | 91 | throw new Error('No .NET call dispatcher has been set.'); 92 | } 93 | 94 | function completePendingCall(asyncCallId: number, success: boolean, resultOrError: any) { 95 | if (!pendingAsyncCalls.hasOwnProperty(asyncCallId)) { 96 | throw new Error(`There is no pending async call with ID ${asyncCallId}.`); 97 | } 98 | 99 | const asyncCall = pendingAsyncCalls[asyncCallId]; 100 | delete pendingAsyncCalls[asyncCallId]; 101 | if (success) { 102 | asyncCall.resolve(resultOrError); 103 | } else { 104 | asyncCall.reject(resultOrError); 105 | } 106 | } 107 | 108 | interface PendingAsyncCall { 109 | resolve: (value?: T | PromiseLike) => void; 110 | reject: (reason?: any) => void; 111 | } 112 | 113 | /** 114 | * Represents the ability to dispatch calls from JavaScript to a .NET runtime. 115 | */ 116 | export interface DotNetCallDispatcher { 117 | /** 118 | * Optional. If implemented, invoked by the runtime to perform a synchronous call to a .NET method. 119 | * 120 | * @param assemblyName The short name (without key/version or .dll extension) of the .NET assembly holding the method to invoke. The value may be null when invoking instance methods. 121 | * @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier. 122 | * @param dotNetObjectId If given, the call will be to an instance method on the specified DotNetObject. Pass null or undefined to call static methods. 123 | * @param argsJson JSON representation of arguments to pass to the method. 124 | * @returns JSON representation of the result of the invocation. 125 | */ 126 | invokeDotNetFromJS?(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, argsJson: string): string | null; 127 | 128 | /** 129 | * Invoked by the runtime to begin an asynchronous call to a .NET method. 130 | * 131 | * @param callId A value identifying the asynchronous operation. This value should be passed back in a later call from .NET to JS. 132 | * @param assemblyName The short name (without key/version or .dll extension) of the .NET assembly holding the method to invoke. The value may be null when invoking instance methods. 133 | * @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier. 134 | * @param dotNetObjectId If given, the call will be to an instance method on the specified DotNetObject. Pass null to call static methods. 135 | * @param argsJson JSON representation of arguments to pass to the method. 136 | */ 137 | beginInvokeDotNetFromJS(callId: number, assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, argsJson: string): void; 138 | } 139 | 140 | /** 141 | * Receives incoming calls from .NET and dispatches them to JavaScript. 142 | */ 143 | export const jsCallDispatcher = { 144 | /** 145 | * Finds the JavaScript function matching the specified identifier. 146 | * 147 | * @param identifier Identifies the globally-reachable function to be returned. 148 | * @returns A Function instance. 149 | */ 150 | findJSFunction, 151 | 152 | /** 153 | * Invokes the specified synchronous JavaScript function. 154 | * 155 | * @param identifier Identifies the globally-reachable function to invoke. 156 | * @param argsJson JSON representation of arguments to be passed to the function. 157 | * @returns JSON representation of the invocation result. 158 | */ 159 | invokeJSFromDotNet: (identifier: string, argsJson: string) => { 160 | const result = findJSFunction(identifier).apply(null, parseJsonWithRevivers(argsJson)); 161 | return result === null || result === undefined 162 | ? null 163 | : JSON.stringify(result, argReplacer); 164 | }, 165 | 166 | /** 167 | * Invokes the specified synchronous or asynchronous JavaScript function. 168 | * 169 | * @param asyncHandle A value identifying the asynchronous operation. This value will be passed back in a later call to endInvokeJSFromDotNet. 170 | * @param identifier Identifies the globally-reachable function to invoke. 171 | * @param argsJson JSON representation of arguments to be passed to the function. 172 | */ 173 | beginInvokeJSFromDotNet: (asyncHandle: number, identifier: string, argsJson: string): void => { 174 | // Coerce synchronous functions into async ones, plus treat 175 | // synchronous exceptions the same as async ones 176 | const promise = new Promise(resolve => { 177 | const synchronousResultOrPromise = findJSFunction(identifier).apply(null, parseJsonWithRevivers(argsJson)); 178 | resolve(synchronousResultOrPromise); 179 | }); 180 | 181 | // We only listen for a result if the caller wants to be notified about it 182 | if (asyncHandle) { 183 | // On completion, dispatch result back to .NET 184 | // Not using "await" because it codegens a lot of boilerplate 185 | promise.then( 186 | result => getRequiredDispatcher().beginInvokeDotNetFromJS(0, 'Microsoft.JSInterop', 'DotNetDispatcher.EndInvoke', null, JSON.stringify([asyncHandle, true, result], argReplacer)), 187 | error => getRequiredDispatcher().beginInvokeDotNetFromJS(0, 'Microsoft.JSInterop', 'DotNetDispatcher.EndInvoke', null, JSON.stringify([asyncHandle, false, formatError(error)])) 188 | ); 189 | } 190 | }, 191 | 192 | /** 193 | * Receives notification that an async call from JS to .NET has completed. 194 | * @param asyncCallId The identifier supplied in an earlier call to beginInvokeDotNetFromJS. 195 | * @param success A flag to indicate whether the operation completed successfully. 196 | * @param resultOrExceptionMessage Either the operation result or an error message. 197 | */ 198 | endInvokeDotNetFromJS: (asyncCallId: string, success: boolean, resultOrExceptionMessage: any): void => { 199 | const resultOrError = success ? resultOrExceptionMessage : new Error(resultOrExceptionMessage); 200 | completePendingCall(parseInt(asyncCallId), success, resultOrError); 201 | } 202 | } 203 | 204 | function parseJsonWithRevivers(json: string): any { 205 | return json ? JSON.parse(json, (key, initialValue) => { 206 | // Invoke each reviver in order, passing the output from the previous reviver, 207 | // so that each one gets a chance to transform the value 208 | return jsonRevivers.reduce( 209 | (latestValue, reviver) => reviver(key, latestValue), 210 | initialValue 211 | ); 212 | }) : null; 213 | } 214 | 215 | function formatError(error: any): string { 216 | if (error instanceof Error) { 217 | return `${error.message}\n${error.stack}`; 218 | } else { 219 | return error ? error.toString() : 'null'; 220 | } 221 | } 222 | 223 | function findJSFunction(identifier: string): Function { 224 | if (cachedJSFunctions.hasOwnProperty(identifier)) { 225 | return cachedJSFunctions[identifier]; 226 | } 227 | 228 | let result: any = window; 229 | let resultIdentifier = 'window'; 230 | identifier.split('.').forEach(segment => { 231 | if (segment in result) { 232 | result = result[segment]; 233 | resultIdentifier += '.' + segment; 234 | } else { 235 | throw new Error(`Could not find '${segment}' in '${resultIdentifier}'.`); 236 | } 237 | }); 238 | 239 | if (result instanceof Function) { 240 | return result; 241 | } else { 242 | throw new Error(`The value '${resultIdentifier}' is not a function.`); 243 | } 244 | } 245 | 246 | class DotNetObject { 247 | constructor(private _id: number) { 248 | } 249 | 250 | public invokeMethod(methodIdentifier: string, ...args: any[]): T { 251 | return invokePossibleInstanceMethod(null, methodIdentifier, this._id, args); 252 | } 253 | 254 | public invokeMethodAsync(methodIdentifier: string, ...args: any[]): Promise { 255 | return invokePossibleInstanceMethodAsync(null, methodIdentifier, this._id, args); 256 | } 257 | 258 | public dispose() { 259 | const promise = invokeMethodAsync( 260 | 'Microsoft.JSInterop', 261 | 'DotNetDispatcher.ReleaseDotNetObject', 262 | this._id); 263 | promise.catch(error => console.error(error)); 264 | } 265 | 266 | public serializeAsArg() { 267 | return `__dotNetObject:${this._id}`; 268 | } 269 | } 270 | 271 | const dotNetObjectValueFormat = /^__dotNetObject\:(\d+)$/; 272 | attachReviver(function reviveDotNetObject(key: any, value: any) { 273 | if (typeof value === 'string') { 274 | const match = value.match(dotNetObjectValueFormat); 275 | if (match) { 276 | return new DotNetObject(parseInt(match[1])); 277 | } 278 | } 279 | 280 | // Unrecognized - let another reviver handle it 281 | return value; 282 | }); 283 | 284 | function argReplacer(key: string, value: any) { 285 | return value instanceof DotNetObject ? value.serializeAsArg() : value; 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /src/Microsoft.JSInterop.JS/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "noEmitOnError": true, 5 | "removeComments": false, 6 | "sourceMap": true, 7 | "target": "es5", 8 | "lib": ["es2015", "dom", "es2015.promise"], 9 | "strict": true, 10 | "declaration": true, 11 | "outDir": "dist" 12 | }, 13 | "include": [ 14 | "src/**/*.ts" 15 | ], 16 | "exclude": [ 17 | "dist/**" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /src/Microsoft.JSInterop/DotNetDispatcher.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using Microsoft.JSInterop.Internal; 5 | using System; 6 | using System.Collections.Concurrent; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Reflection; 10 | using System.Threading.Tasks; 11 | 12 | namespace Microsoft.JSInterop 13 | { 14 | /// 15 | /// Provides methods that receive incoming calls from JS to .NET. 16 | /// 17 | public static class DotNetDispatcher 18 | { 19 | private static ConcurrentDictionary> _cachedMethodsByAssembly 20 | = new ConcurrentDictionary>(); 21 | 22 | /// 23 | /// Receives a call from JS to .NET, locating and invoking the specified method. 24 | /// 25 | /// The assembly containing the method to be invoked. 26 | /// The identifier of the method to be invoked. The method must be annotated with a matching this identifier string. 27 | /// For instance method calls, identifies the target object. 28 | /// A JSON representation of the parameters. 29 | /// A JSON representation of the return value, or null. 30 | public static string Invoke(string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) 31 | { 32 | // This method doesn't need [JSInvokable] because the platform is responsible for having 33 | // some way to dispatch calls here. The logic inside here is the thing that checks whether 34 | // the targeted method has [JSInvokable]. It is not itself subject to that restriction, 35 | // because there would be nobody to police that. This method *is* the police. 36 | 37 | // DotNetDispatcher only works with JSRuntimeBase instances. 38 | var jsRuntime = (JSRuntimeBase)JSRuntime.Current; 39 | 40 | var targetInstance = (object)null; 41 | if (dotNetObjectId != default) 42 | { 43 | targetInstance = jsRuntime.ArgSerializerStrategy.FindDotNetObject(dotNetObjectId); 44 | } 45 | 46 | var syncResult = InvokeSynchronously(assemblyName, methodIdentifier, targetInstance, argsJson); 47 | return syncResult == null ? null : Json.Serialize(syncResult, jsRuntime.ArgSerializerStrategy); 48 | } 49 | 50 | /// 51 | /// Receives a call from JS to .NET, locating and invoking the specified method asynchronously. 52 | /// 53 | /// A value identifying the asynchronous call that should be passed back with the result, or null if no result notification is required. 54 | /// The assembly containing the method to be invoked. 55 | /// The identifier of the method to be invoked. The method must be annotated with a matching this identifier string. 56 | /// For instance method calls, identifies the target object. 57 | /// A JSON representation of the parameters. 58 | /// A JSON representation of the return value, or null. 59 | public static void BeginInvoke(string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) 60 | { 61 | // This method doesn't need [JSInvokable] because the platform is responsible for having 62 | // some way to dispatch calls here. The logic inside here is the thing that checks whether 63 | // the targeted method has [JSInvokable]. It is not itself subject to that restriction, 64 | // because there would be nobody to police that. This method *is* the police. 65 | 66 | // DotNetDispatcher only works with JSRuntimeBase instances. 67 | // If the developer wants to use a totally custom IJSRuntime, then their JS-side 68 | // code has to implement its own way of returning async results. 69 | var jsRuntimeBaseInstance = (JSRuntimeBase)JSRuntime.Current; 70 | 71 | var targetInstance = dotNetObjectId == default 72 | ? null 73 | : jsRuntimeBaseInstance.ArgSerializerStrategy.FindDotNetObject(dotNetObjectId); 74 | 75 | object syncResult = null; 76 | Exception syncException = null; 77 | 78 | try 79 | { 80 | syncResult = InvokeSynchronously(assemblyName, methodIdentifier, targetInstance, argsJson); 81 | } 82 | catch (Exception ex) 83 | { 84 | syncException = ex; 85 | } 86 | 87 | // If there was no callId, the caller does not want to be notified about the result 88 | if (callId != null) 89 | { 90 | // Invoke and coerce the result to a Task so the caller can use the same async API 91 | // for both synchronous and asynchronous methods 92 | var task = CoerceToTask(syncResult, syncException); 93 | 94 | task.ContinueWith(completedTask => 95 | { 96 | try 97 | { 98 | var result = TaskGenericsUtil.GetTaskResult(completedTask); 99 | jsRuntimeBaseInstance.EndInvokeDotNet(callId, true, result); 100 | } 101 | catch (Exception ex) 102 | { 103 | ex = UnwrapException(ex); 104 | jsRuntimeBaseInstance.EndInvokeDotNet(callId, false, ex); 105 | } 106 | }); 107 | } 108 | } 109 | 110 | private static Task CoerceToTask(object syncResult, Exception syncException) 111 | { 112 | if (syncException != null) 113 | { 114 | return Task.FromException(syncException); 115 | } 116 | else if (syncResult is Task syncResultTask) 117 | { 118 | return syncResultTask; 119 | } 120 | else 121 | { 122 | return Task.FromResult(syncResult); 123 | } 124 | } 125 | 126 | private static object InvokeSynchronously(string assemblyName, string methodIdentifier, object targetInstance, string argsJson) 127 | { 128 | if (targetInstance != null) 129 | { 130 | if (assemblyName != null) 131 | { 132 | throw new ArgumentException($"For instance method calls, '{nameof(assemblyName)}' should be null. Value received: '{assemblyName}'."); 133 | } 134 | 135 | assemblyName = targetInstance.GetType().Assembly.GetName().Name; 136 | } 137 | 138 | var (methodInfo, parameterTypes) = GetCachedMethodInfo(assemblyName, methodIdentifier); 139 | 140 | // There's no direct way to say we want to deserialize as an array with heterogenous 141 | // entry types (e.g., [string, int, bool]), so we need to deserialize in two phases. 142 | // First we deserialize as object[], for which SimpleJson will supply JsonObject 143 | // instances for nonprimitive values. 144 | var suppliedArgs = (object[])null; 145 | var suppliedArgsLength = 0; 146 | if (argsJson != null) 147 | { 148 | suppliedArgs = Json.Deserialize(argsJson).ToArray(); 149 | suppliedArgsLength = suppliedArgs.Length; 150 | } 151 | if (suppliedArgsLength != parameterTypes.Length) 152 | { 153 | throw new ArgumentException($"In call to '{methodIdentifier}', expected {parameterTypes.Length} parameters but received {suppliedArgsLength}."); 154 | } 155 | 156 | // Second, convert each supplied value to the type expected by the method 157 | var runtime = (JSRuntimeBase)JSRuntime.Current; 158 | var serializerStrategy = runtime.ArgSerializerStrategy; 159 | for (var i = 0; i < suppliedArgsLength; i++) 160 | { 161 | if (parameterTypes[i] == typeof(JSAsyncCallResult)) 162 | { 163 | // For JS async call results, we have to defer the deserialization until 164 | // later when we know what type it's meant to be deserialized as 165 | suppliedArgs[i] = new JSAsyncCallResult(suppliedArgs[i]); 166 | } 167 | else 168 | { 169 | suppliedArgs[i] = serializerStrategy.DeserializeObject( 170 | suppliedArgs[i], parameterTypes[i]); 171 | } 172 | } 173 | 174 | try 175 | { 176 | return methodInfo.Invoke(targetInstance, suppliedArgs); 177 | } 178 | catch (Exception ex) 179 | { 180 | throw UnwrapException(ex); 181 | } 182 | } 183 | 184 | /// 185 | /// Receives notification that a call from .NET to JS has finished, marking the 186 | /// associated as completed. 187 | /// 188 | /// The identifier for the function invocation. 189 | /// A flag to indicate whether the invocation succeeded. 190 | /// If is true, specifies the invocation result. If is false, gives the corresponding to the invocation failure. 191 | [JSInvokable(nameof(DotNetDispatcher) + "." + nameof(EndInvoke))] 192 | public static void EndInvoke(long asyncHandle, bool succeeded, JSAsyncCallResult result) 193 | => ((JSRuntimeBase)JSRuntime.Current).EndInvokeJS(asyncHandle, succeeded, result.ResultOrException); 194 | 195 | /// 196 | /// Releases the reference to the specified .NET object. This allows the .NET runtime 197 | /// to garbage collect that object if there are no other references to it. 198 | /// 199 | /// To avoid leaking memory, the JavaScript side code must call this for every .NET 200 | /// object it obtains a reference to. The exception is if that object is used for 201 | /// the entire lifetime of a given user's session, in which case it is released 202 | /// automatically when the JavaScript runtime is disposed. 203 | /// 204 | /// The identifier previously passed to JavaScript code. 205 | [JSInvokable(nameof(DotNetDispatcher) + "." + nameof(ReleaseDotNetObject))] 206 | public static void ReleaseDotNetObject(long dotNetObjectId) 207 | { 208 | // DotNetDispatcher only works with JSRuntimeBase instances. 209 | var jsRuntime = (JSRuntimeBase)JSRuntime.Current; 210 | jsRuntime.ArgSerializerStrategy.ReleaseDotNetObject(dotNetObjectId); 211 | } 212 | 213 | private static (MethodInfo, Type[]) GetCachedMethodInfo(string assemblyName, string methodIdentifier) 214 | { 215 | if (string.IsNullOrWhiteSpace(assemblyName)) 216 | { 217 | throw new ArgumentException("Cannot be null, empty, or whitespace.", nameof(assemblyName)); 218 | } 219 | 220 | if (string.IsNullOrWhiteSpace(methodIdentifier)) 221 | { 222 | throw new ArgumentException("Cannot be null, empty, or whitespace.", nameof(methodIdentifier)); 223 | } 224 | 225 | var assemblyMethods = _cachedMethodsByAssembly.GetOrAdd(assemblyName, ScanAssemblyForCallableMethods); 226 | if (assemblyMethods.TryGetValue(methodIdentifier, out var result)) 227 | { 228 | return result; 229 | } 230 | else 231 | { 232 | throw new ArgumentException($"The assembly '{assemblyName}' does not contain a public method with [{nameof(JSInvokableAttribute)}(\"{methodIdentifier}\")]."); 233 | } 234 | } 235 | 236 | private static IReadOnlyDictionary ScanAssemblyForCallableMethods(string assemblyName) 237 | { 238 | // TODO: Consider looking first for assembly-level attributes (i.e., if there are any, 239 | // only use those) to avoid scanning, especially for framework assemblies. 240 | var result = new Dictionary(); 241 | var invokableMethods = GetRequiredLoadedAssembly(assemblyName) 242 | .GetExportedTypes() 243 | .SelectMany(type => type.GetMethods( 244 | BindingFlags.Public | 245 | BindingFlags.DeclaredOnly | 246 | BindingFlags.Instance | 247 | BindingFlags.Static)) 248 | .Where(method => method.IsDefined(typeof(JSInvokableAttribute), inherit: false)); 249 | foreach (var method in invokableMethods) 250 | { 251 | var identifier = method.GetCustomAttribute(false).Identifier ?? method.Name; 252 | var parameterTypes = method.GetParameters().Select(p => p.ParameterType).ToArray(); 253 | 254 | try 255 | { 256 | result.Add(identifier, (method, parameterTypes)); 257 | } 258 | catch (ArgumentException) 259 | { 260 | if (result.ContainsKey(identifier)) 261 | { 262 | throw new InvalidOperationException($"The assembly '{assemblyName}' contains more than one " + 263 | $"[JSInvokable] method with identifier '{identifier}'. All [JSInvokable] methods within the same " + 264 | $"assembly must have different identifiers. You can pass a custom identifier as a parameter to " + 265 | $"the [JSInvokable] attribute."); 266 | } 267 | else 268 | { 269 | throw; 270 | } 271 | } 272 | } 273 | 274 | return result; 275 | } 276 | 277 | private static Assembly GetRequiredLoadedAssembly(string assemblyName) 278 | { 279 | // We don't want to load assemblies on demand here, because we don't necessarily trust 280 | // "assemblyName" to be something the developer intended to load. So only pick from the 281 | // set of already-loaded assemblies. 282 | // In some edge cases this might force developers to explicitly call something on the 283 | // target assembly (from .NET) before they can invoke its allowed methods from JS. 284 | var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies(); 285 | return loadedAssemblies.FirstOrDefault(a => a.GetName().Name.Equals(assemblyName, StringComparison.Ordinal)) 286 | ?? throw new ArgumentException($"There is no loaded assembly with the name '{assemblyName}'."); 287 | } 288 | 289 | private static Exception UnwrapException(Exception ex) 290 | { 291 | while ((ex is AggregateException || ex is TargetInvocationException) && ex.InnerException != null) 292 | { 293 | ex = ex.InnerException; 294 | } 295 | 296 | return ex; 297 | } 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /src/Microsoft.JSInterop/DotNetObjectRef.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Threading; 6 | 7 | namespace Microsoft.JSInterop 8 | { 9 | /// 10 | /// Wraps a JS interop argument, indicating that the value should not be serialized as JSON 11 | /// but instead should be passed as a reference. 12 | /// 13 | /// To avoid leaking memory, the reference must later be disposed by JS code or by .NET code. 14 | /// 15 | public class DotNetObjectRef : IDisposable 16 | { 17 | /// 18 | /// Gets the object instance represented by this wrapper. 19 | /// 20 | public object Value { get; } 21 | 22 | // We track an associated IJSRuntime purely so that this class can be IDisposable 23 | // in the normal way. Developers are more likely to use objectRef.Dispose() than 24 | // some less familiar API such as JSRuntime.Current.UntrackObjectRef(objectRef). 25 | private IJSRuntime _attachedToRuntime; 26 | 27 | /// 28 | /// Constructs an instance of . 29 | /// 30 | /// The value being wrapped. 31 | public DotNetObjectRef(object value) 32 | { 33 | Value = value; 34 | } 35 | 36 | /// 37 | /// Ensures the is associated with the specified . 38 | /// Developers do not normally need to invoke this manually, since it is called automatically by 39 | /// framework code. 40 | /// 41 | /// The . 42 | public void EnsureAttachedToJsRuntime(IJSRuntime runtime) 43 | { 44 | // The reason we populate _attachedToRuntime here rather than in the constructor 45 | // is to ensure developers can't accidentally try to reuse DotNetObjectRef across 46 | // different IJSRuntime instances. This method gets called as part of serializing 47 | // the DotNetObjectRef during an interop call. 48 | 49 | var existingRuntime = Interlocked.CompareExchange(ref _attachedToRuntime, runtime, null); 50 | if (existingRuntime != null && existingRuntime != runtime) 51 | { 52 | throw new InvalidOperationException($"The {nameof(DotNetObjectRef)} is already associated with a different {nameof(IJSRuntime)}. Do not attempt to re-use {nameof(DotNetObjectRef)} instances with multiple {nameof(IJSRuntime)} instances."); 53 | } 54 | } 55 | 56 | /// 57 | /// Stops tracking this object reference, allowing it to be garbage collected 58 | /// (if there are no other references to it). Once the instance is disposed, it 59 | /// can no longer be used in interop calls from JavaScript code. 60 | /// 61 | public void Dispose() 62 | { 63 | _attachedToRuntime?.UntrackObjectRef(this); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Microsoft.JSInterop/ICustomArgSerializer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.JSInterop.Internal 5 | { 6 | // This is "soft" internal because we're trying to avoid expanding JsonUtil into a sophisticated 7 | // API. Developers who want that would be better served by using a different JSON package 8 | // instead. Also the perf implications of the ICustomArgSerializer approach aren't ideal 9 | // (it forces structs to be boxed, and returning a dictionary means lots more allocations 10 | // and boxing of any value-typed properties). 11 | 12 | /// 13 | /// Internal. Intended for framework use only. 14 | /// 15 | public interface ICustomArgSerializer 16 | { 17 | /// 18 | /// Internal. Intended for framework use only. 19 | /// 20 | object ToJsonPrimitive(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Microsoft.JSInterop/IJSInProcessRuntime.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.JSInterop 5 | { 6 | /// 7 | /// Represents an instance of a JavaScript runtime to which calls may be dispatched. 8 | /// 9 | public interface IJSInProcessRuntime : IJSRuntime 10 | { 11 | /// 12 | /// Invokes the specified JavaScript function synchronously. 13 | /// 14 | /// The JSON-serializable return type. 15 | /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. 16 | /// JSON-serializable arguments. 17 | /// An instance of obtained by JSON-deserializing the return value. 18 | T Invoke(string identifier, params object[] args); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Microsoft.JSInterop/IJSRuntime.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Threading.Tasks; 5 | 6 | namespace Microsoft.JSInterop 7 | { 8 | /// 9 | /// Represents an instance of a JavaScript runtime to which calls may be dispatched. 10 | /// 11 | public interface IJSRuntime 12 | { 13 | /// 14 | /// Invokes the specified JavaScript function asynchronously. 15 | /// 16 | /// The JSON-serializable return type. 17 | /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. 18 | /// JSON-serializable arguments. 19 | /// An instance of obtained by JSON-deserializing the return value. 20 | Task InvokeAsync(string identifier, params object[] args); 21 | 22 | /// 23 | /// Stops tracking the .NET object represented by the . 24 | /// This allows it to be garbage collected (if nothing else holds a reference to it) 25 | /// and means the JS-side code can no longer invoke methods on the instance or pass 26 | /// it as an argument to subsequent calls. 27 | /// 28 | /// The reference to stop tracking. 29 | /// This method is called automatically by . 30 | void UntrackObjectRef(DotNetObjectRef dotNetObjectRef); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Microsoft.JSInterop/InteropArgSerializerStrategy.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using Microsoft.JSInterop.Internal; 5 | using SimpleJson; 6 | using System; 7 | using System.Collections.Generic; 8 | 9 | namespace Microsoft.JSInterop 10 | { 11 | internal class InteropArgSerializerStrategy : PocoJsonSerializerStrategy 12 | { 13 | private readonly JSRuntimeBase _jsRuntime; 14 | private const string _dotNetObjectPrefix = "__dotNetObject:"; 15 | private object _storageLock = new object(); 16 | private long _nextId = 1; // Start at 1, because 0 signals "no object" 17 | private Dictionary _trackedRefsById = new Dictionary(); 18 | private Dictionary _trackedIdsByRef = new Dictionary(); 19 | 20 | public InteropArgSerializerStrategy(JSRuntimeBase jsRuntime) 21 | { 22 | _jsRuntime = jsRuntime ?? throw new ArgumentNullException(nameof(jsRuntime)); 23 | } 24 | 25 | protected override bool TrySerializeKnownTypes(object input, out object output) 26 | { 27 | switch (input) 28 | { 29 | case DotNetObjectRef marshalByRefValue: 30 | EnsureDotNetObjectTracked(marshalByRefValue, out var id); 31 | 32 | // Special value format recognized by the code in Microsoft.JSInterop.js 33 | // If we have to make it more clash-resistant, we can do 34 | output = _dotNetObjectPrefix + id; 35 | 36 | return true; 37 | 38 | case ICustomArgSerializer customArgSerializer: 39 | output = customArgSerializer.ToJsonPrimitive(); 40 | return true; 41 | 42 | default: 43 | return base.TrySerializeKnownTypes(input, out output); 44 | } 45 | } 46 | 47 | public override object DeserializeObject(object value, Type type) 48 | { 49 | if (value is string valueString) 50 | { 51 | if (valueString.StartsWith(_dotNetObjectPrefix)) 52 | { 53 | var dotNetObjectId = long.Parse(valueString.Substring(_dotNetObjectPrefix.Length)); 54 | return FindDotNetObject(dotNetObjectId); 55 | } 56 | } 57 | 58 | return base.DeserializeObject(value, type); 59 | } 60 | 61 | public object FindDotNetObject(long dotNetObjectId) 62 | { 63 | lock (_storageLock) 64 | { 65 | return _trackedRefsById.TryGetValue(dotNetObjectId, out var dotNetObjectRef) 66 | ? dotNetObjectRef.Value 67 | : throw new ArgumentException($"There is no tracked object with id '{dotNetObjectId}'. Perhaps the reference was already released.", nameof(dotNetObjectId)); 68 | } 69 | } 70 | 71 | /// 72 | /// Stops tracking the specified .NET object reference. 73 | /// This overload is typically invoked from JS code via JS interop. 74 | /// 75 | /// The ID of the . 76 | public void ReleaseDotNetObject(long dotNetObjectId) 77 | { 78 | lock (_storageLock) 79 | { 80 | if (_trackedRefsById.TryGetValue(dotNetObjectId, out var dotNetObjectRef)) 81 | { 82 | _trackedRefsById.Remove(dotNetObjectId); 83 | _trackedIdsByRef.Remove(dotNetObjectRef); 84 | } 85 | } 86 | } 87 | 88 | /// 89 | /// Stops tracking the specified .NET object reference. 90 | /// This overload is typically invoked from .NET code by . 91 | /// 92 | /// The . 93 | public void ReleaseDotNetObject(DotNetObjectRef dotNetObjectRef) 94 | { 95 | lock (_storageLock) 96 | { 97 | if (_trackedIdsByRef.TryGetValue(dotNetObjectRef, out var dotNetObjectId)) 98 | { 99 | _trackedRefsById.Remove(dotNetObjectId); 100 | _trackedIdsByRef.Remove(dotNetObjectRef); 101 | } 102 | } 103 | } 104 | 105 | private void EnsureDotNetObjectTracked(DotNetObjectRef dotNetObjectRef, out long dotNetObjectId) 106 | { 107 | dotNetObjectRef.EnsureAttachedToJsRuntime(_jsRuntime); 108 | 109 | lock (_storageLock) 110 | { 111 | // Assign an ID only if it doesn't already have one 112 | if (!_trackedIdsByRef.TryGetValue(dotNetObjectRef, out dotNetObjectId)) 113 | { 114 | dotNetObjectId = _nextId++; 115 | _trackedRefsById.Add(dotNetObjectId, dotNetObjectRef); 116 | _trackedIdsByRef.Add(dotNetObjectRef, dotNetObjectId); 117 | } 118 | } 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Microsoft.JSInterop/JSAsyncCallResult.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.JSInterop.Internal 5 | { 6 | // This type takes care of a special case in handling the result of an async call from 7 | // .NET to JS. The information about what type the result should be exists only on the 8 | // corresponding TaskCompletionSource. We don't have that information at the time 9 | // that we deserialize the incoming argsJson before calling DotNetDispatcher.EndInvoke. 10 | // Declaring the EndInvoke parameter type as JSAsyncCallResult defers the deserialization 11 | // until later when we have access to the TaskCompletionSource. 12 | // 13 | // There's no reason why developers would need anything similar to this in user code, 14 | // because this is the mechanism by which we resolve the incoming argsJson to the correct 15 | // user types before completing calls. 16 | // 17 | // It's marked as 'public' only because it has to be for use as an argument on a 18 | // [JSInvokable] method. 19 | 20 | /// 21 | /// Intended for framework use only. 22 | /// 23 | public class JSAsyncCallResult 24 | { 25 | internal object ResultOrException { get; } 26 | 27 | /// 28 | /// Constructs an instance of . 29 | /// 30 | /// The result of the call. 31 | internal JSAsyncCallResult(object resultOrException) 32 | { 33 | ResultOrException = resultOrException; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Microsoft.JSInterop/JSException.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | 6 | namespace Microsoft.JSInterop 7 | { 8 | /// 9 | /// Represents errors that occur during an interop call from .NET to JavaScript. 10 | /// 11 | public class JSException : Exception 12 | { 13 | /// 14 | /// Constructs an instance of . 15 | /// 16 | /// The exception message. 17 | public JSException(string message) : base(message) 18 | { 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Microsoft.JSInterop/JSInProcessRuntimeBase.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.JSInterop 5 | { 6 | /// 7 | /// Abstract base class for an in-process JavaScript runtime. 8 | /// 9 | public abstract class JSInProcessRuntimeBase : JSRuntimeBase, IJSInProcessRuntime 10 | { 11 | /// 12 | /// Invokes the specified JavaScript function synchronously. 13 | /// 14 | /// The JSON-serializable return type. 15 | /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. 16 | /// JSON-serializable arguments. 17 | /// An instance of obtained by JSON-deserializing the return value. 18 | public T Invoke(string identifier, params object[] args) 19 | { 20 | var resultJson = InvokeJS(identifier, Json.Serialize(args, ArgSerializerStrategy)); 21 | return Json.Deserialize(resultJson, ArgSerializerStrategy); 22 | } 23 | 24 | /// 25 | /// Performs a synchronous function invocation. 26 | /// 27 | /// The identifier for the function to invoke. 28 | /// A JSON representation of the arguments. 29 | /// A JSON representation of the result. 30 | protected abstract string InvokeJS(string identifier, string argsJson); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Microsoft.JSInterop/JSInvokableAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | 6 | namespace Microsoft.JSInterop 7 | { 8 | /// 9 | /// Identifies a .NET method as allowing invocation from JavaScript code. 10 | /// Any method marked with this attribute may receive arbitrary parameter values 11 | /// from untrusted callers. All inputs should be validated carefully. 12 | /// 13 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] 14 | public class JSInvokableAttribute : Attribute 15 | { 16 | /// 17 | /// Gets the identifier for the method. The identifier must be unique within the scope 18 | /// of an assembly. 19 | /// 20 | /// If not set, the identifier is taken from the name of the method. In this case the 21 | /// method name must be unique within the assembly. 22 | /// 23 | public string Identifier { get; } 24 | 25 | /// 26 | /// Constructs an instance of without setting 27 | /// an identifier for the method. 28 | /// 29 | public JSInvokableAttribute() 30 | { 31 | } 32 | 33 | /// 34 | /// Constructs an instance of using the specified 35 | /// identifier. 36 | /// 37 | /// An identifier for the method, which must be unique within the scope of the assembly. 38 | public JSInvokableAttribute(string identifier) 39 | { 40 | if (string.IsNullOrEmpty(identifier)) 41 | { 42 | throw new ArgumentException("Cannot be null or empty", nameof(identifier)); 43 | } 44 | 45 | Identifier = identifier; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Microsoft.JSInterop/JSRuntime.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Threading; 6 | 7 | namespace Microsoft.JSInterop 8 | { 9 | /// 10 | /// Provides mechanisms for accessing the current . 11 | /// 12 | public static class JSRuntime 13 | { 14 | private static AsyncLocal _currentJSRuntime 15 | = new AsyncLocal(); 16 | 17 | /// 18 | /// Gets the current , if any. 19 | /// 20 | public static IJSRuntime Current => _currentJSRuntime.Value; 21 | 22 | /// 23 | /// Sets the current JS runtime to the supplied instance. 24 | /// 25 | /// This is intended for framework use. Developers should not normally need to call this method. 26 | /// 27 | /// The new current . 28 | public static void SetCurrentJSRuntime(IJSRuntime instance) 29 | { 30 | _currentJSRuntime.Value = instance 31 | ?? throw new ArgumentNullException(nameof(instance)); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Microsoft.JSInterop/JSRuntimeBase.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Concurrent; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace Microsoft.JSInterop 10 | { 11 | /// 12 | /// Abstract base class for a JavaScript runtime. 13 | /// 14 | public abstract class JSRuntimeBase : IJSRuntime 15 | { 16 | private long _nextPendingTaskId = 1; // Start at 1 because zero signals "no response needed" 17 | private readonly ConcurrentDictionary _pendingTasks 18 | = new ConcurrentDictionary(); 19 | 20 | internal InteropArgSerializerStrategy ArgSerializerStrategy { get; } 21 | 22 | /// 23 | /// Constructs an instance of . 24 | /// 25 | public JSRuntimeBase() 26 | { 27 | ArgSerializerStrategy = new InteropArgSerializerStrategy(this); 28 | } 29 | 30 | /// 31 | public void UntrackObjectRef(DotNetObjectRef dotNetObjectRef) 32 | => ArgSerializerStrategy.ReleaseDotNetObject(dotNetObjectRef); 33 | 34 | /// 35 | /// Invokes the specified JavaScript function asynchronously. 36 | /// 37 | /// The JSON-serializable return type. 38 | /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. 39 | /// JSON-serializable arguments. 40 | /// An instance of obtained by JSON-deserializing the return value. 41 | public Task InvokeAsync(string identifier, params object[] args) 42 | { 43 | // We might consider also adding a default timeout here in case we don't want to 44 | // risk a memory leak in the scenario where the JS-side code is failing to complete 45 | // the operation. 46 | 47 | var taskId = Interlocked.Increment(ref _nextPendingTaskId); 48 | var tcs = new TaskCompletionSource(); 49 | _pendingTasks[taskId] = tcs; 50 | 51 | try 52 | { 53 | var argsJson = args?.Length > 0 54 | ? Json.Serialize(args, ArgSerializerStrategy) 55 | : null; 56 | BeginInvokeJS(taskId, identifier, argsJson); 57 | return tcs.Task; 58 | } 59 | catch 60 | { 61 | _pendingTasks.TryRemove(taskId, out _); 62 | throw; 63 | } 64 | } 65 | 66 | /// 67 | /// Begins an asynchronous function invocation. 68 | /// 69 | /// The identifier for the function invocation, or zero if no async callback is required. 70 | /// The identifier for the function to invoke. 71 | /// A JSON representation of the arguments. 72 | protected abstract void BeginInvokeJS(long asyncHandle, string identifier, string argsJson); 73 | 74 | internal void EndInvokeDotNet(string callId, bool success, object resultOrException) 75 | { 76 | // For failures, the common case is to call EndInvokeDotNet with the Exception object. 77 | // For these we'll serialize as something that's useful to receive on the JS side. 78 | // If the value is not an Exception, we'll just rely on it being directly JSON-serializable. 79 | if (!success && resultOrException is Exception) 80 | { 81 | resultOrException = resultOrException.ToString(); 82 | } 83 | 84 | // We pass 0 as the async handle because we don't want the JS-side code to 85 | // send back any notification (we're just providing a result for an existing async call) 86 | BeginInvokeJS(0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", Json.Serialize(new[] { 87 | callId, 88 | success, 89 | resultOrException 90 | }, ArgSerializerStrategy)); 91 | } 92 | 93 | internal void EndInvokeJS(long asyncHandle, bool succeeded, object resultOrException) 94 | { 95 | if (!_pendingTasks.TryRemove(asyncHandle, out var tcs)) 96 | { 97 | throw new ArgumentException($"There is no pending task with handle '{asyncHandle}'."); 98 | } 99 | 100 | if (succeeded) 101 | { 102 | var resultType = TaskGenericsUtil.GetTaskCompletionSourceResultType(tcs); 103 | if (resultOrException is SimpleJson.JsonObject || resultOrException is SimpleJson.JsonArray) 104 | { 105 | resultOrException = ArgSerializerStrategy.DeserializeObject(resultOrException, resultType); 106 | } 107 | 108 | TaskGenericsUtil.SetTaskCompletionSourceResult(tcs, resultOrException); 109 | } 110 | else 111 | { 112 | TaskGenericsUtil.SetTaskCompletionSourceException(tcs, new JSException(resultOrException.ToString())); 113 | } 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/Microsoft.JSInterop/Json/CamelCase.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | 6 | namespace Microsoft.JSInterop 7 | { 8 | internal static class CamelCase 9 | { 10 | public static string MemberNameToCamelCase(string value) 11 | { 12 | if (string.IsNullOrEmpty(value)) 13 | { 14 | throw new ArgumentException( 15 | $"The value '{value ?? "null"}' is not a valid member name.", 16 | nameof(value)); 17 | } 18 | 19 | // If we don't need to modify the value, bail out without creating a char array 20 | if (!char.IsUpper(value[0])) 21 | { 22 | return value; 23 | } 24 | 25 | // We have to modify at least one character 26 | var chars = value.ToCharArray(); 27 | 28 | var length = chars.Length; 29 | if (length < 2 || !char.IsUpper(chars[1])) 30 | { 31 | // Only the first character needs to be modified 32 | // Note that this branch is functionally necessary, because the 'else' branch below 33 | // never looks at char[1]. It's always looking at the n+2 character. 34 | chars[0] = char.ToLowerInvariant(chars[0]); 35 | } 36 | else 37 | { 38 | // If chars[0] and chars[1] are both upper, then we'll lowercase the first char plus 39 | // any consecutive uppercase ones, stopping if we find any char that is followed by a 40 | // non-uppercase one 41 | var i = 0; 42 | while (i < length) 43 | { 44 | chars[i] = char.ToLowerInvariant(chars[i]); 45 | 46 | i++; 47 | 48 | // If the next-plus-one char isn't also uppercase, then we're now on the last uppercase, so stop 49 | if (i < length - 1 && !char.IsUpper(chars[i + 1])) 50 | { 51 | break; 52 | } 53 | } 54 | } 55 | 56 | return new string(chars); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Microsoft.JSInterop/Json/Json.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.JSInterop 5 | { 6 | /// 7 | /// Provides mechanisms for converting between .NET objects and JSON strings for use 8 | /// when making calls to JavaScript functions via . 9 | /// 10 | /// Warning: This is not intended as a general-purpose JSON library. It is only intended 11 | /// for use when making calls via . Eventually its implementation 12 | /// will be replaced by something more general-purpose. 13 | /// 14 | public static class Json 15 | { 16 | /// 17 | /// Serializes the value as a JSON string. 18 | /// 19 | /// The value to serialize. 20 | /// The JSON string. 21 | public static string Serialize(object value) 22 | => SimpleJson.SimpleJson.SerializeObject(value); 23 | 24 | internal static string Serialize(object value, SimpleJson.IJsonSerializerStrategy serializerStrategy) 25 | => SimpleJson.SimpleJson.SerializeObject(value, serializerStrategy); 26 | 27 | /// 28 | /// Deserializes the JSON string, creating an object of the specified generic type. 29 | /// 30 | /// The type of object to create. 31 | /// The JSON string. 32 | /// An object of the specified type. 33 | public static T Deserialize(string json) 34 | => SimpleJson.SimpleJson.DeserializeObject(json); 35 | 36 | internal static T Deserialize(string json, SimpleJson.IJsonSerializerStrategy serializerStrategy) 37 | => SimpleJson.SimpleJson.DeserializeObject(json, serializerStrategy); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Microsoft.JSInterop/Json/SimpleJson/README.txt: -------------------------------------------------------------------------------- 1 | SimpleJson is from https://github.com/facebook-csharp-sdk/simple-json 2 | 3 | LICENSE (from https://github.com/facebook-csharp-sdk/simple-json/blob/08b6871e8f63e866810d25e7a03c48502c9a234b/LICENSE.txt): 4 | ===== 5 | Copyright (c) 2011, The Outercurve Foundation 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining 8 | a copy of this software and associated documentation files (the 9 | "Software"), to deal in the Software without restriction, including 10 | without limitation the rights to use, copy, modify, merge, publish, 11 | distribute, sublicense, and/or sell copies of the Software, and to 12 | permit persons to whom the Software is furnished to do so, subject to 13 | the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 22 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 24 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/Microsoft.JSInterop/Microsoft.JSInterop.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/Microsoft.JSInterop/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("Microsoft.JSInterop.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] 4 | -------------------------------------------------------------------------------- /src/Microsoft.JSInterop/TaskGenericsUtil.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Concurrent; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | 9 | namespace Microsoft.JSInterop 10 | { 11 | internal static class TaskGenericsUtil 12 | { 13 | private static ConcurrentDictionary _cachedResultGetters 14 | = new ConcurrentDictionary(); 15 | 16 | private static ConcurrentDictionary _cachedResultSetters 17 | = new ConcurrentDictionary(); 18 | 19 | public static void SetTaskCompletionSourceResult(object taskCompletionSource, object result) 20 | => CreateResultSetter(taskCompletionSource).SetResult(taskCompletionSource, result); 21 | 22 | public static void SetTaskCompletionSourceException(object taskCompletionSource, Exception exception) 23 | => CreateResultSetter(taskCompletionSource).SetException(taskCompletionSource, exception); 24 | 25 | public static Type GetTaskCompletionSourceResultType(object taskCompletionSource) 26 | => CreateResultSetter(taskCompletionSource).ResultType; 27 | 28 | public static object GetTaskResult(Task task) 29 | { 30 | var getter = _cachedResultGetters.GetOrAdd(task.GetType(), taskInstanceType => 31 | { 32 | var resultType = GetTaskResultType(taskInstanceType); 33 | return resultType == null 34 | ? new VoidTaskResultGetter() 35 | : (ITaskResultGetter)Activator.CreateInstance( 36 | typeof(TaskResultGetter<>).MakeGenericType(resultType)); 37 | }); 38 | return getter.GetResult(task); 39 | } 40 | 41 | private static Type GetTaskResultType(Type taskType) 42 | { 43 | // It might be something derived from Task or Task, so we have to scan 44 | // up the inheritance hierarchy to find the Task or Task 45 | while (taskType != typeof(Task) && 46 | (!taskType.IsGenericType || taskType.GetGenericTypeDefinition() != typeof(Task<>))) 47 | { 48 | taskType = taskType.BaseType 49 | ?? throw new ArgumentException($"The type '{taskType.FullName}' is not inherited from '{typeof(Task).FullName}'."); 50 | } 51 | 52 | return taskType.IsGenericType 53 | ? taskType.GetGenericArguments().Single() 54 | : null; 55 | } 56 | 57 | interface ITcsResultSetter 58 | { 59 | Type ResultType { get; } 60 | void SetResult(object taskCompletionSource, object result); 61 | void SetException(object taskCompletionSource, Exception exception); 62 | } 63 | 64 | private interface ITaskResultGetter 65 | { 66 | object GetResult(Task task); 67 | } 68 | 69 | private class TaskResultGetter : ITaskResultGetter 70 | { 71 | public object GetResult(Task task) => ((Task)task).Result; 72 | } 73 | 74 | private class VoidTaskResultGetter : ITaskResultGetter 75 | { 76 | public object GetResult(Task task) 77 | { 78 | task.Wait(); // Throw if the task failed 79 | return null; 80 | } 81 | } 82 | 83 | private class TcsResultSetter : ITcsResultSetter 84 | { 85 | public Type ResultType => typeof(T); 86 | 87 | public void SetResult(object tcs, object result) 88 | { 89 | var typedTcs = (TaskCompletionSource)tcs; 90 | 91 | // If necessary, attempt a cast 92 | var typedResult = result is T resultT 93 | ? resultT 94 | : (T)Convert.ChangeType(result, typeof(T)); 95 | 96 | typedTcs.SetResult(typedResult); 97 | } 98 | 99 | public void SetException(object tcs, Exception exception) 100 | { 101 | var typedTcs = (TaskCompletionSource)tcs; 102 | typedTcs.SetException(exception); 103 | } 104 | } 105 | 106 | private static ITcsResultSetter CreateResultSetter(object taskCompletionSource) 107 | { 108 | return _cachedResultSetters.GetOrAdd(taskCompletionSource.GetType(), tcsType => 109 | { 110 | var resultType = tcsType.GetGenericArguments().Single(); 111 | return (ITcsResultSetter)Activator.CreateInstance( 112 | typeof(TcsResultSetter<>).MakeGenericType(resultType)); 113 | }); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/Mono.WebAssembly.Interop/InternalCalls.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Runtime.CompilerServices; 5 | 6 | namespace WebAssembly.JSInterop 7 | { 8 | /// 9 | /// Methods that map to the functions compiled into the Mono WebAssembly runtime, 10 | /// as defined by 'mono_add_internal_call' calls in driver.c 11 | /// 12 | internal class InternalCalls 13 | { 14 | // The exact namespace, type, and method names must match the corresponding entries 15 | // in driver.c in the Mono distribution 16 | 17 | // We're passing asyncHandle by ref not because we want it to be writable, but so it gets 18 | // passed as a pointer (4 bytes). We can pass 4-byte values, but not 8-byte ones. 19 | [MethodImpl(MethodImplOptions.InternalCall)] 20 | public static extern string InvokeJSMarshalled(out string exception, ref long asyncHandle, string functionIdentifier, string argsJson); 21 | 22 | [MethodImpl(MethodImplOptions.InternalCall)] 23 | public static extern TRes InvokeJSUnmarshalled(out string exception, string functionIdentifier, T0 arg0, T1 arg1, T2 arg2); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Mono.WebAssembly.Interop/Mono.WebAssembly.Interop.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/Mono.WebAssembly.Interop/MonoWebAssemblyJSRuntime.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using Microsoft.JSInterop; 5 | using WebAssembly.JSInterop; 6 | 7 | namespace Mono.WebAssembly.Interop 8 | { 9 | /// 10 | /// Provides methods for invoking JavaScript functions for applications running 11 | /// on the Mono WebAssembly runtime. 12 | /// 13 | public class MonoWebAssemblyJSRuntime : JSInProcessRuntimeBase 14 | { 15 | /// 16 | protected override string InvokeJS(string identifier, string argsJson) 17 | { 18 | var noAsyncHandle = default(long); 19 | var result = InternalCalls.InvokeJSMarshalled(out var exception, ref noAsyncHandle, identifier, argsJson); 20 | return exception != null 21 | ? throw new JSException(exception) 22 | : result; 23 | } 24 | 25 | /// 26 | protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson) 27 | { 28 | InternalCalls.InvokeJSMarshalled(out _, ref asyncHandle, identifier, argsJson); 29 | } 30 | 31 | // Invoked via Mono's JS interop mechanism (invoke_method) 32 | private static string InvokeDotNet(string assemblyName, string methodIdentifier, string dotNetObjectId, string argsJson) 33 | => DotNetDispatcher.Invoke(assemblyName, methodIdentifier, dotNetObjectId == null ? default : long.Parse(dotNetObjectId), argsJson); 34 | 35 | // Invoked via Mono's JS interop mechanism (invoke_method) 36 | private static void BeginInvokeDotNet(string callId, string assemblyNameOrDotNetObjectId, string methodIdentifier, string argsJson) 37 | { 38 | // Figure out whether 'assemblyNameOrDotNetObjectId' is the assembly name or the instance ID 39 | // We only need one for any given call. This helps to work around the limitation that we can 40 | // only pass a maximum of 4 args in a call from JS to Mono WebAssembly. 41 | string assemblyName; 42 | long dotNetObjectId; 43 | if (char.IsDigit(assemblyNameOrDotNetObjectId[0])) 44 | { 45 | dotNetObjectId = long.Parse(assemblyNameOrDotNetObjectId); 46 | assemblyName = null; 47 | } 48 | else 49 | { 50 | dotNetObjectId = default; 51 | assemblyName = assemblyNameOrDotNetObjectId; 52 | } 53 | 54 | DotNetDispatcher.BeginInvoke(callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson); 55 | } 56 | 57 | #region Custom MonoWebAssemblyJSRuntime methods 58 | 59 | /// 60 | /// Invokes the JavaScript function registered with the specified identifier. 61 | /// 62 | /// The .NET type corresponding to the function's return value type. 63 | /// The identifier used when registering the target function. 64 | /// The result of the function invocation. 65 | public TRes InvokeUnmarshalled(string identifier) 66 | => InvokeUnmarshalled(identifier, null, null, null); 67 | 68 | /// 69 | /// Invokes the JavaScript function registered with the specified identifier. 70 | /// 71 | /// The type of the first argument. 72 | /// The .NET type corresponding to the function's return value type. 73 | /// The identifier used when registering the target function. 74 | /// The first argument. 75 | /// The result of the function invocation. 76 | public TRes InvokeUnmarshalled(string identifier, T0 arg0) 77 | => InvokeUnmarshalled(identifier, arg0, null, null); 78 | 79 | /// 80 | /// Invokes the JavaScript function registered with the specified identifier. 81 | /// 82 | /// The type of the first argument. 83 | /// The type of the second argument. 84 | /// The .NET type corresponding to the function's return value type. 85 | /// The identifier used when registering the target function. 86 | /// The first argument. 87 | /// The second argument. 88 | /// The result of the function invocation. 89 | public TRes InvokeUnmarshalled(string identifier, T0 arg0, T1 arg1) 90 | => InvokeUnmarshalled(identifier, arg0, arg1, null); 91 | 92 | /// 93 | /// Invokes the JavaScript function registered with the specified identifier. 94 | /// 95 | /// The type of the first argument. 96 | /// The type of the second argument. 97 | /// The type of the third argument. 98 | /// The .NET type corresponding to the function's return value type. 99 | /// The identifier used when registering the target function. 100 | /// The first argument. 101 | /// The second argument. 102 | /// The third argument. 103 | /// The result of the function invocation. 104 | public TRes InvokeUnmarshalled(string identifier, T0 arg0, T1 arg1, T2 arg2) 105 | { 106 | var result = InternalCalls.InvokeJSUnmarshalled(out var exception, identifier, arg0, arg1, arg2); 107 | return exception != null 108 | ? throw new JSException(exception) 109 | : result; 110 | } 111 | 112 | #endregion 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /test/Microsoft.JSInterop.Test/DotNetDispatcherTest.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using Xunit; 8 | 9 | namespace Microsoft.JSInterop.Test 10 | { 11 | public class DotNetDispatcherTest 12 | { 13 | private readonly static string thisAssemblyName 14 | = typeof(DotNetDispatcherTest).Assembly.GetName().Name; 15 | private readonly TestJSRuntime jsRuntime 16 | = new TestJSRuntime(); 17 | 18 | [Fact] 19 | public void CannotInvokeWithEmptyAssemblyName() 20 | { 21 | var ex = Assert.Throws(() => 22 | { 23 | DotNetDispatcher.Invoke(" ", "SomeMethod", default, "[]"); 24 | }); 25 | 26 | Assert.StartsWith("Cannot be null, empty, or whitespace.", ex.Message); 27 | Assert.Equal("assemblyName", ex.ParamName); 28 | } 29 | 30 | [Fact] 31 | public void CannotInvokeWithEmptyMethodIdentifier() 32 | { 33 | var ex = Assert.Throws(() => 34 | { 35 | DotNetDispatcher.Invoke("SomeAssembly", " ", default, "[]"); 36 | }); 37 | 38 | Assert.StartsWith("Cannot be null, empty, or whitespace.", ex.Message); 39 | Assert.Equal("methodIdentifier", ex.ParamName); 40 | } 41 | 42 | [Fact] 43 | public void CannotInvokeMethodsOnUnloadedAssembly() 44 | { 45 | var assemblyName = "Some.Fake.Assembly"; 46 | var ex = Assert.Throws(() => 47 | { 48 | DotNetDispatcher.Invoke(assemblyName, "SomeMethod", default, null); 49 | }); 50 | 51 | Assert.Equal($"There is no loaded assembly with the name '{assemblyName}'.", ex.Message); 52 | } 53 | 54 | // Note: Currently it's also not possible to invoke generic methods. 55 | // That's not something determined by DotNetDispatcher, but rather by the fact that we 56 | // don't close over the generics in the reflection code. 57 | // Not defining this behavior through unit tests because the default outcome is 58 | // fine (an exception stating what info is missing). 59 | 60 | [Theory] 61 | [InlineData("MethodOnInternalType")] 62 | [InlineData("PrivateMethod")] 63 | [InlineData("ProtectedMethod")] 64 | [InlineData("StaticMethodWithoutAttribute")] // That's not really its identifier; just making the point that there's no way to invoke it 65 | [InlineData("InstanceMethodWithoutAttribute")] // That's not really its identifier; just making the point that there's no way to invoke it 66 | public void CannotInvokeUnsuitableMethods(string methodIdentifier) 67 | { 68 | var ex = Assert.Throws(() => 69 | { 70 | DotNetDispatcher.Invoke(thisAssemblyName, methodIdentifier, default, null); 71 | }); 72 | 73 | Assert.Equal($"The assembly '{thisAssemblyName}' does not contain a public method with [JSInvokableAttribute(\"{methodIdentifier}\")].", ex.Message); 74 | } 75 | 76 | [Fact] 77 | public Task CanInvokeStaticVoidMethod() => WithJSRuntime(jsRuntime => 78 | { 79 | // Arrange/Act 80 | SomePublicType.DidInvokeMyInvocableStaticVoid = false; 81 | var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticVoid", default, null); 82 | 83 | // Assert 84 | Assert.Null(resultJson); 85 | Assert.True(SomePublicType.DidInvokeMyInvocableStaticVoid); 86 | }); 87 | 88 | [Fact] 89 | public Task CanInvokeStaticNonVoidMethod() => WithJSRuntime(jsRuntime => 90 | { 91 | // Arrange/Act 92 | var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticNonVoid", default, null); 93 | var result = Json.Deserialize(resultJson); 94 | 95 | // Assert 96 | Assert.Equal("Test", result.StringVal); 97 | Assert.Equal(123, result.IntVal); 98 | }); 99 | 100 | [Fact] 101 | public Task CanInvokeStaticNonVoidMethodWithoutCustomIdentifier() => WithJSRuntime(jsRuntime => 102 | { 103 | // Arrange/Act 104 | var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, nameof(SomePublicType.InvokableMethodWithoutCustomIdentifier), default, null); 105 | var result = Json.Deserialize(resultJson); 106 | 107 | // Assert 108 | Assert.Equal("InvokableMethodWithoutCustomIdentifier", result.StringVal); 109 | Assert.Equal(456, result.IntVal); 110 | }); 111 | 112 | [Fact] 113 | public Task CanInvokeStaticWithParams() => WithJSRuntime(jsRuntime => 114 | { 115 | // Arrange: Track a .NET object to use as an arg 116 | var arg3 = new TestDTO { IntVal = 999, StringVal = "My string" }; 117 | jsRuntime.Invoke("unimportant", new DotNetObjectRef(arg3)); 118 | 119 | // Arrange: Remaining args 120 | var argsJson = Json.Serialize(new object[] { 121 | new TestDTO { StringVal = "Another string", IntVal = 456 }, 122 | new[] { 100, 200 }, 123 | "__dotNetObject:1" 124 | }); 125 | 126 | // Act 127 | var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticWithParams", default, argsJson); 128 | var result = Json.Deserialize(resultJson); 129 | 130 | // Assert: First result value marshalled via JSON 131 | var resultDto1 = (TestDTO)jsRuntime.ArgSerializerStrategy.DeserializeObject(result[0], typeof(TestDTO)); 132 | Assert.Equal("ANOTHER STRING", resultDto1.StringVal); 133 | Assert.Equal(756, resultDto1.IntVal); 134 | 135 | // Assert: Second result value marshalled by ref 136 | var resultDto2Ref = (string)result[1]; 137 | Assert.Equal("__dotNetObject:2", resultDto2Ref); 138 | var resultDto2 = (TestDTO)jsRuntime.ArgSerializerStrategy.FindDotNetObject(2); 139 | Assert.Equal("MY STRING", resultDto2.StringVal); 140 | Assert.Equal(1299, resultDto2.IntVal); 141 | }); 142 | 143 | [Fact] 144 | public Task CanInvokeInstanceVoidMethod() => WithJSRuntime(jsRuntime => 145 | { 146 | // Arrange: Track some instance 147 | var targetInstance = new SomePublicType(); 148 | jsRuntime.Invoke("unimportant", new DotNetObjectRef(targetInstance)); 149 | 150 | // Act 151 | var resultJson = DotNetDispatcher.Invoke(null, "InvokableInstanceVoid", 1, null); 152 | 153 | // Assert 154 | Assert.Null(resultJson); 155 | Assert.True(targetInstance.DidInvokeMyInvocableInstanceVoid); 156 | }); 157 | 158 | [Fact] 159 | public Task CanInvokeBaseInstanceVoidMethod() => WithJSRuntime(jsRuntime => 160 | { 161 | // Arrange: Track some instance 162 | var targetInstance = new DerivedClass(); 163 | jsRuntime.Invoke("unimportant", new DotNetObjectRef(targetInstance)); 164 | 165 | // Act 166 | var resultJson = DotNetDispatcher.Invoke(null, "BaseClassInvokableInstanceVoid", 1, null); 167 | 168 | // Assert 169 | Assert.Null(resultJson); 170 | Assert.True(targetInstance.DidInvokeMyBaseClassInvocableInstanceVoid); 171 | }); 172 | 173 | [Fact] 174 | public Task CannotUseDotNetObjectRefAfterDisposal() => WithJSRuntime(jsRuntime => 175 | { 176 | // This test addresses the case where the developer calls objectRef.Dispose() 177 | // from .NET code, as opposed to .dispose() from JS code 178 | 179 | // Arrange: Track some instance, then dispose it 180 | var targetInstance = new SomePublicType(); 181 | var objectRef = new DotNetObjectRef(targetInstance); 182 | jsRuntime.Invoke("unimportant", objectRef); 183 | objectRef.Dispose(); 184 | 185 | // Act/Assert 186 | var ex = Assert.Throws( 187 | () => DotNetDispatcher.Invoke(null, "InvokableInstanceVoid", 1, null)); 188 | Assert.StartsWith("There is no tracked object with id '1'.", ex.Message); 189 | }); 190 | 191 | [Fact] 192 | public Task CannotUseDotNetObjectRefAfterReleaseDotNetObject() => WithJSRuntime(jsRuntime => 193 | { 194 | // This test addresses the case where the developer calls .dispose() 195 | // from JS code, as opposed to objectRef.Dispose() from .NET code 196 | 197 | // Arrange: Track some instance, then dispose it 198 | var targetInstance = new SomePublicType(); 199 | var objectRef = new DotNetObjectRef(targetInstance); 200 | jsRuntime.Invoke("unimportant", objectRef); 201 | DotNetDispatcher.ReleaseDotNetObject(1); 202 | 203 | // Act/Assert 204 | var ex = Assert.Throws( 205 | () => DotNetDispatcher.Invoke(null, "InvokableInstanceVoid", 1, null)); 206 | Assert.StartsWith("There is no tracked object with id '1'.", ex.Message); 207 | }); 208 | 209 | [Fact] 210 | public Task CanInvokeInstanceMethodWithParams() => WithJSRuntime(jsRuntime => 211 | { 212 | // Arrange: Track some instance plus another object we'll pass as a param 213 | var targetInstance = new SomePublicType(); 214 | var arg2 = new TestDTO { IntVal = 1234, StringVal = "My string" }; 215 | jsRuntime.Invoke("unimportant", 216 | new DotNetObjectRef(targetInstance), 217 | new DotNetObjectRef(arg2)); 218 | var argsJson = "[\"myvalue\",\"__dotNetObject:2\"]"; 219 | 220 | // Act 221 | var resultJson = DotNetDispatcher.Invoke(null, "InvokableInstanceMethod", 1, argsJson); 222 | 223 | // Assert 224 | Assert.Equal("[\"You passed myvalue\",\"__dotNetObject:3\"]", resultJson); 225 | var resultDto = (TestDTO)jsRuntime.ArgSerializerStrategy.FindDotNetObject(3); 226 | Assert.Equal(1235, resultDto.IntVal); 227 | Assert.Equal("MY STRING", resultDto.StringVal); 228 | }); 229 | 230 | [Fact] 231 | public void CannotInvokeWithIncorrectNumberOfParams() 232 | { 233 | // Arrange 234 | var argsJson = Json.Serialize(new object[] { 1, 2, 3, 4 }); 235 | 236 | // Act/Assert 237 | var ex = Assert.Throws(() => 238 | { 239 | DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticWithParams", default, argsJson); 240 | }); 241 | 242 | Assert.Equal("In call to 'InvocableStaticWithParams', expected 3 parameters but received 4.", ex.Message); 243 | } 244 | 245 | [Fact] 246 | public Task CanInvokeAsyncMethod() => WithJSRuntime(async jsRuntime => 247 | { 248 | // Arrange: Track some instance plus another object we'll pass as a param 249 | var targetInstance = new SomePublicType(); 250 | var arg2 = new TestDTO { IntVal = 1234, StringVal = "My string" }; 251 | jsRuntime.Invoke("unimportant", new DotNetObjectRef(targetInstance), new DotNetObjectRef(arg2)); 252 | 253 | // Arrange: all args 254 | var argsJson = Json.Serialize(new object[] 255 | { 256 | new TestDTO { IntVal = 1000, StringVal = "String via JSON" }, 257 | "__dotNetObject:2" 258 | }); 259 | 260 | // Act 261 | var callId = "123"; 262 | var resultTask = jsRuntime.NextInvocationTask; 263 | DotNetDispatcher.BeginInvoke(callId, null, "InvokableAsyncMethod", 1, argsJson); 264 | await resultTask; 265 | var result = Json.Deserialize(jsRuntime.LastInvocationArgsJson); 266 | var resultValue = (SimpleJson.JsonArray)result[2]; 267 | 268 | // Assert: Correct info to complete the async call 269 | Assert.Equal(0, jsRuntime.LastInvocationAsyncHandle); // 0 because it doesn't want a further callback from JS to .NET 270 | Assert.Equal("DotNet.jsCallDispatcher.endInvokeDotNetFromJS", jsRuntime.LastInvocationIdentifier); 271 | Assert.Equal(3, result.Count); 272 | Assert.Equal(callId, result[0]); 273 | Assert.True((bool)result[1]); // Success flag 274 | 275 | // Assert: First result value marshalled via JSON 276 | var resultDto1 = (TestDTO)jsRuntime.ArgSerializerStrategy.DeserializeObject(resultValue[0], typeof(TestDTO)); 277 | Assert.Equal("STRING VIA JSON", resultDto1.StringVal); 278 | Assert.Equal(2000, resultDto1.IntVal); 279 | 280 | // Assert: Second result value marshalled by ref 281 | var resultDto2Ref = (string)resultValue[1]; 282 | Assert.Equal("__dotNetObject:3", resultDto2Ref); 283 | var resultDto2 = (TestDTO)jsRuntime.ArgSerializerStrategy.FindDotNetObject(3); 284 | Assert.Equal("MY STRING", resultDto2.StringVal); 285 | Assert.Equal(2468, resultDto2.IntVal); 286 | }); 287 | 288 | Task WithJSRuntime(Action testCode) 289 | { 290 | return WithJSRuntime(jsRuntime => 291 | { 292 | testCode(jsRuntime); 293 | return Task.CompletedTask; 294 | }); 295 | } 296 | 297 | async Task WithJSRuntime(Func testCode) 298 | { 299 | // Since the tests rely on the asynclocal JSRuntime.Current, ensure we 300 | // are on a distinct async context with a non-null JSRuntime.Current 301 | await Task.Yield(); 302 | 303 | var runtime = new TestJSRuntime(); 304 | JSRuntime.SetCurrentJSRuntime(runtime); 305 | await testCode(runtime); 306 | } 307 | 308 | internal class SomeInteralType 309 | { 310 | [JSInvokable("MethodOnInternalType")] public void MyMethod() { } 311 | } 312 | 313 | public class SomePublicType 314 | { 315 | public static bool DidInvokeMyInvocableStaticVoid; 316 | public bool DidInvokeMyInvocableInstanceVoid; 317 | 318 | [JSInvokable("PrivateMethod")] private static void MyPrivateMethod() { } 319 | [JSInvokable("ProtectedMethod")] protected static void MyProtectedMethod() { } 320 | protected static void StaticMethodWithoutAttribute() { } 321 | protected static void InstanceMethodWithoutAttribute() { } 322 | 323 | [JSInvokable("InvocableStaticVoid")] public static void MyInvocableVoid() 324 | { 325 | DidInvokeMyInvocableStaticVoid = true; 326 | } 327 | 328 | [JSInvokable("InvocableStaticNonVoid")] 329 | public static object MyInvocableNonVoid() 330 | => new TestDTO { StringVal = "Test", IntVal = 123 }; 331 | 332 | [JSInvokable("InvocableStaticWithParams")] 333 | public static object[] MyInvocableWithParams(TestDTO dtoViaJson, int[] incrementAmounts, TestDTO dtoByRef) 334 | => new object[] 335 | { 336 | new TestDTO // Return via JSON marshalling 337 | { 338 | StringVal = dtoViaJson.StringVal.ToUpperInvariant(), 339 | IntVal = dtoViaJson.IntVal + incrementAmounts.Sum() 340 | }, 341 | new DotNetObjectRef(new TestDTO // Return by ref 342 | { 343 | StringVal = dtoByRef.StringVal.ToUpperInvariant(), 344 | IntVal = dtoByRef.IntVal + incrementAmounts.Sum() 345 | }) 346 | }; 347 | 348 | [JSInvokable] 349 | public static TestDTO InvokableMethodWithoutCustomIdentifier() 350 | => new TestDTO { StringVal = "InvokableMethodWithoutCustomIdentifier", IntVal = 456 }; 351 | 352 | [JSInvokable] 353 | public void InvokableInstanceVoid() 354 | { 355 | DidInvokeMyInvocableInstanceVoid = true; 356 | } 357 | 358 | [JSInvokable] 359 | public object[] InvokableInstanceMethod(string someString, TestDTO someDTO) 360 | { 361 | // Returning an array to make the point that object references 362 | // can be embedded anywhere in the result 363 | return new object[] 364 | { 365 | $"You passed {someString}", 366 | new DotNetObjectRef(new TestDTO 367 | { 368 | IntVal = someDTO.IntVal + 1, 369 | StringVal = someDTO.StringVal.ToUpperInvariant() 370 | }) 371 | }; 372 | } 373 | 374 | [JSInvokable] 375 | public async Task InvokableAsyncMethod(TestDTO dtoViaJson, TestDTO dtoByRef) 376 | { 377 | await Task.Delay(50); 378 | return new object[] 379 | { 380 | new TestDTO // Return via JSON 381 | { 382 | StringVal = dtoViaJson.StringVal.ToUpperInvariant(), 383 | IntVal = dtoViaJson.IntVal * 2, 384 | }, 385 | new DotNetObjectRef(new TestDTO // Return by ref 386 | { 387 | StringVal = dtoByRef.StringVal.ToUpperInvariant(), 388 | IntVal = dtoByRef.IntVal * 2, 389 | }) 390 | }; 391 | } 392 | } 393 | 394 | public class BaseClass 395 | { 396 | public bool DidInvokeMyBaseClassInvocableInstanceVoid; 397 | 398 | [JSInvokable] 399 | public void BaseClassInvokableInstanceVoid() 400 | { 401 | DidInvokeMyBaseClassInvocableInstanceVoid = true; 402 | } 403 | } 404 | 405 | public class DerivedClass : BaseClass 406 | { 407 | } 408 | 409 | public class TestDTO 410 | { 411 | public string StringVal { get; set; } 412 | public int IntVal { get; set; } 413 | } 414 | 415 | public class TestJSRuntime : JSInProcessRuntimeBase 416 | { 417 | private TaskCompletionSource _nextInvocationTcs = new TaskCompletionSource(); 418 | public Task NextInvocationTask => _nextInvocationTcs.Task; 419 | public long LastInvocationAsyncHandle { get; private set; } 420 | public string LastInvocationIdentifier { get; private set; } 421 | public string LastInvocationArgsJson { get; private set; } 422 | 423 | protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson) 424 | { 425 | LastInvocationAsyncHandle = asyncHandle; 426 | LastInvocationIdentifier = identifier; 427 | LastInvocationArgsJson = argsJson; 428 | _nextInvocationTcs.SetResult(null); 429 | _nextInvocationTcs = new TaskCompletionSource(); 430 | } 431 | 432 | protected override string InvokeJS(string identifier, string argsJson) 433 | { 434 | LastInvocationAsyncHandle = default; 435 | LastInvocationIdentifier = identifier; 436 | LastInvocationArgsJson = argsJson; 437 | _nextInvocationTcs.SetResult(null); 438 | _nextInvocationTcs = new TaskCompletionSource(); 439 | return null; 440 | } 441 | } 442 | } 443 | } 444 | -------------------------------------------------------------------------------- /test/Microsoft.JSInterop.Test/DotNetObjectRefTest.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Threading.Tasks; 7 | using Xunit; 8 | 9 | namespace Microsoft.JSInterop.Test 10 | { 11 | public class DotNetObjectRefTest 12 | { 13 | [Fact] 14 | public void CanAccessValue() 15 | { 16 | var obj = new object(); 17 | Assert.Same(obj, new DotNetObjectRef(obj).Value); 18 | } 19 | 20 | [Fact] 21 | public void CanAssociateWithSameRuntimeMultipleTimes() 22 | { 23 | var objRef = new DotNetObjectRef(new object()); 24 | var jsRuntime = new TestJsRuntime(); 25 | objRef.EnsureAttachedToJsRuntime(jsRuntime); 26 | objRef.EnsureAttachedToJsRuntime(jsRuntime); 27 | } 28 | 29 | [Fact] 30 | public void CannotAssociateWithDifferentRuntimes() 31 | { 32 | var objRef = new DotNetObjectRef(new object()); 33 | var jsRuntime1 = new TestJsRuntime(); 34 | var jsRuntime2 = new TestJsRuntime(); 35 | objRef.EnsureAttachedToJsRuntime(jsRuntime1); 36 | 37 | var ex = Assert.Throws( 38 | () => objRef.EnsureAttachedToJsRuntime(jsRuntime2)); 39 | Assert.Contains("Do not attempt to re-use", ex.Message); 40 | } 41 | 42 | [Fact] 43 | public void NotifiesAssociatedJsRuntimeOfDisposal() 44 | { 45 | // Arrange 46 | var objRef = new DotNetObjectRef(new object()); 47 | var jsRuntime = new TestJsRuntime(); 48 | objRef.EnsureAttachedToJsRuntime(jsRuntime); 49 | 50 | // Act 51 | objRef.Dispose(); 52 | 53 | // Assert 54 | Assert.Equal(new[] { objRef }, jsRuntime.UntrackedRefs); 55 | } 56 | 57 | class TestJsRuntime : IJSRuntime 58 | { 59 | public List UntrackedRefs = new List(); 60 | 61 | public Task InvokeAsync(string identifier, params object[] args) 62 | => throw new NotImplementedException(); 63 | 64 | public void UntrackObjectRef(DotNetObjectRef dotNetObjectRef) 65 | => UntrackedRefs.Add(dotNetObjectRef); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /test/Microsoft.JSInterop.Test/JSInProcessRuntimeBaseTest.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using Xunit; 8 | 9 | namespace Microsoft.JSInterop.Test 10 | { 11 | public class JSInProcessRuntimeBaseTest 12 | { 13 | [Fact] 14 | public void DispatchesSyncCallsAndDeserializesResults() 15 | { 16 | // Arrange 17 | var runtime = new TestJSInProcessRuntime 18 | { 19 | NextResultJson = Json.Serialize( 20 | new TestDTO { IntValue = 123, StringValue = "Hello" }) 21 | }; 22 | 23 | // Act 24 | var syncResult = runtime.Invoke("test identifier 1", "arg1", 123, true ); 25 | var call = runtime.InvokeCalls.Single(); 26 | 27 | // Assert 28 | Assert.Equal(123, syncResult.IntValue); 29 | Assert.Equal("Hello", syncResult.StringValue); 30 | Assert.Equal("test identifier 1", call.Identifier); 31 | Assert.Equal("[\"arg1\",123,true]", call.ArgsJson); 32 | } 33 | 34 | [Fact] 35 | public void SerializesDotNetObjectWrappersInKnownFormat() 36 | { 37 | // Arrange 38 | var runtime = new TestJSInProcessRuntime { NextResultJson = null }; 39 | var obj1 = new object(); 40 | var obj2 = new object(); 41 | var obj3 = new object(); 42 | 43 | // Act 44 | // Showing we can pass the DotNetObject either as top-level args or nested 45 | var syncResult = runtime.Invoke("test identifier", 46 | new DotNetObjectRef(obj1), 47 | new Dictionary 48 | { 49 | { "obj2", new DotNetObjectRef(obj2) }, 50 | { "obj3", new DotNetObjectRef(obj3) } 51 | }); 52 | 53 | // Assert: Handles null result string 54 | Assert.Null(syncResult); 55 | 56 | // Assert: Serialized as expected 57 | var call = runtime.InvokeCalls.Single(); 58 | Assert.Equal("test identifier", call.Identifier); 59 | Assert.Equal("[\"__dotNetObject:1\",{\"obj2\":\"__dotNetObject:2\",\"obj3\":\"__dotNetObject:3\"}]", call.ArgsJson); 60 | 61 | // Assert: Objects were tracked 62 | Assert.Same(obj1, runtime.ArgSerializerStrategy.FindDotNetObject(1)); 63 | Assert.Same(obj2, runtime.ArgSerializerStrategy.FindDotNetObject(2)); 64 | Assert.Same(obj3, runtime.ArgSerializerStrategy.FindDotNetObject(3)); 65 | } 66 | 67 | [Fact] 68 | public void SyncCallResultCanIncludeDotNetObjects() 69 | { 70 | // Arrange 71 | var runtime = new TestJSInProcessRuntime 72 | { 73 | NextResultJson = "[\"__dotNetObject:2\",\"__dotNetObject:1\"]" 74 | }; 75 | var obj1 = new object(); 76 | var obj2 = new object(); 77 | 78 | // Act 79 | var syncResult = runtime.Invoke("test identifier", 80 | new DotNetObjectRef(obj1), 81 | "some other arg", 82 | new DotNetObjectRef(obj2)); 83 | var call = runtime.InvokeCalls.Single(); 84 | 85 | // Assert 86 | Assert.Equal(new[] { obj2, obj1 }, syncResult); 87 | } 88 | 89 | class TestDTO 90 | { 91 | public int IntValue { get; set; } 92 | public string StringValue { get; set; } 93 | } 94 | 95 | class TestJSInProcessRuntime : JSInProcessRuntimeBase 96 | { 97 | public List InvokeCalls { get; set; } = new List(); 98 | 99 | public string NextResultJson { get; set; } 100 | 101 | protected override string InvokeJS(string identifier, string argsJson) 102 | { 103 | InvokeCalls.Add(new InvokeArgs { Identifier = identifier, ArgsJson = argsJson }); 104 | return NextResultJson; 105 | } 106 | 107 | public class InvokeArgs 108 | { 109 | public string Identifier { get; set; } 110 | public string ArgsJson { get; set; } 111 | } 112 | 113 | protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson) 114 | => throw new NotImplementedException("This test only covers sync calls"); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /test/Microsoft.JSInterop.Test/JSRuntimeBaseTest.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using Microsoft.JSInterop.Internal; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using Xunit; 9 | 10 | namespace Microsoft.JSInterop.Test 11 | { 12 | public class JSRuntimeBaseTest 13 | { 14 | [Fact] 15 | public void DispatchesAsyncCallsWithDistinctAsyncHandles() 16 | { 17 | // Arrange 18 | var runtime = new TestJSRuntime(); 19 | 20 | // Act 21 | runtime.InvokeAsync("test identifier 1", "arg1", 123, true ); 22 | runtime.InvokeAsync("test identifier 2", "some other arg"); 23 | 24 | // Assert 25 | Assert.Collection(runtime.BeginInvokeCalls, 26 | call => 27 | { 28 | Assert.Equal("test identifier 1", call.Identifier); 29 | Assert.Equal("[\"arg1\",123,true]", call.ArgsJson); 30 | }, 31 | call => 32 | { 33 | Assert.Equal("test identifier 2", call.Identifier); 34 | Assert.Equal("[\"some other arg\"]", call.ArgsJson); 35 | Assert.NotEqual(runtime.BeginInvokeCalls[0].AsyncHandle, call.AsyncHandle); 36 | }); 37 | } 38 | 39 | [Fact] 40 | public void CanCompleteAsyncCallsAsSuccess() 41 | { 42 | // Arrange 43 | var runtime = new TestJSRuntime(); 44 | 45 | // Act/Assert: Tasks not initially completed 46 | var unrelatedTask = runtime.InvokeAsync("unrelated call", Array.Empty()); 47 | var task = runtime.InvokeAsync("test identifier", Array.Empty()); 48 | Assert.False(unrelatedTask.IsCompleted); 49 | Assert.False(task.IsCompleted); 50 | 51 | // Act/Assert: Task can be completed 52 | runtime.OnEndInvoke( 53 | runtime.BeginInvokeCalls[1].AsyncHandle, 54 | /* succeeded: */ true, 55 | "my result"); 56 | Assert.False(unrelatedTask.IsCompleted); 57 | Assert.True(task.IsCompleted); 58 | Assert.Equal("my result", task.Result); 59 | } 60 | 61 | [Fact] 62 | public void CanCompleteAsyncCallsAsFailure() 63 | { 64 | // Arrange 65 | var runtime = new TestJSRuntime(); 66 | 67 | // Act/Assert: Tasks not initially completed 68 | var unrelatedTask = runtime.InvokeAsync("unrelated call", Array.Empty()); 69 | var task = runtime.InvokeAsync("test identifier", Array.Empty()); 70 | Assert.False(unrelatedTask.IsCompleted); 71 | Assert.False(task.IsCompleted); 72 | 73 | // Act/Assert: Task can be failed 74 | runtime.OnEndInvoke( 75 | runtime.BeginInvokeCalls[1].AsyncHandle, 76 | /* succeeded: */ false, 77 | "This is a test exception"); 78 | Assert.False(unrelatedTask.IsCompleted); 79 | Assert.True(task.IsCompleted); 80 | 81 | Assert.IsType(task.Exception); 82 | Assert.IsType(task.Exception.InnerException); 83 | Assert.Equal("This is a test exception", ((JSException)task.Exception.InnerException).Message); 84 | } 85 | 86 | [Fact] 87 | public void CannotCompleteSameAsyncCallMoreThanOnce() 88 | { 89 | // Arrange 90 | var runtime = new TestJSRuntime(); 91 | 92 | // Act/Assert 93 | runtime.InvokeAsync("test identifier", Array.Empty()); 94 | var asyncHandle = runtime.BeginInvokeCalls[0].AsyncHandle; 95 | runtime.OnEndInvoke(asyncHandle, true, null); 96 | var ex = Assert.Throws(() => 97 | { 98 | // Second "end invoke" will fail 99 | runtime.OnEndInvoke(asyncHandle, true, null); 100 | }); 101 | Assert.Equal($"There is no pending task with handle '{asyncHandle}'.", ex.Message); 102 | } 103 | 104 | [Fact] 105 | public void SerializesDotNetObjectWrappersInKnownFormat() 106 | { 107 | // Arrange 108 | var runtime = new TestJSRuntime(); 109 | var obj1 = new object(); 110 | var obj2 = new object(); 111 | var obj3 = new object(); 112 | 113 | // Act 114 | // Showing we can pass the DotNetObject either as top-level args or nested 115 | var obj1Ref = new DotNetObjectRef(obj1); 116 | var obj1DifferentRef = new DotNetObjectRef(obj1); 117 | runtime.InvokeAsync("test identifier", 118 | obj1Ref, 119 | new Dictionary 120 | { 121 | { "obj2", new DotNetObjectRef(obj2) }, 122 | { "obj3", new DotNetObjectRef(obj3) }, 123 | { "obj1SameRef", obj1Ref }, 124 | { "obj1DifferentRef", obj1DifferentRef }, 125 | }); 126 | 127 | // Assert: Serialized as expected 128 | var call = runtime.BeginInvokeCalls.Single(); 129 | Assert.Equal("test identifier", call.Identifier); 130 | Assert.Equal("[\"__dotNetObject:1\",{\"obj2\":\"__dotNetObject:2\",\"obj3\":\"__dotNetObject:3\",\"obj1SameRef\":\"__dotNetObject:1\",\"obj1DifferentRef\":\"__dotNetObject:4\"}]", call.ArgsJson); 131 | 132 | // Assert: Objects were tracked 133 | Assert.Same(obj1, runtime.ArgSerializerStrategy.FindDotNetObject(1)); 134 | Assert.Same(obj2, runtime.ArgSerializerStrategy.FindDotNetObject(2)); 135 | Assert.Same(obj3, runtime.ArgSerializerStrategy.FindDotNetObject(3)); 136 | Assert.Same(obj1, runtime.ArgSerializerStrategy.FindDotNetObject(4)); 137 | } 138 | 139 | [Fact] 140 | public void SupportsCustomSerializationForArguments() 141 | { 142 | // Arrange 143 | var runtime = new TestJSRuntime(); 144 | 145 | // Arrange/Act 146 | runtime.InvokeAsync("test identifier", 147 | new WithCustomArgSerializer()); 148 | 149 | // Asssert 150 | var call = runtime.BeginInvokeCalls.Single(); 151 | Assert.Equal("[{\"key1\":\"value1\",\"key2\":123}]", call.ArgsJson); 152 | } 153 | 154 | class TestJSRuntime : JSRuntimeBase 155 | { 156 | public List BeginInvokeCalls = new List(); 157 | 158 | public class BeginInvokeAsyncArgs 159 | { 160 | public long AsyncHandle { get; set; } 161 | public string Identifier { get; set; } 162 | public string ArgsJson { get; set; } 163 | } 164 | 165 | protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson) 166 | { 167 | BeginInvokeCalls.Add(new BeginInvokeAsyncArgs 168 | { 169 | AsyncHandle = asyncHandle, 170 | Identifier = identifier, 171 | ArgsJson = argsJson, 172 | }); 173 | } 174 | 175 | public void OnEndInvoke(long asyncHandle, bool succeeded, object resultOrException) 176 | => EndInvokeJS(asyncHandle, succeeded, resultOrException); 177 | } 178 | 179 | class WithCustomArgSerializer : ICustomArgSerializer 180 | { 181 | public object ToJsonPrimitive() 182 | { 183 | return new Dictionary 184 | { 185 | { "key1", "value1" }, 186 | { "key2", 123 }, 187 | }; 188 | } 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /test/Microsoft.JSInterop.Test/JSRuntimeTest.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using Xunit; 8 | 9 | namespace Microsoft.JSInterop.Test 10 | { 11 | public class JSRuntimeTest 12 | { 13 | [Fact] 14 | public async Task CanHaveDistinctJSRuntimeInstancesInEachAsyncContext() 15 | { 16 | var tasks = Enumerable.Range(0, 20).Select(async _ => 17 | { 18 | var jsRuntime = new FakeJSRuntime(); 19 | JSRuntime.SetCurrentJSRuntime(jsRuntime); 20 | await Task.Delay(50).ConfigureAwait(false); 21 | Assert.Same(jsRuntime, JSRuntime.Current); 22 | }); 23 | 24 | await Task.WhenAll(tasks); 25 | Assert.Null(JSRuntime.Current); 26 | } 27 | 28 | private class FakeJSRuntime : IJSRuntime 29 | { 30 | public Task InvokeAsync(string identifier, params object[] args) 31 | => throw new NotImplementedException(); 32 | 33 | public void UntrackObjectRef(DotNetObjectRef dotNetObjectRef) 34 | => throw new NotImplementedException(); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/Microsoft.JSInterop.Test/JsonUtilTest.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using Microsoft.JSInterop.Internal; 5 | using System; 6 | using System.Collections.Generic; 7 | using Xunit; 8 | 9 | namespace Microsoft.JSInterop.Test 10 | { 11 | public class JsonUtilTest 12 | { 13 | // It's not useful to have a complete set of behavior specifications for 14 | // what the JSON serializer/deserializer does in all cases here. We merely 15 | // expose a simple wrapper over a third-party library that maintains its 16 | // own specs and tests. 17 | // 18 | // We should only add tests here to cover behaviors that Blazor itself 19 | // depends on. 20 | 21 | [Theory] 22 | [InlineData(null, "null")] 23 | [InlineData("My string", "\"My string\"")] 24 | [InlineData(123, "123")] 25 | [InlineData(123.456f, "123.456")] 26 | [InlineData(123.456d, "123.456")] 27 | [InlineData(true, "true")] 28 | public void CanSerializePrimitivesToJson(object value, string expectedJson) 29 | { 30 | Assert.Equal(expectedJson, Json.Serialize(value)); 31 | } 32 | 33 | [Theory] 34 | [InlineData("null", null)] 35 | [InlineData("\"My string\"", "My string")] 36 | [InlineData("123", 123L)] // Would also accept 123 as a System.Int32, but Int64 is fine as a default 37 | [InlineData("123.456", 123.456d)] 38 | [InlineData("true", true)] 39 | public void CanDeserializePrimitivesFromJson(string json, object expectedValue) 40 | { 41 | Assert.Equal(expectedValue, Json.Deserialize(json)); 42 | } 43 | 44 | [Fact] 45 | public void CanSerializeClassToJson() 46 | { 47 | // Arrange 48 | var person = new Person 49 | { 50 | Id = 1844, 51 | Name = "Athos", 52 | Pets = new[] { "Aramis", "Porthos", "D'Artagnan" }, 53 | Hobby = Hobbies.Swordfighting, 54 | SecondaryHobby = Hobbies.Reading, 55 | Nicknames = new List { "Comte de la Fère", "Armand" }, 56 | BirthInstant = new DateTimeOffset(1825, 8, 6, 18, 45, 21, TimeSpan.FromHours(-6)), 57 | Age = new TimeSpan(7665, 1, 30, 0), 58 | Allergies = new Dictionary { { "Ducks", true }, { "Geese", false } }, 59 | }; 60 | 61 | // Act/Assert 62 | Assert.Equal( 63 | "{\"id\":1844,\"name\":\"Athos\",\"pets\":[\"Aramis\",\"Porthos\",\"D'Artagnan\"],\"hobby\":2,\"secondaryHobby\":1,\"nullHobby\":null,\"nicknames\":[\"Comte de la Fère\",\"Armand\"],\"birthInstant\":\"1825-08-06T18:45:21.0000000-06:00\",\"age\":\"7665.01:30:00\",\"allergies\":{\"Ducks\":true,\"Geese\":false}}", 64 | Json.Serialize(person)); 65 | } 66 | 67 | [Fact] 68 | public void CanDeserializeClassFromJson() 69 | { 70 | // Arrange 71 | var json = "{\"id\":1844,\"name\":\"Athos\",\"pets\":[\"Aramis\",\"Porthos\",\"D'Artagnan\"],\"hobby\":2,\"secondaryHobby\":1,\"nullHobby\":null,\"nicknames\":[\"Comte de la Fère\",\"Armand\"],\"birthInstant\":\"1825-08-06T18:45:21.0000000-06:00\",\"age\":\"7665.01:30:00\",\"allergies\":{\"Ducks\":true,\"Geese\":false}}"; 72 | 73 | // Act 74 | var person = Json.Deserialize(json); 75 | 76 | // Assert 77 | Assert.Equal(1844, person.Id); 78 | Assert.Equal("Athos", person.Name); 79 | Assert.Equal(new[] { "Aramis", "Porthos", "D'Artagnan" }, person.Pets); 80 | Assert.Equal(Hobbies.Swordfighting, person.Hobby); 81 | Assert.Equal(Hobbies.Reading, person.SecondaryHobby); 82 | Assert.Null(person.NullHobby); 83 | Assert.Equal(new[] { "Comte de la Fère", "Armand" }, person.Nicknames); 84 | Assert.Equal(new DateTimeOffset(1825, 8, 6, 18, 45, 21, TimeSpan.FromHours(-6)), person.BirthInstant); 85 | Assert.Equal(new TimeSpan(7665, 1, 30, 0), person.Age); 86 | Assert.Equal(new Dictionary { { "Ducks", true }, { "Geese", false } }, person.Allergies); 87 | } 88 | 89 | [Fact] 90 | public void CanDeserializeWithCaseInsensitiveKeys() 91 | { 92 | // Arrange 93 | var json = "{\"ID\":1844,\"NamE\":\"Athos\"}"; 94 | 95 | // Act 96 | var person = Json.Deserialize(json); 97 | 98 | // Assert 99 | Assert.Equal(1844, person.Id); 100 | Assert.Equal("Athos", person.Name); 101 | } 102 | 103 | [Fact] 104 | public void DeserializationPrefersPropertiesOverFields() 105 | { 106 | // Arrange 107 | var json = "{\"member1\":\"Hello\"}"; 108 | 109 | // Act 110 | var person = Json.Deserialize(json); 111 | 112 | // Assert 113 | Assert.Equal("Hello", person.Member1); 114 | Assert.Null(person.member1); 115 | } 116 | 117 | [Fact] 118 | public void CanSerializeStructToJson() 119 | { 120 | // Arrange 121 | var commandResult = new SimpleStruct 122 | { 123 | StringProperty = "Test", 124 | BoolProperty = true, 125 | NullableIntProperty = 1 126 | }; 127 | 128 | // Act 129 | var result = Json.Serialize(commandResult); 130 | 131 | // Assert 132 | Assert.Equal("{\"stringProperty\":\"Test\",\"boolProperty\":true,\"nullableIntProperty\":1}", result); 133 | } 134 | 135 | [Fact] 136 | public void CanDeserializeStructFromJson() 137 | { 138 | // Arrange 139 | var json = "{\"stringProperty\":\"Test\",\"boolProperty\":true,\"nullableIntProperty\":1}"; 140 | 141 | //Act 142 | var simpleError = Json.Deserialize(json); 143 | 144 | // Assert 145 | Assert.Equal("Test", simpleError.StringProperty); 146 | Assert.True(simpleError.BoolProperty); 147 | Assert.Equal(1, simpleError.NullableIntProperty); 148 | } 149 | 150 | [Fact] 151 | public void CanCreateInstanceOfClassWithPrivateConstructor() 152 | { 153 | // Arrange 154 | var expectedName = "NameValue"; 155 | var json = $"{{\"Name\":\"{expectedName}\"}}"; 156 | 157 | // Act 158 | var instance = Json.Deserialize(json); 159 | 160 | // Assert 161 | Assert.Equal(expectedName, instance.Name); 162 | } 163 | 164 | [Fact] 165 | public void CanSetValueOfPublicPropertiesWithNonPublicSetters() 166 | { 167 | // Arrange 168 | var expectedPrivateValue = "PrivateValue"; 169 | var expectedProtectedValue = "ProtectedValue"; 170 | var expectedInternalValue = "InternalValue"; 171 | 172 | var json = "{" + 173 | $"\"PrivateSetter\":\"{expectedPrivateValue}\"," + 174 | $"\"ProtectedSetter\":\"{expectedProtectedValue}\"," + 175 | $"\"InternalSetter\":\"{expectedInternalValue}\"," + 176 | "}"; 177 | 178 | // Act 179 | var instance = Json.Deserialize(json); 180 | 181 | // Assert 182 | Assert.Equal(expectedPrivateValue, instance.PrivateSetter); 183 | Assert.Equal(expectedProtectedValue, instance.ProtectedSetter); 184 | Assert.Equal(expectedInternalValue, instance.InternalSetter); 185 | } 186 | 187 | [Fact] 188 | public void RejectsTypesWithAmbiguouslyNamedProperties() 189 | { 190 | var ex = Assert.Throws(() => 191 | { 192 | Json.Deserialize("{}"); 193 | }); 194 | 195 | Assert.Equal($"The type '{typeof(ClashingProperties).FullName}' contains multiple public properties " + 196 | $"with names case-insensitively matching '{nameof(ClashingProperties.PROP1).ToLowerInvariant()}'. " + 197 | $"Such types cannot be used for JSON deserialization.", 198 | ex.Message); 199 | } 200 | 201 | [Fact] 202 | public void RejectsTypesWithAmbiguouslyNamedFields() 203 | { 204 | var ex = Assert.Throws(() => 205 | { 206 | Json.Deserialize("{}"); 207 | }); 208 | 209 | Assert.Equal($"The type '{typeof(ClashingFields).FullName}' contains multiple public fields " + 210 | $"with names case-insensitively matching '{nameof(ClashingFields.Field1).ToLowerInvariant()}'. " + 211 | $"Such types cannot be used for JSON deserialization.", 212 | ex.Message); 213 | } 214 | 215 | [Fact] 216 | public void NonEmptyConstructorThrowsUsefulException() 217 | { 218 | // Arrange 219 | var json = "{\"Property\":1}"; 220 | var type = typeof(NonEmptyConstructorPoco); 221 | 222 | // Act 223 | var exception = Assert.Throws(() => 224 | { 225 | Json.Deserialize(json); 226 | }); 227 | 228 | // Assert 229 | Assert.Equal( 230 | $"Cannot deserialize JSON into type '{type.FullName}' because it does not have a public parameterless constructor.", 231 | exception.Message); 232 | } 233 | 234 | // Test cases based on https://github.com/JamesNK/Newtonsoft.Json/blob/122afba9908832bd5ac207164ee6c303bfd65cf1/Src/Newtonsoft.Json.Tests/Utilities/StringUtilsTests.cs#L41 235 | // The only difference is that our logic doesn't have to handle space-separated words, 236 | // because we're only use this for camelcasing .NET member names 237 | // 238 | // Not all of the following cases are really valid .NET member names, but we have no reason 239 | // to implement more logic to detect invalid member names besides the basics (null or empty). 240 | [Theory] 241 | [InlineData("URLValue", "urlValue")] 242 | [InlineData("URL", "url")] 243 | [InlineData("ID", "id")] 244 | [InlineData("I", "i")] 245 | [InlineData("Person", "person")] 246 | [InlineData("xPhone", "xPhone")] 247 | [InlineData("XPhone", "xPhone")] 248 | [InlineData("X_Phone", "x_Phone")] 249 | [InlineData("X__Phone", "x__Phone")] 250 | [InlineData("IsCIA", "isCIA")] 251 | [InlineData("VmQ", "vmQ")] 252 | [InlineData("Xml2Json", "xml2Json")] 253 | [InlineData("SnAkEcAsE", "snAkEcAsE")] 254 | [InlineData("SnA__kEcAsE", "snA__kEcAsE")] 255 | [InlineData("already_snake_case_", "already_snake_case_")] 256 | [InlineData("IsJSONProperty", "isJSONProperty")] 257 | [InlineData("SHOUTING_CASE", "shoutinG_CASE")] 258 | [InlineData("9999-12-31T23:59:59.9999999Z", "9999-12-31T23:59:59.9999999Z")] 259 | [InlineData("Hi!! This is text. Time to test.", "hi!! This is text. Time to test.")] 260 | [InlineData("BUILDING", "building")] 261 | [InlineData("BUILDINGProperty", "buildingProperty")] 262 | public void MemberNameToCamelCase_Valid(string input, string expectedOutput) 263 | { 264 | Assert.Equal(expectedOutput, CamelCase.MemberNameToCamelCase(input)); 265 | } 266 | 267 | [Theory] 268 | [InlineData("")] 269 | [InlineData(null)] 270 | public void MemberNameToCamelCase_Invalid(string input) 271 | { 272 | var ex = Assert.Throws(() => 273 | CamelCase.MemberNameToCamelCase(input)); 274 | Assert.Equal("value", ex.ParamName); 275 | Assert.StartsWith($"The value '{input ?? "null"}' is not a valid member name.", ex.Message); 276 | } 277 | 278 | class NonEmptyConstructorPoco 279 | { 280 | public NonEmptyConstructorPoco(int parameter) {} 281 | 282 | public int Property { get; set; } 283 | } 284 | 285 | struct SimpleStruct 286 | { 287 | public string StringProperty { get; set; } 288 | public bool BoolProperty { get; set; } 289 | public int? NullableIntProperty { get; set; } 290 | } 291 | 292 | class Person 293 | { 294 | public int Id { get; set; } 295 | public string Name { get; set; } 296 | public string[] Pets { get; set; } 297 | public Hobbies Hobby { get; set; } 298 | public Hobbies? SecondaryHobby { get; set; } 299 | public Hobbies? NullHobby { get; set; } 300 | public IList Nicknames { get; set; } 301 | public DateTimeOffset BirthInstant { get; set; } 302 | public TimeSpan Age { get; set; } 303 | public IDictionary Allergies { get; set; } 304 | } 305 | 306 | enum Hobbies { Reading = 1, Swordfighting = 2 } 307 | 308 | #pragma warning disable 0649 309 | class ClashingProperties 310 | { 311 | public string Prop1 { get; set; } 312 | public int PROP1 { get; set; } 313 | } 314 | 315 | class ClashingFields 316 | { 317 | public string Field1; 318 | public int field1; 319 | } 320 | 321 | class PrefersPropertiesOverFields 322 | { 323 | public string member1; 324 | public string Member1 { get; set; } 325 | } 326 | #pragma warning restore 0649 327 | 328 | class PrivateConstructor 329 | { 330 | public string Name { get; set; } 331 | 332 | private PrivateConstructor() 333 | { 334 | } 335 | 336 | public PrivateConstructor(string name) 337 | { 338 | Name = name; 339 | } 340 | } 341 | 342 | class NonPublicSetterOnPublicProperty 343 | { 344 | public string PrivateSetter { get; private set; } 345 | public string ProtectedSetter { get; protected set; } 346 | public string InternalSetter { get; internal set; } 347 | } 348 | } 349 | } 350 | -------------------------------------------------------------------------------- /test/Microsoft.JSInterop.Test/Microsoft.JSInterop.Test.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp2.1 5 | false 6 | 7.3 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | --------------------------------------------------------------------------------