Renvoyer des résultats de type anonyme?


194

En utilisant l'exemple simple ci-dessous, quelle est la meilleure façon de renvoyer les résultats de plusieurs tables à l'aide de Linq vers SQL?

Disons que j'ai deux tables:

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

Je veux rendre tous les chiens avec leur BreedName. Je devrais obliger tous les chiens à utiliser quelque chose comme ça sans problème:

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

Mais si je veux des chiens de races et que j'essaye, j'ai des problèmes:

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

Maintenant, je me rends compte que le compilateur ne me permet pas de renvoyer un ensemble de types anonymes car il attend des chiens, mais existe-t-il un moyen de le retourner sans avoir à créer un type personnalisé? Ou dois-je créer ma propre classe pour DogsWithBreedNameset spécifier ce type dans la sélection? Ou existe-t-il un autre moyen plus simple?


Par curiosité, pourquoi tous les exemples Linq montrent-ils l'utilisation de types anonymes, s'ils ne fonctionnent pas. Par exemple, cet exemple le faitforeach (var cust in query) Console.WriteLine("id = {0}, City = {1}", cust.CustomerID, cust.City);
Hot Licks

@Hot Licks - la table Customer dans ces exemples est une entité représentée par une classe. L'exemple ne semble tout simplement pas montrer les définitions de ces classes.
Jonathan S.

Cela ne vous dit pas non plus qu'un swizzle du compilateur remplace "var" par le nom de la classe.
Hot Licks

Réponses:


213

J'ai tendance à opter pour ce modèle:

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

Cela signifie que vous avez une classe supplémentaire, mais elle est rapide et facile à coder, facilement extensible, réutilisable et sécurisée.


J'aime cette approche mais maintenant je ne sais pas comment afficher le nom du chien. Si je lie le résultat à un DataGrid, puis-je obtenir les propriétés de Dog sans les définir explicitement dans la classe DogWithBreed ou dois-je créer le getter / setter pour chaque champ que je veux afficher?
Jonathan S.

4
DataGrids ne vous permet-il pas de spécifier la propriété en tant que "Dog.Name"? J'oublie maintenant pourquoi je les déteste assez pour ne jamais les utiliser ...
teedyay

@JonathanS. comment avez-vous fait cela dans la colonne du modèle? dites-moi que je suis dans une situation similaire
rahularyansharma

Hé, j'aime cette méthode pour encapsuler les deux classes en une seule. Il est facile de travailler avec. Sans parler de la création de classes simples à utiliser uniquement dans le contexte actuel, rendez-le plus propre.
Linger

6
Cela ne répond vraiment pas à la question posée par l'OP: "existe-t-il un moyen de retourner cela sans avoir à créer un type personnalisé"?
tjscience

69

Vous pouvez renvoyer des types anonymes, mais ce n'est vraiment pas joli .

Dans ce cas, je pense qu'il serait bien préférable de créer le type approprié. S'il ne doit être utilisé qu'à partir du type contenant la méthode, faites-en un type imbriqué.

Personnellement, j'aimerais que C # obtienne des "types anonymes nommés" - c'est-à-dire le même comportement que les types anonymes, mais avec des noms et des déclarations de propriété, mais c'est tout.

EDIT: D'autres suggèrent de retourner les chiens, puis d'accéder au nom de la race via un chemin de propriété, etc. C'est une approche parfaitement raisonnable, mais IME cela conduit à des situations où vous avez fait une requête d'une manière particulière en raison des données que vous souhaitez utiliser - et que les méta-informations sont perdues lorsque vous revenez juste IEnumerable<Dog>- la requête peut s'attendre à ce que vous utilisiez (disons) Breedplutôt qu'en Ownerraison de certaines options de chargement, etc., mais si vous l'oubliez et commencez à utiliser d'autres propriétés, votre application peut fonctionner mais pas aussi efficacement que vous l'aviez prévu à l'origine. Bien sûr, je pourrais parler de détritus, ou de sur-optimisation, etc ...


3
Hé, je ne suis pas du genre à ne pas vouloir des fonctionnalités à cause de la peur de leur utilisation abusive, mais pouvez-vous imaginer les types de codes cruels que nous verrions s'ils permettaient la diffusion de types anonymes nommés? (frisson)
Dave Markle

19
Nous pourrions voir des abus. Nous pourrions également voir un code beaucoup plus simple où nous voulons simplement un tuple, essentiellement. Pas tout doit être un objet avec un comportement complexe. Parfois, «juste les données» est la bonne chose. OMI, bien sûr.
Jon Skeet

1
Merci, donc votre préférence est de créer des types même si c'est pour une vue unique comme celle-ci? J'ai beaucoup de rapports qui coupent les mêmes données de différentes manières et j'espérais ne pas avoir à créer tous ces différents types (DogsWithBreeds, DogsWithOwnerNames, etc.)
Jonathan S.

1
J'essaierais de ne pas avoir besoin de le découper de tant de façons, ou de mettre la partie de découpage à l'endroit qui a besoin des données afin que vous puissiez utiliser des types anonymes - mais au-delà, oui. Ça craint à certains égards, mais telle est la vie, je le crains :(
Jon Skeet

17

Juste pour ajouter ma valeur de deux cents :-) J'ai récemment appris une façon de gérer des objets anonymes. Il ne peut être utilisé que lors du ciblage du framework .NET 4 et uniquement lors de l'ajout d'une référence à System.Web.dll, mais c'est assez 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" };
    }
}

Pour pouvoir ajouter une référence à System.Web.dll, vous devrez suivre les conseils de rushonerok : Assurez-vous que le cadre cible de votre [projet] est ".NET Framework 4" et non ".NET Framework 4 Client Profile".


2
École ASP.NET Mvc;)
T-moty

8

Non, vous ne pouvez pas retourner de types anonymes sans passer par une supercherie.

Si vous n'utilisiez pas C #, ce que vous recherchez (renvoyer plusieurs données sans type concret) s'appelle un Tuple.

Il existe de nombreuses implémentations de tuple C #, en utilisant celle illustrée ici , votre code fonctionnerait comme ceci.

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

Et sur le site appelant:

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

7
Cela ne fonctionne pas. Lève une exception NotSupportedException : seuls les constructeurs et les initialiseurs sans paramètre sont pris en charge dans LINQ to Entities
mshsayem

1
Vrai, la classe Tuple n'a pas de constructeur par défaut et ne fonctionnera pas correctement avec LINQ to Entities de cette manière. Cela fonctionne cependant très bien avec LINQ to SQL comme dans la question. Je n'ai pas essayé cela, mais cela peut fonctionner avec ... select Tuple.Create(d, b).
joshperry

1
Étant donné que Tuples n'est pas pris en charge par certains fournisseurs LINQ, ne pouvez-vous pas sélectionner un type anonyme, le convertir en IEnumerable, puis sélectionner un tuple à partir de cela?
TehPers

8

Vous pouvez faire quelque chose comme ça:


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

8

Vous devez d'abord utiliser la ToList()méthode pour prendre des lignes de la base de données, puis sélectionner les éléments en tant que classe. Essaye ça:

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

Donc, l'astuce est la premièreToList() . Il effectue immédiatement la requête et obtient les données de la base de données. La deuxième astuce consiste à sélectionner des éléments et à utiliser l'initialiseur d'objets pour générer de nouveaux objets avec des éléments chargés.

J'espère que cela t'aides.


8

En C # 7, vous pouvez maintenant utiliser des tuples! ... ce qui élimine le besoin de créer une classe juste pour retourner le résultat.

Voici un exemple de code:

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

Vous devrez peut-être installer le package de nuget System.ValueTuple.


4

Maintenant, je me rends compte que le compilateur ne me permet pas de renvoyer un ensemble de types anonymes car il attend des chiens, mais existe-t-il un moyen de le retourner sans avoir à créer un type personnalisé?

Utilisez use object pour renvoyer une liste de types anonymes sans créer de type personnalisé. Cela fonctionnera sans l'erreur du compilateur (dans .net 4.0). J'ai renvoyé la liste au client, puis je l'ai analysée sur 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;
}

1
À mon avis, il serait plus correct et plus lisible si la signature de votre méthode ressemblait à ceci: public IEnumerable <object> GetDogsWithBreedNames ()
pistol-pete

3

Sélectionnez simplement des chiens, puis utilisez dog.Breed.BreedName, cela devrait fonctionner correctement.

Si vous avez beaucoup de chiens, utilisez DataLoadOptions.LoadWith pour réduire le nombre d'appels db.


2

Vous ne pouvez pas retourner directement des types anonymes, mais vous pouvez les boucler via votre méthode générique. Tout comme la plupart des méthodes d'extension LINQ. Il n'y a pas de magie là-dedans, alors qu'il semble qu'ils renverraient des types anonymes. Si le paramètre est anonyme, le résultat peut également être anonyme.

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

Ci-dessous un exemple basé sur le code de la question d'origine:

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

0

Eh bien, si vous retournez des chiens, vous feriez:

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 vous voulez que Breed soit chargé et non paresseux, utilisez simplement la construction DataLoadOptions appropriée .


0

BreedIddans le Dogtableau est évidemment une clé étrangère vers la ligne correspondante dans le Breedtableau. Si votre base de données est correctement configurée, LINQ to SQL doit automatiquement créer une association entre les deux tables. La classe Dog résultante aura une propriété Breed, et la classe Breed devrait avoir une collection Dogs. En le configurant de cette façon, vous pouvez toujours revenir IEnumerable<Dog>, qui est un objet qui inclut la propriété de la race. La seule mise en garde est que vous devez précharger l'objet de race avec les objets de chien dans la requête afin qu'ils soient accessibles après la suppression du contexte de données et (comme une autre affiche l'a suggéré) exécuter une méthode sur la collection qui provoquera la requête à effectuer immédiatement (ToArray dans ce cas):

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

}

Il est alors trivial d'accéder à la race pour chaque chien:

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

0

Si l'idée principale est de faire en sorte que l'instruction SQL select envoyée au serveur de base de données ne contienne que les champs obligatoires et pas tous les champs Entity, alors vous pouvez le faire:

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

0

Essayez ceci pour obtenir des données dynamiques. Vous pouvez convertir le code de la liste <>

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

0

Si vous avez une configuration de relation dans votre base de données avec une restriction de clé foriegn sur BreedId, ne l'avez-vous pas déjà?

Mappage de relations DBML

Je peux donc maintenant appeler:

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

Et dans le code qui appelle ça:

var album = GetAlbum(1);

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

Donc, dans votre cas, vous appelleriez quelque chose comme dog.Breed.BreedName - comme je l'ai dit, cela dépend de la configuration de votre base de données avec ces relations.

Comme d'autres l'ont mentionné, les DataLoadOptions aideront à réduire les appels de base de données en cas de problème.


0

Cela ne répond pas exactement à votre question, mais Google m'a conduit ici en fonction des mots clés. Voici comment interroger un type anonyme dans une liste:

var anon = model.MyType.Select(x => new { x.Item1, x.Item2});
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.