C # 'dinámico' no puede acceder a propiedades de tipos anónimos declarados en otro ensamblado

87

El código a continuación funciona bien siempre que tenga una clase ClassSameAssemblyen el mismo ensamblado que la clase Program. Pero cuando muevo la clase ClassSameAssemblya un ensamblado separado, RuntimeBinderExceptionse lanza un (ver más abajo). ¿Es posible solucionarlo?

using System;

namespace ConsoleApplication2
{
    public static class ClassSameAssembly
    {
        public static dynamic GetValues()
        {
            return new
            {
                Name = "Michael", Age = 20
            };
        }
    }

    internal class Program
    {
        private static void Main(string[] args)
        {
            var d = ClassSameAssembly.GetValues();
            Console.WriteLine("{0} is {1} years old", d.Name, d.Age);
        }
    }
}

Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: 'objeto' no contiene una definición para 'Nombre'

at CallSite.Target(Closure , CallSite , Object )
at System.Dynamic.UpdateDelegates.UpdateAndExecute1[T0,TRet](CallSite site, T0 arg0)
at ConsoleApplication2.Program.Main(String[] args) in C:\temp\Projects\ConsoleApplication2\ConsoleApplication2\Program.cs:line 23
mehanik
fuente
StackTrace: en CallSite.Target (Closure, CallSite, Object) en System.Dynamic.UpdateDelegates.UpdateAndExecute1 [T0, TRet] (sitio de CallSite, T0 arg0) en ConsoleApplication2.Program.Main (String [] args) en C: \ temp \ Projects \ ConsoleApplication2 \ ConsoleApplication2 \ Program.cs: línea 23 en System.AppDomain._nExecuteAssembly (ensamblado RuntimeAssembly, String [] args) en System.AppDomain.nExecuteAssembly (ensamblado RuntimeAssembly, String [] args) en System.AppDomain.ExecuteAssembly ( String assemblyFile, Evidence assemblySecurity, String [] args)
mehanik
en Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly () en System.Threading.ThreadHelper.ThreadStart_Context (Estado del objeto) en System.Threading.ExecutionContext.Run (ExecutionContext executionContext, ContextCallback callback, Estado del objeto, System.CtxhreSyncreading). ExecutionContext.Run (ExecutionContext executionContext, devolución de llamada ContextCallback, estado del objeto) en System.Threading.ThreadHelper.ThreadStart () InnerException:
mehanik
¿Alguna solución final con código fuente completo?
Kiquenet

Respuestas:

116

Creo que el problema es que el tipo anónimo se genera como internal, por lo que el archivador no lo "sabe" realmente como tal.

Intente usar ExpandoObject en su lugar:

public static dynamic GetValues()
{
    dynamic expando = new ExpandoObject();
    expando.Name = "Michael";
    expando.Age = 20;
    return expando;
}

Sé que es algo feo, pero es lo mejor que se me ocurre en este momento ... No creo que puedas usar un inicializador de objeto con él, porque aunque está fuertemente tipado, ExpandoObjectel compilador no sabrá qué hacer. con "Nombre" y "Edad". Es posible que pueda hacer esto:

 dynamic expando = new ExpandoObject()
 {
     { "Name", "Michael" },
     { "Age", 20 }
 };
 return expando;

pero eso no es mucho mejor ...

Potencialmente, podría escribir un método de extensión para convertir un tipo anónimo en un expando con el mismo contenido a través de la reflexión. Entonces podrías escribir:

return new { Name = "Michael", Age = 20 }.ToExpando();

Aunque eso es bastante horrible :(

Jon Skeet
fuente
1
Gracias Jon. Simplemente tuve el mismo problema al usar una clase que resultó ser privada para el ensamblado.
Dave Markle
2
Sin embargo, me encantaría algo como tu horrible ejemplo al final, pero no tan horrible. Para usar: props dinámicos = new {Metadata = DetailModelMetadata.Create, PageTitle = "New Content", PageHeading = "Content Management"}; ¡Y agregar los accesorios nombrados como miembros dinámicos sería genial!
ProfK
La interfaz improvisada del marco de código abierto hace mucho con el dlr, tiene una sintaxis de inicialización en línea que funciona para cualquier objeto dinámico o estático. return Build<ExpandoObject>.NewObject(Name:"Micheal", Age: 20);
jbtule
1
¿Alguna muestra de código fuente completo para un método de extensión para convertir un tipo anónimo en un expando?
Kiquenet
1
@ Md.lbrahim: No puedes, básicamente. Tendría que hacerlo en objecto en un tipo genérico (puede requerir que sea una clase ...) y verificar el tipo en el momento de la ejecución.
Jon Skeet
63

Puede utilizar [assembly: InternalsVisibleTo("YourAssemblyName")]para hacer visibles los componentes internos del ensamblaje.

ema
fuente
2
La respuesta de Jon es más completa, pero esto en realidad me proporciona una solución razonablemente simple. Gracias :)
kelloti
Estuve golpeándome la cabeza durante horas en diferentes foros, pero no encontré una respuesta simple excepto esta. Gracias Luke. Pero todavía no puedo entender por qué un tipo dinámico no es accesible fuera de un ensamblado como lo hace en el mismo ensamblaje. Me refiero a por qué está esta restricción en .Net.
Faisal Mq
@FaisalMq es porque el compilador que genera las clases anónimas las declara "internas". No sé cuál es la verdadera razón.
ema
2
Sí, creo que esta respuesta es importante, porque no quiero cambiar el código de trabajo, solo necesito probarlo desde otro ensamblado
PandaWood
Una nota para agregar aquí es que debe reiniciar Visual Studio después de este cambio para que esto funcione.
Rady
11

Me encontré con un problema similar y me gustaría agregar a la respuesta de Jon Skeets que hay otra opción. La razón por la que me enteré fue que me di cuenta de que muchos métodos de extensión en Asp MVC3 usan clases anónimas como entrada para proporcionar atributos html (nuevo {alt = "Image alt", style = "padding-top: 5px"} =>

De todos modos, esas funciones usan el constructor de la clase RouteValueDictionary. Lo intenté yo mismo, y efectivamente funciona, aunque solo en el primer nivel (utilicé una estructura de varios niveles). Entonces, en el código, esto sería:

object o = new {
    name = "theName",
    props = new {
        p1 = "prop1",
        p2 = "prop2"
    }
}
SeparateAssembly.TextFunc(o)

//In SeparateAssembly:
public void TextFunc(Object o) {
  var rvd = new RouteValueDictionary(o);

//Does not work:
Console.WriteLine(o.name);
Console.WriteLine(o.props.p1);

//DOES work!
Console.WriteLine(rvd["name"]);

//Does not work
Console.WriteLine(rvd["props"].p1);
Console.WriteLine(rvd["props"]["p1"]);

Entonces ... ¿Qué está pasando realmente aquí? Un vistazo dentro de RouteValueDictionary revela este código (valores ~ = o arriba):

foreach (PropertyDescriptor descriptor in TypeDescriptor.GetProperties(values))
    object obj2 = descriptor.GetValue(values);
    //"this.Add" would of course need to be adapted
    this.Add(descriptor.Name, obj2);
}

Entonces, usando TypeDescriptor.GetProperties (o) podríamos obtener las propiedades y los valores a pesar de que el tipo anónimo se construye como interno en un ensamblaje separado. Y, por supuesto, esto sería bastante fácil de ampliar para hacerlo recursivo. Y para hacer un método de extensión si quisiera.

¡Espero que esto ayude!

/Víctor

Víctor
fuente
Perdón por esa confusión. Código actualizado de prop1 => p1 cuando corresponda. Aún así, la idea con toda la publicación era presentar TypeDescriptor.GetProperties como una opción para resolver el problema, que con suerte estaba claro de todos modos ...
Victor
Es realmente estúpido que la dinámica no pueda hacer esto por nosotros. Realmente amo y realmente odio la dinámica.
Chris Marisic
2

Aquí hay una versión rudimentaria de un método de extensión para ToExpandoObject que estoy seguro tiene espacio para pulir.

    public static ExpandoObject ToExpandoObject(this object value)
    {
        // Throw is a helper in my project, replace with your own check(s)
        Throw<ArgumentNullException>.If(value, Predicates.IsNull, "value");

        var obj = new ExpandoObject() as IDictionary<string, object>;

        foreach (var property in value.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance))
        {
            obj.Add(property.Name, property.GetValue(value, null));
        }

        return obj as ExpandoObject;
    }

    [TestCase(1, "str", 10.75, 9.000989, true)]
    public void ToExpandoObjectTests(int int1, string str1, decimal dec1, double dbl1, bool bl1)
    {
        DateTime now = DateTime.Now;

        dynamic value = new {Int = int1, String = str1, Decimal = dec1, Double = dbl1, Bool = bl1, Now = now}.ToExpandoObject();

        Assert.AreEqual(int1, value.Int);
        Assert.AreEqual(str1, value.String);
        Assert.AreEqual(dec1, value.Decimal);
        Assert.AreEqual(dbl1, value.Double);
        Assert.AreEqual(bl1, value.Bool);
        Assert.AreEqual(now, value.Now);
    }
Ryan Rodemoyer
fuente
1

Una solución más limpia sería:

var d = ClassSameAssembly.GetValues().ToDynamic();

Que ahora es un ExpandoObject.

Recuerde hacer referencia:

Microsoft.CSharp.dll
Zylv3r
fuente
1

La siguiente solución funcionó para mí en mis proyectos de aplicación de consola

Ponga este [ensamblado: InternalsVisibleTo ("YourAssemblyName")] en \ Properties \ AssemblyInfo.cs del proyecto separado con la función que devuelve el objeto dinámico.

"YourAssemblyName" es el nombre de ensamblado del proyecto de llamada. Puede obtenerlo a través de Assembly.GetExecutingAssembly (). FullName ejecutándolo en la llamada al proyecto.

Cha
fuente
0

Método de extensión ToExpando (mencionado en la respuesta de Jon) para los valientes

public static class ExtensionMethods
{
    public static ExpandoObject ToExpando(this object obj)
    {
        IDictionary<string, object> expando = new ExpandoObject();
        foreach (PropertyDescriptor propertyDescriptor in TypeDescriptor.GetProperties(obj))
        {
            var value = propertyDescriptor.GetValue(obj);
            expando.Add(propertyDescriptor.Name, value == null || new[]
            {
                typeof (Enum),
                typeof (String),
                typeof (Char),
                typeof (Guid),
                typeof (Boolean),
                typeof (Byte),
                typeof (Int16),
                typeof (Int32),
                typeof (Int64),
                typeof (Single),
                typeof (Double),
                typeof (Decimal),
                typeof (SByte),
                typeof (UInt16),
                typeof (UInt32),
                typeof (UInt64),
                typeof (DateTime),
                typeof (DateTimeOffset),
                typeof (TimeSpan),
            }.Any(oo => oo.IsInstanceOfType(value))
                ? value
                : value.ToExpando());
        }

        return (ExpandoObject)expando;
    }
}
Matas Vaitkevicius
fuente
0

Si ya está utilizando Newtonsoft.Json en su proyecto (o está dispuesto a agregarlo para este propósito), podría implementar ese horrible método de extensión al que se refiere Jon Skeet en su respuesta de esta manera:

public static class ObjectExtensions
{
    public static ExpandoObject ToExpando(this object obj)
        => JsonConvert.DeserializeObject<ExpandoObject>(JsonConvert.SerializeObject(obj));
}
huysentruitw
fuente