A C# source generator inspired by TypeScript's utility types (Pick, Omit) that simplifies creating DTOs and other derived types. Instead of hand-writing classes that are mostly copies of your domain models, you describe the shape you want and Shapely generates it.
When building APIs you often need types that are slight variations of your domain models — a response DTO that omits an internal field, a command object that picks only a few properties, or a composed type that pulls properties from two different services. In TypeScript you'd reach for Pick<T, K> or Omit<T, K>. Shapely brings that pattern to C#, with additional ideas suited to the language.
A common scenario: one service returns a type where a property contains just IDs. You need to enrich it with full objects from a second service. With Shapely you can model that as a shaped class that picks the bulk of its properties from the first type and replaces the ID property's type with the full type from the second service — all without manually writing out every property.
dotnet add package Shapely
Decorate a partial class with one or more shaping attributes:
using Shapely.Abstractions;
[All(typeof(Product))]
public partial class ProductDto { }Shapely generates all public properties from Product into ProductDto at compile time. No mapping code, no property duplication.
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public string InternalCode { get; set; }
}
[All(typeof(Product))]
public partial class ProductDto { }
// Generated:
// public partial class ProductDto
// {
// public int Id { get; set; }
// public string Name { get; set; }
// public decimal Price { get; set; }
// public string InternalCode { get; set; }
// }[Pick(typeof(Product), nameof(Product.Id), nameof(Product.Name))]
public partial class ProductSummaryDto { }
// Generated: Id and Name only[Omit(typeof(Product), nameof(Product.InternalCode))]
public partial class ProductPublicDto { }
// Generated: Id, Name, Price — InternalCode droppedStack multiple attributes to pull properties from more than one type. This is especially useful when you're composing a response from multiple services:
// Service A returns orders with a list of product IDs
public class Order
{
public int OrderId { get; set; }
public DateTime PlacedAt { get; set; }
public IEnumerable<int> ProductIds { get; set; }
}
// Service B returns full product data
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
}
// Composed response: everything from Order except the IDs,
// plus the resolved products from Service B
[Omit(typeof(Order), nameof(Order.ProductIds))]
[Pick(typeof(Product), nameof(Product.Name))]
public partial class OrderWithProductsDto { }Shapely raises a compile-time error (SHAPELY002) if two sources contribute a property with the same name.
After shaping, you can transform property types without changing the property selection.
[All(typeof(Product))]
[TransformAllTypes(typeof(decimal), typeof(double))]
public partial class ProductApiDto { }
// Price becomes double instead of decimal[All(typeof(Order))]
[TransformType(typeof(IEnumerable<int>), typeof(IEnumerable<Product>), nameof(Order.ProductIds))]
public partial class EnrichedOrderDto { }
// ProductIds becomes IEnumerable<Product>Control whether generated properties use set or init accessors.
[All(typeof(Product))]
[TransformAllSetAccessors(SetAccessor.Init)]
public partial class ImmutableProductDto { }
// All properties become init-only[All(typeof(Product))]
[TransformSetAccessor(SetAccessor.Init, nameof(Product.Id))]
public partial class ProductDto { }
// Only Id becomes init-only; others stay as setShapely ships Roslyn analyzers that give you compile-time feedback:
| ID | Description |
|---|---|
SHAPELY001 |
Class with Shapely attributes must be partial |
SHAPELY002 |
Two sources produce a property with the same name |
SHAPELY003 |
Property named in a type transformation does not exist on the class |
SHAPELY004 |
A property appears in duplicate type transformation rules |
SHAPELY005 |
Property named in Pick/Omit does not exist on the source type |
SHAPELY006 |
A property appears in duplicate accessor transformation rules |
SHAPELY007 |
Property named in an accessor transformation does not exist on the generated class |
Shapely works well alongside Mapperly for object mapping, but there is an ordering constraint: Roslyn source generators run in parallel, so Shapely-generated types are not visible to the Mapperly generator in the same project. The workaround is to keep your Shapely-shaped types in a separate project from your Mapperly mappers. That way the shaped types are compiled before Mapperly's generator runs, and Mapperly can see them normally.
MIT