¿Devolver resultados de tipo anónimo?

194

Usando el ejemplo simple a continuación, ¿cuál es la mejor manera de devolver resultados de múltiples tablas usando Linq a SQL?

Digamos que tengo dos tablas:

Dogs:   Name, Age, BreedId
Breeds: BreedId, BreedName

Quiero devolver todos los perros con sus BreedName. Debo hacer que todos los perros usen algo como esto sin problemas:

public IQueryable<Dog> GetDogs()
{
    var db = new DogDataContext(ConnectString);
    var result = from d in db.Dogs
                 join b in db.Breeds on d.BreedId equals b.BreedId
                 select d;
    return result;
}

Pero si quiero perros con razas y pruebo esto, tengo problemas:

public IQueryable<Dog> GetDogsWithBreedNames()
{
    var db = new DogDataContext(ConnectString);
    var result = from d in db.Dogs
                 join b in db.Breeds on d.BreedId equals b.BreedId
                 select new
                        {
                            Name = d.Name,
                            BreedName = b.BreedName
                        };
    return result;
}

Ahora me doy cuenta de que el compilador no me permite devolver un conjunto de tipos anónimos, ya que espera perros, pero ¿hay alguna manera de devolver esto sin tener que crear un tipo personalizado? ¿O tengo que crear mi propia clase DogsWithBreedNamesy especificar ese tipo en la selección? ¿O hay otra manera más fácil?

Jonathan S.
fuente
Solo por curiosidad, ¿por qué todos los ejemplos de Linq muestran el uso de tipos anónimos, si no funcionan? Por ejemplo, este ejemploforeach (var cust in query) Console.WriteLine("id = {0}, City = {1}", cust.CustomerID, cust.City);
Hot Licks
@Hot Licks: la tabla Cliente en esos ejemplos es una entidad representada por una clase. El ejemplo simplemente no parece mostrar las definiciones de esas clases.
Jonathan S.
Tampoco le dice que una compilación swizzle está reemplazando "var" con el nombre de la clase.
Hot Licks

Respuestas:

213

Tiendo a ir por este patrón:

public class DogWithBreed
{
    public Dog Dog { get; set; }
    public string BreedName  { get; set; }
}

public IQueryable<DogWithBreed> GetDogsWithBreedNames()
{
    var db = new DogDataContext(ConnectString);
    var result = from d in db.Dogs
                 join b in db.Breeds on d.BreedId equals b.BreedId
                 select new DogWithBreed()
                        {
                            Dog = d,
                            BreedName = b.BreedName
                        };
    return result;
}

Significa que tiene una clase adicional, pero es rápida y fácil de codificar, fácilmente extensible, reutilizable y segura.

teedyay
fuente
Me gusta este enfoque, pero ahora no estoy seguro de cómo mostrar el nombre del perro. Si estoy vinculando el resultado a un DataGrid, ¿puedo obtener las propiedades de Dog sin definirlas explícitamente en la clase DogWithBreed o tengo que crear el getter / setter para cada campo que quiero mostrar?
Jonathan S.
44
¿DataGrids no le permite especificar la propiedad como "Dog.Name"? Me olvido ahora por qué los odio suficiente para nunca utilizarlos ...
teedyay
@JonathanS. ? cómo u hizo presente en la columna de la plantilla por favor, dime que estoy en situación similar
rahularyansharma
Hola, me gusta este método de encapsular las dos clases en una. Hace que sea fácil trabajar con él. Sin mencionar que crear clases simples para usar solo en el contexto actual lo hace más limpio.
Permanece el
66
Esto realmente no responde la pregunta que tenía el OP, "¿hay alguna manera de devolver esto sin tener que crear un tipo personalizado"?
tjscience
69

Usted puede regresar tipos anónimos, pero en realidad no es bastante .

En este caso, creo que sería mucho mejor crear el tipo apropiado. Si solo se va a usar desde el tipo que contiene el método, conviértalo en un tipo anidado.

Personalmente, me gustaría que C # obtenga "tipos anónimos con nombre", es decir, el mismo comportamiento que los tipos anónimos, pero con nombres y declaraciones de propiedades, pero eso es todo.

EDITAR: otros sugieren perros que regresan, y luego acceder al nombre de la raza a través de una ruta de propiedad, etc. Ese es un enfoque perfectamente razonable, pero IME conduce a situaciones en las que ha realizado una consulta de una manera particular debido a los datos que desea use, y esa metainformación se pierde cuando regresa IEnumerable<Dog>, la consulta puede esperar que use (digamos) en Breedlugar de Ownerdebido a algunas opciones de carga, etc., pero si olvida eso y comienza a usar otras propiedades, su aplicación puede funcionar, pero no tan eficientemente como lo había previsto originalmente. Por supuesto, podría estar hablando basura, o sobre optimizando, etc.

Jon Skeet
fuente
3
Oye, no soy de los que no quieren características por miedo a que sean maltratadas, pero ¿te imaginas los tipos de códigos crudos que veríamos si permitieran que se pasen tipos anónimos con nombre? (escalofrío)
Dave Markle
19
Podríamos ver algunos abusos. También podríamos ver un código mucho más simple donde solo queremos una tupla, básicamente. No todo debe ser un objeto con un comportamiento complejo. A veces, "solo los datos" es lo correcto. OMI, por supuesto.
Jon Skeet
1
Gracias, ¿entonces su preferencia es crear tipos incluso si es para una vista única como esta? Tengo muchos informes que dividen los mismos datos de diferentes maneras y esperaba no tener que crear todos estos tipos diferentes (DogsWithBreeds, DogsWithOwnerNames, etc.)
Jonathan S.
1
Trataría de no tener que dividirlo de muchas maneras, o colocar la parte de corte en el lugar que necesita los datos para que pueda usar tipos anónimos, pero más allá de eso, sí. Apesta de alguna manera, pero así es la vida, me temo :(
Jon Skeet
17

Solo para agregar el valor de mis dos centavos :-) Recientemente aprendí una forma de manejar objetos anónimos. Solo se puede usar cuando se dirige al marco .NET 4 y solo cuando se agrega una referencia a System.Web.dll, pero es bastante simple:

...
using System.Web.Routing;
...

class Program
{
    static void Main(string[] args)
    {

        object anonymous = CallMethodThatReturnsObjectOfAnonymousType();
        //WHAT DO I DO WITH THIS?
        //I know! I'll use a RouteValueDictionary from System.Web.dll
        RouteValueDictionary rvd = new RouteValueDictionary(anonymous);
        Console.WriteLine("Hello, my name is {0} and I am a {1}", rvd["Name"], rvd["Occupation"]);
    }

    private static object CallMethodThatReturnsObjectOfAnonymousType()
    {
        return new { Id = 1, Name = "Peter Perhac", Occupation = "Software Developer" };
    }
}

Para poder agregar una referencia a System.Web.dll, deberá seguir los consejos de rushonerok : Asegúrese de que el marco de destino [del proyecto] sea ".NET Framework 4" y no ".NET Framework 4 Client Profile".

Peter Perháč
fuente
2
ASP.NET Mvc School;)
T-moty
8

No, no puede devolver tipos anónimos sin pasar por algunos trucos.

Si no estuviera usando C #, lo que estaría buscando (devolver datos múltiples sin un tipo concreto) se llama Tupla.

Hay muchas implementaciones de tuplas C #, usando la que se muestra aquí , su código funcionaría así.

public IEnumerable<Tuple<Dog,Breed>> GetDogsWithBreedNames()
{
    var db = new DogDataContext(ConnectString);
    var result = from d in db.Dogs
                 join b in db.Breeds on d.BreedId equals b.BreedId
                 select new Tuple<Dog,Breed>(d, b);

    return result;
}

Y en el sitio de llamadas:

void main() {
    IEnumerable<Tuple<Dog,Breed>> dogs = GetDogsWithBreedNames();
    foreach(Tuple<Dog,Breed> tdog in dogs)
    {
        Console.WriteLine("Dog {0} {1}", tdog.param1.Name, tdog.param2.BreedName);
    }
}
joshperry
fuente
77
Esto no funciona. Lanza una excepción NotSupported : solo los constructores e inicializadores sin parámetros son compatibles con LINQ to Entities
mshsayem
1
Es cierto que la clase Tuple no tiene un constructor predeterminado y no funcionará correctamente con LINQ to Entities de esta manera. Sin embargo, funciona bien con LINQ to SQL como en la pregunta. No he probado esto, pero puede funcionar ... select Tuple.Create(d, b).
joshperry
1
Dado que algunos proveedores de LINQ no admiten las tuplas, ¿no podría seleccionar un tipo anónimo, convertirlo a IEnumerable y luego seleccionar una tupla?
TehPers
8

Podrías hacer algo como esto:


public System.Collections.IEnumerable GetDogsWithBreedNames()
{
    var db = new DogDataContext(ConnectString);
    var result = from d in db.Dogs
                 join b in db.Breeds on d.BreedId equals b.BreedId
                 select new
                        {
                            Name = d.Name,
                            BreedName = b.BreedName
                        };
    return result.ToList();
}
tjscience
fuente
8

ToList()Primero debe usar el método para tomar filas de la base de datos y luego seleccionar elementos como una clase. Prueba esto:

public partial class Dog {
    public string BreedName  { get; set; }}

List<Dog> GetDogsWithBreedNames(){
    var db = new DogDataContext(ConnectString);
    var result = (from d in db.Dogs
                  join b in db.Breeds on d.BreedId equals b.BreedId
                  select new
                  {
                      Name = d.Name,
                      BreedName = b.BreedName
                  }).ToList()
                    .Select(x=> 
                          new Dog{
                              Name = x.Name,
                              BreedName = x.BreedName,
                          }).ToList();
return result;}

Entonces, el truco es primeroToList() . Inmediatamente realiza la consulta y obtiene los datos de la base de datos. El segundo truco consiste en seleccionar elementos y usar el inicializador de objetos para generar nuevos objetos con elementos cargados.

Espero que esto ayude.

Hakan KOSE
fuente
8

¡En C # 7 ahora puede usar tuplas! ... lo que elimina la necesidad de crear una clase solo para devolver el resultado.

Aquí hay un código de muestra:

public List<(string Name, string BreedName)> GetDogsWithBreedNames()
{
    var db = new DogDataContext(ConnectString);
    var result = from d in db.Dogs
             join b in db.Breeds on d.BreedId equals b.BreedId
             select new
             {
                Name = d.Name,
                BreedName = b.BreedName
             }.ToList();

    return result.Select(r => (r.Name, r.BreedName)).ToList();
}

Sin embargo, es posible que deba instalar el paquete Nuget System.ValueTuple.

Rosdi Kasim
fuente
4

Ahora me doy cuenta de que el compilador no me permite devolver un conjunto de tipos anónimos, ya que espera perros, pero ¿hay alguna manera de devolver esto sin tener que crear un tipo personalizado?

Use use object para devolver una lista de tipos anónimos sin crear un tipo personalizado. Esto funcionará sin el error del compilador (en .net 4.0). Devolví la lista al cliente y luego la analicé en JavaScript:

public object GetDogsWithBreedNames()
{
    var db = new DogDataContext(ConnectString);
    var result = from d in db.Dogs
                 join b in db.Breeds on d.BreedId equals b.BreedId
                 select new
                        {
                            Name = d.Name,
                            BreedName = b.BreedName
                        };
    return result;
}
Sergey
fuente
1
En mi opinión, sería más correcto y legible si la firma de su método tuviera el siguiente aspecto: public IEnumerable <object> GetDogsWithBreedNames ()
pistol-pete
3

Simplemente seleccione perros, luego use dog.Breed.BreedName, esto debería funcionar bien.

Si tiene muchos perros, use DataLoadOptions.LoadWith para reducir la cantidad de llamadas db.

Andrey Shchekin
fuente
2

No puede devolver tipos anónimos directamente, pero puede recorrerlos a través de su método genérico. Lo mismo ocurre con la mayoría de los métodos de extensión LINQ. No hay magia allí, aunque parece que devolverían tipos anónimos. Si el parámetro es anónimo, el resultado también puede ser anónimo.

var result = Repeat(new { Name = "Foo Bar", Age = 100 }, 10);

private static IEnumerable<TResult> Repeat<TResult>(TResult element, int count)
{
    for(int i=0; i<count; i++)
    {
        yield return element;
    }
}

Debajo de un ejemplo basado en el código de la pregunta original:

var result = GetDogsWithBreedNames((Name, BreedName) => new {Name, BreedName });


public static IQueryable<TResult> GetDogsWithBreedNames<TResult>(Func<object, object, TResult> creator)
{
    var db = new DogDataContext(ConnectString);
    var result = from d in db.Dogs
                    join b in db.Breeds on d.BreedId equals b.BreedId
                    select creator(d.Name, b.BreedName);
    return result;
}
George Mamaladze
fuente
0

Bueno, si regresas a Dogs, harías:

public IQueryable<Dog> GetDogsWithBreedNames()
{
    var db = new DogDataContext(ConnectString);
    return from d in db.Dogs
           join b in db.Breeds on d.BreedId equals b.BreedId
           select d;
}

Si desea la Raza cargada de entusiasmo y no de carga perezosa, simplemente use la construcción de DataLoadOptions adecuada .

Dave Markle
fuente
0

BreedIden la Dogtabla es obviamente una clave foránea para la fila correspondiente en la Breedtabla. Si tiene su base de datos configurada correctamente, LINQ to SQL debería crear automáticamente una asociación entre las dos tablas. La clase de perro resultante tendrá una propiedad de raza, y la clase de raza debe tener una colección de perros. Al configurarlo de esta manera, aún puede regresar IEnumerable<Dog>, que es un objeto que incluye la propiedad de raza. La única advertencia es que debe precargar el objeto de raza junto con los objetos de perro en la consulta para que se pueda acceder después de que se haya eliminado el contexto de datos y (como ha sugerido otro póster) ejecutar un método en la colección que causará el consulta que se realizará de inmediato (ToArray en este caso):

public IEnumerable<Dog> GetDogs()
{
    using (var db = new DogDataContext(ConnectString))
    {
        db.LoadOptions.LoadWith<Dog>(i => i.Breed);
        return db.Dogs.ToArray();
    }

}

Entonces es trivial acceder a la raza para cada perro:

foreach (var dog in GetDogs())
{
    Console.WriteLine("Dog's Name: {0}", dog.Name);
    Console.WriteLine("Dog's Breed: {0}", dog.Breed.Name);        
}
kad81
fuente
0

Si la idea principal es hacer que la instrucción de selección SQL enviada al servidor de la base de datos tenga solo los campos requeridos, y no todos los campos de la entidad, entonces puede hacer esto:

public class Class1
{
    public IList<Car> getCarsByProjectionOnSmallNumberOfProperties()
    {

        try
        {
            //Get the SQL Context:
            CompanyPossessionsDAL.POCOContext.CompanyPossessionsContext dbContext 
                = new CompanyPossessionsDAL.POCOContext.CompanyPossessionsContext();

            //Specify the Context of your main entity e.g. Car:
            var oDBQuery = dbContext.Set<Car>();

            //Project on some of its fields, so the created select statment that is
            // sent to the database server, will have only the required fields By making a new anonymouse type
            var queryProjectedOnSmallSetOfProperties 
                = from x in oDBQuery
                    select new
                    {
                        x.carNo,
                        x.eName,
                        x.aName
                    };

            //Convert the anonymouse type back to the main entity e.g. Car
            var queryConvertAnonymousToOriginal 
                = from x in queryProjectedOnSmallSetOfProperties
                    select new Car
                    {
                        carNo = x.carNo,
                        eName = x.eName,
                        aName = x.aName
                    };

            //return the IList<Car> that is wanted
            var lst = queryConvertAnonymousToOriginal.ToList();
            return lst;

        }
        catch (Exception ex)
        {
            System.Diagnostics.Debug.WriteLine(ex.ToString());
            throw;
        }
    }
}
Reader Man San
fuente
0

Pruebe esto para obtener datos dinámicos. Puede convertir el código para la Lista <>

public object GetDogsWithBreedNames()
{
    var db = new DogDataContext(ConnectString);
    var result = from d in db.Dogs
                 join b in db.Breeds on d.BreedId equals b.BreedId
                 select new
                        {
                            Name = d.Name,
                            BreedName = b.BreedName
                        };
    return result.FirstOrDefault();
}

dynamic dogInfo=GetDogsWithBreedNames();
var name = dogInfo.GetType().GetProperty("Name").GetValue(dogInfo, null);
var breedName = dogInfo.GetType().GetProperty("BreedName").GetValue(dogInfo, null);
Yargicx
fuente
0

Si tiene una configuración de relación en su base de datos con una restricción de clave extranjera en BreedId, ¿no lo tiene ya?

Mapeo de relaciones DBML

Entonces ahora puedo llamar:

internal Album GetAlbum(int albumId)
{
    return Albums.SingleOrDefault(a => a.AlbumID == albumId);
}

Y en el código que llama eso:

var album = GetAlbum(1);

foreach (Photo photo in album.Photos)
{
    [...]
}

Entonces, en su caso, llamaría algo así como dog.Breed.BreedName, como dije, esto se basa en que su base de datos se configure con estas relaciones.

Como otros han mencionado, DataLoadOptions ayudará a reducir las llamadas a la base de datos si eso es un problema.

Zhaph - Ben Duguid
fuente
0

Esto no responde exactamente a su pregunta, pero Google me llevó aquí en función de las palabras clave. Así es como puede consultar un tipo anónimo de una lista:

var anon = model.MyType.Select(x => new { x.Item1, x.Item2});
Daniel
fuente