Zwracać wyniki typu anonimowego?

194

Korzystając z prostego przykładu poniżej, jaki jest najlepszy sposób na zwrócenie wyników z wielu tabel za pomocą Linq do SQL?

Powiedzmy, że mam dwie tabele:

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

Chcę zwrócić wszystkie psy z nimi BreedName. Powinienem sprawić, by wszystkie psy używały czegoś takiego bez problemu:

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

Ale jeśli chcę psów z rasami i spróbuję tego, mam problemy:

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

Teraz zdaję sobie sprawę, że kompilator nie pozwoli mi zwrócić zestawu anonimowych typów, ponieważ oczekuje Psy, ale czy istnieje sposób, aby to zwrócić bez konieczności tworzenia niestandardowego typu? Czy też muszę utworzyć własną klasę DogsWithBreedNamesi określić ten typ w zaznaczeniu? Czy jest inny łatwiejszy sposób?

Jonathan S.
źródło
Właśnie z ciekawości, dlaczego wszystkie przykłady Linq pokazują użycie anonimowych typów, jeśli nie działają. Przykład ten przykład robiforeach (var cust in query) Console.WriteLine("id = {0}, City = {1}", cust.CustomerID, cust.City);
Hot Licks
@Hot Licks - tabela Customer w tych przykładach jest bytem reprezentowanym przez klasę. Ten przykład po prostu nie pokazuje definicji tych klas.
Jonathan S.
Nie mówi też, że zamiana kompilatora zastępuje „var” nazwą klasy.
Hot Licks

Odpowiedzi:

213

Zwykle wybieram ten wzór:

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

Oznacza to, że masz dodatkową klasę, ale kodowanie jest szybkie i łatwe, łatwo rozszerzalne, wielokrotnego użytku i bezpieczne dla typu.

teedyay
źródło
Lubię to podejście, ale teraz nie jestem pewien, jak wyświetlić imię psa. Jeśli wiążę wynik z DataGrid, czy mogę uzyskać właściwości od Dog bez wyraźnego zdefiniowania ich w klasie DogWithBreed, czy też muszę utworzyć obiekt pobierający / ustawiający dla każdego pola, które chcę wyświetlić?
Jonathan S.
4
Czy DataGrids nie pozwalają ci określić właściwości jako „Dog.Name”? Zapominam teraz, dlaczego nienawidzę ich wystarczająco, aby nigdy ich nie używać ...
teedyay
@JonathanS. jak to zrobiłeś w kolumnie z szablonami? proszę powiedz mi, że jestem w podobnej sytuacji
rahularyansharma
Hej, podoba mi się ta metoda kapsułkowania dwóch klas w jedną. Ułatwia pracę. Nie wspominając o tworzeniu prostych klas, które będą używane tylko w bieżącym kontekście, czynią go czystszym.
Linger
6
To naprawdę nie odpowiada na pytanie, jakie miał OP: „Czy istnieje sposób, aby to zwrócić bez konieczności tworzenia niestandardowego typu”?
tjscience
69

Państwo może powrócić anonimowych typów, ale to naprawdę nie jest ładny .

W tym przypadku myślę, że znacznie lepiej byłoby stworzyć odpowiedni typ. Jeśli ma być używany tylko z typu zawierającego metodę, uczyń go typem zagnieżdżonym.

Osobiście chciałbym, aby C # dostał „nazwane typy anonimowe” - tj. Takie samo zachowanie jak typy anonimowe, ale z nazwami i deklaracjami własności, ale to wszystko.

EDYCJA: Inni sugerują powrót psów, a następnie uzyskanie dostępu do nazwy rasy za pośrednictwem ścieżki własności itp. To całkowicie rozsądne podejście, ale IME prowadzi do sytuacji, w których wykonałeś zapytanie w określony sposób ze względu na dane, które chcesz użyj - a meta-informacje zostaną utracone, gdy tylko wrócisz IEnumerable<Dog>- zapytanie może się spodziewać że użyjesz (powiedzmy), Breeda nie z Ownerpowodu niektórych opcji ładowania itp., ale jeśli zapomnisz o tym i zaczniesz używać innych właściwości, aplikacja może działać, ale nie tak skutecznie, jak pierwotnie przewidywano. Oczywiście, mógłbym mówić o śmieciach lub o nadmiernej optymalizacji itp.

Jon Skeet
źródło
3
Hej, nie jestem kimś, kto nie chce cech z powodu strachu przed tym, że zostaną wykorzystane, ale czy możesz sobie wyobrazić, jaki rodzaj podejrzanego kodu zobaczylibyśmy, gdyby pozwolili na wymienienie nazwanych typów anonimowych? (dreszcz)
Dave Markle
19
Możemy zobaczyć pewne nadużycia. Możemy również zobaczyć trochę prostszy kod, w którym po prostu chcemy krotkę. Nie wszystko musi być przedmiotem o złożonym zachowaniu. Czasami „tylko dane” są właściwą rzeczą. Oczywiście IMO.
Jon Skeet
1
Dzięki, więc wolisz tworzyć typy, nawet jeśli dotyczą one jednorazowego widoku takiego jak ten? Mam wiele raportów, które wycinają te same dane na różne sposoby i miałem nadzieję, że nie będę musiał tworzyć wszystkich tych różnych typów (DogsWithBreeds, DogsWithOwnerNames itp.)
Jonathan S.,
1
Staram się nie musieć kroić go na tak wiele sposobów, ani umieszczać części krojenia w miejscu, które potrzebuje danych, abyś mógł używać anonimowych typów - ale poza tym tak. W pewnym sensie jest do bani, ale obawiam się, że takie jest życie :(
Jon Skeet
17

Żeby dodać wartość dwóch centów :-) Niedawno nauczyłem się obsługiwać anonimowe obiekty. Można go używać tylko podczas atakowania frameworku .NET 4 i to tylko podczas dodawania odwołania do System.Web.dll, ale jest to dość proste:

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

Aby móc dodać odwołanie do System.Web.dll, musisz postępować zgodnie ze wskazówkami rushonerok : Upewnij się, że docelową strukturą [projektu] jest „.NET Framework 4”, a nie „.NET Framework 4 Client Profile”.

Peter Perháč
źródło
2
ASP.NET Mvc School;)
T-moty
8

Nie, nie możesz zwrócić anonimowych typów bez przechodzenia przez pewne sztuczki.

Jeśli nie używasz C #, to czego byś szukał (zwracając wiele danych bez konkretnego typu) nazywa się Tuple.

Istnieje wiele implementacji krotek C #, używając tej pokazanej tutaj , twój kod działałby w ten sposób.

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

A na stronie dzwoniącej:

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
źródło
7
To nie działa. Zgłasza wyjątek NotSupportedException : Tylko konstruktory bez parametrów i inicjalizatory są obsługiwane w LINQ do
encji
1
To prawda, że ​​klasa Tuple nie ma domyślnego konstruktora i w ten sposób nie będzie działać poprawnie z LINQ to Entities. Działa to jednak dobrze z LINQ-SQL, jak w pytaniu. Nie próbowałem tego, ale może działać ... select Tuple.Create(d, b).
joshperry
1
Ponieważ krotki nie są obsługiwane przez niektórych dostawców LINQ, czy nie możesz wybrać anonimowego typu, przekonwertować go na IEnumerable, a następnie wybrać krotkę z tego?
TehPers
8

Możesz zrobić coś takiego:


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
źródło
8

ToList()Najpierw musisz użyć metody, aby pobrać wiersze z bazy danych, a następnie wybrać elementy jako klasę. Spróbuj tego:

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

Tak więc sztuczka jest pierwszaToList() . Natychmiast wykonuje zapytanie i pobiera dane z bazy danych. Druga sztuczka to wybieranie przedmiotów i używanie inicjatora obiektów do generowania nowych obiektów z załadowanymi elementami.

Mam nadzieję że to pomoże.

Hakan KOSE
źródło
8

W C # 7 możesz teraz używać krotek! ... co eliminuje potrzebę tworzenia klasy tylko po to, aby zwrócić wynik.

Oto przykładowy kod:

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

Może być jednak konieczne zainstalowanie pakietu nuget System.ValueTuple.

Rosdi Kasim
źródło
4

Teraz zdaję sobie sprawę, że kompilator nie pozwoli mi zwrócić zestawu anonimowych typów, ponieważ oczekuje Psy, ale czy istnieje sposób, aby to zwrócić bez konieczności tworzenia niestandardowego typu?

Użyj obiektu use, aby zwrócić listę typów Anonimowych bez tworzenia typu niestandardowego. Będzie to działać bez błędu kompilatora (w .net 4.0). Zwróciłem listę klientowi, a następnie przeanalizowałem ją w 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;
}
Siergiej
źródło
1
Moim zdaniem bardziej poprawny i czytelny byłby, gdyby podpis metody wyglądał tak: public IEnumerable <object> GetDogsWithBreedNames ()
pistol-pete
3

Po prostu wybierz psy, a następnie użyj dog.Breed.BreedName, to powinno działać dobrze.

Jeśli masz dużo psów, użyj DataLoadOptions.LoadWith, aby zmniejszyć liczbę wywołań db.

Andrey Shchekin
źródło
2

Nie możesz zwrócić anonimowych typów bezpośrednio, ale możesz zapętlić je za pomocą ogólnej metody. Podobnie jak większość metod rozszerzenia LINQ. Nie ma tam magii, choć wygląda na to, że zwrócą anonimowe typy. Jeśli parametr jest anonimowy, wynik może być również anonimowy.

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

Poniżej przykład oparty na kodzie z pierwotnego pytania:

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
źródło
0

Cóż, jeśli oddajesz Psy, zrobiłbyś:

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

Jeśli chcesz rasy ładowanej chętnie, a nie leniwie, po prostu użyj odpowiedniej konstrukcji DataLoadOptions .

Dave Markle
źródło
0

BreedIdw Dogtabeli jest oczywiście klucz obcy do odpowiedniego wiersza w Breedtabeli. Jeśli baza danych jest poprawnie skonfigurowana, LINQ to SQL powinien automatycznie utworzyć powiązanie między dwiema tabelami. Wynikowa klasa psów będzie miała właściwość rasy, a klasa rasy powinna mieć kolekcję psów. Konfigurując go w ten sposób, możesz nadal powrócić IEnumerable<Dog>, który jest przedmiotem zawierającym własność rasy. Jedynym zastrzeżeniem jest to, że musisz wstępnie załadować obiekt rasy wraz z obiektami psa w zapytaniu, aby można było uzyskać do nich dostęp po usunięciu kontekstu danych i (jak sugerował inny plakat) wykonać metodę w kolekcji, która spowoduje zapytanie, które należy wykonać natychmiast (w tym przypadku ToArray):

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

}

Dostęp do rasy dla każdego psa jest więc trywialny:

foreach (var dog in GetDogs())
{
    Console.WriteLine("Dog's Name: {0}", dog.Name);
    Console.WriteLine("Dog's Breed: {0}", dog.Breed.Name);        
}
kad81
źródło
0

Jeśli głównym pomysłem jest, aby instrukcja SQL select wysyłana do serwera bazy danych zawierała tylko wymagane pola, a nie wszystkie pola encji, możesz to zrobić:

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
źródło
0

Spróbuj tego, aby uzyskać dynamiczne dane. Możesz przekonwertować kod dla List <>

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
źródło
0

Jeśli masz konfigurację relacji w bazie danych z ograniczeniem klucza foriegn na Breed, czy już tego nie rozumiesz?

Mapowanie relacji DBML

Mogę teraz zadzwonić:

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

A w kodzie, który to nazywa:

var album = GetAlbum(1);

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

Więc w twoim przypadku będziesz nazywał coś w rodzaju dog.Breed.BreedName - jak powiedziałem, zależy to od skonfigurowania bazy danych z tymi relacjami.

Jak wspomnieli inni, DataLoadOptions pomoże zmniejszyć liczbę wywołań bazy danych, jeśli jest to problem.

Zhaph - Ben Duguid
źródło
0

To nie do końca odpowiada na twoje pytanie, ale Google zaprowadziło mnie tutaj na podstawie słów kluczowych. W ten sposób możesz wyszukać anonimowy typ z listy:

var anon = model.MyType.Select(x => new { x.Item1, x.Item2});
Daniel
źródło