Skip to content

Conversation

@magesoe
Copy link
Contributor

@magesoe magesoe commented Nov 6, 2025

Generated with claude, not checked code yet.

Goal is to provide a mapper along with the query to get the result in a strongly typed way

This commit adds functionality to map query results to dynamic objects with selected columns and linked entities:

- Add Alias property to ExpandBuilder for tracking LinkEntity aliases
- Generate unique aliases for all LinkEntity objects in QueryExpressionBuilder
- Create ResultMapper class that transforms Entity objects into dynamic objects with only selected columns
- Add EntityExtensions for extracting aliased attribute values from linked entities
- Add GetResultMapper() method to QueryExpressionBuilder for creating mapping functions
- Support nested linked entities with deep object mapping
- Add comprehensive unit tests for all result mapper functionality

The mapper handles:
- Simple column selections from main entity
- Linked entities with aliases
- Multi-level nested linked entities
- Missing values (returns null)
- Multiple linked entities at the same level
This commit enhances the result mapper to support strongly-typed projections instead of dynamic objects:

- Add QueryProjection<TEntity> class for type-safe value extraction using expressions
- Add LinkedEntityProjection class for projecting nested linked entities
- Add generic GetResultMapper<TResult> method that returns Func<Entity, TResult>
- Add GetAliasMap() method to retrieve relationship-to-alias mappings
- Add HasAliasedValues() extension method to check for linked entity presence
- Deprecate dynamic ResultMapper in favor of strongly-typed approach
- Add comprehensive unit tests with DTO classes

Benefits:
- Full IntelliSense support for result properties
- Compile-time type checking
- Clear contract for what data is available
- Easier to refactor and maintain
- Support for nested linked entities with type safety

Usage example:
var mapper = builder.GetResultMapper(proj => new AccountDto
{
    Name = proj.Get(a => a.Name),
    Contact = proj.GetLinked(a => a.account_primary_contact,
        contact => new ContactDto
        {
            FirstName = contact.Get<Contact, string>(c => c.FirstName)
        })
});
This commit adds a complete C# source generator that analyzes queries at compile-time and generates strongly-typed result classes based on selected columns:

**Source Generator Features:**
- Analyzes QueryExpressionBuilder chains at compile-time
- Detects .Project() calls to trigger generation
- Generates result classes with properties for each selected column
- Generates nested classes for linked entities (Expand calls)
- Supports multi-level nested relationships
- Provides full IntelliSense and compile-time type safety

**New Components:**
- src/QueryBuilder.SourceGenerator/ - Complete source generator implementation
  - QueryResultGenerator.cs - Main generator with syntax analysis
  - Uses Roslyn APIs to parse query builder syntax trees
  - Generates classes in DataverseQuery.Generated namespace

- src/QueryBuilder/ProjectionBuilder.cs - Builder for projections
  - Wraps QueryExpressionBuilder for projection scenarios
  - Provides .To<TResult>() method for type-safe mapping

- QueryExpressionBuilder.Project() - Marker method for generator
  - Returns ProjectionBuilder<TEntity>
  - Triggers source generator analysis

- test/QueryBuilder.Tests/SourceGeneratorProjectionTests.cs - Comprehensive tests
  - Tests simple column selection
  - Tests linked entity projections
  - Tests nested linked entities
  - Tests multiple linked entities

- PROJECTION_EXAMPLES.md - Complete usage documentation

**Usage Example:**
var projection = new QueryExpressionBuilder<Account>()
    .Select(e => e.Name, e => e.AccountNumber)
    .Expand(a => a.account_primary_contact,
        c => c.Select(x => x.FirstName, x => x.LastName))
    .Project();

var mapper = projection.To(proj => new
{
    Name = proj.Get(a => a.Name),
    AccountNumber = proj.Get(a => a.AccountNumber),
    PrimaryContact = proj.GetLinked(
        a => a.account_primary_contact,
        contact => new
        {
            FirstName = contact.Get<Contact, string>(c => c.FirstName),
            LastName = contact.Get<Contact, string>(c => c.LastName)
        })
});

// Result has full IntelliSense support!
var results = entities.Select(mapper).ToList();
foreach (var result in results)
{
    Console.WriteLine(result.Name); // Compile-time checked!
    Console.WriteLine(result.PrimaryContact.FirstName); // Type-safe!
}

**Benefits:**
- Zero runtime overhead - all generation at compile-time
- Exact types matching selected columns
- Impossible to access properties not selected
- IntelliSense everywhere
- Refactoring-safe
This enhances the source generator to automatically create mapper extension methods, eliminating ALL boilerplate code:

**What's Auto-Generated Now:**
- Result classes (Query{N}Result) with exact selected properties
- Nested classes for linked entities
- **NEW: Extension methods (ToQuery{N}Result()) with complete mapper logic**

**Before (manual mapper required):**
```csharp
var projection = builder.Select(e => e.Name).Project();
var mapper = projection.To(proj => new
{
    Name = proj.Get(a => a.Name)
});
```

**After (zero boilerplate):**
```csharp
var projection = builder.Select(e => e.Name).Project();
var mapper = projection.ToQuery0Result();  // Auto-generated!
```

**Generated Extension Method Example:**
```csharp
public static class Query0ResultExtensions
{
    public static Func<Entity, Query0Result> ToQuery0Result(
        this ProjectionBuilder<Account> projection)
    {
        return projection.To(proj => new Query0Result
        {
            Name = proj.Get(e => e.Name),
            account_primary_contact = proj.GetLinked(
                e => e.account_primary_contact,
                linked => new Query0Result_account_primary_contact
                {
                    FirstName = linked.Get<Contact, string>(x => x.FirstName),
                    LastName = linked.Get<Contact, string>(x => x.LastName)
                })
        });
    }
}
```

**Changes:**
- Enhanced GenerateCode() to call GenerateExtensionMethod()
- Added GenerateExtensionMethod() to create ToQuery{N}Result() methods
- Extension methods handle main entity properties and nested linked entities
- Updated tests with comments showing auto-generated usage
- Updated PROJECTION_EXAMPLES.md with "Quick Start" section highlighting auto-generated mappers

**Benefits:**
- Absolutely ZERO mapping boilerplate code
- Just call .ToQuery{N}Result() and you're done
- Complete type safety maintained
- Full IntelliSense for result properties
- Handles nested linked entities automatically
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants