diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..24a5c16
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,43 @@
+# NuGet
+*.nuget.props
+*.nuget.targets
+
+**/packages/*
+!**/packages/build/
+
+# User-specific files
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# Build results
+[Ss]amples/**/*.js
+[Dd]ebug/
+[Rr]elease/
+x64/
+x86/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+
+# Visual Studio 2015 cache/options directory
+.vs/
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!*.[Cc]ache/
+
+
+
+
+
diff --git a/.nuget/NuGet.exe b/.nuget/NuGet.exe
new file mode 100644
index 0000000..e42e6d8
Binary files /dev/null and b/.nuget/NuGet.exe differ
diff --git a/Deploy/Logo/Logo-big.png b/Deploy/Logo/Logo-big.png
new file mode 100644
index 0000000..0a21a22
Binary files /dev/null and b/Deploy/Logo/Logo-big.png differ
diff --git a/Deploy/Logo/Logo-small.png b/Deploy/Logo/Logo-small.png
new file mode 100644
index 0000000..cd29db6
Binary files /dev/null and b/Deploy/Logo/Logo-small.png differ
diff --git a/Deploy/Logo/WebSocketRPC.pptx b/Deploy/Logo/WebSocketRPC.pptx
new file mode 100644
index 0000000..d7e5631
Binary files /dev/null and b/Deploy/Logo/WebSocketRPC.pptx differ
diff --git a/Deploy/Nuget/Build.cmd b/Deploy/Nuget/Build.cmd
new file mode 100644
index 0000000..a06db5f
--- /dev/null
+++ b/Deploy/Nuget/Build.cmd
@@ -0,0 +1,31 @@
+::
+@echo off
+:: timeout /T 5
+
+:: settings
+set nugetPath=%cd%\..\.nuget
+set version=0.5.0
+set output=%cd%\bin
+
+:: Create output directory
+IF NOT EXIST "%output%\" (
+ mkdir "%output%"
+)
+
+:: Remove old files
+echo.
+echo Remvoing old packages:
+for /r %%f in (*.nupkg) do (
+ echo %%f
+ del "%%f"
+)
+
+echo.
+echo Creating packages for:
+for /r %%f in (*.nuspec) do (
+ echo %%f
+ "%nugetPath%\nuget.exe" pack "%%f" -Version %version% -OutputDirectory "%output%" -Verbosity quiet
+)
+
+echo.
+pause
\ No newline at end of file
diff --git a/Deploy/Nuget/Push.cmd b/Deploy/Nuget/Push.cmd
new file mode 100644
index 0000000..6bc281f
--- /dev/null
+++ b/Deploy/Nuget/Push.cmd
@@ -0,0 +1,16 @@
+::
+@echo off
+timeout /T 5
+
+:: settings
+set nugetPath=%cd%\..\.nuget
+
+echo.
+echo Pushing packages:
+for /r %%f in (*.nupkg) do (
+ echo %%f
+ "%nugetPath%\nuget.exe" push "%%f"
+)
+
+echo.
+pause
\ No newline at end of file
diff --git a/Deploy/Nuget/UpdateNuGet.cmd b/Deploy/Nuget/UpdateNuGet.cmd
new file mode 100644
index 0000000..8364212
--- /dev/null
+++ b/Deploy/Nuget/UpdateNuGet.cmd
@@ -0,0 +1,13 @@
+::
+@echo off
+set nugetPath=%cd%\..\.nuget
+
+:: Make sure the nuget executable is writable
+attrib -R "%nugetPath%\nuget.exe"
+
+echo.
+echo Updating NuGet...
+"%nugetPath%\nuget.exe" update -Self
+
+echo.
+pause
\ No newline at end of file
diff --git a/Deploy/Nuget/nuSpecs/WebsocketRPC.JS.nuspec b/Deploy/Nuget/nuSpecs/WebsocketRPC.JS.nuspec
new file mode 100644
index 0000000..fb9de94
--- /dev/null
+++ b/Deploy/Nuget/nuSpecs/WebsocketRPC.JS.nuspec
@@ -0,0 +1,28 @@
+
+
+
+
+ WebsocketRPC.JS
+ $version$
+ WebsocketRPC.JS
+ Darko Jurić
+ Darko Jurić
+
+
+ Generates the Javascript code for websocket-connection using the provided .NET interface.
+
+ Generates the Javascript code for websocket-connection using the provided .NET interface.
+ websocket; websocket-client; Javascript; RPC; C#; .NET
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Deploy/Nuget/nuSpecs/WebsocketRPC.nuspec b/Deploy/Nuget/nuSpecs/WebsocketRPC.nuspec
new file mode 100644
index 0000000..378cc5b
--- /dev/null
+++ b/Deploy/Nuget/nuSpecs/WebsocketRPC.nuspec
@@ -0,0 +1,30 @@
+
+
+
+
+ WebsocketRPC
+ $version$
+ WebsocketRPC
+ Darko Jurić
+ Darko Jurić
+
+
+ Provides full duplex RPC over websocket.
+
+ Provides full duplex RPC over websocket.
+ websocket; RPC; C#; .NET
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..024e9e3
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,23 @@
+The MIT License (MIT)
+
+Copyright (c) Darko Jurić
+
+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.
+
+
+Licensing for JSON.NET dependency also applies: https://www.newtonsoft.com/json
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..a483fb0
--- /dev/null
+++ b/README.md
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+**WebSokcetRPC** - RPC over weboskcets for .NET
+Leightweight .NET framework for making RPC over websockets. Supports full duplex connections; .NET or Javascript clients.
+
+
+
+
+## Why WebSocketRPC ?
+
++ **Lightweight**
+The only dependency is JSON.NET library used for serialization/deserialization.
+
++ **Simple**
+There is only one relevant method: **Bind** for binding object/interface onto connection, and **CallAsync** for calling RPCs.
+
++ **Connection relaying in a single line of code**
+ The library is capable to relay incoming requests to a background services with just one command: **Relay**
+
++ **Automatic Javascript code generation** *(WebsocketRPC.JS package)*
+ Javascript websokcet client code is automatically generated (with JsDoc comments) from an existing .NET
+ interface (API contract).
+
+
+## Sample
+
+To scratch the surface... *(RPC in both directions, connection relaying, multi-service, .NET clients)*
+
+**Server**
+ ``` csharp
+class MathAPI
+{
+ public async Task LongRunningTask(int a, int b)
+ {
+ await Task.Delay(250);
+ return a + b;
+ }
+}
+
+....
+//generate js code
+File.WriteAllText("MathAPI.js", RPCJs.GenerateCallerWithDoc());
+//run server
+Server.ListenAsync("http://localhost:8000/", CancellationToken.None,
+ (c, wc) => c.Bind(new MathAPI()))
+ .Wait(0);
+ ```
+
+ **Client**
+ ``` javascript
+var api = new MathAPI("ws://localhost:8000");
+api.connect(async () =>
+{
+ var r = await api.longRunningTask(5, 3);
+ console.log("Result: " + r);
+});
+ ```
+
+
+## Getting started
++ Samples
+
+
+## How to Engage, Contribute and Provide Feedback
+Remember: Your opinion is important and will define the future roadmap.
++ questions, comments - message on Github, or write to: darko.juric2 [at] gmail.com
++ **spread the word**
+
+## Final word
+If you like the project please **star it** in order to help to spread the word. That way you will make the framework more significant and in the same time you will motivate me to improve it, so the benefit is mutual.
\ No newline at end of file
diff --git a/Samples/ClientJs/Program.cs b/Samples/ClientJs/Program.cs
new file mode 100644
index 0000000..5896d7d
--- /dev/null
+++ b/Samples/ClientJs/Program.cs
@@ -0,0 +1,45 @@
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using WebsocketRPC;
+
+namespace TestClientJs
+{
+ interface IRemoteAPI
+ {
+ bool WriteProgress(float progress);
+ }
+
+ class LocalAPI
+ {
+ public async Task LongRunningTask(int a, int b)
+ {
+ for (var p = 0; p <= 100; p += 5)
+ {
+ await Task.Delay(250);
+ await RPC.For(this).CallAsync(x => x.WriteProgress((float)p / 100));
+ }
+
+ return a + b;
+ }
+ }
+
+ public class Program
+ {
+ //if access denied execute: "netsh http delete urlacl url=http://+:8001/"
+ static void Main(string[] args)
+ {
+ //generate js code
+ File.WriteAllText($"../Site/{nameof(LocalAPI)}.js", RPCJs.GenerateCallerWithDoc());
+
+ //start server and bind its local and remote API
+ var cts = new CancellationTokenSource();
+ Server.ListenAsync("http://localhost:8001/", cts.Token, (c, ws) => c.Bind(new LocalAPI())).Wait(0);
+
+ Console.Write("Running: '{0}'. Press [Enter] to exit.", nameof(TestClientJs));
+ Console.ReadLine();
+ cts.Cancel();
+ }
+ }
+}
diff --git a/Samples/ClientJs/ServerClientJs.csproj b/Samples/ClientJs/ServerClientJs.csproj
new file mode 100644
index 0000000..ad5aaee
--- /dev/null
+++ b/Samples/ClientJs/ServerClientJs.csproj
@@ -0,0 +1,61 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {C0449FA5-C667-4B5C-878A-04773903B130}
+ Exe
+ Properties
+ ClientJs
+ ClientJs
+ v4.7
+ 512
+
+
+
+ true
+ full
+ false
+ bin\
+ DEBUG;TRACE
+ prompt
+ 4
+ false
+
+
+ pdbonly
+ true
+ bin\
+ TRACE
+ prompt
+ 4
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+ {965791bf-8f77-4a69-97e2-8b6b66cf9863}
+ WebSocketRPC.JS
+
+
+ {AB4A5DDF-1F91-4AF8-9E9C-242832576C5E}
+ WebsocketRPC
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Samples/ClientJs/Site/Index.html b/Samples/ClientJs/Site/Index.html
new file mode 100644
index 0000000..8335921
--- /dev/null
+++ b/Samples/ClientJs/Site/Index.html
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Samples/MultiService/MultiService.csproj b/Samples/MultiService/MultiService.csproj
new file mode 100644
index 0000000..6eceac2
--- /dev/null
+++ b/Samples/MultiService/MultiService.csproj
@@ -0,0 +1,60 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {2B69F62E-991C-4E4B-B1FF-2A5906415764}
+ Exe
+ Properties
+ ClientJsMultiService
+ ClientJsMultiService
+ v4.7
+ 512
+
+
+
+ true
+ full
+ false
+ bin\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ pdbonly
+ true
+ bin\
+ TRACE
+ prompt
+ 4
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {965791bf-8f77-4a69-97e2-8b6b66cf9863}
+ WebSocketRPC.JS
+
+
+ {ab4a5ddf-1f91-4af8-9e9c-242832576c5e}
+ WebsocketRPC
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Samples/MultiService/Program.cs b/Samples/MultiService/Program.cs
new file mode 100644
index 0000000..740804a
--- /dev/null
+++ b/Samples/MultiService/Program.cs
@@ -0,0 +1,72 @@
+using System;
+using System.IO;
+using System.Threading;
+using WebsocketRPC;
+
+namespace ClientJsMultiService
+{
+ ///
+ /// Numeric service providing operations on numbers.
+ ///
+ public class NumericService
+ {
+ ///
+ /// Adds two numbers.
+ ///
+ /// First number.
+ /// Second number.
+ /// Result.
+ public int Add(int a, int b)
+ {
+ return a + b;
+ }
+ }
+
+ ///
+ /// Text service providing operations on strings.
+ ///
+ public class TextService
+ {
+ ///
+ /// Concatenates two strings.
+ ///
+ /// First string.
+ /// Second string.
+ /// Concatenated string.
+ public string Add(string a, string b)
+ {
+ return a + b;
+ }
+ }
+
+ public class Program
+ {
+ //if access denied execute: "netsh http delete urlacl url=http://+:8001/"
+ static void Main(string[] args)
+ {
+ //generate js code
+ File.WriteAllText($"../Site/{nameof(NumericService)}.js", RPCJs.GenerateCallerWithDoc());
+ File.WriteAllText($"../Site/{nameof(TextService)}.js", RPCJs.GenerateCallerWithDoc());
+
+ //start server and bind its local and remote APIs
+ var cts = new CancellationTokenSource();
+ Server.ListenAsync("http://localhost:8001/", cts.Token, (c, wc) =>
+ {
+ var path = wc.RequestUri.AbsolutePath;
+ if (path == "/numericService")
+ {
+ c.Bind(new NumericService());
+ }
+ else if (path == "/textService")
+ {
+ c.Bind(new TextService());
+ }
+ })
+ .Wait(0);
+
+ Console.Write("Running: '{0}'. Press [Enter] to exit.", nameof(ClientJsMultiService));
+ Console.ReadLine();
+ cts.Cancel();
+ }
+ }
+}
diff --git a/Samples/MultiService/Site/Index.html b/Samples/MultiService/Site/Index.html
new file mode 100644
index 0000000..0345c88
--- /dev/null
+++ b/Samples/MultiService/Site/Index.html
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Samples/README.md b/Samples/README.md
new file mode 100644
index 0000000..8c10612
--- /dev/null
+++ b/Samples/README.md
@@ -0,0 +1,15 @@
+Samples
+=====================
+
+The recommendation is to run samples in the following order (from the more simple ones to more complex ones):
+
+1. ServerClientSample
+2. ServerClientJs
+3. MultiService
+4. Serialization
+5. RelayConnectionSample
+
+**Remarks**
++ If a sample contains JavaScript client, the additional step is to open the included Index.html.
++ Samples that contain multiple executables have 'Run.bat' for one-click run.
+
diff --git a/Samples/RelayConnectionSample/BackgroundService/BackgroundService.csproj b/Samples/RelayConnectionSample/BackgroundService/BackgroundService.csproj
new file mode 100644
index 0000000..07b1d50
--- /dev/null
+++ b/Samples/RelayConnectionSample/BackgroundService/BackgroundService.csproj
@@ -0,0 +1,54 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {4616F442-466A-4B0E-8939-DA6367A5E365}
+ Exe
+ BackgroundService
+ BackgroundService
+ v4.7
+ 512
+ true
+
+
+
+ AnyCPU
+ true
+ full
+ false
+ bin\
+ DEBUG;TRACE
+ prompt
+ 4
+ false
+
+
+ AnyCPU
+ pdbonly
+ true
+ bin\
+ TRACE
+ prompt
+ 4
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {ab4a5ddf-1f91-4af8-9e9c-242832576c5e}
+ WebsocketRPC
+
+
+
+
\ No newline at end of file
diff --git a/Samples/RelayConnectionSample/BackgroundService/Program.cs b/Samples/RelayConnectionSample/BackgroundService/Program.cs
new file mode 100644
index 0000000..523c4e5
--- /dev/null
+++ b/Samples/RelayConnectionSample/BackgroundService/Program.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Threading;
+using WebsocketRPC;
+
+namespace BackgroundService
+{
+ public class BackgroundService
+ {
+ public string ConcatString(string a, string b)
+ {
+ return a + b;
+ }
+ }
+
+ public class Program
+ {
+ //if access denied execute: "netsh http delete urlacl url=http://+:9001/"
+ static void Main(string[] args)
+ {
+ //start server and bind its local and remote API
+ var cts = new CancellationTokenSource();
+ Server.ListenAsync("http://localhost:9001/", cts.Token, (c, ws) => c.Bind(new BackgroundService())).Wait(0);
+
+ Console.Write("Running: '{0}'. Press [Enter] to exit.", nameof(BackgroundService));
+ Console.ReadLine();
+ cts.Cancel();
+ }
+ }
+}
diff --git a/Samples/RelayConnectionSample/FrontendService/FrontendService.csproj b/Samples/RelayConnectionSample/FrontendService/FrontendService.csproj
new file mode 100644
index 0000000..f8216c5
--- /dev/null
+++ b/Samples/RelayConnectionSample/FrontendService/FrontendService.csproj
@@ -0,0 +1,61 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {79BCCE0B-3666-4866-AF36-8FA2C71479C0}
+ Exe
+ FrontendService
+ FrontendService
+ v4.7
+ 512
+ true
+
+
+
+ AnyCPU
+ true
+ full
+ false
+ bin\
+ DEBUG;TRACE
+ prompt
+ 4
+ false
+
+
+ AnyCPU
+ pdbonly
+ true
+ bin\
+ TRACE
+ prompt
+ 4
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {965791bf-8f77-4a69-97e2-8b6b66cf9863}
+ WebSocketRPC.JS
+
+
+ {ab4a5ddf-1f91-4af8-9e9c-242832576c5e}
+ WebsocketRPC
+
+
+
+
\ No newline at end of file
diff --git a/Samples/RelayConnectionSample/FrontendService/Program.cs b/Samples/RelayConnectionSample/FrontendService/Program.cs
new file mode 100644
index 0000000..2725f92
--- /dev/null
+++ b/Samples/RelayConnectionSample/FrontendService/Program.cs
@@ -0,0 +1,40 @@
+using System;
+using System.IO;
+using System.Threading;
+using WebsocketRPC;
+
+namespace FrontendService
+{
+ ///
+ /// Background service contract.
+ ///
+ public interface IBackgroundService
+ {
+ ///
+ /// Concatenates two strings.
+ ///
+ /// First string.
+ /// Second string.
+ /// Concatenated string.
+ string ConcatString(string a, string b);
+ }
+
+ public class Program
+ {
+ //if access denied execute: "netsh http delete urlacl url=http://+:8001/"
+ static void Main(string[] args)
+ {
+ //generate js code
+ File.WriteAllText($"../Site/{nameof(IBackgroundService)}.js", RPCJs.GenerateCallerWithDoc());
+
+ //start server and bind its local and remote API
+ var cts = new CancellationTokenSource();
+ Client.ConnectAsync("ws://localhost:9001/", cts.Token, c => c.Bind()).Wait(0); //this -> background
+ Server.ListenAsync("http://localhost:8001/", cts.Token, (c, ws) => c.Relay()).Wait(0); //client -> background rely
+
+ Console.Write("Running: '{0}'. Press [Enter] to exit.", nameof(FrontendService));
+ Console.ReadLine();
+ cts.Cancel();
+ }
+ }
+}
diff --git a/Samples/RelayConnectionSample/FrontendService/Site/Index.html b/Samples/RelayConnectionSample/FrontendService/Site/Index.html
new file mode 100644
index 0000000..5363500
--- /dev/null
+++ b/Samples/RelayConnectionSample/FrontendService/Site/Index.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Samples/RelayConnectionSample/Run.bat b/Samples/RelayConnectionSample/Run.bat
new file mode 100644
index 0000000..fa26f17
--- /dev/null
+++ b/Samples/RelayConnectionSample/Run.bat
@@ -0,0 +1,18 @@
+set bcgService = "BackgroundService\bin\BackgroundService.exe"
+set fntService = "FrontendService\bin\FrontendService.exe"
+
+if NOT EXIST %bcgService% (
+ echo Build 'BackgroundService' project first.
+ goto :eof
+)
+
+if NOT EXIST %fntService% (
+ echo Build 'FrontendService' project first.
+ goto :eof
+)
+
+start %bcgService%
+timeout 1>nul
+start %fntService%
+timeout 1>nul
+start "FrontendService\Site\Index.html"
\ No newline at end of file
diff --git a/Samples/Serialization/Program.cs b/Samples/Serialization/Program.cs
new file mode 100644
index 0000000..a823935
--- /dev/null
+++ b/Samples/Serialization/Program.cs
@@ -0,0 +1,92 @@
+using DotImaging;
+using Newtonsoft.Json;
+using System;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using WebsocketRPC;
+using System.Runtime.CompilerServices;
+
+namespace ServerClientJsSerialization
+{
+ class JpgBase64Converter : JsonConverter
+ {
+ private Type supportedType = typeof(Bgr[,]);
+
+ public override bool CanConvert(Type objectType)
+ {
+ return objectType.Equals(supportedType);
+ }
+
+ public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
+ {
+ var im = value as Bgr[,];
+ var bytes = im.EncodeAsJpeg();
+ var jsBase64Jpg = Convert.ToBase64String(bytes);
+
+ writer.WriteValue(jsBase64Jpg);
+ }
+
+ public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+ {
+ throw new NotSupportedException();
+ }
+
+ public override bool CanRead => false;
+ }
+
+ ///
+ /// The image processing service API.
+ ///
+ public class ImageProcessingAPI
+ {
+ const int CHANNEL_COUNT = 3;
+
+ ///
+ /// Swaps the channels for an image provided by a url.
+ ///
+ /// Channel ordering. Each value has to be [0..2] range.
+ /// Image url.
+ /// Processed image.
+ public Bgr[,] SwapImageChannels(Uri imgUrl, int[] order)
+ {
+ if (order.Any(x => x < 0 || x > CHANNEL_COUNT - 1))
+ throw new ArgumentException(String.Format("Each element of the channel order must be in: [{0}..{1}] range.", 0, CHANNEL_COUNT - 1));
+
+ Bgr[,] image = null;
+ try { image = imgUrl.GetBytes().DecodeAsColorImage(); }
+ catch { throw new Exception("The specified url does not point to a valid image."); }
+
+ image.Apply(c => swapChannels(c, order), inPlace: true);
+ return image;
+ }
+
+ unsafe Bgr swapChannels(Bgr c, int[] order)
+ {
+ var uC = (byte*)Unsafe.AsPointer(ref c);
+ var swapC = new Bgr(uC[order[0]], uC[order[1]], uC[order[2]]);
+ return swapC;
+ }
+ }
+
+ class Program
+ {
+ //if access denied execute: "netsh http delete urlacl url=http://+:8001/"
+ static void Main(string[] args)
+ {
+ RPCSettings.MaxMessageSize = 1 * 1024 * 1024; //1MiB
+ RPCSettings.AddConverter(new JpgBase64Converter());
+
+ //generate js code
+ File.WriteAllText($"../Site/{nameof(ImageProcessingAPI)}.js", RPCJs.GenerateCallerWithDoc());
+
+ //start server and bind its local and remote API
+ var cts = new CancellationTokenSource();
+ Server.ListenAsync("http://localhost:8001/", cts.Token, (c, ws) => c.Bind(new ImageProcessingAPI())).Wait(0);
+
+ Console.Write("Running: '{0}'. Press [Enter] to exit.", nameof(ServerClientJsSerialization));
+ Console.ReadLine();
+ cts.Cancel();
+ }
+ }
+}
diff --git a/Samples/Serialization/Serialization.csproj b/Samples/Serialization/Serialization.csproj
new file mode 100644
index 0000000..e76d994
--- /dev/null
+++ b/Samples/Serialization/Serialization.csproj
@@ -0,0 +1,97 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {9F82F6DD-238B-4F65-A95C-55F2BA20F1B0}
+ Exe
+ ServerClientJsSerialization
+ ServerClientJsSerialization
+ v4.7
+ 512
+ true
+
+
+
+
+
+ AnyCPU
+ true
+ full
+ false
+ bin\
+ DEBUG;TRACE
+ prompt
+ 4
+ false
+ true
+ bin\ServerClientJsSerialization.xml
+
+
+ AnyCPU
+ pdbonly
+ true
+ bin\
+ TRACE
+ prompt
+ 4
+ false
+ true
+ bin\ServerClientJsSerialization.xml
+
+
+
+ ..\..\packages\DotImaging.GenericImage.4.8.3\lib\net45\DotImaging.GenericImage.dll
+
+
+ ..\..\packages\DotImaging.IO.4.8.3\lib\net45\DotImaging.IO.dll
+
+
+ ..\..\packages\DotImaging.IO.Web.4.8.3\lib\net45\DotImaging.IO.Web.dll
+
+
+ ..\..\packages\DotImaging.Primitives2D.4.8.3\lib\net45\DotImaging.Primitives2D.dll
+
+
+ ..\..\packages\VideoLibrary.1.3.3\lib\portable-net45+win+wpa81+MonoAndroid10+xamarinios10+MonoTouch10\libvideo.dll
+
+
+ ..\..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll
+
+
+
+ ..\..\packages\System.Runtime.CompilerServices.Unsafe.4.4.0\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll
+
+
+
+
+
+
+
+ {965791bf-8f77-4a69-97e2-8b6b66cf9863}
+ WebSocketRPC.JS
+
+
+ {ab4a5ddf-1f91-4af8-9e9c-242832576c5e}
+ WebsocketRPC
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
+
+
+
+
\ No newline at end of file
diff --git a/Samples/Serialization/Site/Index.html b/Samples/Serialization/Site/Index.html
new file mode 100644
index 0000000..928c6b6
--- /dev/null
+++ b/Samples/Serialization/Site/Index.html
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Samples/Serialization/packages.config b/Samples/Serialization/packages.config
new file mode 100644
index 0000000..d601900
--- /dev/null
+++ b/Samples/Serialization/packages.config
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Samples/ServerClientSample/Client/Client.csproj b/Samples/ServerClientSample/Client/Client.csproj
new file mode 100644
index 0000000..4dfa062
--- /dev/null
+++ b/Samples/ServerClientSample/Client/Client.csproj
@@ -0,0 +1,54 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {E0DBB446-3B2B-4BC2-871F-925AB60917E3}
+ Exe
+ Properties
+ Client
+ Client
+ v4.7
+ 512
+
+
+
+ true
+ full
+ false
+ bin\
+ DEBUG;TRACE
+ prompt
+ 4
+ false
+
+
+ pdbonly
+ true
+ bin\
+ TRACE
+ prompt
+ 4
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+ {ab4a5ddf-1f91-4af8-9e9c-242832576c5e}
+ WebsocketRPC
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Samples/ServerClientSample/Client/Program.cs b/Samples/ServerClientSample/Client/Program.cs
new file mode 100644
index 0000000..16b903f
--- /dev/null
+++ b/Samples/ServerClientSample/Client/Program.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Linq;
+using System.Threading;
+using WebsocketRPC;
+
+namespace TestClient
+{
+ public interface ILocalAPI
+ {
+ int LongRunningTask(int a, int b);
+ }
+
+ public class RemoteAPI //:IRemoteAPI
+ {
+ public bool WriteProgress(float progress)
+ {
+ Console.WriteLine("\rCompleted: {0}%.", progress * 100);
+ return true;
+ }
+ }
+
+ public class Program
+ {
+ //if access denied execute: "netsh http delete urlacl url=http://+:8001/"
+ static void Main(string[] args)
+ {
+ //Debug.Listeners.Add(new TextWriterTraceListener(Console.Out));
+
+ Console.ForegroundColor = ConsoleColor.Green;
+ Console.WriteLine("Client\n");
+
+ //start client and bind to its local API
+ var cts = new CancellationTokenSource();
+ Client.ConnectAsync("ws://localhost:8001/", cts.Token, c =>
+ {
+ c.Bind(new RemoteAPI());
+ c.OnOpen += async () =>
+ {
+ var r = await RPC.For().CallAsync(x => x.LongRunningTask(5, 3));
+ Console.WriteLine("Result: " + r.First());
+ };
+ })
+ .Wait(0);
+
+ Console.Write("Running: '{0}'. Press [Enter] to exit.", nameof(TestClient));
+ Console.ReadLine();
+ cts.Cancel();
+ }
+ }
+}
diff --git a/Samples/ServerClientSample/Run.bat b/Samples/ServerClientSample/Run.bat
new file mode 100644
index 0000000..85a42b0
--- /dev/null
+++ b/Samples/ServerClientSample/Run.bat
@@ -0,0 +1,16 @@
+set serverApp = "Server\bin\Server.exe"
+set clientApp = "Server\bin\Client.exe"
+
+if NOT EXIST %serverApp% (
+ echo Build 'Server' project first.
+ goto :eof
+)
+
+if NOT EXIST %clientApp% (
+ echo Build 'Client' project first.
+ goto :eof
+)
+
+start %serverApp%
+timeout 1>nul
+start %clientApp%
\ No newline at end of file
diff --git a/Samples/ServerClientSample/Server/Program.cs b/Samples/ServerClientSample/Server/Program.cs
new file mode 100644
index 0000000..659ab05
--- /dev/null
+++ b/Samples/ServerClientSample/Server/Program.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using WebsocketRPC;
+
+namespace TestServer
+{
+ interface IRemoteAPI
+ {
+ bool WriteProgress(float progress);
+ }
+
+ class LocalAPI
+ {
+ public async Task LongRunningTask(int a, int b)
+ {
+ for (var p = 0; p <= 100; p += 5)
+ {
+ await Task.Delay(250);
+ await RPC.For(this).CallAsync(x => x.WriteProgress((float)p / 100));
+ }
+
+ return a + b;
+ }
+ }
+
+ public class Program
+ {
+ //if access denied execute: "netsh http delete urlacl url=http://+:8001/"
+ static void Main(string[] args)
+ {
+ Console.ForegroundColor = ConsoleColor.Red;
+ Console.WriteLine("Server\n");
+
+ //start server and bind to its local and remote API
+ var cts = new CancellationTokenSource();
+ Server.ListenAsync("http://localhost:8001/", cts.Token, (c, wc) => c.Bind(new LocalAPI())).Wait(0);
+
+ Console.Write("Running: '{0}'. Press [Enter] to exit.", nameof(TestServer));
+ Console.ReadLine();
+ cts.Cancel();
+ }
+ }
+}
diff --git a/Samples/ServerClientSample/Server/Server.csproj b/Samples/ServerClientSample/Server/Server.csproj
new file mode 100644
index 0000000..fc26b37
--- /dev/null
+++ b/Samples/ServerClientSample/Server/Server.csproj
@@ -0,0 +1,54 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {841054C8-559B-4E6F-8DCD-44C2D3DA2F0B}
+ Exe
+ Properties
+ Server
+ Server
+ v4.7
+ 512
+
+
+
+ true
+ full
+ false
+ bin\
+ DEBUG;TRACE
+ prompt
+ 4
+ false
+
+
+ pdbonly
+ true
+ bin\
+ TRACE
+ prompt
+ 4
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+ {ab4a5ddf-1f91-4af8-9e9c-242832576c5e}
+ WebsocketRPC
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Source/WebSocketRPC.JS/ClientAPIBase.cs b/Source/WebSocketRPC.JS/ClientAPIBase.cs
new file mode 100644
index 0000000..7ccfb71
--- /dev/null
+++ b/Source/WebSocketRPC.JS/ClientAPIBase.cs
@@ -0,0 +1,15 @@
+using System.IO;
+using Microsoft.Azure.WebJobs;
+using Microsoft.Azure.WebJobs.Host;
+
+namespace WebSocketRPC.JS
+{
+ public static class ClientAPIBase
+ {
+ [FunctionName("ClientAPIBase")]
+ public static void Run([BlobTrigger("samples-workitems/{name}", Connection = "")]Stream myBlob, string name, TraceWriter log)
+ {
+ log.Info($"C# Blob trigger function Processed blob\n Name:{name} \n Size: {myBlob.Length} Bytes");
+ }
+ }
+}
diff --git a/Source/WebSocketRPC.JS/ClientAPIBase.js b/Source/WebSocketRPC.JS/ClientAPIBase.js
new file mode 100644
index 0000000..7e492cb
--- /dev/null
+++ b/Source/WebSocketRPC.JS/ClientAPIBase.js
@@ -0,0 +1,160 @@
+//auto-generated using WebSocketRPC.JS library
+
+//from client
+var registeredFunctions = [];
+function callRPC(funcName, funcArgVals)
+{
+ if (ws === null) return;
+
+ if (!!registeredFunctions[funcName] === false)
+ {
+ registeredFunctions[funcName] = { FunctionName: funcName, OnReturn: null, OnError: null /*used by Promise*/ };
+ }
+
+ var promise = new Promise((resolve, reject) =>
+ {
+ registeredFunctions[funcName].OnReturn = resolve;
+ registeredFunctions[funcName].OnError = reject;
+ });
+
+ ws.send(JSON.stringify({ FunctionName: funcName, Arguments: funcArgVals }));
+ return promise;
+}
+
+function onRPCResponse(data)
+{
+ if (!!data.Error && data.Error)
+ registeredFunctions[data.FunctionName].OnError(data.Error);
+ else
+ registeredFunctions[data.FunctionName].OnReturn(data.ReturnValue);
+}
+
+//to client
+function onCallRequest(data, onError)
+{
+ var jsonFName = data.FunctionName[0].toLowerCase() + data.FunctionName.substring(1);
+ var isFunc = typeof obj[jsonFName] === 'function';
+ if (!isFunc)
+ {
+ onError({ title: "METHOD_NOT_IMPLEMENTED", summary: "The requested method is not implemented: " + jsonFName + "." });
+ return;
+ }
+
+ var r = obj[jsonFName].apply(obj, data.Arguments);
+ ws.send(JSON.stringify({ FunctionName: data.FunctionName, ReturnValue: r }));
+}
+
+function getAllFunctions(obj)
+{
+ var funcs = {};
+ for (var f in obj)
+ {
+ if (typeof obj[f] === 'function')
+ funcs[f] = obj[f];
+ }
+
+ return funcs;
+}
+
+//common
+function onMessage(msg, onError)
+{
+ var data = null;
+ try { data = JSON.parse(msg.data); } catch (e) { }
+
+ var isRPCResponse = (data !== null) && !!registeredFunctions[data.FunctionName] && (!!data.ReturnValue || !!data.Error);
+ var isCallRequest = (data !== null) && !!data.Arguments;
+
+ if (isRPCResponse)
+ onRPCResponse(data);
+ else if (isCallRequest)
+ onCallRequest(data, onError);
+ else if (obj.onOtherMessage)
+ obj.onOtherMessage(msg.data);
+}
+
+/**
+* {Establishes the connection with the web-socket server.}
+* @param {func} {Callback called when connection is established. Args: .}
+* @param {func} {Callback called if error happens. Args: .}
+* @param {func} {Callback called when connection is closed. Args: .}
+* @return {void} {}
+*/
+this.connect = function (onOpen, onError, onClose)
+{
+ if (!!window.WebSocket === false)
+ onError({ id: -1, title: "SOCKET_NOSUPPORT", summary: "This browser does not support Web sockets." });
+
+ //reset
+ if (ws)
+ {
+ ws.close();
+ ws = null;
+ }
+
+ ws = new WebSocket(url);
+
+ ws.onopen = onOpen;
+ ws.onmessage = msg => onMessage(msg, onError);
+
+ ws.onerror = function (err)
+ {
+ if (ws.readyState === 1)
+ onError({ id: -1, title: "SOCKET_ERR", summary: err }); //if normal error (connection error is handled in onclose)
+ };
+
+ ws.onclose = function (evt)
+ {
+ switch (evt.code)
+ {
+ case 1000:
+ onClose({ id: evt.code, closeReason: evt.reason || "normal", summary: "Websocket connection was closed." });
+ break;
+ case 1006:
+ onClose({ id: evt.code, closeReason: evt.reason || "abnormal", summary: "Websocket connection was closed abnormally." });
+ break;
+ case 1008:
+ onClose({ id: evt.code, closeReason: evt.reason || "policy violation", summary: "Websocket connection was closed due policy violation." });
+ break;
+ case 1009:
+ onClose({ id: evt.code, closeReason: evt.reason ||"message too big", summary: "Websocket connection was closed due too large message." });
+ break;
+ case 3001:
+ break; //nothing
+ default:
+ onClose({ id: evt.code, closeReason: evt.reason || "undefined", summary: "Websocket connection was closed (error code: " + evt.code + ")." });
+ }
+
+ ws = null;
+ };
+};
+
+var ws = null;
+var obj = this;
+
+/*
+ * Action triggered if the client receives a user-defined message (excluding RPC messages)
+ * @param {Message} - message.
+*/
+this.onOtherMessage = null;
+
+/*
+ * Send the message using the underlying websocket connection.
+ * @param {Message} - message.
+*/
+this.send = (msg) => ws.send(msg);
+
+/*
+ * CLoses the underlying websocket connection.
+ * @param {number} - Status code (see https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Status_codes for details).
+ * @param {string} - Human readable close reason (the max length is 123 bytes / ASCII characters).
+*/
+this.close = (code, closeReason) =>
+{
+ code = (code === undefined) ? 1000 : code;
+ closeReason = closeReason || "";
+
+ ws.close(code, closeReason);
+}
+
+/************************** API **************************/
\ No newline at end of file
diff --git a/Source/WebSocketRPC.JS/Components/JsCallerGenerator.cs b/Source/WebSocketRPC.JS/Components/JsCallerGenerator.cs
new file mode 100644
index 0000000..297bc6b
--- /dev/null
+++ b/Source/WebSocketRPC.JS/Components/JsCallerGenerator.cs
@@ -0,0 +1,131 @@
+#region License
+// Copyright © 2017 Darko Jurić
+//
+// 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.
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Text;
+
+namespace WebsocketRPC
+{
+ static class JsCallerGenerator
+ {
+ public static (string, MethodInfo[]) GetMethods(params Expression>[] omittedMethods)
+ {
+ //get omitted methods
+ var omittedMethodNames = new HashSet();
+ foreach (var oM in omittedMethods)
+ {
+ var mInfo = (oM.Body as MethodCallExpression)?.Method;
+ if (mInfo == null)
+ continue;
+
+ omittedMethodNames.Add(mInfo.Name);
+ }
+
+ //get methods
+ var objType = typeof(T);
+ var methodList = objType.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
+
+ var overloadedMethodNames = methodList.GroupBy(x => x.Name)
+ .DefaultIfEmpty()
+ .Where(x => x.Count() > 1)
+ .Select(x => x.Key);
+
+ if (overloadedMethodNames.Any())
+ throw new NotSupportedException("Overloaded functions are not supported: " + String.Join(", ", overloadedMethodNames));
+
+
+ methodList = methodList.Where(x => !omittedMethodNames.Contains(x.Name))
+ .ToArray();
+
+ return (objType.Name, methodList);
+ }
+
+ public static string GenerateHeader(string className)
+ {
+ var t = new string[] {
+ $"function {className}(url)",
+ $"{{"
+ };
+
+ var sb = new StringBuilder();
+ sb.Append(String.Join(Environment.NewLine, t));
+ sb.Append(Environment.NewLine);
+
+ //API base
+ var assembly = Assembly.GetExecutingAssembly();
+ var resourceName = assembly.GetName().Name + "." + "ClientAPIBase.js";
+
+ var lines = new List();
+ using (Stream stream = assembly.GetManifestResourceStream(resourceName))
+ using (StreamReader reader = new StreamReader(stream))
+ {
+ string l = null;
+ while ((l = reader.ReadLine()) != null)
+ lines.Add(l);
+ }
+
+ var apiBase = String.Join(Environment.NewLine, lines.Select(x => "\t" + x));
+ sb.Append(apiBase);
+ sb.Append(Environment.NewLine);
+
+ return sb.ToString();
+ }
+
+ public static string GenerateMethod(string methodName, string[] argNames)
+ {
+ var jsMName = Char.ToLower(methodName.First()) + methodName.Substring(1);
+ var argList = String.Join(", ", argNames);
+
+ var t = new string[] {
+ $"\t this.{jsMName} = function({argList}) {{",
+ $"\t return callRPC(\"{methodName}\", Object.values(this.{jsMName}.arguments));",
+ $"\t }};",
+ $""
+ };
+
+ var sb = new StringBuilder();
+ sb.Append(String.Join(Environment.NewLine, t));
+
+ return sb.ToString();
+ }
+
+ public static string GenerateFooter()
+ {
+ var t = new string[] {
+ $"}}"
+ };
+
+ var sb = new StringBuilder();
+ sb.Append(String.Join(Environment.NewLine, t));
+
+ return sb.ToString();
+ }
+ }
+}
diff --git a/Source/WebSocketRPC.JS/Components/JsDocGenerator.cs b/Source/WebSocketRPC.JS/Components/JsDocGenerator.cs
new file mode 100644
index 0000000..9b54814
--- /dev/null
+++ b/Source/WebSocketRPC.JS/Components/JsDocGenerator.cs
@@ -0,0 +1,175 @@
+#region License
+// Copyright © 2017 Darko Jurić
+//
+// 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.
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Xml;
+
+namespace WebsocketRPC
+{
+ static class JsDocGenerator
+ {
+ public static XmlNodeList GetMemberNodes(string xmlDocPath)
+ {
+ if (!File.Exists(xmlDocPath))
+ throw new FileNotFoundException("The provided xml-doc path points to a non-existent file.");
+
+ var xmlDoc = new XmlDocument();
+ xmlDoc.Load(xmlDocPath);
+
+ var mElems = xmlDoc.GetElementsByTagName("members");
+ if (mElems.Count != 1)
+ return mElems; //as I can not construct empty list
+
+ return mElems[0].ChildNodes;
+ }
+
+ public static string GetClassDoc(XmlNodeList members, string className)
+ {
+ string s = null;
+ for (int eIdx = 0; eIdx < members.Count; eIdx++)
+ {
+ var n = members[eIdx];
+ if (n.Name != "member" || (bool)n.Attributes["name"]?.Value?.Contains(className) == false)
+ continue;
+
+ s = n.InnerText.Trim('\r', '\n', ' ');
+ break;
+ }
+
+ if (s == null)
+ return String.Empty;
+
+ var jsDoc = new StringBuilder();
+ jsDoc.AppendLine("/**");
+ jsDoc.AppendLine(String.Format(" * {0}", s));
+ jsDoc.AppendLine(" * @constructor");
+ jsDoc.AppendLine("*/");
+
+ return jsDoc.ToString();
+ }
+
+ public static string GetMethodDoc(XmlNodeList mmebers, string methodName,
+ IList pNames, IList pTypes, Type returnType, string linePrefix = "\t")
+ {
+ var mElem = getMethod(mmebers, methodName);
+
+ var s = getSummary(mElem);
+ var p = getParams(mElem);
+ var r = getReturn(mElem);
+
+ var jsDoc = new StringBuilder();
+ jsDoc.AppendLine();
+ jsDoc.AppendLine(String.Format("{0}/**", linePrefix));
+ {
+ jsDoc.AppendLine(String.Format("{0} * @description - {1}", linePrefix, s));
+ jsDoc.AppendLine(String.Format("{0} *", linePrefix));
+
+ for (int i = 0; i < pNames.Count; i++)
+ {
+ if (!p.ContainsKey(pNames[i]))
+ continue;
+
+ jsDoc.AppendLine(String.Format("{0} * @param {{{1}}} - {2}", linePrefix, pTypes[i].Name, p[pNames[i]]));
+ }
+
+ jsDoc.AppendLine(String.Format("{0} * @returns {{{1}}} - {2}", linePrefix, returnType.Name, r));
+ }
+ jsDoc.AppendLine(String.Format("{0}*/", linePrefix));
+
+ return jsDoc.ToString();
+ }
+
+ static XmlNode getMethod(XmlNodeList nodes, string mName)
+ {
+ for (int eIdx = 0; eIdx < nodes.Count; eIdx++)
+ {
+ var n = nodes[eIdx];
+ if (n.Name != "member" || (bool)n.Attributes["name"]?.Value?.Contains(mName) == false)
+ continue;
+
+ return n;
+ }
+
+ return null;
+ }
+
+ static string getSummary(XmlNode node)
+ {
+ string s = String.Empty;
+
+ for (int eIdx = 0; eIdx < node.ChildNodes.Count; eIdx++)
+ {
+ var n = node.ChildNodes[eIdx];
+ if (n.Name != "summary")
+ continue;
+
+ s = n.InnerText.Trim('\r', '\n', ' ');
+ break;
+ }
+
+ return s;
+ }
+
+ static Dictionary getParams(XmlNode node)
+ {
+ var pInfos = new Dictionary();
+
+ for (int eIdx = 0; eIdx < node.ChildNodes.Count; eIdx++)
+ {
+ var n = node.ChildNodes[eIdx];
+
+ if (n.Name != "param")
+ continue;
+
+ var pName = n.Attributes["name"]?.Value;
+ var pDesc = n.InnerText;
+
+ pInfos.Add(pName, pDesc);
+ }
+
+ return pInfos;
+ }
+
+ static string getReturn(XmlNode node)
+ {
+ string s = String.Empty;
+
+ for (int eIdx = 0; eIdx < node.ChildNodes.Count; eIdx++)
+ {
+ var n = node.ChildNodes[eIdx];
+ if (n.Name != "returns")
+ continue;
+
+ s = n.InnerText.Trim('\r', '\n', ' ');
+ break;
+ }
+
+ return s;
+ }
+ }
+}
diff --git a/Source/WebSocketRPC.JS/RPCJs.cs b/Source/WebSocketRPC.JS/RPCJs.cs
new file mode 100644
index 0000000..90b72cf
--- /dev/null
+++ b/Source/WebSocketRPC.JS/RPCJs.cs
@@ -0,0 +1,116 @@
+#region License
+// Copyright © 2017 Darko Jurić
+//
+// 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.
+#endregion
+
+using System;
+using System.IO;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Text;
+
+namespace WebsocketRPC
+{
+ ///
+ /// Contains function related to Javascript RPC.
+ ///
+ public static class RPCJs
+ {
+ ///
+ /// Generates Javascript code from the provided class or interface type.
+ ///
+ /// Class or interface type.
+ /// The methods of the class / interface that should be omitted when creating the Javascript code.
+ /// Javascript API.
+ public static string GenerateCaller(params Expression>[] omittedMethods)
+ {
+ var (tName, mInfos) = JsCallerGenerator.GetMethods(omittedMethods);
+
+ var sb = new StringBuilder();
+ sb.Append(JsCallerGenerator.GenerateHeader(tName));
+
+ foreach (var m in mInfos)
+ {
+ var mName = m.Name;
+ var pNames = m.GetParameters().Select(x => x.Name).ToArray();
+
+ sb.Append(JsCallerGenerator.GenerateMethod(mName, pNames));
+ }
+
+ sb.Append(JsCallerGenerator.GenerateFooter());
+ return sb.ToString();
+ }
+
+ ///
+ /// Generates Javascript code including JsDoc comments from the provided class or interface type.
+ ///
+ /// Class or interface type.
+ /// Xml assembly definition file.
+ /// The methods of the class / interface that should be omitted when creating the Javascript code.
+ /// Javascript API.
+ public static string GenerateCallerWithDoc(string xmlDocPath, params Expression>[] omittedMethods)
+ {
+ var (tName, mInfos) = JsCallerGenerator.GetMethods(omittedMethods);
+
+ var xmlMemberNodes = JsDocGenerator.GetMemberNodes(xmlDocPath);
+ var sb = new StringBuilder();
+
+ sb.Append(JsDocGenerator.GetClassDoc(xmlMemberNodes, tName));
+ sb.Append(JsCallerGenerator.GenerateHeader(tName));
+
+ foreach (var m in mInfos)
+ {
+ var mName = m.Name;
+ var pTypes = m.GetParameters().Select(x => x.ParameterType).ToArray();
+ var pNames = m.GetParameters().Select(x => x.Name).ToArray();
+
+ sb.Append(JsDocGenerator.GetMethodDoc(xmlMemberNodes, mName, pNames, pTypes, m.ReturnType));
+ sb.Append(JsCallerGenerator.GenerateMethod(mName, pNames));
+ }
+
+ sb.Append(JsCallerGenerator.GenerateFooter());
+ return sb.ToString();
+ }
+
+ ///
+ /// Generates Javascript code including JsDoc comments from the provided class or interface type.
+ /// The xml assembly definition is taken form the executing assembly if available.
+ ///
+ /// Class or interface type.
+ /// The methods of the class / interface that should be omitted when creating the Javascript code.
+ /// Javascript API.
+ public static string GenerateCallerWithDoc(params Expression>[] omittedMethods)
+ {
+ var assembly = Assembly.GetEntryAssembly();
+ var fInfo = new FileInfo(assembly.Location);
+
+ var xmlDocPath = Path.ChangeExtension(assembly.Location, ".xml");
+
+ if (!File.Exists(xmlDocPath))
+ return GenerateCaller(omittedMethods);
+ else
+ return GenerateCallerWithDoc(xmlDocPath, omittedMethods);
+ }
+ }
+}
diff --git a/Source/WebSocketRPC.JS/WebSocketRPC.JS.csproj b/Source/WebSocketRPC.JS/WebSocketRPC.JS.csproj
new file mode 100644
index 0000000..94d8a4a
--- /dev/null
+++ b/Source/WebSocketRPC.JS/WebSocketRPC.JS.csproj
@@ -0,0 +1,50 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {965791BF-8F77-4A69-97E2-8B6B66CF9863}
+ Library
+ Properties
+ WebSocketRPC.JS
+ WebSocketRPC.JS
+ v4.7
+ 512
+
+
+
+ true
+ full
+ false
+ bin\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ pdbonly
+ true
+ bin\
+ TRACE
+ prompt
+ 4
+ bin\WebSocketRPC.JS.xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Source/WebsocketRPC/ClientServer/Client.cs b/Source/WebsocketRPC/ClientServer/Client.cs
new file mode 100644
index 0000000..e8f0ef1
--- /dev/null
+++ b/Source/WebsocketRPC/ClientServer/Client.cs
@@ -0,0 +1,75 @@
+#region License
+// Copyright © 2017 Darko Jurić
+//
+// 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.
+#endregion
+
+using System;
+using System.Net.WebSockets;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace WebsocketRPC
+{
+ ///
+ /// Websocket client.
+ ///
+ public static class Client
+ {
+ ///
+ /// Creates and starts a new websocket listening client.
+ ///
+ /// The target uri of the format: "ws://(address)/[path]".
+ /// Cancellation token.
+ /// Action executed when connection is established.
+ /// Websocket option set method.
+ /// Client task.
+ public static async Task ConnectAsync(string uri, CancellationToken token, Action onConnection, Action setOptions = null)
+ {
+ ClientWebSocket webSocket = null;
+
+ try
+ {
+ webSocket = new ClientWebSocket();
+ setOptions?.Invoke(webSocket.Options);
+ await webSocket.ConnectAsync(new Uri(uri), token);
+ }
+ catch (Exception ex)
+ {
+ webSocket?.Dispose();
+ throw ex;
+ }
+
+ var connection = new Connection { Socket = webSocket, Cookies = webSocket.Options.Cookies?.GetCookies(new Uri(uri)) };
+ try
+ {
+ onConnection(connection);
+ connection.InvokeOpenAsync();
+ await Connection.ListenReceiveAsync(connection, token);
+ }
+ finally
+ {
+ webSocket?.Dispose();
+ }
+ }
+ }
+}
diff --git a/Source/WebsocketRPC/ClientServer/Connection.cs b/Source/WebsocketRPC/ClientServer/Connection.cs
new file mode 100644
index 0000000..7037e98
--- /dev/null
+++ b/Source/WebsocketRPC/ClientServer/Connection.cs
@@ -0,0 +1,210 @@
+#region License
+// Copyright © 2017 Darko Jurić
+//
+// 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.
+#endregion
+
+using System;
+using System.Diagnostics;
+using System.Net;
+using System.Net.WebSockets;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace WebsocketRPC
+{
+ ///
+ /// Represents the websocket connection.
+ ///
+ public class Connection
+ {
+ internal static int MaxMessageSize { get; set; } = 64 * 1024; //x KiB
+ static string messageToBig = "The message exceeds the maximum allowed message size: {0} bytes.";
+
+ ///
+ /// Gets or sets the underlying socket.
+ ///
+ internal WebSocket Socket { get; set; }
+ ///
+ /// Gets the cookie collection.
+ ///
+ public CookieCollection Cookies { get; internal set; }
+
+ ///
+ /// Message receive event. Args: message, is text message.
+ ///
+ public event Action, bool> OnReceive;
+ ///
+ /// Open event.
+ ///
+ public event Action OnOpen;
+ ///
+ /// Close event.
+ ///
+ public event Action OnClose;
+ ///
+ /// Error event Args: exception.
+ ///
+ public event Action OnError;
+
+ ///
+ /// Sends the specified data.
+ ///
+ /// Binary data to send.
+ /// True if the operation was successful, false otherwise.
+ public async Task SendAsync(ArraySegment data)
+ {
+ if (Socket.State != WebSocketState.Open)
+ return false;
+
+ if (data.Count >= MaxMessageSize)
+ {
+ await CloseAsync(WebSocketCloseStatus.MessageTooBig, String.Format(messageToBig, MaxMessageSize));
+ return false;
+ }
+
+ await Socket.SendAsync(data, WebSocketMessageType.Binary, true, CancellationToken.None);
+ return true;
+ }
+
+ ///
+ /// Sends the specified data.
+ ///
+ /// Text data to send.
+ /// String encoding.
+ /// True if the operation was successful, false otherwise.
+ public async Task SendAsync(string data, Encoding e)
+ {
+ if (Socket.State != WebSocketState.Open)
+ return false;
+
+ var bData = e.GetBytes(data);
+ var segment = new ArraySegment(bData, 0, bData.Length);
+ if (segment.Count >= MaxMessageSize)
+ {
+ await CloseAsync(WebSocketCloseStatus.MessageTooBig, String.Format(messageToBig, MaxMessageSize));
+ return false;
+ }
+
+ Debug.WriteLine("Sending: " + data);
+ await Socket.SendAsync(segment, WebSocketMessageType.Text, true, CancellationToken.None);
+ return true;
+ }
+
+ ///
+ /// Closes the connection.
+ ///
+ /// Close reason.
+ /// Status description.
+ /// Task.
+ public async Task CloseAsync(WebSocketCloseStatus closeStatus = WebSocketCloseStatus.NormalClosure, string statusDescription = "")
+ {
+ if (Socket.State != WebSocketState.Open)
+ return;
+
+ await Socket.CloseAsync(closeStatus, statusDescription, CancellationToken.None);
+ OnClose?.Invoke();
+ clearEvents();
+ }
+
+ ///
+ /// Listens for the receive messages for the specified connection.
+ ///
+ /// Connection.
+ /// Cancellation token.
+ /// Task.
+ internal static async Task ListenReceiveAsync(Connection connection, CancellationToken token)
+ {
+ var webSocket = connection.Socket;
+ using (var registration = token.Register(() => connection.CloseAsync().Wait()))
+ {
+ try
+ {
+ byte[] receiveBuffer = new byte[1024];
+
+ while (webSocket.State == WebSocketState.Open)
+ {
+ WebSocketReceiveResult receiveResult = null;
+ var count = 0;
+ do
+ {
+ receiveResult = await webSocket.ReceiveAsync(new ArraySegment(receiveBuffer), CancellationToken.None);
+ count += receiveResult.Count;
+
+ if (count >= MaxMessageSize)
+ {
+ await connection.CloseAsync(WebSocketCloseStatus.MessageTooBig, String.Format(messageToBig, MaxMessageSize));
+ return;
+ }
+ }
+ while (receiveResult?.EndOfMessage == false);
+
+
+ if (receiveResult.MessageType == WebSocketMessageType.Close)
+ {
+ await connection.CloseAsync();
+ }
+ else
+ {
+ Debug.WriteLine("Received: " + new ArraySegment(receiveBuffer, 0, count).ToString(Encoding.ASCII));
+ connection.OnReceive?.Invoke(new ArraySegment(receiveBuffer, 0, count), receiveResult.MessageType == WebSocketMessageType.Text);
+ }
+
+ if (token.IsCancellationRequested)
+ break;
+ }
+ }
+ catch (Exception ex)
+ {
+ while (ex.InnerException != null) ex = ex.InnerException;
+ connection.OnError?.Invoke(ex);
+ }
+ }
+ }
+
+ ///
+ /// Invokes the open event.
+ ///
+ internal void InvokeOpenAsync()
+ {
+ OnOpen?.Invoke();
+ }
+
+ ///
+ /// Invokes the error event.
+ ///
+ /// Exception.
+ internal void InvokeErrorAsync(Exception ex)
+ {
+ OnError?.Invoke(ex);
+ }
+
+ private void clearEvents()
+ {
+ OnOpen = null;
+ OnClose = null;
+ OnError = null;
+ OnReceive = null;
+ }
+ }
+}
diff --git a/Source/WebsocketRPC/ClientServer/Helper.cs b/Source/WebsocketRPC/ClientServer/Helper.cs
new file mode 100644
index 0000000..3368632
--- /dev/null
+++ b/Source/WebsocketRPC/ClientServer/Helper.cs
@@ -0,0 +1,48 @@
+#region License
+// Copyright © 2017 Darko Jurić
+//
+// 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.
+#endregion
+
+using System;
+using System.Text;
+
+namespace WebsocketRPC
+{
+ ///
+ /// Provides the helper methods.
+ ///
+ public static class HelperExtensions
+ {
+ ///
+ /// Converts the specified binary data to a string data using the specified encoding.
+ ///
+ /// Binary data.
+ /// Encoding.
+ /// Text data.
+ public static string ToString(this ArraySegment segment, Encoding e)
+ {
+ var str = e.GetString(segment.Array, segment.Offset, segment.Count);
+ return str;
+ }
+ }
+}
diff --git a/Source/WebsocketRPC/ClientServer/Server.cs b/Source/WebsocketRPC/ClientServer/Server.cs
new file mode 100644
index 0000000..b4367fa
--- /dev/null
+++ b/Source/WebsocketRPC/ClientServer/Server.cs
@@ -0,0 +1,125 @@
+#region License
+// Copyright © 2017 Darko Jurić
+//
+// 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.
+#endregion
+
+using System;
+using System.Net;
+using System.Net.WebSockets;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace WebsocketRPC
+{
+ //sample:
+ /*Server.ListenAsync("http://+:8001/", CancellationToken.None, c =>
+ {
+ c.OnOpenAsync = () => Task.Run(() => Console.WriteLine("Connection opened."));
+ c.OnErrorAsync = e => Task.Run(() => Console.WriteLine("Error: " + e.Message));
+ c.OnCloseAsync = () => Task.Run(() => Console.WriteLine("Connection closed."));
+
+ c.OnReceiveAsync = (s, d) => Task.Run(() => Console.WriteLine("Received: " + d.AsString(Encoding.UTF8)));
+ })
+ .Wait();*/
+
+ ///
+ /// Websocket server.
+ ///
+ public static class Server
+ {
+ ///
+ /// Creates and starts a new instance of the http / websocket server.
+ ///
+ /// The http/https URI listening prefix.
+ /// Cancellation token.
+ /// Action executed when connection is created.
+ /// Server task.
+ public static async Task ListenAsync(string httpListenerPrefix, CancellationToken token, Action onConnection)
+ {
+ var listener = new HttpListener();
+ listener.Prefixes.Add(httpListenerPrefix);
+ listener.Start();
+
+ while (true)
+ {
+ HttpListenerContext listenerContext = await listener.GetContextAsync();
+
+ if (listenerContext.Request.IsWebSocketRequest)
+ {
+ ListenAsync(listenerContext, token, onConnection).Wait(0);
+ }
+ else
+ {
+ listenerContext.Response.StatusCode = 400;
+ listenerContext.Response.Close();
+ }
+
+ if (token.IsCancellationRequested)
+ break;
+ }
+
+ listener.Stop();
+ }
+
+ ///
+ /// Creates and starts a new listening websocket from the provided http listener context.
+ /// In case of the invalid http request / websocket creation error, the http connection is closed.
+ ///
+ /// Http listener context.
+ /// Cancellation token.
+ /// Action executed when connection is created.
+ /// Websocket listener task.
+ public static async Task ListenAsync(HttpListenerContext listenerContext, CancellationToken token, Action onConnection)
+ {
+ if (!listenerContext.Request.IsWebSocketRequest)
+ return;
+
+ WebSocketContext webSocketContext = null;
+ WebSocket webSocket = null;
+ try
+ {
+ webSocketContext = await listenerContext.AcceptWebSocketAsync(subProtocol: null);
+ webSocket = webSocketContext.WebSocket;
+ }
+ catch (Exception)
+ {
+ listenerContext.Response.StatusCode = 500;
+ listenerContext.Response.Close();
+ return;
+ }
+
+ var connection = new Connection { Socket = webSocket, Cookies = webSocketContext.CookieCollection };
+ try
+ {
+ onConnection(connection, webSocketContext);
+ connection.InvokeOpenAsync();
+ await Connection.ListenReceiveAsync(connection, token);
+ }
+ finally
+ {
+ if (webSocket != null)
+ webSocket.Dispose();
+ }
+ }
+ }
+}
diff --git a/Source/WebsocketRPC/ConnectionBinders/Binder.cs b/Source/WebsocketRPC/ConnectionBinders/Binder.cs
new file mode 100644
index 0000000..46a2410
--- /dev/null
+++ b/Source/WebsocketRPC/ConnectionBinders/Binder.cs
@@ -0,0 +1,82 @@
+#region License
+// Copyright © 2017 Darko Jurić
+//
+// 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.
+#endregion
+
+using System;
+using System.Linq.Expressions;
+using System.Threading.Tasks;
+using static WebsocketRPC.RPCSettings;
+
+namespace WebsocketRPC
+{
+ class Binder : BinderBase, ILocalBinder, IRemoteBinder
+ {
+ LocalInvoker lInvoker = null;
+ RemoteInvoker rInvoker = null;
+
+ public Binder(Connection connection, TObj obj)
+ : base(connection)
+ {
+ lInvoker = new LocalInvoker();
+ rInvoker = new RemoteInvoker();
+ Object = obj;
+
+ Connection.OnOpen += () =>
+ {
+ rInvoker.Initialize(r => Connection.SendAsync(r.ToJson(), Encoding));
+ };
+
+ Connection.OnReceive += async (d, isText) =>
+ {
+ if (!isText)
+ return;
+
+ var msg = Request.FromJson(d.ToString(Encoding));
+ if (msg.IsEmpty) return;
+
+ var result = await lInvoker.InvokeAsync(Object, msg);
+ await Connection.SendAsync(result.ToJson(), Encoding);
+ };
+
+ Connection.OnReceive += async (d, isText) =>
+ {
+ if (!isText)
+ return;
+
+ var msg = Response.FromJson(d.ToString(Encoding));
+ if (msg.IsEmpty) return;
+
+ rInvoker.Receive(msg);
+ await Task.FromResult(0);
+ };
+ }
+
+ public TObj Object { get; private set; }
+
+ public async Task CallAsync(Expression> functionExpression)
+ {
+ return await rInvoker.InvokeAsync(functionExpression);
+ }
+ }
+}
diff --git a/Source/WebsocketRPC/ConnectionBinders/BinderBase.cs b/Source/WebsocketRPC/ConnectionBinders/BinderBase.cs
new file mode 100644
index 0000000..0c5add5
--- /dev/null
+++ b/Source/WebsocketRPC/ConnectionBinders/BinderBase.cs
@@ -0,0 +1,108 @@
+#region License
+// Copyright © 2017 Darko Jurić
+//
+// 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.
+#endregion
+
+using System;
+using System.Diagnostics;
+using System.Linq.Expressions;
+using System.Threading.Tasks;
+
+namespace WebsocketRPC
+{
+ ///
+ /// The base binder interface.
+ ///
+ public interface IBinder
+ {
+ ///
+ /// Gets the associated connection.
+ ///
+ Connection Connection { get; }
+ }
+
+ ///
+ /// Local binder interface for the specified object type.
+ ///
+ /// Object type.
+ public interface ILocalBinder : IBinder
+ {
+ ///
+ /// Gets the associated object.
+ ///
+ T Object { get; }
+ }
+
+ ///
+ /// Remote binder interface for the specified interface type.
+ ///
+ /// Interface type.
+ public interface IRemoteBinder : IBinder
+ {
+ ///
+ /// Calls the RPC method.
+ ///
+ /// Result.
+ /// Method getter.
+ /// RPC invoking task.
+ Task CallAsync(Expression> functionExpression);
+ }
+
+ ///
+ /// Relay connection binder for the specified interface type.
+ ///
+ /// Interface type.
+ public interface IRelayBinder : IBinder
+ { }
+
+ abstract class BinderBase : IBinder
+ {
+ public Connection Connection { get; private set; }
+
+ protected BinderBase(Connection connection)
+ {
+ Connection = connection;
+
+ Connection.OnOpen += () =>
+ {
+ Debug.WriteLine("Open");
+
+ RPC.AllBinders.Add(this);
+ };
+
+ Connection.OnClose += () =>
+ {
+ Debug.WriteLine("Close");
+
+ RPC.AllBinders.Remove(this);
+ };
+
+ Connection.OnError += e =>
+ {
+ Debug.WriteLine("Error");
+
+ RPC.AllBinders.Remove(this);
+ };
+ }
+ }
+}
diff --git a/Source/WebsocketRPC/ConnectionBinders/LocalBinder.cs b/Source/WebsocketRPC/ConnectionBinders/LocalBinder.cs
new file mode 100644
index 0000000..2223e97
--- /dev/null
+++ b/Source/WebsocketRPC/ConnectionBinders/LocalBinder.cs
@@ -0,0 +1,55 @@
+#region License
+// Copyright © 2017 Darko Jurić
+//
+// 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.
+#endregion
+
+using static WebsocketRPC.RPCSettings;
+
+namespace WebsocketRPC
+{
+ class LocalBinder : BinderBase, ILocalBinder
+ {
+ LocalInvoker lInvoker = null;
+
+ public LocalBinder(Connection connection, TObj obj)
+ : base(connection)
+ {
+ lInvoker = new LocalInvoker();
+ Object = obj;
+
+ Connection.OnReceive += async (d, isText) =>
+ {
+ if (!isText)
+ return;
+
+ var msg = Request.FromJson(d.ToString(Encoding));
+ if (msg.IsEmpty) return;
+
+ var result = await lInvoker.InvokeAsync(Object, msg);
+ await Connection.SendAsync(result.ToJson(), Encoding);
+ };
+ }
+
+ public TObj Object { get; private set; }
+ }
+}
diff --git a/Source/WebsocketRPC/ConnectionBinders/RelayBinder.cs b/Source/WebsocketRPC/ConnectionBinders/RelayBinder.cs
new file mode 100644
index 0000000..7bb0a89
--- /dev/null
+++ b/Source/WebsocketRPC/ConnectionBinders/RelayBinder.cs
@@ -0,0 +1,80 @@
+#region License
+// Copyright © 2017 Darko Jurić
+//
+// 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.
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.WebSockets;
+using System.Threading;
+
+namespace WebsocketRPC
+{
+ class RelayBinder: BinderBase, IRelayBinder
+ {
+ IRemoteBinder remoteBinder = null;
+
+ public RelayBinder(Connection connection, Func>> getRemoteBinders)
+ : base(connection)
+ {
+ Connection.OnOpen += async () =>
+ {
+ var rBinders = getRemoteBinders();
+ if (rBinders.Skip(1).Any()) //>1
+ {
+ var msg = "There are multiple target connections for a single relay connection.";
+ Connection.InvokeErrorAsync(new Exception(msg));
+ await Connection.CloseAsync(WebSocketCloseStatus.PolicyViolation, msg);
+ return;
+ }
+ else if (rBinders.Any() == false)
+ {
+ var msg = "There are no target connections for a single relay connection.";
+ Connection.InvokeErrorAsync(new Exception(msg));
+ await Connection.CloseAsync(WebSocketCloseStatus.PolicyViolation, msg);
+ return;
+ }
+
+ remoteBinder = rBinders.First();
+ remoteBinder.Connection.OnReceive += onReveiveResponse; //rely to the origin
+ };
+
+ Connection.OnReceive += async (d, isText) =>
+ {
+ await remoteBinder.Connection.SendAsync(d);
+ };
+
+ Connection.OnClose += () =>
+ {
+ remoteBinder.Connection.OnReceive -= onReveiveResponse;
+ };
+ }
+
+ async void onReveiveResponse(ArraySegment d, bool isText)
+ {
+ var msgType = isText ? WebSocketMessageType.Text : WebSocketMessageType.Binary;
+ await Connection.Socket.SendAsync(d, msgType, true, CancellationToken.None);
+ }
+ }
+}
diff --git a/Source/WebsocketRPC/ConnectionBinders/RemoteBinder.cs b/Source/WebsocketRPC/ConnectionBinders/RemoteBinder.cs
new file mode 100644
index 0000000..ecf352d
--- /dev/null
+++ b/Source/WebsocketRPC/ConnectionBinders/RemoteBinder.cs
@@ -0,0 +1,66 @@
+#region License
+// Copyright © 2017 Darko Jurić
+//
+// 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.
+#endregion
+
+using System;
+using System.Linq.Expressions;
+using System.Threading.Tasks;
+using static WebsocketRPC.RPCSettings;
+
+namespace WebsocketRPC
+{
+ class RemoteBinder : BinderBase, IRemoteBinder
+ {
+ RemoteInvoker rInvoker = null;
+
+ public RemoteBinder(Connection connection)
+ : base(connection)
+ {
+ rInvoker = new RemoteInvoker();
+
+ Connection.OnOpen += async () =>
+ {
+ rInvoker.Initialize(r => Connection.SendAsync(r.ToJson(), Encoding));
+ await Task.FromResult(0);
+ };
+
+ Connection.OnReceive += async (d, isText) =>
+ {
+ if (isText)
+ return;
+
+ var msg = Response.FromJson(d.ToString(Encoding));
+ if (msg.IsEmpty) return;
+
+ rInvoker.Receive(msg);
+ await Task.FromResult(0);
+ };
+ }
+
+ public async Task CallAsync(Expression> functionExpression)
+ {
+ return await rInvoker.InvokeAsync(functionExpression);
+ }
+ }
+}
diff --git a/Source/WebsocketRPC/Invokers/LocalInvoker.cs b/Source/WebsocketRPC/Invokers/LocalInvoker.cs
new file mode 100644
index 0000000..a68c4da
--- /dev/null
+++ b/Source/WebsocketRPC/Invokers/LocalInvoker.cs
@@ -0,0 +1,126 @@
+#region License
+// Copyright © 2017 Darko Jurić
+//
+// 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.
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using Newtonsoft.Json.Linq;
+using System.Runtime.CompilerServices;
+using System.Threading.Tasks;
+using NameInfoPairs = System.Collections.Generic.Dictionary;
+
+namespace WebsocketRPC
+{
+ class LocalInvoker
+ {
+ static Dictionary cache = new Dictionary();
+ NameInfoPairs methods;
+
+ public LocalInvoker()
+ {
+ cache.TryGetValue(typeof(TObj), out methods);
+ if (methods != null) return;
+
+ var methodList = typeof(TObj).GetMethods(BindingFlags.Public | BindingFlags.Instance);
+
+ //check constrains
+ verifyType(methodList);
+
+ //initialize and cache it
+ methods = methodList.ToDictionary(x => x.Name, x => x);
+ cache[typeof(TObj)] = methods;
+ }
+
+ static void verifyType(MethodInfo[] methodList)
+ {
+ //check constraints
+ if (typeof(TObj).IsInterface)
+ throw new Exception("The specified type must be a object type.");
+
+ var overloadedMethodNames = methodList.GroupBy(x => x.Name)
+ .DefaultIfEmpty()
+ .Where(x => x.Count() > 1)
+ .Select(x => x.Key);
+
+ if (overloadedMethodNames.Any())
+ throw new NotSupportedException("Overloaded functions are not supported: " + String.Join(", ", overloadedMethodNames));
+
+ var propertyList = typeof(TObj).GetProperties(BindingFlags.Public | BindingFlags.Instance);
+ if (propertyList.Any())
+ throw new NotSupportedException("The interface must not declare any properties: " + String.Join(", ", propertyList.Select(x => x.Name)) + ".");
+ }
+
+ public async Task InvokeAsync(TObj obj, Request clientMessage)
+ {
+ var (result, error) = await invokeAsync(obj, clientMessage.FunctionName, clientMessage.Arguments);
+
+ return new Response { FunctionName = clientMessage.FunctionName, ReturnValue = result, Error = error?.Message };
+ }
+
+ async Task<(JToken Result, Exception Error)> invokeAsync(TObj obj, string functionName, JToken[] args)
+ {
+ if (!methods.ContainsKey(functionName))
+ throw new ArgumentException(functionName + ": The object does not contain the provided method name: " + functionName + ".");
+
+ var methodParams = methods[functionName].GetParameters();
+ if (methodParams.Length != args.Length)
+ throw new ArgumentException(functionName + ": The number of provided parameters mismatches the number of required arguments.");
+
+ var argObjs = new object[args.Length];
+ for (int i = 0; i < methodParams.Length; i++)
+ argObjs[i] = args[i].ToObject(methodParams[i].ParameterType, RPCSettings.Serializer);
+
+ try
+ {
+ var returnVal = await invokeAsync(methods[functionName], obj, argObjs);
+ var result = (returnVal != null) ? JToken.FromObject(returnVal, RPCSettings.Serializer) : null;
+ return (result, null);
+ }
+ catch (Exception ex)
+ {
+ while (ex.InnerException != null) ex = ex.InnerException;
+ return (null, ex);
+ }
+ }
+
+ async Task invokeAsync(MethodInfo method, TObj obj, object[] args)
+ {
+ object returnVal = method.Invoke(obj, args);
+
+ //async method support
+ if (method.GetCustomAttribute() != null)
+ {
+ var task = (Task)returnVal;
+ await task.ConfigureAwait(false);
+
+ var resultProperty = task.GetType().GetProperty("Result");
+ returnVal = resultProperty.GetValue(task);
+ }
+
+ return returnVal;
+ }
+ }
+}
diff --git a/Source/WebsocketRPC/Invokers/Messages.cs b/Source/WebsocketRPC/Invokers/Messages.cs
new file mode 100644
index 0000000..2d6a53f
--- /dev/null
+++ b/Source/WebsocketRPC/Invokers/Messages.cs
@@ -0,0 +1,89 @@
+#region License
+// Copyright © 2017 Darko Jurić
+//
+// 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.
+#endregion
+
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using System.Linq;
+
+namespace WebsocketRPC
+{
+ struct Request
+ {
+ public string FunctionName;
+ public JToken[] Arguments;
+
+ public static Request FromJson(string json)
+ {
+ var root = JObject.Parse(json);
+ var r = new Request
+ {
+ FunctionName = root[nameof(FunctionName)]?.Value(),
+ Arguments = root[nameof(Arguments)]?.Children().ToArray()
+ };
+
+ if (r.FunctionName == null || r.Arguments == null)
+ return default(Request);
+
+ return r;
+ }
+
+ public string ToJson()
+ {
+ return JsonConvert.SerializeObject(this);
+ }
+
+ public bool IsEmpty => FunctionName == null && Arguments == null;
+ }
+
+ struct Response
+ {
+ public string FunctionName;
+ public JToken ReturnValue;
+ public string Error;
+
+ public static Response FromJson(string json)
+ {
+ var root = JObject.Parse(json);
+ var r = new Response
+ {
+ FunctionName = root[nameof(FunctionName)]?.Value(),
+ ReturnValue = root[nameof(ReturnValue)]?.Value(),
+ Error = root[nameof(Error)]?.Value()
+ };
+
+ if (r.FunctionName == null || r.ReturnValue == null)
+ return default(Response);
+
+ return r;
+ }
+
+ public string ToJson()
+ {
+ return JsonConvert.SerializeObject(this);
+ }
+
+ public bool IsEmpty => FunctionName == null && ReturnValue == null && Error == null;
+ }
+}
diff --git a/Source/WebsocketRPC/Invokers/RemoteInvoker.cs b/Source/WebsocketRPC/Invokers/RemoteInvoker.cs
new file mode 100644
index 0000000..561706a
--- /dev/null
+++ b/Source/WebsocketRPC/Invokers/RemoteInvoker.cs
@@ -0,0 +1,137 @@
+#region License
+// Copyright © 2017 Darko Jurić
+//
+// 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.
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Newtonsoft.Json.Linq;
+using System.Threading.Tasks;
+using System.Linq.Expressions;
+using System.Reflection;
+
+namespace WebsocketRPC
+{
+ class RemoteInvoker
+ {
+ static HashSet verifiedTypes = new HashSet();
+
+ Func sendAsync;
+ Dictionary> runningMethods;
+
+ public RemoteInvoker()
+ {
+ runningMethods = new Dictionary>();
+ if (verifiedTypes.Contains(typeof(TInterface))) return;
+
+ //verify constraints
+ verifyType();
+
+ //cache it
+ verifiedTypes.Add(typeof(TInterface));
+ }
+
+ static void verifyType()
+ {
+ if (!typeof(TInterface).IsInterface)
+ throw new Exception("The specified type must be an interface type.");
+
+ var methodList = typeof(TInterface).GetMethods(BindingFlags.Public | BindingFlags.Instance);
+
+ var asyncFuncs = methodList.Where(x => x.ReturnType.IsAssignableFrom(typeof(Task)));
+ if (asyncFuncs.Any())
+ throw new NotSupportedException("Functions returning Task are not supported: " + String.Join(", ", asyncFuncs.Select(x => x.Name)) +
+ ". Declare them as non async ones in the interface contract.");
+
+ var propertyList = typeof(TInterface).GetProperties(BindingFlags.Public | BindingFlags.Instance);
+ if (propertyList.Any())
+ throw new NotSupportedException("The interface must not declare any properties: " + String.Join(", ", propertyList.Select(x => x.Name)) + ".");
+ }
+
+
+ public void Initialize(Func sendAsync)
+ {
+ this.sendAsync = sendAsync;
+ }
+
+ public void Receive(Response response)
+ {
+ if (runningMethods.ContainsKey(response.FunctionName) == false)
+ return;
+
+ runningMethods[response.FunctionName].SetResult(response);
+ }
+
+ public async Task InvokeAsync(Expression> functionExpression)
+ {
+ if (sendAsync == null)
+ throw new Exception("The invoker is not initialized.");
+
+ var (funcName, argVals) = getFunctionInfo(functionExpression);
+ var r = await invokeAsync(funcName, argVals);
+ return r.Result;
+ }
+
+ async Task<(TResult Result, Exception Error)> invokeAsync(string name, params object[] args)
+ {
+ var msg = new Request
+ {
+ FunctionName = name,
+ Arguments = args.Select(a => JToken.FromObject(a, RPCSettings.Serializer)).ToArray()
+ };
+
+ runningMethods[name] = new TaskCompletionSource();
+ await sendAsync(msg);
+ await runningMethods[name].Task;
+
+ var response = runningMethods[name].Task.Result;
+ runningMethods.Remove(name);
+
+ var result = response.ReturnValue.ToObject(RPCSettings.Serializer);
+ var ex = response.Error;
+
+ return (result, (ex != null) ? new Exception(ex) : null);
+ }
+
+ //the idea is taken from: https://stackoverflow.com/questions/3766698/get-end-values-from-lambda-expressions-method-parameters?rq=1
+ static (string fName, object[] argVals) getFunctionInfo(Expression expression)
+ {
+ var call = expression.Body as MethodCallExpression;
+ if (call == null)
+ throw new ArgumentException("Not a method call: " + expression.Name);
+
+ var values = new List();
+ foreach (var argument in call.Arguments)
+ {
+ var lambda = Expression.Lambda(argument, expression.Parameters);
+ var value = lambda.Compile().DynamicInvoke(new object[1]);
+
+ values.Add(value);
+ }
+
+ var fName = ((MethodCallExpression)expression.Body).Method.Name;
+ return (fName, values.ToArray());
+ }
+ }
+}
diff --git a/Source/WebsocketRPC/RPC.cs b/Source/WebsocketRPC/RPC.cs
new file mode 100644
index 0000000..86aa11d
--- /dev/null
+++ b/Source/WebsocketRPC/RPC.cs
@@ -0,0 +1,164 @@
+#region License
+// Copyright © 2017 Darko Jurić
+//
+// 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.
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace WebsocketRPC
+{
+ ///
+ /// Provides methods for invoking the RPC.
+ ///
+ public static class RPC
+ {
+ ///
+ /// Gets the all binders.
+ ///
+ public static readonly List AllBinders = new List();
+
+ ///
+ /// Creates two-way RPC receiving-sending binding for the provided connection.
+ ///
+ /// Object type.
+ /// Interface type.
+ /// Existing connection to bind to.
+ /// Object to bind to.
+ /// Binder.
+ public static IBinder Bind(this Connection connection, TObj obj)
+ {
+ return new Binder(connection, obj);
+ }
+
+ ///
+ /// Creates one way RPC receiving binding for the provided connection.
+ ///
+ /// Object type.
+ /// Existing connection to bind to.
+ /// Object to bind to.
+ /// Binder.
+ public static IBinder Bind(this Connection connection, TObj obj)
+ {
+ return new LocalBinder(connection, obj);
+ }
+
+ ///
+ /// Creates one way RPC sending binding for the provided connection.
+ ///
+ /// Interface type.
+ /// Existing connection to bind to.
+ /// Binder.
+ public static IBinder Bind(this Connection connection)
+ {
+ return new RemoteBinder(connection);
+ }
+
+ ///
+ /// Gets all two-way or one-way sending binders.
+ ///
+ /// Interface type.
+ /// Binders.
+ public static IEnumerable> For()
+ {
+ return AllBinders.OfType>();
+ }
+
+ ///
+ /// Gets all two-way binders associated with the specified object.
+ ///
+ /// Interface type.
+ /// Target object.
+ /// Binders.
+ public static IEnumerable> For(object obj)
+ {
+ var lBinderType = typeof(ILocalBinder<>).MakeGenericType(obj.GetType());
+
+ var binders = AllBinders.OfType>()
+ .Where(x =>
+ {
+ var xType = x.GetType();
+
+ var isLocalBinder = lBinderType.IsAssignableFrom(xType);
+ if (!isLocalBinder) return false;
+
+ var isObjBinder = xType.GetProperty(nameof(ILocalBinder.Object)).GetValue(x, null) == obj;
+ return isObjBinder;
+ });
+
+ return binders;
+ }
+
+ ///
+ /// Calls the remote method.
+ ///
+ /// Interface type.
+ /// Method result type.
+ /// Remote binder collection.
+ /// Method getter.
+ /// The collection of the RPC invoking tasks.
+ public static async Task CallAsync(this IEnumerable> binders, Expression> functionExpression)
+ {
+ var tasks = new List>();
+ foreach (var b in binders)
+ {
+ var t = b.CallAsync(functionExpression);
+ tasks.Add(t);
+ }
+
+ await Task.WhenAll(tasks);
+ var results = tasks.Where(x => x.Status == TaskStatus.RanToCompletion)
+ .Select(x => x.Result)
+ .ToArray();
+
+ return results;
+ }
+
+ ///
+ /// Gets whether the data contain RPC message or not.
+ ///
+ /// Received data.
+ /// True if the data contain RPC message, false otherwise.
+ public static bool IsRPC(this ArraySegment data)
+ {
+ var str = data.ToString(Encoding.ASCII);
+ return !Request.FromJson(str).IsEmpty || !Response.FromJson(str).IsEmpty;
+ }
+
+ ///
+ /// Relays the incoming data for binders associated with the interface to the specified target connection.
+ /// The number of such binders must be equal to one, due 1:1 mapping.
+ ///
+ /// Interface tpe.
+ /// Target connection.
+ /// Binder.
+ public static IBinder Relay(this Connection connection)
+ {
+ return new RelayBinder(connection, () => AllBinders.OfType>());
+ }
+ }
+}
diff --git a/Source/WebsocketRPC/RPCSettings.cs b/Source/WebsocketRPC/RPCSettings.cs
new file mode 100644
index 0000000..cc5b644
--- /dev/null
+++ b/Source/WebsocketRPC/RPCSettings.cs
@@ -0,0 +1,95 @@
+#region License
+// Copyright © 2017 Darko Jurić
+//
+// 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.
+#endregion
+
+using Newtonsoft.Json;
+using Newtonsoft.Json.Serialization;
+using System;
+using System.Text;
+
+namespace WebsocketRPC
+{
+ ///
+ /// RPC settings.
+ ///
+ public static class RPCSettings
+ {
+ class CamelCaseExceptDictionaryKeysResolver : CamelCasePropertyNamesContractResolver
+ {
+ protected override JsonDictionaryContract CreateDictionaryContract(Type objectType)
+ {
+ JsonDictionaryContract contract = base.CreateDictionaryContract(objectType);
+
+ contract.DictionaryKeyResolver = propertyName => propertyName;
+
+ return contract;
+ }
+ }
+
+ ///
+ /// Gets the messaging serializer.
+ ///
+ internal static readonly JsonSerializer Serializer = JsonSerializer.Create(new JsonSerializerSettings { ContractResolver = new CamelCaseExceptDictionaryKeysResolver() });
+
+ ///
+ /// Adds the serialization type converter.
+ ///
+ /// Converter.
+ public static void AddConverter(JsonConverter converter)
+ {
+ Serializer.Converters.Add(converter);
+ }
+
+ ///
+ /// Gets or sets the maximum message size [1..inf].
+ ///
+ public static int MaxMessageSize
+ {
+ get { return Connection.MaxMessageSize; }
+ set
+ {
+ if (value <= 0)
+ throw new ArgumentOutOfRangeException("The message size must be set to a strictly positive value.");
+
+ Connection.MaxMessageSize = value;
+ }
+ }
+
+ static Encoding encoding = Encoding.ASCII;
+ ///
+ /// Gets or sets the string RPC messaging encoding.
+ ///
+ public static Encoding Encoding
+ {
+ get { return encoding; }
+ set
+ {
+ if (encoding == null)
+ throw new ArgumentException("The provided value must not be null.");
+
+ encoding = value;
+ }
+ }
+ }
+}
diff --git a/Source/WebsocketRPC/WebsocketRPC.csproj b/Source/WebsocketRPC/WebsocketRPC.csproj
new file mode 100644
index 0000000..c016772
--- /dev/null
+++ b/Source/WebsocketRPC/WebsocketRPC.csproj
@@ -0,0 +1,64 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {AB4A5DDF-1F91-4AF8-9E9C-242832576C5E}
+ Library
+ Properties
+ WebsocketRPC
+ WebsocketRPC
+ v4.7
+ 512
+
+
+
+ true
+ full
+ false
+ bin\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ pdbonly
+ true
+ bin\
+ TRACE
+ prompt
+ 4
+ bin\WebsocketRPC.xml
+
+
+
+ ..\..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Source/WebsocketRPC/packages.config b/Source/WebsocketRPC/packages.config
new file mode 100644
index 0000000..e157ba1
--- /dev/null
+++ b/Source/WebsocketRPC/packages.config
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/WebsocketRPC.sln b/WebsocketRPC.sln
new file mode 100644
index 0000000..dbc8a3b
--- /dev/null
+++ b/WebsocketRPC.sln
@@ -0,0 +1,138 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 15
+VisualStudioVersion = 15.0.26730.16
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Source", "Source", "{3CE373E0-6D9A-48C4-808D-510E6FB69D5C}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{E5509F73-1E5E-45B4-AED7-4A38F8DF1DDE}"
+ ProjectSection(SolutionItems) = preProject
+ Samples\README.md = Samples\README.md
+ EndProjectSection
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebsocketRPC", "Source\WebsocketRPC\WebsocketRPC.csproj", "{AB4A5DDF-1F91-4AF8-9E9C-242832576C5E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebSocketRPC.JS", "Source\WebSocketRPC.JS\WebSocketRPC.JS.csproj", "{965791BF-8F77-4A69-97E2-8B6B66CF9863}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ServerClientSample", "ServerClientSample", "{E4A64C0A-0127-42AD-A439-2063A145F536}"
+ ProjectSection(SolutionItems) = preProject
+ Samples\ServerClientSample\Run.bat = Samples\ServerClientSample\Run.bat
+ EndProjectSection
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServerClientJs", "Samples\ClientJs\ServerClientJs.csproj", "{C0449FA5-C667-4B5C-878A-04773903B130}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client", "Samples\ServerClientSample\Client\Client.csproj", "{E0DBB446-3B2B-4BC2-871F-925AB60917E3}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Server", "Samples\ServerClientSample\Server\Server.csproj", "{841054C8-559B-4E6F-8DCD-44C2D3DA2F0B}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{26CCD24E-8415-4FD5-8AB3-17433A265B08}"
+ ProjectSection(SolutionItems) = preProject
+ .nuget\NuGet.exe = .nuget\NuGet.exe
+ EndProjectSection
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MultiService", "Samples\MultiService\MultiService.csproj", "{2B69F62E-991C-4E4B-B1FF-2A5906415764}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RelayConnectionSample", "RelayConnectionSample", "{2D53E72C-CA18-43B0-8B22-628401177763}"
+ ProjectSection(SolutionItems) = preProject
+ Samples\RelayConnectionSample\Run.bat = Samples\RelayConnectionSample\Run.bat
+ EndProjectSection
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FrontendService", "Samples\RelayConnectionSample\FrontendService\FrontendService.csproj", "{79BCCE0B-3666-4866-AF36-8FA2C71479C0}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BackgroundService", "Samples\RelayConnectionSample\BackgroundService\BackgroundService.csproj", "{4616F442-466A-4B0E-8939-DA6367A5E365}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Serialization", "Samples\Serialization\Serialization.csproj", "{9F82F6DD-238B-4F65-A95C-55F2BA20F1B0}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{6B3680DB-211E-492C-BC67-46FF1F00A730}"
+ ProjectSection(SolutionItems) = preProject
+ LICENSE.md = LICENSE.md
+ README.md = README.md
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Deploy", "Deploy", "{B998D2DB-7CA7-4A1B-8B98-801973A12D20}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Logo", "Logo", "{6E575BA8-2787-4A5D-9742-8ACBA07C93D9}"
+ ProjectSection(SolutionItems) = preProject
+ Deploy\Logo\Logo-big.png = Deploy\Logo\Logo-big.png
+ Deploy\Logo\Logo-small.png = Deploy\Logo\Logo-small.png
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NuGet", "NuGet", "{CBCEF1A5-1044-49DB-8A19-2443FFBE3DFE}"
+ ProjectSection(SolutionItems) = preProject
+ Deploy\Nuget\Build.cmd = Deploy\Nuget\Build.cmd
+ Deploy\Nuget\Push.cmd = Deploy\Nuget\Push.cmd
+ Deploy\Nuget\UpdateNuGet.cmd = Deploy\Nuget\UpdateNuGet.cmd
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "nuSpecs", "nuSpecs", "{07BE9233-A701-4899-8FB0-21D8EE0057D3}"
+ ProjectSection(SolutionItems) = preProject
+ Deploy\Nuget\nuSpecs\WebsocketRPC.JS.nuspec = Deploy\Nuget\nuSpecs\WebsocketRPC.JS.nuspec
+ Deploy\Nuget\nuSpecs\WebsocketRPC.nuspec = Deploy\Nuget\nuSpecs\WebsocketRPC.nuspec
+ EndProjectSection
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {AB4A5DDF-1F91-4AF8-9E9C-242832576C5E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AB4A5DDF-1F91-4AF8-9E9C-242832576C5E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AB4A5DDF-1F91-4AF8-9E9C-242832576C5E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AB4A5DDF-1F91-4AF8-9E9C-242832576C5E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {965791BF-8F77-4A69-97E2-8B6B66CF9863}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {965791BF-8F77-4A69-97E2-8B6B66CF9863}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {965791BF-8F77-4A69-97E2-8B6B66CF9863}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {965791BF-8F77-4A69-97E2-8B6B66CF9863}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C0449FA5-C667-4B5C-878A-04773903B130}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C0449FA5-C667-4B5C-878A-04773903B130}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C0449FA5-C667-4B5C-878A-04773903B130}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C0449FA5-C667-4B5C-878A-04773903B130}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E0DBB446-3B2B-4BC2-871F-925AB60917E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E0DBB446-3B2B-4BC2-871F-925AB60917E3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E0DBB446-3B2B-4BC2-871F-925AB60917E3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E0DBB446-3B2B-4BC2-871F-925AB60917E3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {841054C8-559B-4E6F-8DCD-44C2D3DA2F0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {841054C8-559B-4E6F-8DCD-44C2D3DA2F0B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {841054C8-559B-4E6F-8DCD-44C2D3DA2F0B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {841054C8-559B-4E6F-8DCD-44C2D3DA2F0B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2B69F62E-991C-4E4B-B1FF-2A5906415764}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2B69F62E-991C-4E4B-B1FF-2A5906415764}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2B69F62E-991C-4E4B-B1FF-2A5906415764}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2B69F62E-991C-4E4B-B1FF-2A5906415764}.Release|Any CPU.Build.0 = Release|Any CPU
+ {79BCCE0B-3666-4866-AF36-8FA2C71479C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {79BCCE0B-3666-4866-AF36-8FA2C71479C0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {79BCCE0B-3666-4866-AF36-8FA2C71479C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {79BCCE0B-3666-4866-AF36-8FA2C71479C0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4616F442-466A-4B0E-8939-DA6367A5E365}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4616F442-466A-4B0E-8939-DA6367A5E365}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4616F442-466A-4B0E-8939-DA6367A5E365}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4616F442-466A-4B0E-8939-DA6367A5E365}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9F82F6DD-238B-4F65-A95C-55F2BA20F1B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9F82F6DD-238B-4F65-A95C-55F2BA20F1B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9F82F6DD-238B-4F65-A95C-55F2BA20F1B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9F82F6DD-238B-4F65-A95C-55F2BA20F1B0}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {AB4A5DDF-1F91-4AF8-9E9C-242832576C5E} = {3CE373E0-6D9A-48C4-808D-510E6FB69D5C}
+ {965791BF-8F77-4A69-97E2-8B6B66CF9863} = {3CE373E0-6D9A-48C4-808D-510E6FB69D5C}
+ {E4A64C0A-0127-42AD-A439-2063A145F536} = {E5509F73-1E5E-45B4-AED7-4A38F8DF1DDE}
+ {C0449FA5-C667-4B5C-878A-04773903B130} = {E5509F73-1E5E-45B4-AED7-4A38F8DF1DDE}
+ {E0DBB446-3B2B-4BC2-871F-925AB60917E3} = {E4A64C0A-0127-42AD-A439-2063A145F536}
+ {841054C8-559B-4E6F-8DCD-44C2D3DA2F0B} = {E4A64C0A-0127-42AD-A439-2063A145F536}
+ {2B69F62E-991C-4E4B-B1FF-2A5906415764} = {E5509F73-1E5E-45B4-AED7-4A38F8DF1DDE}
+ {2D53E72C-CA18-43B0-8B22-628401177763} = {E5509F73-1E5E-45B4-AED7-4A38F8DF1DDE}
+ {79BCCE0B-3666-4866-AF36-8FA2C71479C0} = {2D53E72C-CA18-43B0-8B22-628401177763}
+ {4616F442-466A-4B0E-8939-DA6367A5E365} = {2D53E72C-CA18-43B0-8B22-628401177763}
+ {9F82F6DD-238B-4F65-A95C-55F2BA20F1B0} = {E5509F73-1E5E-45B4-AED7-4A38F8DF1DDE}
+ {6E575BA8-2787-4A5D-9742-8ACBA07C93D9} = {B998D2DB-7CA7-4A1B-8B98-801973A12D20}
+ {CBCEF1A5-1044-49DB-8A19-2443FFBE3DFE} = {B998D2DB-7CA7-4A1B-8B98-801973A12D20}
+ {07BE9233-A701-4899-8FB0-21D8EE0057D3} = {CBCEF1A5-1044-49DB-8A19-2443FFBE3DFE}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {591A6475-8DF2-42DA-AFF1-8EF88BCF6EE4}
+ EndGlobalSection
+EndGlobal