AL

arika0093/linqraft

Developer tools
83 stars 품질 40 트렌드 40

*Write projections easily with generation*.

개요

*Write projections easily with generation*.

README

Features

Linqraft is a Roslyn Source Generator for easily writing IQueryable projections.

Query-based automatic DTO generation

You can write projections with UseLinqraft().Select(...) and get DTOs generated automatically based on the shape of your selector expression. This allows for “what you see is what you get” declarations, unifying DTO definition and query writing. Complex queries are fully supported, including nested DTOs, collections, and calculated fields.

For example, the following declaration will automatically generate OrderDto and OrderItemDto:

// orders: List
var orders = await dbContext.Orders
    .UseLinqraft()
    .Select(o => new {
        // CustomerName: string
        CustomerName = o.Customer.Name,
        // Items: IEnumerable
        Items = o.OrderItems.Select(oi => new {
            ProductName = oi.Product.Name,
            Quantity = oi.Quantity,
            // calculated field are supported !
            Price = oi.UnitPrice * oi.Quantity,
        }),
    })
    .ToListAsync();

Null-propagation operator support

C# Expression Trees do not support the null-propagation operator (?.). Linqraft generates queries through anonymous type generation, allowing you to write queries using the null-propagation operator.

var orders = await dbContext.Orders
    .UseLinqraft()
    .Select(o => new {
        // CustomerName: string?
        CustomerName = o.Customer?.Name,
        // Items: IEnumerable
        ProductNames = o.OrderItems.Select(oi => oi.Product?.Name),
        // > If all OrderItems.Products are null, the array will be empty.
    })
    .ToListAsync();

Drop-in Replacement Analyzers

Analyzers are provided to replace existing Select code with Linqraft. The replacement is completed in an instant.

// before
// order is (anonymous type)
var order = await dbContext.Orders
    .Select(o => new {
        CustomerName = o.Customer != null ? o.Customer.Name : null,
        Items = o.OrderItems.Select(oi => new {
            ProductName = oi.Product != null ? oi.Product.Name : null,
        }),
    })
    .FirstOrDefaultAsync();

// after (apply code fix)
// order is OrderDto✨️
var order = await dbContext.Orders
    .UseLinqraft().Select(o => new {
        // automatically replace null checks with null-propagation operator
        CustomerName = o.Customer?.Name,
        Items = o.OrderItems.Select(oi => new {
            ProductName = oi.Product?.Name,
        }),
    })
    .FirstOrDefaultAsync();

Projection Helpers for simplify

Linqraft also supports explicit projection helpers for cases where you want the generator to rewrite part of a selector body:

var rows = dbContext.Orders
    .UseLinqraft()
    // add 2nd argument to Select, which is a helper object
    // that provides methods for rewriting parts of the selector body.
    .Select((o, helper) => new
    {
        CustomerName = helper.AsLeftJoin(o.Customer).Name,
        RequiredCustomerName = helper.AsInnerJoin(o.Customer).Name,
        Customer = helper.AsProjection(o.Customer),
        CustomerSummary = helper.Project(o.Customer).Select(customer => new
        {
            customer.Id,
            customer.Name,
        }),
        FirstLargeItem = helper.AsInline(o.FirstLargeItemProductName),
    });

More details about projection helpers are available in the Projection Helpers section below.

No Dependencies

Thanks to the magic of Source Generators and Interceptors, there are no dependencies in production. The generated code consists of simple LINQ queries that work with any LINQ provider, such as EF Core or Dapper.

Installation

Linqraft is available on NuGet. Install it with the following command:

dotnet add package Linqraft

Linqraft requires C# 12.0 or later. On .NET 8 and later, it works out of the box.

AI Agent Integration (APM)

Linqraft ships an APM skill so AI coding agents can discover and use the library automatically. Install it with:

# use APM (installs only the skills/ package files via sparse checkout)
apm install arika0093/Linqraft/skills
# or install manually
curl https://raw.githubusercontent.com/arika0093/Linqraft/main/skills/.apm/skills/linqraft/SKILL.md -o your/skills/linqraft/SKILL.md

Since Linqraft’s DTO generation is an unfamiliar pattern for AI, installing the skill is recommended.

Basic Usage

Linqraft supports anonymous projections, auto-generated DTOs, pre-existing DTOs, grouped and flattened projections, and runtime DTO generation through LinqraftKit.Generate.

Explicit DTO Pattern

Use UseLinqraft().Select(...) when you want Linqraft to generate a named DTO from the selector body.

var orders = await dbContext.Orders
    .UseLinqraft().Select(o => new
    {
        o.Id,
        CustomerName = o.Customer?.Name,
        CustomerCountry = o.Customer?.Address?.Country?.Name,
        Items = o.OrderItems.Select(oi => new
        {
            ProductName = oi.Product?.Name,
            oi.Quantity,
        }),
    })
    .ToListAsync();

Linqraft generates a partial OrderDto and any required nested DTOs automatically.

public partial class OrderDto
{
    public required int Id { get; set; }
    public required string? CustomerName { get; set; }
    public required string? CustomerCountry { get; set; }
    public required IEnumerable Items { get; set; }
}

namespace LinqraftGenerated_HASH
{
    public partial class ItemsDto
    {
        public required string? ProductName { get; set; }
        public required int Quantity { get; set; }
    }
}

The same pattern also works for in-memory IEnumerable pipelines:

var orders = myList
    .UseLinqraft().Select(o => new
    {
        o.Id,
        CustomerName = o.Customer?.Name,
    })
    .ToList();

Anonymous Pattern

Use UseLinqraft().Select(...) without generic type parameters when you only need a one-off result shape.

var orders = await dbContext.Orders
    .UseLinqraft().Select(o => new
    {
        o.Id,
        CustomerName = o.Customer?.Name,
        TotalAmount = o.OrderItems.Sum(oi => oi.Quantity * oi.UnitPrice),
    })
    .ToListAsync();

This is ideal for quick exploration, ad-hoc queries, and prototypes.

[!NOTE] This is mostly the same as a normal Select(new => { ... }), but with support for features like the null-propagation operator.

Pre-existing DTO Pattern

Use UseLinqraft().Select(...) with your own DTO class when the type already exists or must carry custom attributes and interfaces.

public class OrderDto
{
    [JsonPropertyName("order_id")]
    public int Id { get; set; }

    [Required]
    public string CustomerName { get; set; } = "";

    public decimal TotalAmount { get; set; }
}

var orders = await dbContext.Orders
    .UseLinqraft()
    .Select(o => new OrderDto
    {
        Id = o.Id,
        CustomerName = o.Customer?.Name ?? string.Empty,
        TotalAmount = o.OrderItems.Sum(oi => oi.Quantity * oi.UnitPrice),
    })
    .ToListAsync();

This keeps Linqraft’s null-propagation support while leaving the DTO definition fully under your control.

Aggregation and Flattening Helpers

In addition to Select, the following two projection patterns are also available (same as normal LINQ):

  • SelectMany: flattening projections over nested collections
  • GroupBy: projections over groupings

Use GroupBy(...) when the final result is a projection over an IGrouping:

var result = dbContext.HealthChecks
    .UseLinqraft()
    .GroupBy(
        check => check.Region,
        group => new
        {
            Region = group.Key,
            AllHealthy = group.All(x => x.Status == "Healthy"),
            Checks = group.Select(x => new
            {
                x.Id,
                x.Status,
            }),
        })
    .ToListAsync();

Use SelectMany(...) when you want the final result to be flattened:

var rows = dbContext.Orders
    .UseLinqraft()
    .SelectMany(order =>
        order.Items.Select(item => new
        {
            OrderId = order.Id,
            item.ProductName,
            item.Quantity,
        }))
    .ToListAsync();

LinqraftKit.Generate

Use LinqraftKit.Generate(...) when you want Linqraft to generate a DTO from a runtime object instead of from an IEnumerable or IQueryable projection.

var id = 42;
var dto = LinqraftKit.Generate(
    new {
        Id = id,
        Customer = new { Name = "Ada" },
        ItemNames = new[] { "Keyboard", "Mouse" },
    },
    capture: () => id
);

This works well for combining runtime values, nested anonymous objects, arrays, lists, and values produced by other Linqraft projections.

Usage Documentation

Local Variable Capture

Pass the required local variables to capture: via a delegate (Func) that returns either a single value or an anonymous object/tuple for multiple values, like this:

var threshold = 100;
var multiplier = 2;
var suffix = " units";

var converted = dbContext.Entities
    .UseLinqraft()
    .Select(
        x => new
        {
            x.Id,
            IsExpensive = x.Price > threshold,
            DoubledValue = x.Value * multiplier,
            Description = x.Name + suffix,
        },
        capture: () => (threshold, multiplier, suffix)
    );

Linqraft analyzers can detect missing capture values and can rewrite legacy anonymous-object captures to the delegate form automatically.

Array Nullability Removal

Linqraft can automatically remove nullability from generated collection properties when an empty collection is a better semantic result than null. This avoids repetitive post-processing such as dto.Items ?? [].

var dto = query
    .UseLinqraft()
    .Select(e => new
    {
        // ChildNames: List
        ChildNames = e.Child?.Select(c => c.Name).ToList(),
        // ChildDtos: IEnumerable
        ChildDtos = e.Child?.Select(c => new { c.Name, c.Description }),
    });

Partial Classes

All generated DTOs are partial, so you can extend them with methods, interfaces, attributes, or predeclared properties whose accessibility you control yourself.

// can add methods, interfaces, attributes, etc.
[DebuggerDisplay("{GetDisplayName()}")]
public partial class OrderDto : ISampleInterface
{
    public string GetDisplayName() => $"Order #{Id} - {CustomerName}";
    public bool IsLargeOrder => TotalAmount > 1000;
}

var order = await dbContext.Orders
    .UseLinqraft()
    .Select(o => new
    {
        o.Id,
        CustomerName = o.Customer?.Name,
        TotalAmount = o.OrderItems.Sum(oi => oi.Quantity * oi.UnitPrice),
    })
    .FirstOrDefaultAsync();
// you can call the method you added to the partial class
if (order is not null)
{
    Console.WriteLine($"{order.GetDisplayName()} is {(order.IsLargeOrder ? "large" : "small")}.");
}

Mapping Methods

Linqraft normally generates projection code inline through interceptors, but sometimes you want a reusable extension method that can be called from multiple places or embedded inside EF Core compiled queries through EF.CompileAsyncQuery. In that case, you can write a template method and let Linqraft generate the full mapping method based on it.

For example, you can write a template like this:

// your declaration
public static partial class OrderQueries
{
    [LinqraftMapping]
    internal static IQueryable ProjectToOrderDto(this LinqraftMapper source) =>
        source.Select(o => new
        {
            o.Id,
            CustomerName = o.Customer?.Name,
            Items = o.Items.Select(i => new
            {
                i.ProductName,
                i.Quantity,
            }),
        });
}

// generated code such as:
public static partial class OrderQueries
{
    internal static IQueryable ProjectToOrderDto(this IQueryable source) =>
        source.Select(o => new OrderDto { /* ... */ });

    internal static IEnumerable ProjectToOrderDto(this IEnumerable source) =>
        source.Select(o => new OrderDto { /* ... */ });
}

// and you can call it like this:
var orders = await dbContext.Orders
    .ProjectToOrderDto()
    .ToListAsync();

This is useful in cases such as:

  • Reusable projections shared across multiple callers
  • EF Core compiled queries through EF.CompileAsyncQuery
  • EF Core precompiled queries on newer runtimes
  • Dedicated query classes for complex projections

Projection Helpers

Projection helpers are exposed through the selector’s IProjectionHelper parameter. They let you ask Linqraft to rewrite a specific fragment inside the generated projection instead of treating the selector body as a plain expression tree.

Analyzers

Linqraft ships analyzers and code fixes that can migrate ordinary Select calls to UseLinqraft().Select(...), add missing captures, simplify null checks, and highlight projection-specific issues.

See Analyzers for the full rule list and code-fix catalog.

Advanced Features

FAQ

How is the performance?

Linqraft’s performance is designed to be on par with hand-written DTO code because it generates the same code you would write manually.

Don’t believe it? Check out the Benchmark.

Does Linqraft only work with Entity Framework?

No. Linqraft works with any LINQ provider that supports IEnumerable and/or IQueryable.

// Entity Framework Core
var efOrders = await dbContext.Orders
    .UseLinqraft()
    .Select(o => new { o.Id, o.CustomerName })
    .ToListAsync();

// In-memory collection
var inMemoryOrders = myList
    .UseLinqraft()
    .Select(o => new { o.Id, o.CustomerName })
    .ToList();

// LINQ to Objects
var filtered = items
    .Where(x => x.IsActive)
    .UseLinqraft()
    .Select(x => new { x.Id, x.Name })
    .ToArray();

Can generated DTOs be used elsewhere?

Yes. Generated DTOs are regular C# classes, so they work as API response models, method return types, parameters, variables, serializer targets, and test fixtures.

[HttpGet]
public async Task>> GetOrders()
{
    var orders = await dbContext.Orders
        .UseLinqraft()
        .Select(o => new { o.Id, o.CustomerName })
        .ToListAsync();

    return Ok(orders);
}

Can I inspect the generated code?

Yes. Use F12 / Go to Definition on a generated DTO name, or emit the generated files to disk:


  
    true
    Generated
  

License

Copyright 2025-2026 arika0093

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0
View this README on GitHub

추천 도구

다른 키워드를 입력하거나 필터를 제거해 보세요.

설치

npx skillfish add arika0093/linqraft