Dynamic compiling code based on. Net standard

Time:2019-11-3

At the end of the previous article [alternative implementation based on. Net core microservice], it is mentioned how to automatically generate the client agent of microservice, make it transparent to the caller, and integrate the boring things with the framework to improve the convenience of use. After trying the emit intermediate language, we finally decided to use the pattern of generating code fragments and then compiling them dynamically.

1. background:

First, in the previous article, we realized the transparent user-oriented invocation of microservices through the framework, but we need to write a client agent for each service, which is extremely cumbersome. Second, in the project, the front-end site uses the traditional. Net framework framework, and the back-end microservices we use the. Net core framework to transform the front-end site into the. Net core framework in a short time In order to support both frameworks. How to create client agent of microservice automatically by. Net standard framework has become a problem we must solve.

2. Problem transformation

Let’s go back and take a look at what we expect the micro service client agent to look like:

Based on the above analysis, we only need to determine whether there is a return value for each method in the service interface. If there is a return value, call the invoke < ReturnType > method. If there is no return value, call the invokewithoutreturn method. Then, pass in the interface name, method name and method parameters in order. If you are familiar with Java, this problem is easy to solve. You can use dynamic proxy to create such an anonymous class, but in the world of. Net, the implementation of dynamic proxy is very troublesome.
First of all, I think it’s realized through the emit of the intermediate language IL, but it’s really unfriendly to use. After several setbacks, I finally chose to give up. Then I thought that this code segment can be generated dynamically and loaded into the system program set after dynamic compilation, which should be OK. So under the guidance of this direction, we try to achieve this problem step by step.

3. Solutions

How to generate this snippet Through the above analysis, we know that we only need to get the common methods from the interface reflection, and copy the signature of each method of the interface as it is. We can call the relevant methods in the remoteserviceproxy base class according to whether the interface method has a return value. However, we need to pay special attention to the generic method translation. The following is the reference implementation for generating this code fragment

Find the assembly files for the service interface and process each file

private static StringBuilder CreateApiProxyCode()
{
 var path = GetBinPath();
 var dir = new DirectoryInfo(path);
 //Get the microservice interface file in the project
 var files = dir.GetFiles("XZL*.Api.dll");
 var codeStringBuilder = new StringBuilder(1024);
 //Add necessary using
 codeStringBuilder
 .AppendLine("using System;")
 .AppendLine("using System.Collections.Generic;")
 .AppendLine("using System.Text;")
 .AppendLine("using XZL.Infrastructure.ApiService;")
 .AppendLine("using XZL.Infrastructure.Defines;")
 .AppendLine("using XZL.Model;")
 .AppendLine("namespace XZL.ApiClientProxy")
 .AppendLine("{"); //namespace begin
 //Process interface information in each file
 foreach (var file in files)
 {
 CreateApiProxyCodeFromFile(codeStringBuilder, file);
 }
 codeStringBuilder.AppendLine("}"); //namespace end
 return codeStringBuilder;
}

Handle the interface types in each file, and find out the dependent assemblies of each assembly for dynamic compilation later

private static void CreateApiProxyCodeFromFile(StringBuilder fileCodeBuilder, FileInfo file)
 {
 try
 {
 Assembly apiAssembly = Assembly.Load(file.Name.Substring(0, file.Name.Length - 4));
 var types = apiAssembly
 .GetTypes()
 .Where(c => c.IsInterface && c.IsPublic)
 .ToList();
 var apiSvcType = typeof(IApiService);
 bool isNeed = false;
 foreach (Type type in types)
 {
 //Find the expected interface type
 if (!apiSvcType.IsAssignableFrom(type))
 {
 continue;
 }
 //Find all the ways to interface
 var methods = type.GetMethods(BindingFlags.Public 
 | BindingFlags.FlattenHierarchy 
 | BindingFlags.Instance);
 if (!methods.Any())
 {
 continue;
 }
 //Define proxy class name, implement interface and inherit remoteserviceproxy
 fileCodeBuilder.AppendLine($"public class {type.FullName.Replace(".", "_")}Proxy :" +
  $"RemoteServiceProxy, {type.FullName}")
 .AppendLine("{"); //class begin
 //Process each method
 foreach (var mth in methods)
 {
 CreateApiProxyCodeFromMethod(fileCodeBuilder, type, mth);
 }
 fileCodeBuilder.AppendLine("}"); //class end
 isNeed = true;
 }
 if (isNeed)
 {
 var apiRefAsms = apiAssembly.GetReferencedAssemblies();
 refAssemblyList.Add(apiAssembly.GetName());
 refAssemblyList.AddRange(apiRefAsms);
 }
 }
 catch
 {
 }
 }

Handle each method in the interface

private static void CreateApiProxyCodeFromMethod(
 StringBuilder fileCodeBuilder, 
 Type type,
 MethodInfo mth)
{
 var isMthReturn = !mth.ReturnType.Equals(typeof(void));
 fileCodeBuilder.Append("public ");
 //Add return value
 if (isMthReturn)
 {
 fileCodeBuilder.Append(GetFriendlyTypeName(mth.ReturnType)).Append(" ");
 }
 else
 {
 fileCodeBuilder.Append(" void ");
 }
 //Method parameter start
 fileCodeBuilder.Append(mth.Name).Append("("); 
 var mthParams = mth.GetParameters();
 if (mthParams.Any())
 {
 var mthparaList = new List<string>();
 foreach (var p in mthParams)
 {
 mthparaList.Add(GetFriendlyTypeName(p.ParameterType) + " " + p.Name);
 }
 fileCodeBuilder.Append(string.Join(",", mthparaList));
 }
 //End of method parameter
 fileCodeBuilder.Append(")");
 //Method body start
 fileCodeBuilder.AppendLine("{"); 
 if (isMthReturn)
 {
 // return value
 fileCodeBuilder.Append("return Invoke<")
 .Append(GetFriendlyTypeName(mth.ReturnType))
 .Append(">");
 }
 else
 {
 fileCodeBuilder.Append(" InvokeWithoutReturn");
 }
 //Splicing interface name and method name
 fileCodeBuilder.Append($"(\"{type.FullName}\",\"{mth.Name}\"");
 //Method parameters
 if (mthParams.Any())
 {
 fileCodeBuilder.Append(",").Append(string.Join(",", mthParams.Select(t => t.Name)));
 }
 fileCodeBuilder.Append(");");
 //End of method body
 fileCodeBuilder.AppendLine("}"); 
}

Get generic type string


private static string GetFriendlyTypeName(Type type)
{
 if (!type.IsGenericType)
 {
 return type.FullName;
 }
 string friendlyName = type.Name;
 int iBacktick = friendlyName.IndexOf('`');
 if (iBacktick > 0)
 {
 friendlyName = friendlyName.Remove(iBacktick);
 }
 friendlyName += "<";
 Type[] typeParameters = type.GetGenericArguments();
 for (int i = 0; i < typeParameters.Length; ++i)
 {
 string typeParamName = GetFriendlyTypeName(typeParameters[i]);
 friendlyName += (i == 0 ? typeParamName : "," + typeParamName);
 }
 friendlyName += ">";
 return friendlyName;
}

How to add a dependency

Since we are compiling the source code, the dependencies in the source code are indispensable. In the previous step, we have found out the dependencies of each assembly together. Next, we will sort out all the dependencies

//Cache assembly dependency
 var references = new List<MetadataReference>(); 
 var refAsmFiles = new List<string>();
 //System dependency
 var sysRefLocation = typeof(Enumerable).GetTypeInfo().Assembly.Location;
 refAsmFiles.Add(sysRefLocation);
 //Refasmfiles native cached assembly dependencies
 refAsmFiles.Add(typeof(object).GetTypeInfo().Assembly.Location);
 refAsmFiles.AddRange(refAssemblyList.Select(t => Assembly.Load(t).Location).Distinct().ToList());
 //Traditional. Netframework needs to add mscorlib.dll
 var coreDir = Directory.GetParent(sysRefLocation);
 var mscorlibFile = coreDir.FullName + Path.DirectorySeparatorChar + "mscorlib.dll";
 if (File.Exists(mscorlibFile))
 {
 references.Add(MetadataReference.CreateFromFile(mscorlibFile));
 }
 var apiAsms = refAsmFiles.Select(t => MetadataReference.CreateFromFile(t)).ToList();
 references.AddRange(apiAsms);
 //Current assembly dependency
 var thisAssembly = Assembly.GetEntryAssembly();
 if (thisAssembly != null)
 {
 var referencedAssemblies = thisAssembly.GetReferencedAssemblies();
 foreach (var referencedAssembly in referencedAssemblies)
 {
 var loadedAssembly = Assembly.Load(referencedAssembly);
 references.Add(MetadataReference.CreateFromFile(loadedAssembly.Location));
 }
 }

Compile

With code snippets and build assembly dependencies, the next step is the most important build

//Define the compiled file name
var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Proxy");
if (!Directory.Exists(path))
{
 Directory.CreateDirectory(path);
}
var apiRemoteProxyDllFile = Path.Combine(path, 
 apiRemoteAsmName + DateTime.Now.ToString("yyyyMMddHHmmssfff") + ".dll");
var tree = SyntaxFactory.ParseSyntaxTree(codeBuilder.ToString());
var compilation = CSharpCompilation.Create(apiRemoteAsmName)
 .WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
 .AddReferences(references)
 .AddSyntaxTrees(tree);
//Perform compilation
EmitResult compilationResult = compilation.Emit(apiRemoteProxyDllFile);
if (compilationResult.Success)
{
 // Load the assembly
 apiRemoteAsm = Assembly.LoadFrom(apiRemoteProxyDllFile);
}
else
{
 foreach (Diagnostic codeIssue in compilationResult.Diagnostics)
 {
 string issue = $"ID: {codeIssue.Id}, Message: {codeIssue.GetMessage()}," +
 $" Location: { codeIssue.Location.GetLineSpan()}, " +
 $"Severity: { codeIssue.Severity}";
 Appruntimes. Instance. Logger. Error ("exception in auto compile code," + issue);
 }
}

epilogue

After the above processing, although it is not perfect, it has successfully realized what we expected. In the previous getservice, when it was found that it belongs to the remote service, it only needs to return the proxy object in the following form. At the same time, in order to make the call more smooth, we set the time of compilation to happen when the program starts, PS of course, there may be some other more appropriate time

static ConcurrentDictionary<string, Object> svcInstance = new ConcurrentDictionary<string, object>();
var typeName = "XZL.ApiClientProxy." + typeof(TService).FullName.Replace(".", "_") + "Proxy";
object obj = null;
if (svcInstance.TryGetValue(typeName, out obj) && obj != null)
{
 return (TService)obj;
}
try
{
 obj = (TService)apiRemoteAsm.CreateInstance(typeName);
 svcInstance.TryAdd(typeName, obj);
}
catch
{
 Throw new icvipexception ($"no valid agent for {typeof (tservice). Fullname} was found");
}
return (TService)obj;

summary

The above is the dynamic compilation and implementation code based on. Net standard introduced by Xiaobian. I hope it can help you. If you have any questions, please leave me a message and Xiaobian will reply to you in time. Thank you very much for your support of the developepaer website!