Restituire risultati di tipo anonimo?


194

Utilizzando il semplice esempio seguente, qual è il modo migliore per restituire risultati da più tabelle usando Linq to SQL?

Di 'che ho due tavoli:

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

Voglio restituire tutti i cani con loro BreedName. Dovrei convincere tutti i cani a usare qualcosa del genere senza problemi:

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

Ma se voglio cani con razze e provare questo ho problemi:

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

Ora mi rendo conto che il compilatore non mi consente di restituire un set di tipi anonimi poiché si aspetta Dogs, ma c'è un modo per restituirlo senza dover creare un tipo personalizzato? O devo creare la mia classe per DogsWithBreedNamese specificare quel tipo nella selezione? O c'è un altro modo più semplice?


Solo per curiosità, perché tutti gli esempi di Linq mostrano l'uso di tipi anonimi, se non funzionano. Ad esempio, questo esempio faforeach (var cust in query) Console.WriteLine("id = {0}, City = {1}", cust.CustomerID, cust.City);
Hot Licks

@Hot Licks - la tabella Customer in quegli esempi è un'entità rappresentata da una classe. L'esempio non sembra mostrare le definizioni di quelle classi.
Jonathan S.

Né ti dice che un swizzle del compilatore sta sostituendo "var" con il nome della classe.
Hot Licks

Risposte:


213

Tendo ad andare per questo modello:

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 che hai una classe in più, ma è facile e veloce da programmare, facilmente estensibile, riutilizzabile e sicuro.


Mi piace questo approccio ma ora non sono sicuro di come visualizzare il nome del cane. Se sto vincolando il risultato a un DataGrid, posso ottenere le proprietà da Dog senza definirle esplicitamente nella classe DogWithBreed o devo creare il getter / setter per ogni campo che voglio visualizzare?
Jonathan S.

4
DataGrids non ti consente di specificare la proprietà come "Dog.Name"? Ora dimentico perché li odio abbastanza da non usarli mai ...
Teedyay,

@JonathanS. come hai fatto nella colonna modello? per favore dimmi che mi trovo in una situazione simile
rahularyansharma

Ehi, mi piace questo metodo per incapsulare le due classi in una. Semplifica il lavoro. Per non parlare della creazione di classi semplici da utilizzare solo nel contesto attuale, lo rendono più pulito.
Linger

6
Questo in realtà non risponde alla domanda dell'OP, "esiste un modo per restituirlo senza dover creare un tipo personalizzato"?
tjscience,

69

È possibile tornare tipi anonimi, ma non è davvero carina .

In questo caso, penso che sarebbe molto meglio creare il tipo appropriato. Se verrà utilizzato solo all'interno del tipo contenente il metodo, rendilo un tipo nidificato.

Personalmente vorrei che C # ottenga "tipi anonimi nominati", ovvero lo stesso comportamento dei tipi anonimi, ma con nomi e dichiarazioni di proprietà, ma il gioco è fatto.

EDIT: Altri stanno suggerendo di restituire cani e quindi di accedere al nome della razza tramite un percorso di proprietà ecc. Questo è un approccio perfettamente ragionevole, ma l'IME porta a situazioni in cui hai fatto una query in un modo particolare a causa dei dati che desideri uso - e che le meta-informazioni vengono perse quando si ritorna IEnumerable<Dog>- la query potrebbe essere in attesa di utilizzare (ad esempio) Breedpiuttosto che a Ownercausa di alcune opzioni di caricamento, ecc., ma se lo si dimentica e si inizia a utilizzare altre proprietà, l'app potrebbe funzionare ma non nel modo più efficiente che avevi previsto inizialmente. Certo, potrei parlare di spazzatura, o troppo ottimizzazione, ecc ...


3
Ehi, non sono uno che non vuole funzionalità a causa della paura per il modo in cui verranno abusati, ma puoi immaginare i tipi di codice crufty che vedremmo se permettessero che i tipi anonimi vengano distribuiti? (brividi)
Dave Markle,

19
Potremmo vedere alcuni abusi. Potremmo anche vedere un codice molto più semplice in cui vogliamo solo una tupla, in pratica. Non tutto deve essere un oggetto con un comportamento complesso. A volte "solo i dati" è la cosa giusta. IMO, ovviamente.
Jon Skeet,

1
Grazie, quindi la tua preferenza è quella di creare tipi anche se è per una visione unica come questa? Ho molti rapporti che suddividono gli stessi dati in modi diversi e speravo di non dover creare tutti questi tipi diversi (DogsWithBreeds, DogsWithOwnerNames, ecc.)
Jonathan S.

1
Proverei a non doverlo suddividere in molti modi, o mettere la parte di sezionamento nel posto che necessita dei dati in modo da poter usare tipi anonimi - ma oltre a ciò, sì. Fa schifo in qualche modo, ma questa è la vita che temo :(
Jon Skeet

17

Solo per aggiungere il valore dei miei due centesimi :-) Di recente ho imparato un modo di gestire oggetti anonimi. Può essere utilizzato solo quando si utilizza il framework .NET 4 e solo quando si aggiunge un riferimento a System.Web.dll ma è abbastanza semplice:

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

Per poter aggiungere un riferimento a System.Web.dll dovrai seguire i consigli di rushonerok : Assicurati che il framework di destinazione del tuo progetto sia ".NET Framework 4" e non "Profilo client .NET Framework 4".


2
ASP.NET Mvc School;)
T-moty

8

No, non puoi restituire tipi anonimi senza dover affrontare alcuni trucchi.

Se non stavi usando C #, quello che avresti cercato (restituendo più dati senza un tipo concreto) è chiamato Tupla.

Ci sono molte implementazioni di tuple in C #, usando quella mostrata qui , il tuo codice funzionerebbe così.

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

E sul sito chiamante:

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
Questo non funziona. Genera NotSupportedException : solo i costruttori e gli inizializzatori senza parametri sono supportati in LINQ to Entities
mshsayem

1
È vero, la classe Tuple non ha un costruttore predefinito e non funzionerà correttamente con LINQ to Entities in questo modo. Funziona comunque bene con LINQ to SQL come nella domanda. Non l'ho provato, ma potrebbe funzionare ... select Tuple.Create(d, b).
joshperry,

1
Poiché le tuple non sono supportate da alcuni provider LINQ, non è possibile selezionare un tipo anonimo, convertirlo in IEnumerable, quindi selezionare una tupla da quello?
TehPers,

8

Potresti fare qualcosa del genere:


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

È necessario utilizzare ToList()prima il metodo per prendere le righe dal database e quindi selezionare gli elementi come classe. Prova questo:

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

Quindi, il trucco è il primoToList() . Esegue immediatamente la query e ottiene i dati dal database. Il secondo trucco è selezionare gli oggetti e usare l'inizializzatore di oggetti per generare nuovi oggetti con oggetti caricati.

Spero che questo ti aiuti.


8

In C # 7 ora puoi usare le tuple! ... che elimina la necessità di creare una classe solo per restituire il risultato.

Ecco un codice di esempio:

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

Tuttavia, potrebbe essere necessario installare il pacchetto nuget System.ValueTuple.


4

Ora mi rendo conto che il compilatore non mi consente di restituire un set di tipi anonimi poiché si aspetta Dogs, ma c'è un modo per restituirlo senza dover creare un tipo personalizzato?

Utilizzare use object per restituire un elenco di tipi anonimi senza creare un tipo personalizzato. Funzionerà senza l'errore del compilatore (in .net 4.0). Ho restituito l'elenco al client e quindi l'ho analizzato su 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
Secondo me sarebbe più corretto e leggibile se la tua firma del metodo fosse simile a questa: public IEnumerable <oggetto> GetDogsWithBreedNames ()
pistola-pete

3

Basta selezionare i cani, quindi utilizzare dog.Breed.BreedName , questo dovrebbe funzionare bene.

Se hai molti cani, usa DataLoadOptions.LoadWith per ridurre il numero di chiamate db.


2

Non è possibile restituire direttamente tipi anonimi, ma è possibile eseguirne il ciclo attraverso il metodo generico. Quindi fai la maggior parte dei metodi di estensione LINQ. Non c'è magia lì dentro, mentre sembra che restituirebbero tipi anonimi. Se il parametro è anonimo, il risultato può anche essere anonimo.

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

Di seguito un esempio basato sul codice della domanda originale:

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

Bene, se stai tornando Cani, faresti:

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

Se vuoi che Breed sia caricato con entusiasmo e non caricato in modo pigro, usa semplicemente il costrutto DataLoadOptions appropriato .


0

BreedIdnella Dogtabella è ovviamente una chiave esterna per la riga corrispondente nella Breedtabella. Se il database è configurato correttamente, LINQ to SQL dovrebbe creare automaticamente un'associazione tra le due tabelle. La classe Cane risultante avrà una proprietà Razza e la classe Razza dovrebbe avere una collezione Cani. Impostandolo in questo modo, puoi comunque tornare IEnumerable<Dog>, che è un oggetto che include la proprietà razza. L'unica avvertenza è che è necessario precaricare l'oggetto razza insieme agli oggetti cane nella query in modo che sia possibile accedervi dopo che il contesto dei dati è stato eliminato e (come ha suggerito un altro poster) eseguire un metodo sulla raccolta che causerà il query da eseguire immediatamente (ToArray in questo caso):

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

}

È quindi banale accedere alla razza per ogni cane:

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

0

Se l'idea principale è fare in modo che l'istruzione select SQL inviata al server Database contenga solo i campi richiesti e non tutti i campi Entity, allora puoi farlo:

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

Prova questo per ottenere dati dinamici. È possibile convertire il codice per Elenco <>

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

Se hai una configurazione delle relazioni nel tuo database con un vincolo di chiave foriegn su BreedId, non lo capisci già?

Mappatura delle relazioni DBML

Quindi ora posso chiamare:

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

E nel codice che lo chiama:

var album = GetAlbum(1);

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

Quindi nel tuo caso chiameresti qualcosa come dog.Breed.BreedName - come ho detto, questo si basa sul fatto che il tuo database sia impostato con queste relazioni.

Come altri hanno già detto, DataLoadOptions aiuterà a ridurre le chiamate al database se questo è un problema.


0

Questo non risponde esattamente alla tua domanda, ma Google mi ha guidato qui in base alle parole chiave. Ecco come è possibile eseguire una query su un tipo anonimo da un elenco:

var anon = model.MyType.Select(x => new { x.Item1, x.Item2});
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.