C # 'dinâmico' não pode acessar propriedades de tipos anônimos declarados em outro assembly

87

O código abaixo está funcionando bem, contanto que eu tenha aula ClassSameAssemblyna mesma montagem da classe Program. Mas quando eu movo a classe ClassSameAssemblypara uma montagem separada, um RuntimeBinderException(veja abaixo) é lançado. É possível resolver isso?

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' não contém uma definição para 'Nome'

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
fonte
StackTrace: em CallSite.Target (Closure, CallSite, Object) em System.Dynamic.UpdateDelegates.UpdateAndExecute1 [T0, TRet] (CallSite site, T0 arg0) em ConsoleApplication2.Program.Main (String [] args) em C: \ temp \ Projects \ ConsoleApplication2 \ ConsoleApplication2 \ Program.cs: linha 23 em System.AppDomain._nExecuteAssembly (RuntimeAssembly assembly, String [] args) em System.AppDomain.nExecuteAssembly (RuntimeAssembly assembly, String [] args) em System.AppDomain String assemblyFile, Evidence assemblySecurity, String [] args)
mehanik
em Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly () em System.Threading.ThreadHelper.ThreadStart_Context (estado do objeto) em System.Threading.ExecutionContext.Run (ExecutionContext verificationContext, ContextCallback callback em System state.hreading, ContextCallback callback em Object state.yncreading). ExecutionContext.Run (ExecutionContext executionContext, ContextCallback callback, Object state) em System.Threading.ThreadHelper.ThreadStart () InnerException:
mehanik
alguma solução final com código-fonte completo?
Kiquenet

Respostas:

116

Eu acredito que o problema é que o tipo anônimo é gerado como internal, então o fichário realmente não "sabe" sobre ele como tal.

Tente usar ExpandoObject em vez disso:

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

Eu sei que é um tanto feio, mas é o melhor que consigo pensar no momento ... Não acho que você possa usar um inicializador de objeto com ele, porque embora seja fortemente tipado como ExpandoObjecto compilador não saberá o que fazer com "Nome" e "Idade". Você pode ser capaz de fazer isso:

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

mas isso não é muito melhor ...

Você poderia potencialmente escrever um método de extensão para converter um tipo anônimo a um expando com o mesmo conteúdo através de reflexão. Então você pode escrever:

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

Mas isso é horrível :(

Jon Skeet
fonte
1
Obrigado Jon. Eu apenas tive o mesmo problema ao usar uma classe que por acaso era privada da assembleia.
Dave Markle
2
Eu adoraria algo como o seu exemplo horrível no final, mas não tão horrível. Para usar: props dinâmicos = new {Metadata = DetailModelMetadata.Create, PageTitle = "Novo conteúdo", PageHeading = "Gerenciamento de conteúdo"}; e ter os adereços nomeados adicionados como membros dinâmicos seria ótimo!
ProfK
A interface improvisada da estrutura de código aberto faz muito com o dlr, pois possui uma sintaxe de inicialização embutida que funciona para qualquer objeto dinâmico ou estático. return Build<ExpandoObject>.NewObject(Name:"Micheal", Age: 20);
jbtule
1
qualquer amostra de código-fonte completa para um método de extensão para converter um tipo anônimo em um expando?
Kiquenet
1
@ Md.lbrahim: Você não pode, basicamente. Você teria que fazer isso em objectou em um tipo genérico (você pode exigir que seja uma classe ...) e verificar o tipo em tempo de execução.
Jon Skeet
62

Você pode usar [assembly: InternalsVisibleTo("YourAssemblyName")]para tornar visíveis os internos da montagem.

ema
fonte
2
A resposta de Jon é mais completa, mas na verdade oferece uma solução alternativa razoavelmente simples para mim. Obrigado :)
kelloti
Fiquei batendo a cabeça por horas em diferentes fóruns, mas não encontrei uma resposta simples, exceto esta. Obrigado Luke. Mas ainda não consigo entender por que um tipo dinâmico não é acessível fora de um assembly como ele faz no mesmo assembly. Quero dizer, por que essa restrição em .Net.
Faisal Mq
@FaisalMq é porque o compilador que gera as classes anônimas as declara "internas". Não sei qual é o verdadeiro motivo.
ema,
1
Sim, acho que essa resposta é importante, porque não quero alterar o código de trabalho, só preciso testá-lo em outro assembly
PandaWood
Uma observação a ser adicionada aqui é que você precisa reiniciar o Visual Studio após essa alteração para que isso funcione.
Rady,
11

Encontrei um problema semelhante e gostaria de acrescentar à resposta de Jon Skeets que há outra opção. A razão pela qual descobri foi que percebi que muitos métodos de extensão no Asp MVC3 usam classes anônimas como entrada para fornecer atributos html (new {alt = "Image alt", style = "padding-top: 5px"} =>

De qualquer forma - essas funções usam o construtor da classe RouteValueDictionary. Eu mesmo tentei fazer isso e, com certeza, funciona - embora apenas o primeiro nível (usei uma estrutura de vários níveis). SO - no código seria:

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"]);

ASSIM ... O que realmente está acontecendo aqui? Uma espiada dentro do RouteValueDictionary revela este código (valores ~ = o acima):

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);
}

SO - usando TypeDescriptor.GetProperties (o), poderíamos obter as propriedades e valores apesar do tipo anônimo ser construído como interno em uma montagem separada! E, claro, isso seria muito fácil de estender para torná-lo recursivo. E fazer um método de extensão se você quiser.

Espero que isto ajude!

/Vencedor

Vencedor
fonte
Desculpe pela confusão. Código atualizado de prop1 => p1 onde apropriado. Ainda assim - a ideia com todo o post era apresentar TypeDescriptor.GetProperties como uma opção para resolver o problema, que espero que tenha ficado claro de qualquer maneira ...
Victor
É realmente estúpido que a dinâmica não possa fazer isso por nós. Eu realmente amo e realmente odeio dinâmica.
Chris Marisic
2

Aqui está uma versão rudimentar de um método de extensão para ToExpandoObject que tenho certeza que tem espaço para polimento.

    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
fonte
1

Uma solução mais limpa seria:

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

Que agora é um ExpandoObject.

Lembre-se de fazer referência:

Microsoft.CSharp.dll
Zylv3r
fonte
0

Método de extensão ToExpando (mencionado na resposta de Jon) para os corajosos

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
fonte
0

A solução abaixo funcionou para mim em meus projetos de aplicativos de console

Coloque este [assembly: InternalsVisibleTo ("YourAssemblyName")] em \ Properties \ AssemblyInfo.cs do projeto separado com a função que retorna o objeto dinâmico.

"YourAssemblyName" é o nome do assembly do projeto de chamada. Você pode obter isso por meio de Assembly.GetExecutingAssembly (). FullName executando-o na chamada do projeto.


fonte
0

Se você já estiver usando Newtonsoft.Json em seu projeto (ou se estiver disposto a adicioná-lo para essa finalidade), poderá implementar aquele método de extensão horrível ao qual Jon Skeet se refere em sua resposta da seguinte maneira:

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