Consume jsinterop from submodule (#1351)

* Remove JSInterop files from this repo

* Add jsinterop submodule

* In Blazor.sln, reference jsinterop projects from submodule

* Update other references to jsinterop

* Fix TypeScript warning

* Include submodules in test/pack

* Update to newer jsinterop to fix JS pack issue

* Update to newer jsinterop to obtain strong naming

* Allow jsinterop submodule to inherit Directory.Build.props/targets

* Get latest jsinterop

* For AppVeyor builds, restore git submodules (happens automatically elsewhere)

* Update README.md with instructions to restore submodules
This commit is contained in:
Steve Sanderson 2018-08-29 11:10:35 +01:00 committed by GitHub
parent 520d47316f
commit cf59ed94ad
47 changed files with 85 additions and 4961 deletions

View File

@ -1,6 +1,7 @@
init:
- git config --global core.autocrlf true
- git config --global core.autocrlf true
install:
- git submodule update --init --recursive
- ps: Install-Product node 8 x64
branches:
only:

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "modules/jsinterop"]
path = modules/jsinterop
url = https://github.com/dotnet/jsinterop.git

View File

@ -95,12 +95,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Blazor
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Blazor.Analyzers.Test", "test\Microsoft.AspNetCore.Blazor.Analyzers.Test\Microsoft.AspNetCore.Blazor.Analyzers.Test.csproj", "{CF3B5990-7A05-4993-AACA-D2C8D7AFF6E6}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.JSInterop", "src\Microsoft.JSInterop\Microsoft.JSInterop.csproj", "{C866B19D-AFFF-45B7-8DAB-71805F39D516}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.JSInterop.Test", "test\Microsoft.JSInterop.Test\Microsoft.JSInterop.Test.csproj", "{BA1CE1FD-89D8-423F-A21B-6B212674EB39}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mono.WebAssembly.Interop", "src\Mono.WebAssembly.Interop\Mono.WebAssembly.Interop.csproj", "{C56873E6-8F49-476E-AF51-B5D187832CF5}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspnetCore.Blazor.Server.Test", "test\Microsoft.AspnetCore.Blazor.Server.Test\Microsoft.AspnetCore.Blazor.Server.Test.csproj", "{142AA6BC-5110-486B-A34D-6878E5E2CE95}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ServerSideBlazor", "ServerSideBlazor", "{3173A9C0-4F66-4064-83EC-3C206F1430FB}"
@ -119,6 +113,18 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Blazor
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Blazor.TagHelperWorkaround", "src\Microsoft.AspNetCore.Blazor.TagHelperWorkaround\Microsoft.AspNetCore.Blazor.TagHelperWorkaround.csproj", "{F71D78AB-A07E-415D-BF3F-1B9991628214}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mono.WebAssembly.Interop", "modules\jsinterop\src\Mono.WebAssembly.Interop\Mono.WebAssembly.Interop.csproj", "{37856984-9702-4062-B8B7-91A38AD8F2BF}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.JSInterop", "modules\jsinterop\src\Microsoft.JSInterop\Microsoft.JSInterop.csproj", "{5F992F3C-4980-4E8E-BEE0-6EC08E369A57}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.JSInterop.JS", "modules\jsinterop\src\Microsoft.JSInterop.JS\Microsoft.JSInterop.JS.csproj", "{4A5D7F9D-9CED-44C1-983E-054D8E99A292}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "jsinterop", "jsinterop", "{1386F99B-3862-40C2-B24D-796C07DC7921}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "modules", "modules", "{F380B6B6-9486-42BC-9B24-C388F8BF13A3}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.JSInterop.Test", "modules\jsinterop\test\Microsoft.JSInterop.Test\Microsoft.JSInterop.Test.csproj", "{ECF02708-4CA4-44B3-B23F-274F4B417FA5}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -362,30 +368,6 @@ Global
{CF3B5990-7A05-4993-AACA-D2C8D7AFF6E6}.Release|Any CPU.Build.0 = Release|Any CPU
{CF3B5990-7A05-4993-AACA-D2C8D7AFF6E6}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
{CF3B5990-7A05-4993-AACA-D2C8D7AFF6E6}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
{C866B19D-AFFF-45B7-8DAB-71805F39D516}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C866B19D-AFFF-45B7-8DAB-71805F39D516}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C866B19D-AFFF-45B7-8DAB-71805F39D516}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU
{C866B19D-AFFF-45B7-8DAB-71805F39D516}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU
{C866B19D-AFFF-45B7-8DAB-71805F39D516}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C866B19D-AFFF-45B7-8DAB-71805F39D516}.Release|Any CPU.Build.0 = Release|Any CPU
{C866B19D-AFFF-45B7-8DAB-71805F39D516}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
{C866B19D-AFFF-45B7-8DAB-71805F39D516}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
{BA1CE1FD-89D8-423F-A21B-6B212674EB39}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BA1CE1FD-89D8-423F-A21B-6B212674EB39}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BA1CE1FD-89D8-423F-A21B-6B212674EB39}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU
{BA1CE1FD-89D8-423F-A21B-6B212674EB39}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU
{BA1CE1FD-89D8-423F-A21B-6B212674EB39}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BA1CE1FD-89D8-423F-A21B-6B212674EB39}.Release|Any CPU.Build.0 = Release|Any CPU
{BA1CE1FD-89D8-423F-A21B-6B212674EB39}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
{BA1CE1FD-89D8-423F-A21B-6B212674EB39}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
{C56873E6-8F49-476E-AF51-B5D187832CF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C56873E6-8F49-476E-AF51-B5D187832CF5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C56873E6-8F49-476E-AF51-B5D187832CF5}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU
{C56873E6-8F49-476E-AF51-B5D187832CF5}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU
{C56873E6-8F49-476E-AF51-B5D187832CF5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C56873E6-8F49-476E-AF51-B5D187832CF5}.Release|Any CPU.Build.0 = Release|Any CPU
{C56873E6-8F49-476E-AF51-B5D187832CF5}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
{C56873E6-8F49-476E-AF51-B5D187832CF5}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
{142AA6BC-5110-486B-A34D-6878E5E2CE95}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{142AA6BC-5110-486B-A34D-6878E5E2CE95}.Debug|Any CPU.Build.0 = Debug|Any CPU
{142AA6BC-5110-486B-A34D-6878E5E2CE95}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU
@ -442,6 +424,38 @@ Global
{F71D78AB-A07E-415D-BF3F-1B9991628214}.Release|Any CPU.Build.0 = Release|Any CPU
{F71D78AB-A07E-415D-BF3F-1B9991628214}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
{F71D78AB-A07E-415D-BF3F-1B9991628214}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
{37856984-9702-4062-B8B7-91A38AD8F2BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{37856984-9702-4062-B8B7-91A38AD8F2BF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{37856984-9702-4062-B8B7-91A38AD8F2BF}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU
{37856984-9702-4062-B8B7-91A38AD8F2BF}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU
{37856984-9702-4062-B8B7-91A38AD8F2BF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{37856984-9702-4062-B8B7-91A38AD8F2BF}.Release|Any CPU.Build.0 = Release|Any CPU
{37856984-9702-4062-B8B7-91A38AD8F2BF}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
{37856984-9702-4062-B8B7-91A38AD8F2BF}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
{5F992F3C-4980-4E8E-BEE0-6EC08E369A57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5F992F3C-4980-4E8E-BEE0-6EC08E369A57}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5F992F3C-4980-4E8E-BEE0-6EC08E369A57}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU
{5F992F3C-4980-4E8E-BEE0-6EC08E369A57}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU
{5F992F3C-4980-4E8E-BEE0-6EC08E369A57}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5F992F3C-4980-4E8E-BEE0-6EC08E369A57}.Release|Any CPU.Build.0 = Release|Any CPU
{5F992F3C-4980-4E8E-BEE0-6EC08E369A57}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
{5F992F3C-4980-4E8E-BEE0-6EC08E369A57}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
{4A5D7F9D-9CED-44C1-983E-054D8E99A292}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4A5D7F9D-9CED-44C1-983E-054D8E99A292}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4A5D7F9D-9CED-44C1-983E-054D8E99A292}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU
{4A5D7F9D-9CED-44C1-983E-054D8E99A292}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU
{4A5D7F9D-9CED-44C1-983E-054D8E99A292}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4A5D7F9D-9CED-44C1-983E-054D8E99A292}.Release|Any CPU.Build.0 = Release|Any CPU
{4A5D7F9D-9CED-44C1-983E-054D8E99A292}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
{4A5D7F9D-9CED-44C1-983E-054D8E99A292}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
{ECF02708-4CA4-44B3-B23F-274F4B417FA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ECF02708-4CA4-44B3-B23F-274F4B417FA5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ECF02708-4CA4-44B3-B23F-274F4B417FA5}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU
{ECF02708-4CA4-44B3-B23F-274F4B417FA5}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU
{ECF02708-4CA4-44B3-B23F-274F4B417FA5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ECF02708-4CA4-44B3-B23F-274F4B417FA5}.Release|Any CPU.Build.0 = Release|Any CPU
{ECF02708-4CA4-44B3-B23F-274F4B417FA5}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
{ECF02708-4CA4-44B3-B23F-274F4B417FA5}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -484,9 +498,6 @@ Global
{3A457B14-D91B-4FFF-A81A-8F350BDB911F} = {E8EBA72C-D555-43AE-BC98-F0B2D05F6A07}
{6DDD6A29-0A3E-417F-976C-5FE3FDA74055} = {B867E038-B3CE-43E3-9292-61568C46CDEB}
{CF3B5990-7A05-4993-AACA-D2C8D7AFF6E6} = {ADA3AE29-F6DE-49F6-8C7C-B321508CAE8E}
{C866B19D-AFFF-45B7-8DAB-71805F39D516} = {B867E038-B3CE-43E3-9292-61568C46CDEB}
{BA1CE1FD-89D8-423F-A21B-6B212674EB39} = {ADA3AE29-F6DE-49F6-8C7C-B321508CAE8E}
{C56873E6-8F49-476E-AF51-B5D187832CF5} = {7B5CAAB1-A3EB-44F7-87E3-A13ED89FC17D}
{142AA6BC-5110-486B-A34D-6878E5E2CE95} = {ADA3AE29-F6DE-49F6-8C7C-B321508CAE8E}
{3173A9C0-4F66-4064-83EC-3C206F1430FB} = {F5FDD4E5-6A52-4A86-BE5E-5E42CB1DC8DA}
{5655AFF9-612C-4947-8221-7DB6949A6CA4} = {3173A9C0-4F66-4064-83EC-3C206F1430FB}
@ -496,6 +507,11 @@ Global
{72004416-E278-4787-B84F-40C7E5668D74} = {6BDD3018-3961-488E-97D3-1FB7320A8AC6}
{CCEC81C4-1A3C-40DC-952F-074712C46180} = {36A7DEB7-5F88-4BFB-B57E-79EEC9950E25}
{F71D78AB-A07E-415D-BF3F-1B9991628214} = {B867E038-B3CE-43E3-9292-61568C46CDEB}
{37856984-9702-4062-B8B7-91A38AD8F2BF} = {1386F99B-3862-40C2-B24D-796C07DC7921}
{5F992F3C-4980-4E8E-BEE0-6EC08E369A57} = {1386F99B-3862-40C2-B24D-796C07DC7921}
{4A5D7F9D-9CED-44C1-983E-054D8E99A292} = {1386F99B-3862-40C2-B24D-796C07DC7921}
{1386F99B-3862-40C2-B24D-796C07DC7921} = {F380B6B6-9486-42BC-9B24-C388F8BF13A3}
{ECF02708-4CA4-44B3-B23F-274F4B417FA5} = {ADA3AE29-F6DE-49F6-8C7C-B321508CAE8E}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {504DA352-6788-4DC0-8705-82167E72A4D3}

View File

@ -34,11 +34,16 @@ To get started with Blazor and build your first Blazor web app check out our [ge
Prerequisites:
- [Node.js](https://nodejs.org/) (>8.3)
- Restore Git submodules by running the following command at the repo root:
git submodule update --init --recursive
The Blazor repository uses the same set of build tools as the other ASP.NET Core projects. The [developer documentation](https://github.com/aspnet/Home/wiki/Building-from-source) for building is the authoritative guide. **Please read this document and check your PATH setup if you have trouble building or using Visual Studio**
To build at the command line, run `build.cmd` or `build.sh` from the solution directory.
If you get a build error similar to *The project file "(some path)\blazor\modules\jsinterop\src\Mono.WebAssembly.Interop\Mono.WebAssembly.Interop.csproj" was not found.*, it's because you didn't yet restore the Git submodules. Please see *Prerequisites* above.
## Run unit tests
Run `build.cmd /t:Test` or `build.sh /t:Test`
@ -62,14 +67,14 @@ Prerequisites:
- Follow the steps [here](https://github.com/aspnet/Home/wiki/Building-from-source) to set up a local copy of dotnet
- Visual Studio 2017 15.7 latest preview - [download](https://www.visualstudio.com/thank-you-downloading-visual-studio/?ch=pre&sku=Enterprise&rel=15)
We recommend getting the latest preview version of Visual Studio and updating it frequently. The Blazor
editing experience in Visual Studio depends on new features of the Razor language tooling and
will be updated frequently.
When installing Visual Studio choose the following workloads:
- ASP.NET and Web Development
- Visual Studio extension development features
Before attempting to open the Blazor repo in Visual Studio, restore Git submodules by running the following command at the repo root:
git submodule update --init --recursive
If you have problems using Visual Studio with `Blazor.sln` please refer to the [developer documentation](https://github.com/aspnet/Home/wiki/Building-from-source).
## Developing the Blazor VS Tooling

View File

@ -21,6 +21,12 @@
<DisablePackageReferenceRestrictions>true</DisablePackageReferenceRestrictions>
</PropertyGroup>
<!-- Submodule support -->
<ItemGroup>
<ProjectsToTest Include="$(RepositoryRoot)modules\*\test\*\*.csproj" />
<ProjectsToPack Include="$(RepositoryRoot)modules\*\src\*\*.csproj" />
</ItemGroup>
<!--
By default, this excludes the end-to-end tests from the repo-level build command.
The CI will script these directly by passing BlazorAllTests=true

View File

@ -0,0 +1,3 @@
<Project>
<Import Project="..\Directory.Build.props" />
</Project>

View File

@ -0,0 +1,3 @@
<Project>
<Import Project="..\Directory.Build.targets" />
</Project>

1
modules/jsinterop Submodule

@ -0,0 +1 @@
Subproject commit bba427d9af709017a44ebdca905916ce1d2c96f7

View File

@ -15,7 +15,7 @@
<ItemGroup>
<!-- Share the InternalCalls.cs source here so we get access to the same interop externs -->
<Compile Include="..\..\src\Mono.WebAssembly.Interop\InternalCalls.cs" />
<Compile Include="..\..\modules\jsinterop\src\Mono.WebAssembly.Interop\InternalCalls.cs" />
</ItemGroup>
</Project>

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
@ -13,7 +13,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="$(AspNetCorePackageVersion)" />
<WebpackInputs Include="**\*.ts" Exclude="node_modules\**" />
<WebpackInputs Include="..\Microsoft.JSInterop\TypeScript\src\**" />
<WebpackInputs Include="..\..\modules\jsinterop\src\Microsoft.JSInterop.JS\src\**" />
</ItemGroup>
<Import Project="..\Microsoft.AspNetCore.Blazor.BuildTools\ReferenceFromSource.props" />

View File

@ -1,4 +1,4 @@
import '../../Microsoft.JSInterop/JavaScriptRuntime/src/Microsoft.JSInterop';
import '../../../modules/jsinterop/src/Microsoft.JSInterop.JS/src/Microsoft.JSInterop';
import './GlobalExports';
import * as Environment from './Environment';
import * as signalR from '@aspnet/signalr';

View File

@ -1,4 +1,4 @@
import '../../Microsoft.JSInterop/JavaScriptRuntime/src/Microsoft.JSInterop';
import '../../../modules/jsinterop/src/Microsoft.JSInterop.JS/src/Microsoft.JSInterop';
import './GlobalExports';
import * as Environment from './Environment';
import { monoPlatform } from './Platform/Mono/MonoPlatform';

View File

@ -185,7 +185,7 @@ function addScriptTagsToDocument() {
const meminitXHR = Module['memoryInitializerRequest'] = new XMLHttpRequest();
meminitXHR.open('GET', `${monoRuntimeUrlBase}/mono.js.mem`);
meminitXHR.responseType = 'arraybuffer';
meminitXHR.send(null);
meminitXHR.send(undefined);
}
const scriptElem = document.createElement('script');
@ -283,7 +283,7 @@ function asyncLoad(url) {
}
};
xhr.onerror = reject;
xhr.send(null);
xhr.send(undefined);
});
}

View File

@ -10,7 +10,7 @@
<ItemGroup>
<ProjectReference Include="..\Microsoft.AspNetCore.Blazor\Microsoft.AspNetCore.Blazor.csproj" />
<ProjectReference Include="..\Mono.WebAssembly.Interop\Mono.WebAssembly.Interop.csproj" />
<ProjectReference Include="..\..\modules\jsinterop\src\Mono.WebAssembly.Interop\Mono.WebAssembly.Interop.csproj" />
</ItemGroup>
</Project>

View File

@ -5,7 +5,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Microsoft.JSInterop\Microsoft.JSInterop.csproj" />
<ProjectReference Include="..\..\modules\jsinterop\src\Microsoft.JSInterop\Microsoft.JSInterop.csproj" />
</ItemGroup>
</Project>

View File

@ -1 +0,0 @@
JavaScriptRuntime/dist/

View File

@ -1,299 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.JSInterop.Internal;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
namespace Microsoft.JSInterop
{
/// <summary>
/// Provides methods that receive incoming calls from JS to .NET.
/// </summary>
public static class DotNetDispatcher
{
private static ConcurrentDictionary<string, IReadOnlyDictionary<string, (MethodInfo, Type[])>> _cachedMethodsByAssembly
= new ConcurrentDictionary<string, IReadOnlyDictionary<string, (MethodInfo, Type[])>>();
/// <summary>
/// Receives a call from JS to .NET, locating and invoking the specified method.
/// </summary>
/// <param name="assemblyName">The assembly containing the method to be invoked.</param>
/// <param name="methodIdentifier">The identifier of the method to be invoked. The method must be annotated with a <see cref="JSInvokableAttribute"/> matching this identifier string.</param>
/// <param name="dotNetObjectId">For instance method calls, identifies the target object.</param>
/// <param name="argsJson">A JSON representation of the parameters.</param>
/// <returns>A JSON representation of the return value, or null.</returns>
public static string Invoke(string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson)
{
// This method doesn't need [JSInvokable] because the platform is responsible for having
// some way to dispatch calls here. The logic inside here is the thing that checks whether
// the targeted method has [JSInvokable]. It is not itself subject to that restriction,
// because there would be nobody to police that. This method *is* the police.
// DotNetDispatcher only works with JSRuntimeBase instances.
var jsRuntime = (JSRuntimeBase)JSRuntime.Current;
var targetInstance = (object)null;
if (dotNetObjectId != default)
{
targetInstance = jsRuntime.ArgSerializerStrategy.FindDotNetObject(dotNetObjectId);
}
var syncResult = InvokeSynchronously(assemblyName, methodIdentifier, targetInstance, argsJson);
return syncResult == null ? null : Json.Serialize(syncResult, jsRuntime.ArgSerializerStrategy);
}
/// <summary>
/// Receives a call from JS to .NET, locating and invoking the specified method asynchronously.
/// </summary>
/// <param name="callId">A value identifying the asynchronous call that should be passed back with the result, or null if no result notification is required.</param>
/// <param name="assemblyName">The assembly containing the method to be invoked.</param>
/// <param name="methodIdentifier">The identifier of the method to be invoked. The method must be annotated with a <see cref="JSInvokableAttribute"/> matching this identifier string.</param>
/// <param name="dotNetObjectId">For instance method calls, identifies the target object.</param>
/// <param name="argsJson">A JSON representation of the parameters.</param>
/// <returns>A JSON representation of the return value, or null.</returns>
public static void BeginInvoke(string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson)
{
// This method doesn't need [JSInvokable] because the platform is responsible for having
// some way to dispatch calls here. The logic inside here is the thing that checks whether
// the targeted method has [JSInvokable]. It is not itself subject to that restriction,
// because there would be nobody to police that. This method *is* the police.
// DotNetDispatcher only works with JSRuntimeBase instances.
// If the developer wants to use a totally custom IJSRuntime, then their JS-side
// code has to implement its own way of returning async results.
var jsRuntimeBaseInstance = (JSRuntimeBase)JSRuntime.Current;
var targetInstance = dotNetObjectId == default
? null
: jsRuntimeBaseInstance.ArgSerializerStrategy.FindDotNetObject(dotNetObjectId);
object syncResult = null;
Exception syncException = null;
try
{
syncResult = InvokeSynchronously(assemblyName, methodIdentifier, targetInstance, argsJson);
}
catch (Exception ex)
{
syncException = ex;
}
// If there was no callId, the caller does not want to be notified about the result
if (callId != null)
{
// Invoke and coerce the result to a Task so the caller can use the same async API
// for both synchronous and asynchronous methods
var task = CoerceToTask(syncResult, syncException);
task.ContinueWith(completedTask =>
{
try
{
var result = TaskGenericsUtil.GetTaskResult(completedTask);
jsRuntimeBaseInstance.EndInvokeDotNet(callId, true, result);
}
catch (Exception ex)
{
ex = UnwrapException(ex);
jsRuntimeBaseInstance.EndInvokeDotNet(callId, false, ex);
}
});
}
}
private static Task CoerceToTask(object syncResult, Exception syncException)
{
if (syncException != null)
{
return Task.FromException(syncException);
}
else if (syncResult is Task syncResultTask)
{
return syncResultTask;
}
else
{
return Task.FromResult(syncResult);
}
}
private static object InvokeSynchronously(string assemblyName, string methodIdentifier, object targetInstance, string argsJson)
{
if (targetInstance != null)
{
if (assemblyName != null)
{
throw new ArgumentException($"For instance method calls, '{nameof(assemblyName)}' should be null. Value received: '{assemblyName}'.");
}
assemblyName = targetInstance.GetType().Assembly.GetName().Name;
}
var (methodInfo, parameterTypes) = GetCachedMethodInfo(assemblyName, methodIdentifier);
// There's no direct way to say we want to deserialize as an array with heterogenous
// entry types (e.g., [string, int, bool]), so we need to deserialize in two phases.
// First we deserialize as object[], for which SimpleJson will supply JsonObject
// instances for nonprimitive values.
var suppliedArgs = (object[])null;
var suppliedArgsLength = 0;
if (argsJson != null)
{
suppliedArgs = Json.Deserialize<SimpleJson.JsonArray>(argsJson).ToArray<object>();
suppliedArgsLength = suppliedArgs.Length;
}
if (suppliedArgsLength != parameterTypes.Length)
{
throw new ArgumentException($"In call to '{methodIdentifier}', expected {parameterTypes.Length} parameters but received {suppliedArgsLength}.");
}
// Second, convert each supplied value to the type expected by the method
var runtime = (JSRuntimeBase)JSRuntime.Current;
var serializerStrategy = runtime.ArgSerializerStrategy;
for (var i = 0; i < suppliedArgsLength; i++)
{
if (parameterTypes[i] == typeof(JSAsyncCallResult))
{
// For JS async call results, we have to defer the deserialization until
// later when we know what type it's meant to be deserialized as
suppliedArgs[i] = new JSAsyncCallResult(suppliedArgs[i]);
}
else
{
suppliedArgs[i] = serializerStrategy.DeserializeObject(
suppliedArgs[i], parameterTypes[i]);
}
}
try
{
return methodInfo.Invoke(targetInstance, suppliedArgs);
}
catch (Exception ex)
{
throw UnwrapException(ex);
}
}
/// <summary>
/// Receives notification that a call from .NET to JS has finished, marking the
/// associated <see cref="Task"/> as completed.
/// </summary>
/// <param name="asyncHandle">The identifier for the function invocation.</param>
/// <param name="succeeded">A flag to indicate whether the invocation succeeded.</param>
/// <param name="result">If <paramref name="succeeded"/> is <c>true</c>, specifies the invocation result. If <paramref name="succeeded"/> is <c>false</c>, gives the <see cref="Exception"/> corresponding to the invocation failure.</param>
[JSInvokable(nameof(DotNetDispatcher) + "." + nameof(EndInvoke))]
public static void EndInvoke(long asyncHandle, bool succeeded, JSAsyncCallResult result)
=> ((JSRuntimeBase)JSRuntime.Current).EndInvokeJS(asyncHandle, succeeded, result.ResultOrException);
/// <summary>
/// Releases the reference to the specified .NET object. This allows the .NET runtime
/// to garbage collect that object if there are no other references to it.
///
/// To avoid leaking memory, the JavaScript side code must call this for every .NET
/// object it obtains a reference to. The exception is if that object is used for
/// the entire lifetime of a given user's session, in which case it is released
/// automatically when the JavaScript runtime is disposed.
/// </summary>
/// <param name="dotNetObjectId">The identifier previously passed to JavaScript code.</param>
[JSInvokable(nameof(DotNetDispatcher) + "." + nameof(ReleaseDotNetObject))]
public static void ReleaseDotNetObject(long dotNetObjectId)
{
// DotNetDispatcher only works with JSRuntimeBase instances.
var jsRuntime = (JSRuntimeBase)JSRuntime.Current;
jsRuntime.ArgSerializerStrategy.ReleaseDotNetObject(dotNetObjectId);
}
private static (MethodInfo, Type[]) GetCachedMethodInfo(string assemblyName, string methodIdentifier)
{
if (string.IsNullOrWhiteSpace(assemblyName))
{
throw new ArgumentException("Cannot be null, empty, or whitespace.", nameof(assemblyName));
}
if (string.IsNullOrWhiteSpace(methodIdentifier))
{
throw new ArgumentException("Cannot be null, empty, or whitespace.", nameof(methodIdentifier));
}
var assemblyMethods = _cachedMethodsByAssembly.GetOrAdd(assemblyName, ScanAssemblyForCallableMethods);
if (assemblyMethods.TryGetValue(methodIdentifier, out var result))
{
return result;
}
else
{
throw new ArgumentException($"The assembly '{assemblyName}' does not contain a public method with [{nameof(JSInvokableAttribute)}(\"{methodIdentifier}\")].");
}
}
private static IReadOnlyDictionary<string, (MethodInfo, Type[])> ScanAssemblyForCallableMethods(string assemblyName)
{
// TODO: Consider looking first for assembly-level attributes (i.e., if there are any,
// only use those) to avoid scanning, especially for framework assemblies.
var result = new Dictionary<string, (MethodInfo, Type[])>();
var invokableMethods = GetRequiredLoadedAssembly(assemblyName)
.GetExportedTypes()
.SelectMany(type => type.GetMethods(
BindingFlags.Public |
BindingFlags.DeclaredOnly |
BindingFlags.Instance |
BindingFlags.Static))
.Where(method => method.IsDefined(typeof(JSInvokableAttribute), inherit: false));
foreach (var method in invokableMethods)
{
var identifier = method.GetCustomAttribute<JSInvokableAttribute>(false).Identifier ?? method.Name;
var parameterTypes = method.GetParameters().Select(p => p.ParameterType).ToArray();
try
{
result.Add(identifier, (method, parameterTypes));
}
catch (ArgumentException)
{
if (result.ContainsKey(identifier))
{
throw new InvalidOperationException($"The assembly '{assemblyName}' contains more than one " +
$"[JSInvokable] method with identifier '{identifier}'. All [JSInvokable] methods within the same " +
$"assembly must have different identifiers. You can pass a custom identifier as a parameter to " +
$"the [JSInvokable] attribute.");
}
else
{
throw;
}
}
}
return result;
}
private static Assembly GetRequiredLoadedAssembly(string assemblyName)
{
// We don't want to load assemblies on demand here, because we don't necessarily trust
// "assemblyName" to be something the developer intended to load. So only pick from the
// set of already-loaded assemblies.
// In some edge cases this might force developers to explicitly call something on the
// target assembly (from .NET) before they can invoke its allowed methods from JS.
var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies();
return loadedAssemblies.FirstOrDefault(a => a.GetName().Name.Equals(assemblyName, StringComparison.Ordinal))
?? throw new ArgumentException($"There is no loaded assembly with the name '{assemblyName}'.");
}
private static Exception UnwrapException(Exception ex)
{
while ((ex is AggregateException || ex is TargetInvocationException) && ex.InnerException != null)
{
ex = ex.InnerException;
}
return ex;
}
}
}

View File

@ -1,66 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading;
namespace Microsoft.JSInterop
{
/// <summary>
/// Wraps a JS interop argument, indicating that the value should not be serialized as JSON
/// but instead should be passed as a reference.
///
/// To avoid leaking memory, the reference must later be disposed by JS code or by .NET code.
/// </summary>
public class DotNetObjectRef : IDisposable
{
/// <summary>
/// Gets the object instance represented by this wrapper.
/// </summary>
public object Value { get; }
// We track an associated IJSRuntime purely so that this class can be IDisposable
// in the normal way. Developers are more likely to use objectRef.Dispose() than
// some less familiar API such as JSRuntime.Current.UntrackObjectRef(objectRef).
private IJSRuntime _attachedToRuntime;
/// <summary>
/// Constructs an instance of <see cref="DotNetObjectRef"/>.
/// </summary>
/// <param name="value">The value being wrapped.</param>
public DotNetObjectRef(object value)
{
Value = value;
}
/// <summary>
/// Ensures the <see cref="DotNetObjectRef"/> is associated with the specified <see cref="IJSRuntime"/>.
/// Developers do not normally need to invoke this manually, since it is called automatically by
/// framework code.
/// </summary>
/// <param name="runtime">The <see cref="IJSRuntime"/>.</param>
public void EnsureAttachedToJsRuntime(IJSRuntime runtime)
{
// The reason we populate _attachedToRuntime here rather than in the constructor
// is to ensure developers can't accidentally try to reuse DotNetObjectRef across
// different IJSRuntime instances. This method gets called as part of serializing
// the DotNetObjectRef during an interop call.
var existingRuntime = Interlocked.CompareExchange(ref _attachedToRuntime, runtime, null);
if (existingRuntime != null && existingRuntime != runtime)
{
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.");
}
}
/// <summary>
/// Stops tracking this object reference, allowing it to be garbage collected
/// (if there are no other references to it). Once the instance is disposed, it
/// can no longer be used in interop calls from JavaScript code.
/// </summary>
public void Dispose()
{
_attachedToRuntime?.UntrackObjectRef(this);
}
}
}

View File

@ -1,22 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.JSInterop.Internal
{
// This is "soft" internal because we're trying to avoid expanding JsonUtil into a sophisticated
// API. Developers who want that would be better served by using a different JSON package
// instead. Also the perf implications of the ICustomArgSerializer approach aren't ideal
// (it forces structs to be boxed, and returning a dictionary means lots more allocations
// and boxing of any value-typed properties).
/// <summary>
/// Internal. Intended for framework use only.
/// </summary>
public interface ICustomArgSerializer
{
/// <summary>
/// Internal. Intended for framework use only.
/// </summary>
object ToJsonPrimitive();
}
}

View File

@ -1,20 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.JSInterop
{
/// <summary>
/// Represents an instance of a JavaScript runtime to which calls may be dispatched.
/// </summary>
public interface IJSInProcessRuntime : IJSRuntime
{
/// <summary>
/// Invokes the specified JavaScript function synchronously.
/// </summary>
/// <typeparam name="T">The JSON-serializable return type.</typeparam>
/// <param name="identifier">An identifier for the function to invoke. For example, the value <code>"someScope.someFunction"</code> will invoke the function <code>window.someScope.someFunction</code>.</param>
/// <param name="args">JSON-serializable arguments.</param>
/// <returns>An instance of <typeparamref name="T"/> obtained by JSON-deserializing the return value.</returns>
T Invoke<T>(string identifier, params object[] args);
}
}

View File

@ -1,32 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading.Tasks;
namespace Microsoft.JSInterop
{
/// <summary>
/// Represents an instance of a JavaScript runtime to which calls may be dispatched.
/// </summary>
public interface IJSRuntime
{
/// <summary>
/// Invokes the specified JavaScript function asynchronously.
/// </summary>
/// <typeparam name="T">The JSON-serializable return type.</typeparam>
/// <param name="identifier">An identifier for the function to invoke. For example, the value <code>"someScope.someFunction"</code> will invoke the function <code>window.someScope.someFunction</code>.</param>
/// <param name="args">JSON-serializable arguments.</param>
/// <returns>An instance of <typeparamref name="T"/> obtained by JSON-deserializing the return value.</returns>
Task<T> InvokeAsync<T>(string identifier, params object[] args);
/// <summary>
/// Stops tracking the .NET object represented by the <see cref="DotNetObjectRef"/>.
/// This allows it to be garbage collected (if nothing else holds a reference to it)
/// and means the JS-side code can no longer invoke methods on the instance or pass
/// it as an argument to subsequent calls.
/// </summary>
/// <param name="dotNetObjectRef">The reference to stop tracking.</param>
/// <remarks>This method is called automaticallly by <see cref="DotNetObjectRef.Dispose"/>.</remarks>
void UntrackObjectRef(DotNetObjectRef dotNetObjectRef);
}
}

View File

@ -1,121 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.JSInterop.Internal;
using SimpleJson;
using System;
using System.Collections.Generic;
namespace Microsoft.JSInterop
{
internal class InteropArgSerializerStrategy : PocoJsonSerializerStrategy
{
private readonly JSRuntimeBase _jsRuntime;
private const string _dotNetObjectPrefix = "__dotNetObject:";
private object _storageLock = new object();
private long _nextId = 1; // Start at 1, because 0 signals "no object"
private Dictionary<long, DotNetObjectRef> _trackedRefsById = new Dictionary<long, DotNetObjectRef>();
private Dictionary<DotNetObjectRef, long> _trackedIdsByRef = new Dictionary<DotNetObjectRef, long>();
public InteropArgSerializerStrategy(JSRuntimeBase jsRuntime)
{
_jsRuntime = jsRuntime ?? throw new ArgumentNullException(nameof(jsRuntime));
}
protected override bool TrySerializeKnownTypes(object input, out object output)
{
switch (input)
{
case DotNetObjectRef marshalByRefValue:
EnsureDotNetObjectTracked(marshalByRefValue, out var id);
// Special value format recognized by the code in Microsoft.JSInterop.js
// If we have to make it more clash-resistant, we can do
output = _dotNetObjectPrefix + id;
return true;
case ICustomArgSerializer customArgSerializer:
output = customArgSerializer.ToJsonPrimitive();
return true;
default:
return base.TrySerializeKnownTypes(input, out output);
}
}
public override object DeserializeObject(object value, Type type)
{
if (value is string valueString)
{
if (valueString.StartsWith(_dotNetObjectPrefix))
{
var dotNetObjectId = long.Parse(valueString.Substring(_dotNetObjectPrefix.Length));
return FindDotNetObject(dotNetObjectId);
}
}
return base.DeserializeObject(value, type);
}
public object FindDotNetObject(long dotNetObjectId)
{
lock (_storageLock)
{
return _trackedRefsById.TryGetValue(dotNetObjectId, out var dotNetObjectRef)
? dotNetObjectRef.Value
: throw new ArgumentException($"There is no tracked object with id '{dotNetObjectId}'. Perhaps the reference was already released.", nameof(dotNetObjectId));
}
}
/// <summary>
/// Stops tracking the specified .NET object reference.
/// This overload is typically invoked from JS code via JS interop.
/// </summary>
/// <param name="dotNetObjectId">The ID of the <see cref="DotNetObjectRef"/>.</param>
public void ReleaseDotNetObject(long dotNetObjectId)
{
lock (_storageLock)
{
if (_trackedRefsById.TryGetValue(dotNetObjectId, out var dotNetObjectRef))
{
_trackedRefsById.Remove(dotNetObjectId);
_trackedIdsByRef.Remove(dotNetObjectRef);
}
}
}
/// <summary>
/// Stops tracking the specified .NET object reference.
/// This overload is typically invoked from .NET code by <see cref="DotNetObjectRef.Dispose"/>.
/// </summary>
/// <param name="dotNetObjectRef">The <see cref="DotNetObjectRef"/>.</param>
public void ReleaseDotNetObject(DotNetObjectRef dotNetObjectRef)
{
lock (_storageLock)
{
if (_trackedIdsByRef.TryGetValue(dotNetObjectRef, out var dotNetObjectId))
{
_trackedRefsById.Remove(dotNetObjectId);
_trackedIdsByRef.Remove(dotNetObjectRef);
}
}
}
private void EnsureDotNetObjectTracked(DotNetObjectRef dotNetObjectRef, out long dotNetObjectId)
{
dotNetObjectRef.EnsureAttachedToJsRuntime(_jsRuntime);
lock (_storageLock)
{
// Assign an ID only if it doesn't already have one
if (!_trackedIdsByRef.TryGetValue(dotNetObjectRef, out dotNetObjectId))
{
dotNetObjectId = _nextId++;
_trackedRefsById.Add(dotNetObjectId, dotNetObjectRef);
_trackedIdsByRef.Add(dotNetObjectRef, dotNetObjectId);
}
}
}
}
}

View File

@ -1,36 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.JSInterop.Internal
{
// This type takes care of a special case in handling the result of an async call from
// .NET to JS. The information about what type the result should be exists only on the
// corresponding TaskCompletionSource<T>. We don't have that information at the time
// that we deserialize the incoming argsJson before calling DotNetDispatcher.EndInvoke.
// Declaring the EndInvoke parameter type as JSAsyncCallResult defers the deserialization
// until later when we have access to the TaskCompletionSource<T>.
//
// There's no reason why developers would need anything similar to this in user code,
// because this is the mechanism by which we resolve the incoming argsJson to the correct
// user types before completing calls.
//
// It's marked as 'public' only because it has to be for use as an argument on a
// [JSInvokable] method.
/// <summary>
/// Intended for framework use only.
/// </summary>
public class JSAsyncCallResult
{
internal object ResultOrException { get; }
/// <summary>
/// Constructs an instance of <see cref="JSAsyncCallResult"/>.
/// </summary>
/// <param name="resultOrException">The result of the call.</param>
internal JSAsyncCallResult(object resultOrException)
{
ResultOrException = resultOrException;
}
}
}

View File

@ -1,21 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.JSInterop
{
/// <summary>
/// Represents errors that occur during an interop call from .NET to JavaScript.
/// </summary>
public class JSException : Exception
{
/// <summary>
/// Constructs an instance of <see cref="JSException"/>.
/// </summary>
/// <param name="message">The exception message.</param>
public JSException(string message) : base(message)
{
}
}
}

View File

@ -1,32 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.JSInterop
{
/// <summary>
/// Abstract base class for an in-process JavaScript runtime.
/// </summary>
public abstract class JSInProcessRuntimeBase : JSRuntimeBase, IJSInProcessRuntime
{
/// <summary>
/// Invokes the specified JavaScript function synchronously.
/// </summary>
/// <typeparam name="T">The JSON-serializable return type.</typeparam>
/// <param name="identifier">An identifier for the function to invoke. For example, the value <code>"someScope.someFunction"</code> will invoke the function <code>window.someScope.someFunction</code>.</param>
/// <param name="args">JSON-serializable arguments.</param>
/// <returns>An instance of <typeparamref name="T"/> obtained by JSON-deserializing the return value.</returns>
public T Invoke<T>(string identifier, params object[] args)
{
var resultJson = InvokeJS(identifier, Json.Serialize(args, ArgSerializerStrategy));
return Json.Deserialize<T>(resultJson, ArgSerializerStrategy);
}
/// <summary>
/// Performs a synchronous function invocation.
/// </summary>
/// <param name="identifier">The identifier for the function to invoke.</param>
/// <param name="argsJson">A JSON representation of the arguments.</param>
/// <returns>A JSON representation of the result.</returns>
protected abstract string InvokeJS(string identifier, string argsJson);
}
}

View File

@ -1,48 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.JSInterop
{
/// <summary>
/// Identifies a .NET method as allowing invocation from JavaScript code.
/// Any method marked with this attribute may receive arbitrary parameter values
/// from untrusted callers. All inputs should be validated carefully.
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class JSInvokableAttribute : Attribute
{
/// <summary>
/// Gets the identifier for the method. The identifier must be unique within the scope
/// of an assembly.
///
/// If not set, the identifier is taken from the name of the method. In this case the
/// method name must be unique within the assembly.
/// </summary>
public string Identifier { get; }
/// <summary>
/// Constructs an instance of <see cref="JSInvokableAttribute"/> without setting
/// an identifier for the method.
/// </summary>
public JSInvokableAttribute()
{
}
/// <summary>
/// Constructs an instance of <see cref="JSInvokableAttribute"/> using the specified
/// identifier.
/// </summary>
/// <param name="identifier">An identifier for the method, which must be unique within the scope of the assembly.</param>
public JSInvokableAttribute(string identifier)
{
if (string.IsNullOrEmpty(identifier))
{
throw new ArgumentException("Cannot be null or empty", nameof(identifier));
}
Identifier = identifier;
}
}
}

View File

@ -1,34 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading;
namespace Microsoft.JSInterop
{
/// <summary>
/// Provides mechanisms for accessing the current <see cref="IJSRuntime"/>.
/// </summary>
public static class JSRuntime
{
private static AsyncLocal<IJSRuntime> _currentJSRuntime
= new AsyncLocal<IJSRuntime>();
/// <summary>
/// Gets the current <see cref="IJSRuntime"/>, if any.
/// </summary>
public static IJSRuntime Current => _currentJSRuntime.Value;
/// <summary>
/// Sets the current JS runtime to the supplied instance.
///
/// This is intended for framework use. Developers should not normally need to call this method.
/// </summary>
/// <param name="instance">The new current <see cref="IJSRuntime"/>.</param>
public static void SetCurrentJSRuntime(IJSRuntime instance)
{
_currentJSRuntime.Value = instance
?? throw new ArgumentNullException(nameof(instance));
}
}
}

View File

@ -1,116 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.JSInterop
{
/// <summary>
/// Abstract base class for a JavaScript runtime.
/// </summary>
public abstract class JSRuntimeBase : IJSRuntime
{
private long _nextPendingTaskId = 1; // Start at 1 because zero signals "no response needed"
private readonly ConcurrentDictionary<long, object> _pendingTasks
= new ConcurrentDictionary<long, object>();
internal InteropArgSerializerStrategy ArgSerializerStrategy { get; }
/// <summary>
/// Constructs an instance of <see cref="JSRuntimeBase"/>.
/// </summary>
public JSRuntimeBase()
{
ArgSerializerStrategy = new InteropArgSerializerStrategy(this);
}
/// <inheritdoc />
public void UntrackObjectRef(DotNetObjectRef dotNetObjectRef)
=> ArgSerializerStrategy.ReleaseDotNetObject(dotNetObjectRef);
/// <summary>
/// Invokes the specified JavaScript function asynchronously.
/// </summary>
/// <typeparam name="T">The JSON-serializable return type.</typeparam>
/// <param name="identifier">An identifier for the function to invoke. For example, the value <code>"someScope.someFunction"</code> will invoke the function <code>window.someScope.someFunction</code>.</param>
/// <param name="args">JSON-serializable arguments.</param>
/// <returns>An instance of <typeparamref name="T"/> obtained by JSON-deserializing the return value.</returns>
public Task<T> InvokeAsync<T>(string identifier, params object[] args)
{
// We might consider also adding a default timeout here in case we don't want to
// risk a memory leak in the scenario where the JS-side code is failing to complete
// the operation.
var taskId = Interlocked.Increment(ref _nextPendingTaskId);
var tcs = new TaskCompletionSource<T>();
_pendingTasks[taskId] = tcs;
try
{
var argsJson = args?.Length > 0
? Json.Serialize(args, ArgSerializerStrategy)
: null;
BeginInvokeJS(taskId, identifier, argsJson);
return tcs.Task;
}
catch
{
_pendingTasks.TryRemove(taskId, out _);
throw;
}
}
/// <summary>
/// Begins an asynchronous function invocation.
/// </summary>
/// <param name="asyncHandle">The identifier for the function invocation, or zero if no async callback is required.</param>
/// <param name="identifier">The identifier for the function to invoke.</param>
/// <param name="argsJson">A JSON representation of the arguments.</param>
protected abstract void BeginInvokeJS(long asyncHandle, string identifier, string argsJson);
internal void EndInvokeDotNet(string callId, bool success, object resultOrException)
{
// For failures, the common case is to call EndInvokeDotNet with the Exception object.
// For these we'll serialize as something that's useful to receive on the JS side.
// If the value is not an Exception, we'll just rely on it being directly JSON-serializable.
if (!success && resultOrException is Exception)
{
resultOrException = resultOrException.ToString();
}
// We pass 0 as the async handle because we don't want the JS-side code to
// send back any notification (we're just providing a result for an existing async call)
BeginInvokeJS(0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", Json.Serialize(new[] {
callId,
success,
resultOrException
}, ArgSerializerStrategy));
}
internal void EndInvokeJS(long asyncHandle, bool succeeded, object resultOrException)
{
if (!_pendingTasks.TryRemove(asyncHandle, out var tcs))
{
throw new ArgumentException($"There is no pending task with handle '{asyncHandle}'.");
}
if (succeeded)
{
var resultType = TaskGenericsUtil.GetTaskCompletionSourceResultType(tcs);
if (resultOrException is SimpleJson.JsonObject || resultOrException is SimpleJson.JsonArray)
{
resultOrException = ArgSerializerStrategy.DeserializeObject(resultOrException, resultType);
}
TaskGenericsUtil.SetTaskCompletionSourceResult(tcs, resultOrException);
}
else
{
TaskGenericsUtil.SetTaskCompletionSourceException(tcs, new JSException(resultOrException.ToString()));
}
}
}
}

View File

@ -1,287 +0,0 @@
// This is a single-file self-contained module to avoid the need for a Webpack build
module DotNet {
(window as any).DotNet = DotNet; // Ensure reachable from anywhere
export type JsonReviver = ((key: any, value: any) => any);
const jsonRevivers: JsonReviver[] = [];
const pendingAsyncCalls: { [id: number]: PendingAsyncCall<any> } = {};
const cachedJSFunctions: { [identifier: string]: Function } = {};
let nextAsyncCallId = 1; // Start at 1 because zero signals "no response needed"
let dotNetDispatcher: DotNetCallDispatcher | null = null;
/**
* Sets the specified .NET call dispatcher as the current instance so that it will be used
* for future invocations.
*
* @param dispatcher An object that can dispatch calls from JavaScript to a .NET runtime.
*/
export function attachDispatcher(dispatcher: DotNetCallDispatcher) {
dotNetDispatcher = dispatcher;
}
/**
* Adds a JSON reviver callback that will be used when parsing arguments received from .NET.
* @param reviver The reviver to add.
*/
export function attachReviver(reviver: JsonReviver) {
jsonRevivers.push(reviver);
}
/**
* Invokes the specified .NET public method synchronously. Not all hosting scenarios support
* synchronous invocation, so if possible use invokeMethodAsync instead.
*
* @param assemblyName The short name (without key/version or .dll extension) of the .NET assembly containing the method.
* @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier.
* @param args Arguments to pass to the method, each of which must be JSON-serializable.
* @returns The result of the operation.
*/
export function invokeMethod<T>(assemblyName: string, methodIdentifier: string, ...args: any[]): T {
return invokePossibleInstanceMethod<T>(assemblyName, methodIdentifier, null, args);
}
/**
* Invokes the specified .NET public method asynchronously.
*
* @param assemblyName The short name (without key/version or .dll extension) of the .NET assembly containing the method.
* @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier.
* @param args Arguments to pass to the method, each of which must be JSON-serializable.
* @returns A promise representing the result of the operation.
*/
export function invokeMethodAsync<T>(assemblyName: string, methodIdentifier: string, ...args: any[]): Promise<T> {
return invokePossibleInstanceMethodAsync(assemblyName, methodIdentifier, null, args);
}
function invokePossibleInstanceMethod<T>(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, args: any[]): T {
const dispatcher = getRequiredDispatcher();
if (dispatcher.invokeDotNetFromJS) {
const argsJson = JSON.stringify(args, argReplacer);
const resultJson = dispatcher.invokeDotNetFromJS(assemblyName, methodIdentifier, dotNetObjectId, argsJson);
return resultJson ? parseJsonWithRevivers(resultJson) : null;
} else {
throw new Error('The current dispatcher does not support synchronous calls from JS to .NET. Use invokeMethodAsync instead.');
}
}
function invokePossibleInstanceMethodAsync<T>(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, args: any[]): Promise<T> {
const asyncCallId = nextAsyncCallId++;
const resultPromise = new Promise<T>((resolve, reject) => {
pendingAsyncCalls[asyncCallId] = { resolve, reject };
});
try {
const argsJson = JSON.stringify(args, argReplacer);
getRequiredDispatcher().beginInvokeDotNetFromJS(asyncCallId, assemblyName, methodIdentifier, dotNetObjectId, argsJson);
} catch(ex) {
// Synchronous failure
completePendingCall(asyncCallId, false, ex);
}
return resultPromise;
}
function getRequiredDispatcher(): DotNetCallDispatcher {
if (dotNetDispatcher !== null) {
return dotNetDispatcher;
}
throw new Error('No .NET call dispatcher has been set.');
}
function completePendingCall(asyncCallId: number, success: boolean, resultOrError: any) {
if (!pendingAsyncCalls.hasOwnProperty(asyncCallId)) {
throw new Error(`There is no pending async call with ID ${asyncCallId}.`);
}
const asyncCall = pendingAsyncCalls[asyncCallId];
delete pendingAsyncCalls[asyncCallId];
if (success) {
asyncCall.resolve(resultOrError);
} else {
asyncCall.reject(resultOrError);
}
}
interface PendingAsyncCall<T> {
resolve: (value?: T | PromiseLike<T>) => void;
reject: (reason?: any) => void;
}
/**
* Represents the ability to dispatch calls from JavaScript to a .NET runtime.
*/
export interface DotNetCallDispatcher {
/**
* Optional. If implemented, invoked by the runtime to perform a synchronous call to a .NET method.
*
* @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.
* @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier.
* @param dotNetObjectId If given, the call will be to an instance method on the specified DotNetObject. Pass null or undefined to call static methods.
* @param argsJson JSON representation of arguments to pass to the method.
* @returns JSON representation of the result of the invocation.
*/
invokeDotNetFromJS?(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, argsJson: string): string | null;
/**
* Invoked by the runtime to begin an asynchronous call to a .NET method.
*
* @param callId A value identifying the asynchronous operation. This value should be passed back in a later call from .NET to JS.
* @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.
* @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier.
* @param dotNetObjectId If given, the call will be to an instance method on the specified DotNetObject. Pass null to call static methods.
* @param argsJson JSON representation of arguments to pass to the method.
*/
beginInvokeDotNetFromJS(callId: number, assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, argsJson: string): void;
}
/**
* Receives incoming calls from .NET and dispatches them to JavaScript.
*/
export const jsCallDispatcher = {
/**
* Finds the JavaScript function matching the specified identifier.
*
* @param identifier Identifies the globally-reachable function to be returned.
* @returns A Function instance.
*/
findJSFunction,
/**
* Invokes the specified synchronous JavaScript function.
*
* @param identifier Identifies the globally-reachable function to invoke.
* @param argsJson JSON representation of arguments to be passed to the function.
* @returns JSON representation of the invocation result.
*/
invokeJSFromDotNet: (identifier: string, argsJson: string) => {
const result = findJSFunction(identifier).apply(null, parseJsonWithRevivers(argsJson));
return result === null || result === undefined
? null
: JSON.stringify(result, argReplacer);
},
/**
* Invokes the specified synchronous or asynchronous JavaScript function.
*
* @param asyncHandle A value identifying the asynchronous operation. This value will be passed back in a later call to endInvokeJSFromDotNet.
* @param identifier Identifies the globally-reachable function to invoke.
* @param argsJson JSON representation of arguments to be passed to the function.
*/
beginInvokeJSFromDotNet: (asyncHandle: number, identifier: string, argsJson: string): void => {
// Coerce synchronous functions into async ones, plus treat
// synchronous exceptions the same as async ones
const promise = new Promise<any>(resolve => {
const synchronousResultOrPromise = findJSFunction(identifier).apply(null, parseJsonWithRevivers(argsJson));
resolve(synchronousResultOrPromise);
});
// We only listen for a result if the caller wants to be notified about it
if (asyncHandle) {
// On completion, dispatch result back to .NET
// Not using "await" because it codegens a lot of boilerplate
promise.then(
result => getRequiredDispatcher().beginInvokeDotNetFromJS(0, 'Microsoft.JSInterop', 'DotNetDispatcher.EndInvoke', null, JSON.stringify([asyncHandle, true, result], argReplacer)),
error => getRequiredDispatcher().beginInvokeDotNetFromJS(0, 'Microsoft.JSInterop', 'DotNetDispatcher.EndInvoke', null, JSON.stringify([asyncHandle, false, formatError(error)]))
);
}
},
/**
* Receives notification that an async call from JS to .NET has completed.
* @param asyncCallId The identifier supplied in an earlier call to beginInvokeDotNetFromJS.
* @param success A flag to indicate whether the operation completed successfully.
* @param resultOrExceptionMessage Either the operation result or an error message.
*/
endInvokeDotNetFromJS: (asyncCallId: string, success: boolean, resultOrExceptionMessage: any): void => {
const resultOrError = success ? resultOrExceptionMessage : new Error(resultOrExceptionMessage);
completePendingCall(parseInt(asyncCallId), success, resultOrError);
}
}
function parseJsonWithRevivers(json: string): any {
return json ? JSON.parse(json, (key, initialValue) => {
// Invoke each reviver in order, passing the output from the previous reviver,
// so that each one gets a chance to transform the value
return jsonRevivers.reduce(
(latestValue, reviver) => reviver(key, latestValue),
initialValue
);
}) : null;
}
function formatError(error: any): string {
if (error instanceof Error) {
return `${error.message}\n${error.stack}`;
} else {
return error ? error.toString() : 'null';
}
}
function findJSFunction(identifier: string): Function {
if (cachedJSFunctions.hasOwnProperty(identifier)) {
return cachedJSFunctions[identifier];
}
let result: any = window;
let resultIdentifier = 'window';
identifier.split('.').forEach(segment => {
if (segment in result) {
result = result[segment];
resultIdentifier += '.' + segment;
} else {
throw new Error(`Could not find '${segment}' in '${resultIdentifier}'.`);
}
});
if (result instanceof Function) {
return result;
} else {
throw new Error(`The value '${resultIdentifier}' is not a function.`);
}
}
class DotNetObject {
constructor(private _id: number) {
}
public invokeMethod<T>(methodIdentifier: string, ...args: any[]): T {
return invokePossibleInstanceMethod<T>(null, methodIdentifier, this._id, args);
}
public invokeMethodAsync<T>(methodIdentifier: string, ...args: any[]): Promise<T> {
return invokePossibleInstanceMethodAsync<T>(null, methodIdentifier, this._id, args);
}
public dispose() {
const promise = invokeMethodAsync<any>(
'Microsoft.JSInterop',
'DotNetDispatcher.ReleaseDotNetObject',
this._id);
promise.catch(error => console.error(error));
}
public serializeAsArg() {
return `__dotNetObject:${this._id}`;
}
}
const dotNetObjectValueFormat = /^__dotNetObject\:(\d+)$/;
attachReviver(function reviveDotNetObject(key: any, value: any) {
if (typeof value === 'string') {
const match = value.match(dotNetObjectValueFormat);
if (match) {
return new DotNetObject(parseInt(match[1]));
}
}
// Unrecognized - let another reviver handle it
return value;
});
function argReplacer(key: string, value: any) {
return value instanceof DotNetObject ? value.serializeAsArg() : value;
}
}

View File

@ -1,59 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.JSInterop
{
internal static class CamelCase
{
public static string MemberNameToCamelCase(string value)
{
if (string.IsNullOrEmpty(value))
{
throw new ArgumentException(
$"The value '{value ?? "null"}' is not a valid member name.",
nameof(value));
}
// If we don't need to modify the value, bail out without creating a char array
if (!char.IsUpper(value[0]))
{
return value;
}
// We have to modify at least one character
var chars = value.ToCharArray();
var length = chars.Length;
if (length < 2 || !char.IsUpper(chars[1]))
{
// Only the first character needs to be modified
// Note that this branch is functionally necessary, because the 'else' branch below
// never looks at char[1]. It's always looking at the n+2 character.
chars[0] = char.ToLowerInvariant(chars[0]);
}
else
{
// If chars[0] and chars[1] are both upper, then we'll lowercase the first char plus
// any consecutive uppercase ones, stopping if we find any char that is followed by a
// non-uppercase one
var i = 0;
while (i < length)
{
chars[i] = char.ToLowerInvariant(chars[i]);
i++;
// If the next-plus-one char isn't also uppercase, then we're now on the last uppercase, so stop
if (i < length - 1 && !char.IsUpper(chars[i + 1]))
{
break;
}
}
}
return new string(chars);
}
}
}

View File

@ -1,39 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.JSInterop
{
/// <summary>
/// Provides mechanisms for converting between .NET objects and JSON strings for use
/// when making calls to JavaScript functions via <see cref="IJSRuntime"/>.
///
/// Warning: This is not intended as a general-purpose JSON library. It is only intended
/// for use when making calls via <see cref="IJSRuntime"/>. Eventually its implementation
/// will be replaced by something more general-purpose.
/// </summary>
public static class Json
{
/// <summary>
/// Serializes the value as a JSON string.
/// </summary>
/// <param name="value">The value to serialize.</param>
/// <returns>The JSON string.</returns>
public static string Serialize(object value)
=> SimpleJson.SimpleJson.SerializeObject(value);
internal static string Serialize(object value, SimpleJson.IJsonSerializerStrategy serializerStrategy)
=> SimpleJson.SimpleJson.SerializeObject(value, serializerStrategy);
/// <summary>
/// Deserializes the JSON string, creating an object of the specified generic type.
/// </summary>
/// <typeparam name="T">The type of object to create.</typeparam>
/// <param name="json">The JSON string.</param>
/// <returns>An object of the specified type.</returns>
public static T Deserialize<T>(string json)
=> SimpleJson.SimpleJson.DeserializeObject<T>(json);
internal static T Deserialize<T>(string json, SimpleJson.IJsonSerializerStrategy serializerStrategy)
=> SimpleJson.SimpleJson.DeserializeObject<T>(json, serializerStrategy);
}
}

View File

@ -1,29 +0,0 @@
SimpleJson is from https://github.com/facebook-csharp-sdk/simple-json
CHANGES MADE
============
* Better handling of System.DateTime serialized by Json.NET
as suggested in https://github.com/facebook-csharp-sdk/simple-json/issues/78
LICENSE (from https://github.com/facebook-csharp-sdk/simple-json/blob/08b6871e8f63e866810d25e7a03c48502c9a234b/LICENSE.txt):
=====
Copyright (c) 2011, The Outercurve Foundation
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.TypeScript.MSBuild" Version="2.9.2" />
</ItemGroup>
</Project>

View File

@ -1,3 +0,0 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Microsoft.JSInterop.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]

View File

@ -1,116 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading.Tasks;
namespace Microsoft.JSInterop
{
internal static class TaskGenericsUtil
{
private static ConcurrentDictionary<Type, ITaskResultGetter> _cachedResultGetters
= new ConcurrentDictionary<Type, ITaskResultGetter>();
private static ConcurrentDictionary<Type, ITcsResultSetter> _cachedResultSetters
= new ConcurrentDictionary<Type, ITcsResultSetter>();
public static void SetTaskCompletionSourceResult(object taskCompletionSource, object result)
=> CreateResultSetter(taskCompletionSource).SetResult(taskCompletionSource, result);
public static void SetTaskCompletionSourceException(object taskCompletionSource, Exception exception)
=> CreateResultSetter(taskCompletionSource).SetException(taskCompletionSource, exception);
public static Type GetTaskCompletionSourceResultType(object taskCompletionSource)
=> CreateResultSetter(taskCompletionSource).ResultType;
public static object GetTaskResult(Task task)
{
var getter = _cachedResultGetters.GetOrAdd(task.GetType(), taskInstanceType =>
{
var resultType = GetTaskResultType(taskInstanceType);
return resultType == null
? new VoidTaskResultGetter()
: (ITaskResultGetter)Activator.CreateInstance(
typeof(TaskResultGetter<>).MakeGenericType(resultType));
});
return getter.GetResult(task);
}
private static Type GetTaskResultType(Type taskType)
{
// It might be something derived from Task or Task<T>, so we have to scan
// up the inheritance hierarchy to find the Task or Task<T>
while (taskType != typeof(Task) &&
(!taskType.IsGenericType || taskType.GetGenericTypeDefinition() != typeof(Task<>)))
{
taskType = taskType.BaseType
?? throw new ArgumentException($"The type '{taskType.FullName}' is not inherited from '{typeof(Task).FullName}'.");
}
return taskType.IsGenericType
? taskType.GetGenericArguments().Single()
: null;
}
interface ITcsResultSetter
{
Type ResultType { get; }
void SetResult(object taskCompletionSource, object result);
void SetException(object taskCompletionSource, Exception exception);
}
private interface ITaskResultGetter
{
object GetResult(Task task);
}
private class TaskResultGetter<T> : ITaskResultGetter
{
public object GetResult(Task task) => ((Task<T>)task).Result;
}
private class VoidTaskResultGetter : ITaskResultGetter
{
public object GetResult(Task task)
{
task.Wait(); // Throw if the task failed
return null;
}
}
private class TcsResultSetter<T> : ITcsResultSetter
{
public Type ResultType => typeof(T);
public void SetResult(object tcs, object result)
{
var typedTcs = (TaskCompletionSource<T>)tcs;
// If necessary, attempt a cast
var typedResult = result is T resultT
? resultT
: (T)Convert.ChangeType(result, typeof(T));
typedTcs.SetResult(typedResult);
}
public void SetException(object tcs, Exception exception)
{
var typedTcs = (TaskCompletionSource<T>)tcs;
typedTcs.SetException(exception);
}
}
private static ITcsResultSetter CreateResultSetter(object taskCompletionSource)
{
return _cachedResultSetters.GetOrAdd(taskCompletionSource.GetType(), tcsType =>
{
var resultType = tcsType.GetGenericArguments().Single();
return (ITcsResultSetter)Activator.CreateInstance(
typeof(TcsResultSetter<>).MakeGenericType(resultType));
});
}
}
}

View File

@ -1,19 +0,0 @@
{
"compilerOptions": {
"baseUrl": ".",
"noEmitOnError": true,
"removeComments": false,
"sourceMap": true,
"target": "es5",
"lib": ["es2015", "dom", "es2015.promise"],
"strict": true,
"declaration": true,
"outDir": "JavaScriptRuntime/dist"
},
"include": [
"JavaScriptRuntime/src/**/*.ts"
],
"exclude": [
"JavaScriptRuntime/dist/**"
]
}

View File

@ -1,25 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Runtime.CompilerServices;
namespace Mono.WebAssembly.Interop
{
/// <summary>
/// Methods that map to the functions compiled into the Mono WebAssembly runtime,
/// as defined by 'mono_add_internal_call' calls in driver.c
/// </summary>
internal class InternalCalls
{
// The exact namespace, type, and method names must match the corresponding entries
// in driver.c in the Mono distribution
// We're passing asyncHandle by ref not because we want it to be writable, but so it gets
// passed as a pointer (4 bytes). We can pass 4-byte values, but not 8-byte ones.
[MethodImpl(MethodImplOptions.InternalCall)]
public static extern string InvokeJSMarshalled(out string exception, ref long asyncHandle, string functionIdentifier, string argsJson);
[MethodImpl(MethodImplOptions.InternalCall)]
public static extern TRes InvokeJSUnmarshalled<T0, T1, T2, TRes>(out string exception, string functionIdentifier, T0 arg0, T1 arg1, T2 arg2);
}
}

View File

@ -1,11 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Microsoft.JSInterop\Microsoft.JSInterop.csproj" />
</ItemGroup>
</Project>

View File

@ -1,113 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.JSInterop;
namespace Mono.WebAssembly.Interop
{
/// <summary>
/// Provides methods for invoking JavaScript functions for applications running
/// on the Mono WebAssembly runtime.
/// </summary>
public class MonoWebAssemblyJSRuntime : JSInProcessRuntimeBase
{
/// <inheritdoc />
protected override string InvokeJS(string identifier, string argsJson)
{
var noAsyncHandle = default(long);
var result = InternalCalls.InvokeJSMarshalled(out var exception, ref noAsyncHandle, identifier, argsJson);
return exception != null
? throw new JSException(exception)
: result;
}
/// <inheritdoc />
protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson)
{
InternalCalls.InvokeJSMarshalled(out _, ref asyncHandle, identifier, argsJson);
}
// Invoked via Mono's JS interop mechanism (invoke_method)
private static string InvokeDotNet(string assemblyName, string methodIdentifier, string dotNetObjectId, string argsJson)
=> DotNetDispatcher.Invoke(assemblyName, methodIdentifier, dotNetObjectId == null ? default : long.Parse(dotNetObjectId), argsJson);
// Invoked via Mono's JS interop mechanism (invoke_method)
private static void BeginInvokeDotNet(string callId, string assemblyNameOrDotNetObjectId, string methodIdentifier, string argsJson)
{
// Figure out whether 'assemblyNameOrDotNetObjectId' is the assembly name or the instance ID
// We only need one for any given call. This helps to work around the limitation that we can
// only pass a maximum of 4 args in a call from JS to Mono WebAssembly.
string assemblyName;
long dotNetObjectId;
if (char.IsDigit(assemblyNameOrDotNetObjectId[0]))
{
dotNetObjectId = long.Parse(assemblyNameOrDotNetObjectId);
assemblyName = null;
}
else
{
dotNetObjectId = default;
assemblyName = assemblyNameOrDotNetObjectId;
}
DotNetDispatcher.BeginInvoke(callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson);
}
#region Custom MonoWebAssemblyJSRuntime methods
/// <summary>
/// Invokes the JavaScript function registered with the specified identifier.
/// </summary>
/// <typeparam name="TRes">The .NET type corresponding to the function's return value type.</typeparam>
/// <param name="identifier">The identifier used when registering the target function.</param>
/// <returns>The result of the function invocation.</returns>
public TRes InvokeUnmarshalled<TRes>(string identifier)
=> InvokeUnmarshalled<object, object, object, TRes>(identifier, null, null, null);
/// <summary>
/// Invokes the JavaScript function registered with the specified identifier.
/// </summary>
/// <typeparam name="T0">The type of the first argument.</typeparam>
/// <typeparam name="TRes">The .NET type corresponding to the function's return value type.</typeparam>
/// <param name="identifier">The identifier used when registering the target function.</param>
/// <param name="arg0">The first argument.</param>
/// <returns>The result of the function invocation.</returns>
public TRes InvokeUnmarshalled<T0, TRes>(string identifier, T0 arg0)
=> InvokeUnmarshalled<T0, object, object, TRes>(identifier, arg0, null, null);
/// <summary>
/// Invokes the JavaScript function registered with the specified identifier.
/// </summary>
/// <typeparam name="T0">The type of the first argument.</typeparam>
/// <typeparam name="T1">The type of the second argument.</typeparam>
/// <typeparam name="TRes">The .NET type corresponding to the function's return value type.</typeparam>
/// <param name="identifier">The identifier used when registering the target function.</param>
/// <param name="arg0">The first argument.</param>
/// <param name="arg1">The second argument.</param>
/// <returns>The result of the function invocation.</returns>
public TRes InvokeUnmarshalled<T0, T1, TRes>(string identifier, T0 arg0, T1 arg1)
=> InvokeUnmarshalled<T0, T1, object, TRes>(identifier, arg0, arg1, null);
/// <summary>
/// Invokes the JavaScript function registered with the specified identifier.
/// </summary>
/// <typeparam name="T0">The type of the first argument.</typeparam>
/// <typeparam name="T1">The type of the second argument.</typeparam>
/// <typeparam name="T2">The type of the third argument.</typeparam>
/// <typeparam name="TRes">The .NET type corresponding to the function's return value type.</typeparam>
/// <param name="identifier">The identifier used when registering the target function.</param>
/// <param name="arg0">The first argument.</param>
/// <param name="arg1">The second argument.</param>
/// <param name="arg2">The third argument.</param>
/// <returns>The result of the function invocation.</returns>
public TRes InvokeUnmarshalled<T0, T1, T2, TRes>(string identifier, T0 arg0, T1 arg1, T2 arg2)
{
var result = InternalCalls.InvokeJSUnmarshalled<T0, T1, T2, TRes>(out var exception, identifier, arg0, arg1, arg2);
return exception != null
? throw new JSException(exception)
: result;
}
#endregion
}
}

View File

@ -1,443 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Linq;
using System.Threading.Tasks;
using Xunit;
namespace Microsoft.JSInterop.Test
{
public class DotNetDispatcherTest
{
private readonly static string thisAssemblyName
= typeof(DotNetDispatcherTest).Assembly.GetName().Name;
private readonly TestJSRuntime jsRuntime
= new TestJSRuntime();
[Fact]
public void CannotInvokeWithEmptyAssemblyName()
{
var ex = Assert.Throws<ArgumentException>(() =>
{
DotNetDispatcher.Invoke(" ", "SomeMethod", default, "[]");
});
Assert.StartsWith("Cannot be null, empty, or whitespace.", ex.Message);
Assert.Equal("assemblyName", ex.ParamName);
}
[Fact]
public void CannotInvokeWithEmptyMethodIdentifier()
{
var ex = Assert.Throws<ArgumentException>(() =>
{
DotNetDispatcher.Invoke("SomeAssembly", " ", default, "[]");
});
Assert.StartsWith("Cannot be null, empty, or whitespace.", ex.Message);
Assert.Equal("methodIdentifier", ex.ParamName);
}
[Fact]
public void CannotInvokeMethodsOnUnloadedAssembly()
{
var assemblyName = "Some.Fake.Assembly";
var ex = Assert.Throws<ArgumentException>(() =>
{
DotNetDispatcher.Invoke(assemblyName, "SomeMethod", default, null);
});
Assert.Equal($"There is no loaded assembly with the name '{assemblyName}'.", ex.Message);
}
// Note: Currently it's also not possible to invoke generic methods.
// That's not something determined by DotNetDispatcher, but rather by the fact that we
// don't close over the generics in the reflection code.
// Not defining this behavior through unit tests because the default outcome is
// fine (an exception stating what info is missing).
[Theory]
[InlineData("MethodOnInternalType")]
[InlineData("PrivateMethod")]
[InlineData("ProtectedMethod")]
[InlineData("StaticMethodWithoutAttribute")] // That's not really its identifier; just making the point that there's no way to invoke it
[InlineData("InstanceMethodWithoutAttribute")] // That's not really its identifier; just making the point that there's no way to invoke it
public void CannotInvokeUnsuitableMethods(string methodIdentifier)
{
var ex = Assert.Throws<ArgumentException>(() =>
{
DotNetDispatcher.Invoke(thisAssemblyName, methodIdentifier, default, null);
});
Assert.Equal($"The assembly '{thisAssemblyName}' does not contain a public method with [JSInvokableAttribute(\"{methodIdentifier}\")].", ex.Message);
}
[Fact]
public Task CanInvokeStaticVoidMethod() => WithJSRuntime(jsRuntime =>
{
// Arrange/Act
SomePublicType.DidInvokeMyInvocableStaticVoid = false;
var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticVoid", default, null);
// Assert
Assert.Null(resultJson);
Assert.True(SomePublicType.DidInvokeMyInvocableStaticVoid);
});
[Fact]
public Task CanInvokeStaticNonVoidMethod() => WithJSRuntime(jsRuntime =>
{
// Arrange/Act
var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticNonVoid", default, null);
var result = Json.Deserialize<TestDTO>(resultJson);
// Assert
Assert.Equal("Test", result.StringVal);
Assert.Equal(123, result.IntVal);
});
[Fact]
public Task CanInvokeStaticNonVoidMethodWithoutCustomIdentifier() => WithJSRuntime(jsRuntime =>
{
// Arrange/Act
var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, nameof(SomePublicType.InvokableMethodWithoutCustomIdentifier), default, null);
var result = Json.Deserialize<TestDTO>(resultJson);
// Assert
Assert.Equal("InvokableMethodWithoutCustomIdentifier", result.StringVal);
Assert.Equal(456, result.IntVal);
});
[Fact]
public Task CanInvokeStaticWithParams() => WithJSRuntime(jsRuntime =>
{
// Arrange: Track a .NET object to use as an arg
var arg3 = new TestDTO { IntVal = 999, StringVal = "My string" };
jsRuntime.Invoke<object>("unimportant", new DotNetObjectRef(arg3));
// Arrange: Remaining args
var argsJson = Json.Serialize(new object[] {
new TestDTO { StringVal = "Another string", IntVal = 456 },
new[] { 100, 200 },
"__dotNetObject:1"
});
// Act
var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticWithParams", default, argsJson);
var result = Json.Deserialize<object[]>(resultJson);
// Assert: First result value marshalled via JSON
var resultDto1 = (TestDTO)jsRuntime.ArgSerializerStrategy.DeserializeObject(result[0], typeof(TestDTO));
Assert.Equal("ANOTHER STRING", resultDto1.StringVal);
Assert.Equal(756, resultDto1.IntVal);
// Assert: Second result value marshalled by ref
var resultDto2Ref = (string)result[1];
Assert.Equal("__dotNetObject:2", resultDto2Ref);
var resultDto2 = (TestDTO)jsRuntime.ArgSerializerStrategy.FindDotNetObject(2);
Assert.Equal("MY STRING", resultDto2.StringVal);
Assert.Equal(1299, resultDto2.IntVal);
});
[Fact]
public Task CanInvokeInstanceVoidMethod() => WithJSRuntime(jsRuntime =>
{
// Arrange: Track some instance
var targetInstance = new SomePublicType();
jsRuntime.Invoke<object>("unimportant", new DotNetObjectRef(targetInstance));
// Act
var resultJson = DotNetDispatcher.Invoke(null, "InvokableInstanceVoid", 1, null);
// Assert
Assert.Null(resultJson);
Assert.True(targetInstance.DidInvokeMyInvocableInstanceVoid);
});
[Fact]
public Task CanInvokeBaseInstanceVoidMethod() => WithJSRuntime(jsRuntime =>
{
// Arrange: Track some instance
var targetInstance = new DerivedClass();
jsRuntime.Invoke<object>("unimportant", new DotNetObjectRef(targetInstance));
// Act
var resultJson = DotNetDispatcher.Invoke(null, "BaseClassInvokableInstanceVoid", 1, null);
// Assert
Assert.Null(resultJson);
Assert.True(targetInstance.DidInvokeMyBaseClassInvocableInstanceVoid);
});
[Fact]
public Task CannotUseDotNetObjectRefAfterDisposal() => WithJSRuntime(jsRuntime =>
{
// This test addresses the case where the developer calls objectRef.Dispose()
// from .NET code, as opposed to .dispose() from JS code
// Arrange: Track some instance, then dispose it
var targetInstance = new SomePublicType();
var objectRef = new DotNetObjectRef(targetInstance);
jsRuntime.Invoke<object>("unimportant", objectRef);
objectRef.Dispose();
// Act/Assert
var ex = Assert.Throws<ArgumentException>(
() => DotNetDispatcher.Invoke(null, "InvokableInstanceVoid", 1, null));
Assert.StartsWith("There is no tracked object with id '1'.", ex.Message);
});
[Fact]
public Task CannotUseDotNetObjectRefAfterReleaseDotNetObject() => WithJSRuntime(jsRuntime =>
{
// This test addresses the case where the developer calls .dispose()
// from JS code, as opposed to objectRef.Dispose() from .NET code
// Arrange: Track some instance, then dispose it
var targetInstance = new SomePublicType();
var objectRef = new DotNetObjectRef(targetInstance);
jsRuntime.Invoke<object>("unimportant", objectRef);
DotNetDispatcher.ReleaseDotNetObject(1);
// Act/Assert
var ex = Assert.Throws<ArgumentException>(
() => DotNetDispatcher.Invoke(null, "InvokableInstanceVoid", 1, null));
Assert.StartsWith("There is no tracked object with id '1'.", ex.Message);
});
[Fact]
public Task CanInvokeInstanceMethodWithParams() => WithJSRuntime(jsRuntime =>
{
// Arrange: Track some instance plus another object we'll pass as a param
var targetInstance = new SomePublicType();
var arg2 = new TestDTO { IntVal = 1234, StringVal = "My string" };
jsRuntime.Invoke<object>("unimportant",
new DotNetObjectRef(targetInstance),
new DotNetObjectRef(arg2));
var argsJson = "[\"myvalue\",\"__dotNetObject:2\"]";
// Act
var resultJson = DotNetDispatcher.Invoke(null, "InvokableInstanceMethod", 1, argsJson);
// Assert
Assert.Equal("[\"You passed myvalue\",\"__dotNetObject:3\"]", resultJson);
var resultDto = (TestDTO)jsRuntime.ArgSerializerStrategy.FindDotNetObject(3);
Assert.Equal(1235, resultDto.IntVal);
Assert.Equal("MY STRING", resultDto.StringVal);
});
[Fact]
public void CannotInvokeWithIncorrectNumberOfParams()
{
// Arrange
var argsJson = Json.Serialize(new object[] { 1, 2, 3, 4 });
// Act/Assert
var ex = Assert.Throws<ArgumentException>(() =>
{
DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticWithParams", default, argsJson);
});
Assert.Equal("In call to 'InvocableStaticWithParams', expected 3 parameters but received 4.", ex.Message);
}
[Fact]
public Task CanInvokeAsyncMethod() => WithJSRuntime(async jsRuntime =>
{
// Arrange: Track some instance plus another object we'll pass as a param
var targetInstance = new SomePublicType();
var arg2 = new TestDTO { IntVal = 1234, StringVal = "My string" };
jsRuntime.Invoke<object>("unimportant", new DotNetObjectRef(targetInstance), new DotNetObjectRef(arg2));
// Arrange: all args
var argsJson = Json.Serialize(new object[]
{
new TestDTO { IntVal = 1000, StringVal = "String via JSON" },
"__dotNetObject:2"
});
// Act
var callId = "123";
var resultTask = jsRuntime.NextInvocationTask;
DotNetDispatcher.BeginInvoke(callId, null, "InvokableAsyncMethod", 1, argsJson);
await resultTask;
var result = Json.Deserialize<SimpleJson.JsonArray>(jsRuntime.LastInvocationArgsJson);
var resultValue = (SimpleJson.JsonArray)result[2];
// Assert: Correct info to complete the async call
Assert.Equal(0, jsRuntime.LastInvocationAsyncHandle); // 0 because it doesn't want a further callback from JS to .NET
Assert.Equal("DotNet.jsCallDispatcher.endInvokeDotNetFromJS", jsRuntime.LastInvocationIdentifier);
Assert.Equal(3, result.Count);
Assert.Equal(callId, result[0]);
Assert.True((bool)result[1]); // Success flag
// Assert: First result value marshalled via JSON
var resultDto1 = (TestDTO)jsRuntime.ArgSerializerStrategy.DeserializeObject(resultValue[0], typeof(TestDTO));
Assert.Equal("STRING VIA JSON", resultDto1.StringVal);
Assert.Equal(2000, resultDto1.IntVal);
// Assert: Second result value marshalled by ref
var resultDto2Ref = (string)resultValue[1];
Assert.Equal("__dotNetObject:3", resultDto2Ref);
var resultDto2 = (TestDTO)jsRuntime.ArgSerializerStrategy.FindDotNetObject(3);
Assert.Equal("MY STRING", resultDto2.StringVal);
Assert.Equal(2468, resultDto2.IntVal);
});
Task WithJSRuntime(Action<TestJSRuntime> testCode)
{
return WithJSRuntime(jsRuntime =>
{
testCode(jsRuntime);
return Task.CompletedTask;
});
}
async Task WithJSRuntime(Func<TestJSRuntime, Task> testCode)
{
// Since the tests rely on the asynclocal JSRuntime.Current, ensure we
// are on a distinct async context with a non-null JSRuntime.Current
await Task.Yield();
var runtime = new TestJSRuntime();
JSRuntime.SetCurrentJSRuntime(runtime);
await testCode(runtime);
}
internal class SomeInteralType
{
[JSInvokable("MethodOnInternalType")] public void MyMethod() { }
}
public class SomePublicType
{
public static bool DidInvokeMyInvocableStaticVoid;
public bool DidInvokeMyInvocableInstanceVoid;
[JSInvokable("PrivateMethod")] private static void MyPrivateMethod() { }
[JSInvokable("ProtectedMethod")] protected static void MyProtectedMethod() { }
protected static void StaticMethodWithoutAttribute() { }
protected static void InstanceMethodWithoutAttribute() { }
[JSInvokable("InvocableStaticVoid")] public static void MyInvocableVoid()
{
DidInvokeMyInvocableStaticVoid = true;
}
[JSInvokable("InvocableStaticNonVoid")]
public static object MyInvocableNonVoid()
=> new TestDTO { StringVal = "Test", IntVal = 123 };
[JSInvokable("InvocableStaticWithParams")]
public static object[] MyInvocableWithParams(TestDTO dtoViaJson, int[] incrementAmounts, TestDTO dtoByRef)
=> new object[]
{
new TestDTO // Return via JSON marshalling
{
StringVal = dtoViaJson.StringVal.ToUpperInvariant(),
IntVal = dtoViaJson.IntVal + incrementAmounts.Sum()
},
new DotNetObjectRef(new TestDTO // Return by ref
{
StringVal = dtoByRef.StringVal.ToUpperInvariant(),
IntVal = dtoByRef.IntVal + incrementAmounts.Sum()
})
};
[JSInvokable]
public static TestDTO InvokableMethodWithoutCustomIdentifier()
=> new TestDTO { StringVal = "InvokableMethodWithoutCustomIdentifier", IntVal = 456 };
[JSInvokable]
public void InvokableInstanceVoid()
{
DidInvokeMyInvocableInstanceVoid = true;
}
[JSInvokable]
public object[] InvokableInstanceMethod(string someString, TestDTO someDTO)
{
// Returning an array to make the point that object references
// can be embedded anywhere in the result
return new object[]
{
$"You passed {someString}",
new DotNetObjectRef(new TestDTO
{
IntVal = someDTO.IntVal + 1,
StringVal = someDTO.StringVal.ToUpperInvariant()
})
};
}
[JSInvokable]
public async Task<object[]> InvokableAsyncMethod(TestDTO dtoViaJson, TestDTO dtoByRef)
{
await Task.Delay(50);
return new object[]
{
new TestDTO // Return via JSON
{
StringVal = dtoViaJson.StringVal.ToUpperInvariant(),
IntVal = dtoViaJson.IntVal * 2,
},
new DotNetObjectRef(new TestDTO // Return by ref
{
StringVal = dtoByRef.StringVal.ToUpperInvariant(),
IntVal = dtoByRef.IntVal * 2,
})
};
}
}
public class BaseClass
{
public bool DidInvokeMyBaseClassInvocableInstanceVoid;
[JSInvokable]
public void BaseClassInvokableInstanceVoid()
{
DidInvokeMyBaseClassInvocableInstanceVoid = true;
}
}
public class DerivedClass : BaseClass
{
}
public class TestDTO
{
public string StringVal { get; set; }
public int IntVal { get; set; }
}
public class TestJSRuntime : JSInProcessRuntimeBase
{
private TaskCompletionSource<object> _nextInvocationTcs = new TaskCompletionSource<object>();
public Task NextInvocationTask => _nextInvocationTcs.Task;
public long LastInvocationAsyncHandle { get; private set; }
public string LastInvocationIdentifier { get; private set; }
public string LastInvocationArgsJson { get; private set; }
protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson)
{
LastInvocationAsyncHandle = asyncHandle;
LastInvocationIdentifier = identifier;
LastInvocationArgsJson = argsJson;
_nextInvocationTcs.SetResult(null);
_nextInvocationTcs = new TaskCompletionSource<object>();
}
protected override string InvokeJS(string identifier, string argsJson)
{
LastInvocationAsyncHandle = default;
LastInvocationIdentifier = identifier;
LastInvocationArgsJson = argsJson;
_nextInvocationTcs.SetResult(null);
_nextInvocationTcs = new TaskCompletionSource<object>();
return null;
}
}
}
}

View File

@ -1,68 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Xunit;
namespace Microsoft.JSInterop.Test
{
public class DotNetObjectRefTest
{
[Fact]
public void CanAccessValue()
{
var obj = new object();
Assert.Same(obj, new DotNetObjectRef(obj).Value);
}
[Fact]
public void CanAssociateWithSameRuntimeMultipleTimes()
{
var objRef = new DotNetObjectRef(new object());
var jsRuntime = new TestJsRuntime();
objRef.EnsureAttachedToJsRuntime(jsRuntime);
objRef.EnsureAttachedToJsRuntime(jsRuntime);
}
[Fact]
public void CannotAssociateWithDifferentRuntimes()
{
var objRef = new DotNetObjectRef(new object());
var jsRuntime1 = new TestJsRuntime();
var jsRuntime2 = new TestJsRuntime();
objRef.EnsureAttachedToJsRuntime(jsRuntime1);
var ex = Assert.Throws<InvalidOperationException>(
() => objRef.EnsureAttachedToJsRuntime(jsRuntime2));
Assert.Contains("Do not attempt to re-use", ex.Message);
}
[Fact]
public void NotifiesAssociatedJsRuntimeOfDisposal()
{
// Arrange
var objRef = new DotNetObjectRef(new object());
var jsRuntime = new TestJsRuntime();
objRef.EnsureAttachedToJsRuntime(jsRuntime);
// Act
objRef.Dispose();
// Assert
Assert.Equal(new[] { objRef }, jsRuntime.UntrackedRefs);
}
class TestJsRuntime : IJSRuntime
{
public List<DotNetObjectRef> UntrackedRefs = new List<DotNetObjectRef>();
public Task<T> InvokeAsync<T>(string identifier, params object[] args)
=> throw new NotImplementedException();
public void UntrackObjectRef(DotNetObjectRef dotNetObjectRef)
=> UntrackedRefs.Add(dotNetObjectRef);
}
}
}

View File

@ -1,117 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using Xunit;
namespace Microsoft.JSInterop.Test
{
public class JSInProcessRuntimeBaseTest
{
[Fact]
public void DispatchesSyncCallsAndDeserializesResults()
{
// Arrange
var runtime = new TestJSInProcessRuntime
{
NextResultJson = Json.Serialize(
new TestDTO { IntValue = 123, StringValue = "Hello" })
};
// Act
var syncResult = runtime.Invoke<TestDTO>("test identifier 1", "arg1", 123, true );
var call = runtime.InvokeCalls.Single();
// Assert
Assert.Equal(123, syncResult.IntValue);
Assert.Equal("Hello", syncResult.StringValue);
Assert.Equal("test identifier 1", call.Identifier);
Assert.Equal("[\"arg1\",123,true]", call.ArgsJson);
}
[Fact]
public void SerializesDotNetObjectWrappersInKnownFormat()
{
// Arrange
var runtime = new TestJSInProcessRuntime { NextResultJson = null };
var obj1 = new object();
var obj2 = new object();
var obj3 = new object();
// Act
// Showing we can pass the DotNetObject either as top-level args or nested
var syncResult = runtime.Invoke<object>("test identifier",
new DotNetObjectRef(obj1),
new Dictionary<string, object>
{
{ "obj2", new DotNetObjectRef(obj2) },
{ "obj3", new DotNetObjectRef(obj3) }
});
// Assert: Handles null result string
Assert.Null(syncResult);
// Assert: Serialized as expected
var call = runtime.InvokeCalls.Single();
Assert.Equal("test identifier", call.Identifier);
Assert.Equal("[\"__dotNetObject:1\",{\"obj2\":\"__dotNetObject:2\",\"obj3\":\"__dotNetObject:3\"}]", call.ArgsJson);
// Assert: Objects were tracked
Assert.Same(obj1, runtime.ArgSerializerStrategy.FindDotNetObject(1));
Assert.Same(obj2, runtime.ArgSerializerStrategy.FindDotNetObject(2));
Assert.Same(obj3, runtime.ArgSerializerStrategy.FindDotNetObject(3));
}
[Fact]
public void SyncCallResultCanIncludeDotNetObjects()
{
// Arrange
var runtime = new TestJSInProcessRuntime
{
NextResultJson = "[\"__dotNetObject:2\",\"__dotNetObject:1\"]"
};
var obj1 = new object();
var obj2 = new object();
// Act
var syncResult = runtime.Invoke<object[]>("test identifier",
new DotNetObjectRef(obj1),
"some other arg",
new DotNetObjectRef(obj2));
var call = runtime.InvokeCalls.Single();
// Assert
Assert.Equal(new[] { obj2, obj1 }, syncResult);
}
class TestDTO
{
public int IntValue { get; set; }
public string StringValue { get; set; }
}
class TestJSInProcessRuntime : JSInProcessRuntimeBase
{
public List<InvokeArgs> InvokeCalls { get; set; } = new List<InvokeArgs>();
public string NextResultJson { get; set; }
protected override string InvokeJS(string identifier, string argsJson)
{
InvokeCalls.Add(new InvokeArgs { Identifier = identifier, ArgsJson = argsJson });
return NextResultJson;
}
public class InvokeArgs
{
public string Identifier { get; set; }
public string ArgsJson { get; set; }
}
protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson)
=> throw new NotImplementedException("This test only covers sync calls");
}
}
}

View File

@ -1,191 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.JSInterop.Internal;
using System;
using System.Collections.Generic;
using System.Linq;
using Xunit;
namespace Microsoft.JSInterop.Test
{
public class JSRuntimeBaseTest
{
[Fact]
public void DispatchesAsyncCallsWithDistinctAsyncHandles()
{
// Arrange
var runtime = new TestJSRuntime();
// Act
runtime.InvokeAsync<object>("test identifier 1", "arg1", 123, true );
runtime.InvokeAsync<object>("test identifier 2", "some other arg");
// Assert
Assert.Collection(runtime.BeginInvokeCalls,
call =>
{
Assert.Equal("test identifier 1", call.Identifier);
Assert.Equal("[\"arg1\",123,true]", call.ArgsJson);
},
call =>
{
Assert.Equal("test identifier 2", call.Identifier);
Assert.Equal("[\"some other arg\"]", call.ArgsJson);
Assert.NotEqual(runtime.BeginInvokeCalls[0].AsyncHandle, call.AsyncHandle);
});
}
[Fact]
public void CanCompleteAsyncCallsAsSuccess()
{
// Arrange
var runtime = new TestJSRuntime();
// Act/Assert: Tasks not initially completed
var unrelatedTask = runtime.InvokeAsync<string>("unrelated call", Array.Empty<object>());
var task = runtime.InvokeAsync<string>("test identifier", Array.Empty<object>());
Assert.False(unrelatedTask.IsCompleted);
Assert.False(task.IsCompleted);
// Act/Assert: Task can be completed
runtime.OnEndInvoke(
runtime.BeginInvokeCalls[1].AsyncHandle,
/* succeeded: */ true,
"my result");
Assert.False(unrelatedTask.IsCompleted);
Assert.True(task.IsCompleted);
Assert.Equal("my result", task.Result);
}
[Fact]
public void CanCompleteAsyncCallsAsFailure()
{
// Arrange
var runtime = new TestJSRuntime();
// Act/Assert: Tasks not initially completed
var unrelatedTask = runtime.InvokeAsync<string>("unrelated call", Array.Empty<object>());
var task = runtime.InvokeAsync<string>("test identifier", Array.Empty<object>());
Assert.False(unrelatedTask.IsCompleted);
Assert.False(task.IsCompleted);
// Act/Assert: Task can be failed
runtime.OnEndInvoke(
runtime.BeginInvokeCalls[1].AsyncHandle,
/* succeeded: */ false,
"This is a test exception");
Assert.False(unrelatedTask.IsCompleted);
Assert.True(task.IsCompleted);
Assert.IsType<AggregateException>(task.Exception);
Assert.IsType<JSException>(task.Exception.InnerException);
Assert.Equal("This is a test exception", ((JSException)task.Exception.InnerException).Message);
}
[Fact]
public void CannotCompleteSameAsyncCallMoreThanOnce()
{
// Arrange
var runtime = new TestJSRuntime();
// Act/Assert
runtime.InvokeAsync<string>("test identifier", Array.Empty<object>());
var asyncHandle = runtime.BeginInvokeCalls[0].AsyncHandle;
runtime.OnEndInvoke(asyncHandle, true, null);
var ex = Assert.Throws<ArgumentException>(() =>
{
// Second "end invoke" will fail
runtime.OnEndInvoke(asyncHandle, true, null);
});
Assert.Equal($"There is no pending task with handle '{asyncHandle}'.", ex.Message);
}
[Fact]
public void SerializesDotNetObjectWrappersInKnownFormat()
{
// Arrange
var runtime = new TestJSRuntime();
var obj1 = new object();
var obj2 = new object();
var obj3 = new object();
// Act
// Showing we can pass the DotNetObject either as top-level args or nested
var obj1Ref = new DotNetObjectRef(obj1);
var obj1DifferentRef = new DotNetObjectRef(obj1);
runtime.InvokeAsync<object>("test identifier",
obj1Ref,
new Dictionary<string, object>
{
{ "obj2", new DotNetObjectRef(obj2) },
{ "obj3", new DotNetObjectRef(obj3) },
{ "obj1SameRef", obj1Ref },
{ "obj1DifferentRef", obj1DifferentRef },
});
// Assert: Serialized as expected
var call = runtime.BeginInvokeCalls.Single();
Assert.Equal("test identifier", call.Identifier);
Assert.Equal("[\"__dotNetObject:1\",{\"obj2\":\"__dotNetObject:2\",\"obj3\":\"__dotNetObject:3\",\"obj1SameRef\":\"__dotNetObject:1\",\"obj1DifferentRef\":\"__dotNetObject:4\"}]", call.ArgsJson);
// Assert: Objects were tracked
Assert.Same(obj1, runtime.ArgSerializerStrategy.FindDotNetObject(1));
Assert.Same(obj2, runtime.ArgSerializerStrategy.FindDotNetObject(2));
Assert.Same(obj3, runtime.ArgSerializerStrategy.FindDotNetObject(3));
Assert.Same(obj1, runtime.ArgSerializerStrategy.FindDotNetObject(4));
}
[Fact]
public void SupportsCustomSerializationForArguments()
{
// Arrange
var runtime = new TestJSRuntime();
// Arrange/Act
runtime.InvokeAsync<object>("test identifier",
new WithCustomArgSerializer());
// Asssert
var call = runtime.BeginInvokeCalls.Single();
Assert.Equal("[{\"key1\":\"value1\",\"key2\":123}]", call.ArgsJson);
}
class TestJSRuntime : JSRuntimeBase
{
public List<BeginInvokeAsyncArgs> BeginInvokeCalls = new List<BeginInvokeAsyncArgs>();
public class BeginInvokeAsyncArgs
{
public long AsyncHandle { get; set; }
public string Identifier { get; set; }
public string ArgsJson { get; set; }
}
protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson)
{
BeginInvokeCalls.Add(new BeginInvokeAsyncArgs
{
AsyncHandle = asyncHandle,
Identifier = identifier,
ArgsJson = argsJson,
});
}
public void OnEndInvoke(long asyncHandle, bool succeeded, object resultOrException)
=> EndInvokeJS(asyncHandle, succeeded, resultOrException);
}
class WithCustomArgSerializer : ICustomArgSerializer
{
public object ToJsonPrimitive()
{
return new Dictionary<string, object>
{
{ "key1", "value1" },
{ "key2", 123 },
};
}
}
}
}

View File

@ -1,37 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Linq;
using System.Threading.Tasks;
using Xunit;
namespace Microsoft.JSInterop.Test
{
public class JSRuntimeTest
{
[Fact]
public async Task CanHaveDistinctJSRuntimeInstancesInEachAsyncContext()
{
var tasks = Enumerable.Range(0, 20).Select(async _ =>
{
var jsRuntime = new FakeJSRuntime();
JSRuntime.SetCurrentJSRuntime(jsRuntime);
await Task.Delay(50).ConfigureAwait(false);
Assert.Same(jsRuntime, JSRuntime.Current);
});
await Task.WhenAll(tasks);
Assert.Null(JSRuntime.Current);
}
private class FakeJSRuntime : IJSRuntime
{
public Task<T> InvokeAsync<T>(string identifier, params object[] args)
=> throw new NotImplementedException();
public void UntrackObjectRef(DotNetObjectRef dotNetObjectRef)
=> throw new NotImplementedException();
}
}
}

View File

@ -1,286 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.JSInterop.Internal;
using System;
using System.Collections.Generic;
using Xunit;
namespace Microsoft.JSInterop.Test
{
public class JsonUtilTest
{
// It's not useful to have a complete set of behavior specifications for
// what the JSON serializer/deserializer does in all cases here. We merely
// expose a simple wrapper over a third-party library that maintains its
// own specs and tests.
//
// We should only add tests here to cover behaviors that Blazor itself
// depends on.
[Theory]
[InlineData(null, "null")]
[InlineData("My string", "\"My string\"")]
[InlineData(123, "123")]
[InlineData(123.456f, "123.456")]
[InlineData(123.456d, "123.456")]
[InlineData(true, "true")]
public void CanSerializePrimitivesToJson(object value, string expectedJson)
{
Assert.Equal(expectedJson, Json.Serialize(value));
}
[Theory]
[InlineData("null", null)]
[InlineData("\"My string\"", "My string")]
[InlineData("123", 123L)] // Would also accept 123 as a System.Int32, but Int64 is fine as a default
[InlineData("123.456", 123.456d)]
[InlineData("true", true)]
public void CanDeserializePrimitivesFromJson(string json, object expectedValue)
{
Assert.Equal(expectedValue, Json.Deserialize<object>(json));
}
[Fact]
public void CanSerializeClassToJson()
{
// Arrange
var person = new Person
{
Id = 1844,
Name = "Athos",
Pets = new[] { "Aramis", "Porthos", "D'Artagnan" },
Hobby = Hobbies.Swordfighting,
Nicknames = new List<string> { "Comte de la Fère", "Armand" },
BirthInstant = new DateTimeOffset(1825, 8, 6, 18, 45, 21, TimeSpan.FromHours(-6)),
Age = new TimeSpan(7665, 1, 30, 0),
Allergies = new Dictionary<string, object> { { "Ducks", true }, { "Geese", false } },
};
// Act/Assert
Assert.Equal(
"{\"id\":1844,\"name\":\"Athos\",\"pets\":[\"Aramis\",\"Porthos\",\"D'Artagnan\"],\"hobby\":2,\"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}}",
Json.Serialize(person));
}
[Fact]
public void CanDeserializeClassFromJson()
{
// Arrange
var json = "{\"id\":1844,\"name\":\"Athos\",\"pets\":[\"Aramis\",\"Porthos\",\"D'Artagnan\"],\"hobby\":2,\"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}}";
// Act
var person = Json.Deserialize<Person>(json);
// Assert
Assert.Equal(1844, person.Id);
Assert.Equal("Athos", person.Name);
Assert.Equal(new[] { "Aramis", "Porthos", "D'Artagnan" }, person.Pets);
Assert.Equal(Hobbies.Swordfighting, person.Hobby);
Assert.Equal(new[] { "Comte de la Fère", "Armand" }, person.Nicknames);
Assert.Equal(new DateTimeOffset(1825, 8, 6, 18, 45, 21, TimeSpan.FromHours(-6)), person.BirthInstant);
Assert.Equal(new TimeSpan(7665, 1, 30, 0), person.Age);
Assert.Equal(new Dictionary<string, object> { { "Ducks", true }, { "Geese", false } }, person.Allergies);
}
[Fact]
public void CanDeserializeWithCaseInsensitiveKeys()
{
// Arrange
var json = "{\"ID\":1844,\"NamE\":\"Athos\"}";
// Act
var person = Json.Deserialize<Person>(json);
// Assert
Assert.Equal(1844, person.Id);
Assert.Equal("Athos", person.Name);
}
[Fact]
public void DeserializationPrefersPropertiesOverFields()
{
// Arrange
var json = "{\"member1\":\"Hello\"}";
// Act
var person = Json.Deserialize<PrefersPropertiesOverFields>(json);
// Assert
Assert.Equal("Hello", person.Member1);
Assert.Null(person.member1);
}
[Fact]
public void CanSerializeStructToJson()
{
// Arrange
var commandResult = new SimpleStruct
{
StringProperty = "Test",
BoolProperty = true,
NullableIntProperty = 1
};
// Act
var result = Json.Serialize(commandResult);
// Assert
Assert.Equal("{\"stringProperty\":\"Test\",\"boolProperty\":true,\"nullableIntProperty\":1}", result);
}
[Fact]
public void CanDeserializeStructFromJson()
{
// Arrange
var json = "{\"stringProperty\":\"Test\",\"boolProperty\":true,\"nullableIntProperty\":1}";
//Act
var simpleError = Json.Deserialize<SimpleStruct>(json);
// Assert
Assert.Equal("Test", simpleError.StringProperty);
Assert.True(simpleError.BoolProperty);
Assert.Equal(1, simpleError.NullableIntProperty);
}
[Fact]
public void RejectsTypesWithAmbiguouslyNamedProperties()
{
var ex = Assert.Throws<InvalidOperationException>(() =>
{
Json.Deserialize<ClashingProperties>("{}");
});
Assert.Equal($"The type '{typeof(ClashingProperties).FullName}' contains multiple public properties " +
$"with names case-insensitively matching '{nameof(ClashingProperties.PROP1).ToLowerInvariant()}'. " +
$"Such types cannot be used for JSON deserialization.",
ex.Message);
}
[Fact]
public void RejectsTypesWithAmbiguouslyNamedFields()
{
var ex = Assert.Throws<InvalidOperationException>(() =>
{
Json.Deserialize<ClashingFields>("{}");
});
Assert.Equal($"The type '{typeof(ClashingFields).FullName}' contains multiple public fields " +
$"with names case-insensitively matching '{nameof(ClashingFields.Field1).ToLowerInvariant()}'. " +
$"Such types cannot be used for JSON deserialization.",
ex.Message);
}
[Fact]
public void NonEmptyConstructorThrowsUsefulException()
{
// Arrange
var json = "{\"Property\":1}";
var type = typeof(NonEmptyConstructorPoco);
// Act
var exception = Assert.Throws<InvalidOperationException>(() =>
{
Json.Deserialize<NonEmptyConstructorPoco>(json);
});
// Assert
Assert.Equal(
$"Cannot deserialize JSON into type '{type.FullName}' because it does not have a public parameterless constructor.",
exception.Message);
}
// Test cases based on https://github.com/JamesNK/Newtonsoft.Json/blob/122afba9908832bd5ac207164ee6c303bfd65cf1/Src/Newtonsoft.Json.Tests/Utilities/StringUtilsTests.cs#L41
// The only difference is that our logic doesn't have to handle space-separated words,
// because we're only use this for camelcasing .NET member names
//
// Not all of the following cases are really valid .NET member names, but we have no reason
// to implement more logic to detect invalid member names besides the basics (null or empty).
[Theory]
[InlineData("URLValue", "urlValue")]
[InlineData("URL", "url")]
[InlineData("ID", "id")]
[InlineData("I", "i")]
[InlineData("Person", "person")]
[InlineData("xPhone", "xPhone")]
[InlineData("XPhone", "xPhone")]
[InlineData("X_Phone", "x_Phone")]
[InlineData("X__Phone", "x__Phone")]
[InlineData("IsCIA", "isCIA")]
[InlineData("VmQ", "vmQ")]
[InlineData("Xml2Json", "xml2Json")]
[InlineData("SnAkEcAsE", "snAkEcAsE")]
[InlineData("SnA__kEcAsE", "snA__kEcAsE")]
[InlineData("already_snake_case_", "already_snake_case_")]
[InlineData("IsJSONProperty", "isJSONProperty")]
[InlineData("SHOUTING_CASE", "shoutinG_CASE")]
[InlineData("9999-12-31T23:59:59.9999999Z", "9999-12-31T23:59:59.9999999Z")]
[InlineData("Hi!! This is text. Time to test.", "hi!! This is text. Time to test.")]
[InlineData("BUILDING", "building")]
[InlineData("BUILDINGProperty", "buildingProperty")]
public void MemberNameToCamelCase_Valid(string input, string expectedOutput)
{
Assert.Equal(expectedOutput, CamelCase.MemberNameToCamelCase(input));
}
[Theory]
[InlineData("")]
[InlineData(null)]
public void MemberNameToCamelCase_Invalid(string input)
{
var ex = Assert.Throws<ArgumentException>(() =>
CamelCase.MemberNameToCamelCase(input));
Assert.Equal("value", ex.ParamName);
Assert.StartsWith($"The value '{input ?? "null"}' is not a valid member name.", ex.Message);
}
class NonEmptyConstructorPoco
{
public NonEmptyConstructorPoco(int parameter) {}
public int Property { get; set; }
}
struct SimpleStruct
{
public string StringProperty { get; set; }
public bool BoolProperty { get; set; }
public int? NullableIntProperty { get; set; }
}
class Person
{
public int Id { get; set; }
public string Name { get; set; }
public string[] Pets { get; set; }
public Hobbies Hobby { get; set; }
public IList<string> Nicknames { get; set; }
public DateTimeOffset BirthInstant { get; set; }
public TimeSpan Age { get; set; }
public IDictionary<string, object> Allergies { get; set; }
}
enum Hobbies { Reading = 1, Swordfighting = 2 }
#pragma warning disable 0649
class ClashingProperties
{
public string Prop1 { get; set; }
public int PROP1 { get; set; }
}
class ClashingFields
{
public string Field1;
public int field1;
}
class PrefersPropertiesOverFields
{
public string member1;
public string Member1 { get; set; }
}
#pragma warning restore 0649
}
}

View File

@ -1,20 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.5.0" />
<PackageReference Include="xunit" Version="2.3.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
<DotNetCliToolReference Include="dotnet-xunit" Version="2.3.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.JSInterop\Microsoft.JSInterop.csproj" />
</ItemGroup>
</Project>