Kepler.Core is a flexible EF Core extension that enables safe, dynamic, and declarative selection of allowed fields in LINQ queries. Ideal for APIs with strict DTO projection and reduced over-fetching.
$ dotnet add package Kepler.Core
Typed policies for EF Core: projections, filters, nested navigation, and security — all defined in one fluent, compile-safe class.
📌 Status: BETA — Active development
Kepler.Core is stable for basic usage, but APIs may change as real-world feedback comes in.
Kepler.Core is a lightweight extension for Entity Framework Core that centralizes what data may be fetched, filtered, ordered, or traversed from an entity — using a single policy per entity.
It helps:
dotnet add package Kepler.Core
using Kepler.Core.Builder;
using Kepler.Core.Enums;
using YourDomain.Entities;
[KeplerPolicyName("Public")]
public class ProductPublicPolicy : IKeplerPolicy<Product>
{
public void Configure(IKeplerPolicyBuilder<Product> builder)
{
builder
.AllowFields(x => x.Name, x => x.Price, x => x.Id)
.AllowFilter(x => x.Name, FilterOperationEnum.StartsWith)
.AllowOrderBy(x => x.Price, x => x.SellStartDate);
}
}
[KeplerPolicyName("Nav")]
public class ProductNavigationPolicy : IKeplerPolicy<Product>
{
public void Configure(IKeplerPolicyBuilder<Product> builder)
{
builder
.AllowFields(x => x.Color!, x => x.Name!, x => x.MakeFlag, x => x.SellStartDate, x => x.ProductID)
// Nested collections with selective projection
.AllowNestedFields(x => x.ProductCostHistories,
x => x.SelectFields<ProductCostHistory>(x => x.ProductID, x => x.StartDate, x => x.StandardCost))
// Filtered nested collections
.AllowFilteredNestedFields(x => x.ProductListPriceHistories.Where(x => x.ListPrice < 74),
x => x.SelectFields<ProductListPriceHistory>(
plph => plph.ProductID,
plph => plph.StartDate,
plph => plph.EndDate!,
plph => plph.ListPrice,
plph => plph.ModifiedDate
))
.AllowFilteredNestedFields(x => x.ProductInventories.Where(x => x.Quantity <= 324),
x => x.SelectFields<ProductInventory>(
pi => pi.ProductID,
pi => pi.LocationID,
pi => pi.Shelf!,
pi => pi.Bin,
pi => pi.Quantity
))
.AllowFilteredNestedFields(x => x.ProductReviews.Where(x => x.Rating == 5),
x => x.SelectFields<ProductReview>(x => x.Rating, x => x.ProductReviewID))
.AllowNestedFields(x => x.ProductModel!,
x => x.SelectFields<ProductModel>(x => x.ProductModelID, x => x.ModifiedDate!))
.AllowOrderBy(x => x.Name!, x => x.SellStartDate)
.AllowFilter(x => x.MakeFlag, FilterOperationEnum.Equals)
.AllowFilter(x => x.ProductID, FilterOperationEnum.Equals)
.AllowFilter(x => x.Name, FilterOperationEnum.StartsWith);
}
}
builder.Services.AddKepler()
.AddKeplerPolicy<Product, ProductPublicPolicy>()
.ValidateKeplerPolicies();
Basic usage:
var products = await query
.ApplyKeplerPolicy(KeplerPolicyConfig.Create("Public", filters: dto))
.ToListAsync();
With SQL visibility:
var products = await query
.ApplyKeplerPolicy(
KeplerPolicyConfig.CreateWithSql("Public", filters: dto),
out string? generatedSql)
.ToListAsync();
Console.WriteLine(generatedSql);
// SELECT [Name], [Price], [Id] FROM [Products] WHERE [Name] LIKE @p0
With Lambda inspection:
var products = await query
.ApplyKeplerPolicy(
KeplerPolicyConfig.CreateWithLambda("Public", filters: dto),
out Expression? projectionLambda)
.ToListAsync();
Console.WriteLine(projectionLambda);
// {x => new Product() {Name = x.Name, Price = x.Price, Id = x.Id}}
With Full Debug Info (SQL + Lambda):
var products = await query
.ApplyKeplerPolicy(
KeplerPolicyConfig.CreateWithFullDebug("Public", filters: dto),
out KeplerDebugInfo? debug)
.ToListAsync();
Console.WriteLine($"SQL: {debug?.GeneratedSql}");
Console.WriteLine($"Lambda: {debug?.ProjectionLambda}");
// Order by SellStartDate descending
var products = await query
.ApplyKeplerPolicy(config)
.ApplyKeplerOrdering(KeplerOrderingConfig.CreateDescending("Public", "SellStartDate"), out string? sql)
.ApplyKeplerPagination(page: 1, pageSize: 10)
.ToListAsync();
// Chain multiple order by
var products = await query
.ApplyKeplerPolicy(config)
.ApplyKeplerOrdering(KeplerOrderingConfig.CreateDescending("Public", "CreatedAt"))
.ThenApplyKeplerOrdering(KeplerOrderingConfig.CreateAscending("Public", "Name"))
.ToListAsync();
var productsWithCount = query
.ApplyKeplerPolicy(config)
.ApplyKeplerOrdering(KeplerOrderingConfig.CreateDescending("Public", "SellStartDate"))
.ApplyKeplerPaginationWithCount(page: 1, pageSize: 10, out int totalCount)
.ToList();
builder
.AllowFields(x => x.Name, x => x.Email)
.AllowFilter(x => x.Status, FilterOperationEnum.Equals)
.AllowOrderBy(x => x.CreatedAt)
.MaxDepth(2); // Limit nested traversal depth
Exclude sensitive fields automatically (across all policies):
// Option 1: Via attribute
[KeplerGlobalExclude("Contains PII")]
public string Password { get; set; }
// Option 2: Via EF Core config
builder.Entity<User>()
.GloballyExclude(x => x.ApiKey, x => x.InternalNotes);
builder
.AllowNestedFields(x => x.Orders, nested =>
nested.SelectFields(x => x.Id, x => x.Total)
);
Clean, self-documenting configuration:
// Projection
KeplerPolicyConfig.Create("PolicyName")
KeplerPolicyConfig.CreateWithLambda("PolicyName")
KeplerPolicyConfig.CreateWithSql("PolicyName")
KeplerPolicyConfig.CreateWithFullDebug("PolicyName")
// Ordering
KeplerOrderingConfig.Create("PolicyName", "FieldName")
KeplerOrderingConfig.CreateAscending("PolicyName", "FieldName")
KeplerOrderingConfig.CreateDescending("PolicyName", "FieldName")
KeplerOrderingConfig.CreateWithSql("PolicyName", "FieldName")
// All support optional filters and ignoreGlobalExceptions
KeplerPolicyConfig.CreateWithSql("PolicyName", filters: dto, ignoreGlobalExceptions: false)
You can now inspect the full policy configuration, including allowed fields, nested projections, global exclusions, filters, order-by fields, and excluded fields.
var config = KeplerPolicyHelper.GetPolicyConfiguration(typeof(Product), "Nav", "Test1");
// ❌ Manual projections (repetitive)
var products = await query
.Select(x => new ProductDto
{
Id = x.Id,
Name = x.Name,
Price = x.Price
})
.ToListAsync();
// ❌ Over-fetching (loads everything)
var products = await query.ToListAsync();
// ❌ No visibility into what's happening
// ✅ One-liner, type-safe, controlled
var products = await query
.ApplyKeplerPolicy(KeplerPolicyConfig.Create("Public"))
.ToListAsync();
// ✅ See exactly what's being fetched
var products = await query
.ApplyKeplerPolicy(
KeplerPolicyConfig.CreateWithSql("Public"),
out string? sql)
.ToListAsync();
MIT — see LICENSE.
If Kepler solves your problem:
⭐ Star the repo
🐞 Report issues
💬 Suggest improvements
Built with ❤️ by Mohammad Ali Ebrahimzadeh