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
⚠️ v7-only — dropped in v8. The
[ExpandFrom]attribute and the projection-class pattern it supports exist only in the v7 runtime-reflection line (Skyward.Api.Popcorn/.DotNetCore). v8 does not ship it. The v8 replacements, in rough order of preference:
- Decorate the source type directly with
[Never]on internal properties (one source of truth, no projection class).- Write a three-line hand factory (
public static CarDto From(CarEntity src) => new(...)) when the domain/API boundary is hard.- Use Mapster.SourceGenerator for complex flattening — it is AOT-compatible and composes with Popcorn.
Full rationale and code samples: MigrationV7toV8.md §7.
The tutorial below is preserved for v7 users still on that line.
The power of Popcorn comes in its ability to expand objects dynamically based on the specified object’s properties.
There are currently 3 ways that an object can be “Mapped” so it will be expanded by Popcorn. by Popcorn.
## Overview
The ExpandFrom attribute is assigned to an object’s projection class to tell Popcorn specifically where to look when attempting to expand the passed base object. The ExpandFrom object takes two properties:
This is where the limitations show a little. It is easy to configure the ExpandFrom property, but it doesn’t extend as many customizable options as mapping on the popcorn configuration does
Let’s say we have a Manager class that inherits from an Employee class:
public class Employee
{
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTimeOffset Birthday { get; set; }
public EmploymentType Employment { get; set; }
public int VacationDays { get; set; }
public List<Car> Vehicles { get; set; }
}
public class Manager : Employee
{
public List<Employee> Subordinates { get; set; }
}
We want to create a projection:
public class EmployeeProjection
{
[IncludeByDefault]
public string FirstName { get; set; }
[IncludeByDefault]
public string LastName { get; set; }
public string FullName { get; set; }
public string Birthday { get; set; }
public int? VacationDays { get; set; }
public EmploymentType? Employment { get; set; }
[SubPropertyIncludeByDefault("[Make,Model,Color]")]
public List<CarProjection> Vehicles { get; set; }
}
public class ManagerProjection : EmployeeProjection
{
[IncludeByDefault]
public List<EmployeeProjection> Subordinates { get; set; }
}
Now, it doesn’t really matter how we set the mapping of the base EmployeeProjection class, but let’s say we quickly want to set the mapping for the ManagerProjection. All we need to do is add the ExpandFrom attribute to our Manager projection and we are done!
[ExpandFrom(typeof(Manager))]
public class ManagerProjection : EmployeeProjection
{
[IncludeByDefault]
public List<EmployeeProjection> Subordinates { get; set; }
}
Now, an important thing to notice here is we elected to not declare default includes in our ExpandFrom attribute for two reasons:
That’s really it! If we make a call to our GET “Managers” endpoint we see the below as our base response and we can customize it with include statements to our heart’s content.
http://localhost:49699/api/example/managers
{
"Success": true,
"Data": [
{
"Subordinates": [
{
"FirstName": "Liz",
"LastName": "Lemon",
"Employment": "Employed"
}
],
"FirstName": "Stacy",
"LastName": "Hughes"
},
{
"Subordinates": [
{
"Subordinates": [
{
"FirstName": "Liz",
"LastName": "Lemon",
"Employment": "Employed"
}
],
"FirstName": "Stacy",
"LastName": "Hughes"
},
{
"FirstName": "Jack",
"LastName": "Donaghy",
"Employment": "Employed"
}
],
"FirstName": "Jamal",
"LastName": "Henry"
}
]
}