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

时间:2010-08-07 22:27:16

标签: c# .net reflection assemblies

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

这是我的ReflectionHelper类的简化摘录

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() + @"
        }
    }
}
";
    }
}

和调用代码:

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反序列化器以某种方式解决了这个问题 - 问题是:如何? 经过反射器挖掘System.Xml.Serialization一个小时后,我开始接受我在这里缺乏一些基本知识而不确定我在寻找什么......

我完全有可能重新发明一个方向盘和/或向错误的方向挖掘,在这种情况下请大声说出来!

3 个答案:

答案 0 :(得分:1)

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

从任何类型

中检索字段集

Reflection为此提供了GetFields()方法:

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

我在这里包含BindingFlags参数以确保它包含非公共字段,否则默认情况下它只返回公共字段。

设置任何类型的字段值

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

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是私有类型,您也可以这样做。

答案 1 :(得分:0)

我一直在研究这个问题。我不确定它是否会有所帮助,但无论如何我认为它可能就是这样。最近我使用Serialization和DeSerealization这个我必须通过网络发送的类。由于有两个不同的程序(客户端和服务器),起初我在两个源中实现了类,然后使用了序列化。它失败了,因为.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,但至少编译器会编译它。

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

答案 2 :(得分:0)

如果我尝试使用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以避免对字段/类的可见性进行任何检查。