From 1c83f3d6ada0e699fc2779c27a11e548a5a8a653 Mon Sep 17 00:00:00 2001 From: Koen Bekkenutte <2912652+kbekkenutte@users.noreply.github.com> Date: Thu, 27 May 2021 04:17:35 +0800 Subject: [PATCH] Add project files. --- .editorconfig | 133 +++++++ .gitattributes | 63 +++ .gitignore | 363 +++++++++++++++++ Directory.Build.props | 14 + EntityFrameworkCore.Projections.sln | 82 ++++ samples/BasicSample/BasicSample.csproj | 19 + samples/BasicSample/Program.cs | 72 ++++ .../ComplexModelTests.cs | 89 +++++ ...orkCore.Projections.FunctionalTests.csproj | 35 ++ .../Helpers/SampleDbContext.cs | 26 ++ .../StatefullPropertyTests.cs | 79 ++++ .../StatefullSimpleFunctionTests.cs | 81 ++++ .../StatelessComplexFunctionTests.cs | 59 +++ .../StatelessPropertyTests.cs | 49 +++ .../StatelessSimpleFunctionTests.cs | 49 +++ ...meworkCore.Projections.Abstractions.csproj | 8 + .../ProjectableAttribute.cs | 13 + ...FrameworkCore.Projections.Generator.csproj | 16 + .../ExpressionSyntaxRewriter.cs | 64 +++ .../ProjectableDescriptor.cs | 28 ++ .../ProjectableInterpreter.cs | 86 ++++ .../ProjectionExpressionGenerator.cs | 62 +++ .../SyntaxReceiver.cs | 30 ++ .../EntityFrameworkCore.Projections.csproj | 15 + .../Extensions/DbContextOptionsExtensions.cs | 27 ++ .../Extensions/TypeExtensions.cs | 26 ++ .../Internal/ProjectionOptionsExtension.cs | 65 +++ ...ppedQueryTranslationPreprocessorFactory.cs | 54 +++ .../Services/ExpressionArgumentReplacer.cs | 31 ++ .../Services/ProjectableExpressionReplacer.cs | 60 +++ .../ProjectionExpressionClassNameGenerator.cs | 42 ++ .../Services/ProjectionExpressionResolver.cs | 62 +++ ...orkCore.Projections.Generator.Tests.csproj | 36 ++ ...lessProjectableComputedMethod.verified.txt | 11 + ...exProjectableComputedProperty.verified.txt | 11 + ...edMethodWithMultipleArguments.verified.txt | 11 + ...putedMethodWithSingleArgument.verified.txt | 11 + ...ectableComputedPropertyMethod.verified.txt | 11 + ...ableComputedPropertyUsingThis.verified.txt | 11 + ...ropertyToNavigationalProperty.verified.txt | 12 + ...ComputedInNestedClassProperty.verified.txt | 11 + ...leProjectableComputedProperty.verified.txt | 11 + ...Tests.SimpleProjectableMethod.verified.txt | 11 + ...sts.SimpleProjectableProperty.verified.txt | 11 + .../ProjectionExpressionGeneratorTests.cs | 372 ++++++++++++++++++ ...tityFrameworkCore.Projections.Tests.csproj | 26 ++ .../Extensions/TypeExtensionTests.cs | 63 +++ ...ectionExpressionClassNameGeneratorTests.cs | 33 ++ 48 files changed, 2554 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 Directory.Build.props create mode 100644 EntityFrameworkCore.Projections.sln create mode 100644 samples/BasicSample/BasicSample.csproj create mode 100644 samples/BasicSample/Program.cs create mode 100644 samples/EntityFrameworkCore.Projections.FunctionalTests/ComplexModelTests.cs create mode 100644 samples/EntityFrameworkCore.Projections.FunctionalTests/EntityFrameworkCore.Projections.FunctionalTests.csproj create mode 100644 samples/EntityFrameworkCore.Projections.FunctionalTests/Helpers/SampleDbContext.cs create mode 100644 samples/EntityFrameworkCore.Projections.FunctionalTests/StatefullPropertyTests.cs create mode 100644 samples/EntityFrameworkCore.Projections.FunctionalTests/StatefullSimpleFunctionTests.cs create mode 100644 samples/EntityFrameworkCore.Projections.FunctionalTests/StatelessComplexFunctionTests.cs create mode 100644 samples/EntityFrameworkCore.Projections.FunctionalTests/StatelessPropertyTests.cs create mode 100644 samples/EntityFrameworkCore.Projections.FunctionalTests/StatelessSimpleFunctionTests.cs create mode 100644 src/EntityFrameworkCore.Projections.Abstractions/EntityFrameworkCore.Projections.Abstractions.csproj create mode 100644 src/EntityFrameworkCore.Projections.Abstractions/ProjectableAttribute.cs create mode 100644 src/EntityFrameworkCore.Projections.Generator/EntityFrameworkCore.Projections.Generator.csproj create mode 100644 src/EntityFrameworkCore.Projections.Generator/ExpressionSyntaxRewriter.cs create mode 100644 src/EntityFrameworkCore.Projections.Generator/ProjectableDescriptor.cs create mode 100644 src/EntityFrameworkCore.Projections.Generator/ProjectableInterpreter.cs create mode 100644 src/EntityFrameworkCore.Projections.Generator/ProjectionExpressionGenerator.cs create mode 100644 src/EntityFrameworkCore.Projections.Generator/SyntaxReceiver.cs create mode 100644 src/EntityFrameworkCore.Projections/EntityFrameworkCore.Projections.csproj create mode 100644 src/EntityFrameworkCore.Projections/Extensions/DbContextOptionsExtensions.cs create mode 100644 src/EntityFrameworkCore.Projections/Extensions/TypeExtensions.cs create mode 100644 src/EntityFrameworkCore.Projections/Infrastructure/Internal/ProjectionOptionsExtension.cs create mode 100644 src/EntityFrameworkCore.Projections/Infrastructure/Internal/WrappedQueryTranslationPreprocessorFactory.cs create mode 100644 src/EntityFrameworkCore.Projections/Services/ExpressionArgumentReplacer.cs create mode 100644 src/EntityFrameworkCore.Projections/Services/ProjectableExpressionReplacer.cs create mode 100644 src/EntityFrameworkCore.Projections/Services/ProjectionExpressionClassNameGenerator.cs create mode 100644 src/EntityFrameworkCore.Projections/Services/ProjectionExpressionResolver.cs create mode 100644 tests/EntityFrameworkCore.Projections.Generator.Tests/EntityFrameworkCore.Projections.Generator.Tests.csproj create mode 100644 tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.ArgumentlessProjectableComputedMethod.verified.txt create mode 100644 tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.MoreComplexProjectableComputedProperty.verified.txt create mode 100644 tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableComputedMethodWithMultipleArguments.verified.txt create mode 100644 tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableComputedMethodWithSingleArgument.verified.txt create mode 100644 tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableComputedPropertyMethod.verified.txt create mode 100644 tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableComputedPropertyUsingThis.verified.txt create mode 100644 tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyToNavigationalProperty.verified.txt create mode 100644 tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.SimpleProjectableComputedInNestedClassProperty.verified.txt create mode 100644 tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.SimpleProjectableComputedProperty.verified.txt create mode 100644 tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.SimpleProjectableMethod.verified.txt create mode 100644 tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.SimpleProjectableProperty.verified.txt create mode 100644 tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.cs create mode 100644 tests/EntityFrameworkCore.Projections.Tests/EntityFrameworkCore.Projections.Tests.csproj create mode 100644 tests/EntityFrameworkCore.Projections.Tests/Extensions/TypeExtensionTests.cs create mode 100644 tests/EntityFrameworkCore.Projections.Tests/Services/ProjectionExpressionClassNameGeneratorTests.cs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..30882e4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,133 @@ +# Rules in this file were initially inferred by Visual Studio IntelliCode from the C:\Users\Koen\Source\Repos\EntityFrameworkCore.Events codebase based on best match to current usage at 7/15/2020 +# You can modify the rules from these initially generated values to suit your own policies +# You can learn more about editorconfig here: https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference +[*.cs] + + +#Core editorconfig formatting - indentation + +#use soft tabs (spaces) for indentation +indent_style = space + +#Formatting - new line options + +#place else statements on a new line +csharp_new_line_before_else = true +#require members of object intializers to be on separate lines +csharp_new_line_before_members_in_object_initializers = true +#require braces to be on a new line for control_blocks, types, properties, accessors, and methods (also known as "Allman" style) +csharp_new_line_before_open_brace = control_blocks, types, properties, accessors, methods + +#Formatting - organize using options + +#sort System.* using directives alphabetically, and place them before other usings +dotnet_sort_system_directives_first = true + +#Formatting - spacing options + +#require NO space between a cast and the value +csharp_space_after_cast = false +#require a space before the colon for bases or interfaces in a type declaration +csharp_space_after_colon_in_inheritance_clause = true +#require a space after a keyword in a control flow statement such as a for loop +csharp_space_after_keywords_in_control_flow_statements = true +#require a space before the colon for bases or interfaces in a type declaration +csharp_space_before_colon_in_inheritance_clause = true +#remove space within empty argument list parentheses +csharp_space_between_method_call_empty_parameter_list_parentheses = false +#remove space between method call name and opening parenthesis +csharp_space_between_method_call_name_and_opening_parenthesis = false +#do not place space characters after the opening parenthesis and before the closing parenthesis of a method call +csharp_space_between_method_call_parameter_list_parentheses = false +#remove space within empty parameter list parentheses for a method declaration +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +#place a space character after the opening parenthesis and before the closing parenthesis of a method declaration parameter list. +csharp_space_between_method_declaration_parameter_list_parentheses = false + +#Formatting - wrapping options + +#leave code block on single line +csharp_preserve_single_line_blocks = true + +#Style - Code block preferences + +#prefer curly braces even for one line of code +csharp_prefer_braces = true:suggestion + +#Style - expression bodied member options + +#prefer block bodies for constructors +csharp_style_expression_bodied_constructors = false:suggestion +#prefer expression-bodied members for methods +csharp_style_expression_bodied_methods = true:suggestion +#prefer expression-bodied members for properties +csharp_style_expression_bodied_properties = true:suggestion + +#Style - expression level options + +#prefer tuple names to ItemX properties +dotnet_style_explicit_tuple_names = true:suggestion +#prefer the language keyword for member access expressions, instead of the type name, for types that have a keyword to represent them +dotnet_style_predefined_type_for_member_access = true:suggestion + +#Style - Expression-level preferences + +#prefer default over default(T) +csharp_prefer_simple_default_expression = true:suggestion +#prefer objects to be initialized using object initializers when possible +dotnet_style_object_initializer = true:suggestion +#prefer inferred tuple element names +dotnet_style_prefer_inferred_tuple_names = true:suggestion + +#Style - implicit and explicit types + +#prefer var over explicit type in all cases, unless overridden by another code style rule +csharp_style_var_elsewhere = true:suggestion +#prefer var is used to declare variables with built-in system types such as int +csharp_style_var_for_built_in_types = true:suggestion +#prefer var when the type is already mentioned on the right-hand side of a declaration expression +csharp_style_var_when_type_is_apparent = true:suggestion + +#Style - language keyword and framework type options + +#prefer the language keyword for local variables, method parameters, and class members, instead of the type name, for types that have a keyword to represent them +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion + +#Style - Miscellaneous preferences + +#prefer anonymous functions over local functions +csharp_style_pattern_local_over_anonymous_function = false:suggestion + +#Style - modifier options + +#prefer accessibility modifiers to be declared except for public interface members. This will currently not differ from always and will act as future proofing for if C# adds default interface methods. +dotnet_style_require_accessibility_modifiers = false + +#Style - Modifier preferences + +#when this rule is set to a list of modifiers, prefer the specified ordering. +csharp_preferred_modifier_order = public,protected,private,readonly,override,async,sealed,static,abstract,virtual:suggestion + +#Style - Pattern matching + +#prefer pattern matching instead of is expression with type casts +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion + +#Style - qualification options + +#prefer fields not to be prefaced with this. or Me. in Visual Basic +dotnet_style_qualification_for_field = false:suggestion +#prefer methods not to be prefaced with this. or Me. in Visual Basic +dotnet_style_qualification_for_method = false:suggestion +#prefer properties not to be prefaced with this. or Me. in Visual Basic +dotnet_style_qualification_for_property = false:suggestion + +# Instance fields are camelCase and start with _ +dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion +dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields +dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style + +dotnet_naming_symbols.instance_fields.applicable_kinds = field + +dotnet_naming_style.instance_field_style.capitalization = camel_case +dotnet_naming_style.instance_field_style.required_prefix = _ diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9491a2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,363 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Oo]ut/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..27e8f16 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,14 @@ + + + + true + 9.0 + enable + true + + + + Koen Bekkenutte + + + \ No newline at end of file diff --git a/EntityFrameworkCore.Projections.sln b/EntityFrameworkCore.Projections.sln new file mode 100644 index 0000000..fde2c5f --- /dev/null +++ b/EntityFrameworkCore.Projections.sln @@ -0,0 +1,82 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31313.381 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A43F1828-D9B6-40F7-82B6-CA0070843E2F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{F5E4436F-87F2-46AB-A9EB-59B4BF21BF7A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EntityFrameworkCore.Projections.Generator", "src\EntityFrameworkCore.Projections.Generator\EntityFrameworkCore.Projections.Generator.csproj", "{698E3EEC-64F9-4F96-B700-D61D04FD0704}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EntityFrameworkCore.Projections.Generator.Tests", "tests\EntityFrameworkCore.Projections.Generator.Tests\EntityFrameworkCore.Projections.Generator.Tests.csproj", "{20F85652-2923-4211-9262-C64BA8C9ED89}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7B02E555-9633-4522-8C20-AD93C713C9AE}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + Directory.Build.props = Directory.Build.props + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EntityFrameworkCore.Projections", "src\EntityFrameworkCore.Projections\EntityFrameworkCore.Projections.csproj", "{EE4D6CC1-78DE-4279-A567-C3D360C479F8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{07584D01-2D30-404B-B0D1-32080C0CC18A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BasicSample", "samples\BasicSample\BasicSample.csproj", "{1B4A8710-4182-494D-B1C5-6B7CDB9C9DB9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EntityFrameworkCore.Projections.Abstractions", "src\EntityFrameworkCore.Projections.Abstractions\EntityFrameworkCore.Projections.Abstractions.csproj", "{C8038180-36F8-4077-922B-91F428EAC7D9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EntityFrameworkCore.Projections.FunctionalTests", "samples\EntityFrameworkCore.Projections.FunctionalTests\EntityFrameworkCore.Projections.FunctionalTests.csproj", "{56007397-59B0-4DCB-80C4-3AD0BE3F319F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EntityFrameworkCore.Projections.Tests", "tests\EntityFrameworkCore.Projections.Tests\EntityFrameworkCore.Projections.Tests.csproj", "{2F0DD7D7-867F-4478-9E22-45C114B61C46}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {698E3EEC-64F9-4F96-B700-D61D04FD0704}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {698E3EEC-64F9-4F96-B700-D61D04FD0704}.Debug|Any CPU.Build.0 = Debug|Any CPU + {698E3EEC-64F9-4F96-B700-D61D04FD0704}.Release|Any CPU.ActiveCfg = Release|Any CPU + {698E3EEC-64F9-4F96-B700-D61D04FD0704}.Release|Any CPU.Build.0 = Release|Any CPU + {20F85652-2923-4211-9262-C64BA8C9ED89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {20F85652-2923-4211-9262-C64BA8C9ED89}.Debug|Any CPU.Build.0 = Debug|Any CPU + {20F85652-2923-4211-9262-C64BA8C9ED89}.Release|Any CPU.ActiveCfg = Release|Any CPU + {20F85652-2923-4211-9262-C64BA8C9ED89}.Release|Any CPU.Build.0 = Release|Any CPU + {EE4D6CC1-78DE-4279-A567-C3D360C479F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EE4D6CC1-78DE-4279-A567-C3D360C479F8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EE4D6CC1-78DE-4279-A567-C3D360C479F8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EE4D6CC1-78DE-4279-A567-C3D360C479F8}.Release|Any CPU.Build.0 = Release|Any CPU + {1B4A8710-4182-494D-B1C5-6B7CDB9C9DB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1B4A8710-4182-494D-B1C5-6B7CDB9C9DB9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1B4A8710-4182-494D-B1C5-6B7CDB9C9DB9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1B4A8710-4182-494D-B1C5-6B7CDB9C9DB9}.Release|Any CPU.Build.0 = Release|Any CPU + {C8038180-36F8-4077-922B-91F428EAC7D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C8038180-36F8-4077-922B-91F428EAC7D9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C8038180-36F8-4077-922B-91F428EAC7D9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C8038180-36F8-4077-922B-91F428EAC7D9}.Release|Any CPU.Build.0 = Release|Any CPU + {56007397-59B0-4DCB-80C4-3AD0BE3F319F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {56007397-59B0-4DCB-80C4-3AD0BE3F319F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {56007397-59B0-4DCB-80C4-3AD0BE3F319F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {56007397-59B0-4DCB-80C4-3AD0BE3F319F}.Release|Any CPU.Build.0 = Release|Any CPU + {2F0DD7D7-867F-4478-9E22-45C114B61C46}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2F0DD7D7-867F-4478-9E22-45C114B61C46}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2F0DD7D7-867F-4478-9E22-45C114B61C46}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2F0DD7D7-867F-4478-9E22-45C114B61C46}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {698E3EEC-64F9-4F96-B700-D61D04FD0704} = {A43F1828-D9B6-40F7-82B6-CA0070843E2F} + {20F85652-2923-4211-9262-C64BA8C9ED89} = {F5E4436F-87F2-46AB-A9EB-59B4BF21BF7A} + {EE4D6CC1-78DE-4279-A567-C3D360C479F8} = {A43F1828-D9B6-40F7-82B6-CA0070843E2F} + {1B4A8710-4182-494D-B1C5-6B7CDB9C9DB9} = {07584D01-2D30-404B-B0D1-32080C0CC18A} + {C8038180-36F8-4077-922B-91F428EAC7D9} = {A43F1828-D9B6-40F7-82B6-CA0070843E2F} + {56007397-59B0-4DCB-80C4-3AD0BE3F319F} = {F5E4436F-87F2-46AB-A9EB-59B4BF21BF7A} + {2F0DD7D7-867F-4478-9E22-45C114B61C46} = {F5E4436F-87F2-46AB-A9EB-59B4BF21BF7A} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {D17BD356-592C-4628-9D81-A04E24FF02F3} + EndGlobalSection +EndGlobal diff --git a/samples/BasicSample/BasicSample.csproj b/samples/BasicSample/BasicSample.csproj new file mode 100644 index 0000000..f9a981f --- /dev/null +++ b/samples/BasicSample/BasicSample.csproj @@ -0,0 +1,19 @@ + + + + Exe + net5.0 + disable + true + + + + + + + + + + + + diff --git a/samples/BasicSample/Program.cs b/samples/BasicSample/Program.cs new file mode 100644 index 0000000..3aaf770 --- /dev/null +++ b/samples/BasicSample/Program.cs @@ -0,0 +1,72 @@ +using EntityFrameworkCore.Projections; +using EntityFrameworkCore.Projections.Extensions; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Linq; + +namespace BasicSample +{ + public partial class User + { + public int Id { get; set; } + + public string FirstName { get; set; } + + public string LastName { get; set; } + + [Projectable] + public string FullName + => FirstName + " " + LastName; + + [Projectable] + public string FullNameFunc() + => FirstName + " " + LastName; + } + + public class ApplicationDbContext : DbContext + { + public ApplicationDbContext(DbContextOptions options) : base(options) + { + } + + public DbSet Users { get; set; } + } + + + class Program + { + static void Main(string[] args) + { + using var dbConnection = new SqliteConnection("Filename=:memory:"); + dbConnection.Open(); + + using var serviceProvider = new ServiceCollection() + .AddDbContext(options => { + options + .UseSqlite(dbConnection) + .UseProjections(); + }) + .BuildServiceProvider(); + + var dbContext = serviceProvider.GetRequiredService(); + dbContext.Database.EnsureCreated(); + + var user = new User { FirstName = "Jon", LastName = "Doe" }; + + dbContext.Users.Add(user); + + dbContext.SaveChanges(); + + var query = dbContext.Users + .Select(x => new { + Foo = x.FullNameFunc() + }); + + Console.WriteLine(query.ToQueryString()); + + var r1 = query.ToArray(); + } + } +} diff --git a/samples/EntityFrameworkCore.Projections.FunctionalTests/ComplexModelTests.cs b/samples/EntityFrameworkCore.Projections.FunctionalTests/ComplexModelTests.cs new file mode 100644 index 0000000..9afa7ff --- /dev/null +++ b/samples/EntityFrameworkCore.Projections.FunctionalTests/ComplexModelTests.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using EntityFrameworkCore.Projections.FunctionalTests.Helpers; +using EntityFrameworkCore.Projections.Services; +using Microsoft.EntityFrameworkCore; +using ScenarioTests; +using Xunit; + +#nullable disable + +namespace EntityFrameworkCore.Projections.FunctionalTests +{ + + public partial class ComplexModelTests + { + public class User + { + public int Id { get; set; } + + public string DisplayName { get; set; } + + public ICollection Orders { get; set; } + + // todo: since Order is a nested class, we currently have to fully express the location of this class + [Projectable] + public EntityFrameworkCore.Projections.FunctionalTests.ComplexModelTests.Order LastOrder => + Orders.OrderByDescending(x => x.RecordDate).FirstOrDefault(); + + // todo: since Order is a nested class, we currently have to fully express the location of this class + [Projectable] + [NotMapped] + public IEnumerable Last2Orders => + Orders.OrderByDescending(x => x.RecordDate).Take(2); + + } + + public class Order + { + public int Id { get; set; } + + public DateTime RecordDate { get; set; } + } + + [Scenario(NamingPolicy = ScenarioTestMethodNamingPolicy.Test)] + public void PlayScenario(ScenarioContext scenario) + { + using var dbContext = new SampleDbContext(); + + scenario.Fact("We can project over a projectable navigation property", () => { + const string expectedQueryString = +@"SELECT ( + SELECT TOP(1) [o].[RecordDate] + FROM [Order] AS [o] + WHERE [u].[Id] = [o].[UserId] + ORDER BY [o].[RecordDate] DESC) +FROM [User] AS [u]"; + + var query = dbContext.Set() + .Select(x => x.LastOrder.RecordDate); + + Assert.Equal(expectedQueryString, query.ToQueryString()); + }); + + scenario.Fact("We can project over a projectable navigation collection property", () => { + const string expectedQueryString = +@"SELECT [t0].[RecordDate] +FROM [User] AS [u] +INNER JOIN ( + SELECT [t].[RecordDate], [t].[UserId] + FROM ( + SELECT [o].[RecordDate], [o].[UserId], ROW_NUMBER() OVER(PARTITION BY [o].[UserId] ORDER BY [o].[RecordDate] DESC) AS [row] + FROM [Order] AS [o] + ) AS [t] + WHERE [t].[row] <= 2 +) AS [t0] ON [u].[Id] = [t0].[UserId]"; + + var query = dbContext.Set() + .SelectMany(x => x.Last2Orders) + .Select(x => x.RecordDate); + + Assert.Equal(expectedQueryString, query.ToQueryString()); + }); + } + } +} diff --git a/samples/EntityFrameworkCore.Projections.FunctionalTests/EntityFrameworkCore.Projections.FunctionalTests.csproj b/samples/EntityFrameworkCore.Projections.FunctionalTests/EntityFrameworkCore.Projections.FunctionalTests.csproj new file mode 100644 index 0000000..7d6f036 --- /dev/null +++ b/samples/EntityFrameworkCore.Projections.FunctionalTests/EntityFrameworkCore.Projections.FunctionalTests.csproj @@ -0,0 +1,35 @@ + + + + net5.0 + false + true + $(BaseIntermediateOutputPath)Generated + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + diff --git a/samples/EntityFrameworkCore.Projections.FunctionalTests/Helpers/SampleDbContext.cs b/samples/EntityFrameworkCore.Projections.FunctionalTests/Helpers/SampleDbContext.cs new file mode 100644 index 0000000..7208755 --- /dev/null +++ b/samples/EntityFrameworkCore.Projections.FunctionalTests/Helpers/SampleDbContext.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using EntityFrameworkCore.Projections.Extensions; +using Microsoft.EntityFrameworkCore; + +namespace EntityFrameworkCore.Projections.FunctionalTests.Helpers +{ + public class SampleDbContext : DbContext + where TEntity : class + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseSqlServer("Server=(localdb)\v11.0;Integrated Security=true"); // Fake connection string as we're actually never connecting + optionsBuilder.UseProjections(); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(); + } + } +} diff --git a/samples/EntityFrameworkCore.Projections.FunctionalTests/StatefullPropertyTests.cs b/samples/EntityFrameworkCore.Projections.FunctionalTests/StatefullPropertyTests.cs new file mode 100644 index 0000000..ce3cad2 --- /dev/null +++ b/samples/EntityFrameworkCore.Projections.FunctionalTests/StatefullPropertyTests.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Data.SqlTypes; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using EntityFrameworkCore.Projections.FunctionalTests.Helpers; +using Microsoft.EntityFrameworkCore; +using ScenarioTests; +using Xunit; + +namespace EntityFrameworkCore.Projections.FunctionalTests +{ + public partial class StatefullPropertyTests + { + public record Entity + { + public int Id { get; set; } + + [Projectable] + public int Computed1 => Id; + + [Projectable] + public int Computed2 => Id * 2; + } + + [Scenario(NamingPolicy = ScenarioTestMethodNamingPolicy.Test)] + public void PlayScenario(ScenarioContext scenario) + { + // Setup + using var dbContext = new SampleDbContext(); + + scenario.Fact("We can filter on a projectable property", () => { + const string expectedQueryString = "SELECT [e].[Id]\r\nFROM [Entity] AS [e]\r\nWHERE [e].[Id] = 1"; + + var query = dbContext.Set() + .Where(x => x.Computed1 == 1); + + Assert.Equal(expectedQueryString, query.ToQueryString()); + }); + + scenario.Fact("We can select on a projectable property", () => { + const string expectedQueryString = "SELECT [e].[Id]\r\nFROM [Entity] AS [e]"; + + var query = dbContext.Set() + .Select(x => x.Computed1); + + Assert.Equal(expectedQueryString, query.ToQueryString()); + }); + + scenario.Fact("We can filter on a more complex projectable property", () => { + const string expectedQueryString = "SELECT [e].[Id]\r\nFROM [Entity] AS [e]\r\nWHERE ([e].[Id] * 2) = 2"; + + var query = dbContext.Set() + .Where(x => x.Computed2 == 2); + + Assert.Equal(expectedQueryString, query.ToQueryString()); + }); + + scenario.Fact("We can select a more complex projectable property", () => { + const string expectedQueryString = "SELECT [e].[Id] * 2\r\nFROM [Entity] AS [e]"; + + var query = dbContext.Set() + .Select(x => x.Computed2); + + Assert.Equal(expectedQueryString, query.ToQueryString()); + }); + + scenario.Fact("We can combine multiple projectable properties", () => { + const string expectedQueryString = "SELECT [e].[Id] + ([e].[Id] * 2)\r\nFROM [Entity] AS [e]"; + + var query = dbContext.Set() + .Select(x => x.Computed1 + x.Computed2); + + Assert.Equal(expectedQueryString, query.ToQueryString()); + }); + } + } +} diff --git a/samples/EntityFrameworkCore.Projections.FunctionalTests/StatefullSimpleFunctionTests.cs b/samples/EntityFrameworkCore.Projections.FunctionalTests/StatefullSimpleFunctionTests.cs new file mode 100644 index 0000000..cea1175 --- /dev/null +++ b/samples/EntityFrameworkCore.Projections.FunctionalTests/StatefullSimpleFunctionTests.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Data.SqlTypes; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using EntityFrameworkCore.Projections.FunctionalTests.Helpers; +using Microsoft.EntityFrameworkCore; +using ScenarioTests; +using Xunit; + +namespace EntityFrameworkCore.Projections.FunctionalTests +{ + public partial class StatefullSimpleFunctionTests + { + public record Entity + { + public int Id { get; set; } + + [Projectable] + public int Computed1() => Id; + + [Projectable] + public int Computed2() => Id * 2; + + public int Test(int i) => i; + } + + [Scenario(NamingPolicy = ScenarioTestMethodNamingPolicy.Test)] + public void PlayScenario(ScenarioContext scenario) + { + // Setup + using var dbContext = new SampleDbContext(); + + scenario.Fact("We can filter on a projectable property", () => { + const string expectedQueryString = "SELECT [e].[Id]\r\nFROM [Entity] AS [e]\r\nWHERE [e].[Id] = 1"; + + var query = dbContext.Set() + .Where(x => x.Computed1() == 1); + + Assert.Equal(expectedQueryString, query.ToQueryString()); + }); + + scenario.Fact("We can select on a projectable property", () => { + const string expectedQueryString = "SELECT [e].[Id]\r\nFROM [Entity] AS [e]"; + + var query = dbContext.Set() + .Select(x => x.Computed1()); + + Assert.Equal(expectedQueryString, query.ToQueryString()); + }); + + scenario.Fact("We can filter on a more complex projectable property", () => { + const string expectedQueryString = "SELECT [e].[Id]\r\nFROM [Entity] AS [e]\r\nWHERE ([e].[Id] * 2) = 2"; + + var query = dbContext.Set() + .Where(x => x.Computed2() == 2); + + Assert.Equal(expectedQueryString, query.ToQueryString()); + }); + + scenario.Fact("We can select a more complex projectable property", () => { + const string expectedQueryString = "SELECT [e].[Id] * 2\r\nFROM [Entity] AS [e]"; + + var query = dbContext.Set() + .Select(x => x.Computed2()); + + Assert.Equal(expectedQueryString, query.ToQueryString()); + }); + + scenario.Fact("We can combine multiple projectable properties", () => { + const string expectedQueryString = "SELECT [e].[Id] + ([e].[Id] * 2)\r\nFROM [Entity] AS [e]"; + + var query = dbContext.Set() + .Select(x => x.Computed1() + x.Computed2()); + + Assert.Equal(expectedQueryString, query.ToQueryString()); + }); + } + } +} diff --git a/samples/EntityFrameworkCore.Projections.FunctionalTests/StatelessComplexFunctionTests.cs b/samples/EntityFrameworkCore.Projections.FunctionalTests/StatelessComplexFunctionTests.cs new file mode 100644 index 0000000..15c84ca --- /dev/null +++ b/samples/EntityFrameworkCore.Projections.FunctionalTests/StatelessComplexFunctionTests.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Data.SqlTypes; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using EntityFrameworkCore.Projections.FunctionalTests.Helpers; +using Microsoft.EntityFrameworkCore; +using ScenarioTests; +using Xunit; + +namespace EntityFrameworkCore.Projections.FunctionalTests +{ + public partial class StatelessComplexFunctionTests + { + public record Entity + { + public int Id { get; set; } + + [Projectable] + public int Computed(int argument1) => argument1; + } + + [Scenario(NamingPolicy = ScenarioTestMethodNamingPolicy.Test)] + public void PlayScenario(ScenarioContext scenario) + { + // Setup + using var dbContext = new SampleDbContext(); + + scenario.Fact("We can filter on a projectable property", () => { + const string expectedQueryString = "SELECT [e].[Id]\r\nFROM [Entity] AS [e]\r\nWHERE 0 = 1"; + + var query = dbContext.Set() + .Where(x => x.Computed(0) == 1); + + Assert.Equal(expectedQueryString, query.ToQueryString()); + }); + + scenario.Fact("We can select on a projectable property", () => { + const string expectedQueryString = "SELECT 0\r\nFROM [Entity] AS [e]"; + + var query = dbContext.Set() + .Select(x => x.Computed(0)); + + Assert.Equal(expectedQueryString, query.ToQueryString()); + }); + + scenario.Fact("We can pass in variables", () => { + const string expectedQueryString = "SELECT 0\r\nFROM [Entity] AS [e]"; + + var argument = 0; + var query = dbContext.Set() + .Select(x => x.Computed(argument)); + + Assert.Equal(expectedQueryString, query.ToQueryString()); + }); + } + } +} diff --git a/samples/EntityFrameworkCore.Projections.FunctionalTests/StatelessPropertyTests.cs b/samples/EntityFrameworkCore.Projections.FunctionalTests/StatelessPropertyTests.cs new file mode 100644 index 0000000..dc2d59a --- /dev/null +++ b/samples/EntityFrameworkCore.Projections.FunctionalTests/StatelessPropertyTests.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Data.SqlTypes; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using EntityFrameworkCore.Projections.FunctionalTests.Helpers; +using Microsoft.EntityFrameworkCore; +using ScenarioTests; +using Xunit; + +namespace EntityFrameworkCore.Projections.FunctionalTests +{ + public partial class StatelessPropertyTests + { + public record Entity + { + public int Id { get; set; } + + [Projectable] + public int Computed => 0; + } + + [Scenario(NamingPolicy = ScenarioTestMethodNamingPolicy.Test)] + public void PlayScenario(ScenarioContext scenario) + { + // Setup + using var dbContext = new SampleDbContext(); + + scenario.Fact("We can filter on a projectable property", () => { + const string expectedQueryString = "SELECT [e].[Id]\r\nFROM [Entity] AS [e]\r\nWHERE 0 = 1"; + + var query = dbContext.Set() + .Where(x => x.Computed == 1); + + Assert.Equal(expectedQueryString, query.ToQueryString()); + }); + + scenario.Fact("We can select on a projectable property", () => { + const string expectedQueryString = "SELECT 0\r\nFROM [Entity] AS [e]"; + + var query = dbContext.Set() + .Select(x => x.Computed); + + Assert.Equal(expectedQueryString, query.ToQueryString()); + }); + } + } +} diff --git a/samples/EntityFrameworkCore.Projections.FunctionalTests/StatelessSimpleFunctionTests.cs b/samples/EntityFrameworkCore.Projections.FunctionalTests/StatelessSimpleFunctionTests.cs new file mode 100644 index 0000000..7f19500 --- /dev/null +++ b/samples/EntityFrameworkCore.Projections.FunctionalTests/StatelessSimpleFunctionTests.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Data.SqlTypes; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using EntityFrameworkCore.Projections.FunctionalTests.Helpers; +using Microsoft.EntityFrameworkCore; +using ScenarioTests; +using Xunit; + +namespace EntityFrameworkCore.Projections.FunctionalTests +{ + public partial class StatelessSimpleFunctionTests + { + public record Entity + { + public int Id { get; set; } + + [Projectable] + public int Computed() => 0; + } + + [Scenario(NamingPolicy = ScenarioTestMethodNamingPolicy.Test)] + public void PlayScenario(ScenarioContext scenario) + { + // Setup + using var dbContext = new SampleDbContext(); + + scenario.Fact("We can filter on a projectable property", () => { + const string expectedQueryString = "SELECT [e].[Id]\r\nFROM [Entity] AS [e]\r\nWHERE 0 = 1"; + + var query = dbContext.Set() + .Where(x => x.Computed() == 1); + + Assert.Equal(expectedQueryString, query.ToQueryString()); + }); + + scenario.Fact("We can select on a projectable property", () => { + const string expectedQueryString = "SELECT 0\r\nFROM [Entity] AS [e]"; + + var query = dbContext.Set() + .Select(x => x.Computed()); + + Assert.Equal(expectedQueryString, query.ToQueryString()); + }); + } + } +} diff --git a/src/EntityFrameworkCore.Projections.Abstractions/EntityFrameworkCore.Projections.Abstractions.csproj b/src/EntityFrameworkCore.Projections.Abstractions/EntityFrameworkCore.Projections.Abstractions.csproj new file mode 100644 index 0000000..e25c77a --- /dev/null +++ b/src/EntityFrameworkCore.Projections.Abstractions/EntityFrameworkCore.Projections.Abstractions.csproj @@ -0,0 +1,8 @@ + + + + netstandard2.0 + EntityFrameworkCore.Projections + + + diff --git a/src/EntityFrameworkCore.Projections.Abstractions/ProjectableAttribute.cs b/src/EntityFrameworkCore.Projections.Abstractions/ProjectableAttribute.cs new file mode 100644 index 0000000..4f30c4a --- /dev/null +++ b/src/EntityFrameworkCore.Projections.Abstractions/ProjectableAttribute.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EntityFrameworkCore.Projections +{ + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = true, AllowMultiple = false)] + public sealed class ProjectableAttribute : Attribute + { + } +} diff --git a/src/EntityFrameworkCore.Projections.Generator/EntityFrameworkCore.Projections.Generator.csproj b/src/EntityFrameworkCore.Projections.Generator/EntityFrameworkCore.Projections.Generator.csproj new file mode 100644 index 0000000..04062af --- /dev/null +++ b/src/EntityFrameworkCore.Projections.Generator/EntityFrameworkCore.Projections.Generator.csproj @@ -0,0 +1,16 @@ + + + + netstandard2.0 + $(NoWarn);nullable;NU5128 + + + + + + + + + + + diff --git a/src/EntityFrameworkCore.Projections.Generator/ExpressionSyntaxRewriter.cs b/src/EntityFrameworkCore.Projections.Generator/ExpressionSyntaxRewriter.cs new file mode 100644 index 0000000..4f78e5a --- /dev/null +++ b/src/EntityFrameworkCore.Projections.Generator/ExpressionSyntaxRewriter.cs @@ -0,0 +1,64 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EntityFrameworkCore.Projections.Generator +{ + public class ExpressionSyntaxRewriter : CSharpSyntaxRewriter + { + readonly INamedTypeSymbol _targetTypeSymbol; + readonly SemanticModel _semanticModel; + + public ExpressionSyntaxRewriter(INamedTypeSymbol targetTypeSymbol, SemanticModel semanticModel) + { + _targetTypeSymbol = targetTypeSymbol; + _semanticModel = semanticModel; + } + + public override SyntaxNode? VisitMemberAccessExpression(MemberAccessExpressionSyntax node) + { + var symbolInfo = _semanticModel.GetSymbolInfo(node); + + if (symbolInfo.Symbol is not null && SymbolEqualityComparer.Default.Equals(symbolInfo.Symbol.ContainingType, _targetTypeSymbol)) + { + var scopedNode = node.ChildNodes().FirstOrDefault(); + if (scopedNode is ThisExpressionSyntax) + { + var nextNode = node.ChildNodes().Skip(1).FirstOrDefault() as SimpleNameSyntax; + + if (nextNode is not null) + { + return SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName(ProjectionExpressionGenerator.ProjectionTargetParameterName), + nextNode + ); + } + } + } + + return base.VisitMemberAccessExpression(node); + } + + public override SyntaxNode? VisitIdentifierName(IdentifierNameSyntax node) + { + var symbolInfo = _semanticModel.GetSymbolInfo(node); + + if (symbolInfo.Symbol is not null && symbolInfo.Symbol.Kind is SymbolKind.Property or SymbolKind.Method or SymbolKind.Field && SymbolEqualityComparer.Default.Equals(symbolInfo.Symbol.ContainingType, _targetTypeSymbol)) + { + return SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName(ProjectionExpressionGenerator.ProjectionTargetParameterName), + node + ); + } + else + { + return base.VisitIdentifierName(node); + } + } + } +} diff --git a/src/EntityFrameworkCore.Projections.Generator/ProjectableDescriptor.cs b/src/EntityFrameworkCore.Projections.Generator/ProjectableDescriptor.cs new file mode 100644 index 0000000..1216aef --- /dev/null +++ b/src/EntityFrameworkCore.Projections.Generator/ProjectableDescriptor.cs @@ -0,0 +1,28 @@ +using Microsoft.CodeAnalysis; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EntityFrameworkCore.Projections.Generator +{ + public class ProjectableDescriptor + { + public IEnumerable UsingDirectives { get; set; } + + public string ClassNamespace { get; set; } + + public IEnumerable NestedInClassNames { get; set; } + + public string ClassName { get; set; } + + public string MemberName { get; set; } + + public string ReturnTypeName { get; set; } + + public string ParametersListString { get; set; } + + public SyntaxNode Body { get; set; } + } +} diff --git a/src/EntityFrameworkCore.Projections.Generator/ProjectableInterpreter.cs b/src/EntityFrameworkCore.Projections.Generator/ProjectableInterpreter.cs new file mode 100644 index 0000000..1f338e3 --- /dev/null +++ b/src/EntityFrameworkCore.Projections.Generator/ProjectableInterpreter.cs @@ -0,0 +1,86 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EntityFrameworkCore.Projections.Generator +{ + public static class ProjectableInterpreter + { + static IEnumerable GetNestedInClassPath(INamedTypeSymbol namedTypeSymbol) + { + if (namedTypeSymbol.ContainingType is not null) + { + foreach (var nestedInClassName in GetNestedInClassPath(namedTypeSymbol.ContainingType)) + { + yield return nestedInClassName; + } + } + + yield return namedTypeSymbol.Name; + } + + public static ProjectableDescriptor? GetDescriptor(MemberDeclarationSyntax memberDeclarationSyntax, GeneratorExecutionContext context) + { + var semanticModel = context.Compilation.GetSemanticModel(memberDeclarationSyntax.SyntaxTree); + var memberSymbol = semanticModel.GetDeclaredSymbol(memberDeclarationSyntax); + + if (memberSymbol is null) + { + return null; + } + + var projectableAttributeTypeSymbol = context.Compilation.GetTypeByMetadataName("EntityFrameworkCore.Projections.ProjectableAttribute"); + + var projectableAttributeClass = memberSymbol.GetAttributes() + .Where(x => x.AttributeClass.Name == "ProjectableAttribute") + .FirstOrDefault(); + + if (projectableAttributeClass is null || !SymbolEqualityComparer.Default.Equals(projectableAttributeClass.AttributeClass, projectableAttributeTypeSymbol)) + { + return null; + } + + var expressionSyntaxRewriter = new ExpressionSyntaxRewriter(memberSymbol.ContainingType, semanticModel); + + var descriptor = new ProjectableDescriptor + { + ClassName = memberSymbol.ContainingType.Name, + ClassNamespace = memberSymbol.ContainingType.ContainingNamespace.IsGlobalNamespace ? null : memberSymbol.ContainingType.ContainingNamespace.ToDisplayString(), + MemberName = memberSymbol.Name, + NestedInClassNames = GetNestedInClassPath(memberSymbol.ContainingType) + }; + + if (memberDeclarationSyntax is MethodDeclarationSyntax methodDeclarationSyntax) + { + descriptor.ReturnTypeName = methodDeclarationSyntax.ReturnType.ToString(); + descriptor.Body = expressionSyntaxRewriter.Visit(methodDeclarationSyntax.ExpressionBody.Expression); + descriptor.ParametersListString = methodDeclarationSyntax.ParameterList.ToString(); + } + else if (memberDeclarationSyntax is PropertyDeclarationSyntax propertyDeclarationSyntax) + { + descriptor.ReturnTypeName = propertyDeclarationSyntax.Type.ToString(); + descriptor.Body = expressionSyntaxRewriter.Visit(propertyDeclarationSyntax.ExpressionBody.Expression); + descriptor.ParametersListString = "()"; + } + else + { + return null; + } + + descriptor.UsingDirectives = + memberDeclarationSyntax.SyntaxTree + .GetRoot() + .DescendantNodes() + .OfType() + .Select(x => x.ToString()); + + + return descriptor; + } + } +} diff --git a/src/EntityFrameworkCore.Projections.Generator/ProjectionExpressionGenerator.cs b/src/EntityFrameworkCore.Projections.Generator/ProjectionExpressionGenerator.cs new file mode 100644 index 0000000..b47dd22 --- /dev/null +++ b/src/EntityFrameworkCore.Projections.Generator/ProjectionExpressionGenerator.cs @@ -0,0 +1,62 @@ +using EntityFrameworkCore.Projections.Services; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EntityFrameworkCore.Projections.Generator +{ + [Generator] + public class ProjectionExpressionGenerator : ISourceGenerator + { + public const string ProjectionTargetParameterName = "projectionTarget"; + + public void Execute(GeneratorExecutionContext context) + { + if (context.SyntaxReceiver is not SyntaxReceiver receiver) + { + return; + } + + if (receiver.Candidates.Count > 0) + { + var projectables = receiver.Candidates + .Select(x => ProjectableInterpreter.GetDescriptor(x, context)) + .Where(x => x is not null); + + var resultBuilder = new StringBuilder(); + + foreach (var projectable in projectables) + { + resultBuilder.Clear(); + + foreach (var usingDirective in projectable.UsingDirectives) + { + resultBuilder.AppendLine(usingDirective); + } + + var generatedClassName = ProjectionExpressionClassNameGenerator.GenerateName(projectable.ClassNamespace, projectable.NestedInClassNames, projectable.MemberName); + + resultBuilder.Append($@" +namespace EntityFrameworkCore.Projections.Generated +#nullable disable +{{ + public static class {generatedClassName} + {{ + public static System.Linq.Expressions.Expression> Expression{projectable.ParametersListString} => + {ProjectionTargetParameterName} => {projectable.Body}; + }} +}}"); + + context.AddSource($"{generatedClassName}_Generated", SourceText.From(resultBuilder.ToString(), Encoding.UTF8)); + } + } + } + + public void Initialize(GeneratorInitializationContext context) => + context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); + } +} diff --git a/src/EntityFrameworkCore.Projections.Generator/SyntaxReceiver.cs b/src/EntityFrameworkCore.Projections.Generator/SyntaxReceiver.cs new file mode 100644 index 0000000..b2fed90 --- /dev/null +++ b/src/EntityFrameworkCore.Projections.Generator/SyntaxReceiver.cs @@ -0,0 +1,30 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EntityFrameworkCore.Projections.Generator +{ + public class SyntaxReceiver : ISyntaxReceiver + { + public List Candidates { get; } = new List(); + + public void OnVisitSyntaxNode(SyntaxNode syntaxNode) + { + if (syntaxNode is MemberDeclarationSyntax memberDeclarationSyntax && memberDeclarationSyntax.AttributeLists.Count > 0) + { + var hasProjectableAttribute = memberDeclarationSyntax.AttributeLists + .SelectMany(x => x.Attributes) + .Any(x => x.Name.ToString().Contains("Projectable")); + + if (hasProjectableAttribute) + { + Candidates.Add(memberDeclarationSyntax); + } + } + } + } +} diff --git a/src/EntityFrameworkCore.Projections/EntityFrameworkCore.Projections.csproj b/src/EntityFrameworkCore.Projections/EntityFrameworkCore.Projections.csproj new file mode 100644 index 0000000..068f5b3 --- /dev/null +++ b/src/EntityFrameworkCore.Projections/EntityFrameworkCore.Projections.csproj @@ -0,0 +1,15 @@ + + + + net5.0 + + + + + + + + + + + diff --git a/src/EntityFrameworkCore.Projections/Extensions/DbContextOptionsExtensions.cs b/src/EntityFrameworkCore.Projections/Extensions/DbContextOptionsExtensions.cs new file mode 100644 index 0000000..692dc0a --- /dev/null +++ b/src/EntityFrameworkCore.Projections/Extensions/DbContextOptionsExtensions.cs @@ -0,0 +1,27 @@ +using EntityFrameworkCore.Projections.Infrastructure.Internal; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; + +namespace EntityFrameworkCore.Projections.Extensions +{ + public static class DbContextOptionsExtensions + { + public static DbContextOptionsBuilder UseProjections(this DbContextOptionsBuilder optionsBuilder) + where TContext : DbContext + => (DbContextOptionsBuilder)UseProjections((DbContextOptionsBuilder)optionsBuilder); + + public static DbContextOptionsBuilder UseProjections(this DbContextOptionsBuilder optionsBuilder) + { + var extension = optionsBuilder.Options.FindExtension() ?? new ProjectionOptionsExtension(); + ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension); + + return optionsBuilder; + } + } +} diff --git a/src/EntityFrameworkCore.Projections/Extensions/TypeExtensions.cs b/src/EntityFrameworkCore.Projections/Extensions/TypeExtensions.cs new file mode 100644 index 0000000..823785c --- /dev/null +++ b/src/EntityFrameworkCore.Projections/Extensions/TypeExtensions.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics.Arm; +using System.Text; +using System.Threading.Tasks; + +namespace EntityFrameworkCore.Projections.Extensions +{ + public static class TypeExtensions + { + public static IEnumerable GetNestedTypePath(this Type type) + { + if (type.IsNested && type.DeclaringType is not null) + { + foreach (var containingType in type.DeclaringType.GetNestedTypePath()) + { + yield return containingType; + } + } + + yield return type; + } + } +} diff --git a/src/EntityFrameworkCore.Projections/Infrastructure/Internal/ProjectionOptionsExtension.cs b/src/EntityFrameworkCore.Projections/Infrastructure/Internal/ProjectionOptionsExtension.cs new file mode 100644 index 0000000..4d06b9f --- /dev/null +++ b/src/EntityFrameworkCore.Projections/Infrastructure/Internal/ProjectionOptionsExtension.cs @@ -0,0 +1,65 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EntityFrameworkCore.Projections.Infrastructure.Internal +{ + public class ProjectionOptionsExtension : IDbContextOptionsExtension + { + public ProjectionOptionsExtension() + { + Info = new ExtensionInfo(this); + } + + public DbContextOptionsExtensionInfo Info { get; } + + public void ApplyServices(IServiceCollection services) + { + var existingPreprocessorFactoryRegistration = services.FirstOrDefault(x => x.ServiceType == typeof(IQueryTranslationPreprocessorFactory)); + + if (existingPreprocessorFactoryRegistration?.ImplementationType is null) + { + throw new InvalidOperationException("Expected a QueryTranslationPreprocessor to be registered. Please make sure to register your database provider first"); + } + + // Ensure that we can still resolve this factory + services.Add(new ServiceDescriptor(existingPreprocessorFactoryRegistration.ImplementationType, existingPreprocessorFactoryRegistration.ImplementationType, existingPreprocessorFactoryRegistration.Lifetime)); + services.Remove(existingPreprocessorFactoryRegistration); + + services.Add(new ServiceDescriptor( + typeof(IQueryTranslationPreprocessorFactory), + serviceProvider => new WrappedQueryTranslationPreprocessorFactory((IQueryTranslationPreprocessorFactory)serviceProvider.GetRequiredService(existingPreprocessorFactoryRegistration.ImplementationType), serviceProvider.GetRequiredService()), + existingPreprocessorFactoryRegistration.Lifetime + )); + + } + + public void Validate(IDbContextOptions options) + { + } + + sealed class ExtensionInfo : DbContextOptionsExtensionInfo + { + public ExtensionInfo(IDbContextOptionsExtension extension) : base(extension) + { + } + + public override bool IsDatabaseProvider => false; + public override string LogFragment => string.Empty; + public override long GetServiceProviderHashCode() => 0; + + public override void PopulateDebugInfo(IDictionary debugInfo) + { + if (debugInfo == null) + { + throw new ArgumentNullException(nameof(debugInfo)); + } + } + } + } +} diff --git a/src/EntityFrameworkCore.Projections/Infrastructure/Internal/WrappedQueryTranslationPreprocessorFactory.cs b/src/EntityFrameworkCore.Projections/Infrastructure/Internal/WrappedQueryTranslationPreprocessorFactory.cs new file mode 100644 index 0000000..6190ef0 --- /dev/null +++ b/src/EntityFrameworkCore.Projections/Infrastructure/Internal/WrappedQueryTranslationPreprocessorFactory.cs @@ -0,0 +1,54 @@ +using EntityFrameworkCore.Projections.Services; +using Microsoft.EntityFrameworkCore.Query; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; + +namespace EntityFrameworkCore.Projections.Infrastructure.Internal +{ + public class WrappedQueryTranslationPreprocessorFactory : IQueryTranslationPreprocessorFactory + { + readonly IQueryTranslationPreprocessorFactory _originalFactory; + readonly QueryTranslationPreprocessorDependencies _dependencies; + + public WrappedQueryTranslationPreprocessorFactory(IQueryTranslationPreprocessorFactory originalFactory, QueryTranslationPreprocessorDependencies dependencies) + { + _originalFactory = originalFactory; + _dependencies = dependencies; + } + + public QueryTranslationPreprocessor Create(QueryCompilationContext queryCompilationContext) + { + var originalPreprocessor = _originalFactory.Create(queryCompilationContext); + + return new WrappedQueryTranslationPreprocessor(originalPreprocessor, _dependencies, queryCompilationContext); + } + } + + public class WrappedQueryTranslationPreprocessor : QueryTranslationPreprocessor + { + readonly ProjectableExpressionReplacer _projectableExpressionReplacer; + readonly QueryTranslationPreprocessor _originalPreprocessor; + + public WrappedQueryTranslationPreprocessor(QueryTranslationPreprocessor originalPreprocessor, QueryTranslationPreprocessorDependencies dependencies, QueryCompilationContext queryCompilationContext) : base(dependencies, queryCompilationContext) + { + _originalPreprocessor = originalPreprocessor; + _projectableExpressionReplacer = new ProjectableExpressionReplacer(); + } + + public override Expression NormalizeQueryableMethod(Expression expression) + { + return _originalPreprocessor.NormalizeQueryableMethod(expression); + } + + public override Expression Process(Expression query) + { + query = _projectableExpressionReplacer.Visit(query); + + return _originalPreprocessor.Process(query); + } + } +} diff --git a/src/EntityFrameworkCore.Projections/Services/ExpressionArgumentReplacer.cs b/src/EntityFrameworkCore.Projections/Services/ExpressionArgumentReplacer.cs new file mode 100644 index 0000000..62b4549 --- /dev/null +++ b/src/EntityFrameworkCore.Projections/Services/ExpressionArgumentReplacer.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; + +namespace EntityFrameworkCore.Projections.Services +{ + public class ExpressionArgumentReplacer : ExpressionVisitor + { + readonly Expression _targetExpression; + + public ExpressionArgumentReplacer(Expression targetExpression) + { + _targetExpression = targetExpression; + } + + protected override Expression VisitParameter(ParameterExpression node) + { + if (node.Name == "projectionTarget") + { + return _targetExpression; + } + else + { + return base.VisitParameter(node); + } + } + } +} diff --git a/src/EntityFrameworkCore.Projections/Services/ProjectableExpressionReplacer.cs b/src/EntityFrameworkCore.Projections/Services/ProjectableExpressionReplacer.cs new file mode 100644 index 0000000..a08c6f0 --- /dev/null +++ b/src/EntityFrameworkCore.Projections/Services/ProjectableExpressionReplacer.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; + +namespace EntityFrameworkCore.Projections.Services +{ + public class ProjectableExpressionReplacer : ExpressionVisitor + { + readonly ProjectionExpressionResolver _resolver = new(); + + protected override Expression VisitMethodCall(MethodCallExpression node) + { + if (node.Method.GetCustomAttributes(true).OfType().Any()) + { + var reflectedExpressionFactory = _resolver.FindGeneratedExpressionFactory(node.Method); + var reflectedExpresssion = reflectedExpressionFactory(node.Arguments); + if (reflectedExpresssion is not null) + { + if (node.Object is not null) + { + var expressionArgumentReplacer = new ExpressionArgumentReplacer(node.Object); + return expressionArgumentReplacer.Visit(reflectedExpresssion.Body); + } + else + { + return reflectedExpresssion.Body; + } + } + } + + return base.VisitMethodCall(node); + } + + protected override Expression VisitMember(MemberExpression node) + { + if (node.Member.GetCustomAttributes(true).OfType().Any()) + { + var reflectedExpressionFactory = _resolver.FindGeneratedExpressionFactory(node.Member); + var reflectedExpression = reflectedExpressionFactory(null); + if (reflectedExpression is not null) + { + if (node.Expression is not null) + { + var expressionArgumentReplacer = new ExpressionArgumentReplacer(node.Expression); + return expressionArgumentReplacer.Visit(reflectedExpression.Body); + } + else + { + return reflectedExpression.Body; + } + } + } + + return base.VisitMember(node); + } + } +} \ No newline at end of file diff --git a/src/EntityFrameworkCore.Projections/Services/ProjectionExpressionClassNameGenerator.cs b/src/EntityFrameworkCore.Projections/Services/ProjectionExpressionClassNameGenerator.cs new file mode 100644 index 0000000..b2968f8 --- /dev/null +++ b/src/EntityFrameworkCore.Projections/Services/ProjectionExpressionClassNameGenerator.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EntityFrameworkCore.Projections.Services +{ + public static class ProjectionExpressionClassNameGenerator + { + public const string Namespace = "EntityFrameworkCore.Projections.Generated"; + + public static string GenerateName(string? namespaceName, IEnumerable nestedInClassNames, string memberName) + { + var stringBuilder = new StringBuilder(); + + return GenerateNameImpl(stringBuilder, namespaceName, nestedInClassNames, memberName); + } + + public static string GenerateFullName(string? namespaceName, IEnumerable nestedInClassNames, string memberName) + { + var stringBuilder = new StringBuilder(Namespace); + stringBuilder.Append('.'); + + return GenerateNameImpl(stringBuilder, namespaceName, nestedInClassNames, memberName); + } + + static string GenerateNameImpl(StringBuilder stringBuilder, string? namespaceName, IEnumerable nestedInClassNames, string memberName) + { + stringBuilder.Append(namespaceName?.Replace('.', '_')); + stringBuilder.Append('_'); + foreach (var className in nestedInClassNames) + { + stringBuilder.Append(className); + stringBuilder.Append('_'); + } + stringBuilder.Append(memberName); + + return stringBuilder.ToString(); + } + } +} diff --git a/src/EntityFrameworkCore.Projections/Services/ProjectionExpressionResolver.cs b/src/EntityFrameworkCore.Projections/Services/ProjectionExpressionResolver.cs new file mode 100644 index 0000000..3056697 --- /dev/null +++ b/src/EntityFrameworkCore.Projections/Services/ProjectionExpressionResolver.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using EntityFrameworkCore.Projections.Extensions; + +namespace EntityFrameworkCore.Projections.Services +{ + public sealed class ProjectionExpressionResolver + { + readonly ConcurrentDictionary?, LambdaExpression>> _lookupCache = new(); + + public Func?, LambdaExpression> FindGeneratedExpressionFactory(MemberInfo projectableMemberInfo) + { + var reflectedType = projectableMemberInfo.ReflectedType ?? throw new InvalidOperationException("Expected a valid type here"); + var generatedContainingTypeName = ProjectionExpressionClassNameGenerator.GenerateFullName(reflectedType.Namespace, reflectedType.GetNestedTypePath().Select(x => x.Name), projectableMemberInfo.Name); + + return _lookupCache.GetOrAdd(generatedContainingTypeName, _ => { + var expressionFactoryMethod = reflectedType.Assembly + .GetTypes() + .Where(x => x.FullName == generatedContainingTypeName) + .SelectMany(x => x.GetMethods()) + .FirstOrDefault(); + + if (expressionFactoryMethod is null) + { + throw new InvalidOperationException("Unable to resolve generated expression") { + Data = { + ["GeneratedContainingTypeName"] = generatedContainingTypeName + } + }; + } + + return new Func?, LambdaExpression>(argumentExpressions => + { + if (argumentExpressions is null || argumentExpressions.Count is 0) + { + return expressionFactoryMethod.Invoke(null, null) as LambdaExpression ?? throw new InvalidOperationException("Expected lambda"); + } + else + { + var test1 = argumentExpressions.Cast()!; + + var expressionFactoryConstructionMethod = + Expression.Lambda>( + Expression.Call( + expressionFactoryMethod, + argumentExpressions + ) + ).Compile(); + + return expressionFactoryConstructionMethod.Invoke(); + } + }); + }); + } + } +} diff --git a/tests/EntityFrameworkCore.Projections.Generator.Tests/EntityFrameworkCore.Projections.Generator.Tests.csproj b/tests/EntityFrameworkCore.Projections.Generator.Tests/EntityFrameworkCore.Projections.Generator.Tests.csproj new file mode 100644 index 0000000..5f3cbbf --- /dev/null +++ b/tests/EntityFrameworkCore.Projections.Generator.Tests/EntityFrameworkCore.Projections.Generator.Tests.csproj @@ -0,0 +1,36 @@ + + + + net5.0 + + false + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + diff --git a/tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.ArgumentlessProjectableComputedMethod.verified.txt b/tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.ArgumentlessProjectableComputedMethod.verified.txt new file mode 100644 index 0000000..96f5700 --- /dev/null +++ b/tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.ArgumentlessProjectableComputedMethod.verified.txt @@ -0,0 +1,11 @@ +using System; +using EntityFrameworkCore.Projections; + +namespace EntityFrameworkCore.Projections.Generated +{ + public static class Foo_C_Foo + { + public static System.Linq.Expressions.Expression> Expression() => + projectionTarget => 0; + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.MoreComplexProjectableComputedProperty.verified.txt b/tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.MoreComplexProjectableComputedProperty.verified.txt new file mode 100644 index 0000000..af0f539 --- /dev/null +++ b/tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.MoreComplexProjectableComputedProperty.verified.txt @@ -0,0 +1,11 @@ +using System; +using EntityFrameworkCore.Projections; + +namespace EntityFrameworkCore.Projections.Generated +{ + public static class Foo_C_Foo + { + public static System.Linq.Expressions.Expression> Expression() => + projectionTarget => projectionTarget.Bar + projectionTarget.Bar + projectionTarget.Bar; + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableComputedMethodWithMultipleArguments.verified.txt b/tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableComputedMethodWithMultipleArguments.verified.txt new file mode 100644 index 0000000..10562c0 --- /dev/null +++ b/tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableComputedMethodWithMultipleArguments.verified.txt @@ -0,0 +1,11 @@ +using System; +using EntityFrameworkCore.Projections; + +namespace EntityFrameworkCore.Projections.Generated +{ + public static class Foo_C_Foo + { + public static System.Linq.Expressions.Expression> Expression(int a, string b, object d) => + projectionTarget => a; + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableComputedMethodWithSingleArgument.verified.txt b/tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableComputedMethodWithSingleArgument.verified.txt new file mode 100644 index 0000000..a66122a --- /dev/null +++ b/tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableComputedMethodWithSingleArgument.verified.txt @@ -0,0 +1,11 @@ +using System; +using EntityFrameworkCore.Projections; + +namespace EntityFrameworkCore.Projections.Generated +{ + public static class Foo_C_Foo + { + public static System.Linq.Expressions.Expression> Expression(int i) => + projectionTarget => i; + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableComputedPropertyMethod.verified.txt b/tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableComputedPropertyMethod.verified.txt new file mode 100644 index 0000000..a6849fc --- /dev/null +++ b/tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableComputedPropertyMethod.verified.txt @@ -0,0 +1,11 @@ +using System; +using EntityFrameworkCore.Projections; + +namespace EntityFrameworkCore.Projections.Generated +{ + public static class Foo_C_Foo + { + public static System.Linq.Expressions.Expression> Expression() => + projectionTarget => projectionTarget.Bar(); + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableComputedPropertyUsingThis.verified.txt b/tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableComputedPropertyUsingThis.verified.txt new file mode 100644 index 0000000..90dbb6d --- /dev/null +++ b/tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableComputedPropertyUsingThis.verified.txt @@ -0,0 +1,11 @@ +using System; +using EntityFrameworkCore.Projections; + +namespace EntityFrameworkCore.Projections.Generated +{ + public static class Foo_C_Foo + { + public static System.Linq.Expressions.Expression> Expression() => + projectionTarget => projectionTarget.Bar; + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyToNavigationalProperty.verified.txt b/tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyToNavigationalProperty.verified.txt new file mode 100644 index 0000000..2620672 --- /dev/null +++ b/tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyToNavigationalProperty.verified.txt @@ -0,0 +1,12 @@ +using System; +using System.Linq; +using EntityFrameworkCore.Projections; + +namespace EntityFrameworkCore.Projections.Generated +{ + public static class Foo_C_Foo + { + public static System.Linq.Expressions.Expression> Expression() => + projectionTarget => projectionTarget.Dees.First(); + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.SimpleProjectableComputedInNestedClassProperty.verified.txt b/tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.SimpleProjectableComputedInNestedClassProperty.verified.txt new file mode 100644 index 0000000..c30eec3 --- /dev/null +++ b/tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.SimpleProjectableComputedInNestedClassProperty.verified.txt @@ -0,0 +1,11 @@ +using System; +using EntityFrameworkCore.Projections; + +namespace EntityFrameworkCore.Projections.Generated +{ + public static class Foo_C_D_Foo + { + public static System.Linq.Expressions.Expression> Expression() => + projectionTarget => projectionTarget.Bar + 1; + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.SimpleProjectableComputedProperty.verified.txt b/tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.SimpleProjectableComputedProperty.verified.txt new file mode 100644 index 0000000..b0e9668 --- /dev/null +++ b/tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.SimpleProjectableComputedProperty.verified.txt @@ -0,0 +1,11 @@ +using System; +using EntityFrameworkCore.Projections; + +namespace EntityFrameworkCore.Projections.Generated +{ + public static class Foo_C_Foo + { + public static System.Linq.Expressions.Expression> Expression() => + projectionTarget => projectionTarget.Bar + 1; + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.SimpleProjectableMethod.verified.txt b/tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.SimpleProjectableMethod.verified.txt new file mode 100644 index 0000000..4dd8c80 --- /dev/null +++ b/tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.SimpleProjectableMethod.verified.txt @@ -0,0 +1,11 @@ +using System; +using EntityFrameworkCore.Projections; + +namespace EntityFrameworkCore.Projections.Generated +{ + public static class Foo_C_Foo + { + public static System.Linq.Expressions.Expression> Expression() => + projectionTarget => 1; + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.SimpleProjectableProperty.verified.txt b/tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.SimpleProjectableProperty.verified.txt new file mode 100644 index 0000000..4dd8c80 --- /dev/null +++ b/tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.SimpleProjectableProperty.verified.txt @@ -0,0 +1,11 @@ +using System; +using EntityFrameworkCore.Projections; + +namespace EntityFrameworkCore.Projections.Generated +{ + public static class Foo_C_Foo + { + public static System.Linq.Expressions.Expression> Expression() => + projectionTarget => 1; + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.cs b/tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.cs new file mode 100644 index 0000000..a483340 --- /dev/null +++ b/tests/EntityFrameworkCore.Projections.Generator.Tests/ProjectionExpressionGeneratorTests.cs @@ -0,0 +1,372 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using VerifyXunit; +using Xunit; +using Xunit.Abstractions; + +namespace EntityFrameworkCore.Projections.Generator.Tests +{ + [UsesVerify] + public class ProjectionExpressionGeneratorTests + { + readonly ITestOutputHelper _testOutputHelper; + + public ProjectionExpressionGeneratorTests(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + } + + [Fact] + public void EmtpyCode_Noop() + { + var compilation = CreateCompilation(@" +class C { } +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Empty(result.GeneratedTrees); + } + + [Fact] + public Task SimpleProjectableMethod() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projections; +namespace Foo { + class C { + [Projectable] + public int Foo() => 1; + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task SimpleProjectableProperty() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projections; +namespace Foo { + class C { + [Projectable] + public int Foo => 1; + } +} +"); + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task SimpleProjectableComputedProperty() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projections; +namespace Foo { + class C { + public int Bar { get; set; } + + [Projectable] + public int Foo => Bar + 1; + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task SimpleProjectableComputedInNestedClassProperty() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projections; +namespace Foo { + class C { + class D { + public int Bar { get; set; } + + [Projectable] + public int Foo => Bar + 1; + } + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task ProjectableComputedPropertyUsingThis() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projections; +namespace Foo { + class C { + public int Bar { get; set; } + + [Projectable] + public int Foo => this.Bar; + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task ProjectableComputedPropertyMethod() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projections; +namespace Foo { + class C { + public int Bar() => 1; + + [Projectable] + public int Foo => Bar(); + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + + [Fact] + public Task MoreComplexProjectableComputedProperty() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projections; +namespace Foo { + class C { + public int Bar { get; set; } + + [Projectable] + public int Foo => Bar + this.Bar + Bar; + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task ArgumentlessProjectableComputedMethod() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projections; +namespace Foo { + class C { + [Projectable] + public int Foo() => 0; + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task ProjectableComputedMethodWithSingleArgument() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projections; +namespace Foo { + class C { + [Projectable] + public int Foo(int i) => i; + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task ProjectableComputedMethodWithMultipleArguments() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projections; +namespace Foo { + class C { + [Projectable] + public int Foo(int a, string b, object d) => a; + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task ProjectablePropertyToNavigationalProperty() + { + var compilation = CreateCompilation(@" +using System; +using System.Linq; +using EntityFrameworkCore.Projections; +namespace Foo { + class D { } + + class C { + public System.Collections.Generic.List Dees { get; set; } + + [Projectable] + public D Foo => Dees.First(); + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + + #region Helpers + + Compilation CreateCompilation(string source, bool expectedToCompile = true) + { + var references = Basic.Reference.Assemblies.NetStandard20.All.ToList(); + references.Add(MetadataReference.CreateFromFile(typeof(ProjectableAttribute).Assembly.Location)); + + var assemblyPath = Path.GetDirectoryName(typeof(object).Assembly.Location); + + var compilation = CSharpCompilation.Create("compilation", + new[] { CSharpSyntaxTree.ParseText(source) }, + references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + +#if DEBUG + + if (expectedToCompile) + { + var compilationDiagnostics = compilation.GetDiagnostics(); + + if (!compilationDiagnostics.IsEmpty) + { + _testOutputHelper.WriteLine($"Original compilation diagnostics produced:"); + + foreach (var diagnostic in compilationDiagnostics) + { + _testOutputHelper.WriteLine($" > " + diagnostic.ToString()); + } + + if (compilationDiagnostics.Any(x => x.Severity == DiagnosticSeverity.Error)) + { + Debug.Fail("Compilation diagnostics produced"); + } + } + } +#endif + + return compilation; + } + + private GeneratorDriverRunResult RunGenerator(Compilation compilation) + { + _testOutputHelper.WriteLine("Running generator and updating compilation..."); + + var subject = new ProjectionExpressionGenerator(); + var driver = CSharpGeneratorDriver + .Create(subject) + .RunGenerators(compilation); + + var result = driver.GetRunResult(); + + if (result.Diagnostics.IsEmpty) + { + _testOutputHelper.WriteLine("Run did not produce diagnostics"); + } + else + { + _testOutputHelper.WriteLine($"Diagnostics produced:"); + + foreach (var diagnostic in result.Diagnostics) + { + _testOutputHelper.WriteLine($" > " + diagnostic.ToString()); + } + } + + foreach (var newSyntaxTree in result.GeneratedTrees) + { + _testOutputHelper.WriteLine($"Produced syntax tree with path produced: {newSyntaxTree.FilePath}"); + _testOutputHelper.WriteLine(newSyntaxTree.GetText().ToString()); + } + + return driver.GetRunResult(); + } + + #endregion + } +} diff --git a/tests/EntityFrameworkCore.Projections.Tests/EntityFrameworkCore.Projections.Tests.csproj b/tests/EntityFrameworkCore.Projections.Tests/EntityFrameworkCore.Projections.Tests.csproj new file mode 100644 index 0000000..685b4c4 --- /dev/null +++ b/tests/EntityFrameworkCore.Projections.Tests/EntityFrameworkCore.Projections.Tests.csproj @@ -0,0 +1,26 @@ + + + + net5.0 + + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/tests/EntityFrameworkCore.Projections.Tests/Extensions/TypeExtensionTests.cs b/tests/EntityFrameworkCore.Projections.Tests/Extensions/TypeExtensionTests.cs new file mode 100644 index 0000000..08317d8 --- /dev/null +++ b/tests/EntityFrameworkCore.Projections.Tests/Extensions/TypeExtensionTests.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using EntityFrameworkCore.Projections.Extensions; +using Xunit; + +namespace EntityFrameworkCore.Projections.Tests.Extensions +{ + public class TypeExtensionTests + { + class InnerType + { + public class SubsequentlyInnerType + { + + } + } + + [Fact] + public void GetNestedTypePath_OuterType_Returns1Entry() + { + var subject = typeof(TypeExtensionTests); + + var result = subject.GetNestedTypePath(); + + Assert.Single(result); + } + + + [Fact] + public void GetNestedTypePath_InnerType_Returns2Entries() + { + var subject = typeof(InnerType); + + var result = subject.GetNestedTypePath(); + + Assert.Equal(2, result.Count()); + } + + [Fact] + public void GetNestedTypePath_SubsequentlyInnerType_Returns3Entries() + { + var subject = typeof(InnerType.SubsequentlyInnerType); + + var result = subject.GetNestedTypePath(); + + Assert.Equal(3, result.Count()); + } + + [Fact] + public void GetNestedTypePath_SubsequentlyInnerType_ReturnsTypesInOrder() + { + var subject = typeof(InnerType.SubsequentlyInnerType); + + var result = subject.GetNestedTypePath(); + + Assert.Equal(typeof(TypeExtensionTests), result.First()); + Assert.Equal(typeof(InnerType.SubsequentlyInnerType), result.Last()); + } + } +} diff --git a/tests/EntityFrameworkCore.Projections.Tests/Services/ProjectionExpressionClassNameGeneratorTests.cs b/tests/EntityFrameworkCore.Projections.Tests/Services/ProjectionExpressionClassNameGeneratorTests.cs new file mode 100644 index 0000000..ba8adab --- /dev/null +++ b/tests/EntityFrameworkCore.Projections.Tests/Services/ProjectionExpressionClassNameGeneratorTests.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using EntityFrameworkCore.Projections.Services; +using Xunit; + +namespace EntityFrameworkCore.Projections.Tests.Services +{ + public class ProjectionExpressionClassNameGeneratorTests + { + [Theory] + [InlineData("ns", new string[] { "a" }, "m", "ns_a_m")] + [InlineData("ns", new string[] { "a", "b" }, "m", "ns_a_b_m")] + [InlineData(null, new string[] { "a" }, "m", "_a_m")] + public void GenerateName(string? namespaceName, string[] nestedTypeNames, string memberName, string expected) + { + var result = ProjectionExpressionClassNameGenerator.GenerateName(namespaceName, nestedTypeNames, memberName); + + Assert.Equal(expected, result); + } + + [Fact] + public void GeneratedFullName() + { + var expected = $"{ProjectionExpressionClassNameGenerator.Namespace}.{ProjectionExpressionClassNameGenerator.GenerateName("a", new[] { "b" }, "m")}"; + var result = ProjectionExpressionClassNameGenerator.GenerateFullName("a", new [] { "b" }, "m"); + + Assert.Equal(expected, result); + } + } +}