From 90d7bdcd21ddb46103b7b9d42671dff24c975ffa Mon Sep 17 00:00:00 2001 From: Alan Brault Date: Mon, 21 Jul 2025 13:43:39 -0400 Subject: [PATCH] chore: initial commit Signed-off-by: Alan Brault --- .editorconfig | 218 ++++++++++ .gitignore | 406 ++++++++++++++++++ Directory.Build.props | 49 +++ Directory.Build.targets | 47 ++ Directory.Packages.props | 19 + MapperSourceGenSample.sln | 57 +++ MapperSourceGenSample.sln.DotSettings | 316 ++++++++++++++ README.md | 119 +++++ global.json | 8 + .../Domain/Customers/Customer.cs | 67 +++ .../Domain/Orders/Order.cs | 36 ++ .../MapperSourceGen.Sample.csproj | 23 + .../MapperSourceGen.Sample/packages.lock.json | 10 + .../Extensions/TypeSymbolExtensions.cs | 24 ++ .../MapperGenerator.cs | 215 ++++++++++ .../MapperSourceGen.SourceGenerator.csproj | 31 ++ .../Model/HierarchyInfo.Syntax.cs | 71 +++ .../Model/HierarchyInfo.cs | 115 +++++ .../Model/PropertyInfo.cs | 105 +++++ .../Model/TypeInfo.cs | 48 +++ .../Properties/launchSettings.json | 9 + .../packages.lock.json | 121 ++++++ src/MapperSourceGen/MapAliasAttribute.cs | 27 ++ src/MapperSourceGen/MapIgnoreAttribute.cs | 9 + src/MapperSourceGen/MapperAttribute.cs | 9 + src/MapperSourceGen/MapperSourceGen.csproj | 19 + src/MapperSourceGen/packages.lock.json | 6 + tests/.editorconfig | 9 + tests/Directory.Build.props | 16 + .../Constants.cs | 16 + .../Fixture.cs | 39 ++ .../MapperGeneratorFacts.cs | 20 + ...pperSourceGen.SourceGenerator.Tests.csproj | 51 +++ .../ModuleInitializer.cs | 18 + .../packages.lock.json | 254 +++++++++++ ...eGenerator.Tests.MyEntityDto.g.verified.cs | 12 + ...nerator.Tests.MyEntityMapper.g.verified.cs | 28 ++ 37 files changed, 2647 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 Directory.Build.props create mode 100644 Directory.Build.targets create mode 100644 Directory.Packages.props create mode 100644 MapperSourceGenSample.sln create mode 100644 MapperSourceGenSample.sln.DotSettings create mode 100644 README.md create mode 100644 global.json create mode 100644 sample/MapperSourceGen.Sample/Domain/Customers/Customer.cs create mode 100644 sample/MapperSourceGen.Sample/Domain/Orders/Order.cs create mode 100644 sample/MapperSourceGen.Sample/MapperSourceGen.Sample.csproj create mode 100644 sample/MapperSourceGen.Sample/packages.lock.json create mode 100644 src/MapperSourceGen.SourceGenerator/Extensions/TypeSymbolExtensions.cs create mode 100644 src/MapperSourceGen.SourceGenerator/MapperGenerator.cs create mode 100644 src/MapperSourceGen.SourceGenerator/MapperSourceGen.SourceGenerator.csproj create mode 100644 src/MapperSourceGen.SourceGenerator/Model/HierarchyInfo.Syntax.cs create mode 100644 src/MapperSourceGen.SourceGenerator/Model/HierarchyInfo.cs create mode 100644 src/MapperSourceGen.SourceGenerator/Model/PropertyInfo.cs create mode 100644 src/MapperSourceGen.SourceGenerator/Model/TypeInfo.cs create mode 100644 src/MapperSourceGen.SourceGenerator/Properties/launchSettings.json create mode 100644 src/MapperSourceGen.SourceGenerator/packages.lock.json create mode 100644 src/MapperSourceGen/MapAliasAttribute.cs create mode 100644 src/MapperSourceGen/MapIgnoreAttribute.cs create mode 100644 src/MapperSourceGen/MapperAttribute.cs create mode 100644 src/MapperSourceGen/MapperSourceGen.csproj create mode 100644 src/MapperSourceGen/packages.lock.json create mode 100644 tests/.editorconfig create mode 100644 tests/Directory.Build.props create mode 100644 tests/MapperSourceGen.SourceGenerator.Tests/Constants.cs create mode 100644 tests/MapperSourceGen.SourceGenerator.Tests/Fixture.cs create mode 100644 tests/MapperSourceGen.SourceGenerator.Tests/MapperGeneratorFacts.cs create mode 100644 tests/MapperSourceGen.SourceGenerator.Tests/MapperSourceGen.SourceGenerator.Tests.csproj create mode 100644 tests/MapperSourceGen.SourceGenerator.Tests/ModuleInitializer.cs create mode 100644 tests/MapperSourceGen.SourceGenerator.Tests/packages.lock.json create mode 100644 tests/MapperSourceGen.SourceGenerator.Tests/ref/MapperGeneratorFacts.Should_Emit_Dto_And_Mapper#MapperSourceGen.SourceGenerator.Tests.MyEntityDto.g.verified.cs create mode 100644 tests/MapperSourceGen.SourceGenerator.Tests/ref/MapperGeneratorFacts.Should_Emit_Dto_And_Mapper#MapperSourceGen.SourceGenerator.Tests.MyEntityMapper.g.verified.cs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..543a92b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,218 @@ +# Remove the line below if you want to inherit .editorconfig settings from higher directories +root = true + +[*] +#### Core EditorConfig Options #### + +# Indentation and spacing +indent_size = 4 +indent_style = space +tab_width = 4 + +# New line preferences +end_of_line = lf +insert_final_newline = true + +# Razor files +[*.{cs,razor}] +dotnet_diagnostic.CA2007.severity = none + +# C# files +[*.cs] +#### .NET Coding Conventions #### + +# this. and Me. preferences +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true +dotnet_style_predefined_type_for_member_access = true + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members + +# Expression-level preferences +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_throw_expression = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_object_initializer = true:suggestion +dotnet_style_prefer_auto_properties = true +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true +dotnet_style_prefer_conditional_expression_over_return = true +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion + +# Field preferences +dotnet_style_readonly_field = true:suggestion + +# Parameter preferences +dotnet_code_quality_unused_parameters = all:suggestion + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = false:warning +csharp_style_var_for_built_in_types = false:warning +csharp_style_var_when_type_is_apparent = false:warning + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true +csharp_style_expression_bodied_constructors = false +csharp_style_expression_bodied_indexers = true +csharp_style_expression_bodied_lambdas = true +csharp_style_expression_bodied_local_functions = false +csharp_style_expression_bodied_methods = false +csharp_style_expression_bodied_operators = false +csharp_style_expression_bodied_properties = true + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion + +# Null-checking preferences +csharp_style_conditional_delegate_call = true:suggestion + +# Modifier preferences +csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async + +# Code-block preferences +csharp_prefer_braces = true + +# Expression-level preferences +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_pattern_local_over_anonymous_function = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable + +# 'using' directive preferences +csharp_using_directive_placement = inside_namespace + +# Constructor Preferences +csharp_style_prefer_primary_constructors = false +dotnet_diagnostic.IDE0290.severity = none +resharper_convert_to_primary_constructor_highlighting = none + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_open_brace = methods, properties, control_blocks, types +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = control_flow_statements, expressions +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Resharper Formatting Rules #### + +resharper_csharp_accessor_declaration_braces = next_line +resharper_csharp_accessor_owner_declaration_braces = next_line +resharper_csharp_align_first_arg_by_paren = true +resharper_csharp_align_linq_query = true +resharper_csharp_align_multiline_argument = true +resharper_csharp_align_multiline_array_and_object_initializer = false +resharper_csharp_align_multiline_binary_expressions_chain = true +resharper_csharp_align_multiline_binary_patterns = true +resharper_csharp_align_multiline_calls_chain = true +resharper_csharp_align_multiline_expression = true +resharper_csharp_align_multiline_extends_list = true +resharper_csharp_align_multiline_parameter = true +resharper_csharp_align_multiline_property_pattern = true +resharper_csharp_align_multiline_statement_conditions = true +resharper_csharp_align_multiline_switch_expression = false +resharper_csharp_align_multiple_declaration = true +resharper_csharp_align_multline_type_parameter_constrains = true +resharper_csharp_align_multline_type_parameter_list = true +resharper_csharp_align_tuple_components = true +resharper_csharp_alignment_tab_fill_style = optimal_fill +resharper_csharp_allow_far_alignment = true +resharper_csharp_brace_style = next_line +resharper_csharp_case_block_braces = next_line +resharper_csharp_continuous_indent_multiplier = 1 +resharper_csharp_indent_anonymous_method_block = true +resharper_csharp_indent_braces_inside_statement_conditions = true +resharper_csharp_indent_inside_namespace = true +resharper_csharp_indent_nested_fixed_stmt = false +resharper_csharp_indent_nested_for_stmt = false +resharper_csharp_indent_nested_foreach_stmt = false +resharper_csharp_indent_nested_lock_stmt = false +resharper_csharp_indent_nested_usings_stmt = false +resharper_csharp_indent_nested_while_stmt = false +resharper_csharp_indent_preprocessor_if = no_indent +resharper_csharp_indent_preprocessor_other = usual_indent +resharper_csharp_indent_preprocessor_region = no_indent +resharper_csharp_indent_type_constraints = true +resharper_csharp_initializer_braces = next_line +resharper_csharp_invocable_declaration_braces = next_line +resharper_csharp_keep_existing_attribute_arrangement = false +resharper_csharp_keep_existing_initializer_arrangement = false +resharper_csharp_max_attribute_length_for_same_line = 120 +resharper_csharp_max_initializer_elements_on_line = 1 +resharper_csharp_naming_rule.enum_member = AaBb +resharper_csharp_other_braces = next_line +resharper_csharp_outdent_binary_ops = true +resharper_csharp_outdent_binary_pattern_ops = true +resharper_csharp_outdent_dots = true +resharper_csharp_place_attribute_on_same_line = false +resharper_csharp_place_record_field_attribute_on_same_line = true +resharper_csharp_place_simple_initializer_on_single_line = true +resharper_csharp_type_declaration_braces = next_line +resharper_csharp_wrap_object_and_collection_initializer_style = chop_always + +dotnet_diagnostic.CA2007.severity = error + +# warning suppressions +dotnet_diagnostic.CA2255.severity = none diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9c0288a --- /dev/null +++ b/.gitignore @@ -0,0 +1,406 @@ +## 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/main/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/ +[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 +*.tlog +*.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 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# 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/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# 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 + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Rider +.idea/* + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml + +.DS_Store + +# SonarQube +.sonarqube diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..c7f4bd1 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,49 @@ + + + + Alan Brault + enable + false + 12 + en-US + enable + true + 0.0.1 + + + + true + false + AllEnabledByDefault + + + + embedded + true + true + false + https://git.visus.io/alan.brault/MapperSourceGenSample + true + git + https://git.visus.io/alan.brault/MapperSourceGenSample.git + + + + true + true + + + + $(BeforePack);IncludeAnalyzersInPackage + + + + + <_Parameter1>$(AssemblyName).Tests + + + <_Parameter1>DynamicProxyGenAssembly2 + + + + diff --git a/Directory.Build.targets b/Directory.Build.targets new file mode 100644 index 0000000..b5bc844 --- /dev/null +++ b/Directory.Build.targets @@ -0,0 +1,47 @@ + + + analyzers/dotnet + $(GeneratorProjectBaseTargetPath)/$(AnalyzerLanguage) + $(NoWarn);RS1041 + + + + + $(GeneratorProjectBaseTargetPath) + + + + + + + + + + + <_TargetPathsToSymbols Include="@(_AnalyzerFile)" TargetPath="/%(_AnalyzerFile.PackagePath)" Condition="%(_AnalyzerFile.IsSymbol)" /> + + + + + + + <_AnalyzerPath>analyzers/dotnet + <_AnalyzerPath Condition="'$(AnalyzerRoslynVersion)' != ''">$(_AnalyzerPath)/roslyn$(AnalyzerRoslynVersion) + <_AnalyzerPath Condition="'$(AnalyzerLanguage)' != ''">$(_AnalyzerPath)/$(AnalyzerLanguage) + + + + <_AnalyzerPackFile Include="@(_BuildOutputInPackage->WithMetadataValue('TargetFramework', 'netstandard2.0'))" IsSymbol="false" /> + <_AnalyzerPackFile Include="@(_TargetPathsToSymbols->WithMetadataValue('TargetFramework', 'netstandard2.0'))" IsSymbol="true" /> + <_AnalyzerPackFile PackagePath="$(_AnalyzerPath)/%(TargetPath)" /> + + + + + diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 0000000..f7f5a30 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,19 @@ + + + true + true + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MapperSourceGenSample.sln b/MapperSourceGenSample.sln new file mode 100644 index 0000000..2cc1e81 --- /dev/null +++ b/MapperSourceGenSample.sln @@ -0,0 +1,57 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{36DCA314-4C08-44D2-BE6B-4AD5ACD38A35}" + ProjectSection(SolutionItems) = preProject + Directory.Packages.props = Directory.Packages.props + Directory.Build.props = Directory.Build.props + Directory.Build.targets = Directory.Build.targets + README.md = README.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "01. Foundation", "01. Foundation", "{F327E906-C84C-4C79-9FDC-A8BA45ADCA7D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapperSourceGen.SourceGenerator", "src\MapperSourceGen.SourceGenerator\MapperSourceGen.SourceGenerator.csproj", "{A2AA5839-C781-4E1A-B0AF-67F5BDD5C394}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "02. Sample", "02. Sample", "{62B5A0CF-AB5F-4BF1-8D29-E22AB7DC6348}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "03. Unit Tests", "03. Unit Tests", "{9E52F931-B3B3-4F6B-9EDF-89CFE8DC031C}" + ProjectSection(SolutionItems) = preProject + tests\Directory.Build.props = tests\Directory.Build.props + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapperSourceGen", "src\MapperSourceGen\MapperSourceGen.csproj", "{16ADA529-B8A5-43EB-957C-699BF654F894}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapperSourceGen.Sample", "sample\MapperSourceGen.Sample\MapperSourceGen.Sample.csproj", "{9ECCD86F-9E77-41C3-9494-A8E2C40BF5B4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapperSourceGen.SourceGenerator.Tests", "tests\MapperSourceGen.SourceGenerator.Tests\MapperSourceGen.SourceGenerator.Tests.csproj", "{0B318171-7E7A-4283-9B40-EA97E7ECB728}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {A2AA5839-C781-4E1A-B0AF-67F5BDD5C394} = {F327E906-C84C-4C79-9FDC-A8BA45ADCA7D} + {16ADA529-B8A5-43EB-957C-699BF654F894} = {F327E906-C84C-4C79-9FDC-A8BA45ADCA7D} + {9ECCD86F-9E77-41C3-9494-A8E2C40BF5B4} = {62B5A0CF-AB5F-4BF1-8D29-E22AB7DC6348} + {0B318171-7E7A-4283-9B40-EA97E7ECB728} = {9E52F931-B3B3-4F6B-9EDF-89CFE8DC031C} + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A2AA5839-C781-4E1A-B0AF-67F5BDD5C394}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A2AA5839-C781-4E1A-B0AF-67F5BDD5C394}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A2AA5839-C781-4E1A-B0AF-67F5BDD5C394}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A2AA5839-C781-4E1A-B0AF-67F5BDD5C394}.Release|Any CPU.Build.0 = Release|Any CPU + {16ADA529-B8A5-43EB-957C-699BF654F894}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {16ADA529-B8A5-43EB-957C-699BF654F894}.Debug|Any CPU.Build.0 = Debug|Any CPU + {16ADA529-B8A5-43EB-957C-699BF654F894}.Release|Any CPU.ActiveCfg = Release|Any CPU + {16ADA529-B8A5-43EB-957C-699BF654F894}.Release|Any CPU.Build.0 = Release|Any CPU + {9ECCD86F-9E77-41C3-9494-A8E2C40BF5B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9ECCD86F-9E77-41C3-9494-A8E2C40BF5B4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9ECCD86F-9E77-41C3-9494-A8E2C40BF5B4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9ECCD86F-9E77-41C3-9494-A8E2C40BF5B4}.Release|Any CPU.Build.0 = Release|Any CPU + {0B318171-7E7A-4283-9B40-EA97E7ECB728}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0B318171-7E7A-4283-9B40-EA97E7ECB728}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0B318171-7E7A-4283-9B40-EA97E7ECB728}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0B318171-7E7A-4283-9B40-EA97E7ECB728}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/MapperSourceGenSample.sln.DotSettings b/MapperSourceGenSample.sln.DotSettings new file mode 100644 index 0000000..741c5ed --- /dev/null +++ b/MapperSourceGenSample.sln.DotSettings @@ -0,0 +1,316 @@ + + True + Built-in: Full Cleanup + Built-in: Full Cleanup + False + <?xml version="1.0" encoding="utf-16"?> +<Patterns xmlns="urn:schemas-jetbrains-com:member-reordering-patterns"> + <TypePattern DisplayName="Non-reorderable types"> + <TypePattern.Match> + <Or> + <And> + <Kind Is="Interface" /> + <Or> + <HasAttribute Name="System.Runtime.InteropServices.InterfaceTypeAttribute" /> + <HasAttribute Name="System.Runtime.InteropServices.ComImport" /> + </Or> + </And> + <Kind Is="Struct" /> + <HasAttribute Name="JetBrains.Annotations.NoReorderAttribute" /> + <HasAttribute Name="JetBrains.Annotations.NoReorder" /> + </Or> + </TypePattern.Match> + </TypePattern> + <TypePattern DisplayName="xUnit.net Test Classes" RemoveRegions="All"> + <TypePattern.Match> + <And> + <Kind Is="Class" /> + <HasMember> + <And> + <Kind Is="Method" /> + <HasAttribute Name="Xunit.FactAttribute" Inherited="True" /> + </And> + </HasMember> + </And> + </TypePattern.Match> + <Entry DisplayName="Setup/Teardown Methods"> + <Entry.Match> + <Or> + <Kind Is="Constructor" /> + <And> + <Kind Is="Method" /> + <ImplementsInterface Name="System.IDisposable" /> + </And> + </Or> + </Entry.Match> + <Entry.SortBy> + <Kind Order="Constructor" /> + </Entry.SortBy> + </Entry> + <Entry DisplayName="All other members" /> + <Entry DisplayName="Test Methods" Priority="100"> + <Entry.Match> + <And> + <Kind Is="Method" /> + <HasAttribute Name="Xunit.FactAttribute" /> + </And> + </Entry.Match> + <Entry.SortBy> + <Name /> + </Entry.SortBy> + </Entry> + </TypePattern> + <TypePattern DisplayName="NUnit Test Fixtures" RemoveRegions="All"> + <TypePattern.Match> + <And> + <Kind Is="Class" /> + <HasAttribute Name="NUnit.Framework.TestFixtureAttribute" Inherited="True" /> + </And> + </TypePattern.Match> + <Entry DisplayName="Setup/Teardown Methods"> + <Entry.Match> + <And> + <Kind Is="Method" /> + <Or> + <HasAttribute Name="NUnit.Framework.SetUpAttribute" Inherited="True" /> + <HasAttribute Name="NUnit.Framework.TearDownAttribute" Inherited="True" /> + <HasAttribute Name="NUnit.Framework.FixtureSetUpAttribute" Inherited="True" /> + <HasAttribute Name="NUnit.Framework.FixtureTearDownAttribute" Inherited="True" /> + </Or> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="All other members" /> + <Entry DisplayName="Test Methods" Priority="100"> + <Entry.Match> + <And> + <Kind Is="Method" /> + <HasAttribute Name="NUnit.Framework.TestAttribute" /> + </And> + </Entry.Match> + <Entry.SortBy> + <Name /> + </Entry.SortBy> + </Entry> + </TypePattern> + <TypePattern DisplayName="Default Pattern" RemoveRegions="All" Priority="150"> + <Group DisplayName="Group by Access"> + <Group.GroupBy> + <Access /> + </Group.GroupBy> + <Entry DisplayName="Constant or Field Constant"> + <Entry.Match> + <Or> + <Kind Is="Constant" /> + <And> + <Static /> + <Kind Is="Field" /> + </And> + </Or> + </Entry.Match> + <Entry.SortBy> + <Static /> + <Readonly /> + <Name /> + </Entry.SortBy> + </Entry> + </Group> + <Group DisplayName="Group by Access"> + <Group.GroupBy> + <Access /> + </Group.GroupBy> + <Entry DisplayName="Field"> + <Entry.Match> + <Kind Is="Field" /> + </Entry.Match> + <Entry.SortBy> + <Static /> + <Readonly /> + <Name /> + </Entry.SortBy> + </Entry> + </Group> + <Group DisplayName="Group by Access"> + <Group.GroupBy> + <Access /> + </Group.GroupBy> + <Entry DisplayName="Constructor"> + <Entry.Match> + <Kind Is="Constructor" /> + </Entry.Match> + <Entry.SortBy> + <Static /> + <Name /> + </Entry.SortBy> + </Entry> + </Group> + <Group DisplayName="Group by Access"> + <Group.GroupBy> + <Access /> + </Group.GroupBy> + <Entry DisplayName="Destructor"> + <Entry.Match> + <Kind Is="Destructor" /> + </Entry.Match> + <Entry.SortBy> + <Static /> + <Name /> + </Entry.SortBy> + </Entry> + </Group> + <Group DisplayName="Group by Access"> + <Group.GroupBy> + <Access /> + </Group.GroupBy> + <Entry DisplayName="Delegate"> + <Entry.Match> + <Kind Is="Delegate" /> + </Entry.Match> + <Entry.SortBy> + <Static /> + <Name /> + </Entry.SortBy> + </Entry> + </Group> + <Group DisplayName="Group by Access"> + <Group.GroupBy> + <Access /> + </Group.GroupBy> + <Entry DisplayName="Event"> + <Entry.Match> + <Kind Is="Event" /> + </Entry.Match> + <Entry.SortBy> + <Static /> + <Name /> + </Entry.SortBy> + </Entry> + </Group> + <Group DisplayName="Group by Access"> + <Group.GroupBy> + <Access /> + </Group.GroupBy> + <Entry DisplayName="Enum"> + <Entry.Match> + <Kind Is="Enum" /> + </Entry.Match> + <Entry.SortBy> + <Name /> + </Entry.SortBy> + </Entry> + </Group> + <Group DisplayName="Group by Access"> + <Group.GroupBy> + <Access /> + </Group.GroupBy> + <Entry DisplayName="Interface"> + <Entry.Match> + <Kind Is="Interface" /> + </Entry.Match> + <Entry.SortBy> + <Name /> + </Entry.SortBy> + </Entry> + </Group> + <Group DisplayName="Group by Access"> + <Group.GroupBy> + <Access /> + </Group.GroupBy> + <Entry DisplayName="Property"> + <Entry.Match> + <Kind Is="Property" /> + </Entry.Match> + <Entry.SortBy> + <Static /> + <Readonly /> + <Name /> + </Entry.SortBy> + </Entry> + </Group> + <Group DisplayName="Group by Access"> + <Group.GroupBy> + <Access /> + </Group.GroupBy> + <Entry DisplayName="Indexer"> + <Entry.Match> + <Kind Is="Indexer" /> + </Entry.Match> + <Entry.SortBy> + <Static /> + <Name /> + </Entry.SortBy> + </Entry> + </Group> + <Group DisplayName="Group by Access"> + <Group.GroupBy> + <Access /> + </Group.GroupBy> + <Entry DisplayName="Method"> + <Entry.Match> + <Kind Is="Method" /> + </Entry.Match> + <Entry.SortBy> + <Static /> + <Name /> + </Entry.SortBy> + </Entry> + </Group> + <Group DisplayName="Group by Access"> + <Group.GroupBy> + <Access /> + </Group.GroupBy> + <Entry DisplayName="Struct"> + <Entry.Match> + <Kind Is="Struct" /> + </Entry.Match> + <Entry.SortBy> + <Name /> + </Entry.SortBy> + </Entry> + </Group> + <Group DisplayName="Group by Access"> + <Group.GroupBy> + <Access /> + </Group.GroupBy> + <Entry DisplayName="Class"> + <Entry.Match> + <Kind Is="Class" /> + </Entry.Match> + <Entry.SortBy> + <Static /> + <Sealed /> + <Name /> + </Entry.SortBy> + </Entry> + <Entry DisplayName="Record"> + <Entry.Match> + <Kind Is="Record" /> + </Entry.Match> + <Entry.SortBy> + <Readonly /> + <Name /> + </Entry.SortBy> + </Entry> + </Group> + </TypePattern> +</Patterns> +True +True +True +True +True +True + True +True +True + True + True + True + True + True + True + True + True + True + True + diff --git a/README.md b/README.md new file mode 100644 index 0000000..f5c40ae --- /dev/null +++ b/README.md @@ -0,0 +1,119 @@ +# Mapper Source Generator Example + +This project demonstrates how you can use incremental source generators in C# to generate domain transfer objects (DTOs) +and mapping classes from a POCO. + +> [!WARNING] +> This is a sample project and not intended for production use. It is meant to illustrate the concept of using source generators. +> Please look into using either [mapster's](https://github.com/MapsterMapper/Mapster) code generator or [mapperly](https://github.com/riok/mapperly) instead. + +## Requirements +- .NET 8.0 SDK + +## What's Included +- `MapperSourceGen`: The main project that contains the attributes for decorating classes and properties for the source generator. +- `MapperSourceGen.Sample`: A sample class library that demonstrates the use of the source generator. +- `MapperSourceGen.SourceGenerator`: The source generator that generates DTOs and mapping classes. +- `MapperSourceGen.SourceGenerator.Tests`: Unit tests for the source generator to ensure it works as expected (snapshot testing). + +Feel free to inspect the `.csproj` files for the projects to see how they are set up and how the source generator is integrated. + +Additionally, a `launchSettings.json` file is included so that you can run the source generator in debug mode and step through the code. Simply execute `Source Generator`. + +> [!NOTE] +> The source files emitted by the source generator will be under `Dependencies -> .NET 8.0 -> Source Generators` in the `MapperSourceGen.Sample` project if using Rider. + +## Purpose + +As projects grow, the need for transferring data between layers (like from a database to a UI) becomes essential. +However, with that comes the additional complexity of maintaining DTOs and mapping classes. One of the ways this can be +solved is by using source generators to automate the creation of these classes. + +Source generators provide the ability to generate code at compile time, which can help reduce boilerplate code and improve maintainability +and reduce the reliance on expensive reflection-based mapping. + +## How It Works + +The project **MapperSourceGen.SourceGenerator** contains a source generator that scans for classes decorated with the `GenerateMapperAttribute`. +When it finds such a class, it generates a DTO and a mapping class for it. + +### Example + +Let's say you have a class `Order` that you want to generate a DTO and mapping class for. You would decorate it with the `GenerateMapperAttribute` like this: + +**Order.cs** +```csharp +[Mapper] +public sealed class Order +{ + [MapAlias("EntityId")] // renames Id to EntityId in the DTO + public Guid Id { get; set; } + + public Guid? CustomerId { get; set; } + + [MapAlias("OrderId")] // renames IncrementId to OrderId in the DTO + public int IncrementId { get; set;} + + public DateTimeOffset Created { get; set;} + + public DateTimeOffset? Updated { get; set; } + + [MapIgnore] // this property will not be included in the DTO or mapper + public string TransactionId { get; set; } +} +``` + +The source generator will then generate a DTO class `OrderDto` and a mapping class `OrderMapper` that looks like this: + +**OrderMapper.g.cs** +```csharp +public sealed partial class OrderMapper +{ + public static Order ToModel(OrderDto source) + { + ArgumentNullException.ThrowIfNull(source); + + return new Order() + { + Id = source.EntityId, + CustomerId = source.CustomerId, + IncrementId = source.OrderId + Created = source.Created, + Updated = source.Updated + } + } + + public static OrderDto ToDto(Order source) + { + ArgumentNullException.ThrowIfNull(source); + + return new OrderDto() + { + EntityId = source.Id, + CustomerId = source.CustomerId, + OrderId = source.IncrementId, + Created = source.Created, + Updated = source.Updated + } + } +} +``` + +**OrderDto.g.cs** +```csharp +public sealed partial class OrderDto +{ + public Guid? CustomerId { get; set; } + + public Guid EntityId { get; set; } + + public int OrderId { get; set; } + + public DateTimeOffset Created { get; set; } + + public DateTimeOffset? Updated { get; set; } +} +``` + + + diff --git a/global.json b/global.json new file mode 100644 index 0000000..23cc917 --- /dev/null +++ b/global.json @@ -0,0 +1,8 @@ +{ + "sdk": { + "version": "8.0.412", + "rollForward": "latestFeature", + "allowPrerelease": false + } +} + diff --git a/sample/MapperSourceGen.Sample/Domain/Customers/Customer.cs b/sample/MapperSourceGen.Sample/Domain/Customers/Customer.cs new file mode 100644 index 0000000..6cebe8a --- /dev/null +++ b/sample/MapperSourceGen.Sample/Domain/Customers/Customer.cs @@ -0,0 +1,67 @@ +namespace MapperSourceGen.Sample.Domain.Customers; + +/// +/// Model representing a customer in the system. +/// +[Mapper] +public sealed class Customer +{ + /// + /// Represents the address lines of the customer. + /// + public IReadOnlyList AddressLines { get; set; } = []; + + /// + /// Represents the country of the customer. + /// + public string Country { get; set; } = string.Empty; + + /// + /// Represents the date and time when the customer was created. + /// + [MapIgnore] + public DateTimeOffset Created { get; set; } + + /// + /// Represents the unique identifier for the customer. + /// + public Guid CustomerId { get; set; } + + /// + /// Represents the email address of the customer. + /// + public string Email { get; set; } = string.Empty; + + /// + /// Represents the first name of the customer. + /// + public string FirstName { get; set; } = string.Empty; + + /// + /// Represents the last name of the customer. + /// + public string LastName { get; set; } = string.Empty; + + /// + /// Represents the phone number of the customer. + /// + public string Phone { get; set; } = string.Empty; + + /// + /// Represents the postal code of the customer. + /// + [MapAlias("ZipCode")] + public string PostalCode { get; set; } = string.Empty; + + /// + /// Represents the state or province of the customer. + /// + [MapAlias("State")] + public string StateOrProvince { get; set; } = string.Empty; + + /// + /// Represents the date and time when the customer was last updated (nullable). + /// + [MapIgnore] + public DateTimeOffset? Updated { get; set; } +} diff --git a/sample/MapperSourceGen.Sample/Domain/Orders/Order.cs b/sample/MapperSourceGen.Sample/Domain/Orders/Order.cs new file mode 100644 index 0000000..e7270e6 --- /dev/null +++ b/sample/MapperSourceGen.Sample/Domain/Orders/Order.cs @@ -0,0 +1,36 @@ +namespace MapperSourceGen.Sample.Domain.Orders; + +/// +/// Model representing an order in the system. +/// +[Mapper] +public sealed class Order +{ + /// + /// Represents the date and time when the order was created. + /// + public DateTimeOffset Created { get; set; } + + /// + /// Represents the unique identifier for the customer associated with the order (nullable). + /// + public Guid? CustomerId { get; set; } + + /// + /// Represents the unique identifier for the order (private). + /// + [MapAlias("EntityId")] + public Guid Id { get; set; } + + /// + /// Represents the unique identifier integer for the order (public). + /// + [MapAlias("OrderId")] + public int IncrementId { get; set; } + + /// + /// Represents the date and time when the order was last updated. + /// + [MapIgnore] + public DateTimeOffset? Updated { get; set; } +} diff --git a/sample/MapperSourceGen.Sample/MapperSourceGen.Sample.csproj b/sample/MapperSourceGen.Sample/MapperSourceGen.Sample.csproj new file mode 100644 index 0000000..eec6f12 --- /dev/null +++ b/sample/MapperSourceGen.Sample/MapperSourceGen.Sample.csproj @@ -0,0 +1,23 @@ + + + + net8.0 + + + + + + + + + + diff --git a/sample/MapperSourceGen.Sample/packages.lock.json b/sample/MapperSourceGen.Sample/packages.lock.json new file mode 100644 index 0000000..617b656 --- /dev/null +++ b/sample/MapperSourceGen.Sample/packages.lock.json @@ -0,0 +1,10 @@ +{ + "version": 2, + "dependencies": { + "net8.0": { + "mappersourcegen": { + "type": "Project" + } + } + } +} \ No newline at end of file diff --git a/src/MapperSourceGen.SourceGenerator/Extensions/TypeSymbolExtensions.cs b/src/MapperSourceGen.SourceGenerator/Extensions/TypeSymbolExtensions.cs new file mode 100644 index 0000000..d53252a --- /dev/null +++ b/src/MapperSourceGen.SourceGenerator/Extensions/TypeSymbolExtensions.cs @@ -0,0 +1,24 @@ +namespace MapperSourceGen.SourceGenerator.Extensions; + +using System.Runtime.CompilerServices; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +/// +/// Extension methods for . +/// +internal static class TypeSymbolExtensions +{ + /// + /// Converts an to a . + /// + /// The source instance to convert. + /// An instance of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TypeSyntax ToTypeSyntax(this ITypeSymbol source) + { + string typeName = source.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat); + return SyntaxFactory.ParseTypeName(typeName); + } +} diff --git a/src/MapperSourceGen.SourceGenerator/MapperGenerator.cs b/src/MapperSourceGen.SourceGenerator/MapperGenerator.cs new file mode 100644 index 0000000..4496617 --- /dev/null +++ b/src/MapperSourceGen.SourceGenerator/MapperGenerator.cs @@ -0,0 +1,215 @@ +namespace MapperSourceGen.SourceGenerator; + +using System.Collections.Immutable; +using System.Text; +using Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Model; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +/// +/// Incremental source generator that is responsible for generating domain transfer objects (DTOs) and mapper from a +/// class decorated with the MapperAttribute. +/// +[Generator] +public sealed class MapperGenerator : IIncrementalGenerator +{ + private const string MapIgnoreAttributeName = "MapperSourceGen.MapIgnoreAttribute"; + + private const string MapperAttributeName = "MapperSourceGen.MapperAttribute"; + + private const string Source = "source"; + + /// + public void Initialize(IncrementalGeneratorInitializationContext context) + { + // This configures the generator pipeline to look for classes decorated with the MapperAttribute which will then extract + // the hierarchy information and properties to generate DTOs and mapping methods. + IncrementalValuesProvider<(HierarchyInfo Hierarchy, ImmutableArray Properties)> items = + context.SyntaxProvider + .ForAttributeWithMetadataName(MapperAttributeName, + static (node, _) => node is ClassDeclarationSyntax { AttributeLists.Count: > 0 }, + static (ctx, _) => + { + if ( ctx.TargetSymbol is not INamedTypeSymbol classSymbol ) + { + return default; + } + + HierarchyInfo hierarchy = HierarchyInfo.From(classSymbol); + ImmutableArray properties = + [ + ..classSymbol.GetMembers() + .OfType() + .Where(w => !w.IsIndexer + && w is { IsAbstract: false, DeclaredAccessibility: Accessibility.Public } + && !w.GetAttributes().Any(a => a.AttributeClass?.ToDisplayString() == MapIgnoreAttributeName)) + .Select(s => new PropertyInfo(s)) + ]; + + + return ( hierarchy, properties ); + }); + + // This registers the source output for generating the domain transfer object (DTO). + context.RegisterSourceOutput(items, + static (ctx, item) => + { + ImmutableArray properties = [..GenerateAutoProperties(item.Properties)]; + + CompilationUnitSyntax? compilationUnit = item.Hierarchy.GetCompilationUnit(properties, suffix: "Dto"); + if ( compilationUnit is not null ) + { + ctx.AddSource($"{item.Hierarchy.FileNameHint}Dto.g", compilationUnit.GetText(Encoding.UTF8)); + } + }); + + // This registers the source output for generating the mapper class with mapping methods. + context.RegisterSourceOutput(items, + static (ctx, item) => + { + ImmutableArray declarations = + [ + GenerateMapFromMethod(item.Hierarchy, item.Properties), + GenerateMapToMethod(item.Hierarchy, item.Properties) + ]; + + CompilationUnitSyntax? compilationUnit = item.Hierarchy.GetCompilationUnit(declarations, suffix: "Mapper"); + if ( compilationUnit is not null ) + { + ctx.AddSource($"{item.Hierarchy.FileNameHint}Mapper.g", compilationUnit.GetText(Encoding.UTF8)); + } + }); + } + + /// + /// Generates an expression statement that validates the argument for null. + /// + /// + /// An expression statement that will output ArgumentNullException.ThrowIfNull(nameof(source)); + /// + private static ExpressionStatementSyntax GenerateArgumentValidator() + { + MemberAccessExpressionSyntax member = MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("ArgumentNullException"), + IdentifierName("ThrowIfNull")); + + IdentifierNameSyntax nameofIdentifier = IdentifierName(Identifier(TriviaList(), + SyntaxKind.NameOfKeyword, + "nameof", + "nameof", + TriviaList())); + + ArgumentSyntax argument = Argument(InvocationExpression(nameofIdentifier) + .WithArgumentList(ArgumentList(SingletonSeparatedList(Argument(IdentifierName(Source)))))); + + return ExpressionStatement(InvocationExpression(member) + .WithArgumentList(ArgumentList(SingletonSeparatedList(argument)))); + } + + /// + /// Generates auto properties used in the domain transfer object (DTO). + /// + /// An collection of properties to generate auto-properties for. + /// + /// An of items that will output auto-properties: + /// + /// public string Name { get; set; + /// + /// + private static IEnumerable GenerateAutoProperties(IEnumerable properties) + { + return properties.Select(property => PropertyDeclaration(property.PropertyType.ToTypeSyntax(), property.TargetPropertyName) + .AddModifiers(Token(SyntaxKind.PublicKeyword)) + .AddAccessorListAccessors( + AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)), + AccessorDeclaration(SyntaxKind.SetAccessorDeclaration) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) + )); + } + + /// + /// Generates a method that maps a DTO to a model. + /// + /// An instance that contains information about the model. + /// An of properties to be used in mapping the DTO to the model. + /// + /// An instance of containing the method that returns an instance of the + /// model. + /// + /// Properties that are decorated with the MapIgnoreAttribute will not be included in the mapping. + private static MethodDeclarationSyntax GenerateMapFromMethod(HierarchyInfo hierarchy, ImmutableArray properties) + { + ParameterSyntax parameter = Parameter(Identifier(Source)) + .WithType(ParseTypeName($"{hierarchy.FileNameHint}Dto")); + + ReturnStatementSyntax returnStatement = ReturnStatement(GenerateMappingMethod(hierarchy, properties, isSource: false)); + + return MethodDeclaration(ParseTypeName($"{hierarchy.FileNameHint}"), "ToModel") + .AddModifiers(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.StaticKeyword)) + .AddParameterListParameters(parameter) + .WithBody(Block(GenerateArgumentValidator(), returnStatement)); + } + + private static ObjectCreationExpressionSyntax GenerateMappingMethod(HierarchyInfo hierarchy, + ImmutableArray properties, + string? suffix = null, + bool isSource = true) + { + return ObjectCreationExpression(IdentifierName(ParseTypeName($"{hierarchy.FileNameHint}{suffix}").ToString())) + .WithArgumentList(ArgumentList()) + .WithInitializer(InitializerExpression(SyntaxKind.ObjectInitializerExpression, + SeparatedList(GeneratePropertyAssignments(properties, isSource)))); + } + + /// + /// Generates a method that maps a model to a DTO. + /// + /// An instance that contains information about the model. + /// An of properties to be used in mapping the model to the DTO. + /// + /// An instance of containing the method that returns an instance of the + /// DTO. + /// + /// Properties that are decorated with the MapIgnoreAttribute will not be included in the mapping. + private static MethodDeclarationSyntax GenerateMapToMethod(HierarchyInfo hierarchy, ImmutableArray properties) + { + ParameterSyntax parameter = Parameter(Identifier(Source)) + .WithType(ParseTypeName(hierarchy.FileNameHint)); + + ReturnStatementSyntax returnStatement = ReturnStatement(GenerateMappingMethod(hierarchy, properties, "Dto")); + + return MethodDeclaration(ParseTypeName($"{hierarchy.FileNameHint}Dto"), "ToDto") + .AddModifiers(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.StaticKeyword)) + .AddParameterListParameters(parameter) + .WithBody(Block(GenerateArgumentValidator(), returnStatement)); + } + + /// + /// Generates property assignments for the object initializer in the mapping methods. + /// + /// An of properties that are to be included in the initializer. + /// + /// Determines whether the MapAliasAttribute is to be used on the left or right side of + /// assignment. + /// + /// + /// An containing instances for property + /// assignment. + /// + private static IEnumerable GeneratePropertyAssignments(IEnumerable properties, bool isSource = true) + { + return properties.SelectMany(s => new SyntaxNodeOrToken[] + { + AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, + IdentifierName(!isSource ? s.SourcePropertyName : s.TargetPropertyName), + MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, + IdentifierName(Source), + IdentifierName(isSource ? s.SourcePropertyName : s.TargetPropertyName))), + Token(SyntaxKind.CommaToken) + }); + } +} diff --git a/src/MapperSourceGen.SourceGenerator/MapperSourceGen.SourceGenerator.csproj b/src/MapperSourceGen.SourceGenerator/MapperSourceGen.SourceGenerator.csproj new file mode 100644 index 0000000..1a4b14c --- /dev/null +++ b/src/MapperSourceGen.SourceGenerator/MapperSourceGen.SourceGenerator.csproj @@ -0,0 +1,31 @@ + + + + + netstandard2.0 + + + + + cs + + + true + + + true + + + true + + + + + + + + + diff --git a/src/MapperSourceGen.SourceGenerator/Model/HierarchyInfo.Syntax.cs b/src/MapperSourceGen.SourceGenerator/Model/HierarchyInfo.Syntax.cs new file mode 100644 index 0000000..6bfc960 --- /dev/null +++ b/src/MapperSourceGen.SourceGenerator/Model/HierarchyInfo.Syntax.cs @@ -0,0 +1,71 @@ +// This file is ported and adapted from CommunityToolkit.Mvvm (CommunityToolkit/dotnet) + +namespace MapperSourceGen.SourceGenerator.Model; + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +internal partial class HierarchyInfo +{ + /// + /// Gets a representing the type hierarchy with the specified member declarations. + /// + /// + /// The instances to include in the + /// . + /// + /// The prefix to prepend to the file and class name. + /// The suffix to append to the file and class name. + /// An instance of used for generating the final output. + public CompilationUnitSyntax GetCompilationUnit(ImmutableArray memberDeclarations, string? prefix = null, string? suffix = null) + { + TypeDeclarationSyntax typeDeclarationSyntax = + Hierarchy[0].GetSyntax(prefix, suffix) + .AddModifiers(Token(TriviaList(Comment("/// ")), + Hierarchy[0].AccessibilityKind, + TriviaList())) + .AddModifiers(GetKeywordModifierTokens(Hierarchy[0])) + .AddMembers([.. memberDeclarations]) + .NormalizeWhitespace(); + + foreach ( TypeInfo parentType in Hierarchy.AsSpan().Slice(1) ) + { + typeDeclarationSyntax = + parentType.GetSyntax(prefix, suffix) + .AddModifiers(Token(TriviaList(Comment("/// ")), + parentType.AccessibilityKind, + TriviaList())) + .AddModifiers(GetKeywordModifierTokens(parentType)) + .AddMembers(typeDeclarationSyntax) + .NormalizeWhitespace(); + } + + SyntaxTriviaList syntaxTriviaList = TriviaList( + Comment("// "), + Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)), + Trivia(NullableDirectiveTrivia(Token(SyntaxKind.EnableKeyword), true))); + + return CompilationUnit() + .AddMembers(NamespaceDeclaration(IdentifierName(Namespace)) + .WithLeadingTrivia(syntaxTriviaList) + .AddMembers(typeDeclarationSyntax)) + .NormalizeWhitespace(); + } + + private static SyntaxToken[] GetKeywordModifierTokens(TypeInfo typeInfo) + { + HashSet tokens = []; + + if ( typeInfo.IsSealed ) + { + tokens.Add(Token(SyntaxKind.SealedKeyword)); + } + + tokens.Add(Token(SyntaxKind.PartialKeyword)); + + return [.. tokens]; + } +} diff --git a/src/MapperSourceGen.SourceGenerator/Model/HierarchyInfo.cs b/src/MapperSourceGen.SourceGenerator/Model/HierarchyInfo.cs new file mode 100644 index 0000000..9aecab8 --- /dev/null +++ b/src/MapperSourceGen.SourceGenerator/Model/HierarchyInfo.cs @@ -0,0 +1,115 @@ +// This file is ported and adapted from CommunityToolkit.Mvvm (CommunityToolkit/dotnet) + +namespace MapperSourceGen.SourceGenerator.Model; + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using static Microsoft.CodeAnalysis.SymbolDisplayTypeQualificationStyle; + +/// +/// Represents information about a type hierarchy in a source code file. +/// +internal sealed partial class HierarchyInfo : IEquatable +{ + /// + /// Initializes a new instance of the class. + /// + /// The filename hint for the type (including namespace) without extension. + /// The containing namespace for the type. + /// The current hierarchy for the type. + /// is null or empty. + private HierarchyInfo(string fileNameHint, string @namespace, ImmutableArray hierarchy) + { + if ( string.IsNullOrWhiteSpace(fileNameHint) ) + { + throw new ArgumentException($"'{nameof(fileNameHint)}' cannot be null or empty.", nameof(fileNameHint)); + } + + FileNameHint = fileNameHint; + Hierarchy = hierarchy; + Namespace = @namespace; + } + + /// + /// Gets the file name hint (including full namespace) for the type hierarchy. + /// + public string FileNameHint { get; } + + /// + /// Gets a collection of representing the hierarchy of types. + /// + public ImmutableArray Hierarchy { get; } + + /// + /// Gets the namespace of the type hierarchy. + /// + public string Namespace { get; } + + public static HierarchyInfo From(INamedTypeSymbol typeSymbol) + { + if ( typeSymbol is null ) + { + throw new ArgumentNullException(nameof(typeSymbol)); + } + + LinkedList hierarchy = []; + + for ( INamedTypeSymbol? parent = typeSymbol; + parent is not null; + parent = parent.ContainingType ) + { + hierarchy.AddLast(new TypeInfo(parent.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat), + parent.TypeKind, + parent.DeclaredAccessibility, + parent.IsRecord, + parent.IsSealed)); + } + + return new HierarchyInfo(typeSymbol.ToDisplayString(new SymbolDisplayFormat(typeQualificationStyle: NameAndContainingTypesAndNamespaces)), + typeSymbol.ContainingNamespace.ToDisplayString(new SymbolDisplayFormat(typeQualificationStyle: NameAndContainingTypesAndNamespaces)), + [..hierarchy]); + } + + public static bool operator ==(HierarchyInfo? left, HierarchyInfo? right) + { + return Equals(left, right); + } + + public static bool operator !=(HierarchyInfo? left, HierarchyInfo? right) + { + return !Equals(left, right); + } + + /// + public bool Equals(HierarchyInfo? other) + { + if ( other is null ) + { + return false; + } + + if ( ReferenceEquals(this, other) ) + { + return true; + } + + return string.Equals(FileNameHint, other.FileNameHint, StringComparison.OrdinalIgnoreCase) + && string.Equals(Namespace, other.Namespace, StringComparison.OrdinalIgnoreCase); + } + + /// + public override bool Equals(object? obj) + { + return ReferenceEquals(this, obj) || ( obj is HierarchyInfo other && Equals(other) ); + } + + /// + public override int GetHashCode() + { + unchecked + { + return ( StringComparer.OrdinalIgnoreCase.GetHashCode(FileNameHint) * 397 ) + ^ StringComparer.OrdinalIgnoreCase.GetHashCode(Namespace); + } + } +} diff --git a/src/MapperSourceGen.SourceGenerator/Model/PropertyInfo.cs b/src/MapperSourceGen.SourceGenerator/Model/PropertyInfo.cs new file mode 100644 index 0000000..5bd1b57 --- /dev/null +++ b/src/MapperSourceGen.SourceGenerator/Model/PropertyInfo.cs @@ -0,0 +1,105 @@ +namespace MapperSourceGen.SourceGenerator.Model; + +using Microsoft.CodeAnalysis; + +/// +/// Represents information about a property in a source code file. +/// +internal readonly struct PropertyInfo : IEquatable +{ + private const string MapAliasAttributeName = "MapperSourceGen.MapAliasAttribute"; + + /// + /// Initializes a new instance of the struct. + /// + /// instance of . + /// is null. + public PropertyInfo(IPropertySymbol propertySymbol) + { + if ( propertySymbol is null ) + { + throw new ArgumentNullException(nameof(propertySymbol)); + } + + SourcePropertyName = TargetPropertyName = propertySymbol.Name; + + if ( !propertySymbol.GetAttributes().IsEmpty ) + { + AttributeData? attribute = propertySymbol + .GetAttributes() + .FirstOrDefault(f => f.AttributeClass?.ToDisplayString() == MapAliasAttributeName); + + if ( attribute is not null ) + { + TargetPropertyName = attribute.ConstructorArguments[0].Value!.ToString(); + } + } + + PropertyType = propertySymbol.Type; + } + + /// + /// Gets the type of the property. + /// + public ITypeSymbol PropertyType { get; } + + /// + /// Gets the name of the source property. + /// + public string SourcePropertyName { get; } + + /// + /// Gets the name of the target property (if different from the source). + /// + public string TargetPropertyName { get; } + + /// + /// Indicates whether two instances are equal based on their properties. + /// + /// the first instance to compare + /// the second instance to compare + /// true if both instances are equal, otherwise false. + public static bool operator ==(PropertyInfo left, PropertyInfo right) + { + return left.Equals(right); + } + + /// + /// Indicates whether two instances are not equal based on their properties. + /// + /// the first instance to compare + /// the second instance to compare + /// true if both instances are not equal, otherwise false. + public static bool operator !=(PropertyInfo left, PropertyInfo right) + { + return !left.Equals(right); + } + + /// + public bool Equals(PropertyInfo other) + { + return SymbolEqualityComparer.Default.Equals(PropertyType, other.PropertyType) + && string.Equals(SourcePropertyName, other.SourcePropertyName, StringComparison.OrdinalIgnoreCase) + && string.Equals(TargetPropertyName, other.TargetPropertyName, StringComparison.OrdinalIgnoreCase); + } + + /// + public override bool Equals(object? obj) + { + return obj is PropertyInfo other && Equals(other); + } + + /// + public override int GetHashCode() + { + unchecked + { + int hashCode = SymbolEqualityComparer.Default.GetHashCode(PropertyType); + + hashCode = ( hashCode * 397 ) ^ StringComparer.OrdinalIgnoreCase.GetHashCode(SourcePropertyName); + hashCode = ( hashCode * 397 ) ^ StringComparer.OrdinalIgnoreCase.GetHashCode(TargetPropertyName); + + return hashCode; + } + } +} diff --git a/src/MapperSourceGen.SourceGenerator/Model/TypeInfo.cs b/src/MapperSourceGen.SourceGenerator/Model/TypeInfo.cs new file mode 100644 index 0000000..d022306 --- /dev/null +++ b/src/MapperSourceGen.SourceGenerator/Model/TypeInfo.cs @@ -0,0 +1,48 @@ +// This file is ported and adapted from CommunityToolkit.Mvvm (CommunityToolkit/dotnet) + +namespace MapperSourceGen.SourceGenerator.Model; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +internal sealed record TypeInfo( + string QualifiedName, + TypeKind Kind, + Accessibility DeclaredAccessibility, + bool IsRecord, + bool IsSealed) +{ + public SyntaxKind AccessibilityKind => + DeclaredAccessibility switch + { + Accessibility.Public => SyntaxKind.PublicKeyword, + Accessibility.Internal => SyntaxKind.InternalKeyword, + Accessibility.Private => SyntaxKind.PrivateKeyword, + Accessibility.Protected => SyntaxKind.ProtectedKeyword, + _ => SyntaxKind.None + }; + + public Accessibility DeclaredAccessibility { get; } = DeclaredAccessibility; + + public bool IsRecord { get; } = IsRecord; + + public bool IsSealed { get; } = IsSealed; + + public TypeKind Kind { get; } = Kind; + + public string QualifiedName { get; } = QualifiedName; + + public TypeDeclarationSyntax GetSyntax(string? prefix = null, string? suffix = null) + { + return Kind switch + { + TypeKind.Class when IsRecord => + RecordDeclaration(Token(SyntaxKind.RecordKeyword), $"{prefix}{QualifiedName}{suffix}") + .WithOpenBraceToken(Token(SyntaxKind.OpenBraceToken)) + .WithCloseBraceToken(Token(SyntaxKind.CloseBraceToken)), + _ => ClassDeclaration($"{prefix}{QualifiedName}{suffix}") + }; + } +} diff --git a/src/MapperSourceGen.SourceGenerator/Properties/launchSettings.json b/src/MapperSourceGen.SourceGenerator/Properties/launchSettings.json new file mode 100644 index 0000000..78c1508 --- /dev/null +++ b/src/MapperSourceGen.SourceGenerator/Properties/launchSettings.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "SourceGenerator": { + "commandName": "DebugRoslynComponent", + "targetProject": "../../sample/MapperSourceGen.Sample/MapperSourceGen.Sample.csproj" + } + } +} diff --git a/src/MapperSourceGen.SourceGenerator/packages.lock.json b/src/MapperSourceGen.SourceGenerator/packages.lock.json new file mode 100644 index 0000000..2e8244a --- /dev/null +++ b/src/MapperSourceGen.SourceGenerator/packages.lock.json @@ -0,0 +1,121 @@ +{ + "version": 2, + "dependencies": { + ".NETStandard,Version=v2.0": { + "Microsoft.CodeAnalysis.CSharp": { + "type": "Direct", + "requested": "[4.11.0, 4.11.0]", + "resolved": "4.11.0", + "contentHash": "6XYi2EusI8JT4y2l/F3VVVS+ISoIX9nqHsZRaG6W5aFeJ5BEuBosHfT/ABb73FN0RZ1Z3cj2j7cL28SToJPXOw==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.3.4", + "Microsoft.CodeAnalysis.Common": "[4.11.0]", + "System.Buffers": "4.5.1", + "System.Collections.Immutable": "8.0.0", + "System.Memory": "4.5.5", + "System.Numerics.Vectors": "4.5.0", + "System.Reflection.Metadata": "8.0.0", + "System.Runtime.CompilerServices.Unsafe": "6.0.0", + "System.Text.Encoding.CodePages": "7.0.0", + "System.Threading.Tasks.Extensions": "4.5.4" + } + }, + "NETStandard.Library": { + "type": "Direct", + "requested": "[2.0.3, )", + "resolved": "2.0.3", + "contentHash": "st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0" + } + }, + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Transitive", + "resolved": "3.3.4", + "contentHash": "AxkxcPR+rheX0SmvpLVIGLhOUXAKG56a64kV9VQZ4y9gR9ZmPXnqZvHJnmwLSwzrEP6junUF11vuc+aqo5r68g==" + }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.5.1", + "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" + }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "AurL6Y5BA1WotzlEvVaIDpqzpIPvYnnldxru8oXJU2yFxFUy3+pNXjXd1ymO+RA0rq0+590Q8gaz2l3Sr7fmqg==", + "dependencies": { + "System.Memory": "4.5.5", + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.5.5", + "contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==", + "dependencies": { + "System.Buffers": "4.5.1", + "System.Numerics.Vectors": "4.4.0", + "System.Runtime.CompilerServices.Unsafe": "4.5.3" + } + }, + "System.Numerics.Vectors": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ==" + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "ptvgrFh7PvWI8bcVqG5rsA/weWM09EnthFHR5SCnS6IN+P4mj6rE1lBDC4U8HL9/57htKAqy4KQ3bBj84cfYyQ==", + "dependencies": { + "System.Collections.Immutable": "8.0.0", + "System.Memory": "4.5.5" + } + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" + }, + "System.Text.Encoding.CodePages": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "LSyCblMpvOe0N3E+8e0skHcrIhgV2huaNcjUUEa8hRtgEAm36aGkRoC8Jxlb6Ra6GSfF29ftduPNywin8XolzQ==", + "dependencies": { + "System.Memory": "4.5.5", + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "System.Threading.Tasks.Extensions": { + "type": "Transitive", + "resolved": "4.5.4", + "contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "4.5.3" + } + }, + "Microsoft.CodeAnalysis.Common": { + "type": "CentralTransitive", + "requested": "[4.11.0, )", + "resolved": "4.11.0", + "contentHash": "djf8ujmqYImFgB04UGtcsEhHrzVqzHowS+EEl/Yunc5LdrYrZhGBWUTXoCF0NzYXJxtfuD+UVQarWpvrNc94Qg==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.3.4", + "System.Buffers": "4.5.1", + "System.Collections.Immutable": "8.0.0", + "System.Memory": "4.5.5", + "System.Numerics.Vectors": "4.5.0", + "System.Reflection.Metadata": "8.0.0", + "System.Runtime.CompilerServices.Unsafe": "6.0.0", + "System.Text.Encoding.CodePages": "7.0.0", + "System.Threading.Tasks.Extensions": "4.5.4" + } + } + } + } +} \ No newline at end of file diff --git a/src/MapperSourceGen/MapAliasAttribute.cs b/src/MapperSourceGen/MapAliasAttribute.cs new file mode 100644 index 0000000..e5e102f --- /dev/null +++ b/src/MapperSourceGen/MapAliasAttribute.cs @@ -0,0 +1,27 @@ +namespace MapperSourceGen; + +/// +/// Specifies that the target property should appear in the domain transfer object with a different name. +/// +[AttributeUsage(AttributeTargets.Property)] +public sealed class MapAliasAttribute : Attribute +{ + private MapAliasAttribute() + { + } + + /// + /// Initializes a new instance of the class with the specified name. + /// + /// The name to use instead of the property name. + public MapAliasAttribute(string name) + { + ArgumentException.ThrowIfNullOrWhiteSpace(name); + Name = name; + } + + /// + /// The name of the property in the domain transfer object. + /// + public string? Name { get; } +} diff --git a/src/MapperSourceGen/MapIgnoreAttribute.cs b/src/MapperSourceGen/MapIgnoreAttribute.cs new file mode 100644 index 0000000..6440568 --- /dev/null +++ b/src/MapperSourceGen/MapIgnoreAttribute.cs @@ -0,0 +1,9 @@ +namespace MapperSourceGen; + +/// +/// Specifies that the target property should not be included in the domain transfer object or mapper. +/// +[AttributeUsage(AttributeTargets.Property)] +public sealed class MapIgnoreAttribute : Attribute +{ +} diff --git a/src/MapperSourceGen/MapperAttribute.cs b/src/MapperSourceGen/MapperAttribute.cs new file mode 100644 index 0000000..050f54b --- /dev/null +++ b/src/MapperSourceGen/MapperAttribute.cs @@ -0,0 +1,9 @@ +namespace MapperSourceGen; + +/// +/// Specifies that the target class is a candidate for DTO and mapper generation. +/// +[AttributeUsage(AttributeTargets.Class)] +public sealed class MapperAttribute : Attribute +{ +} diff --git a/src/MapperSourceGen/MapperSourceGen.csproj b/src/MapperSourceGen/MapperSourceGen.csproj new file mode 100644 index 0000000..ef106ee --- /dev/null +++ b/src/MapperSourceGen/MapperSourceGen.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + + + + + + + + diff --git a/src/MapperSourceGen/packages.lock.json b/src/MapperSourceGen/packages.lock.json new file mode 100644 index 0000000..dd5b66c --- /dev/null +++ b/src/MapperSourceGen/packages.lock.json @@ -0,0 +1,6 @@ +{ + "version": 2, + "dependencies": { + "net8.0": {} + } +} \ No newline at end of file diff --git a/tests/.editorconfig b/tests/.editorconfig new file mode 100644 index 0000000..806bea4 --- /dev/null +++ b/tests/.editorconfig @@ -0,0 +1,9 @@ +# C# files +[*.cs] + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = true:suggestion +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props new file mode 100644 index 0000000..1e2627a --- /dev/null +++ b/tests/Directory.Build.props @@ -0,0 +1,16 @@ + + + false + false + enable + true + false + true + + + + true + true + + + diff --git a/tests/MapperSourceGen.SourceGenerator.Tests/Constants.cs b/tests/MapperSourceGen.SourceGenerator.Tests/Constants.cs new file mode 100644 index 0000000..e64902b --- /dev/null +++ b/tests/MapperSourceGen.SourceGenerator.Tests/Constants.cs @@ -0,0 +1,16 @@ +namespace MapperSourceGen.SourceGenerator.Tests; + +internal static class Constants +{ + public const string AutoGeneratedStatement = """ + //------------------------------------------------------------------------------ + // + // This code was generated by a tool. + // + // Changes to this file may cause incorrect behavior and will be lost if + // the code is regenerated. + // + //------------------------------------------------------------------------------ + + """; +} diff --git a/tests/MapperSourceGen.SourceGenerator.Tests/Fixture.cs b/tests/MapperSourceGen.SourceGenerator.Tests/Fixture.cs new file mode 100644 index 0000000..9f2caea --- /dev/null +++ b/tests/MapperSourceGen.SourceGenerator.Tests/Fixture.cs @@ -0,0 +1,39 @@ +namespace MapperSourceGen.SourceGenerator.Tests; + +using System.Reflection; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +internal static class Fixture +{ + private static readonly Type[] RequiredAssemblies = + [ + typeof(Binder), + typeof(MapperAttribute) + ]; + + private static IEnumerable AssemblyReferencesForCodeGen => + AppDomain.CurrentDomain + .GetAssemblies() + .Concat(RequiredAssemblies.Select(s => s.Assembly)) + .Distinct() + .Where(w => !w.IsDynamic) + .Select(s => MetadataReference.CreateFromFile(s.Location)); + + public static Task VerifyGenerateSourcesAsync(string source, params IIncrementalGenerator[] generators) + { + var syntaxTree = CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default); + + var compilation = CSharpCompilation.Create( + "compilation", + [syntaxTree], + AssemblyReferencesForCodeGen, + new CSharpCompilationOptions(OutputKind.ConsoleApplication)); + + var driver = CSharpGeneratorDriver.Create(generators); + var runner = driver.RunGenerators(compilation); + var verify = Verify(runner); + + return verify; + } +} diff --git a/tests/MapperSourceGen.SourceGenerator.Tests/MapperGeneratorFacts.cs b/tests/MapperSourceGen.SourceGenerator.Tests/MapperGeneratorFacts.cs new file mode 100644 index 0000000..01b985c --- /dev/null +++ b/tests/MapperSourceGen.SourceGenerator.Tests/MapperGeneratorFacts.cs @@ -0,0 +1,20 @@ +namespace MapperSourceGen.SourceGenerator.Tests; + +public sealed class MapperGeneratorFacts +{ + [Fact] + public Task Should_Emit_Dto_And_Mapper() + { + const string source = """ + namespace MapperSourceGen.SourceGenerator.Tests; + + [Mapper] + partial class MyEntity + { + public int Id { get; set; } + } + """; + + return Fixture.VerifyGenerateSourcesAsync(source, new MapperGenerator()); + } +} diff --git a/tests/MapperSourceGen.SourceGenerator.Tests/MapperSourceGen.SourceGenerator.Tests.csproj b/tests/MapperSourceGen.SourceGenerator.Tests/MapperSourceGen.SourceGenerator.Tests.csproj new file mode 100644 index 0000000..d73b885 --- /dev/null +++ b/tests/MapperSourceGen.SourceGenerator.Tests/MapperSourceGen.SourceGenerator.Tests.csproj @@ -0,0 +1,51 @@ + + + + net8.0 + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + diff --git a/tests/MapperSourceGen.SourceGenerator.Tests/ModuleInitializer.cs b/tests/MapperSourceGen.SourceGenerator.Tests/ModuleInitializer.cs new file mode 100644 index 0000000..aff3127 --- /dev/null +++ b/tests/MapperSourceGen.SourceGenerator.Tests/ModuleInitializer.cs @@ -0,0 +1,18 @@ +namespace MapperSourceGen.SourceGenerator.Tests; + +using System.Runtime.CompilerServices; +using VerifyTests.DiffPlex; + +public static class ModuleInitializer +{ + #pragma warning disable CA2255 + [ModuleInitializer] + #pragma warning restore CA2255 + public static void Initialize() + { + DerivePathInfo((file, _, type, method) => new PathInfo(Path.Combine(Path.GetDirectoryName(file)!, "ref"), type.Name, method.Name)); + + VerifySourceGenerators.Initialize(); + VerifyDiffPlex.Initialize(OutputType.Compact); + } +} diff --git a/tests/MapperSourceGen.SourceGenerator.Tests/packages.lock.json b/tests/MapperSourceGen.SourceGenerator.Tests/packages.lock.json new file mode 100644 index 0000000..6d16fff --- /dev/null +++ b/tests/MapperSourceGen.SourceGenerator.Tests/packages.lock.json @@ -0,0 +1,254 @@ +{ + "version": 2, + "dependencies": { + "net8.0": { + "coverlet.collector": { + "type": "Direct", + "requested": "[6.0.4, )", + "resolved": "6.0.4", + "contentHash": "lkhqpF8Pu2Y7IiN7OntbsTtdbpR1syMsm2F3IgX6ootA4ffRqWL5jF7XipHuZQTdVuWG/gVAAcf8mjk8Tz0xPg==" + }, + "coverlet.msbuild": { + "type": "Direct", + "requested": "[6.0.4, )", + "resolved": "6.0.4", + "contentHash": "Qa7Hg+wrOMDKpXVn2dw4Wlun490bIWsFW0fdNJQFJLZnbU27MCP0HJ2mPgS+3EQBQUb0zKlkwiQzP+j38Hc3Iw==" + }, + "JunitXml.TestLogger": { + "type": "Direct", + "requested": "[6.1.0, )", + "resolved": "6.1.0", + "contentHash": "a3ciawoHOzqcry7yS5z9DerNyF9QZi6fEZZJPILSy6Noj6+r8Ydma+cENA6wvivXDCblpXxw72wWT9QApNy/0w==" + }, + "Microsoft.CodeAnalysis.Common": { + "type": "Direct", + "requested": "[4.11.0, )", + "resolved": "4.11.0", + "contentHash": "djf8ujmqYImFgB04UGtcsEhHrzVqzHowS+EEl/Yunc5LdrYrZhGBWUTXoCF0NzYXJxtfuD+UVQarWpvrNc94Qg==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.3.4", + "System.Collections.Immutable": "8.0.0", + "System.Reflection.Metadata": "8.0.0" + } + }, + "Microsoft.CodeAnalysis.CSharp": { + "type": "Direct", + "requested": "[4.11.0, 4.11.0]", + "resolved": "4.11.0", + "contentHash": "6XYi2EusI8JT4y2l/F3VVVS+ISoIX9nqHsZRaG6W5aFeJ5BEuBosHfT/ABb73FN0RZ1Z3cj2j7cL28SToJPXOw==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.3.4", + "Microsoft.CodeAnalysis.Common": "[4.11.0]", + "System.Collections.Immutable": "8.0.0", + "System.Reflection.Metadata": "8.0.0" + } + }, + "Microsoft.NET.Test.Sdk": { + "type": "Direct", + "requested": "[17.14.1, )", + "resolved": "17.14.1", + "contentHash": "HJKqKOE+vshXra2aEHpi2TlxYX7Z9VFYkr+E5rwEvHC8eIXiyO+K9kNm8vmNom3e2rA56WqxU+/N9NJlLGXsJQ==", + "dependencies": { + "Microsoft.CodeCoverage": "17.14.1", + "Microsoft.TestPlatform.TestHost": "17.14.1" + } + }, + "Verify.DiffPlex": { + "type": "Direct", + "requested": "[3.1.2, )", + "resolved": "3.1.2", + "contentHash": "ySaQ+MffcDfGWzBXB9UHppEGBqzl0L+2CxZcT04xQ3gugsN5AAjBPHkt75Ca61PlAeZCyty/p/Q9ZwaQjNOoTg==", + "dependencies": { + "DiffPlex": "1.7.2", + "Verify": "27.0.0" + } + }, + "Verify.SourceGenerators": { + "type": "Direct", + "requested": "[2.5.0, )", + "resolved": "2.5.0", + "contentHash": "XhAg+fJDPXDH7Ajv/J4Hv8ls0zoeK0LqjZIiOT+quwxOqdplcTuqdPx1+4p1qvYzpTdwkLxyGiIA76MzCljyAQ==", + "dependencies": { + "Verify": "26.5.0" + } + }, + "Verify.Xunit": { + "type": "Direct", + "requested": "[30.5.0, )", + "resolved": "30.5.0", + "contentHash": "S+vPvbWgcZSR/eF5O8OAlz8uXhB2Dr2uKjUKbPLa9FtC5ay0hueYGzKshHmRZJ1jay88VE1Kd2ECPNhb8p3Uyg==", + "dependencies": { + "Argon": "0.30.1", + "DiffEngine": "16.2.3", + "SimpleInfoName": "3.1.2", + "Verify": "30.5.0", + "xunit.abstractions": "2.0.3", + "xunit.extensibility.execution": "2.9.3" + } + }, + "xunit": { + "type": "Direct", + "requested": "[2.9.3, )", + "resolved": "2.9.3", + "contentHash": "TlXQBinK35LpOPKHAqbLY4xlEen9TBafjs0V5KnA4wZsoQLQJiirCR4CbIXvOH8NzkW4YeJKP5P/Bnrodm0h9Q==", + "dependencies": { + "xunit.analyzers": "1.18.0", + "xunit.assert": "2.9.3", + "xunit.core": "[2.9.3]" + } + }, + "xunit.runner.visualstudio": { + "type": "Direct", + "requested": "[3.1.3, )", + "resolved": "3.1.3", + "contentHash": "go7e81n/UI3LeNqoJIJ3thkS4JfJtiQnDbAxLh09JkJqoHthnfbLS5p68s4/Bm12B9umkoYSB5SaDr68hZNleg==" + }, + "Argon": { + "type": "Transitive", + "resolved": "0.30.1", + "contentHash": "kjKnBzxJ1Xp4Sh9B7inrP1YjefXH4X8hV4/J5EoDKloog09Kp4KUVoJS8xxYfUbUzJ+Xe5PKZm3hj5pi4ZuCZw==" + }, + "DiffEngine": { + "type": "Transitive", + "resolved": "16.2.3", + "contentHash": "QWnG0MR3//Ss0G0N9mIfe1HLOrOIRZqau0AOiLt9Gm53ZQf/TLvzoccTkczEW5ACkbhRY5m+p+W7bzFVln2GDw==", + "dependencies": { + "EmptyFiles": "8.10.1", + "System.Management": "8.0.0" + } + }, + "DiffPlex": { + "type": "Transitive", + "resolved": "1.7.2", + "contentHash": "qJEjdxEDBWSFZGB8paBB9HDeJXHGlHlOXeGX3kbTuXWuOsgv2iSAEOOzo5V1/B39Vcxr9IVVrNKewRcX+rsn4g==" + }, + "EmptyFiles": { + "type": "Transitive", + "resolved": "8.10.1", + "contentHash": "vhLPAqdKuo2qjVkrJbCyacGXO9XTha7G1R5amw44m877FDR/gqFjCfdncj8VyHAC6eNqrCXgYTbHJGO5+l3TJg==" + }, + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Transitive", + "resolved": "3.3.4", + "contentHash": "AxkxcPR+rheX0SmvpLVIGLhOUXAKG56a64kV9VQZ4y9gR9ZmPXnqZvHJnmwLSwzrEP6junUF11vuc+aqo5r68g==" + }, + "Microsoft.CodeCoverage": { + "type": "Transitive", + "resolved": "17.14.1", + "contentHash": "pmTrhfFIoplzFVbhVwUquT+77CbGH+h4/3mBpdmIlYtBi9nAB+kKI6dN3A/nV4DFi3wLLx/BlHIPK+MkbQ6Tpg==" + }, + "Microsoft.TestPlatform.ObjectModel": { + "type": "Transitive", + "resolved": "17.14.1", + "contentHash": "xTP1W6Mi6SWmuxd3a+jj9G9UoC850WGwZUps1Wah9r1ZxgXhdJfj1QqDLJkFjHDCvN42qDL2Ps5KjQYWUU0zcQ==", + "dependencies": { + "System.Reflection.Metadata": "8.0.0" + } + }, + "Microsoft.TestPlatform.TestHost": { + "type": "Transitive", + "resolved": "17.14.1", + "contentHash": "d78LPzGKkJwsJXAQwsbJJ7LE7D1wB+rAyhHHAaODF+RDSQ0NgMjDFkSA1Djw18VrxO76GlKAjRUhl+H8NL8Z+Q==", + "dependencies": { + "Microsoft.TestPlatform.ObjectModel": "17.14.1", + "Newtonsoft.Json": "13.0.3" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "SimpleInfoName": { + "type": "Transitive", + "resolved": "3.1.2", + "contentHash": "/OoEZQxSW6DeTJ9nfrg8BLCOCWpxBiWHV4NkG3t+Xpe8tvzm7yCwKwxkhpauMl3fg9OjlIjJMKX61H6VavLkrw==" + }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "WTlRjL6KWIMr/pAaq3rYqh0TJlzpouaQ/W1eelssHgtlwHAH25jXTkUphTYx9HaIIf7XA6qs/0+YhtLEQRkJ+Q==" + }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "AurL6Y5BA1WotzlEvVaIDpqzpIPvYnnldxru8oXJU2yFxFUy3+pNXjXd1ymO+RA0rq0+590Q8gaz2l3Sr7fmqg==" + }, + "System.Management": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "jrK22i5LRzxZCfGb+tGmke2VH7oE0DvcDlJ1HAKYU8cPmD8XnpUT0bYn2Gy98GEhGjtfbR/sxKTVb+dE770pfA==", + "dependencies": { + "System.CodeDom": "8.0.0" + } + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "ptvgrFh7PvWI8bcVqG5rsA/weWM09EnthFHR5SCnS6IN+P4mj6rE1lBDC4U8HL9/57htKAqy4KQ3bBj84cfYyQ==", + "dependencies": { + "System.Collections.Immutable": "8.0.0" + } + }, + "Verify": { + "type": "Transitive", + "resolved": "30.5.0", + "contentHash": "AO3l7Rmw8Ry8rpVMQ98Q3MVO2G0KGiXlxyOPAYuE7NeN33JzNLJSjri/fMQDo3sm4aIwBjWbomssy2EbyJBdrg==", + "dependencies": { + "Argon": "0.30.1", + "DiffEngine": "16.2.3", + "SimpleInfoName": "3.1.2" + } + }, + "xunit.abstractions": { + "type": "Transitive", + "resolved": "2.0.3", + "contentHash": "pot1I4YOxlWjIb5jmwvvQNbTrZ3lJQ+jUGkGjWE3hEFM0l5gOnBWS+H3qsex68s5cO52g+44vpGzhAt+42vwKg==" + }, + "xunit.analyzers": { + "type": "Transitive", + "resolved": "1.18.0", + "contentHash": "OtFMHN8yqIcYP9wcVIgJrq01AfTxijjAqVDy/WeQVSyrDC1RzBWeQPztL49DN2syXRah8TYnfvk035s7L95EZQ==" + }, + "xunit.assert": { + "type": "Transitive", + "resolved": "2.9.3", + "contentHash": "/Kq28fCE7MjOV42YLVRAJzRF0WmEqsmflm0cfpMjGtzQ2lR5mYVj1/i0Y8uDAOLczkL3/jArrwehfMD0YogMAA==" + }, + "xunit.core": { + "type": "Transitive", + "resolved": "2.9.3", + "contentHash": "BiAEvqGvyme19wE0wTKdADH+NloYqikiU0mcnmiNyXaF9HyHmE6sr/3DC5vnBkgsWaE6yPyWszKSPSApWdRVeQ==", + "dependencies": { + "xunit.extensibility.core": "[2.9.3]", + "xunit.extensibility.execution": "[2.9.3]" + } + }, + "xunit.extensibility.core": { + "type": "Transitive", + "resolved": "2.9.3", + "contentHash": "kf3si0YTn2a8J8eZNb+zFpwfoyvIrQ7ivNk5ZYA5yuYk1bEtMe4DxJ2CF/qsRgmEnDr7MnW1mxylBaHTZ4qErA==", + "dependencies": { + "xunit.abstractions": "2.0.3" + } + }, + "xunit.extensibility.execution": { + "type": "Transitive", + "resolved": "2.9.3", + "contentHash": "yMb6vMESlSrE3Wfj7V6cjQ3S4TXdXpRqYeNEI3zsX31uTsGMJjEw6oD5F5u1cHnMptjhEECnmZSsPxB6ChZHDQ==", + "dependencies": { + "xunit.extensibility.core": "[2.9.3]" + } + }, + "mappersourcegen": { + "type": "Project" + }, + "mappersourcegen.sourcegenerator": { + "type": "Project", + "dependencies": { + "Microsoft.CodeAnalysis.CSharp": "[4.11.0, 4.11.0]" + } + } + } + } +} \ No newline at end of file diff --git a/tests/MapperSourceGen.SourceGenerator.Tests/ref/MapperGeneratorFacts.Should_Emit_Dto_And_Mapper#MapperSourceGen.SourceGenerator.Tests.MyEntityDto.g.verified.cs b/tests/MapperSourceGen.SourceGenerator.Tests/ref/MapperGeneratorFacts.Should_Emit_Dto_And_Mapper#MapperSourceGen.SourceGenerator.Tests.MyEntityDto.g.verified.cs new file mode 100644 index 0000000..11dd2a9 --- /dev/null +++ b/tests/MapperSourceGen.SourceGenerator.Tests/ref/MapperGeneratorFacts.Should_Emit_Dto_And_Mapper#MapperSourceGen.SourceGenerator.Tests.MyEntityDto.g.verified.cs @@ -0,0 +1,12 @@ +//HintName: MapperSourceGen.SourceGenerator.Tests.MyEntityDto.g.cs +// +#pragma warning disable +#nullable enable +namespace MapperSourceGen.SourceGenerator.Tests +{ + /// + internal partial class MyEntityDto + { + public int Id { get; set; } + } +} \ No newline at end of file diff --git a/tests/MapperSourceGen.SourceGenerator.Tests/ref/MapperGeneratorFacts.Should_Emit_Dto_And_Mapper#MapperSourceGen.SourceGenerator.Tests.MyEntityMapper.g.verified.cs b/tests/MapperSourceGen.SourceGenerator.Tests/ref/MapperGeneratorFacts.Should_Emit_Dto_And_Mapper#MapperSourceGen.SourceGenerator.Tests.MyEntityMapper.g.verified.cs new file mode 100644 index 0000000..09eb290 --- /dev/null +++ b/tests/MapperSourceGen.SourceGenerator.Tests/ref/MapperGeneratorFacts.Should_Emit_Dto_And_Mapper#MapperSourceGen.SourceGenerator.Tests.MyEntityMapper.g.verified.cs @@ -0,0 +1,28 @@ +//HintName: MapperSourceGen.SourceGenerator.Tests.MyEntityMapper.g.cs +// +#pragma warning disable +#nullable enable +namespace MapperSourceGen.SourceGenerator.Tests +{ + /// + internal partial class MyEntityMapper + { + public static MapperSourceGen.SourceGenerator.Tests.MyEntity ToModel(MapperSourceGen.SourceGenerator.Tests.MyEntityDto source) + { + ArgumentNullException.ThrowIfNull(nameof(source)); + return new MapperSourceGen.SourceGenerator.Tests.MyEntity() + { + Id = source.Id, + }; + } + + public static MapperSourceGen.SourceGenerator.Tests.MyEntityDto ToDto(MapperSourceGen.SourceGenerator.Tests.MyEntity source) + { + ArgumentNullException.ThrowIfNull(nameof(source)); + return new MapperSourceGen.SourceGenerator.Tests.MyEntityDto() + { + Id = source.Id, + }; + } + } +} \ No newline at end of file