DynamicWhere.ex
DynamicWhere.exv3.4.0·docs

.Select<T>(fields)

Projects only the specified fields into a new instance of T. Supports direct properties, whole navigation objects/collections, and nested navigation paths — including paths that traverse collection properties.

Signature

public static IQueryable<T> Select<T>(this IQueryable<T> query, List<string> fields)
    where T : class
ParameterTypeDescription
queryIQueryable<T>Source query to project
fieldsList<string>Property paths to include

Projection rules

Path styleBehaviourExample inputEffect on result
Direct scalarBound directly"Name"Name: "Laptop"
Whole navigation object (non-dotted)Bound as-is"Category"Category: { Id: 5, Name: "Electronics", … }
Whole navigation collection (non-dotted)Bound as-is"Brands"Brands: [{ Id: 1, … }, …]
Dotted through reference navigationRecursively projected"Category.Name"Category: { Id: …, Name: "Electronics" }
Dotted through collection navigationPer-element Select().ToList()"Category.Vendors.Id"Category: { Vendors: [{ Id: 1 }, …] }
Multi-level (reference + collection)Nested recursively"Category.Vendors.Product.Name"Category: { Vendors: [{ Product: { Name: "…" } }] }
Dotted through an application's own struct (3.4.0)Built member by member"Name.Ar"Name: { Ar: "…", En: null }
Dotted through a collection of structs (3.4.0)Per-element Select().ToList(), or ToArray() for an array"Pairs.Shown"Pairs: [{ Shown: "…", Hidden: null }, …]
Dotted beneath a value type the framework declaresLeft unbound"CreatedAt.Year"CreatedAt keeps its default
Note
For nested entities (reference or collection), the Id property is automatically included alongside any requested sub-fields — but only when the nested type actually declares an Id.

Structs (3.4.0)

An application's own struct — a value type outside the System namespaces — is built with exactly what was named, and no Id is added: Name.En keeps its default when only Name.Ar is named. Nested structs are built level by level, a collection inside a struct is carried, and the struct is read with a plain member access, so it works on EF Core and in memory. Until 3.4.0 every value-typed member a path went beneath was skipped, so Select(["Name.Ar"]) returned the struct's default, an empty name.

  • A nullable struct is named through Value: "Alias.Value.Ar" builds it where it has a value and leaves it null where it has none. "Alias.Value" whole, or "Alias.HasValue" alone, leaves it unbound.
  • A collection of structs is built element by element, into a List<T>, any member type a list can be assigned to, or a T[]. A null collection stays null, and a collection of scalars is still bound whole. Until 3.4.0 the collection was bound whole, so each element came back with every member.
  • Left unbound: a class navigation inside a struct (its siblings are built), a struct or an element in which nothing named can be set, a collection of nullable structs, and a path beneath a framework value type such as "CreatedAt.Year" — a typed row cannot hold the year apart from the date. SelectDynamic carries such a path as it is named.
  • On EF Core, a list of structs the source projection builds with a collection initializer, new List<Pair> { new Pair { … } }, cannot be selected from again: a path beneath it fails with InvalidOperationException ("could not be translated"), as it always did for SelectDynamic. A list the projection reads from a query is built as named.

Validations

  • query and fields cannot be null.
  • fields must have at least one entry.
  • Every field must exist on T (case-insensitive, auto-normalized).
  • T must have a parameterless constructor — checked at run time, not by a type constraint — SelectTypeMustHaveParameterlessConstructor.
Warning
Parameterless constructor required. .Select<T>(fields) requires T to have a parameterless (default) constructor. If T does not have one, a LogicException is thrown whose Message is the stable code SelectTypeMustHaveParameterlessConstructor and whose Subject is T's name. Before 3.1.0 the message was an English sentence with the type name inside it — see breaking changes. Most EF Core entity classes have parameterless constructors by default. If your type does not, use .SelectDynamic<T> instead.

Returns

IQueryable<T> — a projected query. New instances of T are constructed via the parameterless constructor and member-bound from the requested fields.

Examples

Direct scalars.

var projected = dbContext.Products
    .Select(new List<string> { "Id", "Name", "Price" });

Dotted path through a reference navigation.

var projected = dbContext.Products
    .Select(new List<string> { "Id", "Name", "Category.Name" });

Category is projected with only the requested Name sub-field (Id auto-included).

Dotted path through a collection navigation.

var projected = dbContext.Products
    .Select(new List<string> { "Id", "Name", "Category.Vendors.Id" });

Category.Vendors is a collection — each Vendor element is projected with only its Id (Id auto-included).

Whole navigation object (non-dotted).

var projected = dbContext.Products
    .Select(new List<string> { "Id", "Name", "Category" });

The entire Category object is bound as-is.

Whole collection (non-dotted).

var projected = dbContext.Products
    .Select(new List<string> { "Id", "Name", "Brands" });

The entire Brands collection is bound as-is.

JSON-driven projection.

{ "fields": ["Id", "Name", "Category.Name"] }

See also