Pick and Omit in C# with Facet .NET: TypeScript Utility Types for .NET
If you’ve worked with TypeScript, you know how powerful utility types like Pick<T, K> and Omit<T, K> are. They let you create new types by selecting or excluding properties from existing types, all at compile-time with full type safety.
What if you could do the same in C#?
With Facet, you can! Let me show you how C# developers can enjoy the same convenience that TypeScript developers have been enjoying for years.
TypeScript’s Pick and Omit: A Quick Refresher
In TypeScript, utility types let you transform types declaratively:
// Original type
interface User {
id: number;
firstName: string;
lastName: string;
email: string;
passwordHash: string;
salary: number;
isActive: boolean;
createdAt: Date;
}
// Pick - Select only specific properties
type UserPublicProfile = Pick<User, 'id' | 'firstName' | 'lastName' | 'email'>;
// Result: { id, firstName, lastName, email }
// Omit - Exclude specific properties
type UserSafeDto = Omit<User, 'passwordHash' | 'salary'>;
// Result: All properties except passwordHash and salary
These are compile-time transformations: no runtime overhead, full IntelliSense support, and type safety guaranteed.
Why Pick and Omit Are So Useful
- API Responses - Hide sensitive fields
- Form Models - Only include editable fields
- Search Filters - Pick filterable properties
- Database Projections - Select only needed columns
- Type Narrowing - Create focused interfaces
The C# issue: manual DTOs
C# developers have been writing this pattern for years:
// Domain model
public class User
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public string PasswordHash { get; set; } // Sensitive!
public decimal Salary { get; set; } // Sensitive!
public bool IsActive { get; set; }
public DateTime CreatedAt { get; set; }
}
// Manual "Pick": only specific properties
public class UserPublicProfile
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
}
// Manual "Omit": everything except sensitive fields
public class UserSafeDto
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
// passwordHash and salary excluded
public bool IsActive { get; set; }
public DateTime CreatedAt { get; set; }
}
This is painful:
- Repetitive property declarations
- No compile-time link to source type
- Easy to get out of sync
- Tedious to maintain
Facet: Pick and Omit for C#
Facet brings TypeScript-style utility types to C# using source generators:
// Install Facet
// dotnet add package Facet
// Domain model (same as before)
public class User
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public string PasswordHash { get; set; }
public decimal Salary { get; set; }
public bool IsActive { get; set; }
public DateTime CreatedAt { get; set; }
}
// Pick - Include only specific properties
[Facet(typeof(User), Include = ["Id", "FirstName", "LastName", "Email"])]
public partial record UserPublicProfile;
// Omit - Exclude specific properties
[Facet(typeof(User), "PasswordHash", "Salary")]
public partial record UserSafeDto;
That’s it! Facet generates the complete DTOs at compile-time with:
- All specified properties
- Constructors for mapping
- LINQ projection expressions
- Full type safety
- IntelliSense support
Side-by-Side Comparison
Pick Example
TypeScript:
type UserContactInfo = Pick<User, 'firstName' | 'lastName' | 'email'>;
C# with Facet:
[Facet(typeof(User), Include = ["FirstName", "LastName", "Email"])]
public partial record UserContactInfo;
Omit Example
TypeScript:
type UserPublic = Omit<User, 'passwordHash' | 'salary'>;
C# with Facet:
[Facet(typeof(User), "PasswordHash", "Salary")]
public partial record UserPublic;
But Facet Goes Further Than TypeScript
While TypeScript’s Pick and Omit only create type definitions, Facet generates actual runtime code with additional features:
1. Automatic Mapping
[Facet(typeof(User), Include = ["FirstName", "LastName", "Email"])]
public partial record UserContactInfo;
// Use it immediately
User user = GetUser();
var contact = new UserContactInfo(user); // Constructor generated!
// Or use extension methods
var contact = user.ToFacet<User, UserContactInfo>();
TypeScript doesn’t generate constructors or mapping logic, you still write that manually.
2. LINQ Projections for EF Core
[Facet(typeof(User), Include = ["Id", "FirstName", "LastName"])]
public partial record UserListItem;
// Optimal SQL query - only selects needed columns!
var users = await dbContext.Users
.Where(u => u.IsActive)
.SelectFacet<UserListItem>()
.ToListAsync();
// Generated SQL:
// SELECT u.Id, u.FirstName, u.LastName
// FROM Users u
// WHERE u.IsActive = 1
TypeScript can’t optimize database queries, it’s a compile-time-only language.
3. Nested Object Support
public class Order
{
public int Id { get; set; }
public DateTime OrderDate { get; set; }
public User Customer { get; set; }
public Address ShippingAddress { get; set; }
}
[Facet(typeof(Address), Include = ["Street", "City", "ZipCode"])]
public partial record AddressDto;
[Facet(typeof(User), Include = ["FirstName", "LastName", "Email"])]
public partial record UserDto;
// Pick properties from Order AND nested types
[Facet(typeof(Order),
Include = ["Id", "OrderDate", "Customer", "ShippingAddress"],
NestedFacets = [typeof(UserDto), typeof(AddressDto)])]
public partial record OrderSummary;
var order = GetOrder();
var summary = new OrderSummary(order);
// summary.Customer is UserDto (picked properties)
// summary.ShippingAddress is AddressDto (picked properties)
4. Reverse Mapping (ToSource)
[Facet(typeof(User),
Include = ["FirstName", "LastName", "Email"],
GenerateToSource = true)]
public partial record UserUpdateRequest;
var request = new UserUpdateRequest
{
FirstName = "John",
LastName = "Doe",
Email = "john@example.com"
};
var user = request.ToSource(); // Maps back to User entity!
5. Property Renaming with MapFrom
[Facet(typeof(User), Include = ["FirstName", "Email"])]
public partial record UserDto
{
// Pick FirstName but rename it to Name
[MapFrom(nameof(User.FirstName))]
public string Name { get; set; } = string.Empty;
}
var dto = new UserDto(user);
// dto.Name = user.FirstName
6. Conditional Inclusion with MapWhen
public class Order
{
public int Id { get; set; }
public OrderStatus Status { get; set; }
public DateTime? CompletedAt { get; set; }
public string? TrackingNumber { get; set; }
}
[Facet(typeof(Order), Include = ["Id", "Status", "CompletedAt", "TrackingNumber"])]
public partial record OrderDto
{
// Only include CompletedAt if order is completed
[MapWhen("Status == OrderStatus.Completed")]
public DateTime? CompletedAt { get; set; }
// Only include tracking if not cancelled
[MapWhen("Status != OrderStatus.Cancelled")]
public string? TrackingNumber { get; set; }
}
Advanced: TypeScript’s Utility Types in C#
Facet enables even more TypeScript-style patterns:
Readonly (with Wrapper)
TypeScript:
type ReadonlyUser = Readonly<User>;
C# with Facet:
[Wrapper(typeof(User), ReadOnly = true)]
public partial class ReadonlyUser;
Partial (with NullableProperties)
TypeScript:
type PartialUser = Partial<User>;
C# with Facet:
[Facet(typeof(User), NullableProperties = true)]
public partial record PartialUser;
Record (with structs)
TypeScript:
type UserRecord = Readonly<User>;
C# with Facet:
[Facet(typeof(User))]
public partial record struct UserRecord;
Performance: Zero Runtime Overhead
Just like TypeScript’s utility types, Facet operates entirely at compile-time:
- ✅ Source code generated during compilation
- ✅ No reflection at runtime
- ✅ No performance penalty
- ✅ Same speed as hand-written code
- ✅ Optimal SQL for EF Core queries
Getting Started
# Install Facet
dotnet add package Facet
dotnet add package Facet.Extensions
dotnet add package Facet.Extensions.EFCore
# Start using Pick and Omit!
Basic usage:
using Facet;
// Your domain model
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public string InternalCode { get; set; }
}
// Pick - Include only
[Facet(typeof(Product), Include = ["Id", "Name", "Price"])]
public partial record ProductDto;
// Omit - Exclude
[Facet(typeof(Product), "InternalCode")]
public partial record ProductPublicDto;
// Use it
var product = new Product { /* ... */ };
var dto = product.ToFacet<Product, ProductDto>();
Conclusion
If you love TypeScript’s Pick and Omit utility types, you’ll love Facet. It brings the same declarative, type-safe approach to C#, but with additional runtime benefits:
✅ Compile-time generation (like TypeScript) ✅ Runtime mapping code (better than TypeScript) ✅ EF Core LINQ projections (impossible in TypeScript) ✅ Nested object handling (more powerful than TypeScript) ✅ Zero performance overhead (like TypeScript) ✅ Full IntelliSense support (like TypeScript)
Stop writing repetitive DTOs. Start using Facet.
Resources:
Previous posts: