Popcorn

Popcorn is a .Net Middleware for your RESTful API that allows your consumers to request exactly as much or as little as they need, with no effort from you.

This project is maintained by Skyward App Company

Contents
Latest Post

Popcorn > Documentation > DotNet > Tutorial: Default Includes

Table Of Contents

If you’re new to Popcorn, start with Getting Started — this tutorial assumes you already have a working app and understand the basics of ?include=.

Popcorn lets you declare which fields show up when the client makes a “bare” request — one with no ?include= parameter or with an empty ?include=[]. You do this with three attributes on the model, all in the Popcorn namespace.

Attribute Emitted when
[Always] Every response, regardless of ?include=. Cannot be negated.
[Default] ?include= is absent, empty, or !default. Can be negated via -FieldName.
[Never] Never. Not even when ?include=[FieldName] asks for it explicitly.

Example model

Suppose we extend the Employee model from Getting Started with a computed FullName property:

using Popcorn;

public class Employee
{
    public string FirstName { get; set; } = "";
    public string LastName  { get; set; } = "";
    public string FullName => $"{FirstName} {LastName}";

    public DateTimeOffset Birthday { get; set; }
    public int VacationDays { get; set; }

    public List<Car> Vehicles { get; set; } = new();
}

A bare GET /employees request would return every public property. For most APIs that’s too much — the client ends up paying for data it won’t render. Let’s declare a smaller default.

Controlling the default set with [Default]

public class Employee
{
    [Default] public string FirstName { get; set; } = "";
    [Default] public string LastName  { get; set; } = "";
    public string FullName => $"{FirstName} {LastName}";

    public DateTimeOffset Birthday { get; set; }
    public int VacationDays { get; set; }

    public List<Car> Vehicles { get; set; } = new();
}
GET /employees
{
  "Success": true,
  "Data": [
    { "FirstName": "Liz",  "LastName": "Lemon"   },
    { "FirstName": "Jack", "LastName": "Donaghy" }
  ]
}

The Birthday, VacationDays, FullName, and Vehicles fields are still available on the wire — clients just have to ask for them:

GET /employees?include=[FirstName,Birthday,FullName]
{
  "Success": true,
  "Data": [
    { "FirstName": "Liz",  "Birthday": "1981-05-01T00:00:00+00:00", "FullName": "Liz Lemon"   },
    { "FirstName": "Jack", "Birthday": "1957-07-12T00:00:00+00:00", "FullName": "Jack Donaghy" }
  ]
}

The “no attributes” rule

If a type has no [Default] or [Always] attributes anywhere, every property is treated as default-included. This keeps getting-started friction low — you can start emitting everything and only introduce attributes when you want to tighten the default set.

Attribute layout Default set
No [Default] / [Always] anywhere on the type All properties
At least one [Default] Only properties marked [Default] or [Always]
[Never] on a property That property is excluded, always, regardless of other rules

Inheritance: [Default] / [Always] on a base class flow through to derived classes. [Never] does the same. You don’t need to re-declare them on the subclass.

[Always] for fields that must be on every response

Some fields — primary keys, tenant IDs, version columns — should always be present even if the client forgot to ask. [Always] guarantees that:

public class Employee
{
    [Always]  public int Id { get; set; }
    [Default] public string FirstName { get; set; } = "";
    [Default] public string LastName  { get; set; } = "";
    ...
}
GET /employees?include=[FirstName]
{
  "Success": true,
  "Data": [
    { "Id": 1, "FirstName": "Liz"  },
    { "Id": 2, "FirstName": "Jack" }
  ]
}

Id shows up even though the client didn’t list it. Negation won’t remove it either — a request for ?include=[!all,-Id] still emits Id. If a field is genuinely sensitive, use [Never], not [Always] — see Internal-Only Fields.

Sub-property defaults via [SubPropertyDefault]

When a property is a complex type (like List<Car>), including the parent without specifying sub-children normally falls back to the child type’s own default set. [SubPropertyDefault] lets you override that decision for this property:

public class Employee
{
    [Default] public string FirstName { get; set; } = "";
    [Default] public string LastName  { get; set; } = "";

    [SubPropertyDefault("[Make,Model,Color]")]
    public List<Car> Vehicles { get; set; } = new();
}
GET /employees?include=[FirstName,Vehicles]
{
  "Success": true,
  "Data": [
    {
      "FirstName": "Liz",
      "Vehicles": [
        { "Make": "Pontiac", "Model": "Firebird", "Color": 2 }
      ]
    },
    ...
  ]
}

Without the attribute, Vehicles would have emitted the Car type’s own default set. The override applies only when the client doesn’t spell out sub-children: ?include=[Vehicles[Year]] still wins (explicit sub-children beat the attribute), and [Never] on a Car property still wins over the attribute (declared “never-emit” always beats “default-emit”).

The include string is parsed once per process into a static readonly field at generation time — no per-request parsing cost.

Negation and !default keyword

?include=[!default] is shorthand for “the default set” — useful when you want the default set plus a couple of extra fields:

GET /employees?include=[!default,Birthday]
→ default set (FirstName + LastName) plus Birthday

GET /employees?include=[!default,-LastName]
→ default set minus LastName (FirstName only)

[Always]-marked fields are still included regardless of negation — -Id is a silent no-op if Id is marked [Always].

See also