From e647916f78f2e94c2d8be52f1a83b5699adf4056 Mon Sep 17 00:00:00 2001
From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com>
Date: Fri, 19 Dec 2025 18:16:08 +0100
Subject: [PATCH] Initial commit
---
.dockerignore | 30 ++
.gitattributes | 2 +
.gitignore | 398 ++++++++++++++++++
LICENSE | 21 +
README.md | 2 +
RemoteExec.Client/RemoteExec.Client.csproj | 18 +
RemoteExec.Client/RemoteExecutionException.cs | 5 +
RemoteExec.Client/RemoteExecutor.cs | 116 +++++
RemoteExec.Server/Dockerfile | 30 ++
RemoteExec.Server/Hubs/RemoteExecutionHub.cs | 113 +++++
RemoteExec.Server/Program.cs | 27 ++
.../Properties/launchSettings.json | 31 ++
RemoteExec.Server/RemoteExec.Server.csproj | 20 +
RemoteExec.Server/RemoteExec.Server.http | 6 +
.../RemoteJobAssemblyLoadContext.cs | 18 +
RemoteExec.Server/RequestAssemblyEventArgs.cs | 24 ++
RemoteExec.Server/WeatherForecast.cs | 12 +
.../appsettings.Development.json | 8 +
RemoteExec.Server/appsettings.json | 9 +
RemoteExec.Shared/RemoteExec.Shared.csproj | 9 +
RemoteExec.Shared/RemoteExecutionRequest.cs | 10 +
RemoteExec.Shared/RemoteExecutionResult.cs | 7 +
RemoteExec.slnx | 6 +
RemoteExec/Program.cs | 17 +
RemoteExec/RemoteExec.Sample.csproj | 14 +
25 files changed, 953 insertions(+)
create mode 100644 .dockerignore
create mode 100644 .gitattributes
create mode 100644 .gitignore
create mode 100644 LICENSE
create mode 100644 README.md
create mode 100644 RemoteExec.Client/RemoteExec.Client.csproj
create mode 100644 RemoteExec.Client/RemoteExecutionException.cs
create mode 100644 RemoteExec.Client/RemoteExecutor.cs
create mode 100644 RemoteExec.Server/Dockerfile
create mode 100644 RemoteExec.Server/Hubs/RemoteExecutionHub.cs
create mode 100644 RemoteExec.Server/Program.cs
create mode 100644 RemoteExec.Server/Properties/launchSettings.json
create mode 100644 RemoteExec.Server/RemoteExec.Server.csproj
create mode 100644 RemoteExec.Server/RemoteExec.Server.http
create mode 100644 RemoteExec.Server/RemoteJobAssemblyLoadContext.cs
create mode 100644 RemoteExec.Server/RequestAssemblyEventArgs.cs
create mode 100644 RemoteExec.Server/WeatherForecast.cs
create mode 100644 RemoteExec.Server/appsettings.Development.json
create mode 100644 RemoteExec.Server/appsettings.json
create mode 100644 RemoteExec.Shared/RemoteExec.Shared.csproj
create mode 100644 RemoteExec.Shared/RemoteExecutionRequest.cs
create mode 100644 RemoteExec.Shared/RemoteExecutionResult.cs
create mode 100644 RemoteExec.slnx
create mode 100644 RemoteExec/Program.cs
create mode 100644 RemoteExec/RemoteExec.Sample.csproj
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..fe1152b
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,30 @@
+**/.classpath
+**/.dockerignore
+**/.env
+**/.git
+**/.gitignore
+**/.project
+**/.settings
+**/.toolstarget
+**/.vs
+**/.vscode
+**/*.*proj.user
+**/*.dbmdl
+**/*.jfm
+**/azds.yaml
+**/bin
+**/charts
+**/docker-compose*
+**/Dockerfile*
+**/node_modules
+**/npm-debug.log
+**/obj
+**/secrets.dev.yaml
+**/values.dev.yaml
+LICENSE
+README.md
+!**/.gitignore
+!.git/HEAD
+!.git/config
+!.git/packed-refs
+!.git/refs/heads/**
\ No newline at end of file
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..dfe0770
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+# Auto detect text files and perform LF normalization
+* text=auto
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8a30d25
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,398 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.tlog
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio 6 auto-generated project file (contains which files were open etc.)
+*.vbp
+
+# Visual Studio 6 workspace and project file (working project files containing files to include in project)
+*.dsw
+*.dsp
+
+# Visual Studio 6 technical files
+*.ncb
+*.aps
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# Visual Studio History (VSHistory) files
+.vshistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
+
+# VS Code files for those working on multiple tools
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+*.code-workspace
+
+# Local History for Visual Studio Code
+.history/
+
+# Windows Installer files from build outputs
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# JetBrains Rider
+*.sln.iml
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..ccfb086
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 Stone_Red
+
+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.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..fa57589
--- /dev/null
+++ b/README.md
@@ -0,0 +1,2 @@
+# RemoteExec
+
diff --git a/RemoteExec.Client/RemoteExec.Client.csproj b/RemoteExec.Client/RemoteExec.Client.csproj
new file mode 100644
index 0000000..16bc780
--- /dev/null
+++ b/RemoteExec.Client/RemoteExec.Client.csproj
@@ -0,0 +1,18 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/RemoteExec.Client/RemoteExecutionException.cs b/RemoteExec.Client/RemoteExecutionException.cs
new file mode 100644
index 0000000..53453a8
--- /dev/null
+++ b/RemoteExec.Client/RemoteExecutionException.cs
@@ -0,0 +1,5 @@
+
+[Serializable]
+internal class RemoteExecutionException(string message) : Exception(message)
+{
+}
\ No newline at end of file
diff --git a/RemoteExec.Client/RemoteExecutor.cs b/RemoteExec.Client/RemoteExecutor.cs
new file mode 100644
index 0000000..31c3096
--- /dev/null
+++ b/RemoteExec.Client/RemoteExecutor.cs
@@ -0,0 +1,116 @@
+using Microsoft.AspNetCore.SignalR.Client;
+
+using RemoteExec.Shared;
+
+using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
+using System.Text.Json;
+
+namespace RemoteExec.Client;
+
+public class RemoteExecutor(string url)
+{
+ private readonly HubConnection _connection =
+ new HubConnectionBuilder()
+ .WithUrl(url)
+ .WithAutomaticReconnect()
+ .Build();
+
+ public async Task StartAsync(CancellationToken cancellationToken = default)
+ {
+ await _connection.StartAsync(cancellationToken);
+ }
+
+ public bool TryExecute(TDelegate del, out TResult? result, params object[] args) where TDelegate : Delegate
+ {
+ try
+ {
+ result = Execute(del, args);
+ return true;
+ }
+ catch
+ {
+ result = default;
+ return false;
+ }
+ }
+
+ public bool TryExecute(TDelegate del, out TResult? result, [NotNullWhen(false)] out Exception? exception, params object[] args) where TDelegate : Delegate
+ {
+ try
+ {
+ result = Execute(del, args);
+ exception = null;
+ return true;
+ }
+ catch (Exception ex)
+ {
+ result = default;
+ exception = ex;
+ return false;
+ }
+ }
+
+ public TResult Execute(TDelegate del, params object[] args) where TDelegate : Delegate
+ {
+ object? execResult = Execute(del, args);
+
+ if (execResult is TResult typedResult)
+ {
+ return typedResult;
+ }
+ else if (execResult is JsonElement jsonElement)
+ {
+ return jsonElement.Deserialize()!;
+ }
+ else
+ {
+ throw new InvalidCastException("The result cannot be cast to the specified type.");
+ }
+ }
+
+ public object? Execute(T del, params object[] args) where T : Delegate
+ {
+ MethodInfo method = del.Method;
+ Type declaringType = method.DeclaringType!;
+ Assembly asm = declaringType.Assembly;
+
+ if (!method.IsStatic)
+ {
+ throw new InvalidOperationException("Only static methods supported");
+ }
+
+ if (asm.IsDynamic)
+ {
+ throw new InvalidOperationException("Dynamic assemblies are not supported");
+ }
+
+ if (string.IsNullOrEmpty(asm.Location))
+ {
+ throw new InvalidOperationException("Assembly location is not available");
+ }
+
+ byte[] dllBytes = File.ReadAllBytes(asm.Location);
+
+ RemoteExecutionRequest request = new RemoteExecutionRequest
+ {
+ AssemblyBytes = dllBytes,
+ TypeName = declaringType.FullName!,
+ MethodName = method.Name,
+ ArgumentTypes = [.. method.GetParameters().Select(p => p.ParameterType.AssemblyQualifiedName!)],
+ Arguments = args
+ };
+
+ RemoteExecutionResult result = _connection
+ .InvokeAsync("Execute", request)
+ .GetAwaiter()
+ .GetResult();
+
+ if (result.Exception != null)
+ {
+ throw new RemoteExecutionException(result.Exception);
+ }
+
+ return result.Result;
+ }
+}
\ No newline at end of file
diff --git a/RemoteExec.Server/Dockerfile b/RemoteExec.Server/Dockerfile
new file mode 100644
index 0000000..b69823d
--- /dev/null
+++ b/RemoteExec.Server/Dockerfile
@@ -0,0 +1,30 @@
+# See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
+
+# This stage is used when running from VS in fast mode (Default for Debug configuration)
+FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
+USER $APP_UID
+WORKDIR /app
+EXPOSE 8080
+EXPOSE 8081
+
+
+# This stage is used to build the service project
+FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
+ARG BUILD_CONFIGURATION=Release
+WORKDIR /src
+COPY ["RemoteExec.Server/RemoteExec.Server.csproj", "RemoteExec.Server/"]
+RUN dotnet restore "./RemoteExec.Server/RemoteExec.Server.csproj"
+COPY . .
+WORKDIR "/src/RemoteExec.Server"
+RUN dotnet build "./RemoteExec.Server.csproj" -c $BUILD_CONFIGURATION -o /app/build
+
+# This stage is used to publish the service project to be copied to the final stage
+FROM build AS publish
+ARG BUILD_CONFIGURATION=Release
+RUN dotnet publish "./RemoteExec.Server.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
+
+# This stage is used in production or when running from VS in regular mode (Default when not using the Debug configuration)
+FROM base AS final
+WORKDIR /app
+COPY --from=publish /app/publish .
+ENTRYPOINT ["dotnet", "RemoteExec.Server.dll"]
\ No newline at end of file
diff --git a/RemoteExec.Server/Hubs/RemoteExecutionHub.cs b/RemoteExec.Server/Hubs/RemoteExecutionHub.cs
new file mode 100644
index 0000000..aae93eb
--- /dev/null
+++ b/RemoteExec.Server/Hubs/RemoteExecutionHub.cs
@@ -0,0 +1,113 @@
+using Microsoft.AspNetCore.SignalR;
+
+using RemoteExec.Shared;
+
+using System.Reflection;
+using System.Runtime.Loader;
+using System.Text.Json;
+
+namespace RemoteExec.Server.Hubs;
+
+public class RemoteExecutionHub : Hub
+{
+ private readonly Dictionary connections = [];
+
+ public override Task OnConnectedAsync()
+ {
+ connections.Add(Context.ConnectionId, new RemoteJobAssemblyLoadContext($"RemoteJob_{Guid.NewGuid()}", true));
+
+ return base.OnConnectedAsync();
+ }
+
+ public override Task OnDisconnectedAsync(Exception? exception)
+ {
+ if (connections.TryGetValue(Context.ConnectionId, out RemoteJobAssemblyLoadContext assemblyLoadContext))
+ {
+ assemblyLoadContext.Unload();
+
+ _ = connections.Remove(Context.ConnectionId);
+ }
+
+ return base.OnDisconnectedAsync(exception);
+ }
+
+ public async Task Execute(RemoteExecutionRequest req)
+ {
+ try
+ {
+ AssemblyLoadContext alc = new AssemblyLoadContext(
+ name: $"RemoteJob_{Guid.NewGuid()}",
+ isCollectible: true);
+
+ using MemoryStream ms = new MemoryStream(req.AssemblyBytes);
+ Assembly asm = alc.LoadFromStream(ms);
+
+ Type type = asm.GetType(req.TypeName, throwOnError: true)!;
+
+ Type?[] argTypes = req.ArgumentTypes
+ .Select(Type.GetType)
+ .ToArray()!;
+
+ MethodInfo? method = type.GetMethod(
+ req.MethodName,
+ BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic,
+ binder: null,
+ argTypes,
+ modifiers: null);
+
+ if (method == null)
+ {
+ throw new MissingMethodException(req.TypeName, req.MethodName);
+ }
+
+ ParameterInfo[] parameters = method.GetParameters();
+ if (parameters.Length != req.Arguments.Length)
+ {
+ throw new ArgumentException("Argument count mismatch");
+ }
+
+ object?[] invokeArgs = new object?[req.Arguments.Length];
+
+ for (int i = 0; i < invokeArgs.Length; i++)
+ {
+ Type targetType = parameters[i].ParameterType;
+ object arg = req.Arguments[i];
+
+ if (arg is JsonElement je)
+ {
+ // Deserialize the JSON element into the expected CLR type
+ invokeArgs[i] = JsonSerializer.Deserialize(je.GetRawText(), targetType);
+ }
+ else if (arg == null)
+ {
+ invokeArgs[i] = null;
+ }
+ else if (!targetType.IsInstanceOfType(arg))
+ {
+ // Fallback for simple primitive conversions
+ invokeArgs[i] = Convert.ChangeType(arg, targetType);
+ }
+ else
+ {
+ invokeArgs[i] = arg;
+ }
+ }
+
+ object? result = method.Invoke(null, invokeArgs);
+
+ alc.Unload();
+
+ return new RemoteExecutionResult
+ {
+ Result = result
+ };
+ }
+ catch (Exception ex)
+ {
+ return new RemoteExecutionResult
+ {
+ Exception = ex.ToString()
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/RemoteExec.Server/Program.cs b/RemoteExec.Server/Program.cs
new file mode 100644
index 0000000..ea540e2
--- /dev/null
+++ b/RemoteExec.Server/Program.cs
@@ -0,0 +1,27 @@
+using RemoteExec.Server.Hubs;
+
+WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
+
+// Add services to the container.
+
+builder.Services.AddControllers();
+builder.Services.AddSignalR(options => options.MaximumReceiveMessageSize = null);
+builder.Services.AddOpenApi();
+
+WebApplication app = builder.Build();
+
+app.MapHub("/remote");
+
+// Configure the HTTP request pipeline.
+if (app.Environment.IsDevelopment())
+{
+ _ = app.MapOpenApi();
+}
+
+app.UseHttpsRedirection();
+
+app.UseAuthorization();
+
+app.MapControllers();
+
+app.Run();
diff --git a/RemoteExec.Server/Properties/launchSettings.json b/RemoteExec.Server/Properties/launchSettings.json
new file mode 100644
index 0000000..35d8381
--- /dev/null
+++ b/RemoteExec.Server/Properties/launchSettings.json
@@ -0,0 +1,31 @@
+{
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "dotnetRunMessages": true,
+ "applicationUrl": "http://localhost:5202"
+ },
+ "https": {
+ "commandName": "Project",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "dotnetRunMessages": true,
+ "applicationUrl": "https://localhost:7109;http://localhost:5202"
+ },
+ "Container (Dockerfile)": {
+ "commandName": "Docker",
+ "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}",
+ "environmentVariables": {
+ "ASPNETCORE_HTTPS_PORTS": "8081",
+ "ASPNETCORE_HTTP_PORTS": "8080"
+ },
+ "publishAllPorts": true,
+ "useSSL": true
+ }
+ },
+ "$schema": "https://json.schemastore.org/launchsettings.json"
+}
\ No newline at end of file
diff --git a/RemoteExec.Server/RemoteExec.Server.csproj b/RemoteExec.Server/RemoteExec.Server.csproj
new file mode 100644
index 0000000..d0da686
--- /dev/null
+++ b/RemoteExec.Server/RemoteExec.Server.csproj
@@ -0,0 +1,20 @@
+
+
+
+ net10.0
+ enable
+ enable
+ 288f55bd-4ce1-4c6a-867f-a12ef417c7cf
+ Linux
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/RemoteExec.Server/RemoteExec.Server.http b/RemoteExec.Server/RemoteExec.Server.http
new file mode 100644
index 0000000..df8ee18
--- /dev/null
+++ b/RemoteExec.Server/RemoteExec.Server.http
@@ -0,0 +1,6 @@
+@RemoteExec.Server_HostAddress = http://localhost:5202
+
+GET {{RemoteExec.Server_HostAddress}}/weatherforecast/
+Accept: application/json
+
+###
diff --git a/RemoteExec.Server/RemoteJobAssemblyLoadContext.cs b/RemoteExec.Server/RemoteJobAssemblyLoadContext.cs
new file mode 100644
index 0000000..0febaba
--- /dev/null
+++ b/RemoteExec.Server/RemoteJobAssemblyLoadContext.cs
@@ -0,0 +1,18 @@
+using System.Reflection;
+using System.Runtime.Loader;
+
+namespace RemoteExec.Server;
+
+public class RemoteJobAssemblyLoadContext(string name) : AssemblyLoadContext(name, true)
+{
+ public event EventHandler? RequestAssembly;
+
+ protected override Assembly? Load(AssemblyName assemblyName)
+ {
+ RequestAssemblyEventArgs requestAssemblyEventArgs = new RequestAssemblyEventArgs(assemblyName);
+
+ RequestAssembly?.Invoke(this, requestAssemblyEventArgs);
+
+ return requestAssemblyEventArgs.GetAssemblyAsync().ConfigureAwait(false).GetAwaiter().GetResult();
+ }
+}
diff --git a/RemoteExec.Server/RequestAssemblyEventArgs.cs b/RemoteExec.Server/RequestAssemblyEventArgs.cs
new file mode 100644
index 0000000..2a319ee
--- /dev/null
+++ b/RemoteExec.Server/RequestAssemblyEventArgs.cs
@@ -0,0 +1,24 @@
+using System.Reflection;
+
+namespace RemoteExec.Server;
+
+public class RequestAssemblyEventArgs(AssemblyName assemblyName) : EventArgs
+{
+ private readonly TaskCompletionSource taskCompletionSource = new TaskCompletionSource();
+
+ private Assembly? assembly = null;
+
+ public AssemblyName Assembly { get; } = assemblyName;
+
+ public async Task GetAssemblyAsync()
+ {
+ await taskCompletionSource.Task;
+ return assembly;
+ }
+
+ public void SetAssembly(Assembly assembly)
+ {
+ this.assembly = assembly;
+ taskCompletionSource.SetResult();
+ }
+}
diff --git a/RemoteExec.Server/WeatherForecast.cs b/RemoteExec.Server/WeatherForecast.cs
new file mode 100644
index 0000000..88a3d78
--- /dev/null
+++ b/RemoteExec.Server/WeatherForecast.cs
@@ -0,0 +1,12 @@
+namespace RemoteExec.Server;
+
+public class WeatherForecast
+{
+ public DateOnly Date { get; set; }
+
+ public int TemperatureC { get; set; }
+
+ public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
+
+ public string? Summary { get; set; }
+}
diff --git a/RemoteExec.Server/appsettings.Development.json b/RemoteExec.Server/appsettings.Development.json
new file mode 100644
index 0000000..0c208ae
--- /dev/null
+++ b/RemoteExec.Server/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/RemoteExec.Server/appsettings.json b/RemoteExec.Server/appsettings.json
new file mode 100644
index 0000000..10f68b8
--- /dev/null
+++ b/RemoteExec.Server/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/RemoteExec.Shared/RemoteExec.Shared.csproj b/RemoteExec.Shared/RemoteExec.Shared.csproj
new file mode 100644
index 0000000..b760144
--- /dev/null
+++ b/RemoteExec.Shared/RemoteExec.Shared.csproj
@@ -0,0 +1,9 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
diff --git a/RemoteExec.Shared/RemoteExecutionRequest.cs b/RemoteExec.Shared/RemoteExecutionRequest.cs
new file mode 100644
index 0000000..2c47d11
--- /dev/null
+++ b/RemoteExec.Shared/RemoteExecutionRequest.cs
@@ -0,0 +1,10 @@
+namespace RemoteExec.Shared;
+
+public sealed class RemoteExecutionRequest
+{
+ public required byte[] AssemblyBytes { get; set; }
+ public required string TypeName { get; set; }
+ public required string MethodName { get; set; }
+ public required string[] ArgumentTypes { get; set; }
+ public required object[] Arguments { get; set; }
+}
diff --git a/RemoteExec.Shared/RemoteExecutionResult.cs b/RemoteExec.Shared/RemoteExecutionResult.cs
new file mode 100644
index 0000000..b069dc0
--- /dev/null
+++ b/RemoteExec.Shared/RemoteExecutionResult.cs
@@ -0,0 +1,7 @@
+namespace RemoteExec.Shared;
+
+public sealed class RemoteExecutionResult
+{
+ public object? Result { get; set; }
+ public string? Exception { get; set; }
+}
diff --git a/RemoteExec.slnx b/RemoteExec.slnx
new file mode 100644
index 0000000..dfc2fd0
--- /dev/null
+++ b/RemoteExec.slnx
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/RemoteExec/Program.cs b/RemoteExec/Program.cs
new file mode 100644
index 0000000..558dfbc
--- /dev/null
+++ b/RemoteExec/Program.cs
@@ -0,0 +1,17 @@
+using RemoteExec.Client;
+
+RemoteExecutor remoteExecutor = new RemoteExecutor("https://localhost:7109/remote");
+await remoteExecutor.StartAsync();
+
+bool success = remoteExecutor.TryExecute(Multiply, out int result, 2, 4);
+
+Console.WriteLine($"Success: {success}, Result: {result}");
+
+result = remoteExecutor.Execute, int>(Multiply, 3, 5);
+
+Console.WriteLine($"Result: {result}");
+
+static int Multiply(int x, int y)
+{
+ return x * y;
+}
\ No newline at end of file
diff --git a/RemoteExec/RemoteExec.Sample.csproj b/RemoteExec/RemoteExec.Sample.csproj
new file mode 100644
index 0000000..c6b27f6
--- /dev/null
+++ b/RemoteExec/RemoteExec.Sample.csproj
@@ -0,0 +1,14 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+