.NET 程序集:理解类型可见性

发布于 2024-09-13 07:41:00 字数 2829 浏览 3 评论 0原文

我正在尝试重现 System.Xml.Serialization 已经执行的操作,但是针对不同的数据源。 目前任务仅限于反序列化。 即给定我知道如何阅读的已定义数据源。编写一个采用随机类型的库,通过反射了解它的字段/属性,然后生成并编译“reader”类,该类可以采用数据源和该随机类型的实例,并将数据源写入对象的字段/属性。

的简化摘录

public class ReflectionHelper
{
    public abstract class FieldReader<T> 
    {
        public abstract void Fill(T entity, XDataReader reader);
    }

    public static FieldReader<T> GetFieldReader<T>()
    {
        Type t = typeof(T);
        string className = GetCSharpName(t);
        string readerClassName = Regex.Replace(className, @"\W+", "_") + "_FieldReader";
        string source = GetFieldReaderCode(t.Namespace, className, readerClassName, fields);

        CompilerParameters prms = new CompilerParameters();
        prms.GenerateInMemory = true;
        prms.ReferencedAssemblies.Add("System.Data.dll");
        prms.ReferencedAssemblies.Add(Assembly.GetExecutingAssembly().GetModules(false)[0].FullyQualifiedName);
        prms.ReferencedAssemblies.Add(t.Module.FullyQualifiedName);

        CompilerResults compiled = new CSharpCodeProvider().CompileAssemblyFromSource(prms, new string[] {source});

        if (compiled.Errors.Count > 0)
        {
            StringWriter w = new StringWriter();
            w.WriteLine("Error(s) compiling {0}:", readerClassName);
            foreach (CompilerError e in compiled.Errors)
                w.WriteLine("{0}: {1}", e.Line, e.ErrorText);
            w.WriteLine();
            w.WriteLine("Generated code:");
            w.WriteLine(source);
            throw new Exception(w.GetStringBuilder().ToString());
        }

        return (FieldReader<T>)compiled.CompiledAssembly.CreateInstance(readerClassName);
    }

    private static string GetFieldReaderCode(string ns, string className, string readerClassName, IEnumerable<EntityField> fields)
    {
        StringWriter w = new StringWriter();

        // write out field setters here

        return @"
using System;
using System.Data;

namespace " + ns + @".Generated
{
    public class " + readerClassName + @" : ReflectionHelper.FieldReader<" + className + @">
    {
        public void Fill(" + className + @" e, XDataReader reader)
        {
" + w.GetStringBuilder().ToString() + @"
        }
    }
}
";
    }
}

这是我的 ReflectionHelper 类和调用代码

class Program
{
    static void Main(string[] args)
    {
        ReflectionHelper.GetFieldReader<Foo>();
        Console.ReadKey(true);
    }

    private class Foo
    {
        public string Field1 = null;
        public int? Field2 = null;
    }
}

:动态编译当然会失败,因为 Foo 类在 Program 类之外不可见。但! .NET XML 反序列化器以某种方式解决了这个问题 - 问题是:如何解决? 经过一个小时通过 Reflector 挖掘 System.Xml.Serialization 后,我开始承认我在这里缺乏某种基础知识,并且不确定我在寻找什么......

而且我完全有可能重新发明轮子和/或者挖错了方向,遇到这种情况请大声说出来!

I am trying to reproduce something that System.Xml.Serialization already does, but for a different source of data.
For now task is limited to deserialization only.
I.e. given defined source of data that I know how to read. Write a library that takes a random type, learns about it fields/properties via reflection, then generates and compiles "reader" class that can take data source and an instance of that random type and writes from data source into the object's fields/properties.

here is a simplified extract from my ReflectionHelper class

public class ReflectionHelper
{
    public abstract class FieldReader<T> 
    {
        public abstract void Fill(T entity, XDataReader reader);
    }

    public static FieldReader<T> GetFieldReader<T>()
    {
        Type t = typeof(T);
        string className = GetCSharpName(t);
        string readerClassName = Regex.Replace(className, @"\W+", "_") + "_FieldReader";
        string source = GetFieldReaderCode(t.Namespace, className, readerClassName, fields);

        CompilerParameters prms = new CompilerParameters();
        prms.GenerateInMemory = true;
        prms.ReferencedAssemblies.Add("System.Data.dll");
        prms.ReferencedAssemblies.Add(Assembly.GetExecutingAssembly().GetModules(false)[0].FullyQualifiedName);
        prms.ReferencedAssemblies.Add(t.Module.FullyQualifiedName);

        CompilerResults compiled = new CSharpCodeProvider().CompileAssemblyFromSource(prms, new string[] {source});

        if (compiled.Errors.Count > 0)
        {
            StringWriter w = new StringWriter();
            w.WriteLine("Error(s) compiling {0}:", readerClassName);
            foreach (CompilerError e in compiled.Errors)
                w.WriteLine("{0}: {1}", e.Line, e.ErrorText);
            w.WriteLine();
            w.WriteLine("Generated code:");
            w.WriteLine(source);
            throw new Exception(w.GetStringBuilder().ToString());
        }

        return (FieldReader<T>)compiled.CompiledAssembly.CreateInstance(readerClassName);
    }

    private static string GetFieldReaderCode(string ns, string className, string readerClassName, IEnumerable<EntityField> fields)
    {
        StringWriter w = new StringWriter();

        // write out field setters here

        return @"
using System;
using System.Data;

namespace " + ns + @".Generated
{
    public class " + readerClassName + @" : ReflectionHelper.FieldReader<" + className + @">
    {
        public void Fill(" + className + @" e, XDataReader reader)
        {
" + w.GetStringBuilder().ToString() + @"
        }
    }
}
";
    }
}

and the calling code:

class Program
{
    static void Main(string[] args)
    {
        ReflectionHelper.GetFieldReader<Foo>();
        Console.ReadKey(true);
    }

    private class Foo
    {
        public string Field1 = null;
        public int? Field2 = null;
    }
}

The dynamic compilation of course fails because Foo class is not visible outside of Program class. But! The .NET XML deserializer somehow works around that - and the question is: How?
After an hour of digging System.Xml.Serialization via Reflector I came to accept that I lack some kind of basic knowledge here and not really sure what am I looking for...

Also it is entirely possible that I am reinventing a wheel and/or digging in a wrong direction, in which case please do speak up!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

任性一次 2024-09-20 07:41:00

您不需要创建动态程序集并动态编译代码来反序列化对象。 XmlSerializer 也不这样做 - 它使用 Reflection API,特别是它使用以下简单概念:

从任何类型检索字段集

Reflection 提供了 GetFields()用于此目的的方法:

foreach (var field in myType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
    // ...

我在此处包含 BindingFlags 参数,以确保它将包含非公共字段,否则默认情况下它将仅返回公共字段。

设置任何类型中字段的值

Reflection 提供了用于此目的的函数 SetValue()。您可以在 FieldInfo 实例(从上面的 GetFields() 返回)上调用此方法,并为其提供要在其中更改该字段值的实例,以及value 将其设置为:

field.SetValue(myObject, myValue);

这基本上相当于 myObject.Field = myValue;,当然,该字段是在运行时而不是编译时识别的。

将它们放在一起

这是一个简单的例子。请注意,您需要进一步扩展它以处理更复杂的类型,例如数组。

public static T Deserialize<T>(XDataReader dataReader) where T : new()
{
    return (T) deserialize(typeof(T), dataReader);
}
private static object deserialize(Type t, XDataReader dataReader)
{
    // Handle the basic, built-in types
    if (t == typeof(string))
        return dataReader.ReadString();
    // etc. for int and all the basic types

    // Looks like the type t is not built-in, so assume it’s a class.
    // Create an instance of the class
    object result = Activator.CreateInstance(t);

    // Iterate through the fields and recursively deserialize each
    foreach (var field in t.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
        field.SetValue(result, deserialize(field.FieldType, dataReader));

    return result;
}

请注意,我必须对 XDataReader 做出一些假设,最值得注意的是它只能读取这样的字符串。我确信您能够更改它,使其适合您的特定读者类别。

一旦您扩展了它以支持您需要的所有类型(包括示例类中的 int?),您就可以通过调用来反序列化对象:

Foo myFoo = Deserialize<Foo>(myDataReader);

即使 Foo< /code> 是私有类型,如您的示例中所示。

You don’t need to create a dynamic assembly and dynamically compile code in order to deserialise an object. XmlSerializer does not do that either — it uses the Reflection API, in particular it uses the following simple concepts:

Retrieving the set of fields from any type

Reflection provides the GetFields() method for this purpose:

foreach (var field in myType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
    // ...

I’m including the BindingFlags parameter here to ensure that it will include non-public fields, because otherwise it will return only public ones by default.

Setting the value of a field in any type

Reflection provides the function SetValue() for this purpose. You call this on a FieldInfo instance (which is returned from GetFields() above) and give it the instance in which you want to change the value of that field, and the value to set it to:

field.SetValue(myObject, myValue);

This is basically equivalent to myObject.Field = myValue;, except of course that the field is identified at runtime instead of compile-time.

Putting it all together

Here is a simple example. Notice you need to extend this further to work with more complex types such as arrays, for example.

public static T Deserialize<T>(XDataReader dataReader) where T : new()
{
    return (T) deserialize(typeof(T), dataReader);
}
private static object deserialize(Type t, XDataReader dataReader)
{
    // Handle the basic, built-in types
    if (t == typeof(string))
        return dataReader.ReadString();
    // etc. for int and all the basic types

    // Looks like the type t is not built-in, so assume it’s a class.
    // Create an instance of the class
    object result = Activator.CreateInstance(t);

    // Iterate through the fields and recursively deserialize each
    foreach (var field in t.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
        field.SetValue(result, deserialize(field.FieldType, dataReader));

    return result;
}

Notice I had to make some assumptions about XDataReader, most notably that it can just read a string like that. I’m sure you’ll be able to change it so that it works with your particular reader class.

Once you’ve extended this to support all the types you need (including int? in your example class), you can deserialize an object by calling:

Foo myFoo = Deserialize<Foo>(myDataReader);

and you can do this even when Foo is a private type as it is in your example.

巴黎夜雨 2024-09-20 07:41:00

如果我尝试使用 sgen.exe(独立的 XML 序列化程序集编译器),则会收到以下错误消息:

Warning: Ignoring 'TestApp.Program'.
  - TestApp.Program is inaccessible due to its protection level. Only public types can be processed.
Warning: Ignoring 'TestApp.Program+Foo'.
  - TestApp.Program+Foo is inaccessible due to its protection level. Only public types can be processed.
Assembly 'c:\...\TestApp\bin\debug\TestApp.exe' does not contain any types that can be serialized using XmlSerializer.

示例代码中调用 new XmlSerializer(typeof(Foo)) 结果是:

System.InvalidOperationException: TestApp.Program+Foo is inaccessible due to its protection level. Only public types can be processed.

在 您认为 XmlSerializer 可以处理这个问题吗?

但是,请记住,在运行时,没有这样的限制。使用反射的可信代码可以自由地忽略访问修饰符。这就是 .NET 二进制序列化正在做的事情。

例如,如果您使用 DynamicMethod 在运行时生成 IL 代码,则您可以通过 skipVisibility = true 来避免对字段/类的可见性进行任何检查。

If I try to use sgen.exe (the standalone XML serialization assembly compiler), I get the following error message:

Warning: Ignoring 'TestApp.Program'.
  - TestApp.Program is inaccessible due to its protection level. Only public types can be processed.
Warning: Ignoring 'TestApp.Program+Foo'.
  - TestApp.Program+Foo is inaccessible due to its protection level. Only public types can be processed.
Assembly 'c:\...\TestApp\bin\debug\TestApp.exe' does not contain any types that can be serialized using XmlSerializer.

Calling new XmlSerializer(typeof(Foo)) in your example code results in:

System.InvalidOperationException: TestApp.Program+Foo is inaccessible due to its protection level. Only public types can be processed.

So what gave you the idea that XmlSerializer can handle this?

However, remember that at runtime, there are no such restrictions. Trusted code using reflection is free to ignore access modifiers. This is what .NET binary serialization is doing.

For example, if you generate IL code at runtime using DynamicMethod, then you can pass skipVisibility = true to avoid any checks for visibility of fields/classes.

宛菡 2024-09-20 07:41:00

我已经在这方面做了一些工作。我不确定这是否有帮助,但无论如何我认为这可能是一种方式。最近,我对一个必须通过网络发送的类进行序列化和反序列化。由于有两个不同的程序(客户端和服务器),首先我在两个源中实现了该类,然后使用序列化。它失败了,因为 .Net 告诉我它没有相同的 ID(我不确定,但它是某种程序集 ID)。

嗯,经过一番谷歌搜索后,我发现这是因为序列化的类位于不同的程序集上,所以解决方案是将该类放在一个独立的库中,然后使用该库编译客户端和服务器。我对您的代码使用了相同的想法,因此我将 Foo 类和 FieldReader 类放在一个独立的库中,比方说:

namespace FooLibrary
{    
    public class Foo
    {
        public string Field1 = null;
        public int? Field2 = null;
    }

    public abstract class FieldReader<T>
    {
        public abstract void Fill(T entity, IDataReader reader);
    }    
}

编译它并将其添加到其他源(using FooLibrary;

)是我用过的代码。它与你的不完全相同,因为我没有 GetCSharpName (我使用 t.Name 代替)和 XDataReader 的代码,所以我使用 IDataReader (只是为了让编译器接受代码并编译它)并更改 EntityField 顺便说一下,对于对象

public class ReflectionHelper
{
    public static FieldReader<T> GetFieldReader<T>()
    {
        Type t = typeof(T);
        string className = t.Name;
        string readerClassName = Regex.Replace(className, @"\W+", "_") + "_FieldReader";
        object[] fields = new object[10];
        string source = GetFieldReaderCode(t.Namespace, className, readerClassName, fields);

        CompilerParameters prms = new CompilerParameters();
        prms.GenerateInMemory = true;
        prms.ReferencedAssemblies.Add("System.Data.dll");
        prms.ReferencedAssemblies.Add(Assembly.GetExecutingAssembly().GetModules(false)[0].FullyQualifiedName);
        prms.ReferencedAssemblies.Add(t.Module.FullyQualifiedName);
        prms.ReferencedAssemblies.Add("FooLibrary1.dll");

        CompilerResults compiled = new CSharpCodeProvider().CompileAssemblyFromSource(prms, new string[] { source });

        if (compiled.Errors.Count > 0)
        {
            StringWriter w = new StringWriter();
            w.WriteLine("Error(s) compiling {0}:", readerClassName);
            foreach (CompilerError e in compiled.Errors)
                w.WriteLine("{0}: {1}", e.Line, e.ErrorText);
            w.WriteLine();
            w.WriteLine("Generated code:");
            w.WriteLine(source);
            throw new Exception(w.GetStringBuilder().ToString());
        }

        return (FieldReader<T>)compiled.CompiledAssembly.CreateInstance(readerClassName);
    }

    private static string GetFieldReaderCode(string ns, string className, string readerClassName, IEnumerable<object> fields)
    {
        StringWriter w = new StringWriter();

        // write out field setters here

        return @"   
using System;   
using System.Data;   
namespace " + ns + ".Generated   
{    
   public class " + readerClassName + @" : FieldReader<" + className + @">    
   {        
         public override void Fill(" + className + @" e, IDataReader reader)          
         " + w.GetStringBuilder().ToString() +         
   }    
  }";        
 } 
}

,我发现了一个小错误,您应该使用 new 或重写 Fill 方法,因为它是抽象的。

好吧,我必须承认 GetFieldReader 返回 null,但至少编译器编译了它。

希望这会对您有所帮助,或者至少引导您找到好的答案
问候

I've been working a bit on this. I'm not sure if it will help but, anyway I think it could be the way. Recently I worked with Serialization and DeSerealization of a class I had to send over the network. As there were two different programs (the client and the server), at first I implemented the class in both sources and then used serialization. It failed as the .Net told me it had not the same ID (I'm not sure but it was some sort of assembly id).

Well, after googling a bit I found that it was because the serialized class was on different assemblies, so the solution was to put that class in a independent library and then compile both client and server with that library. I've used the same idea with your code, so I put both Foo class and FieldReader class in a independent library, let's say:

namespace FooLibrary
{    
    public class Foo
    {
        public string Field1 = null;
        public int? Field2 = null;
    }

    public abstract class FieldReader<T>
    {
        public abstract void Fill(T entity, IDataReader reader);
    }    
}

compile it and add it to the other source (using FooLibrary;)

this is the code I've used. It's not exactly the same as yours, as I don't have the code for GetCSharpName (I used t.Name instead) and XDataReader, so I used IDataReader (just for the compiler to accept the code and compile it) and also change EntityField for object

public class ReflectionHelper
{
    public static FieldReader<T> GetFieldReader<T>()
    {
        Type t = typeof(T);
        string className = t.Name;
        string readerClassName = Regex.Replace(className, @"\W+", "_") + "_FieldReader";
        object[] fields = new object[10];
        string source = GetFieldReaderCode(t.Namespace, className, readerClassName, fields);

        CompilerParameters prms = new CompilerParameters();
        prms.GenerateInMemory = true;
        prms.ReferencedAssemblies.Add("System.Data.dll");
        prms.ReferencedAssemblies.Add(Assembly.GetExecutingAssembly().GetModules(false)[0].FullyQualifiedName);
        prms.ReferencedAssemblies.Add(t.Module.FullyQualifiedName);
        prms.ReferencedAssemblies.Add("FooLibrary1.dll");

        CompilerResults compiled = new CSharpCodeProvider().CompileAssemblyFromSource(prms, new string[] { source });

        if (compiled.Errors.Count > 0)
        {
            StringWriter w = new StringWriter();
            w.WriteLine("Error(s) compiling {0}:", readerClassName);
            foreach (CompilerError e in compiled.Errors)
                w.WriteLine("{0}: {1}", e.Line, e.ErrorText);
            w.WriteLine();
            w.WriteLine("Generated code:");
            w.WriteLine(source);
            throw new Exception(w.GetStringBuilder().ToString());
        }

        return (FieldReader<T>)compiled.CompiledAssembly.CreateInstance(readerClassName);
    }

    private static string GetFieldReaderCode(string ns, string className, string readerClassName, IEnumerable<object> fields)
    {
        StringWriter w = new StringWriter();

        // write out field setters here

        return @"   
using System;   
using System.Data;   
namespace " + ns + ".Generated   
{    
   public class " + readerClassName + @" : FieldReader<" + className + @">    
   {        
         public override void Fill(" + className + @" e, IDataReader reader)          
         " + w.GetStringBuilder().ToString() +         
   }    
  }";        
 } 
}

by the way, I found a tiny mistake, you should use new or override with the Fill method, as it is abstract.

Well, I must admit that GetFieldReader returns null, but at least the compiler compiles it.

Hope that this will help you or at least it guides you to the good answer
regards

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文