Gdy wynik zapytania LINQ ma zawierać tylko kilka potrzebnych pól, tworzenie osobnej klasy często jest zwyczajnie przesadą. Typ anonimowy w C# pozwala zbudować taki obiekt od ręki, zachować kontrolę typów i nie zaśmiecać projektu klasami używanymi tylko w jednym miejscu. Pokażę, jak działa, gdzie sprawdza się najlepiej, jakie ma ograniczenia oraz kiedy lepiej zastąpić go krotką, rekordem albo klasą DTO.
Typ anonimowy najlepiej sprawdza się jako lokalny wynik projekcji
-
Tworzenie odbywa się za pomocą składni
new { ... }, najczęściej z użyciemvar. - Właściwości można odczytywać, ale nie można przypisywać im nowych wartości po utworzeniu obiektu.
- LINQ to najczęstsze miejsce użycia, szczególnie przy wybieraniu tylko części danych.
- Zakres lokalny jest jego mocną stroną, ale równocześnie ogranicza użycie w publicznych API.
- Klasa, rekord lub krotka będą lepsze, gdy dane mają opuszczać metodę albo tworzyć stabilny kontrakt.

Jak działa typ anonimowy w C#
Typ anonimowy to obiekt, którego klasy nie nazywam samodzielnie. Kompilator tworzy odpowiedni typ na podstawie właściwości zapisanych w inicjalizatorze. W kodzie widzę tylko zmienną i jej strukturę, a szczegóły wygenerowanej klasy pozostają po stronie kompilatora. Tak opisuje go również Microsoft Learn.
var user = new
{
Id = 42,
Name = "Anna",
IsActive = true
};
Console.WriteLine(user.Name);W tym przykładzie powstaje obiekt z trzema właściwościami. Ich typy wynikają z przypisanych wartości, więc Id jest typu int, Name typu string, a IsActive typu bool. To nadal jest typowanie statyczne, a var nie oznacza tutaj dynamic.
Można też skrócić zapis, gdy nazwa właściwości ma być taka sama jak nazwa zmiennej.
int id = 7;
string name = "Marek";
var customer = new
{
id,
name
};Kompilator utworzy właściwości o nazwach id i name. W praktyce korzystam z tej składni głównie wtedy, gdy obiekt żyje krótko i nie ma sensu nadawać mu osobnej nazwy domenowej.
Nie można zmieniać właściwości
Właściwości typu anonimowego są tylko do odczytu po utworzeniu obiektu. Próba wykonania poniższego przypisania zakończy się błędem kompilacji.
var product = new
{
Name = "Klawiatura",
Price = 299.99m
};
// Błąd kompilacji
// product.Price = 249.99m;Trzeba jednak uważać na słowo „niemutowalny”. Odczyt wartości właściwości jest stabilny, ale nie oznacza to głębokiej niezmienności całego obiektu. Jeżeli właściwość przechowuje listę, sama lista nadal może być modyfikowana.
var result = new
{
Tags = new List { "C#", "LINQ" }
};
result.Tags.Add(".NET"); Niemutowalne są przypisania właściwości, a niekoniecznie obiekty znajdujące się wewnątrz nich. To rozróżnienie ma znaczenie przy przekazywaniu kolekcji lub innych obiektów referencyjnych.
Dlaczego typy anonimowe tak dobrze pasują do LINQ
Największą wartość pokazują podczas projekcji, czyli wybierania i przekształcania danych w zapytaniu. Zamiast zwracać całe encje z bazy lub tworzyć klasę dla jednego wyniku, można wybrać dokładnie te pola, które są potrzebne w danym miejscu.
var activeUsers = users
.Where(user => user.IsActive)
.Select(user => new
{
user.Id,
user.Name,
Email = user.Email.ToLower()
})
.ToList();Każdy element kolekcji zawiera tutaj tylko trzy właściwości. Projekcja ogranicza ilość danych i jednocześnie jasno pokazuje, czego potrzebuje dalsza część kodu. To szczególnie wygodne przy zapytaniach LINQ to Objects, a także przy zapytaniach wykonywanych przez ORM, takim jak Entity Framework Core.
Typ anonimowy może również łączyć dane i obliczać wartości.
var orderSummaries = orders
.Select(order => new
{
OrderId = order.Id,
CustomerName = order.Customer.Name,
Total = order.Items.Sum(item =>
item.Price * item.Quantity)
})
.ToList();Wynik nie jest już kopią zamówienia. To mały model przygotowany do konkretnego zadania, na przykład do wyświetlenia tabeli. W mojej praktyce właśnie takie lokalne modele wynikowe są najczystszym zastosowaniem typów anonimowych.
Łączenie danych z kilku źródeł
Anonimowe obiekty są przydatne także przy łączeniu kolekcji. Można w nich przechować wspólny klucz i wartości pochodzące z różnych elementów.
var report = from order in orders
join customer in customers
on order.CustomerId equals customer.Id
select new
{
OrderNumber = order.Number,
Customer = customer.Name,
order.Total
};Taki wynik jest czytelniejszy niż przekazywanie pełnych obiektów Order i Customer. Jeżeli raport jest używany tylko w jednej metodzie, tworzenie osobnego typu może nie dać żadnej dodatkowej korzyści.
Ograniczenia, o których łatwo zapomnieć
Najważniejsze ograniczenie wynika z samej nazwy. Typ anonimowy nie ma nazwy, którą można wpisać w deklaracji metody, właściwości lub pola. Dlatego poniższy kod nie jest poprawnym sposobem projektowania publicznego kontraktu.
// Nie można zapisać typu anonimowego jako typu zwracanego
// public ??? GetUser() { ... }Można technicznie zwrócić object albo dynamic, ale wtedy traci się wygodę typowania statycznego. Przy object trzeba rzutować wynik, a przy dynamic błędy pojawią się dopiero w czasie wykonania programu. Nie traktuję tego jako dobrego zamiennika dla DTO.
Typ anonimowy nie nadaje się również do bibliotek, warstw aplikacji i publicznych endpointów, gdzie struktura danych powinna być widoczna i stabilna. W takich miejscach lepiej utworzyć nazwany rekord albo klasę.
Zakres użycia ma znaczenie
Najwygodniej używać go lokalnie, w obrębie jednej metody, zapytania lub krótkiego potoku przetwarzania. Można przekazać taki obiekt do lambdy, przechowywać go w kolekcji i odczytywać jego właściwości, ale jego nazwa nie jest dostępna w kodzie aplikacji.
Nie warto też zamieniać var na dynamic tylko dlatego, że typ został wygenerowany przez kompilator. var zachowuje informacje o właściwościach i pozwala IDE wykrywać błędy. dynamic przenosi sprawdzanie poprawności na później, co w tym przypadku zwykle jest krokiem wstecz.
Porównywanie obiektów
Typy anonimowe mają wygenerowane implementacje Equals, GetHashCode i ToString. Dwa obiekty o tej samej strukturze właściwości mogą być porównywane wartościowo, o ile mają te same nazwy, typy i kolejność właściwości.
var first = new
{
Id = 1,
Name = "Kasia"
};
var second = new
{
Id = 1,
Name = "Kasia"
};
bool areEqual = first.Equals(second); // TrueNie opierałbym jednak kluczowej logiki domenowej na tej właściwości. Działa ona dobrze w lokalnych projekcjach i testach, ale nazwany rekord wyraźniej komunikuje intencję i jest łatwiejszy do rozwijania.
Typ anonimowy, krotka, rekord czy klasa
Te konstrukcje często rozwiązują podobny problem, ale nie są wymienne. Najprostsze kryterium to pytanie, czy wynik ma pozostać lokalnym szczegółem implementacji, czy stanie się częścią interfejsu między metodami albo warstwami.
| Rozwiązanie | Najlepsze zastosowanie | Najważniejsze ograniczenie |
|---|---|---|
| Typ anonimowy | Lokalna projekcja LINQ i tymczasowy wynik | Brak nazwanej struktury do użycia w publicznym API |
| Krotka | Krótki wynik metody z kilkoma wartościami | Przy większej liczbie elementów szybko traci czytelność |
| Rekord | Nazwany model danych z porównaniem wartościowym | Wymaga utworzenia osobnego typu |
| Klasa DTO | Kontrakt API, serializacja i komunikacja między warstwami | Więcej kodu i osobny plik lub deklaracja |
Krotka jest dobrym wyborem, gdy metoda zwraca na przykład parę (bool Success, string Message). Typ anonimowy będzie wygodniejszy, gdy potrzebuję kilku nazwanych pól w projekcji LINQ, ale nie chcę publikować modelu.
Rekord wygrywa wtedy, gdy dane mają własne znaczenie biznesowe. Jeżeli obiekt opisuje na przykład InvoiceSummary albo SearchResult, nazwa pomaga czytelnikowi kodu zrozumieć jego rolę. Mały koszt utworzenia typu często zwraca się przy pierwszej zmianie wymagań.
Praktyczne wzorce i częste błędy
Dobry wzorzec lokalnej projekcji
Typ anonimowy sprawdza się, gdy wynik jest konsumowany od razu, bez przechodzenia przez granicę warstwy.
var cards = products
.Where(product => product.IsAvailable)
.Select(product => new
{
product.Name,
PriceLabel = $"{product.Price:0.00} zł",
HasDiscount = product.DiscountPercent > 0
})
.ToList();
foreach (var card in cards)
{
Console.WriteLine($"{card.Name}: {card.PriceLabel}");
}W tym przypadku format ceny i informacja o rabacie są częścią prezentacji. Nie muszę tworzyć klasy tylko po to, aby użyć jej kilka linijek dalej. Kod pozostaje krótki, a właściwości są dostępne z pełnym wsparciem kompilatora.
Kiedy utworzyć nazwany model
Osobny typ wybieram, gdy wynik jest używany w więcej niż jednym miejscu, jest zwracany z publicznej metody albo będzie serializowany jako kontrakt API. Dotyczy to również sytuacji, w której obiekt ma z czasem dostać walidację, metody pomocnicze, dokumentację lub dodatkowe reguły.
public sealed record ProductCard(
string Name,
decimal Price,
bool HasDiscount);
public IEnumerable GetAvailableProducts()
{
return products
.Where(product => product.IsAvailable)
.Select(product => new ProductCard(
product.Name,
product.Price,
product.DiscountPercent > 0));
} Ten zapis jest dłuższy, ale model ma nazwę i może bez problemu pojawić się w sygnaturze metody. To ważniejsze niż oszczędność kilku linii kodu.
Przeczytaj również: Enum w C# - jak używać go bezpiecznie?
Błąd polegający na użyciu obiektu
Umieszczenie wyniku w zmiennej typu object zwykle psuje cały sens rozwiązania.
object data = new
{
Id = 10,
Name = "Monitor"
};
// Nie można bezpośrednio odczytać data.NameInformacja o właściwościach nadal istnieje w obiekcie, ale typ referencji object jej nie udostępnia. Jeżeli potrzebuję swobodnie przekazywać dane, powinienem użyć nazwanej klasy, rekordu albo świadomie zaprojektowanego słownika.
Najmniejszy typ nie zawsze jest najlepszym typem
Typ anonimowy jest świetnym narzędziem do krótkich, lokalnych operacji. Używam go przede wszystkim do projekcji LINQ, składania danych z kilku źródeł i przygotowywania wyniku konsumowanego natychmiast w tej samej metodzie.
Granica jest prosta. Gdy obiekt ma przejść do innej warstwy, stać się odpowiedzią API, być używany w wielu miejscach albo reprezentować pojęcie biznesowe, wybieram rekord lub klasę DTO. Kilka dodatkowych linii deklaracji daje wtedy czytelny kontrakt, łatwiejsze testowanie i większą odporność kodu na rozwój aplikacji.
Najlepsza decyzja nie polega więc na tym, aby zawsze unikać nazwanych typów. Chodzi o dopasowanie narzędzia do zasięgu danych: anonimowy obiekt dla lokalnego szczegółu, nazwany model dla informacji, która ma żyć dłużej.
