diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..16ef920 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,68 @@ +ο»Ώname: Deploy Docs to GitHub Pages + +on: + push: + branches: + - master + paths: + - 'docs/**' + - '.github/workflows/docs.yml' + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + name: Build VitePress site + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Needed for lastUpdated feature + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: docs/package-lock.json + + - name: Setup Pages + uses: actions/configure-pages@v5 + + - name: Install dependencies + working-directory: docs + run: npm ci + + - name: Build VitePress site + working-directory: docs + run: npm run build + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: docs/.vitepress/dist + + deploy: + name: Deploy to GitHub Pages + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + needs: build + runs-on: ubuntu-latest + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 + diff --git a/.gitignore b/.gitignore index 4e57e37..a5b1eb7 100644 --- a/.gitignore +++ b/.gitignore @@ -366,3 +366,7 @@ FodyWeavers.xsd *.received.* .idea + +# Docs +/docs/.vitepress/cache/ +/docs/.vitepress/dist/ diff --git a/README.md b/README.md index 471c6f5..53e693d 100644 --- a/README.md +++ b/README.md @@ -4,31 +4,38 @@ Flexible projection magic for EF Core [![NuGet version (EntityFrameworkCore.Projectables)](https://img.shields.io/nuget/v/EntityFrameworkCore.Projectables.Abstractions.svg?style=flat-square)](https://www.nuget.org/packages/EntityFrameworkCore.Projectables.Abstractions/) [![.NET](https://github.com/koenbeuk/EntityFrameworkCore.Projectables/actions/workflows/build.yml/badge.svg)](https://github.com/koenbeuk/EntityFrameworkCore.Projectables/actions/workflows/build.yml) +Write properties and methods once β€” use them anywhere in your LINQ queries, translated to efficient SQL automatically. + +πŸ“– **[Full documentation β†’ projectables.github.io](https://projectables.github.io)** + ## NuGet packages -- EntityFrameworkCore.Projectables.Abstractions [![NuGet version](https://img.shields.io/nuget/v/EntityFrameworkCore.Projectables.Abstractions.svg?style=flat-square)](https://www.nuget.org/packages/EntityFrameworkCore.Projectables.Abstractions/) [![NuGet](https://img.shields.io/nuget/dt/EntityFrameworkCore.Projectables.Abstractions.svg?style=flat-square)](https://www.nuget.org/packages/EntityFrameworkCore.Projectables.Abstractions/) -- EntityFrameworkCore.Projectables [![NuGet version](https://img.shields.io/nuget/v/EntityFrameworkCore.Projectables.svg?style=flat-square)](https://www.nuget.org/packages/EntityFrameworkCore.Projectables/) [![NuGet](https://img.shields.io/nuget/dt/EntityFrameworkCore.Projectables.svg?style=flat-square)](https://www.nuget.org/packages/EntityFrameworkCore.Projectables/) -> Starting with V2 of this project we're binding against **EF Core 6**. If you're targeting **EF Core 5** or **EF Core 3.1** then you can use the latest v1 release. These are functionally equivalent. +| Package | | +|---|---| +| `EntityFrameworkCore.Projectables.Abstractions` | [![NuGet version](https://img.shields.io/nuget/v/EntityFrameworkCore.Projectables.Abstractions.svg?style=flat-square)](https://www.nuget.org/packages/EntityFrameworkCore.Projectables.Abstractions/) [![Downloads](https://img.shields.io/nuget/dt/EntityFrameworkCore.Projectables.Abstractions.svg?style=flat-square)](https://www.nuget.org/packages/EntityFrameworkCore.Projectables.Abstractions/) | +| `EntityFrameworkCore.Projectables` | [![NuGet version](https://img.shields.io/nuget/v/EntityFrameworkCore.Projectables.svg?style=flat-square)](https://www.nuget.org/packages/EntityFrameworkCore.Projectables/) [![Downloads](https://img.shields.io/nuget/dt/EntityFrameworkCore.Projectables.svg?style=flat-square)](https://www.nuget.org/packages/EntityFrameworkCore.Projectables/) | +> **EF Core compatibility:** v1.x β†’ EF Core 3.1 / 5 Β· v2.x+ β†’ EF Core 6+ -## Getting started -1. Install the package from [NuGet](https://www.nuget.org/packages/EntityFrameworkCore.Projectables/) -2. Enable Projectables in your DbContext by adding: `dbContextOptions.UseProjectables()` -3. Implement projectable properties and methods, marking them with the `[Projectable]` attribute. -4. Explore our [samples](https://github.com/koenbeuk/EntityFrameworkCore.Projectables/tree/master/samples) and checkout our [Blog Post](https://onthedrift.com/posts/efcore-projectables/) for further guidance. +## Quick start -### Example -Assuming this sample: +```bash +dotnet add package EntityFrameworkCore.Projectables.Abstractions +dotnet add package EntityFrameworkCore.Projectables +``` + +Enable Projectables on your `DbContext`: ```csharp -class Order { - public int Id { get; set; } - public int UserId { get; set; } - public DateTime CreatedDate { get; set; } +options.UseSqlServer(connectionString) + .UseProjectables(); +``` + +Mark properties and methods with `[Projectable]`: +```csharp +class Order { public decimal TaxRate { get; set; } - - public User User { get; set; } public ICollection Items { get; set; } [Projectable] public decimal Subtotal => Items.Sum(item => item.Product.ListPrice * item.Quantity); @@ -38,261 +45,36 @@ class Order { public static class UserExtensions { [Projectable] - public static Order GetMostRecentOrderForUser(this User user, DateTime? cutoffDate) => + public static Order GetMostRecentOrder(this User user, DateTime? cutoffDate) => user.Orders .Where(x => cutoffDate == null || x.CreatedDate >= cutoffDate) .OrderByDescending(x => x.CreatedDate) .FirstOrDefault(); } - -var result = _dbContext.Users - .Where(x => x.UserName == "Jon") - .Select(x => new { - x.GetMostRecentOrderForUser(DateTime.UtcNow.AddDays(-30)).GrandTotal - }); - .FirstOrDefault(); -``` - -The following query gets generated (assuming SQL Server as a database provider) -```sql -DECLARE @__sampleUser_UserName_0 nvarchar(4000) = N'Jon'; - -SELECT ( - SELECT COALESCE(SUM([p].[ListPrice] * CAST([o].[Quantity] AS decimal(18,2))), 0.0) - FROM [OrderItem] AS [o] - INNER JOIN [Products] AS [p] ON [o].[ProductId] = [p].[Id] - WHERE ( - SELECT TOP(1) [o0].[Id] - FROM [Orders] AS [o0] - WHERE [u].[Id] = [o0].[UserId] AND [o0].[FulfilledDate] IS NOT NULL - ORDER BY [o0].[CreatedDate] DESC) IS NOT NULL AND ( - SELECT TOP(1) [o1].[Id] - FROM [Orders] AS [o1] - WHERE [u].[Id] = [o1].[UserId] AND [o1].[FulfilledDate] IS NOT NULL - ORDER BY [o1].[CreatedDate] DESC) = [o].[OrderId]) * ( - SELECT TOP(1) [o2].[TaxRate] - FROM [Orders] AS [o2] - WHERE [u].[Id] = [o2].[UserId] AND [o2].[FulfilledDate] IS NOT NULL - ORDER BY [o2].[CreatedDate] DESC) AS [GrandTotal] -FROM [Users] AS [u] -WHERE [u].[UserName] = @__sampleUser_UserName_0 -``` - -Projectable properties and methods have been inlined! the generated SQL could be improved but this is what EF Core (v8) gives us. - -### How it works -Essentially, there are two components: We have a source generator that can write companion expressions for properties and methods marked with the Projectable attribute. Then, we have a runtime component that intercepts any query and translates any call to a property or method marked with the Projectable attribute, translating the query to use the generated expression instead. - -### FAQ - -#### Are there currently any known limitations? -Currently, there is no support for overloaded methods. Each method name needs to be unique within a given type. - -#### Is this specific to a database provider? -No, the runtime component injects itself into the EFCore query compilation pipeline, thus having no impact on the database provider used. Of course, you're still limited to whatever your database provider can do. - -#### Are there performance implications that I should be aware of? -There are two compatibility modes: Limited and Full (Default). Most of the time, limited compatibility mode is sufficient. However, if you are running into issues with failed query compilation, then you may want to stick with Full compatibility mode. With Full compatibility mode, each query will first be expanded (any calls to Projectable properties and methods will be replaced by their respective expression) before being handed off to EFCore. (This is similar to how LinqKit/LinqExpander/Expressionify works.) Because of this additional step, there is a small performance impact. Limited compatibility mode is smart about things and only expands the query after it has been accepted by EF. The expanded query will then be stored in the Query Cache. With Limited compatibility, you will likely see increased performance over EFCore without projectables. - -#### Can I call additional properties and methods from my Projectable properties and methods? -Yes, you can! Any projectable property/method can call into other properties and methods as long as those properties/methods are native to EFCore or marked with a Projectable attribute. - -#### Can I use projectable extensions methods on non-entity types? -Yes you can. It's perfectly acceptable to have the following code: -```csharp -[Projectable] -public static int Squared(this int i) => i * i; -``` -Any call to squared given any int will perfectly translate to SQL. - -#### How do I deal with nullable properties -Expressions and Lamdas are different and not equal. Expressions can only express a subset of valid CSharp statements that are allowed in lambda's and arrow functions. One obvious limitation is the null-conditional operator. Consider the following example: -```csharp -[Projectable] -public static string? GetFullAddress(this User? user) => user?.Location?.AddressLine1 + " " + user?.Location.AddressLine2; -``` -This is a perfectly valid arrow function but it can't be translated directly to an expression tree. This Project will generate an error by default and suggest 2 solutions: Either you rewrite the function to explicitly check for nullables or you let the generator do that for you! - -Starting from the official release of V2, we can now hint the generator in how to translate this arrow function to an expression tree. We can say: -```csharp -[Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore)] -``` -which will simply generate an expression tree that ignores the null-conditional operator. This generates: -```csharp -user.Location.AddressLine1 + " " + user.Location.AddressLine2 -``` -This is perfect for a database like SQL Server where nullability is implicit and if any of the arguments were to be null, the resulting value will be null. If you are dealing with CosmosDB (which may result to client-side evaluation) or want to be explicit about things. You can configure your projectable as such: -```csharp -[Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Rewrite)] -``` -This will rewrite your expression to explicitly check for nullables. In the former example, this will be rewritten to: -```csharp -(user != null ? user.Location != null ? user.Location?.AddressLine1 + (user != null ? user.Location != null ? user.Location.AddressLine2 : null) : null) -``` -Note that using rewrite (not ignore) may increase the actual SQL query complexity being generated with some database providers such as SQL Server - -#### Can I use projectables in any part of my query? -Certainly, consider the following example: -```csharp -public class User -{ - public int Id { get; set; } - public string FirstName { get; set; } - public string LastName { get; set; } - - [Projectable] - public string FullName => FirstName + " " + LastName; -} - -var query = dbContext.Users - .Where(x => x.FullName.Contains("Jon")) - .GroupBy(x => x.FullName) - .OrderBy(x => x.Key) - .Select(x => x.Key); -``` -Which generates the following SQL (SQLite syntax) -```sql -SELECT (COALESCE("u"."FirstName", '') || ' ') || COALESCE("u"."LastName", '') -FROM "Users" AS "u" -WHERE ('Jon' = '') OR (instr((COALESCE("u"."FirstName", '') || ' ') || COALESCE("u"."LastName", ''), 'Jon') > 0) -GROUP BY (COALESCE("u"."FirstName", '') || ' ') || COALESCE("u"."LastName", '') -ORDER BY (COALESCE("u"."FirstName", '') || ' ') || COALESCE("u"."LastName", '') -``` - -#### Can I use block-bodied members instead of expression-bodied members? - -Yes! As of version 6.x, you can now use traditional block-bodied members with `[Projectable]`. This makes code more readable when dealing with complex conditional logic: - -```csharp -// Expression-bodied (still supported) -[Projectable] -public string Level() => Value > 100 ? "High" : Value > 50 ? "Medium" : "Low"; - -// Block-bodied (now also supported!) -[Projectable(AllowBlockBody = true)] // Note: AllowBlockBody is required to remove the warning for experimental feature usage -public string Level() -{ - if (Value > 100) - return "High"; - else if (Value > 50) - return "Medium"; - else - return "Low"; -} ``` -> This is an experimental feature and may have some limitations. Please refer to the documentation for details. - -Both generate identical SQL. Block-bodied members support: -- If-else statements (converted to ternary/CASE expressions) -- Switch statements -- Local variables (automatically inlined) -- Simple return statements - -The generator will also detect and report side effects (assignments, method calls to non-projectable members, etc.) with precise error messages. See [Block-Bodied Members Documentation](docs/BlockBodiedMembers.md) for complete details. - - -#### How do I expand enum extension methods? -When you have an enum property and want to call an extension method on it (like getting a display name from a `[Display]` attribute), you can use the `ExpandEnumMethods` property on the `[Projectable]` attribute. This will expand the enum method call into a chain of ternary expressions for each enum value, allowing EF Core to translate it to SQL CASE expressions. +Use them anywhere in your queries β€” they are **inlined into SQL automatically**: ```csharp -public enum OrderStatus -{ - [Display(Name = "Pending Review")] - Pending, - - [Display(Name = "Approved")] - Approved, - - [Display(Name = "Rejected")] - Rejected -} - -public static class EnumExtensions -{ - public static string GetDisplayName(this OrderStatus value) - { - // Your implementation here - return value.ToString(); - } - - public static bool IsApproved(this OrderStatus value) - { - return value == OrderStatus.Approved; - } - - public static int GetSortOrder(this OrderStatus value) - { - return (int)value; - } - - public static string Format(this OrderStatus value, string prefix) - { - return prefix + value.ToString(); - } -} - -public class Order -{ - public int Id { get; set; } - public OrderStatus Status { get; set; } - - [Projectable(ExpandEnumMethods = true)] - public string StatusName => Status.GetDisplayName(); - - [Projectable(ExpandEnumMethods = true)] - public bool IsStatusApproved => Status.IsApproved(); - - [Projectable(ExpandEnumMethods = true)] - public int StatusOrder => Status.GetSortOrder(); - - [Projectable(ExpandEnumMethods = true)] - public string FormattedStatus => Status.Format("Status: "); -} -``` - -This generates expression trees equivalent to: -```csharp -// For StatusName -@this.Status == OrderStatus.Pending ? GetDisplayName(OrderStatus.Pending) - : @this.Status == OrderStatus.Approved ? GetDisplayName(OrderStatus.Approved) - : @this.Status == OrderStatus.Rejected ? GetDisplayName(OrderStatus.Rejected) - : null - -// For IsStatusApproved (boolean) -@this.Status == OrderStatus.Pending ? false - : @this.Status == OrderStatus.Approved ? true - : @this.Status == OrderStatus.Rejected ? false - : default(bool) +var result = dbContext.Users + .Where(x => x.UserName == "Jon") + .Select(x => new { + x.GetMostRecentOrder(DateTime.UtcNow.AddDays(-30)).GrandTotal + }) + .FirstOrDefault(); ``` -Which EF Core translates to SQL CASE expressions: -```sql -SELECT CASE - WHEN [o].[Status] = 0 THEN N'Pending Review' - WHEN [o].[Status] = 1 THEN N'Approved' - WHEN [o].[Status] = 2 THEN N'Rejected' -END AS [StatusName] -FROM [Orders] AS [o] -``` +No client-side evaluation. No duplicated expressions. Just clean, efficient SQL. -The `ExpandEnumMethods` feature supports: -- **String return types** - returns `null` as the default fallback -- **Boolean return types** - returns `default(bool)` (false) as the default fallback -- **Integer return types** - returns `default(int)` (0) as the default fallback -- **Other value types** - returns `default(T)` as the default fallback -- **Nullable enum types** - wraps the expansion in a null check -- **Methods with parameters** - parameters are passed through to each enum value call -- **Enum properties on navigation properties** - works with nested navigation +## Documentation -#### How does this relate to [Expressionify](https://github.com/ClaveConsulting/Expressionify)? -Expressionify is a project that was launched before this project. It has some overlapping features and uses similar approaches. When I first published this project, I was not aware of its existence, so shame on me. Currently, Expressionify targets a more focused scope of what this project is doing, and thereby it seems to be more limiting in its capabilities. Check them out though! +The full documentation is hosted at **[projectables.github.io](https://projectables.github.io)** and covers: -#### How does this relate to LinqKit/LinqExpander/...? -There are a few projects like [LinqKit](https://github.com/scottksmith95/LINQKit) that were created before we had source generators in .NET. These are great options if you're stuck with classical EF or don't want to rely on code generation. Otherwise, I would suggest that EntityFrameworkCore.Projectables and Expressionify are superior approaches as they can rely on SourceGenerators to do most of the hard work. +- [Getting Started](https://projectables.github.io/guide/introduction) β€” installation, quick start, core concepts +- [Reference](https://projectables.github.io/reference/projectable-attribute) β€” `[Projectable]` attribute options, compatibility mode, diagnostics +- [Advanced](https://projectables.github.io/advanced/how-it-works) β€” internals, query compiler pipeline, block-bodied members +- [Recipes](https://projectables.github.io/recipes/computed-properties) β€” computed properties, enum display names, reusable query filters -#### Is the available for EFCore 3.1, 5 and 6? -V1 is targeting EF Core 5 and 3.1. V2 and V3 are targeting EF Core 6 and are compatible with EF Core 7. You can upgrade/downgrade between these versions based on your EF Core version requirements. +## License -#### What is next for this project? -TBD... However, one thing I'd like to improve is our expression generation logic as it's currently making a few assumptions (have yet to experience it breaking). Community contributions are very welcome! +MIT diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts new file mode 100644 index 0000000..572b6cf --- /dev/null +++ b/docs/.vitepress/config.mts @@ -0,0 +1,93 @@ +import { defineConfig } from 'vitepress' + +// https://vitepress.dev/reference/site-config +export default defineConfig({ + title: "EF Core Projectables", + description: "Flexible projection magic for EF Core β€” use properties and methods directly in your LINQ queries", + head: [ + ['link', { rel: 'icon', type: 'image/svg+xml', href: '/logo.svg' }], + ['meta', { property: 'og:image', content: 'https://projectables.github.io/social.svg' }], + ['meta', { property: 'og:type', content: 'website' }], + ['meta', { name: 'twitter:card', content: 'summary_large_image' }], + ['meta', { name: 'twitter:image', content: 'https://projectables.github.io/social.svg' }], + ], + themeConfig: { + logo: '/logo.svg', + nav: [ + { text: 'Home', link: '/' }, + { text: 'Guide', link: '/guide/introduction' }, + { text: 'Reference', link: '/reference/projectable-attribute' }, + { text: 'Advanced', link: '/advanced/how-it-works' }, + { text: 'Recipes', link: '/recipes/computed-properties' }, + ], + + sidebar: { + '/guide/': [ + { + text: 'Getting Started', + items: [ + { text: 'Introduction', link: '/guide/introduction' }, + { text: 'Installation', link: '/guide/installation' }, + { text: 'Quick Start', link: '/guide/quickstart' }, + ] + }, + { + text: 'Core Concepts', + items: [ + { text: 'Projectable Properties', link: '/guide/projectable-properties' }, + { text: 'Projectable Methods', link: '/guide/projectable-methods' }, + { text: 'Extension Methods', link: '/guide/extension-methods' }, + ] + } + ], + '/reference/': [ + { + text: 'Reference', + items: [ + { text: '[Projectable] Attribute', link: '/reference/projectable-attribute' }, + { text: 'Compatibility Mode', link: '/reference/compatibility-mode' }, + { text: 'Null-Conditional Rewrite', link: '/reference/null-conditional-rewrite' }, + { text: 'Expand Enum Methods', link: '/reference/expand-enum-methods' }, + { text: 'Use Member Body', link: '/reference/use-member-body' }, + { text: 'Diagnostics', link: '/reference/diagnostics' }, + ] + } + ], + '/advanced/': [ + { + text: 'Advanced', + items: [ + { text: 'How It Works', link: '/advanced/how-it-works' }, + { text: 'Query Compiler Pipeline', link: '/advanced/query-compiler-pipeline' }, + { text: 'Block-Bodied Members', link: '/advanced/block-bodied-members' }, + { text: 'Limitations', link: '/advanced/limitations' }, + ] + } + ], + '/recipes/': [ + { + text: 'Recipes', + items: [ + { text: 'Computed Entity Properties', link: '/recipes/computed-properties' }, + { text: 'Enum Display Names', link: '/recipes/enum-display-names' }, + { text: 'Nullable Navigation Properties', link: '/recipes/nullable-navigation' }, + { text: 'Reusable Query Filters', link: '/recipes/reusable-query-filters' }, + ] + } + ], + }, + + socialLinks: [ + { icon: 'github', link: 'https://github.com/koenbeuk/EntityFrameworkCore.Projectables' } + ], + + search: { + provider: 'local' + }, + + footer: { + message: 'Released under the MIT License.', + copyright: 'Copyright Β© EntityFrameworkCore.Projectables Contributors' + } + } +}) diff --git a/docs/advanced/block-bodied-members.md b/docs/advanced/block-bodied-members.md new file mode 100644 index 0000000..7ce378c --- /dev/null +++ b/docs/advanced/block-bodied-members.md @@ -0,0 +1,324 @@ +ο»Ώ# Block-Bodied Members + +As of v6.x, EF Core Projectables supports **block-bodied** properties and methods decorated with `[Projectable]`, in addition to expression-bodied members (`=>`). + +::: warning Experimental Feature +Block-bodied member support is currently **experimental**. Set `AllowBlockBody = true` on the attribute to acknowledge this and suppress warning EFP0001. +::: + +## Why Block Bodies? + +Expression-bodied members are concise but can become hard to read with complex conditional logic: + +```csharp +// Hard to read as a nested ternary +[Projectable] +public string Level() => Value > 100 ? "High" : Value > 50 ? "Medium" : "Low"; + +// Much easier to read as a block body +[Projectable(AllowBlockBody = true)] +public string Level() +{ + if (Value > 100) + return "High"; + else if (Value > 50) + return "Medium"; + else + return "Low"; +} +``` + +Both generate **identical SQL** β€” the block body is converted to a ternary expression internally. + +## Enabling Block Bodies + +Add `AllowBlockBody = true` to suppress the experimental warning: + +```csharp +[Projectable(AllowBlockBody = true)] +public string GetCategory() +{ + if (Value > 100) + return "High"; + else + return "Low"; +} +``` + +## Supported Constructs + +### Simple Return Statements + +```csharp +[Projectable(AllowBlockBody = true)] +public int GetConstant() +{ + return 42; +} +``` + +--- + +### If-Else Statements + +If-else chains are converted to ternary (`? :`) expressions: + +```csharp +[Projectable(AllowBlockBody = true)] +public string GetCategory() +{ + if (Value > 100) + return "High"; + else if (Value > 50) + return "Medium"; + else + return "Low"; +} +// Converted to: Value > 100 ? "High" : Value > 50 ? "Medium" : "Low" +``` + +--- + +### If Without Else (Fallback Return) + +An `if` statement without an `else` is supported when followed by a fallback `return`: + +```csharp +// Pattern 1: explicit fallback return +[Projectable(AllowBlockBody = true)] +public string GetStatus() +{ + if (IsActive) + return "Active"; + return "Inactive"; // Fallback +} + +// Pattern 2: explicit null return +[Projectable(AllowBlockBody = true)] +public int? GetPremium() +{ + if (IsActive) + return Value * 2; + return null; +} +``` + +--- + +### Multiple Early Returns + +Multiple independent early-return `if` statements are converted to a nested ternary chain: + +```csharp +[Projectable(AllowBlockBody = true)] +public string GetValueCategory() +{ + if (Value > 100) return "Very High"; + if (Value > 50) return "High"; + if (Value > 10) return "Medium"; + return "Low"; +} +// β†’ Value > 100 ? "Very High" : (Value > 50 ? "High" : (Value > 10 ? "Medium" : "Low")) +``` + +--- + +### Switch Statements + +Switch statements are converted to nested ternary expressions: + +```csharp +[Projectable(AllowBlockBody = true)] +public string GetValueLabel() +{ + switch (Value) + { + case 1: return "One"; + case 2: return "Two"; + case 3: return "Three"; + default: return "Many"; + } +} +``` + +Multiple cases mapping to the same result are collapsed: + +```csharp +switch (Value) +{ + case 1: + case 2: + return "Low"; + case 3: + case 4: + case 5: + return "Medium"; + default: + return "High"; +} +``` + +Generated SQL: +```sql +SELECT CASE + WHEN [e].[Value] IN (1, 2) THEN N'Low' + WHEN [e].[Value] IN (3, 4, 5) THEN N'Medium' + ELSE N'High' +END +FROM [Entity] AS [e] +``` + +--- + +### Local Variables + +Local variables declared at the method body level are **inlined** at each usage point: + +```csharp +[Projectable(AllowBlockBody = true)] +public int CalculateDouble() +{ + var doubled = Value * 2; + return doubled + 5; +} +// β†’ (Value * 2) + 5 +``` + +Transitive inlining is supported: + +```csharp +[Projectable(AllowBlockBody = true)] +public int CalculateComplex() +{ + var a = Value * 2; + var b = a + 5; + return b + 10; +} +// β†’ ((Value * 2) + 5) + 10 +``` + +::: warning Variable Duplication +If a local variable is referenced **multiple times**, its initializer is duplicated at each reference point. This can affect performance (and semantics if the initializer has side effects): + +```csharp +[Projectable(AllowBlockBody = true)] +public int Foo() +{ + var x = ExpensiveComputation(); // Inlined at each use + return x + x; // β†’ ExpensiveComputation() + ExpensiveComputation() +} +``` +::: + +**Local variables are only supported at the method body level** β€” not inside nested blocks (inside `if`, `switch`, etc.). + +## SQL Output Examples + +### If-Else β†’ CASE WHEN + +```csharp +public record Entity +{ + public int Value { get; set; } + public bool IsActive { get; set; } + + [Projectable(AllowBlockBody = true)] + public int GetAdjustedValue() + { + if (IsActive && Value > 0) + return Value * 2; + else + return 0; + } +} +``` + +Generated SQL: +```sql +SELECT CASE + WHEN [e].[IsActive] = CAST(1 AS bit) AND [e].[Value] > 0 + THEN [e].[Value] * 2 + ELSE 0 +END +FROM [Entity] AS [e] +``` + +### Switch β†’ CASE WHEN IN + +```csharp +[Projectable(AllowBlockBody = true)] +public string Category +{ + get + { + switch (Status) + { + case 1: case 2: return "Low"; + case 3: case 4: case 5: return "Medium"; + default: return "High"; + } + } +} +``` + +Generated SQL: +```sql +SELECT CASE + WHEN [e].[Status] IN (1, 2) THEN N'Low' + WHEN [e].[Status] IN (3, 4, 5) THEN N'Medium' + ELSE N'High' +END +FROM [Entity] AS [e] +``` + +## Limitations and Unsupported Constructs + +The following statement types produce **warning EFP0003** and are not supported: + +| Construct | Reason | +|---|---| +| `while` / `for` / `foreach` loops | Cannot be represented as expression trees | +| `try` / `catch` / `finally` | Cannot be represented as expression trees | +| `throw` statements | Cannot be represented as expression trees | +| `new MyClass()` in statement position | Object instantiation not supported in this context | + +```csharp +// ❌ Warning EFP0003 β€” loops are not supported +[Projectable(AllowBlockBody = true)] +public int SumItems() +{ + int total = 0; + foreach (var item in Items) // EFP0003 + total += item.Price; + return total; +} + +// βœ… Use LINQ instead +[Projectable] +public int SumItems() => Items.Sum(i => i.Price); +``` + +## Side Effect Detection + +The generator actively detects statements with side effects and reports them as errors (EFP0004) or warnings (EFP0005). See [Diagnostics](/reference/diagnostics) for the full list. + +| Code | Diagnostic | +|---------------------------|------------------------------------------| +| `Bar = 10;` | ❌ EFP0004 β€” property assignment | +| `Bar += 10;` | ❌ EFP0004 β€” compound assignment | +| `Bar++;` | ❌ EFP0004 β€” increment/decrement | +| `Console.WriteLine("x");` | ⚠️ EFP0005 β€” non-projectable method call | + +## How the Conversion Works + +The `BlockStatementConverter` class in the source generator: + +1. Collects all local variable declarations at the method body level. +2. Identifies the `return` statements and their conditions. +3. Converts `if`/`else` chains into ternary expression syntax nodes. +4. Converts `switch` statements into nested ternary expressions (or `case IN (...)` optimized forms). +5. Substitutes local variable references with their initializer expressions (via `VariableReplacementRewriter`). +6. Passes the resulting expression syntax to the standard expression rewriter pipeline. + +The output is equivalent to what would have been produced by an expression-bodied member with the same logic. + diff --git a/docs/advanced/how-it-works.md b/docs/advanced/how-it-works.md new file mode 100644 index 0000000..25fc4a0 --- /dev/null +++ b/docs/advanced/how-it-works.md @@ -0,0 +1,176 @@ +ο»Ώ# How It Works + +Understanding the internals of EF Core Projectables helps you use it effectively and debug issues when they arise. The library has two main components: a **build-time source generator** and a **runtime query interceptor**. + +## Architecture Overview + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ BUILD TIME β”‚ +β”‚ β”‚ +β”‚ Your C# code with [Projectable] members β”‚ +β”‚ β”‚ β”‚ +β”‚ β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Roslyn Source Generator β”‚ β”‚ +β”‚ β”‚ (ProjectionExpressionGenerator) β”‚ β”‚ +β”‚ β”‚ - Scans for [Projectable] β”‚ β”‚ +β”‚ β”‚ - Parses member bodies β”‚ β”‚ +β”‚ β”‚ - Generates Expression<> β”‚ β”‚ +β”‚ β”‚ companion classes β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ +β”‚ β–Ό β”‚ +β”‚ Auto-generated *.g.cs files with Expression<> trees β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ RUNTIME β”‚ +β”‚ β”‚ +β”‚ LINQ query using projectable member β”‚ +β”‚ β”‚ β”‚ +β”‚ β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ ProjectableExpressionReplacer (ExpressionVisitor) β”‚ β”‚ +β”‚ β”‚ - Walks the LINQ expression tree β”‚ β”‚ +β”‚ β”‚ - Detects calls to [Projectable] members β”‚ β”‚ +β”‚ β”‚ - Loads generated Expression<> via reflection β”‚ β”‚ +β”‚ β”‚ - Substitutes the call with the expression β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ +β”‚ β–Ό β”‚ +β”‚ Expanded expression tree (no [Projectable] calls) β”‚ +β”‚ β”‚ β”‚ +β”‚ β–Ό β”‚ +β”‚ Standard EF Core SQL translation β†’ SQL query β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## Build Time: The Source Generator + +### `ProjectionExpressionGenerator` + +This is the entry point for the Roslyn incremental source generator. It implements `IIncrementalGenerator` for high-performance, incremental code generation. + +**Pipeline:** +1. **Filter** β€” Uses `ForAttributeWithMetadataName` to efficiently find all `MemberDeclarationSyntax` nodes decorated with `[ProjectableAttribute]`. +2. **Interpret** β€” Calls `ProjectableInterpreter.GetDescriptor()` to extract all the information needed to generate code. +3. **Generate** β€” Produces a static class with an `Expression>` factory method. + +### `ProjectableInterpreter` + +Reads the attribute arguments, resolves the member's type information (namespace, generic parameters, containing classes), and extracts the expression body. + +**Key tasks:** +- Resolves `NullConditionalRewriteSupport`, `UseMemberBody`, `ExpandEnumMethods`, and `AllowBlockBody` from the attribute. +- Determines the correct parameter list for the generated lambda (including the implicit `@this` parameter for instance members and extension methods). +- Dispatches to `BlockStatementConverter` for block-bodied members. + +### `BlockStatementConverter` + +Converts block-bodied method statements into expression-tree-compatible forms: + +| Statement | Converted to | +|--------------------------------------|---------------------------------| +| `if (cond) return A; else return B;` | `cond ? A : B` | +| `switch (x) { case 1: return "a"; }` | `x == 1 ? "a" : ...` | +| `var v = expr; return v + 1;` | Inline substitution: `expr + 1` | +| Multiple early `return` | Nested ternary chain | + +### Expression Rewriters + +After the body is extracted, several rewriters transform the expression syntax: + +| Rewriter | Purpose | +|-------------------------------|------------------------------------------------------------------| +| `ExpressionSyntaxRewriter` | Rewrites `?.` operators based on `NullConditionalRewriteSupport` | +| `DeclarationSyntaxRewriter` | Adjusts member declarations for the generated class | +| `VariableReplacementRewriter` | Inlines local variables into the return expression | + +### Generated Code + +For a property like: + +```csharp +public class Order +{ + [Projectable] + public decimal GrandTotal => Subtotal + Tax; +} +``` + +The generator produces something like: + +```csharp +// Auto-generated β€” not visible in IntelliSense +[EditorBrowsable(EditorBrowsableState.Never)] +internal static class Order__GrandTotal +{ + public static Expression> Expression() + => @this => @this.Subtotal + @this.Tax; +} +``` + +The class name is deterministic, based on namespace + class name + member name. + +### `ProjectionExpressionClassNameGenerator` + +Generates a stable, unique class name for each projectable member. Handles generics, overloads (via parameter type names), and nested classes. + +## Runtime: The Query Interceptor + +### How Queries Are Intercepted + +When `UseProjectables()` is called, the library registers custom implementations of EF Core's internal query infrastructure. Depending on the [Compatibility Mode](/reference/compatibility-mode): + +**Full mode** β€” registers a `CustomQueryCompiler` that wraps EF Core's default compiler. Before compiling any query, it calls `ProjectableExpressionReplacer.Replace()` on the raw LINQ expression. + +**Limited mode** β€” registers a `CustomQueryTranslationPreprocessor` (via `CustomQueryTranslationPreprocessorFactory`). This runs inside EF Core's own query pipeline after the query is accepted, so the expanded query benefits from EF Core's query cache. + +### `ProjectableExpressionReplacer` + +Inherits from `ExpressionVisitor`. Its `Visit` method walks the LINQ expression tree and looks for: + +- **Property accesses** that correspond to `[Projectable]` properties. +- **Method calls** that correspond to `[Projectable]` methods. + +For each hit, it: +1. Calls `ProjectionExpressionResolver.FindGeneratedExpression()` to locate the auto-generated expression class via reflection. +2. Uses `ExpressionArgumentReplacer` to substitute the lambda parameters with the actual arguments from the call site. +3. Replaces the original call node with the inlined expression body. + +The replacement is done recursively β€” if the inlined expression itself contains projectable calls, they are also expanded. + +### `ProjectionExpressionResolver` + +Discovers the auto-generated companion class by constructing the expected class name (using the same naming logic as the generator) and reflecting into the assembly. + +```csharp +// Roughly equivalent to: +var type = assembly.GetType("Order__GrandTotal"); +var method = type.GetMethod("Expression"); +var expression = (LambdaExpression)method.Invoke(null, null); +``` + +### `ExpressionArgumentReplacer` + +Replaces the `@this` parameter (and any method arguments) in the retrieved lambda with the actual expressions from the call site. This is standard expression tree parameter substitution. + +## Tracking Behavior Handling + +The replacer also manages EF Core's tracking behavior. When a projectable member is used in a `Select` projection, the replacer wraps the expanded query in a `AsNoTracking()` call if necessary, ensuring consistent behavior with and without projectables. + +## Summary + +| Phase | Component | Responsibility | +|---------|--------------------------------------|----------------------------------------------| +| Build | `ProjectionExpressionGenerator` | Source gen entry point, orchestration | +| Build | `ProjectableInterpreter` | Extract descriptor from attribute + syntax | +| Build | `BlockStatementConverter` | Block body β†’ expression conversion | +| Build | `ExpressionSyntaxRewriter` | `?.` handling, null-conditional rewrite | +| Runtime | `CustomQueryCompiler` | Full mode: expand before EF Core | +| Runtime | `CustomQueryTranslationPreprocessor` | Limited mode: expand inside EF Core pipeline | +| Runtime | `ProjectableExpressionReplacer` | Walk and replace projectable calls | +| Runtime | `ProjectionExpressionResolver` | Locate generated expression via reflection | +| Runtime | `ExpressionArgumentReplacer` | Substitute parameters in lambda | + diff --git a/docs/advanced/limitations.md b/docs/advanced/limitations.md new file mode 100644 index 0000000..ff7bdee --- /dev/null +++ b/docs/advanced/limitations.md @@ -0,0 +1,156 @@ +ο»Ώ# Limitations & Known Issues + +This page documents the current limitations of EF Core Projectables and guidance on how to work around them. + +## Method Overloading Is Not Supported + +Each projectable method name must be **unique** within its declaring type. You cannot have two projectable methods with the same name but different parameter lists. + +```csharp +// ❌ Not supported β€” two methods named "GetTotal" +public class Order +{ + [Projectable] + public decimal GetTotal() => Subtotal; + + [Projectable] + public decimal GetTotal(decimal discountRate) => Subtotal * (1 - discountRate); // ❌ +} + +// βœ… Workaround β€” use distinct method names +public class Order +{ + [Projectable] + public decimal GetTotal() => Subtotal; + + [Projectable] + public decimal GetDiscountedTotal(decimal discountRate) => Subtotal * (1 - discountRate); +} +``` + +## Members Must Have a Body + +A `[Projectable]` member must have an **expression body** or a **block body** (with `AllowBlockBody = true`). Abstract members, interface declarations, and auto-properties without accessors are not supported and produce error EFP0006. + +```csharp +// ❌ Error EFP0006 β€” no body +[Projectable] +public string FullName { get; set; } + +// βœ… Expression-bodied property +[Projectable] +public string FullName => FirstName + " " + LastName; +``` + +Use [`UseMemberBody`](/reference/use-member-body) to delegate to another member if the projectable itself can't have a body. + +## Null-Conditional Operators Require Configuration + +The null-conditional operator (`?.`) cannot be used in projectable members unless `NullConditionalRewriteSupport` is set. The default (`None`) produces error EFP0002. + +```csharp +// ❌ Error EFP0002 +[Projectable] +public string? City => Address?.City; + +// βœ… Configured +[Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore)] +public string? City => Address?.City; +``` + +See [Null-Conditional Rewrite](/reference/null-conditional-rewrite). + +## Block Body Restrictions + +When using block-bodied members (experimental), the following constructs are **not supported**: + +- `while`, `for`, `foreach` loops (EFP0003) +- `try` / `catch` / `finally` blocks (EFP0003) +- `throw` statements (EFP0003) +- Local variables inside nested blocks (only top-level variable declarations are supported) + +```csharp +// ❌ Not supported +[Projectable(AllowBlockBody = true)] +public int Process() +{ + for (int i = 0; i < 10; i++) { ... } // EFP0003 + return result; +} + +// βœ… Use LINQ +[Projectable] +public int Process() => Items.Take(10).Sum(i => i.Value); +``` + +## Local Variables Are Inlined (No De-duplication) + +In block-bodied members, local variables are **inlined at every usage point**. If a variable is used multiple times, the initializer expression is duplicated. This can: + +- Increase SQL complexity. +- Change semantics if the initializer has observable side effects (though side effects are detected as EFP0004/EFP0005). + +```csharp +// The initializer "Value * 2" appears twice in the generated expression +[Projectable(AllowBlockBody = true)] +public int Foo() +{ + var doubled = Value * 2; + return doubled + doubled; // β†’ (Value * 2) + (Value * 2) +} +``` + +## Expression Tree Restrictions Apply + +Since projectable members are ultimately compiled to expression trees, all standard expression tree limitations apply: + +- **No `dynamic` typing** β€” expression trees must be statically typed. +- **No `ref` or `out` parameters**. +- **No named/optional parameters in LINQ** β€” parameters must be passed positionally in query expressions. +- **No multi-statement lambdas** β€” expression-bodied members must be single expressions (block bodies go through the converter, but with the limitations above). +- **Only EF Core-translatable operations** β€” the generated expression will ultimately be translated to SQL by EF Core. Any operation that EF Core cannot translate (e.g., calling a .NET method that has no SQL equivalent) will cause a runtime query translation error. + +## EF Core Translatable Operations Only + +The body of a projectable member can only use: + +- Mapped entity properties and navigation properties. +- Other `[Projectable]` members (transitively expanded). +- EF Core built-in functions (e.g., `EF.Functions.Like(...)`, `DateTime.Now`, string methods EF Core knows). +- LINQ methods EF Core supports (`Where`, `Sum`, `Any`, `Select`, etc.). + +```csharp +// ❌ Path.Combine has no SQL equivalent β€” runtime error +[Projectable] +public string FilePath => Path.Combine(Directory, FileName); + +// βœ… String concatenation β€” translated by EF Core +[Projectable] +public string FilePath => Directory + "/" + FileName; +``` + +## Limited Compatibility Mode and Dynamic State + +[Limited mode](/reference/compatibility-mode) caches the expanded query after the first execution. If a projectable member's expansion depends on external state that changes between calls (not through standard EF Core query parameters), the cached expansion may be stale. + +## No Support for Generic Type Parameters on Methods + +Generic method parameters are not supported on projectable methods: + +```csharp +// ❌ Not supported +[Projectable] +public T GetValue() => ...; +``` + +Generic **class** type parameters (on the containing entity) are supported. + +## Performance: First-Execution Overhead + +Both compatibility modes have a one-time cost on first execution: + +- **Full mode:** Expression walking + expansion on every execution. +- **Limited mode:** Expression walking + expansion on first execution; subsequent calls use EF Core's query cache. + +For performance-critical code paths, consider Limited mode to amortize this cost. + diff --git a/docs/advanced/query-compiler-pipeline.md b/docs/advanced/query-compiler-pipeline.md new file mode 100644 index 0000000..d7f294d --- /dev/null +++ b/docs/advanced/query-compiler-pipeline.md @@ -0,0 +1,158 @@ +ο»Ώ# Query Compiler Pipeline + +This page explains how EF Core Projectables integrates with EF Core's internal query compilation pipeline, and the differences between Full and Limited compatibility modes. + +## EF Core's Query Pipeline (Background) + +When you execute a LINQ query against a `DbContext`, EF Core runs it through a multi-stage pipeline: + +``` +LINQ Expression (IQueryable) + ↓ +QueryCompiler.Execute() + ↓ +Query Translation Preprocessor + ↓ +Query Translator (LINQ β†’ SQL model) + ↓ +SQL Generator + ↓ +SQL + Parameters β†’ Database +``` + +Projectables hooks into this pipeline at different points depending on the selected compatibility mode. + +## Full Compatibility Mode + +In Full mode, expansion happens **before** the query reaches EF Core's pipeline: + +``` +LINQ Expression + ↓ +CustomQueryCompiler.Execute() / CreateCompiledQuery() + ↓ ← [Projectables expansion happens HERE] +ProjectableExpressionReplacer.Replace() + ↓ +Expanded LINQ Expression + ↓ +(Delegated to the original EF Core QueryCompiler) + ↓ +Standard EF Core pipeline... + ↓ +SQL +``` + +### `CustomQueryCompiler` + +The `CustomQueryCompiler` class wraps EF Core's default `QueryCompiler`. It overrides all execution entry points: + +```csharp +public override TResult Execute(Expression query) + => _decoratedQueryCompiler.Execute(Expand(query)); + +public override TResult ExecuteAsync(Expression query, CancellationToken cancellationToken) + => _decoratedQueryCompiler.ExecuteAsync(Expand(query), cancellationToken); + +public override Func CreateCompiledQuery(Expression query) + => _decoratedQueryCompiler.CreateCompiledQuery(Expand(query)); +``` + +The `Expand()` method calls `ProjectableExpressionReplacer.Replace()` on the raw expression before passing it downstream. + +### Query Cache Implications + +Because expansion happens before EF Core sees the query, the expanded expression is what gets compiled and cached. This means: + +- EF Core's query cache works on the **expanded** expression. +- Two queries that differ only in which projectable member they call will produce **different cache keys**, even if the expanded SQL is the same. +- Each unique LINQ query shape goes through expansion on **every execution** β€” there is no caching of the expansion step itself. + +## Limited Compatibility Mode + +In Limited mode, expansion happens **inside** EF Core's query translation preprocessor: + +``` +LINQ Expression + ↓ +EF Core QueryCompiler (default) + ↓ +CustomQueryTranslationPreprocessor.Process() + ↓ ← [Projectables expansion happens HERE] +ProjectableExpressionReplacer (via ExpandProjectables() extension) + ↓ +Expanded expression (now stored in EF Core's query cache) + ↓ +Standard EF Core query translator... + ↓ +SQL +``` + +### `CustomQueryTranslationPreprocessor` + +This class wraps EF Core's default `QueryTranslationPreprocessor` and overrides the `Process()` method: + +```csharp +public override Expression Process(Expression query) + => _decoratedPreprocessor.Process(query.ExpandProjectables()); +``` + +`ExpandProjectables()` is an extension method on `Expression` that runs the `ProjectableExpressionReplacer` over the expression tree. + +### Query Cache Benefits + +Because the expansion happens **inside** EF Core's own preprocessing step, EF Core compiles the resulting expanded expression and stores it in its query cache. On subsequent executions with the same query shape: + +1. EF Core computes the cache key from the original (unexpanded) query. +2. It finds the cached compiled query. +3. It executes the cached query directly β€” **no expansion needed**. + +This is why Limited mode can outperform both Full mode and vanilla EF Core for repeated queries. + +### Dynamic Parameter Caveat + +The downside of Limited mode is that EF Core's query cache key is based on the **original** LINQ expression. If your projectable member captures external state (a closure variable that changes between calls), the cache may not distinguish between calls with different values. + +**Safe with Limited mode:** +```csharp +// The threshold is a query parameter β€” EF Core handles it correctly +dbContext.Orders.Where(o => o.ExceedsThreshold(threshold)) +``` + +**Potentially unsafe with Limited mode:** +```csharp +// If GetCurrentUserRegion() returns a different value per call +// and the result is baked into the expression tree at expansion time +// (not captured as a standard EF Core parameter), this may be stale. +dbContext.Orders.Where(o => o.Region == GetCurrentUserRegion()) +``` + +## How Expansion Works + +In both modes, the core expansion logic is in `ProjectableExpressionReplacer`: + +1. **Visit the expression tree** β€” The replacer inherits from `ExpressionVisitor` and recursively visits every node. +2. **Detect projectable calls** β€” For each `MemberExpression` (property access) or `MethodCallExpression`, it checks if the member has a `[ProjectableAttribute]`. +3. **Load the generated expression** β€” Uses `ProjectionExpressionResolver` to find the auto-generated companion class and invoke its `Expression()` factory method via reflection. +4. **Cache the resolved expression** β€” The resolved `LambdaExpression` is cached in a per-replacer dictionary to avoid redundant reflection calls within the same query expansion. +5. **Substitute arguments** β€” Uses `ExpressionArgumentReplacer` to replace the lambda's parameters with the actual arguments from the call site. +6. **Recurse** β€” The substituted expression body is itself visited, expanding any nested projectable calls. + +## Registering the Infrastructure + +Both modes use the same EF Core extension mechanism. `ProjectionOptionsExtension` implements `IDbContextOptionsExtension` and registers the appropriate services: + +```csharp +// Full mode β€” registers CustomQueryCompiler +services.AddScoped(); + +// Limited mode β€” registers CustomQueryTranslationPreprocessorFactory +services.AddScoped(); +``` + +The `CustomConventionSetPlugin` also registers the `ProjectablePropertiesNotMappedConvention`, which ensures EF Core's model builder ignores `[Projectable]` properties (they are computed β€” not mapped to database columns). + +## Query Filters + +The `ProjectablesExpandQueryFiltersConvention` handles the case where global query filters reference projectable members. It ensures that query filters are also expanded when Projectables is active. + diff --git a/docs/guide/extension-methods.md b/docs/guide/extension-methods.md new file mode 100644 index 0000000..d1bee9e --- /dev/null +++ b/docs/guide/extension-methods.md @@ -0,0 +1,124 @@ +ο»Ώ# Extension Methods + +Projectable extension methods let you define query logic outside of your entity classes β€” useful for keeping entities clean, applying logic to types you don't own, or grouping related query helpers. + +## Defining a Projectable Extension Method + +Add `[Projectable]` to any extension method in a **static class**: + +```csharp +using EntityFrameworkCore.Projectables; + +public static class UserExtensions +{ + [Projectable] + public static Order GetMostRecentOrder(this User user, DateTime? cutoffDate) => + user.Orders + .Where(x => cutoffDate == null || x.CreatedDate >= cutoffDate) + .OrderByDescending(x => x.CreatedDate) + .FirstOrDefault(); +} +``` + +## Using Extension Methods in Queries + +```csharp +var result = dbContext.Users + .Where(u => u.UserName == "Jon") + .Select(u => new { + GrandTotal = u.GetMostRecentOrder(DateTime.UtcNow.AddDays(-30)).GrandTotal + }) + .FirstOrDefault(); +``` + +The extension method is fully inlined β€” including any nested projectable members like `GrandTotal`. + +## Extension Methods on Non-Entity Types + +You don't need to restrict projectable extension methods to entity types. They work on **any type** that EF Core can work with in queries: + +```csharp +// On int +public static class IntExtensions +{ + [Projectable] + public static int Squared(this int i) => i * i; +} + +// On string +public static class StringExtensions +{ + [Projectable] + public static bool ContainsIgnoreCase(this string source, string value) => + source.ToLower().Contains(value.ToLower()); +} +``` + +Usage in queries: + +```csharp +var squaredScores = dbContext.Players + .Select(p => new { p.Name, SquaredScore = p.Score.Squared() }) + .ToList(); + +var results = dbContext.Products + .Where(p => p.Name.ContainsIgnoreCase("widget")) + .ToList(); +``` + +## Extension Methods with Multiple Parameters + +```csharp +public static class OrderExtensions +{ + [Projectable] + public static bool IsHighValueOrder(this Order order, decimal threshold, bool includeTax = false) => + (includeTax ? order.GrandTotal : order.Subtotal) > threshold; +} + +var highValue = dbContext.Orders + .Where(o => o.IsHighValueOrder(500, includeTax: true)) + .ToList(); +``` + +## Chaining Extension Methods + +Extension methods can call other projectable members (properties, methods, or other extension methods): + +```csharp +public static class UserExtensions +{ + [Projectable] + public static decimal TotalSpentThisMonth(this User user) => + user.Orders + .Where(o => o.CreatedDate.Month == DateTime.UtcNow.Month) + .Sum(o => o.GrandTotal); // GrandTotal is [Projectable] on Order + + [Projectable] + public static bool IsVipCustomer(this User user) => + user.TotalSpentThisMonth() > 1000; // Calls another [Projectable] extension +} +``` + +## Extension Methods on Nullable Types + +Extension methods on nullable entity types work naturally: + +```csharp +public static class UserExtensions +{ + [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore)] + public static string GetFullAddress(this User? user) => + user?.Location?.AddressLine1 + " " + user?.Location?.AddressLine2; +} +``` + +See [Null-Conditional Rewrite](/reference/null-conditional-rewrite) for details on handling nullable navigation. + +## Important Rules + +- Extension methods **must be in a static class**. +- The `this` parameter represents the entity instance in the generated expression. +- **Method overloading is not supported** β€” each method name must be unique within its declaring static class. +- Default parameter values are supported but the caller must explicitly provide all arguments in LINQ queries (EF Core does not support optional parameters in expression trees). + diff --git a/docs/guide/installation.md b/docs/guide/installation.md new file mode 100644 index 0000000..146438b --- /dev/null +++ b/docs/guide/installation.md @@ -0,0 +1,102 @@ +ο»Ώ# Installation + +Projectables is split into two NuGet packages. You will typically need both. + +## Packages + +### `EntityFrameworkCore.Projectables.Abstractions` + +Contains the `[Projectable]` attribute and the Roslyn **source generator**. This package must be referenced by the project that **defines** your entities and projectable members. + +### `EntityFrameworkCore.Projectables` + +Contains the EF Core **runtime extension** that intercepts queries and expands projectable members into SQL. This package must be referenced by the project that configures your `DbContext`. + +In most single-project setups, you reference both packages in the same project. + +## Install via .NET CLI + +```bash +dotnet add package EntityFrameworkCore.Projectables.Abstractions +dotnet add package EntityFrameworkCore.Projectables +``` + +## Install via Package Manager Console + +```powershell +Install-Package EntityFrameworkCore.Projectables.Abstractions +Install-Package EntityFrameworkCore.Projectables +``` + +## Install via PackageReference (csproj) + +```xml + + + + +``` + +> **Tip:** Replace `*` with the latest stable version from [NuGet](https://www.nuget.org/packages/EntityFrameworkCore.Projectables/). + +## Enable in Your DbContext + +After installing the packages, call `UseProjectables()` when configuring your `DbContextOptions`: + +```csharp +services.AddDbContext(options => + options.UseSqlServer(connectionString) + .UseProjectables()); // πŸ‘ˆ Add this +``` + +Or in `OnConfiguring`: + +```csharp +protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) +{ + optionsBuilder + .UseSqlServer(connectionString) + .UseProjectables(); +} +``` + +That's it β€” you're ready to start using `[Projectable]`! + +## Optional Configuration + +`UseProjectables()` accepts an optional callback to configure advanced options: + +```csharp +options.UseProjectables(projectables => + projectables.CompatibilityMode(CompatibilityMode.Limited)); +``` + +See [Compatibility Mode](/reference/compatibility-mode) for details. + +## Verifying the Installation + +The source generator runs at compile time. You can verify it is working by: + +1. Adding `[Projectable]` to a property in your entity class. +2. Building the project β€” no errors should appear. +3. Using the property in a LINQ query and checking that the generated SQL reflects the inlined logic (e.g., via `ToQueryString()` or EF Core logging). + +## Multi-Project Solutions + +In solutions where entities are in a separate class library: + +``` +MyApp.Domain β†’ references Abstractions (has [Projectable] attributes) +MyApp.Data β†’ references Projectables runtime + Domain +MyApp.Web β†’ references Data +``` + +```xml + + + + + + +``` + diff --git a/docs/guide/introduction.md b/docs/guide/introduction.md new file mode 100644 index 0000000..2df35da --- /dev/null +++ b/docs/guide/introduction.md @@ -0,0 +1,95 @@ +ο»Ώ# Introduction + +**EntityFrameworkCore.Projectables** is a library that lets you write C# properties and methods β€” decorated with a simple `[Projectable]` attribute β€” and use them directly inside any EF Core LINQ query. The library takes care of translating those members into the SQL query, keeping your codebase DRY and your queries efficient. + +## The Problem It Solves + +When using EF Core, you often need to express the same business logic in two places: + +1. **In-memory** β€” as a regular C# property or method on your entity. +2. **In queries** β€” duplicated inline as a LINQ expression so EF Core can translate it to SQL. + +```csharp +// ❌ Without Projectables β€” logic duplicated +class Order { + // C# property (in-memory use) + public decimal GrandTotal => Subtotal + Tax; + + // Must be duplicated inline in every LINQ query +} + +var totals = dbContext.Orders + .Select(o => new { + GrandTotal = o.Items.Sum(i => i.Price) + (o.Items.Sum(i => i.Price) * o.TaxRate) + }) + .ToList(); +``` + +With Projectables, you write the logic once: + +```csharp +// βœ… With Projectables β€” write once, use everywhere +class Order { + [Projectable] public decimal Subtotal => Items.Sum(i => i.Price); + [Projectable] public decimal Tax => Subtotal * TaxRate; + [Projectable] public decimal GrandTotal => Subtotal + Tax; +} + +var totals = dbContext.Orders + .Select(o => new { o.GrandTotal }) // Inlined into SQL automatically + .ToList(); +``` + +## How It Works + +Projectables has two components that work together: + +### 1. Source Generator (build time) + +When you compile your project, a Roslyn source generator scans for members decorated with `[Projectable]` and generates a **companion expression tree** for each one. For example, the `GrandTotal` property above generates something like: + +```csharp +// Auto-generated β€” hidden from IntelliSense +public static Expression> GrandTotal_Expression() + => @this => @this.Items.Sum(i => i.Price) + (@this.Items.Sum(i => i.Price) * @this.TaxRate); +``` + +### 2. Runtime Interceptor (query time) + +At query execution time, a custom EF Core query pre-processor walks your LINQ expression tree. Whenever it encounters a call to a `[Projectable]` member, it **replaces it with the generated expression tree**, substituting the actual parameters. The resulting expanded expression tree is then handed off to EF Core for normal SQL translation. + +``` +LINQ query + β†’ [Projectables interceptor replaces member calls with expressions] + β†’ Expanded expression tree + β†’ EF Core SQL translation + β†’ SQL query +``` + +## Comparison with Similar Libraries + +| Feature | Projectables | Expressionify | LinqKit | +|------------------------------|------------------|---------------|---------| +| Source generator based | βœ… | βœ… | ❌ | +| Works with entity methods | βœ… | βœ… | Partial | +| Works with extension methods | βœ… | βœ… | βœ… | +| Composable projectables | βœ… | ❌ | Partial | +| Block-bodied members | βœ… (experimental) | ❌ | ❌ | +| Enum method expansion | βœ… | ❌ | ❌ | +| Null-conditional rewriting | βœ… | ❌ | ❌ | +| Limited/cached mode | βœ… | ❌ | ❌ | + +## EF Core Version Compatibility + +| Library Version | EF Core Version | +|-----------------|-----------------------------------------| +| v1.x | EF Core 3.1, 5 | +| v2.x, v3.x | EF Core 6, 7 | +| v6.x+ | EF Core 6+ (block-bodied members added) | + +## Next Steps + +- [Install the packages β†’](/guide/installation) +- [Follow the Quick Start β†’](/guide/quickstart) +- [Learn how it works internally β†’](/advanced/how-it-works) + diff --git a/docs/guide/projectable-methods.md b/docs/guide/projectable-methods.md new file mode 100644 index 0000000..b818aaf --- /dev/null +++ b/docs/guide/projectable-methods.md @@ -0,0 +1,118 @@ +ο»Ώ# Projectable Methods + +Projectable methods work like projectable properties but accept parameters, making them ideal for reusable query fragments that vary based on runtime values. + +## Defining a Projectable Method + +Add `[Projectable]` to any **expression-bodied method** on an entity: + +```csharp +public class Order +{ + public int Id { get; set; } + public DateTime CreatedDate { get; set; } + public bool IsFulfilled { get; set; } + public decimal TaxRate { get; set; } + public ICollection Items { get; set; } + + [Projectable] + public decimal Subtotal => Items.Sum(item => item.Product.ListPrice * item.Quantity); + + [Projectable] + public bool IsRecentOrder(int days) => + CreatedDate >= DateTime.UtcNow.AddDays(-days) && IsFulfilled; +} +``` + +## Using Projectable Methods in Queries + +```csharp +// Pass runtime values as arguments +var recentOrders = dbContext.Orders + .Where(o => o.IsRecentOrder(30)) + .ToList(); + +// Use in Select +var summary = dbContext.Orders + .Select(o => new { + o.Id, + IsRecent = o.IsRecentOrder(7), + o.Subtotal + }) + .ToList(); +``` + +The method argument (`30` or `7`) is captured and translated into the generated SQL expression. + +## Methods with Multiple Parameters + +```csharp +public class Product +{ + public decimal ListPrice { get; set; } + public decimal DiscountRate { get; set; } + + [Projectable] + public decimal DiscountedPrice(decimal additionalDiscount, int quantity) => + ListPrice * (1 - DiscountRate - additionalDiscount) * quantity; +} + +// Usage +var prices = dbContext.Products + .Select(p => new { + p.Id, + FinalPrice = p.DiscountedPrice(0.05m, 10) + }) + .ToList(); +``` + +## Composing Methods and Properties + +Projectable methods can call projectable properties and vice versa: + +```csharp +public class Order +{ + [Projectable] public decimal Subtotal => Items.Sum(i => i.Price); + [Projectable] public decimal Tax => Subtotal * TaxRate; + + // Method calling projectable properties + [Projectable] + public bool ExceedsThreshold(decimal threshold) => (Subtotal + Tax) > threshold; +} + +var highValue = dbContext.Orders + .Where(o => o.ExceedsThreshold(500)) + .ToList(); +``` + +## Block-Bodied Methods (Experimental) + +Methods can also use traditional block bodies when `AllowBlockBody = true`: + +```csharp +[Projectable(AllowBlockBody = true)] +public string GetStatus(decimal threshold) +{ + if (GrandTotal > threshold) + return "High Value"; + else if (GrandTotal > threshold / 2) + return "Medium Value"; + else + return "Standard"; +} +``` + +See [Block-Bodied Members](/advanced/block-bodied-members) for full details. + +## Important Rules + +- Methods must be **expression-bodied** (`=>`) unless `AllowBlockBody = true`. +- **Method overloading is not supported** β€” each method name must be unique within its type. +- Parameters are passed through to the generated expression as closures and resolved at query time. +- Parameter types must be supported by EF Core (primitive types, enums, and other EF-translatable types). + +## Difference from Extension Methods + +Instance methods are defined directly on the entity. For query logic that doesn't belong on the entity, or that applies to types you don't own, use [Extension Methods](/guide/extension-methods) instead. + diff --git a/docs/guide/projectable-properties.md b/docs/guide/projectable-properties.md new file mode 100644 index 0000000..b4cd785 --- /dev/null +++ b/docs/guide/projectable-properties.md @@ -0,0 +1,136 @@ +ο»Ώ# Projectable Properties + +Projectable properties let you define computed values on your entities using standard C# expression-bodied properties, and have those computations automatically translated into SQL when used in LINQ queries. + +## Defining a Projectable Property + +Add `[Projectable]` to any **expression-bodied property**: + +```csharp +using EntityFrameworkCore.Projectables; + +public class User +{ + public string FirstName { get; set; } + public string LastName { get; set; } + + [Projectable] + public string FullName => FirstName + " " + LastName; +} +``` + +> **Note:** The `using EntityFrameworkCore.Projectables;` namespace is required for the `[Projectable]` attribute. + +## Using Projectable Properties in Queries + +Once defined, projectable properties can be used in **any part of a LINQ query**: + +### In `Select` + +```csharp +var names = dbContext.Users + .Select(u => u.FullName) + .ToList(); +``` + +Generated SQL (SQLite): +```sql +SELECT (COALESCE("u"."FirstName", '') || ' ') || COALESCE("u"."LastName", '') +FROM "Users" AS "u" +``` + +### In `Where` + +```csharp +var users = dbContext.Users + .Where(u => u.FullName.Contains("Jon")) + .ToList(); +``` + +### In `GroupBy` + +```csharp +var grouped = dbContext.Users + .GroupBy(u => u.FullName) + .Select(g => new { Name = g.Key, Count = g.Count() }) + .ToList(); +``` + +### In `OrderBy` + +```csharp +var sorted = dbContext.Users + .OrderBy(u => u.FullName) + .ToList(); +``` + +### In multiple clauses at once + +```csharp +var query = dbContext.Users + .Where(u => u.FullName.Contains("Jon")) + .GroupBy(u => u.FullName) + .OrderBy(u => u.Key) + .Select(u => u.Key); +``` + +Generated SQL (SQLite): +```sql +SELECT (COALESCE("u"."FirstName", '') || ' ') || COALESCE("u"."LastName", '') +FROM "Users" AS "u" +WHERE ('Jon' = '') OR (instr((COALESCE("u"."FirstName", '') || ' ') || COALESCE("u"."LastName", ''), 'Jon') > 0) +GROUP BY (COALESCE("u"."FirstName", '') || ' ') || COALESCE("u"."LastName", '') +ORDER BY (COALESCE("u"."FirstName", '') || ' ') || COALESCE("u"."LastName", '') +``` + +## Composing Projectable Properties + +Projectable properties can reference **other projectable properties**. The entire chain is expanded into the final SQL: + +```csharp +public class Order +{ + public decimal TaxRate { get; set; } + public ICollection Items { get; set; } + + [Projectable] public decimal Subtotal => Items.Sum(item => item.Product.ListPrice * item.Quantity); + [Projectable] public decimal Tax => Subtotal * TaxRate; // uses Subtotal + [Projectable] public decimal GrandTotal => Subtotal + Tax; // uses Subtotal + Tax +} +``` + +All three properties are inlined transitively in the generated SQL. + +## Block-Bodied Properties (Experimental) + +In addition to expression-bodied properties (`=>`), you can use **block-bodied properties** with `AllowBlockBody = true`: + +```csharp +[Projectable(AllowBlockBody = true)] +public string Category +{ + get + { + if (Score > 90) + return "Excellent"; + else if (Score > 70) + return "Good"; + else + return "Average"; + } +} +``` + +See [Block-Bodied Members](/advanced/block-bodied-members) for the full feature documentation. + +## Important Rules + +- The property **must be expression-bodied** (using `=>`) unless `AllowBlockBody = true` is set. +- The expression must be translatable by EF Core β€” it can only use members that EF Core understands (mapped columns, navigation properties, and other `[Projectable]` members). +- Properties **cannot be overloaded** β€” each property name must be unique within its type. +- The property body has access to `this` (the entity instance) and its navigation properties. + +## Nullable Properties + +If your expression uses the null-conditional operator (`?.`), you need to configure `NullConditionalRewriteSupport`. See [Null-Conditional Rewrite](/reference/null-conditional-rewrite) for details. + diff --git a/docs/guide/quickstart.md b/docs/guide/quickstart.md new file mode 100644 index 0000000..f94d4c0 --- /dev/null +++ b/docs/guide/quickstart.md @@ -0,0 +1,151 @@ +ο»Ώ# Quick Start + +This guide walks you through a complete end-to-end example β€” from defining entities with projectable members to seeing the generated SQL. + +## Step 1 β€” Define Your Entities + +```csharp +public class User +{ + public int Id { get; set; } + public string UserName { get; set; } + public ICollection Orders { get; set; } +} + +public class Order +{ + public int Id { get; set; } + public int UserId { get; set; } + public DateTime CreatedDate { get; set; } + public decimal TaxRate { get; set; } + + public User User { get; set; } + public ICollection Items { get; set; } + + // Mark computed properties with [Projectable] + [Projectable] public decimal Subtotal => Items.Sum(item => item.Product.ListPrice * item.Quantity); + [Projectable] public decimal Tax => Subtotal * TaxRate; + [Projectable] public decimal GrandTotal => Subtotal + Tax; +} + +public class OrderItem +{ + public int Id { get; set; } + public int OrderId { get; set; } + public int Quantity { get; set; } + public Product Product { get; set; } +} + +public class Product +{ + public int Id { get; set; } + public decimal ListPrice { get; set; } +} +``` + +## Step 2 β€” Enable Projectables on Your DbContext + +```csharp +public class AppDbContext : DbContext +{ + public DbSet Users { get; set; } + public DbSet Orders { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder + .UseSqlServer("your-connection-string") + .UseProjectables(); // Enable the runtime interceptor + } +} +``` + +## Step 3 β€” Use Projectable Members in Queries + +Now you can use `GrandTotal`, `Subtotal`, and `Tax` **directly in any LINQ query**: + +```csharp +// In a Select projection +var orderSummaries = dbContext.Orders + .Select(o => new { + o.Id, + o.Subtotal, + o.Tax, + o.GrandTotal + }) + .ToList(); + +// In a Where clause +var highValueOrders = dbContext.Orders + .Where(o => o.GrandTotal > 1000) + .ToList(); + +// In an OrderBy +var sortedOrders = dbContext.Orders + .OrderByDescending(o => o.GrandTotal) + .ToList(); +``` + +## Step 4 β€” Check the Generated SQL + +Use `ToQueryString()` to inspect the SQL EF Core generates: + +```csharp +var query = dbContext.Orders + .Where(o => o.GrandTotal > 1000) + .OrderByDescending(o => o.GrandTotal); + +Console.WriteLine(query.ToQueryString()); +``` + +The `GrandTotal` property β€” which itself uses `Subtotal` (which is also `[Projectable]`) β€” is fully inlined: + +```sql +SELECT [o].[Id], [o].[UserId], [o].[CreatedDate], [o].[TaxRate] +FROM [Orders] AS [o] +WHERE ( + COALESCE(SUM([p].[ListPrice] * CAST([oi].[Quantity] AS decimal(18,2))), 0.0) + + COALESCE(SUM([p].[ListPrice] * CAST([oi].[Quantity] AS decimal(18,2))), 0.0) * [o].[TaxRate] +) > 1000.0 +ORDER BY ( + COALESCE(SUM([p].[ListPrice] * CAST([oi].[Quantity] AS decimal(18,2))), 0.0) + + COALESCE(SUM([p].[ListPrice] * CAST([oi].[Quantity] AS decimal(18,2))), 0.0) * [o].[TaxRate] +) DESC +``` + +All computation happens in the database β€” no data is loaded into memory for filtering or sorting. + +## Adding Extension Methods + +You can also define projectable extension methods β€” useful for logic that doesn't belong on the entity itself: + +```csharp +public static class UserExtensions +{ + [Projectable] + public static Order GetMostRecentOrder(this User user, DateTime? cutoffDate = null) => + user.Orders + .Where(x => cutoffDate == null || x.CreatedDate >= cutoffDate) + .OrderByDescending(x => x.CreatedDate) + .FirstOrDefault(); +} +``` + +Use it in a query just like any regular method: + +```csharp +var result = dbContext.Users + .Where(u => u.UserName == "Jon") + .Select(u => new { + GrandTotal = u.GetMostRecentOrder(DateTime.UtcNow.AddDays(-30)).GrandTotal + }) + .FirstOrDefault(); +``` + +## Next Steps + +- [Projectable Properties in depth β†’](/guide/projectable-properties) +- [Projectable Methods β†’](/guide/projectable-methods) +- [Extension Methods β†’](/guide/extension-methods) +- [Full [Projectable] attribute reference β†’](/reference/projectable-attribute) + diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..502a6fa --- /dev/null +++ b/docs/index.md @@ -0,0 +1,78 @@ +--- +layout: home + +hero: + name: "EF Core Projectables" + text: "Flexible projection magic for EF Core" + tagline: Write properties and methods once β€” use them anywhere in your LINQ queries, translated to efficient SQL automatically. + actions: + - theme: brand + text: Get Started + link: /guide/introduction + - theme: alt + text: Quick Start + link: /guide/quickstart + - theme: alt + text: View on GitHub + link: https://github.com/koenbeuk/EntityFrameworkCore.Projectables + +features: + - icon: 🏷️ + title: Just Add [Projectable] + details: Decorate any property or method with [Projectable] and the source generator does the rest β€” no boilerplate, no manual expression trees. + + - icon: πŸ”Œ + title: Works with Any EF Core Provider + details: Provider-agnostic. SQL Server, PostgreSQL, SQLite, Cosmos DB β€” Projectables hooks into the EF Core query pipeline regardless of your database. + + - icon: ⚑ + title: Performance-First Design + details: Limited compatibility mode expands and caches queries after their first execution. Subsequent calls skip the expansion step entirely, often outperforming native EF Core. + + - icon: πŸ”— + title: Composable by Design + details: Projectable members can call other projectable members. Build a library of reusable query fragments and compose them freely in any query. + + - icon: πŸ›‘οΈ + title: Null-Conditional Rewriting + details: Working with nullable navigation properties? Configure NullConditionalRewriteSupport to automatically handle the ?. operator in generated expressions. + + - icon: πŸ”’ + title: Enum Method Expansion + details: Use ExpandEnumMethods to translate enum extension methods (like display names from [Display] attributes) into SQL CASE expressions automatically. +--- + +## At a Glance + +```csharp +class Order +{ + public decimal TaxRate { get; set; } + public ICollection Items { get; set; } + + [Projectable] + public decimal Subtotal => Items.Sum(item => item.Product.ListPrice * item.Quantity); + + [Projectable] + public decimal Tax => Subtotal * TaxRate; + + [Projectable] + public decimal GrandTotal => Subtotal + Tax; +} + +// Use it anywhere in your queries β€” translated to SQL automatically +var result = dbContext.Users + .Where(u => u.UserName == "Jon") + .Select(u => new { u.GetMostRecentOrder().GrandTotal }) + .FirstOrDefault(); +``` + +The properties are **inlined into the SQL** β€” no client-side evaluation, no N+1. + +## NuGet Packages + +| Package | Description | +|----------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------| +| [`EntityFrameworkCore.Projectables.Abstractions`](https://www.nuget.org/packages/EntityFrameworkCore.Projectables.Abstractions/) | The `[Projectable]` attribute and source generator | +| [`EntityFrameworkCore.Projectables`](https://www.nuget.org/packages/EntityFrameworkCore.Projectables/) | The EF Core runtime extension | + diff --git a/docs/package-lock.json b/docs/package-lock.json new file mode 100644 index 0000000..1765a25 --- /dev/null +++ b/docs/package-lock.json @@ -0,0 +1,2313 @@ +{ + "name": "docs", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "vitepress": "^2.0.0-alpha.16" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@docsearch/css": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-4.6.0.tgz", + "integrity": "sha512-YlcAimkXclvqta47g47efzCM5CFxDwv2ClkDfEs/fC/Ak0OxPH2b3czwa4o8O1TRBf+ujFF2RiUwszz2fPVNJQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@docsearch/js": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-4.6.0.tgz", + "integrity": "sha512-9/rbgkm/BgTq46cwxIohvSAz3koOFjnPpg0mwkJItAfzKbQIj+310PvwtgUY1YITDuGCag6yOL50GW2DBkaaBw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@docsearch/sidepanel-js": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@docsearch/sidepanel-js/-/sidepanel-js-4.6.0.tgz", + "integrity": "sha512-lFT5KLwlzUmpoGArCScNoK41l9a22JYsEPwBzMrz+/ILVR5Ax87UphCuiyDFQWEvEmbwzn/kJx5W/O5BUlN1Rw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@iconify-json/simple-icons": { + "version": "1.2.71", + "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.71.tgz", + "integrity": "sha512-rNoDFbq1fAYiEexBvrw613/xiUOPEu5MKVV/X8lI64AgdTzLQUUemr9f9fplxUMPoxCBP2rWzlhOEeTHk/Sf0Q==", + "dev": true, + "license": "CC0-1.0", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.2", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz", + "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.58.0.tgz", + "integrity": "sha512-mr0tmS/4FoVk1cnaeN244A/wjvGDNItZKR8hRhnmCzygyRXYtKF5jVDSIILR1U97CTzAYmbgIj/Dukg62ggG5w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.58.0.tgz", + "integrity": "sha512-+s++dbp+/RTte62mQD9wLSbiMTV+xr/PeRJEc/sFZFSBRlHPNPVaf5FXlzAL77Mr8FtSfQqCN+I598M8U41ccQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.58.0.tgz", + "integrity": "sha512-MFWBwTcYs0jZbINQBXHfSrpSQJq3IUOakcKPzfeSznONop14Pxuqa0Kg19GD0rNBMPQI2tFtu3UzapZpH0Uc1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.58.0.tgz", + "integrity": "sha512-yiKJY7pj9c9JwzuKYLFaDZw5gma3fI9bkPEIyofvVfsPqjCWPglSHdpdwXpKGvDeYDms3Qal8qGMEHZ1M/4Udg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.58.0.tgz", + "integrity": "sha512-x97kCoBh5MOevpn/CNK9W1x8BEzO238541BGWBc315uOlN0AD/ifZ1msg+ZQB05Ux+VF6EcYqpiagfLJ8U3LvQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.58.0.tgz", + "integrity": "sha512-Aa8jPoZ6IQAG2eIrcXPpjRcMjROMFxCt1UYPZZtCxRV68WkuSigYtQ/7Zwrcr2IvtNJo7T2JfDXyMLxq5L4Jlg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.58.0.tgz", + "integrity": "sha512-Ob8YgT5kD/lSIYW2Rcngs5kNB/44Q2RzBSPz9brf2WEtcGR7/f/E9HeHn1wYaAwKBni+bdXEwgHvUd0x12lQSA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.58.0.tgz", + "integrity": "sha512-K+RI5oP1ceqoadvNt1FecL17Qtw/n9BgRSzxif3rTL2QlIu88ccvY+Y9nnHe/cmT5zbH9+bpiJuG1mGHRVwF4Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.58.0.tgz", + "integrity": "sha512-T+17JAsCKUjmbopcKepJjHWHXSjeW7O5PL7lEFaeQmiVyw4kkc5/lyYKzrv6ElWRX/MrEWfPiJWqbTvfIvjM1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.58.0.tgz", + "integrity": "sha512-cCePktb9+6R9itIJdeCFF9txPU7pQeEHB5AbHu/MKsfH/k70ZtOeq1k4YAtBv9Z7mmKI5/wOLYjQ+B9QdxR6LA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.58.0.tgz", + "integrity": "sha512-iekUaLkfliAsDl4/xSdoCJ1gnnIXvoNz85C8U8+ZxknM5pBStfZjeXgB8lXobDQvvPRCN8FPmmuTtH+z95HTmg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.58.0.tgz", + "integrity": "sha512-68ofRgJNl/jYJbxFjCKE7IwhbfxOl1muPN4KbIqAIe32lm22KmU7E8OPvyy68HTNkI2iV/c8y2kSPSm2mW/Q9Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.58.0.tgz", + "integrity": "sha512-dpz8vT0i+JqUKuSNPCP5SYyIV2Lh0sNL1+FhM7eLC457d5B9/BC3kDPp5BBftMmTNsBarcPcoz5UGSsnCiw4XQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.58.0.tgz", + "integrity": "sha512-4gdkkf9UJ7tafnweBCR/mk4jf3Jfl0cKX9Np80t5i78kjIH0ZdezUv/JDI2VtruE5lunfACqftJ8dIMGN4oHew==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.58.0.tgz", + "integrity": "sha512-YFS4vPnOkDTD/JriUeeZurFYoJhPf9GQQEF/v4lltp3mVcBmnsAdjEWhr2cjUCZzZNzxCG0HZOvJU44UGHSdzw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.58.0.tgz", + "integrity": "sha512-x2xgZlFne+QVNKV8b4wwaCS8pwq3y14zedZ5DqLzjdRITvreBk//4Knbcvm7+lWmms9V9qFp60MtUd0/t/PXPw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.58.0.tgz", + "integrity": "sha512-jIhrujyn4UnWF8S+DHSkAkDEO3hLX0cjzxJZPLF80xFyzyUIYgSMRcYQ3+uqEoyDD2beGq7Dj7edi8OnJcS/hg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.58.0.tgz", + "integrity": "sha512-+410Srdoh78MKSJxTQ+hZ/Mx+ajd6RjjPwBPNd0R3J9FtL6ZA0GqiiyNjCO9In0IzZkCNrpGymSfn+kgyPQocg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.58.0.tgz", + "integrity": "sha512-ZjMyby5SICi227y1MTR3VYBpFTdZs823Rs/hpakufleBoufoOIB6jtm9FEoxn/cgO7l6PM2rCEl5Kre5vX0QrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.58.0.tgz", + "integrity": "sha512-ds4iwfYkSQ0k1nb8LTcyXw//ToHOnNTJtceySpL3fa7tc/AsE+UpUFphW126A6fKBGJD5dhRvg8zw1rvoGFxmw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.58.0.tgz", + "integrity": "sha512-fd/zpJniln4ICdPkjWFhZYeY/bpnaN9pGa6ko+5WD38I0tTqk9lXMgXZg09MNdhpARngmxiCg0B0XUamNw/5BQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.58.0.tgz", + "integrity": "sha512-YpG8dUOip7DCz3nr/JUfPbIUo+2d/dy++5bFzgi4ugOGBIox+qMbbqt/JoORwvI/C9Kn2tz6+Bieoqd5+B1CjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.58.0.tgz", + "integrity": "sha512-b9DI8jpFQVh4hIXFr0/+N/TzLdpBIoPzjt0Rt4xJbW3mzguV3mduR9cNgiuFcuL/TeORejJhCWiAXe3E/6PxWA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.58.0.tgz", + "integrity": "sha512-CSrVpmoRJFN06LL9xhkitkwUcTZtIotYAF5p6XOR2zW0Zz5mzb3IPpcoPhB02frzMHFNo1reQ9xSF5fFm3hUsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.58.0.tgz", + "integrity": "sha512-QFsBgQNTnh5K0t/sBsjJLq24YVqEIVkGpfN2VHsnN90soZyhaiA9UUHufcctVNL4ypJY0wrwad0wslx2KJQ1/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/core": { + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.22.0.tgz", + "integrity": "sha512-iAlTtSDDbJiRpvgL5ugKEATDtHdUVkqgHDm/gbD2ZS9c88mx7G1zSYjjOxp5Qa0eaW0MAQosFRmJSk354PRoQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.22.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.5" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.22.0.tgz", + "integrity": "sha512-jdKhfgW9CRtj3Tor0L7+yPwdG3CgP7W+ZEqSsojrMzCjD1e0IxIbwUMDDpYlVBlC08TACg4puwFGkZfLS+56Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.22.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.4" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.22.0.tgz", + "integrity": "sha512-DyXsOG0vGtNtl7ygvabHd7Mt5EY8gCNqR9Y7Lpbbd/PbJvgWrqaKzH1JW6H6qFkuUa8aCxoiYVv8/YfFljiQxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.22.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.22.0.tgz", + "integrity": "sha512-x/42TfhWmp6H00T6uwVrdTJGKgNdFbrEdhaDwSR5fd5zhQ1Q46bHq9EO61SCEWJR0HY7z2HNDMaBZp8JRmKiIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.22.0" + } + }, + "node_modules/@shikijs/themes": { + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.22.0.tgz", + "integrity": "sha512-o+tlOKqsr6FE4+mYJG08tfCFDS+3CG20HbldXeVoyP+cYSUxDhrFf3GPjE60U55iOkkjbpY2uC3It/eeja35/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.22.0" + } + }, + "node_modules/@shikijs/transformers": { + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-3.22.0.tgz", + "integrity": "sha512-E7eRV7mwDBjueLF6852n2oYeJYxBq3NSsDk+uyruYAXONv4U8holGmIrT+mPRJQ1J1SNOH6L8G19KRzmBawrFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/core": "3.22.0", + "@shikijs/types": "3.22.0" + } + }, + "node_modules/@shikijs/types": { + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.22.0.tgz", + "integrity": "sha512-491iAekgKDBFE67z70Ok5a8KBMsQ2IJwOWw3us/7ffQkIBCyOQfm/aNwVMBUriP02QshIfgHCBSIYAl3u2eWjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.4.tgz", + "integrity": "sha512-uM5iXipgYIn13UUQCZNdWkYk+sysBeA97d5mHsAoAt1u/wpN3+zxOmsVJWosuzX+IMGRzeYUNytztrYznboIkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.2" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.28.tgz", + "integrity": "sha512-kviccYxTgoE8n6OCw96BNdYlBg2GOWfBuOW4Vqwrt7mSKWKwFVvI8egdTltqRgITGPsTFYtKYfxIG8ptX2PJHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@vue/shared": "3.5.28", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.28.tgz", + "integrity": "sha512-/1ZepxAb159jKR1btkefDP+J2xuWL5V3WtleRmxaT+K2Aqiek/Ab/+Ebrw2pPj0sdHO8ViAyyJWfhXXOP/+LQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.28", + "@vue/shared": "3.5.28" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.28.tgz", + "integrity": "sha512-6TnKMiNkd6u6VeVDhZn/07KhEZuBSn43Wd2No5zaP5s3xm8IqFTHBj84HJah4UepSUJTro5SoqqlOY22FKY96g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@vue/compiler-core": "3.5.28", + "@vue/compiler-dom": "3.5.28", + "@vue/compiler-ssr": "3.5.28", + "@vue/shared": "3.5.28", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.28.tgz", + "integrity": "sha512-JCq//9w1qmC6UGLWJX7RXzrGpKkroubey/ZFqTpvEIDJEKGgntuDMqkuWiZvzTzTA5h2qZvFBFHY7fAAa9475g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.28", + "@vue/shared": "3.5.28" + } + }, + "node_modules/@vue/devtools-api": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.0.6.tgz", + "integrity": "sha512-+lGBI+WTvJmnU2FZqHhEB8J1DXcvNlDeEalz77iYgOdY1jTj1ipSBaKj3sRhYcy+kqA8v/BSuvOz1XJucfQmUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^8.0.6" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.0.6.tgz", + "integrity": "sha512-9zXZPTJW72OteDXeSa5RVML3zWDCRcO5t77aJqSs228mdopYj5AiTpihozbsfFJ0IodfNs7pSgOGO3qfCuxDtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^8.0.6", + "birpc": "^2.6.1", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^2.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.0.6.tgz", + "integrity": "sha512-Pp1JylTqlgMJvxW6MGyfTF8vGvlBSCAvMFaDCYa82Mgw7TT5eE5kkHgDvmOGHWeJE4zIDfCpCxHapsK2LtIAJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.28.tgz", + "integrity": "sha512-gr5hEsxvn+RNyu9/9o1WtdYdwDjg5FgjUSBEkZWqgTKlo/fvwZ2+8W6AfKsc9YN2k/+iHYdS9vZYAhpi10kNaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.28" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.28.tgz", + "integrity": "sha512-POVHTdbgnrBBIpnbYU4y7pOMNlPn2QVxVzkvEA2pEgvzbelQq4ZOUxbp2oiyo+BOtiYlm8Q44wShHJoBvDPAjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.28", + "@vue/shared": "3.5.28" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.28.tgz", + "integrity": "sha512-4SXxSF8SXYMuhAIkT+eBRqOkWEfPu6nhccrzrkioA6l0boiq7sp18HCOov9qWJA5HML61kW8p/cB4MmBiG9dSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.28", + "@vue/runtime-core": "3.5.28", + "@vue/shared": "3.5.28", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.28.tgz", + "integrity": "sha512-pf+5ECKGj8fX95bNincbzJ6yp6nyzuLDhYZCeFxUNp8EBrQpPpQaLX3nNCp49+UbgbPun3CeVE+5CXVV1Xydfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.28", + "@vue/shared": "3.5.28" + }, + "peerDependencies": { + "vue": "3.5.28" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.28.tgz", + "integrity": "sha512-cfWa1fCGBxrvaHRhvV3Is0MgmrbSCxYTXCSCau2I0a1Xw1N1pHAvkWCiXPRAqjvToILvguNyEwjevUqAuBQWvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.2.1.tgz", + "integrity": "sha512-3vwDzV+GDUNpdegRY6kzpLm4Igptq+GA0QkJ3W61Iv27YWwW/ufSlOfgQIpN6FZRMG0mkaz4gglJRtq5SeJyIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "14.2.1", + "@vueuse/shared": "14.2.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@vueuse/integrations": { + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-14.2.1.tgz", + "integrity": "sha512-2LIUpBi/67PoXJGqSDQUF0pgQWpNHh7beiA+KG2AbybcNm+pTGWT6oPGlBgUoDWmYwfeQqM/uzOHqcILpKL7nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vueuse/core": "14.2.1", + "@vueuse/shared": "14.2.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "async-validator": "^4", + "axios": "^1", + "change-case": "^5", + "drauu": "^0.4", + "focus-trap": "^7 || ^8", + "fuse.js": "^7", + "idb-keyval": "^6", + "jwt-decode": "^4", + "nprogress": "^0.2", + "qrcode": "^1.5", + "sortablejs": "^1", + "universal-cookie": "^7 || ^8", + "vue": "^3.5.0" + }, + "peerDependenciesMeta": { + "async-validator": { + "optional": true + }, + "axios": { + "optional": true + }, + "change-case": { + "optional": true + }, + "drauu": { + "optional": true + }, + "focus-trap": { + "optional": true + }, + "fuse.js": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "jwt-decode": { + "optional": true + }, + "nprogress": { + "optional": true + }, + "qrcode": { + "optional": true + }, + "sortablejs": { + "optional": true + }, + "universal-cookie": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.2.1.tgz", + "integrity": "sha512-1ButlVtj5Sb/HDtIy1HFr1VqCP4G6Ypqt5MAo0lCgjokrk2mvQKsK2uuy0vqu/Ks+sHfuHo0B9Y9jn9xKdjZsw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.2.1.tgz", + "integrity": "sha512-shTJncjV9JTI4oVNyF1FQonetYAiTBd+Qj7cY89SWbXSkx7gyhrgtEdF2ZAVWS1S3SHlaROO6F2IesJxQEkZBw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/copy-anything": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^5.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/focus-trap": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.8.0.tgz", + "integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "tabbable": "^6.4.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-what": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mark.js": { + "version": "8.11.1", + "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", + "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/minisearch": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.2.0.tgz", + "integrity": "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==", + "dev": true, + "license": "MIT" + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/oniguruma-parser": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz", + "integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/oniguruma-to-es": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.4.tgz", + "integrity": "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "oniguruma-parser": "^0.12.1", + "regex": "^6.0.1", + "regex-recursion": "^6.0.2" + } + }, + "node_modules/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "dev": true, + "license": "MIT" + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.58.0.tgz", + "integrity": "sha512-wbT0mBmWbIvvq8NeEYWWvevvxnOyhKChir47S66WCxw1SXqhw7ssIYejnQEVt7XYQpsj2y8F9PM+Cr3SNEa0gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.58.0", + "@rollup/rollup-android-arm64": "4.58.0", + "@rollup/rollup-darwin-arm64": "4.58.0", + "@rollup/rollup-darwin-x64": "4.58.0", + "@rollup/rollup-freebsd-arm64": "4.58.0", + "@rollup/rollup-freebsd-x64": "4.58.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.58.0", + "@rollup/rollup-linux-arm-musleabihf": "4.58.0", + "@rollup/rollup-linux-arm64-gnu": "4.58.0", + "@rollup/rollup-linux-arm64-musl": "4.58.0", + "@rollup/rollup-linux-loong64-gnu": "4.58.0", + "@rollup/rollup-linux-loong64-musl": "4.58.0", + "@rollup/rollup-linux-ppc64-gnu": "4.58.0", + "@rollup/rollup-linux-ppc64-musl": "4.58.0", + "@rollup/rollup-linux-riscv64-gnu": "4.58.0", + "@rollup/rollup-linux-riscv64-musl": "4.58.0", + "@rollup/rollup-linux-s390x-gnu": "4.58.0", + "@rollup/rollup-linux-x64-gnu": "4.58.0", + "@rollup/rollup-linux-x64-musl": "4.58.0", + "@rollup/rollup-openbsd-x64": "4.58.0", + "@rollup/rollup-openharmony-arm64": "4.58.0", + "@rollup/rollup-win32-arm64-msvc": "4.58.0", + "@rollup/rollup-win32-ia32-msvc": "4.58.0", + "@rollup/rollup-win32-x64-gnu": "4.58.0", + "@rollup/rollup-win32-x64-msvc": "4.58.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/shiki": { + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.22.0.tgz", + "integrity": "sha512-LBnhsoYEe0Eou4e1VgJACes+O6S6QC0w71fCSp5Oya79inkwkm15gQ1UF6VtQ8j/taMDh79hAB49WUk8ALQW3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/core": "3.22.0", + "@shikijs/engine-javascript": "3.22.0", + "@shikijs/engine-oniguruma": "3.22.0", + "@shikijs/langs": "3.22.0", + "@shikijs/themes": "3.22.0", + "@shikijs/types": "3.22.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/superjson": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", + "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "copy-anything": "^4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tabbable": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitepress": { + "version": "2.0.0-alpha.16", + "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-2.0.0-alpha.16.tgz", + "integrity": "sha512-w1nwsefDVIsje7BZr2tsKxkZutDGjG0YoQ2yxO7+a9tvYVqfljYbwj5LMYkPy8Tb7YbPwa22HtIhk62jbrvuEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docsearch/css": "^4.5.3", + "@docsearch/js": "^4.5.3", + "@docsearch/sidepanel-js": "^4.5.3", + "@iconify-json/simple-icons": "^1.2.68", + "@shikijs/core": "^3.21.0", + "@shikijs/transformers": "^3.21.0", + "@shikijs/types": "^3.21.0", + "@types/markdown-it": "^14.1.2", + "@vitejs/plugin-vue": "^6.0.3", + "@vue/devtools-api": "^8.0.5", + "@vue/shared": "^3.5.27", + "@vueuse/core": "^14.1.0", + "@vueuse/integrations": "^14.1.0", + "focus-trap": "^7.8.0", + "mark.js": "8.11.1", + "minisearch": "^7.2.0", + "shiki": "^3.21.0", + "vite": "^7.3.1", + "vue": "^3.5.27" + }, + "bin": { + "vitepress": "bin/vitepress.js" + }, + "peerDependencies": { + "markdown-it-mathjax3": "^4", + "oxc-minify": "*", + "postcss": "^8" + }, + "peerDependenciesMeta": { + "markdown-it-mathjax3": { + "optional": true + }, + "oxc-minify": { + "optional": true + }, + "postcss": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.28.tgz", + "integrity": "sha512-BRdrNfeoccSoIZeIhyPBfvWSLFP4q8J3u8Ju8Ug5vu3LdD+yTM13Sg4sKtljxozbnuMu1NB1X5HBHRYUzFocKg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.5.28", + "@vue/compiler-sfc": "3.5.28", + "@vue/runtime-dom": "3.5.28", + "@vue/server-renderer": "3.5.28", + "@vue/shared": "3.5.28" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 0000000..cb2ecb9 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,10 @@ +{ + "scripts": { + "dev": "vitepress dev", + "build": "vitepress build", + "preview": "vitepress preview" + }, + "devDependencies": { + "vitepress": "^2.0.0-alpha.16" + } +} diff --git a/docs/public/logo.svg b/docs/public/logo.svg new file mode 100644 index 0000000..ece1394 --- /dev/null +++ b/docs/public/logo.svg @@ -0,0 +1,59 @@ +ο»Ώ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/public/social.svg b/docs/public/social.svg new file mode 100644 index 0000000..a11bf63 --- /dev/null +++ b/docs/public/social.svg @@ -0,0 +1,98 @@ +ο»Ώ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + EF Core + + + Projectables + + + + + Flexible projection magic for EF Core + + + + + + [Projectable] + + + + + EF Core 6+ + + + diff --git a/docs/recipes/computed-properties.md b/docs/recipes/computed-properties.md new file mode 100644 index 0000000..695841e --- /dev/null +++ b/docs/recipes/computed-properties.md @@ -0,0 +1,157 @@ +ο»Ώ# Computed Entity Properties + +This recipe shows how to define reusable computed properties on your entities and use them across multiple query operations β€” all translated to SQL without any duplication. + +## The Pattern + +Define computed values as `[Projectable]` properties directly on your entity. These properties can then be used in `Select`, `Where`, `GroupBy`, `OrderBy`, and any combination thereof. + +## Example: Order Totals + +```csharp +public class Order +{ + public int Id { get; set; } + public decimal TaxRate { get; set; } + public DateTime CreatedDate { get; set; } + public ICollection Items { get; set; } + + // Building blocks + [Projectable] + public decimal Subtotal => Items.Sum(item => item.Product.ListPrice * item.Quantity); + + [Projectable] + public decimal Tax => Subtotal * TaxRate; + + // Composed from other projectables + [Projectable] + public decimal GrandTotal => Subtotal + Tax; +} +``` + +### Use in Select + +```csharp +var summaries = dbContext.Orders + .Select(o => new OrderSummaryDto + { + Id = o.Id, + Subtotal = o.Subtotal, // βœ… Inlined into SQL + Tax = o.Tax, // βœ… Inlined into SQL + GrandTotal = o.GrandTotal // βœ… Inlined into SQL + }) + .ToList(); +``` + +### Use in Where + +```csharp +// Only load high-value orders +var highValue = dbContext.Orders + .Where(o => o.GrandTotal > 1000) + .ToList(); +``` + +### Use in OrderBy + +```csharp +// Sort by computed value +var ranked = dbContext.Orders + .OrderByDescending(o => o.GrandTotal) + .Take(10) + .ToList(); +``` + +### All Together + +```csharp +var report = dbContext.Orders + .Where(o => o.GrandTotal > 500) + .OrderByDescending(o => o.GrandTotal) + .GroupBy(o => o.CreatedDate.Year) + .Select(g => new + { + Year = g.Key, + Count = g.Count(), + TotalRevenue = g.Sum(o => o.GrandTotal) + }) + .ToList(); +``` + +All computed values are evaluated **in the database** β€” no data is fetched to memory for filtering or aggregation. + +## Example: User Profile + +```csharp +public class User +{ + public string FirstName { get; set; } + public string LastName { get; set; } + public DateTime BirthDate { get; set; } + public DateTime? LastLoginDate { get; set; } + + [Projectable] + public string FullName => FirstName + " " + LastName; + + [Projectable] + public int Age => DateTime.Today.Year - BirthDate.Year + - (DateTime.Today.DayOfYear < BirthDate.DayOfYear ? 1 : 0); + + [Projectable] + public bool IsActive => LastLoginDate != null + && LastLoginDate >= DateTime.UtcNow.AddDays(-30); +} +``` + +```csharp +// Find active adult users, sorted by name +var results = dbContext.Users + .Where(u => u.IsActive && u.Age >= 18) + .OrderBy(u => u.FullName) + .Select(u => new { u.FullName, u.Age }) + .ToList(); +``` + +## Example: Product Catalog + +```csharp +public class Product +{ + public decimal ListPrice { get; set; } + public decimal DiscountRate { get; set; } + public int StockQuantity { get; set; } + public int ReorderPoint { get; set; } + + [Projectable] + public decimal SalePrice => ListPrice * (1 - DiscountRate); + + [Projectable] + public decimal SavingsAmount => ListPrice - SalePrice; + + [Projectable] + public bool NeedsReorder => StockQuantity <= ReorderPoint; +} +``` + +```csharp +// Products on sale that need restocking +var reorder = dbContext.Products + .Where(p => p.NeedsReorder && p.SalePrice < 50) + .OrderBy(p => p.StockQuantity) + .Select(p => new + { + p.Id, + p.SalePrice, + p.SavingsAmount, + p.StockQuantity + }) + .ToList(); +``` + +## Tips + +- **Compose freely** β€” projectables can call other projectables. Build from simple to complex. +- **Use Limited mode** in production for repeated queries β€” computed properties are cached after the first execution. +- **Keep it pure** β€” projectable properties should be pure computations (no side effects). Everything must be translatable to SQL. +- **Avoid N+1** β€” if a projectable property references navigation properties, make sure to structure your queries so EF Core can generate a single efficient query. + diff --git a/docs/recipes/enum-display-names.md b/docs/recipes/enum-display-names.md new file mode 100644 index 0000000..5cf2cf7 --- /dev/null +++ b/docs/recipes/enum-display-names.md @@ -0,0 +1,172 @@ +ο»Ώ# Enum Display Names in Queries + +This recipe shows how to project human-readable labels from enum values β€” such as names from `[Display]` attributes β€” directly into SQL queries using `ExpandEnumMethods`. + +## The Problem + +You have an enum with display-friendly labels: + +```csharp +public enum OrderStatus +{ + [Display(Name = "Pending Review")] + Pending = 0, + + [Display(Name = "Approved & Processing")] + Approved = 1, + + [Display(Name = "Rejected")] + Rejected = 2, + + [Display(Name = "Shipped")] + Shipped = 3 +} +``` + +And a helper extension method: + +```csharp +public static class OrderStatusExtensions +{ + public static string GetDisplayName(this OrderStatus status) + { + var field = typeof(OrderStatus).GetField(status.ToString()); + var attr = field?.GetCustomAttribute(); + return attr?.Name ?? status.ToString(); + } +} +``` + +The problem: `GetDisplayName` uses reflection β€” EF Core cannot translate this to SQL. + +## The Solution with `ExpandEnumMethods` + +Use `ExpandEnumMethods = true` on the projectable member that calls `GetDisplayName`: + +```csharp +public class Order +{ + public int Id { get; set; } + public OrderStatus Status { get; set; } + + [Projectable(ExpandEnumMethods = true)] + public string StatusLabel => Status.GetDisplayName(); +} +``` + +The source generator evaluates `GetDisplayName` for each enum value at **compile time** and bakes the results into the expression tree as string constants: + +```csharp +// Generated expression equivalent: +Status == OrderStatus.Pending ? "Pending Review" : +Status == OrderStatus.Approved ? "Approved & Processing" : +Status == OrderStatus.Rejected ? "Rejected" : +Status == OrderStatus.Shipped ? "Shipped" : +null +``` + +Which translates to: + +```sql +SELECT CASE + WHEN [o].[Status] = 0 THEN N'Pending Review' + WHEN [o].[Status] = 1 THEN N'Approved & Processing' + WHEN [o].[Status] = 2 THEN N'Rejected' + WHEN [o].[Status] = 3 THEN N'Shipped' +END AS [StatusLabel] +FROM [Orders] AS [o] +``` + +## Using StatusLabel in Queries + +```csharp +// Project enum labels into a DTO +var orders = dbContext.Orders + .Select(o => new OrderDto + { + Id = o.Id, + StatusLabel = o.StatusLabel // Translated to CASE in SQL + }) + .ToList(); + +// Group by display name +var statusCounts = dbContext.Orders + .GroupBy(o => o.StatusLabel) + .Select(g => new { Status = g.Key, Count = g.Count() }) + .ToList(); + +// Filter on the computed label (less efficient β€” prefer filtering on the enum value directly) +var pending = dbContext.Orders + .Where(o => o.StatusLabel == "Pending Review") + .ToList(); +``` + +## Adding More Computed Properties + +```csharp +public class Order +{ + public OrderStatus Status { get; set; } + + [Projectable(ExpandEnumMethods = true)] + public string StatusLabel => Status.GetDisplayName(); + + [Projectable(ExpandEnumMethods = true)] + public bool IsProcessing => Status.IsInProgress(); // Custom bool extension + + [Projectable(ExpandEnumMethods = true)] + public int StatusSortOrder => Status.GetSortOrder(); +} + +public static class OrderStatusExtensions +{ + public static string GetDisplayName(this OrderStatus status) { /* ... */ } + + public static bool IsInProgress(this OrderStatus status) => + status is OrderStatus.Approved or OrderStatus.Shipped; + + public static int GetSortOrder(this OrderStatus status) => + status switch { + OrderStatus.Pending => 1, + OrderStatus.Approved => 2, + OrderStatus.Shipped => 3, + OrderStatus.Rejected => 99, + _ => 0 + }; +} +``` + +## Nullable Enum Properties + +If the enum property is nullable, wrap the call in a null-conditional and configure the rewrite: + +```csharp +public class Order +{ + public OrderStatus? OptionalStatus { get; set; } + + [Projectable( + ExpandEnumMethods = true, + NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore)] + public string? OptionalStatusLabel => OptionalStatus?.GetDisplayName(); +} +``` + +## Enum on Navigation Property + +```csharp +public class Order +{ + public Customer Customer { get; set; } + + [Projectable(ExpandEnumMethods = true)] + public string CustomerTierLabel => Customer.Tier.GetDisplayName(); +} +``` + +## Best Practices + +- **Filter on the enum value** (not the label) for best SQL performance: `Where(o => o.Status == OrderStatus.Pending)`. +- **Use labels only for projection** (`Select`) β€” translating `WHERE StatusLabel = 'Pending Review'` is less efficient than `WHERE Status = 0`. +- If your enum changes frequently, regenerate β€” the display name values are baked in at compile time. + diff --git a/docs/recipes/nullable-navigation.md b/docs/recipes/nullable-navigation.md new file mode 100644 index 0000000..13a310a --- /dev/null +++ b/docs/recipes/nullable-navigation.md @@ -0,0 +1,159 @@ +ο»Ώ# Nullable Navigation Properties + +This recipe covers how to work with optional (nullable) navigation properties in projectable members, using `NullConditionalRewriteSupport` to safely handle `?.` operators. + +## The Challenge + +Navigation properties can be nullable β€” either because the relationship is optional, or because the related entity isn't loaded. Using `?.` in a projectable body without configuration produces **error EFP0002**, because expression trees cannot represent the null-conditional operator directly. + +## Choosing a Strategy + +| Strategy | Best For | +|---|---| +| `Ignore` | SQL Server / databases with implicit null propagation; navigation is usually present | +| `Rewrite` | Cosmos DB; client-side evaluation scenarios; maximum correctness | +| Manual null check | Complex multi-level nullable chains where you want full control | + +## Strategy 1: `Ignore` + +Strips the `?.` β€” `A?.B` becomes `A.B`. In SQL, NULL propagates implicitly in most expressions. + +```csharp +public class User +{ + public Address? Address { get; set; } + + [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore)] + public string? CityName => Address?.City; +} +``` + +Generated expression: `Address.City` + +Generated SQL (SQL Server): +```sql +SELECT [a].[City] +FROM [Users] AS [u] +LEFT JOIN [Addresses] AS [a] ON [u].[AddressId] = [a].[Id] +``` + +If `Address` is `NULL`, SQL returns `NULL` for `City` β€” which matches the expected C# behavior. + +**Use when:** You're on SQL Server (or a database with implicit null propagation), and you're confident that `NULL` will propagate correctly for your use case. + +## Strategy 2: `Rewrite` + +Rewrites `A?.B` as `A != null ? A.B : null` β€” generates explicit null checks in the expression. + +```csharp +public class User +{ + public Address? Address { get; set; } + + [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Rewrite)] + public string? CityName => Address?.City; +} +``` + +Generated expression: `Address != null ? Address.City : null` + +Generated SQL (SQL Server): +```sql +SELECT CASE WHEN [a].[Id] IS NOT NULL THEN [a].[City] END +FROM [Users] AS [u] +LEFT JOIN [Addresses] AS [a] ON [u].[AddressId] = [a].[Id] +``` + +**Use when:** You need explicit null handling, you're targeting Cosmos DB, or you want maximum semantic equivalence to C# code. + +## Multi-Level Nullable Chains + +For deeply nested nullable navigation: + +```csharp +public class User +{ + public Address? Address { get; set; } +} + +public class Address +{ + public City? City { get; set; } +} + +public class City +{ + public string? PostalCode { get; set; } +} + +// Ignore: strips all ?. +[Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore)] +public string? PostalCode => Address?.City?.PostalCode; +// β†’ Address.City.PostalCode + +// Rewrite: explicit null check at each level +[Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Rewrite)] +public string? PostalCode => Address?.City?.PostalCode; +// β†’ Address != null +// ? Address.City != null +// ? Address.City.PostalCode +// : null +// : null +``` + +## Strategy 3: Manual Null Checks + +For maximum control, write the null check explicitly β€” no `NullConditionalRewriteSupport` needed: + +```csharp +[Projectable] +public string? CityName => + Address != null ? Address.City : null; + +[Projectable] +public string? PostalCode => + Address != null && Address.City != null + ? Address.City.PostalCode + : null; +``` + +This approach is verbose but gives you precise control over the generated expression. + +## Extension Methods on Nullable Entity Parameters + +When an extension method's `this` parameter is nullable: + +```csharp +public static class UserExtensions +{ + [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Rewrite)] + public static string? GetFullAddress(this User? user) => + user?.Address?.AddressLine1 + ", " + user?.Address?.City; +} +``` + +## Combining with Other Options + +Null-conditional rewrite is compatible with other `[Projectable]` options: + +```csharp +[Projectable( + NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore, + ExpandEnumMethods = true)] +public string? ShippingStatusLabel => + ShippingInfo?.Status.GetDisplayName(); +``` + +## Practical Recommendation + +``` +Is the property on SQL Server? + β†’ Yes, and null propagation is acceptable: use Ignore (simpler SQL) + β†’ Yes, but you need explicit null behavior: use Rewrite + β†’ No (Cosmos DB, in-memory, or client-side eval): use Rewrite or manual check +``` + +::: tip +Start with `Ignore` for SQL Server projects. Switch to `Rewrite` if you observe unexpected nullability behavior in query results. +::: + diff --git a/docs/recipes/reusable-query-filters.md b/docs/recipes/reusable-query-filters.md new file mode 100644 index 0000000..cec5100 --- /dev/null +++ b/docs/recipes/reusable-query-filters.md @@ -0,0 +1,167 @@ +ο»Ώ# Reusable Query Filters + +This recipe shows how to define reusable filtering logic as projectable extension methods or properties, and compose them across multiple queries without duplicating LINQ expressions. + +## The Pattern + +Define your filtering criteria as `[Projectable]` members that return `bool`. Use them in `Where()` clauses exactly as you would any other property. EF Core translates the expanded expression to a SQL `WHERE` clause. + +## Example: Active Entity Filter + +```csharp +public class User +{ + public bool IsDeleted { get; set; } + public DateTime? LastLoginDate { get; set; } + public DateTime? EmailVerifiedDate { get; set; } + + [Projectable] + public bool IsActive => + !IsDeleted + && EmailVerifiedDate != null + && LastLoginDate >= DateTime.UtcNow.AddDays(-90); +} +``` + +```csharp +// Reuse everywhere +var activeUsers = dbContext.Users.Where(u => u.IsActive).ToList(); +var activeAdmins = dbContext.Users.Where(u => u.IsActive && u.IsAdmin).ToList(); +var activeCount = dbContext.Users.Count(u => u.IsActive); +``` + +Generated SQL (simplified): +```sql +SELECT * FROM [Users] +WHERE [IsDeleted] = 0 + AND [EmailVerifiedDate] IS NOT NULL + AND [LastLoginDate] >= DATEADD(day, -90, GETUTCDATE()) +``` + +## Example: Parameterized Filter as Extension Method + +Extension methods are ideal for filters that accept parameters: + +```csharp +public static class OrderExtensions +{ + [Projectable] + public static bool IsWithinDateRange(this Order order, DateTime from, DateTime to) => + order.CreatedDate >= from && order.CreatedDate <= to; + + [Projectable] + public static bool IsHighValue(this Order order, decimal threshold) => + order.GrandTotal >= threshold; + + [Projectable] + public static bool BelongsToRegion(this Order order, string region) => + order.ShippingAddress != null && order.ShippingAddress.Region == region; +} +``` + +```csharp +var from = DateTime.UtcNow.AddMonths(-1); +var to = DateTime.UtcNow; + +var recentHighValueOrders = dbContext.Orders + .Where(o => o.IsWithinDateRange(from, to)) + .Where(o => o.IsHighValue(500m)) + .ToList(); +``` + +## Example: Composing Multiple Filters + +Build complex filters by composing simpler ones: + +```csharp +public class Order +{ + [Projectable] + public bool IsFulfilled => FulfilledDate != null; + + [Projectable] + public bool IsRecent => CreatedDate >= DateTime.UtcNow.AddDays(-30); + + // Composed from simpler projectables + [Projectable] + public bool IsRecentFulfilledOrder => IsFulfilled && IsRecent; +} + +public static class OrderExtensions +{ + [Projectable] + public static bool IsEligibleForReturn(this Order order) => + order.IsFulfilled + && order.FulfilledDate >= DateTime.UtcNow.AddDays(-30) + && !order.HasOpenReturnRequest; +} +``` + +## Example: Global Query Filters + +Projectable properties work in EF Core's global query filters (configured in `OnModelCreating`): + +```csharp +protected override void OnModelCreating(ModelBuilder modelBuilder) +{ + // Soft-delete global filter using a projectable property + modelBuilder.Entity() + .HasQueryFilter(o => !o.IsDeleted); + + // Tenant isolation filter + modelBuilder.Entity() + .HasQueryFilter(o => o.TenantId == _currentTenantId); +} +``` + +::: info +When using global query filters with Projectables, ensure that `UseProjectables()` is configured on your `DbContext`. The library includes a convention (`ProjectablesExpandQueryFiltersConvention`) that ensures global filters referencing projectable members are also expanded correctly. +::: + +## Example: Specification Pattern + +Projectables pair naturally with the Specification pattern: + +```csharp +public static class OrderSpecifications +{ + [Projectable] + public static bool IsActive(this Order order) => + !order.IsCancelled && !order.IsDeleted; + + [Projectable] + public static bool IsOverdue(this Order order) => + order.IsActive() + && order.DueDate < DateTime.UtcNow + && !order.IsFulfilled; + + [Projectable] + public static bool RequiresAttention(this Order order) => + order.IsOverdue() + || order.HasOpenDispute + || order.PaymentStatus == PaymentStatus.Failed; +} +``` + +```csharp +// Dashboard: count orders requiring attention +var attentionCount = await dbContext.Orders + .Where(o => o.RequiresAttention()) + .CountAsync(); + +// Alert users with overdue orders +var overdueUserIds = await dbContext.Orders + .Where(o => o.IsOverdue()) + .Select(o => o.UserId) + .Distinct() + .ToListAsync(); +``` + +## Tips + +- **Keep filters pure** β€” filter projectables should only read data, never modify it. +- **Compose at the projectable level** β€” compose filters inside projectable members rather than chaining multiple `.Where()` calls for more reusable building blocks. +- **Name clearly** β€” use names that express business intent (`IsEligibleForRefund`) rather than technical details (`HasRefundDateNullAndStatusIsComplete`). +- **Prefer entity-level properties for entity-specific filters**, and extension methods for cross-entity or parameterized filters. +- **Use Limited mode** β€” parameterized filter methods are a perfect use case for [Limited compatibility mode](/reference/compatibility-mode), which caches the expanded query after the first execution. + diff --git a/docs/reference/compatibility-mode.md b/docs/reference/compatibility-mode.md new file mode 100644 index 0000000..2ea2ea8 --- /dev/null +++ b/docs/reference/compatibility-mode.md @@ -0,0 +1,111 @@ +ο»Ώ# Compatibility Mode + +Compatibility mode controls **when** and **how** EF Core Projectables expands your projectable members during query execution. The choice affects both performance and query caching behavior. + +## Configuration + +Set the compatibility mode when registering Projectables: + +```csharp +options.UseProjectables(projectables => + projectables.CompatibilityMode(CompatibilityMode.Limited)); +``` + +## Modes + +### `Full` (Default) + +```csharp +options.UseProjectables(); // Full is the default + +// Or explicitly: +options.UseProjectables(p => p.CompatibilityMode(CompatibilityMode.Full)); +``` + +In Full mode, the expression tree is **expanded on every individual query invocation**, before being passed to EF Core. This is similar to how libraries like LinqKit work. + +**Flow:** +``` +LINQ query + β†’ [Projectables expands all member calls] + β†’ Expanded query sent to EF Core compiler + β†’ SQL generated and executed +``` + +**Characteristics:** +- βœ… Works with **dynamic parameters** β€” captures fresh parameter values on each execution. +- βœ… Maximum compatibility β€” works in all EF Core scenarios. +- ⚠️ Slight overhead per query invocation (expression tree walking + expansion). +- ⚠️ EF Core's query cache key changes with expanded expressions, so the compiled query cache may be less effective. + +**When to use Full:** +- When you're running into query compilation errors with Limited mode. +- When your projectable members depend on dynamic expressions that change between calls. +- As a safe default while getting started. + +--- + +### `Limited` + +```csharp +options.UseProjectables(p => p.CompatibilityMode(CompatibilityMode.Limited)); +``` + +In Limited mode, expansion happens inside **EF Core's query translation preprocessor** β€” after EF Core accepts the query and before it compiles it. The expanded query is then stored in EF Core's query cache. Subsequent executions with the same query shape skip the expansion step entirely. + +**Flow:** +``` +LINQ query + β†’ EF Core query preprocessor + β†’ [Projectables expands member calls here] + β†’ Expanded query compiled and stored in query cache + β†’ SQL generated and executed + +Second execution with same query shape: + β†’ EF Core query cache hit + β†’ Compiled query reused directly (no expansion needed) +``` + +**Characteristics:** +- βœ… **Better performance** β€” after the first execution, cached queries bypass expansion entirely. +- βœ… Often **outperforms vanilla EF Core** for repeated queries. +- ⚠️ Dynamic parameters captured as closures may not work correctly β€” the expanded query is cached with the parameter values from the first execution. +- ⚠️ If a projectable member uses external runtime state (not EF Core query parameters), the cached expansion may be stale. + +**When to use Limited:** +- When all your projectable members' logic is deterministic given the query parameters. +- In production environments where query performance is critical. +- When queries are executed many times with the same shape. + +## Performance Comparison + +| Scenario | Full | Limited | Vanilla EF Core | +|---|---|---|---| +| First query execution | Slower (expansion overhead) | Slower (expansion + compile) | Baseline | +| Subsequent executions | Slower (expansion overhead) | **Faster** (cache hit, no expansion) | Baseline | +| Dynamic projectable parameters | βœ… Correct | ⚠️ May be stale | N/A | + +## Choosing a Mode + +``` +Start with Full (default) + ↓ +Is performance critical? + β†’ No: Stay on Full + β†’ Yes: Try Limited + ↓ + Do your queries produce correct results with Limited? + β†’ Yes: Use Limited + β†’ No: Stay on Full +``` + +## Troubleshooting + +### Queries returning wrong results in Limited mode + +If you're using projectable members that depend on values computed at runtime (outside of EF Core's parameter system), Limited mode may cache the wrong expansion. Switch to Full mode. + +### Query compilation errors in Full mode + +If Full mode causes compilation errors related to expression tree translation, check that your projectable members only use EF Core-translatable expressions. Refer to [Limitations](/advanced/limitations). + diff --git a/docs/reference/diagnostics.md b/docs/reference/diagnostics.md new file mode 100644 index 0000000..a7bc293 --- /dev/null +++ b/docs/reference/diagnostics.md @@ -0,0 +1,296 @@ +ο»Ώ# Diagnostics Reference + +The Projectables source generator emits diagnostics (warnings and errors) during compilation to help you identify and fix issues with your projectable members. + +## Overview + +| ID | Severity | Title | +|---|---|---| +| [EFP0001](#efp0001) | ⚠️ Warning | Block-bodied member support is experimental | +| [EFP0002](#efp0002) | ❌ Error | Null-conditional expression not configured | +| [EFP0003](#efp0003) | ⚠️ Warning | Unsupported statement in block-bodied method | +| [EFP0004](#efp0004) | ❌ Error | Statement with side effects in block-bodied method | +| [EFP0005](#efp0005) | ⚠️ Warning | Potential side effect in block-bodied method | +| [EFP0006](#efp0006) | ❌ Error | Method or property requires a body definition | + +--- + +## EFP0001 β€” Block-bodied member support is experimental {#efp0001} + +**Severity:** Warning +**Category:** Design + +### Message + +``` +Block-bodied member '{0}' is using an experimental feature. +Set AllowBlockBody = true on the Projectable attribute to suppress this warning. +``` + +### Cause + +A `[Projectable]` member uses a block body (`{ ... }`) instead of an expression body (`=>`), which is an experimental feature. + +### Fix + +Suppress the warning by setting `AllowBlockBody = true`: + +```csharp +// Before (warning) +[Projectable] +public string GetCategory() +{ + if (Value > 100) return "High"; + return "Low"; +} + +// After (warning suppressed) +[Projectable(AllowBlockBody = true)] +public string GetCategory() +{ + if (Value > 100) return "High"; + return "Low"; +} +``` + +Or convert to an expression-bodied member: + +```csharp +[Projectable] +public string GetCategory() => Value > 100 ? "High" : "Low"; +``` + +--- + +## EFP0002 β€” Null-conditional expression not configured {#efp0002} + +**Severity:** Error +**Category:** Design + +### Message + +``` +'{0}' has a null-conditional expression exposed but is not configured to rewrite this +(Consider configuring a strategy using the NullConditionalRewriteSupport property +on the Projectable attribute) +``` + +### Cause + +The projectable member's body contains a null-conditional operator (`?.`) but `NullConditionalRewriteSupport` is not configured (defaults to `None`). + +### Fix + +Configure how the `?.` operator should be handled: + +```csharp +// ❌ Error +[Projectable] +public string? FullAddress => Location?.AddressLine1 + " " + Location?.City; + +// βœ… Option 1: Ignore (strips the ?. β€” safe for SQL Server) +[Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore)] +public string? FullAddress => Location?.AddressLine1 + " " + Location?.City; + +// βœ… Option 2: Rewrite (explicit null checks) +[Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Rewrite)] +public string? FullAddress => Location?.AddressLine1 + " " + Location?.City; + +// βœ… Option 3: Rewrite the expression manually +[Projectable] +public string? FullAddress => + Location != null ? Location.AddressLine1 + " " + Location.City : null; +``` + +See [Null-Conditional Rewrite](/reference/null-conditional-rewrite) for details. + +--- + +## EFP0003 β€” Unsupported statement in block-bodied method {#efp0003} + +**Severity:** Warning +**Category:** Design + +### Message + +``` +Method '{0}' contains an unsupported statement: {1} +``` + +### Cause + +A block-bodied `[Projectable]` member contains a statement type that cannot be converted to an expression tree (e.g., loops, try-catch, throw, new object instantiation in statement position). + +### Unsupported Statements + +- `while`, `for`, `foreach` loops +- `try`/`catch`/`finally` blocks +- `throw` statements +- Object instantiation as a statement (not in a `return`) + +### Fix + +Refactor to use only supported constructs (`if`/`else`, `switch`, local variables, `return`), or convert to an expression-bodied member: + +```csharp +// ❌ Warning: loops are not supported +[Projectable(AllowBlockBody = true)] +public int SumItems() +{ + int total = 0; + foreach (var item in Items) // EFP0003 + total += item.Price; + return total; +} + +// βœ… Use LINQ instead +[Projectable] +public int SumItems() => Items.Sum(i => i.Price); +``` + +--- + +## EFP0004 β€” Statement with side effects in block-bodied method {#efp0004} + +**Severity:** Error +**Category:** Design + +### Message + +Context-specific β€” one of: + +- `Property assignment '{0}' has side effects and cannot be used in projectable methods` +- `Compound assignment operator '{0}' has side effects` +- `Increment/decrement operator '{0}' has side effects` + +### Cause + +A block-bodied projectable member modifies state. Expression trees cannot represent side effects. + +### Triggers + +```csharp +// ❌ Property assignment +Bar = 10; + +// ❌ Compound assignment +Bar += 10; + +// ❌ Increment / Decrement +Bar++; +--Count; +``` + +### Fix + +Remove the side-effecting statement. Projectable members must be **pure functions** β€” they can only read data and return a value. + +```csharp +// ❌ Error: has side effects +[Projectable(AllowBlockBody = true)] +public int Foo() +{ + Bar = 10; // EFP0004 + return Bar; +} + +// βœ… Read-only computation +[Projectable] +public int Foo() => Bar + 10; +``` + +--- + +## EFP0005 β€” Potential side effect in block-bodied method {#efp0005} + +**Severity:** Warning +**Category:** Design + +### Message + +``` +Method call '{0}' may have side effects and cannot be guaranteed to be safe in projectable methods +``` + +### Cause + +A block-bodied projectable member calls a method that is **not** itself marked with `[Projectable]`. Such calls may have side effects that cannot be represented in an expression tree. + +### Example + +```csharp +[Projectable(AllowBlockBody = true)] +public int Foo() +{ + Console.WriteLine("test"); // ⚠️ EFP0005 β€” may have side effects + return Bar; +} +``` + +### Fix + +- Remove the method call if it is not needed in a query context. +- If the method is safe to use in queries, mark it with `[Projectable]`. + +--- + +## EFP0006 β€” Method or property requires a body definition {#efp0006} + +**Severity:** Error +**Category:** Design + +### Message + +``` +Method or property '{0}' should expose a body definition (e.g. an expression-bodied member +or a block-bodied method) to be used as the source for the generated expression tree. +``` + +### Cause + +A `[Projectable]` member has no body β€” it is abstract, an interface declaration, or an auto-property without an expression. + +### Fix + +Provide a body, or use [`UseMemberBody`](/reference/use-member-body) to delegate to another member: + +```csharp +// ❌ Error: no body +[Projectable] +public string FullName { get; set; } + +// βœ… Expression-bodied property +[Projectable] +public string FullName => FirstName + " " + LastName; + +// βœ… Delegate to another member +[Projectable(UseMemberBody = nameof(ComputeFullName))] +public string FullName => ComputeFullName(); +private string ComputeFullName() => FirstName + " " + LastName; +``` + +--- + +## Suppressing Diagnostics + +Individual warnings can be suppressed with standard C# pragma directives: + +```csharp +#pragma warning disable EFP0001 +[Projectable] +public string GetValue() +{ + if (IsActive) return "Active"; + return "Inactive"; +} +#pragma warning restore EFP0001 +``` + +Or via `.editorconfig` / `Directory.Build.props`: + +```xml + + $(NoWarn);EFP0001;EFP0003 + +``` + diff --git a/docs/reference/expand-enum-methods.md b/docs/reference/expand-enum-methods.md new file mode 100644 index 0000000..a293e22 --- /dev/null +++ b/docs/reference/expand-enum-methods.md @@ -0,0 +1,169 @@ +ο»Ώ# Expand Enum Methods + +The `ExpandEnumMethods` option allows you to call ordinary C# methods on enum values inside a projectable member and have those calls translated to SQL `CASE` expressions. Without this option, calling a non-projectable method on an enum value would fail SQL translation. + +## The Problem + +You have an enum with a helper method: + +```csharp +public enum OrderStatus { Pending, Approved, Rejected } + +public static class OrderStatusExtensions +{ + public static string GetDisplayName(this OrderStatus status) => + status switch { + OrderStatus.Pending => "Pending Review", + OrderStatus.Approved => "Approved", + OrderStatus.Rejected => "Rejected", + _ => status.ToString() + }; +} +``` + +If you try to use `GetDisplayName()` inside a projectable member without `ExpandEnumMethods`, the generator cannot produce a valid expression tree because `GetDisplayName` is not a database function. + +## The Solution + +Set `ExpandEnumMethods = true` on the projectable member that calls the enum method: + +```csharp +public class Order +{ + public OrderStatus Status { get; set; } + + [Projectable(ExpandEnumMethods = true)] + public string StatusName => Status.GetDisplayName(); +} +``` + +The generator enumerates all values of `OrderStatus` and produces a chain of ternary expressions: + +```csharp +// Generated expression equivalent +Status == OrderStatus.Pending ? GetDisplayName(OrderStatus.Pending) : +Status == OrderStatus.Approved ? GetDisplayName(OrderStatus.Approved) : +Status == OrderStatus.Rejected ? GetDisplayName(OrderStatus.Rejected) : +null +``` + +EF Core then translates this to a SQL `CASE` expression: + +```sql +SELECT CASE + WHEN [o].[Status] = 0 THEN N'Pending Review' + WHEN [o].[Status] = 1 THEN N'Approved' + WHEN [o].[Status] = 2 THEN N'Rejected' +END AS [StatusName] +FROM [Orders] AS [o] +``` + +## Supported Return Types + +| Return type | Default fallback value | +|---|---| +| `string` | `null` | +| `bool` | `default(bool)` β†’ `false` | +| `int` | `default(int)` β†’ `0` | +| Other value types | `default(T)` | +| Nullable types | `null` | + +## Examples + +### Boolean Return + +```csharp +public static bool IsApproved(this OrderStatus status) => + status == OrderStatus.Approved; + +[Projectable(ExpandEnumMethods = true)] +public bool IsStatusApproved => Status.IsApproved(); +``` + +Generated SQL: +```sql +SELECT CASE + WHEN [o].[Status] = 0 THEN CAST(0 AS bit) + WHEN [o].[Status] = 1 THEN CAST(1 AS bit) + WHEN [o].[Status] = 2 THEN CAST(0 AS bit) + ELSE CAST(0 AS bit) +END AS [IsStatusApproved] +FROM [Orders] AS [o] +``` + +### Integer Return + +```csharp +public static int GetSortOrder(this OrderStatus status) => (int)status; + +[Projectable(ExpandEnumMethods = true)] +public int StatusSortOrder => Status.GetSortOrder(); +``` + +Generated SQL: +```sql +SELECT CASE + WHEN [o].[Status] = 0 THEN 0 + WHEN [o].[Status] = 1 THEN 1 + WHEN [o].[Status] = 2 THEN 2 + ELSE 0 +END AS [StatusSortOrder] +FROM [Orders] AS [o] +``` + +### Methods with Additional Parameters + +Additional parameters are passed through to each branch of the expanded ternary: + +```csharp +public static string Format(this OrderStatus status, string prefix) => + prefix + status.ToString(); + +[Projectable(ExpandEnumMethods = true)] +public string FormattedStatus => Status.Format("Status: "); +``` + +### Nullable Enum Types + +If the enum property is nullable, the expansion is wrapped in a null check: + +```csharp +public class Order +{ + public OrderStatus? OptionalStatus { get; set; } + + [Projectable(ExpandEnumMethods = true)] + public string? OptionalStatusName => OptionalStatus?.GetDisplayName(); +} +``` + +### Enum on Navigation Properties + +```csharp +public class Order +{ + public Customer Customer { get; set; } + + [Projectable(ExpandEnumMethods = true)] + public string CustomerTierName => Customer.Tier.GetDisplayName(); +} +``` + +## Limitations + +- The method being expanded **must be deterministic** β€” it will be evaluated at code-generation time for each enum value. +- All enum values must produce valid SQL-translatable results. +- The enum type must be known at compile time (no dynamic enum types). +- Only the outermost enum method call on the enum property is expanded; nested calls may require multiple projectable members. + +## Comparison with `[Projectable]` on the Extension Method + +You might wonder: why not just put `[Projectable]` on `GetDisplayName` itself? + +| Approach | When to use | +|---|---| +| `[Projectable]` on the extension method | The method body is a simple expression EF Core can translate (e.g., `== OrderStatus.Approved`). | +| `ExpandEnumMethods = true` | The method body is complex or references non-EF-translatable code (e.g., reads a `[Display]` attribute via reflection). | + +`ExpandEnumMethods` evaluates the method at **compile time** for each enum value and bakes the results into the expression tree, so the method body doesn't need to be translatable at all. + diff --git a/docs/reference/null-conditional-rewrite.md b/docs/reference/null-conditional-rewrite.md new file mode 100644 index 0000000..9cfc7ca --- /dev/null +++ b/docs/reference/null-conditional-rewrite.md @@ -0,0 +1,152 @@ +ο»Ώ# Null-Conditional Rewrite + +Expression trees β€” the representation EF Core uses internally β€” cannot directly express the null-conditional operator (`?.`). If your projectable member contains `?.`, the source generator needs to know how to handle it. + +## The Problem + +Consider this projectable property: + +```csharp +[Projectable] +public string? FullAddress => + Location?.AddressLine1 + " " + Location?.City; +``` + +This is valid C# code, but it **cannot be converted to an expression tree as-is**. The null-conditional operator is syntactic sugar that cannot be represented directly in an `Expression>`. + +By default (with `NullConditionalRewriteSupport.None`), the generator will report **error EFP0002** and refuse to generate code. + +## The `NullConditionalRewriteSupport` Options + +Configure the strategy on the `[Projectable]` attribute: + +```csharp +[Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore)] +``` + +--- + +### `None` (Default) + +```csharp +[Projectable] // NullConditionalRewriteSupport.None is the default +public string? FullAddress => Location?.AddressLine1 + " " + Location?.City; +``` + +The generator **rejects** any use of `?.`. This produces error EFP0002: + +``` +error EFP0002: 'FullAddress' has a null-conditional expression exposed but is not configured +to rewrite this (Consider configuring a strategy using the NullConditionalRewriteSupport +property on the Projectable attribute) +``` + +**Use when:** Your projectable members never use `?.` β€” this is the safest default that prevents accidental misuse. + +--- + +### `Ignore` + +```csharp +[Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore)] +public string? FullAddress => Location?.AddressLine1 + " " + Location?.City; +``` + +The null-conditional operators are **stripped** β€” `A?.B` becomes `A.B`. + +Generated expression is equivalent to: +```csharp +Location.AddressLine1 + " " + Location.City +``` + +**Behavior in SQL:** The result is `NULL` if any operand is `NULL`, because SQL's null propagation works implicitly in most expressions. This is consistent with how most SQL databases handle null values. + +**Use when:** +- You're using SQL Server or another database where null propagation in expressions works as expected. +- You know the navigation property will not be null in practice (or null is an acceptable result when it is). +- You want simpler, shorter SQL output. + +**Generated SQL example (SQL Server):** +```sql +SELECT ([u].[AddressLine1] + N' ') + [u].[City] +FROM [Users] AS [u] +``` + +--- + +### `Rewrite` + +```csharp +[Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Rewrite)] +public string? FullAddress => Location?.AddressLine1 + " " + Location?.City; +``` + +The null-conditional operators are **rewritten as explicit null checks** β€” `A?.B` becomes `A != null ? A.B : null`. + +Generated expression is equivalent to: +```csharp +(Location != null ? Location.AddressLine1 : null) ++ " " + +(Location != null ? Location.City : null) +``` + +**Use when:** +- You need **explicit null handling** in the generated expression. +- You're targeting Cosmos DB or another provider that evaluates expressions client-side. +- You want the expression to behave identically to the original C# code. + +**Trade-off:** The generated SQL can become significantly more complex, especially with deeply nested null-conditional chains. + +**Generated SQL example (SQL Server):** +```sql +SELECT + CASE WHEN [u].[LocationId] IS NOT NULL THEN [l].[AddressLine1] ELSE NULL END + + N' ' + + CASE WHEN [u].[LocationId] IS NOT NULL THEN [l].[City] ELSE NULL END +FROM [Users] AS [u] +LEFT JOIN [Locations] AS [l] ON [u].[LocationId] = [l].[Id] +``` + +## Comparison Table + +| Option | `?.` allowed | Expression generated | SQL complexity | +|---|---|---|---| +| `None` | ❌ (error EFP0002) | β€” | β€” | +| `Ignore` | βœ… | `A.B` | Simple | +| `Rewrite` | βœ… | `A != null ? A.B : null` | Higher | + +## Practical Recommendation + +- **SQL Server + navigations you control:** Use `Ignore` β€” SQL Server's null semantics match C#'s null-conditional in most cases. +- **Cosmos DB or client-side evaluation:** Use `Rewrite` β€” you need explicit null checks. +- **Unsure:** Start with `Rewrite` for correctness, optimize to `Ignore` if SQL complexity is an issue. + +## Example: Navigation Property Chain + +```csharp +public class User +{ + public Address? Location { get; set; } +} + +public class Address +{ + public string? AddressLine1 { get; set; } + public string? City { get; set; } +} + +// Option 1: Ignore (simpler SQL) +[Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore)] +public static string? GetFullAddress(this User? user) => + user?.Location?.AddressLine1 + " " + user?.Location?.City; +// β†’ user.Location.AddressLine1 + " " + user.Location.City + +// Option 2: Rewrite (explicit null checks, safer) +[Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Rewrite)] +public static string? GetFullAddress(this User? user) => + user?.Location?.AddressLine1 + " " + user?.Location?.City; +// β†’ (user != null ? (user.Location != null ? user.Location.AddressLine1 : null) : null) +// + " " + +// (user != null ? (user.Location != null ? user.Location.City : null) : null) +``` + diff --git a/docs/reference/projectable-attribute.md b/docs/reference/projectable-attribute.md new file mode 100644 index 0000000..1b09deb --- /dev/null +++ b/docs/reference/projectable-attribute.md @@ -0,0 +1,149 @@ +ο»Ώ# `[Projectable]` Attribute + +The `ProjectableAttribute` is the entry point for this library. Place it on any property or method to tell the source generator to produce a companion expression tree for it. + +## Namespace + +```csharp +using EntityFrameworkCore.Projectables; +``` + +## Target + +| Target | Supported | +|---|---| +| Properties | βœ… | +| Methods | βœ… | +| Extension methods | βœ… | +| Constructors | ❌ | +| Indexers | ❌ | + +The attribute can be inherited by derived types (`Inherited = true`). + +## Properties + +### `NullConditionalRewriteSupport` + +**Type:** `NullConditionalRewriteSupport` +**Default:** `NullConditionalRewriteSupport.None` + +Controls how null-conditional operators (`?.`) in the member body are handled by the source generator. + +```csharp +[Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Rewrite)] +public string? FullAddress => Location?.AddressLine1 + " " + Location?.City; +``` + +| Value | Behavior | +|---|---| +| `None` (default) | Null-conditional operators are **not allowed** β€” the generator raises error EFP0002. | +| `Ignore` | Null-conditional operators are **stripped** β€” `A?.B` becomes `A.B`. Safe for databases where null propagates implicitly (SQL Server). | +| `Rewrite` | Null-conditional operators are **rewritten** as explicit null checks β€” `A?.B` becomes `A != null ? A.B : null`. Safer but may increase SQL complexity. | + +See [Null-Conditional Rewrite](/reference/null-conditional-rewrite) for full details. + +--- + +### `UseMemberBody` + +**Type:** `string?` +**Default:** `null` + +Tells the generator to use a **different member's body** as the source for the generated expression tree. Useful when the projectable member's body is not directly available (e.g., interface implementation, abstract member). + +```csharp +public class Order +{ + // The actual computation is defined here + private decimal ComputeGrandTotal() => Subtotal + Tax; + + // The projectable member delegates to it + [Projectable(UseMemberBody = nameof(ComputeGrandTotal))] + public decimal GrandTotal => ComputeGrandTotal(); +} +``` + +See [Use Member Body](/reference/use-member-body) for full details. + +--- + +### `ExpandEnumMethods` + +**Type:** `bool` +**Default:** `false` + +When set to `true`, method calls on enum values inside this projectable member are expanded into a **chain of ternary expressions** β€” one branch per enum value. This allows enum helper methods (like display name lookups) to be translated to SQL `CASE` expressions. + +```csharp +[Projectable(ExpandEnumMethods = true)] +public string StatusName => Status.GetDisplayName(); +``` + +See [Expand Enum Methods](/reference/expand-enum-methods) for full details. + +--- + +### `AllowBlockBody` + +**Type:** `bool` +**Default:** `false` + +Enables **block-bodied member** support (experimental). Without this flag, using a block body with `[Projectable]` produces warning EFP0001. Setting this to `true` suppresses the warning. + +```csharp +[Projectable(AllowBlockBody = true)] +public string Category +{ + get + { + if (Score > 100) return "High"; + else if (Score > 50) return "Medium"; + else return "Low"; + } +} +``` + +See [Block-Bodied Members](/advanced/block-bodied-members) for full details. + +--- + +## Complete Example + +```csharp +public class Order +{ + public OrderStatus Status { get; set; } + public decimal TaxRate { get; set; } + public Address? ShippingAddress { get; set; } + public ICollection Items { get; set; } + + // Simple computed property + [Projectable] + public decimal Subtotal => Items.Sum(i => i.Price * i.Quantity); + + // Composing projectables + [Projectable] + public decimal GrandTotal => Subtotal * (1 + TaxRate); + + // Handling nullable navigation + [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Rewrite)] + public string? ShippingLine => ShippingAddress?.AddressLine1 + ", " + ShippingAddress?.City; + + // Enum expansion + [Projectable(ExpandEnumMethods = true)] + public string StatusLabel => Status.GetDisplayName(); + + // Block-bodied (experimental) + [Projectable(AllowBlockBody = true)] + public string Priority + { + get + { + if (GrandTotal > 1000) return "High"; + if (GrandTotal > 500) return "Medium"; + return "Normal"; + } + } +} +``` + diff --git a/docs/reference/use-member-body.md b/docs/reference/use-member-body.md new file mode 100644 index 0000000..a781faf --- /dev/null +++ b/docs/reference/use-member-body.md @@ -0,0 +1,106 @@ +ο»Ώ# Use Member Body + +The `UseMemberBody` option tells the source generator to use a **different member's body** as the source expression for the generated expression tree. This is useful when the projectable member itself cannot have a body. + +## Basic Usage + +```csharp +[Projectable(UseMemberBody = nameof(ComputeFullName))] +public string FullName => ComputeFullName(); + +private string ComputeFullName() => FirstName + " " + LastName; +``` + +The generator reads the body of `ComputeFullName` and generates an expression tree from it, even though `FullName` is marked as the projectable. + +## Use Cases + +### Interface Members + +Interface members cannot have bodies. Use `UseMemberBody` to delegate to a default implementation or a helper: + +```csharp +public interface IOrderSummary +{ + decimal GrandTotal { get; } +} + +public class Order : IOrderSummary +{ + public decimal TaxRate { get; set; } + public ICollection Items { get; set; } + + // The actual computation + private decimal ComputeGrandTotal() => + Items.Sum(i => i.Price * i.Quantity) * (1 + TaxRate); + + // Marks the interface member as projectable, delegates body + [Projectable(UseMemberBody = nameof(ComputeGrandTotal))] + public decimal GrandTotal => ComputeGrandTotal(); +} +``` + +### Separating Declaration from Implementation + +Keep the entity class clean by delegating computation to private helpers: + +```csharp +public class Customer +{ + public DateTime BirthDate { get; set; } + public DateTime LastOrderDate { get; set; } + + [Projectable(UseMemberBody = nameof(ComputeAge))] + public int Age => ComputeAge(); + + [Projectable(UseMemberBody = nameof(ComputeDaysSinceLastOrder))] + public int DaysSinceLastOrder => ComputeDaysSinceLastOrder(); + + // Implementation details hidden from the projectable declarations + private int ComputeAge() => + DateTime.Today.Year - BirthDate.Year - (DateTime.Today.DayOfYear < BirthDate.DayOfYear ? 1 : 0); + + private int ComputeDaysSinceLastOrder() => + (DateTime.Today - LastOrderDate).Days; +} +``` + +### Reusing Bodies Across Multiple Members + +The same body can power multiple projectable members: + +```csharp +public class Order +{ + private bool IsEligibleForDiscount() => + Items.Count > 5 && TotalValue > 100; + + // Both members share the same expression body + [Projectable(UseMemberBody = nameof(IsEligibleForDiscount))] + public bool CanApplyDiscount => IsEligibleForDiscount(); + + [Projectable(UseMemberBody = nameof(IsEligibleForDiscount))] + public bool ShowDiscountBadge => IsEligibleForDiscount(); +} +``` + +## Rules + +- The referenced member (via `UseMemberBody`) must exist in the **same class** as the projectable member. +- The referenced member must have a **compatible return type**. +- The referenced member must be an **expression-bodied method or property** (it doesn't need `[Projectable]` itself). +- The referenced member must have a **compatible parameter list** β€” if the projectable is a method with parameters, the referenced member must have matching parameters. + +## Method with Parameters + +```csharp +public class Order +{ + [Projectable(UseMemberBody = nameof(ComputeDiscountedTotal))] + public decimal GetDiscountedTotal(decimal discountRate) => ComputeDiscountedTotal(discountRate); + + private decimal ComputeDiscountedTotal(decimal discountRate) => + GrandTotal * (1 - discountRate); +} +``` + diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_CompoundAssignment_ReportsError.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_CompoundAssignment_ReportsError.verified.txt new file mode 100644 index 0000000..a6b0b53 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_CompoundAssignment_ReportsError.verified.txt @@ -0,0 +1,3 @@ +ο»Ώ[ + (11,13): error EFP0004: Compound assignment operator '+=' has side effects and cannot be used in projectable methods +] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_IfWithoutElse_ImplicitReturn.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_IfWithoutElse_ImplicitReturn.verified.txt new file mode 100644 index 0000000..b5f9f5b --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_IfWithoutElse_ImplicitReturn.verified.txt @@ -0,0 +1,17 @@ +ο»Ώ// +#nullable disable +using System; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_C_Foo + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.C @this) => @this.Bar > 10 ? 1 : default; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_IfWithoutElse_UsesDefault.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_IfWithoutElse_UsesDefault.verified.txt new file mode 100644 index 0000000..c22d885 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_IfWithoutElse_UsesDefault.verified.txt @@ -0,0 +1,17 @@ +ο»Ώ// +#nullable disable +using System; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_C_Foo + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.C @this) => @this.Bar > 10 ? 1 : 0; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_IncrementOperator_ReportsError.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_IncrementOperator_ReportsError.verified.txt new file mode 100644 index 0000000..d47a3ba --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_IncrementOperator_ReportsError.verified.txt @@ -0,0 +1,3 @@ +ο»Ώ[ + (12,13): error EFP0004: Increment/decrement operator '++' has side effects and cannot be used in projectable methods +] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalInIfCondition.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalInIfCondition.verified.txt new file mode 100644 index 0000000..47b44c4 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalInIfCondition.verified.txt @@ -0,0 +1,17 @@ +ο»Ώ// +#nullable disable +using System; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_C_Foo + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.C @this) => (@this.Bar * 2) > 10 ? 1 : 0; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalInSwitchExpression.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalInSwitchExpression.verified.txt new file mode 100644 index 0000000..ce11b5b --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalInSwitchExpression.verified.txt @@ -0,0 +1,17 @@ +ο»Ώ// +#nullable disable +using System; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_C_Foo + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.C @this) => (@this.Bar * 2) == 2 ? "Two" : (@this.Bar * 2) == 4 ? "Four" : "Other"; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalsInNestedBlock_ProducesDiagnostic.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalsInNestedBlock_ProducesDiagnostic.verified.txt new file mode 100644 index 0000000..587f792 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalsInNestedBlock_ProducesDiagnostic.verified.txt @@ -0,0 +1,3 @@ +ο»Ώ[ + (13,17): warning EFP0003: Method 'Foo' contains an unsupported statement: Local declarations in nested blocks are not supported +] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_NonProjectableMethodCall_ReportsWarning.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_NonProjectableMethodCall_ReportsWarning.verified.txt new file mode 100644 index 0000000..26e6a19 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_NonProjectableMethodCall_ReportsWarning.verified.txt @@ -0,0 +1,3 @@ +ο»Ώ[ + (11,13): warning EFP0005: Method call 'WriteLine' may have side effects. Only calls to methods marked with [Projectable] are guaranteed to be safe in projectable methods +] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_PropertyAssignment_ReportsError.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_PropertyAssignment_ReportsError.verified.txt new file mode 100644 index 0000000..e684d40 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_PropertyAssignment_ReportsError.verified.txt @@ -0,0 +1,3 @@ +ο»Ώ[ + (11,13): error EFP0004: Assignment operation has side effects and cannot be used in projectable methods +] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_SimpleReturn.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_SimpleReturn.verified.txt new file mode 100644 index 0000000..eeb0754 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_SimpleReturn.verified.txt @@ -0,0 +1,17 @@ +// +#nullable disable +using System; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_C_Foo + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.C @this) => 42; + } + } +} diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_SwitchStatement_Simple.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_SwitchStatement_Simple.verified.txt new file mode 100644 index 0000000..d1a7eb5 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_SwitchStatement_Simple.verified.txt @@ -0,0 +1,17 @@ +ο»Ώ// +#nullable disable +using System; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_C_Foo + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.C @this) => @this.Bar == 1 ? "One" : @this.Bar == 2 ? "Two" : "Other"; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_SwitchStatement_WithMultipleCases.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_SwitchStatement_WithMultipleCases.verified.txt new file mode 100644 index 0000000..c90d6b7 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_SwitchStatement_WithMultipleCases.verified.txt @@ -0,0 +1,17 @@ +ο»Ώ// +#nullable disable +using System; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_C_Foo + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.C @this) => @this.Bar == 1 || @this.Bar == 2 ? "Low" : @this.Bar == 3 || @this.Bar == 4 || @this.Bar == 5 ? "Medium" : "High"; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_SwitchStatement_WithoutDefault.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_SwitchStatement_WithoutDefault.verified.txt new file mode 100644 index 0000000..0a4d15d --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_SwitchStatement_WithoutDefault.verified.txt @@ -0,0 +1,17 @@ +ο»Ώ// +#nullable disable +using System; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_C_Foo + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.C @this) => @this.Bar == 1 ? "One" : @this.Bar == 2 ? "Two" : default; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithIfElse.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithIfElse.verified.txt new file mode 100644 index 0000000..c22d885 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithIfElse.verified.txt @@ -0,0 +1,17 @@ +ο»Ώ// +#nullable disable +using System; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_C_Foo + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.C @this) => @this.Bar > 10 ? 1 : 0; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithIfElseAndCondition.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithIfElseAndCondition.verified.txt new file mode 100644 index 0000000..ef8f31a --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithIfElseAndCondition.verified.txt @@ -0,0 +1,17 @@ +ο»Ώ// +#nullable disable +using System; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_C_Foo + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.C @this) => @this.IsActive && @this.Bar > 0 ? @this.Bar * 2 : 0; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithLocalVariable.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithLocalVariable.verified.txt new file mode 100644 index 0000000..44c2e0f --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithLocalVariable.verified.txt @@ -0,0 +1,17 @@ +ο»Ώ// +#nullable disable +using System; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_C_Foo + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.C @this) => (@this.Bar * 2) + 5; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithMultipleParameters.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithMultipleParameters.verified.txt new file mode 100644 index 0000000..7c1426a --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithMultipleParameters.verified.txt @@ -0,0 +1,17 @@ +ο»Ώ// +#nullable disable +using System; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_C_Add_P0_int_P1_int + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.C @this, int a, int b) => a + b; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithNestedIfElse.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithNestedIfElse.verified.txt new file mode 100644 index 0000000..216b8f2 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithNestedIfElse.verified.txt @@ -0,0 +1,17 @@ +ο»Ώ// +#nullable disable +using System; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_C_Foo + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.C @this) => @this.Bar > 10 ? "High" : @this.Bar > 5 ? "Medium" : "Low"; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithPropertyAccess.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithPropertyAccess.verified.txt new file mode 100644 index 0000000..19e29c9 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithPropertyAccess.verified.txt @@ -0,0 +1,17 @@ +ο»Ώ// +#nullable disable +using System; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_C_Foo + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.C @this) => @this.Bar + 10; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithTransitiveLocalVariables.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithTransitiveLocalVariables.verified.txt new file mode 100644 index 0000000..3e8b98c --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithTransitiveLocalVariables.verified.txt @@ -0,0 +1,17 @@ +ο»Ώ// +#nullable disable +using System; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_C_Foo + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.C @this) => ((@this.Bar * 2) + 5) + 10; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableComputedPropertyWithExplicitBlockGetter.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableComputedPropertyWithExplicitBlockGetter.verified.txt new file mode 100644 index 0000000..1614e52 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableComputedPropertyWithExplicitBlockGetter.verified.txt @@ -0,0 +1,17 @@ +ο»Ώ// +#nullable disable +using System; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_C_Foo + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.C @this) => @this.Bar + 1; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableComputedPropertyWithExplicitExpressionGetter.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableComputedPropertyWithExplicitExpressionGetter.verified.txt new file mode 100644 index 0000000..1614e52 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableComputedPropertyWithExplicitExpressionGetter.verified.txt @@ -0,0 +1,17 @@ +ο»Ώ// +#nullable disable +using System; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_C_Foo + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.C @this) => @this.Bar + 1; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyWithExplicitBlockGetter.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyWithExplicitBlockGetter.verified.txt new file mode 100644 index 0000000..c9f2bbb --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyWithExplicitBlockGetter.verified.txt @@ -0,0 +1,17 @@ +ο»Ώ// +#nullable disable +using System; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_C_Foo + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.C @this) => 1; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyWithExplicitBlockGetterAndMethodCall.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyWithExplicitBlockGetterAndMethodCall.verified.txt new file mode 100644 index 0000000..fb4be05 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyWithExplicitBlockGetterAndMethodCall.verified.txt @@ -0,0 +1,17 @@ +ο»Ώ// +#nullable disable +using System; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_C_Foo + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.C @this) => @this.Bar(); + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyWithExplicitBlockGetterUsingThis.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyWithExplicitBlockGetterUsingThis.verified.txt new file mode 100644 index 0000000..3ad21d6 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyWithExplicitBlockGetterUsingThis.verified.txt @@ -0,0 +1,17 @@ +ο»Ώ// +#nullable disable +using System; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_C_Foo + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.C @this) => @this.Bar; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyWithExplicitExpressionGetter.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyWithExplicitExpressionGetter.verified.txt new file mode 100644 index 0000000..c9f2bbb --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyWithExplicitExpressionGetter.verified.txt @@ -0,0 +1,17 @@ +ο»Ώ// +#nullable disable +using System; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_C_Foo + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.C @this) => 1; + } + } +} \ No newline at end of file