📜 ⬆️ ⬇️

.Net Core, AppDomain, WCF, RPC marshalling TCP / Ip your bike

As you know, in .Net Core, at the moment, there is no AppDomain, and WCF is only a SOAP client .Net Core, WCF and ODATA clients .

Of course, the task can be solved via Web Api with WebSockets to trigger events. But, I just propose an alternative solution for marshaling over TCP / IP and creating objects and calling server-side methods using Reflection.

Here is the remote call of methods and properties. An example is taken from here Basics of operator overloading :
')
//      string typeStr = typeof(Console).AssemblyQualifiedName; var _Console = wrap.GetType(typeStr);//       // "Hello from Client"      _Console.WriteLine("Hello from Client"); //      TestDllForCoreClr.MyArr //   TestDll.dll var MyArr = wrap.GetType("TestDllForCoreClr.MyArr", "TestDll"); //      //      var Point1 = MyArr._new(1, 12, -4); // new MyArr(1, 12, -4); var Point2 = MyArr._new(0, -3, 18); // new MyArr(0, -3, 18); //     PointX     Console.WriteLine("  : "+Point1.x+" "+Point1.y+" "+Point1.z); Console.WriteLine("  : "+Point2.x+" "+Point2.y + " "+ Point2.z); var Point3 = Point1 + Point2; Console.WriteLine("\nPoint1 + Point2 = " + Point3.x + " " + Point3.y + " " + Point3.z); Point3 = Point1 - Point2; Console.WriteLine("Point1 - Point2 = "+ Point3.x + " " + Point3.y + " " + Point3.z); Point3 = -Point1; Console.WriteLine("-Point1 = " + Point3.x + " " + Point3.y + " " + Point3.z); Point2++; Console.WriteLine("Point2++ = "+ Point2.x + " " + Point2.y + " " + Point2.z); Point2--; Console.WriteLine("Point2-- = " + Point2.x + " " + Point2.y + " " + Point2.z); 

Only the wrap.GetType and MyArr._new and _Console methods are not native. Everything else is one-to-one work with objects in C #.

In fact, Point1 and Point2 and Point3 are the heirs of DynamicObject with overridden TryXXX methods, and inside of them the method type is packed, the name of the method and parameters is in the Stream and transferred to the Server using TCP / IP protocol, where it is unpacked and the method that Searched by type, method name and parameters. After receiving the result, the same procedure but, only from the server to the client.

The solution itself is very close with the COM out process interaction on IDispatch. I remember with pleasure dealt with TSocketConnection internals.

But, unlike Idispatch, overloading methods and operators, calling Generic methods with type inference or specifying Generic arguments are used. Support for extension methods for classes that are in the same assembly and for Linq methods.

Also support for asynchronous methods and subscription to events, ref and out parameters, access by index [], support for iterators in foreach.

Unlike Web Api, you do not need to write specifically the server code Controller, Hub s.
This is close to the AppDomain c Remouting but, unlike Remoting, each class is analogous to MarshalByRefObject. That is, we can create any object on the server side and return a link to it (some languages ​​from numbers only support double).

When calling methods, only the following types of parameters are directly serialized: numbers, strings, date, Guid and byte []. For other types, you need to create them on the server side, and references to them are already passed in the method parameters.

So examples can be looked at TypeScript which is close to C # in syntax
CEF, ES6, Angular 2, TypeScript using .Net Core classes. Creating a cross-platform GUI for .Net using CEF

CEF, Angular 2 using .Net Core class events

Calling the server-side method can be viewed here. Cross-platform use of .Net classes from unmanaged code. Or analogue IDispatch on Linux .

In this article, I will focus on the features of using DinamicObject, marshaling to call object and static methods of remote objects.

The first thing we start off with is loading the correct assembly and getting the type. In the first example, we obtained a type by the full name of the type, by the name of the type and the name of the assembly.

 //     //    //public static Assembly GetAssembly(string FileName, bool IsGlabalAssembly = false) // IsGlabalAssembly == true?      typeof(string).GetTypeInfo().Assembly.Location //    Server var assembly = wrap.GetAssembly("TestDll"); //      var @TestClass = assembly.GetType("TestDllForCoreClr.TestClass"); //    ,      . ,      //   //public static Type GetType(string type, string FileName = "", bool IsGlabalAssembly = false) //var @TestClass = wrap.GetType("TestDllForCoreClr.TestClass", "TestDll"); 

Now, having a reference to the type, you can create an object by calling the _new method or call the New wrapper method.

 var TO = @TestClass._new("Property from Constructor"); 

or

 wrap.New(@TestClass,"Property from Constructor"); 

You can construct Generic types:

 var Dictionary2 = wrap.GetType("System.Collections.Generic.Dictionary`2", "System.Collections"); var DictionaryIntString = wrap.GetGenericType(Dictionary2, "System.Int32", "System.String"); var dict = wrap.New(DictionaryIS); 

In wrap.New and wrap.GetGenericType, you can pass references to types or their string representation. For strings, the main thing is that the assemblies are loaded.

The next option is to copy the object to the server. This is important because the Tcp / IP exchange rate is about 15,000 calls per second, with a permanent connection and only 2000 when connecting for each TCP / IP request, the exchange rate .

 var ClientDict = new Dictionary<int, string>() { [1] = "", [2] = "", [3] = "" }; //     Json  . //   . var dict = connector.CoryTo(ClientDict); 

Now dict is a link to the dictionary on the server side, and can be passed in parameters.

 //       //public V GenericMethod<K, V>(Dictionary<K, V> param1, K param2, V param3) resGM = TO.GenericMethod(dict, 99, "Hello"); Console.WriteLine("      " + resGM); 

We can use indexes to access and set values.

 Console.WriteLine("dict[2] " + dict[2]); dict[2] = ""; Console.WriteLine("dict[2] " + dict[2]); 

We can use an iterator

 foreach (string value in dict.Values) Console.WriteLine("Dict Values " + value); 

Now I will turn your attention to the difference in syntax. First of all, it is a call to Generic methods with setting Generic arguments, ref and out parameters, an asynchronous call.

 //     // public V GenericMethodWithRefParam<,V >( param, V param2, ref string param3) //      ref . ,   . //    RefParam,     Value      var OutParam = new ClientRPC.RefParam("TroLoLo"); resGM = TO.GenericMethodWithRefParam(5, "GenericMethodWithRefParam", OutParam); Console.WriteLine($@"      Ref {resGM} {OutParam.Value}"); //       var GenericArgs = new object[] { "System.String", "System.String" }; //          : // var @Int32 = wrap.GetType("System.Int32"); //var GenericArgs = new object[] {@Int32, "System.String" }; //            //      //    // resGM = TO.GenericMethodWithRefParam<String,String>(null, "GenericMethodWithRefParam", ref OutParam) resGM = TO.GenericMethodWithRefParam(GenericArgs, null, "GenericMethodWithRefParam", OutParam); Console.WriteLine($@"      Ref {resGM} {OutParam.Value}"); // Test return null resGM = TO.GenericMethodWithRefParam(GenericArgs, null, null, OutParam); Console.WriteLine($@"      Ref {resGM} {OutParam}"); 

The RefParam class is needed to write the changed parameter in the Value field.

 public class RefParam { public dynamic Value; public RefParam(object Value) { this.Value = Value; } public RefParam() { this.Value = null; } public override string ToString() { return Value?.ToString(); } } 

To call an asynchronous method:

 // public async Task<V> GenericMethodAsync<K, V>(K param, string param4 = "Test") var GenericArgs = new object[] { "System.Int32", "System.String" }; object resTask = await TO.async.GenericMethodAsync(GenericArgs , 44); 

It is necessary to add the word async before the name of the asynchronous method

If you have a Task, then you can wait for the execution by calling:

 int res =await wrap.async.ReturnParam(task); 

Another difference from the real code is that we cannot directly use overload ==

 if (myObject1 == myObject2) Console.WriteLine("    =="); 

Instead, we must explicitly call

 if (myObject1.Equals(myObject2)) Console.WriteLine("  Equals"); 

or, if there is an overload, operator ==

 if (MyArr.op_Equality(myObject1,myObject2)) Console.WriteLine("  op_Equality"); 

There is support for objects that support System.Dynamic.IDynamicMetaObjectProvider. These are ExpandoObject, DinamicObject, JObject, etc.

Take for tests the following object:

 public object GetExpandoObject() { dynamic res = new ExpandoObject(); res.Name = "Test ExpandoObject"; res.Number = 456; res.toString = (Func<string>)(() => res.Name); res.Sum = (Func<int, int, int>)((x, y) => x + y); return res; } 

Now you can use it:

  var EO = TO.GetExpandoObject(); Console.WriteLine(" ExpandoObject  " + EO.Name); Console.WriteLine(" ExpandoObject  " + EO.Number); //   var Delegate = EO.toString; Console.WriteLine("  toString " + Delegate()); //    //  ExpandoObject     Console.WriteLine("  toString " + EO.toString()); var DelegateSum = EO.Sum; Console.WriteLine("  Sum " + DelegateSum(3,4)); //    //  ExpandoObject     Console.WriteLine("  Sum " + EO.Sum(3,4)); //  ExpandoObject } 

As you can see from the example, not only methods and properties are supported, but also delegates. Often you need to bring objects to the interfaces. For this is the keyword _as.

  string[] sa = new string[] { "", "", "", "", "" }; //     var ServerSa = Connector.CoryTo(sa); //   IEnumerable   var en = ServerSa._as("IEnumerable"); var Enumerator = en.GetEnumerator(); while(Enumerator.MoveNext()) Console.WriteLine(Enumerator.Current); //     var @IEnumerable = wrap.GetType("System.Collections.IEnumerable"); var @IEnumerator = wrap.GetType("System.Collections.IEnumerator"); //    ,     en = ServerSa._as(@IEnumerable); Enumerator = en.GetEnumerator(); //       IEnumerator Enumerator = Enumerator._as(@IEnumerator); while (Enumerator.MoveNext()) Console.WriteLine(Enumerator.Current); 

We now turn to semi-automatic serialization.

 var dict = connector.CoryTo(ClientDict); 

Inside connector.CoryTo, Json is serialized.

  public dynamic CoryTo(object obj) { //     //      string type = obj.GetType().AssemblyQualifiedName; var str = JsonConvert.SerializeObject(obj); return CoryTo(type, str); } 

It is necessary that the assembly of a serializable type be loaded on the server. Explanation below.

Also on the client there can be no assembly with a serializable type. Therefore for serialization we can use JObject
Anonymous types .

Jsonobject

We can specify the type, as a string or as a reference to the type and object to be serialized.

  public dynamic CoryTo(object type, object obj) { var str = JsonConvert.SerializeObject(obj); return CoryTo(type, str); } 

And in the end, send to the server:

  // type     Type AutoWrapClient    //     public dynamic CoryTo(object type, string objToStr) { object result; var res = AutoWrapClient.TryInvokeMember(0, "JsonToObject", new object[] { type, objToStr }, out result, this); if (!res) throw new Exception(LastError); return result; } 

It should be noted that for deserialization on the server string, an assembly with a type must be loaded on the server side.

  static void TestSerializeObject(ClientRPC.TCPClientConnector connector) { //      var obj = new TestDllForCoreClr.TestClass("   "); dynamic test = null; try { //     test = connector.CoryTo(obj); } //    //         CoryTo catch (Exception) { Console.WriteLine(" " + connector.LastError); var assembly = wrap.GetAssembly("TestDll"); test = connector.CoryTo(obj); } Console.WriteLine(test.ObjectProperty); } 

Also, assemblies that are not in the Core CLR directory or are not NuGet packages must be manually downloaded:

Build Code
  static Assembly LoadAssembly(string fileName) { var Dir = AppContext.BaseDirectory; string path = Path.Combine(Dir, fileName); Assembly assembly = null; if (File.Exists(path)) { try { var asm = System.Runtime.Loader.AssemblyLoadContext.GetAssemblyName(path); assembly = Assembly.Load(asm); } catch (Exception) { assembly = System.Runtime.Loader.AssemblyLoadContext.Default.LoadFromAssemblyPath(path); } } else throw new Exception("   " + path); return assembly; } 


In order to copy the server object to the client, you need to use the following method:

 var objFromServ = connector.CoryFrom<Dictionary<int, string>>(dict); Console.WriteLine("dict[2] " + objFromServ[2]); 

JObject can be used if this type is not available on the client using:

  connector.CoryFrom<dynamic>( 

Well, in the end. proceed to connect to the server.

 if (LoadLocalServer) { //   dotnet.exe c Server.dll,  . connector = ClientRPC.TCPClientConnector.LoadAndConnectToLocalServer(GetParentDir(dir, 4) + $@"\Server\Server\bin\Release\netcoreapp1.1\Server.dll"); } else { //          //         //   5  connector = new ClientRPC.TCPClientConnector("127.0.0.1", port, false); //  Tcp/IP          . port = ClientRPC.TCPClientConnector.GetAvailablePort(6892); connector.Open(port, 2); } 

Inside LoadAndConnectToLocalServer we start the dotnet.exe process with the address of the Server.dll file:

Server process load code
 public static TCPClientConnector LoadAndConnectToLocalServer(string FileName) { int port = 1025; port = GetAvailablePort(port); ProcessStartInfo startInfo = new ProcessStartInfo("dotnet.exe"); startInfo.Arguments = @""""+ FileName+ $@""" { port}"; Console.WriteLine(startInfo.Arguments); var server = Process.Start(startInfo); Console.WriteLine(server.Id); var connector = new TCPClientConnector("127.0.0.1", port); port++; port = GetAvailablePort(port); connector.Open(port, 2); return connector; } 


Now we can get proxy.

  wrap = ClientRPC.AutoWrapClient.GetProxy(connector); 

And using it to get types, call static methods, create objects, call methods of objects, and so on.

At the end of the work with the server, you need to disconnect from it and, if we started the process, then unload it.

  //    AutoWrapClient     GC.Collect(); GC.WaitForPendingFinalizers(); Console.WriteLine("Press any key"); Console.ReadKey(); //        ,  50  //   connector.ClearDeletedObject(); //   ,   , Tcp/Ip    connector.Close(); //     , //    if (LoadLocalServer) connector.CloseServer(); Console.WriteLine("Press any key"); Console.ReadKey(); 

As for events, you can see the article CEF, Angular 2 using the events of the classes .Net Core .

It describes the process of working with events .Net objects. The only thing that the module code for the client can be obtained:

 var @DescribeEventMethods = wrap.GetType("NetObjectToNative.DescribeEventMethods", "Server"); string CodeModule = @DescribeEventMethods.GetCodeModuleForEvents(@EventTest); 


Please note that when you subscribe to an event with two or more parameters. is created
anonymous class with fields with the appropriate names and types of parameters. So for the event:

  public event Action<string, int> EventWithTwoParameter; 

A wrapper will be created:

 Target.EventWithTwoParameter += (arg1,arg2) => { if (EventWithTwoParameter!=null) { var EventWithTwoParameterObject = new {arg1=arg1,arg2=arg2}; EventWithTwoParameter(EventWithTwoParameterObject); } }; 

CodeModule will contain the following code:

 //  value:  //   // arg1:System.String // arg2:System.Int32 static public void EventWithTwoParameter(dynamic value) { Console.WriteLine("EventWithTwoParameter " + wrap.toString(value)); //    . Console.WriteLine($"EventWithTwoParameter arg1:{value.arg1} arg2:{value.arg2}"); value(ClientRPC.AutoWrapClient.FlagDeleteObject); } 

You can read about using dynamic compilation here. Net Core, 1C, dynamic compilation, Scripting API .

As for the security of Analog System.Security.Permissions in .NET Core , it is advised to run the process under a certain user account with certain rights.

It is regrettable that in C # there are no pseudo-interfaces for speakers, an analogue of type annotation in TypeScript d.ts, for static code checking and IntelliSense.

But you can write the usual code, altering it to a remote one. with minimal gestures.

The sources here are RPCProjects .

Before running the examples, compile the projects and copy the TestDll.dll folder in the Server \ bin \ Release \ netcoreapp1.1 \ and Client \ bin \ Release \ netcoreapp1.1 \ directories from the TestDll \ bin \ Release \ netcoreapp1.1 \ folder.

If the article is of interest, then in the next article I will sign for the mechanisms for exchanging and invoking methods on the server.

PS Actively get rid of russish in code, but it is still quite a lot. If the project is interesting, I will finally clean up the Russian code.

Source: https://habr.com/ru/post/323096/


All Articles