Compare commits

..

42 Commits

Author SHA1 Message Date
CodeLiturgy 4e80156708 Remove EfGenerator, migrated to separate project 2022-09-05 23:18:15 +01:00
CodeLiturgy b8637c94ab Using GetOne and GetMany templates 2022-09-05 22:42:51 +01:00
CodeLiturgy 32bf670b9e Get one template working 2022-09-05 03:58:28 +01:00
CodeLiturgy 9743ff3740 Post-refactor 2022-09-05 02:16:32 +01:00
CodeLiturgy 8a664db1f7 pre-refacotring 2022-09-05 02:16:08 +01:00
CodeLiturgy 1de6350c7d Prepare getone template 2022-09-05 02:01:17 +01:00
CodeLiturgy fa3dfc8e86 Repair done 2022-09-05 01:42:08 +01:00
CodeLiturgy 4c4413da41 Before trying to replace with type syntax 2022-09-05 00:33:19 +01:00
CodeLiturgy 9211cde83c Use new model for ef method 2022-09-04 22:11:04 +01:00
CodeLiturgy 3917d231a2 Add generator attribute source 2022-09-04 19:07:59 +01:00
CodeLiturgy 9257d389ba add EfMethods directory 2022-09-04 16:36:46 +01:00
CodeLiturgy 07c8fc4853 Template ok 2022-09-02 00:07:56 +01:00
Wvader e23d932d13 update template using nameof to specify id 2022-08-29 17:30:32 +01:00
CodeLiturgy 654a54d5ef Update template working 2022-08-29 01:41:05 +01:00
CodeLiturgy f419c0f8ea ok state before introducing update 2022-08-28 19:39:47 +01:00
CodeLiturgy ca95c87839 Add model refactored 2022-08-28 18:14:08 +01:00
CodeLiturgy 12d5839f16 ok 2022-08-28 18:00:04 +01:00
CodeLiturgy fabfa562cf ok 2022-08-28 17:59:55 +01:00
CodeLiturgy 4d9cc4b029 End merge Add with template and Update 2022-08-27 21:01:52 +01:00
CodeLiturgy c34da98ad8 ef update wip 2022-08-27 20:56:19 +01:00
Wvader e232fa6e76 Add template working 2022-08-27 04:19:43 +01:00
Wvader 340a89bbd2 Support Ef Add method 2022-08-24 17:56:44 +01:00
Wvader 1400bd08e0 Prepare for Add/Update generators 2022-08-23 17:48:47 +01:00
Wvader 7de9b48c69 Inject empty constructor 2022-08-22 03:14:14 +01:00
Wvader a6f5116656 Supporting 'friendly constructors' 2022-08-21 22:15:00 +01:00
Wvader aa5b01cdc4 Fix UseUpdate usage 2022-08-20 03:47:55 +01:00
Wvader f7963d2d7e Addapt MapTo to support multiple mappings 2022-08-18 16:48:44 +01:00
Wvader ca82e6fb17 Fix docker errors 2022-08-13 06:33:39 +01:00
Wvader 77603643b4 Bump Nerdbank.GitVersioning 2022-08-13 01:15:25 +01:00
Wvader c241fd4cb5 summer changes 2022-08-02 22:02:26 +01:00
Wvader ffc3dc7729 stuff 2021-12-24 17:06:06 +00:00
Wvader 60ccbd53ca k 2021-12-12 00:10:13 +00:00
Wvader 5faebd9141 work with field 2021-12-11 23:54:21 +00:00
Wvader 768dbb4532 Release 1 2021-12-11 14:22:26 +00:00
Wvader 2b0b03bb60 g 2021-12-11 14:12:16 +00:00
Wvader 18f4ff408a x 2021-12-11 14:09:11 +00:00
Wvader 31ac58f274 tru fix 2021-12-11 11:51:54 +00:00
Wvader 9470afb737 k 2021-12-10 00:04:41 +00:00
Wvader 63b2bcb359 , 2021-12-08 18:29:16 +00:00
Wvader 4bf3599d79 K 2021-12-08 18:20:51 +00:00
Wvader 735fd44ee1 bump 2021-12-08 18:07:51 +00:00
Wvader 4077dfd692 Handle structs and updates 2021-12-08 17:58:21 +00:00
91 changed files with 6734 additions and 5663 deletions

View File

@ -1,72 +1,72 @@
[*.{appxmanifest,asax,ascx,aspx,axaml,build,cg,cginc,compute,cs,cshtml,dtd,fs,fsi,fsscript,fsx,hlsl,hlsli,hlslinc,master,ml,mli,nuspec,paml,razor,resw,resx,skin,usf,ush,vb,xaml,xamlx,xoml,xsd}]
indent_style = space
indent_size = 4
tab_width = 4
[*]
# Microsoft .NET properties
csharp_new_line_before_members_in_object_initializers = false
csharp_preferred_modifier_order = public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion
csharp_space_after_cast = false
csharp_style_var_elsewhere = true:suggestion
csharp_style_var_for_built_in_types = true:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion
dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none
dotnet_style_parentheses_in_other_binary_operators = never_if_unnecessary:none
dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
dotnet_style_predefined_type_for_member_access = true:suggestion
dotnet_style_qualification_for_event = false:suggestion
dotnet_style_qualification_for_field = false:suggestion
dotnet_style_qualification_for_method = false:suggestion
dotnet_style_qualification_for_property = false:suggestion
dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
# ReSharper properties
resharper_autodetect_indent_settings = true
resharper_blank_lines_after_control_transfer_statements = 1
resharper_blank_lines_after_multiline_statements = 1
resharper_blank_lines_around_block_case_section = 1
resharper_blank_lines_around_multiline_case_section = 1
resharper_blank_lines_around_single_line_auto_property = 1
resharper_blank_lines_around_single_line_local_method = 1
resharper_blank_lines_around_single_line_property = 1
resharper_braces_for_for = required
resharper_braces_for_foreach = required
resharper_braces_for_ifelse = required
resharper_braces_for_while = required
resharper_csharp_blank_lines_around_single_line_invocable = 1
resharper_csharp_empty_block_style = together_same_line
resharper_csharp_keep_blank_lines_in_code = 1
resharper_csharp_keep_blank_lines_in_declarations = 1
resharper_csharp_max_line_length = 180
resharper_csharp_wrap_lines = false
resharper_local_function_body = expression_body
resharper_method_or_operator_body = expression_body
resharper_place_accessorholder_attribute_on_same_line = false
resharper_place_field_attribute_on_same_line = false
resharper_space_after_cast = false
resharper_space_within_single_line_array_initializer_braces = true
resharper_use_indent_from_vs = false
resharper_xmldoc_indent_text = ZeroIndent
# ReSharper inspection severities
resharper_arguments_style_literal_highlighting = none
resharper_arguments_style_named_expression_highlighting = none
resharper_arguments_style_other_highlighting = none
resharper_arrange_redundant_parentheses_highlighting = hint
resharper_arrange_this_qualifier_highlighting = hint
resharper_arrange_type_member_modifiers_highlighting = hint
resharper_arrange_type_modifiers_highlighting = hint
resharper_built_in_type_reference_style_for_member_access_highlighting = hint
resharper_built_in_type_reference_style_highlighting = hint
resharper_class_never_instantiated_global_highlighting = none
resharper_redundant_base_qualifier_highlighting = warning
resharper_suggest_var_or_type_built_in_types_highlighting = hint
resharper_suggest_var_or_type_elsewhere_highlighting = hint
resharper_suggest_var_or_type_simple_types_highlighting = hint
resharper_web_config_module_not_resolved_highlighting = warning
resharper_web_config_type_not_resolved_highlighting = warning
resharper_web_config_wrong_module_highlighting = warning
[*.{appxmanifest,asax,ascx,aspx,axaml,build,cg,cginc,compute,cs,cshtml,dtd,fs,fsi,fsscript,fsx,hlsl,hlsli,hlslinc,master,ml,mli,nuspec,paml,razor,resw,resx,skin,usf,ush,vb,xaml,xamlx,xoml,xsd}]
indent_style = space
indent_size = 4
tab_width = 4
[*]
# Microsoft .NET properties
csharp_new_line_before_members_in_object_initializers = false
csharp_preferred_modifier_order = public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion
csharp_space_after_cast = false
csharp_style_var_elsewhere = true:suggestion
csharp_style_var_for_built_in_types = true:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion
dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none
dotnet_style_parentheses_in_other_binary_operators = never_if_unnecessary:none
dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
dotnet_style_predefined_type_for_member_access = true:suggestion
dotnet_style_qualification_for_event = false:suggestion
dotnet_style_qualification_for_field = false:suggestion
dotnet_style_qualification_for_method = false:suggestion
dotnet_style_qualification_for_property = false:suggestion
dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
# ReSharper properties
resharper_autodetect_indent_settings = true
resharper_blank_lines_after_control_transfer_statements = 1
resharper_blank_lines_after_multiline_statements = 1
resharper_blank_lines_around_block_case_section = 1
resharper_blank_lines_around_multiline_case_section = 1
resharper_blank_lines_around_single_line_auto_property = 1
resharper_blank_lines_around_single_line_local_method = 1
resharper_blank_lines_around_single_line_property = 1
resharper_braces_for_for = required
resharper_braces_for_foreach = required
resharper_braces_for_ifelse = required
resharper_braces_for_while = required
resharper_csharp_blank_lines_around_single_line_invocable = 1
resharper_csharp_empty_block_style = together_same_line
resharper_csharp_keep_blank_lines_in_code = 1
resharper_csharp_keep_blank_lines_in_declarations = 1
resharper_csharp_max_line_length = 180
resharper_csharp_wrap_lines = false
resharper_local_function_body = expression_body
resharper_method_or_operator_body = expression_body
resharper_place_accessorholder_attribute_on_same_line = false
resharper_place_field_attribute_on_same_line = false
resharper_space_after_cast = false
resharper_space_within_single_line_array_initializer_braces = true
resharper_use_indent_from_vs = false
resharper_xmldoc_indent_text = ZeroIndent
# ReSharper inspection severities
resharper_arguments_style_literal_highlighting = none
resharper_arguments_style_named_expression_highlighting = none
resharper_arguments_style_other_highlighting = none
resharper_arrange_redundant_parentheses_highlighting = hint
resharper_arrange_this_qualifier_highlighting = hint
resharper_arrange_type_member_modifiers_highlighting = hint
resharper_arrange_type_modifiers_highlighting = hint
resharper_built_in_type_reference_style_for_member_access_highlighting = hint
resharper_built_in_type_reference_style_highlighting = hint
resharper_class_never_instantiated_global_highlighting = none
resharper_redundant_base_qualifier_highlighting = warning
resharper_suggest_var_or_type_built_in_types_highlighting = hint
resharper_suggest_var_or_type_elsewhere_highlighting = hint
resharper_suggest_var_or_type_simple_types_highlighting = hint
resharper_web_config_module_not_resolved_highlighting = warning
resharper_web_config_type_not_resolved_highlighting = warning
resharper_web_config_wrong_module_highlighting = warning

View File

@ -1,28 +1,28 @@
name: Build and Test
on:
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0 # avoid shallow clone so nbgv can do its work.
- uses: dotnet/nbgv@v0.4.0
with:
setAllVars: true
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: 5.0.101
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore
- name: Test
run: dotnet test --no-build --verbosity normal
name: Build and Test
on:
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0 # avoid shallow clone so nbgv can do its work.
- uses: dotnet/nbgv@v0.4.0
with:
setAllVars: true
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: 5.0.101
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore
- name: Test
run: dotnet test --no-build --verbosity normal

View File

@ -1,37 +1,37 @@
name: Publish Packages
on:
push:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0 # avoid shallow clone so nbgv can do its work.
- uses: dotnet/nbgv@v0.4.0
with:
setAllVars: true
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: 5.0.101
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --configuration Release --no-restore /p:PublicRelease=true
- name: Test
run: dotnet test --configuration Release --no-build --verbosity normal
- name: Publish MapTo
uses: brandedoutcast/publish-nuget@v2.5.5
with:
PROJECT_FILE_PATH: src/MapTo/MapTo.csproj
NUGET_KEY: ${{secrets.NUGET_API_KEY}}
NUGET_SOURCE: https://api.nuget.org
TAG_COMMIT: false
INCLUDE_SYMBOLS: true
VERSION_STATIC: ${{env.NBGV_SemVer1}}
name: Publish Packages
on:
push:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0 # avoid shallow clone so nbgv can do its work.
- uses: dotnet/nbgv@v0.4.0
with:
setAllVars: true
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: 5.0.101
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --configuration Release --no-restore /p:PublicRelease=true
- name: Test
run: dotnet test --configuration Release --no-build --verbosity normal
- name: Publish MapTo
uses: brandedoutcast/publish-nuget@v2.5.5
with:
PROJECT_FILE_PATH: src/BlueWest.MapTo/BlueWest.MapTo.csproj
NUGET_KEY: ${{secrets.NUGET_API_KEY}}
NUGET_SOURCE: https://api.nuget.org
TAG_COMMIT: false
INCLUDE_SYMBOLS: true
VERSION_STATIC: ${{env.NBGV_SemVer1}}

884
.gitignore vendored
View File

@ -1,442 +1,442 @@
## 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/
[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/
# 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, .xml, .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/
##
## Visual studio for Mac
##
# globs
Makefile.in
*.userprefs
*.usertasks
config.make
config.status
aclocal.m4
install-sh
autom4te.cache/
*.tar.gz
tarballs/
test-results/
# Mac bundle stuff
*.dmg
*.app
# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
# Windows thumbnail cache files
Thumbs.db
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# JetBrains Rider
.idea/
*.sln.iml
##
## Visual Studio Code
##
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
## 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/
[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/
# 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, .xml, .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/
##
## Visual studio for Mac
##
# globs
Makefile.in
*.userprefs
*.usertasks
config.make
config.status
aclocal.m4
install-sh
autom4te.cache/
*.tar.gz
tarballs/
test-results/
# Mac bundle stuff
*.dmg
*.app
# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
# Windows thumbnail cache files
Thumbs.db
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# JetBrains Rider
.idea/
*.sln.iml
##
## Visual Studio Code
##
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json

40
LICENSE
View File

@ -1,21 +1,21 @@
MIT License
Copyright (c) 2020 Mohammadreza Taikandi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
MIT License
Copyright (c) 2020 Mohammadreza Taikandi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,34 +1,34 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapTo", "src\MapTo\MapTo.csproj", "{4DB371AC-48D0-4F01-8EF3-7707D06EF0A7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapToTests", "test\MapTo.Tests\MapTo.Tests.csproj", "{797DA57B-AC7E-468B-8799-44C5A574C0E3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestConsoleApp", "test\TestConsoleApp\TestConsoleApp.csproj", "{5BE2551A-9EF9-42FA-B6D1-5B5E6A90CC85}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapTo.Integration.Tests", "test\MapTo.Integration.Tests\MapTo.Integration.Tests.csproj", "{23B46FDF-6A1E-4287-88C9-C8C5D7EECB8C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{4DB371AC-48D0-4F01-8EF3-7707D06EF0A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4DB371AC-48D0-4F01-8EF3-7707D06EF0A7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4DB371AC-48D0-4F01-8EF3-7707D06EF0A7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4DB371AC-48D0-4F01-8EF3-7707D06EF0A7}.Release|Any CPU.Build.0 = Release|Any CPU
{797DA57B-AC7E-468B-8799-44C5A574C0E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{797DA57B-AC7E-468B-8799-44C5A574C0E3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{797DA57B-AC7E-468B-8799-44C5A574C0E3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{797DA57B-AC7E-468B-8799-44C5A574C0E3}.Release|Any CPU.Build.0 = Release|Any CPU
{5BE2551A-9EF9-42FA-B6D1-5B5E6A90CC85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5BE2551A-9EF9-42FA-B6D1-5B5E6A90CC85}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5BE2551A-9EF9-42FA-B6D1-5B5E6A90CC85}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5BE2551A-9EF9-42FA-B6D1-5B5E6A90CC85}.Release|Any CPU.Build.0 = Release|Any CPU
{23B46FDF-6A1E-4287-88C9-C8C5D7EECB8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{23B46FDF-6A1E-4287-88C9-C8C5D7EECB8C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{23B46FDF-6A1E-4287-88C9-C8C5D7EECB8C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{23B46FDF-6A1E-4287-88C9-C8C5D7EECB8C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapTo", "src\BlueWest.MapTo\BlueWest.MapTo.csproj", "{4DB371AC-48D0-4F01-8EF3-7707D06EF0A7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapToTests", "test\MapTo.Tests\MapTo.Tests.csproj", "{797DA57B-AC7E-468B-8799-44C5A574C0E3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestConsoleApp", "test\TestConsoleApp\TestConsoleApp.csproj", "{5BE2551A-9EF9-42FA-B6D1-5B5E6A90CC85}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapTo.Integration.Tests", "test\MapTo.Integration.Tests\MapTo.Integration.Tests.csproj", "{23B46FDF-6A1E-4287-88C9-C8C5D7EECB8C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{4DB371AC-48D0-4F01-8EF3-7707D06EF0A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4DB371AC-48D0-4F01-8EF3-7707D06EF0A7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4DB371AC-48D0-4F01-8EF3-7707D06EF0A7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4DB371AC-48D0-4F01-8EF3-7707D06EF0A7}.Release|Any CPU.Build.0 = Release|Any CPU
{797DA57B-AC7E-468B-8799-44C5A574C0E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{797DA57B-AC7E-468B-8799-44C5A574C0E3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{797DA57B-AC7E-468B-8799-44C5A574C0E3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{797DA57B-AC7E-468B-8799-44C5A574C0E3}.Release|Any CPU.Build.0 = Release|Any CPU
{5BE2551A-9EF9-42FA-B6D1-5B5E6A90CC85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5BE2551A-9EF9-42FA-B6D1-5B5E6A90CC85}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5BE2551A-9EF9-42FA-B6D1-5B5E6A90CC85}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5BE2551A-9EF9-42FA-B6D1-5B5E6A90CC85}.Release|Any CPU.Build.0 = Release|Any CPU
{23B46FDF-6A1E-4287-88C9-C8C5D7EECB8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{23B46FDF-6A1E-4287-88C9-C8C5D7EECB8C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{23B46FDF-6A1E-4287-88C9-C8C5D7EECB8C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{23B46FDF-6A1E-4287-88C9-C8C5D7EECB8C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@ -1,6 +1,6 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=Mapto/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=paramref/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Shouldly/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=typeparam/@EntryIndexedValue">True</s:Boolean>
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=Mapto/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=paramref/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Shouldly/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=typeparam/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Usings/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

180
README.md
View File

@ -1,91 +1,91 @@
# MapTo
[![Nuget](https://img.shields.io/nuget/v/mapto?logo=nuget)](https://www.nuget.org/packages/MapTo/)
![Publish Packages](https://github.com/mrtaikandi/MapTo/workflows/Publish%20Packages/badge.svg)
A convention based object to object mapper using [Roslyn source generator](https://github.com/dotnet/roslyn/blob/master/docs/features/source-generators.md).
MapTo is a library to programmatically generate the necessary code to map one object to another during compile-time, eliminating the need to use reflection to map objects and make it much faster in runtime. It provides compile-time safety checks and ease of use by leveraging extension methods.
## Installation
```
dotnet add package MapTo --prerelease
```
## Usage
MapTo relies on a set of attributes to instruct it on how to generate the mappings. To start, declare the destination class as `partial` and annotate it with `MapFrom` attribute. As its name implies, `MapFrom` attribute tells the library what the source class you want to map from is.
```c#
using MapTo;
namespace App.ViewModels
{
[MapFrom(typeof(App.Data.Models.User))]
public partial class UserViewModel
{
public string FirstName { get; }
public string LastName { get; }
[IgnoreProperty]
public string FullName { get; set; }
}
}
```
To get an instance of `UserViewModel` from the `User` class, you can use any of the following methods:
```c#
var user = new User(id: 10) { FirstName = "John", LastName = "Doe" };
var vm = user.ToUserViewModel(); // A generated extension method for User class.
// OR
vm = new UserViewModel(user); // A generated contructor.
// OR
vm = UserViewModel.From(user); // A generated factory method.
```
> Please refer to [sample console app](https://github.com/mrtaikandi/MapTo/tree/master/test/TestConsoleApp) for a more complete example.
## Available Attributes
### IgnoreProperty
By default, MapTo will include all properties with the same name (case-sensitive), whether read-only or not, in the mapping unless annotating them with the `IgnoreProperty` attribute.
```c#
[IgnoreProperty]
public string FullName { get; set; }
```
### MapProperty
This attribute gives you more control over the way the annotated property should get mapped. For instance, if the annotated property should use a property in the source class with a different name.
```c#
[MapProperty(SourcePropertyName = "Id")]
public int Key { get; set; }
```
### MapTypeConverter
A compilation error gets raised by default if the source and destination properties types are not implicitly convertible, but to convert the incompatible source type to the desired destination type, `MapTypeConverter` can be used.
This attribute will accept a type that implements `ITypeConverter<in TSource, out TDestination>` interface.
```c#
[MapFrom(typeof(User))]
public partial class UserViewModel
{
public DateTimeOffset RegisteredAt { get; set; }
[IgnoreProperty]
public ProfileViewModel Profile { get; set; }
[MapTypeConverter(typeof(IdConverter))]
[MapProperty(SourcePropertyName = nameof(User.Id))]
public string Key { get; }
private class IdConverter : ITypeConverter<int, string>
{
public string Convert(int source, object[] converterParameters) => $"{source:X}";
}
}
# MapTo
[![Nuget](https://img.shields.io/nuget/v/mapto?logo=nuget)](https://www.nuget.org/packages/MapTo/)
![Publish Packages](https://github.com/mrtaikandi/MapTo/workflows/Publish%20Packages/badge.svg)
A convention based object to object mapper using [Roslyn source generator](https://github.com/dotnet/roslyn/blob/master/docs/features/source-generators.md).
MapTo is a library to programmatically generate the necessary code to map one object to another during compile-time, eliminating the need to use reflection to map objects and make it much faster in runtime. It provides compile-time safety checks and ease of use by leveraging extension methods.
## Installation
```
dotnet add package MapTo --prerelease
```
## Usage
MapTo relies on a set of attributes to instruct it on how to generate the mappings. To start, declare the destination class as `partial` and annotate it with `MapFrom` attribute. As its name implies, `MapFrom` attribute tells the library what the source class you want to map from is.
```c#
using MapTo;
namespace App.ViewModels
{
[MapFrom(typeof(App.Data.Models.User))]
public partial class UserViewModel
{
public string FirstName { get; }
public string LastName { get; }
[IgnoreProperty]
public string FullName { get; set; }
}
}
```
To get an instance of `UserViewModel` from the `User` class, you can use any of the following methods:
```c#
var user = new User(id: 10) { FirstName = "John", LastName = "Doe" };
var vm = user.ToUserViewModel(); // A generated extension method for User class.
// OR
vm = new UserViewModel(user); // A generated contructor.
// OR
vm = UserViewModel.From(user); // A generated factory method.
```
> Please refer to [sample console app](https://github.com/mrtaikandi/MapTo/tree/master/test/TestConsoleApp) for a more complete example.
## Available Attributes
### IgnoreProperty
By default, MapTo will include all properties with the same name (case-sensitive), whether read-only or not, in the mapping unless annotating them with the `IgnoreProperty` attribute.
```c#
[IgnoreProperty]
public string FullName { get; set; }
```
### MapProperty
This attribute gives you more control over the way the annotated property should get mapped. For instance, if the annotated property should use a property in the source class with a different name.
```c#
[MapProperty(SourcePropertyName = "Id")]
public int Key { get; set; }
```
### MapTypeConverter
A compilation error gets raised by default if the source and destination properties types are not implicitly convertible, but to convert the incompatible source type to the desired destination type, `MapTypeConverter` can be used.
This attribute will accept a type that implements `ITypeConverter<in TSource, out TDestination>` interface.
```c#
[MapFrom(typeof(User))]
public partial class UserViewModel
{
public DateTimeOffset RegisteredAt { get; set; }
[IgnoreProperty]
public ProfileViewModel Profile { get; set; }
[MapTypeConverter(typeof(IdConverter))]
[MapProperty(SourcePropertyName = nameof(User.Id))]
public string Key { get; }
private class IdConverter : ITypeConverter<int, string>
{
public string Convert(int source, object[] converterParameters) => $"{source:X}";
}
}
```

View File

@ -1,49 +1,50 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>9</LangVersion>
<AssemblyName>MapTo</AssemblyName>
<Description>An object to object mapping generator using Roslyn source generator.</Description>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<IncludeSymbols>true</IncludeSymbols>
<PackageId>MapTo</PackageId>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageProjectUrl>https://github.com/mrtaikandi/mapto</PackageProjectUrl>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageVersion>$(Version)</PackageVersion>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<RepositoryUrl>https://github.com/mrtaikandi/mapto</RepositoryUrl>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<RootNamespace>MapTo</RootNamespace>
<AssemblyVersion>1.1</AssemblyVersion>
<FileVersion>1.1</FileVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DocumentationFile>bin\Release\MapTo.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>$(AssemblyName).Tests</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.8.0" />
</ItemGroup>
<ItemGroup>
<None Include="..\..\LICENSE" Pack="true" PackagePath="" Visible="false" />
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
<None Include="MapTo.props" Pack="true" PackagePath="build" Visible="false" />
</ItemGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>9</LangVersion>
<Description>An object to object mapping generator using Roslyn source generator.</Description>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<IncludeSymbols>true</IncludeSymbols>
<PackageId>MapTo</PackageId>
<PackageProjectUrl>https://github.com/mrtaikandi/mapto</PackageProjectUrl>
<PackageVersion>$(Version)</PackageVersion>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<RepositoryUrl>https://github.com/mrtaikandi/mapto</RepositoryUrl>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<RootNamespace>MapTo</RootNamespace>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DocumentationFile>bin\Release\MapTo.xml</DocumentationFile>
<Optimize>false</Optimize>
</PropertyGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>$(AssemblyName).Tests</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.8.0" />
<PackageReference Update="Nerdbank.GitVersioning">
<Version>3.5.109</Version>
</PackageReference>
</ItemGroup>
<ItemGroup>
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
<None Include="MapTo.props" Pack="true" PackagePath="build" Visible="false" />
</ItemGroup>
<ItemGroup>
<Folder Include="bin\Release\netstandard2.0" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,67 @@
using System.Collections.Immutable;
using System.Linq;
using MapTo.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace MapTo
{
internal class ClassMappingContext : MappingContext
{
internal ClassMappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax)
: base(compilation, sourceGenerationOptions, typeSyntax) { }
protected override ImmutableArray<MappedMember> GetSourceMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
{
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IFieldSymbol>().ToArray();
return typeSymbol
.GetAllMembers()
.OfType<IFieldSymbol>()
.Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol))
.Select(property => MapField(sourceTypeSymbol, sourceProperties, property))
.Where(mappedProperty => mappedProperty is not null)
.ToImmutableArray()!;
}
protected override ImmutableArray<MappedMember> GetSourceMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool hasInheritedClass)
{
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray();
return typeSymbol
.GetAllMembers()
.OfType<IPropertySymbol>()
.Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol))
.Select(property => MapProperty(sourceTypeSymbol, sourceProperties, property))
.Where(mappedProperty => mappedProperty is not null)
.ToImmutableArray()!;
}
protected override ImmutableArray<MappedMember> GetTypeMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
{
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IFieldSymbol>().ToArray();
return sourceTypeSymbol
.GetAllMembers()
.OfType<IFieldSymbol>()
.Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol))
.Select(property => MapFieldSimple(typeSymbol, property))
.Where(mappedProperty => mappedProperty is not null)
.ToImmutableArray()!;
}
protected override ImmutableArray<MappedMember> GetTypeMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool hasInheritedClass)
{
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray();
return sourceTypeSymbol
.GetAllMembers()
.OfType<IPropertySymbol>()
.Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol))
.Select(property => MapPropertySimple(typeSymbol, property))
.Where(mappedProperty => mappedProperty is not null)
.ToImmutableArray()!;
}
}
}

View File

@ -1,16 +1,16 @@
// ReSharper disable UnusedType.Global
// ReSharper disable CheckNamespace
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.ComponentModel;
namespace System.Runtime.CompilerServices
{
/// <summary>
/// Reserved to be used by the compiler for tracking metadata.
/// This class should not be used by developers in source code.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
internal static class IsExternalInit { }
// ReSharper disable UnusedType.Global
// ReSharper disable CheckNamespace
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.ComponentModel;
namespace System.Runtime.CompilerServices
{
/// <summary>
/// Reserved to be used by the compiler for tracking metadata.
/// This class should not be used by developers in source code.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
internal static class IsExternalInit { }
}

View File

@ -1,178 +1,178 @@
// ReSharper disable CheckNamespace
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#if NETSTANDARD2_0
namespace System.Diagnostics.CodeAnalysis
{
// These attributes already shipped with .NET Core 3.1 in System.Runtime
#if !NETCOREAPP3_0 && !NETCOREAPP3_1 && !NETSTANDARD2_1
/// <summary>Specifies that null is allowed as an input even if the corresponding type disallows it.</summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property)]
internal sealed class AllowNullAttribute : Attribute { }
/// <summary>Specifies that null is disallowed as an input even if the corresponding type allows it.</summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property)]
internal sealed class DisallowNullAttribute : Attribute { }
/// <summary>Specifies that an output may be null even if the corresponding type disallows it.</summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue)]
internal sealed class MaybeNullAttribute : Attribute { }
/// <summary>
/// Specifies that an output will not be null even if the corresponding type allows it. Specifies that an input
/// argument was not null when the call returns.
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue)]
internal sealed class NotNullAttribute : Attribute { }
/// <summary>
/// Specifies that when a method returns <see cref="ReturnValue" />, the parameter may be null even if the
/// corresponding type disallows it.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter)]
internal sealed class MaybeNullWhenAttribute : Attribute
{
/// <summary>Initializes the attribute with the specified return value condition.</summary>
/// <param name="returnValue">
/// The return value condition. If the method returns this value, the associated parameter may be null.
/// </param>
public MaybeNullWhenAttribute(bool returnValue)
{
ReturnValue = returnValue;
}
/// <summary>Gets the return value condition.</summary>
public bool ReturnValue { get; }
}
/// <summary>
/// Specifies that when a method returns <see cref="ReturnValue" />, the parameter will not be null even if the
/// corresponding type allows it.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter)]
internal sealed class NotNullWhenAttribute : Attribute
{
/// <summary>Initializes the attribute with the specified return value condition.</summary>
/// <param name="returnValue">
/// The return value condition. If the method returns this value, the associated parameter will not be null.
/// </param>
public NotNullWhenAttribute(bool returnValue)
{
ReturnValue = returnValue;
}
/// <summary>Gets the return value condition.</summary>
public bool ReturnValue { get; }
}
/// <summary>Specifies that the output will be non-null if the named parameter is non-null.</summary>
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true)]
internal sealed class NotNullIfNotNullAttribute : Attribute
{
/// <summary>Initializes the attribute with the associated parameter name.</summary>
/// <param name="parameterName">
/// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null.
/// </param>
public NotNullIfNotNullAttribute(string parameterName)
{
ParameterName = parameterName;
}
/// <summary>Gets the associated parameter name.</summary>
public string ParameterName { get; }
}
/// <summary>Applied to a method that will never return under any circumstance.</summary>
[AttributeUsage(AttributeTargets.Method, Inherited = false)]
internal sealed class DoesNotReturnAttribute : Attribute { }
/// <summary>Specifies that the method will not return if the associated Boolean parameter is passed the specified value.</summary>
[AttributeUsage(AttributeTargets.Parameter)]
internal sealed class DoesNotReturnIfAttribute : Attribute
{
/// <summary>Initializes the attribute with the specified parameter value.</summary>
/// <param name="parameterValue">
/// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to
/// the associated parameter matches this value.
/// </param>
public DoesNotReturnIfAttribute(bool parameterValue)
{
ParameterValue = parameterValue;
}
/// <summary>Gets the condition parameter value.</summary>
public bool ParameterValue { get; }
}
#endif
/// <summary>
/// Specifies that the method or property will ensure that the listed field and property members have not-null
/// values.
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
internal sealed class MemberNotNullAttribute : Attribute
{
/// <summary>Initializes the attribute with a field or property member.</summary>
/// <param name="member">
/// The field or property member that is promised to be not-null.
/// </param>
public MemberNotNullAttribute(string member)
{
Members = new[] { member };
}
/// <summary>Initializes the attribute with the list of field and property members.</summary>
/// <param name="members">
/// The list of field and property members that are promised to be not-null.
/// </param>
public MemberNotNullAttribute(params string[] members)
{
Members = members;
}
/// <summary>Gets field or property member names.</summary>
public string[] Members { get; }
}
/// <summary>
/// Specifies that the method or property will ensure that the listed field and property members have not-null
/// values when returning with the specified return value condition.
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
internal sealed class MemberNotNullWhenAttribute : Attribute
{
/// <summary>Initializes the attribute with the specified return value condition and a field or property member.</summary>
/// <param name="returnValue">
/// The return value condition. If the method returns this value, the associated parameter will not be null.
/// </param>
/// <param name="member">
/// The field or property member that is promised to be not-null.
/// </param>
public MemberNotNullWhenAttribute(bool returnValue, string member)
{
ReturnValue = returnValue;
Members = new[] { member };
}
/// <summary>Initializes the attribute with the specified return value condition and list of field and property members.</summary>
/// <param name="returnValue">
/// The return value condition. If the method returns this value, the associated parameter will not be null.
/// </param>
/// <param name="members">
/// The list of field and property members that are promised to be not-null.
/// </param>
public MemberNotNullWhenAttribute(bool returnValue, params string[] members)
{
ReturnValue = returnValue;
Members = members;
}
/// <summary>Gets field or property member names.</summary>
public string[] Members { get; }
/// <summary>Gets the return value condition.</summary>
public bool ReturnValue { get; }
}
}
// ReSharper disable CheckNamespace
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#if NETSTANDARD2_0
namespace System.Diagnostics.CodeAnalysis
{
// These attributes already shipped with .NET Core 3.1 in System.Runtime
#if !NETCOREAPP3_0 && !NETCOREAPP3_1 && !NETSTANDARD2_1
/// <summary>Specifies that null is allowed as an input even if the corresponding type disallows it.</summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property)]
internal sealed class AllowNullAttribute : Attribute { }
/// <summary>Specifies that null is disallowed as an input even if the corresponding type allows it.</summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property)]
internal sealed class DisallowNullAttribute : Attribute { }
/// <summary>Specifies that an output may be null even if the corresponding type disallows it.</summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue)]
internal sealed class MaybeNullAttribute : Attribute { }
/// <summary>
/// Specifies that an output will not be null even if the corresponding type allows it. Specifies that an input
/// argument was not null when the call returns.
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue)]
internal sealed class NotNullAttribute : Attribute { }
/// <summary>
/// Specifies that when a method returns <see cref="ReturnValue" />, the parameter may be null even if the
/// corresponding type disallows it.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter)]
internal sealed class MaybeNullWhenAttribute : Attribute
{
/// <summary>Initializes the attribute with the specified return value condition.</summary>
/// <param name="returnValue">
/// The return value condition. If the method returns this value, the associated parameter may be null.
/// </param>
public MaybeNullWhenAttribute(bool returnValue)
{
ReturnValue = returnValue;
}
/// <summary>Gets the return value condition.</summary>
public bool ReturnValue { get; }
}
/// <summary>
/// Specifies that when a method returns <see cref="ReturnValue" />, the parameter will not be null even if the
/// corresponding type allows it.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter)]
internal sealed class NotNullWhenAttribute : Attribute
{
/// <summary>Initializes the attribute with the specified return value condition.</summary>
/// <param name="returnValue">
/// The return value condition. If the method returns this value, the associated parameter will not be null.
/// </param>
public NotNullWhenAttribute(bool returnValue)
{
ReturnValue = returnValue;
}
/// <summary>Gets the return value condition.</summary>
public bool ReturnValue { get; }
}
/// <summary>Specifies that the output will be non-null if the named parameter is non-null.</summary>
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true)]
internal sealed class NotNullIfNotNullAttribute : Attribute
{
/// <summary>Initializes the attribute with the associated parameter name.</summary>
/// <param name="parameterName">
/// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null.
/// </param>
public NotNullIfNotNullAttribute(string parameterName)
{
ParameterName = parameterName;
}
/// <summary>Gets the associated parameter name.</summary>
public string ParameterName { get; }
}
/// <summary>Applied to a method that will never return under any circumstance.</summary>
[AttributeUsage(AttributeTargets.Method, Inherited = false)]
internal sealed class DoesNotReturnAttribute : Attribute { }
/// <summary>Specifies that the method will not return if the associated Boolean parameter is passed the specified value.</summary>
[AttributeUsage(AttributeTargets.Parameter)]
internal sealed class DoesNotReturnIfAttribute : Attribute
{
/// <summary>Initializes the attribute with the specified parameter value.</summary>
/// <param name="parameterValue">
/// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to
/// the associated parameter matches this value.
/// </param>
public DoesNotReturnIfAttribute(bool parameterValue)
{
ParameterValue = parameterValue;
}
/// <summary>Gets the condition parameter value.</summary>
public bool ParameterValue { get; }
}
#endif
/// <summary>
/// Specifies that the method or property will ensure that the listed field and property members have not-null
/// values.
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
internal sealed class MemberNotNullAttribute : Attribute
{
/// <summary>Initializes the attribute with a field or property member.</summary>
/// <param name="member">
/// The field or property member that is promised to be not-null.
/// </param>
public MemberNotNullAttribute(string member)
{
Members = new[] { member };
}
/// <summary>Initializes the attribute with the list of field and property members.</summary>
/// <param name="members">
/// The list of field and property members that are promised to be not-null.
/// </param>
public MemberNotNullAttribute(params string[] members)
{
Members = members;
}
/// <summary>Gets field or property member names.</summary>
public string[] Members { get; }
}
/// <summary>
/// Specifies that the method or property will ensure that the listed field and property members have not-null
/// values when returning with the specified return value condition.
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
internal sealed class MemberNotNullWhenAttribute : Attribute
{
/// <summary>Initializes the attribute with the specified return value condition and a field or property member.</summary>
/// <param name="returnValue">
/// The return value condition. If the method returns this value, the associated parameter will not be null.
/// </param>
/// <param name="member">
/// The field or property member that is promised to be not-null.
/// </param>
public MemberNotNullWhenAttribute(bool returnValue, string member)
{
ReturnValue = returnValue;
Members = new[] { member };
}
/// <summary>Initializes the attribute with the specified return value condition and list of field and property members.</summary>
/// <param name="returnValue">
/// The return value condition. If the method returns this value, the associated parameter will not be null.
/// </param>
/// <param name="members">
/// The list of field and property members that are promised to be not-null.
/// </param>
public MemberNotNullWhenAttribute(bool returnValue, params string[] members)
{
ReturnValue = returnValue;
Members = members;
}
/// <summary>Gets field or property member names.</summary>
public string[] Members { get; }
/// <summary>Gets the return value condition.</summary>
public bool ReturnValue { get; }
}
}
#endif

View File

@ -1,42 +1,42 @@
using System.Linq;
using MapTo.Extensions;
using MapTo.Sources;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using static MapTo.Sources.Constants;
namespace MapTo
{
internal static class DiagnosticsFactory
{
private const string UsageCategory = "Usage";
private const string CodePrefix = "MT";
private const string ErrorId = CodePrefix + "0";
private const string InfoId = CodePrefix + "1";
private const string WarningId = CodePrefix + "2";
internal static Diagnostic TypeNotFoundError(Location location, string syntaxName) =>
Create($"{ErrorId}010", location, $"Unable to find '{syntaxName}' type.");
internal static Diagnostic MapFromAttributeNotFoundError(Location location) =>
Create($"{ErrorId}020", location, $"Unable to find {MapFromAttributeSource.AttributeName} type.");
internal static Diagnostic NoMatchingPropertyFoundError(Location location, INamedTypeSymbol classType, INamedTypeSymbol sourceType) =>
Create($"{ErrorId}030", location, $"No matching properties found between '{classType.ToDisplayString()}' and '{sourceType.ToDisplayString()}' types.");
internal static Diagnostic NoMatchingPropertyTypeFoundError(ISymbol property) =>
Create($"{ErrorId}031", property.Locations.FirstOrDefault(), $"Cannot create a map for '{property.ToDisplayString()}' property because source and destination types are not implicitly convertible. Consider using '{MapTypeConverterAttributeSource.FullyQualifiedName}' to provide a type converter or ignore the property using '{IgnorePropertyAttributeSource.FullyQualifiedName}'.");
internal static Diagnostic InvalidTypeConverterGenericTypesError(ISymbol property, IPropertySymbol sourceProperty) =>
Create($"{ErrorId}032", property.Locations.FirstOrDefault(), $"Cannot map '{property.ToDisplayString()}' property because the annotated converter does not implement '{RootNamespace}.{ITypeConverterSource.InterfaceName}<{sourceProperty.Type.ToDisplayString()}, {property.GetTypeSymbol()?.ToDisplayString()}>'.");
internal static Diagnostic ConfigurationParseError(string error) =>
Create($"{ErrorId}040", Location.None, error);
internal static Diagnostic MissingConstructorArgument(ConstructorDeclarationSyntax constructorSyntax) =>
Create($"{ErrorId}050", constructorSyntax.GetLocation(), "There are no argument given that corresponds to the required formal parameter.");
private static Diagnostic Create(string id, Location? location, string message, DiagnosticSeverity severity = DiagnosticSeverity.Error) =>
Diagnostic.Create(new DiagnosticDescriptor(id, string.Empty, message, UsageCategory, severity, true), location ?? Location.None);
}
using System.Linq;
using MapTo.Extensions;
using MapTo.Sources;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using static MapTo.Sources.Constants;
namespace MapTo
{
internal static class DiagnosticsFactory
{
private const string UsageCategory = "Usage";
private const string CodePrefix = "MT";
private const string ErrorId = CodePrefix + "0";
private const string InfoId = CodePrefix + "1";
private const string WarningId = CodePrefix + "2";
internal static Diagnostic TypeNotFoundError(Location location, string syntaxName) =>
Create($"{ErrorId}010", location, $"Unable to find '{syntaxName}' type.");
internal static Diagnostic MapFromAttributeNotFoundError(Location location) =>
Create($"{ErrorId}020", location, $"Unable to find {MapFromAttributeSource.AttributeName} type.");
internal static Diagnostic NoMatchingPropertyFoundError(Location location, INamedTypeSymbol classType, INamedTypeSymbol sourceType) =>
Create($"{ErrorId}030", location, $"No matching properties found between '{classType.ToDisplayString()}' and '{sourceType.ToDisplayString()}' types.");
internal static Diagnostic NoMatchingPropertyTypeFoundError(ISymbol property) =>
Create($"{ErrorId}031", property.Locations.FirstOrDefault(), $"Cannot create a map for '{property.ToDisplayString()}' property because source and destination types are not implicitly convertible. Consider using '{MapTypeConverterAttributeSource.FullyQualifiedName}' to provide a type converter or ignore the property using '{IgnoreMemberAttributeSource.FullyQualifiedName}'.");
internal static Diagnostic InvalidTypeConverterGenericTypesError(ISymbol property, IPropertySymbol sourceProperty) =>
Create($"{ErrorId}032", property.Locations.FirstOrDefault(), $"Cannot map '{property.ToDisplayString()}' property because the annotated converter does not implement '{RootNamespace}.{ITypeConverterSource.InterfaceName}<{sourceProperty.Type.ToDisplayString()}, {property.GetTypeSymbol()?.ToDisplayString()}>'.");
internal static Diagnostic ConfigurationParseError(string error) =>
Create($"{ErrorId}040", Location.None, error);
internal static Diagnostic MissingConstructorArgument(ConstructorDeclarationSyntax constructorSyntax) =>
Create($"{ErrorId}050", constructorSyntax.GetLocation(), "There are no argument given that corresponds to the required formal parameter.");
private static Diagnostic Create(string id, Location? location, string message, DiagnosticSeverity severity = DiagnosticSeverity.Error) =>
Diagnostic.Create(new DiagnosticDescriptor(id, string.Empty, message, UsageCategory, severity, true), location ?? Location.None);
}
}

View File

@ -0,0 +1,86 @@
using MapTo.Sources;
using Microsoft.CodeAnalysis;
using System;
using System.Collections.Generic;
using System.Text;
namespace MapTo.Extensions
{
internal static class CommonExtensions
{
internal static SourceBuilder WriteComment(this SourceBuilder builder, string comment = "")
{
return builder.WriteLine($"// {comment}");
}
internal static SourceBuilder WriteCommentArray(this SourceBuilder builder, IEnumerable<object> enumerable, string name = "")
{
builder.WriteComment($"Printing Array: {name}");
foreach (var o in enumerable)
{
if (o != null)
{
builder.WriteComment($" {o.ToString()}");
}
}
builder.WriteComment($"End printing Array: {name}");
return builder;
}
internal static SourceBuilder WriteModelInfo(this SourceBuilder builder, MappingModel model)
{
foreach (var targetSourceType in model.MappedSourceTypes)
{
builder
.WriteLine()
.WriteComment($" IsTypeUpdatable {model.IsTypeUpdatable}")
.WriteComment($" HasMappedBaseClass {model.HasMappedBaseClass.ToString()}")
.WriteComment($" Namespace {model.Namespace}")
.WriteComment($" Options {model.Options.ToString()}")
.WriteComment($" Type {model.Type}")
.WriteComment($" TypeIdentifierName {model.TypeIdentifierName}")
.WriteComment($" SourceNamespace {targetSourceType.SourceNamespace}")
.WriteComment($" SourceTypeFullName {targetSourceType.SourceTypeFullName}")
.WriteComment($" SourceTypeIdentifierName {targetSourceType.SourceTypeIdentifierName}");
}
return builder;
}
internal static SourceBuilder WriteMappedProperties(this SourceBuilder builder, System.Collections.Immutable.ImmutableArray<MappedMember> mappedProperties)
{
foreach (var item in mappedProperties)
{
string str = "";
if (item.NamedTypeSymbol != null)
foreach (var named in item.NamedTypeSymbol?.TypeArguments)
{
str += $"typeToString: {named.ToString()} ";
bool? containedTypeIsJsonEXtension = named?.HasAttribute(MappingContext.JsonExtensionAttributeSymbol);
str += $"typeArgumentTypeIsJsonExtensioN: {containedTypeIsJsonEXtension.ToString()}";
}
builder .WriteComment($" Name {item.Name}")
.WriteComment($" Type {item.Type}")
.WriteComment($" MappedSourcePropertyTypeName {item.MappedSourcePropertyTypeName}")
.WriteComment($" IsEnumerable {item.IsEnumerable}")
.WriteComment($" FullyQualifiedType {item.FullyQualifiedType}")
.WriteComment($" EnumerableTypeArgument {item.EnumerableTypeArgument}")
.WriteComment($" SourcePropertyName {item.SourcePropertyName}")
.WriteComment($" TypeSymbol {item.FullyQualifiedType.ToString()}")
.WriteComment($" isReadOnly {item.isReadOnly.ToString()}")
.WriteComment($" isEnumerable {item.isEnumerable.ToString()}")
.WriteComment($" INamedTypeSymbol {item.NamedTypeSymbol?.ToString()}")
.WriteComment($" INamedTypeSymbolTypeArguments {str}")
.WriteLine();
}
return builder;
}
}
}

View File

@ -0,0 +1,379 @@
using MapTo.Sources;
using static MapTo.Sources.Constants;
using System;
using System.Collections.Generic;
using System.Text;
using System.Collections.Immutable;
using System.Diagnostics;
using Microsoft.CodeAnalysis;
#pragma warning disable CS8602
namespace MapTo.Extensions
{
internal static class CommonSource
{
internal static SourceCode GenerateStructOrClass(this MappingModel model, string structOrClass)
{
const bool writeDebugInfo = false;
List<string> constructorHeaders = new List<string>();
using var builder = new SourceBuilder()
.WriteLine(GeneratedFilesHeader)
.WriteNullableContextOptionIf(model.Options.SupportNullableReferenceTypes)
.WriteUsings(model.Usings)
.WriteLine()
// Namespace declaration
.WriteLine($"namespace {model.Namespace}")
.WriteOpeningBracket();
builder
// Class declaration
.WriteLine($"partial {structOrClass} {model.TypeIdentifierName}")
.WriteOpeningBracket()
.WriteLine()
// Class body
.GeneratePublicEmptyConstructor(model, ref constructorHeaders, true)
.GeneratePublicConstructor(model, ref constructorHeaders)
.GeneratePublicConstructor(model, ref constructorHeaders, true);
if (model.IsTypeUpdatable) builder.GenerateUpdateMethod(model);
if (model.IsJsonExtension) builder.WriteToJsonMethod(model);
builder
.WriteLine()
// End class declaration
.WriteClosingBracket()
.WriteLine()
// End namespace declaration
.WriteClosingBracket();
return new(builder.ToString(), $"{model.Namespace}.{model.TypeIdentifierName}.g.cs");
}
private static SourceBuilder GeneratePublicConstructor(this SourceBuilder builder, MappingModel model, ref List<string> constructorHeaders, bool filterNonMapped = false)
{
const string mappingContextParameterName = "context";
foreach (var targetSourceType in model.MappedSourceTypes)
{
var sourceClassParameterName = targetSourceType.SourceTypeIdentifierName.ToCamelCase();
var baseConstructor = /*model.HasMappedBaseClass ? $" : base({mappingContextParameterName}, {sourceClassParameterName})" :*/ string.Empty;
var stringBuilder = new StringBuilder();
var otherProperties = new List<MappedMember>();
foreach (var property in targetSourceType.TypeProperties)
{
if (!targetSourceType.SourceProperties.IsMappedProperty(property) && !filterNonMapped)
{
stringBuilder.Append(", ");
stringBuilder.Append($"{property.FullyQualifiedType} {property.SourcePropertyName.ToCamelCase()}");
otherProperties.Add(property);
}
}
foreach (var property in targetSourceType.TypeFields)
{
if (!targetSourceType.SourceFields.IsMappedProperty(property) && !filterNonMapped)
{
stringBuilder.Append(", ");
stringBuilder.Append($"{property.FullyQualifiedType} {property.SourcePropertyName.ToCamelCase()}");
otherProperties.Add(property);
}
}
var readOnlyPropertiesArguments = stringBuilder.ToString();
var constructorHeader =
$"public {model.TypeIdentifierName}({targetSourceType.SourceType} {sourceClassParameterName}{readOnlyPropertiesArguments}){baseConstructor}";
bool hasAlreadyConstructor = false;
foreach (var header in constructorHeaders)
{
if(constructorHeader.Contains(header)) hasAlreadyConstructor = true;
}
if (hasAlreadyConstructor) continue;
constructorHeaders.Add(constructorHeader);
builder
.WriteLine(constructorHeader)
.WriteOpeningBracket()
.WriteAssignmentMethod(model, filterNonMapped ? null : otherProperties.ToArray().ToImmutableArray(), sourceClassParameterName, mappingContextParameterName, filterNonMapped);
builder.WriteClosingBracket()
.WriteLine();
}
// End constructor declaration
return builder;
}
private static SourceBuilder GeneratePublicEmptyConstructor(this SourceBuilder builder, MappingModel model, ref List<string> constructorHeaders, bool filterNonMapped = false)
{
const string mappingContextParameterName = "context";
foreach (var targetSourceType in model.MappedSourceTypes)
{
var baseConstructor = /*model.HasMappedBaseClass ? $" : base({mappingContextParameterName}, {sourceClassParameterName})" :*/ string.Empty;
var constructorHeader =
$"public {model.TypeIdentifierName}(){baseConstructor}";
bool hasAlreadyConstructor = false;
foreach (var header in constructorHeaders)
{
if(constructorHeader.Contains(header)) hasAlreadyConstructor = true;
}
if (hasAlreadyConstructor) continue;
constructorHeaders.Add(constructorHeader);
builder
.WriteLine(constructorHeader)
.WriteOpeningBracket()
.WriteClosingBracket()
.WriteLine();
}
// End constructor declaration
return builder;
}
private static bool IsMappedProperty(this System.Collections.Immutable.ImmutableArray<MappedMember> properties, MappedMember property)
{
foreach (var prop in properties)
{
if (prop.Name == property.Name) return true;
}
return false;
}
private static SourceBuilder WriteToJsonMethod(this SourceBuilder builder, MappingModel model)
{
builder
.WriteLine($"public string ToJson()")
.WriteOpeningBracket()
.WriteLine("var stringBuilder = new System.Text.StringBuilder();")
.WriteLine(GetStringBuilderAppendNoInterpolation("{"));
foreach (var targetSourceType in model.MappedSourceTypes)
{
foreach (var property in targetSourceType.TypeProperties)
{
if (!property.isEnumerable)
HandlePropertyEnumerable(builder, property);
else
{
builder = WriteJsonField(builder, property);
}
}
foreach (var property in targetSourceType.TypeFields)
{
if (!property.isEnumerable)
HandleFieldEnumerable(builder, property);
else
{
builder.WriteLine(GetStringBuilderAppend($"\\\"{property.Name.ToCamelCase()}\\\" : [{GetJsonArrayValue(property, ref builder)}],"));
}
}
builder.WriteLine(GetStringBuilderAppendNoInterpolation("}"));
builder.WriteLine("return stringBuilder.ToString();");
builder.WriteClosingBracket();
}
return builder;
}
private static SourceBuilder WriteJsonField(SourceBuilder builder, MappedMember property)
{
builder.WriteLine(
GetStringBuilderAppend(
$"\\\"{property.Name.ToCamelCase()}\\\" : [{GetJsonArrayValue(property, ref builder)}],"));
return builder;
}
private static void HandleEnumerable(SourceBuilder builder, MappedMember property)
{
var symbol = property.ActualSymbol as IPropertySymbol;
#pragma warning disable CS8602
builder.WriteCommentArray(symbol.Parameters, nameof(symbol.Parameters));
#pragma warning restore CS8602
builder.WriteCommentArray(symbol.TypeCustomModifiers, nameof(symbol.TypeCustomModifiers));
builder.WriteComment($"Is enumerable {(property.ActualSymbol as IPropertySymbol).Parameters}");
builder.WriteLine(
GetStringBuilderAppend($"\\\"{property.Name.ToCamelCase()}\\\" : {GetJsonValue(property, builder)},"));
}
private static void HandleFieldEnumerable(SourceBuilder builder, MappedMember property)
{
HandleEnumerable(builder, property);
}
private static void HandlePropertyEnumerable(SourceBuilder builder, MappedMember property)
{
HandleEnumerable(builder, property);
}
private static string GetJsonArrayValue(MappedMember member, ref SourceBuilder builder)
{
if (member.isEnumerable)
{
// get underlying type (check if is a json extension)
builder.WriteLine("var arrStrBuilder = new StringBuilder();");
foreach (var named in member.NamedTypeSymbol?.TypeArguments!)
{
bool? containedTypeIsJsonEXtension = named?.HasAttribute(MappingContext.JsonExtensionAttributeSymbol);
if (!containedTypeIsJsonEXtension.HasValue) continue;
builder.WriteLine($"foreach (var v in {member.SourcePropertyName.ToString()})");
builder.WriteOpeningBracket();
builder.WriteLine("arrStrBuilder.Append(v.ToJson());");
builder.WriteLine("arrStrBuilder.Append(\", \");");
builder.WriteClosingBracket();
}
builder.WriteLine("arrStrBuilder.Remove(arrStrBuilder.Length -1, 1);");
}
return "{arrStrBuilder.ToString()}";
}
private static string GetJsonValue(MappedMember member, SourceBuilder builder)
{
if (member.FullyQualifiedType == "string") return $"\\\"{{{member.SourcePropertyName}}}\\\"";
if (member.FullyQualifiedType is "int" or "double" or "float" or "long") return $"{{{member.SourcePropertyName}}}";
return "";
}
private static string GetStringBuilderAppend(string stringToAppend)
{
return $"stringBuilder.Append($\"{stringToAppend}\");";
}
private static string GetStringBuilderAppendNoInterpolation(string stringToAppend)
{
return $"stringBuilder.Append(\"{stringToAppend}\");";
}
private static SourceBuilder WriteAssignmentMethod(this SourceBuilder builder, MappingModel model, System.Collections.Immutable.ImmutableArray<MappedMember>? otherProperties,
string? sourceClassParameterName, string mappingContextParameterName, bool fromUpdate)
{
List<MappedMember> _addedMembers = new List<MappedMember>();
foreach (var targetSourceType in model.MappedSourceTypes)
{
foreach (var property in targetSourceType.SourceProperties)
{
if (property.isReadOnly && fromUpdate) continue;
if(_addedMembers.Contains(property)) continue;
builder.WriteLine($"{property.Name} = {sourceClassParameterName}.{property.SourcePropertyName};");
_addedMembers.Add(property);
}
foreach (var property in targetSourceType.SourceFields)
{
if (property.isReadOnly && fromUpdate) continue;
if(_addedMembers.Contains(property)) continue;
builder.WriteLine($"{property.Name} = {sourceClassParameterName}.{property.SourcePropertyName};");
_addedMembers.Add(property);
}
if (otherProperties == null) return builder;
foreach (var property in otherProperties)
{
if(_addedMembers.Contains(property)) continue;
builder.WriteLine(property.MappedSourcePropertyTypeName is null
? $"{property.Name} = {property.SourcePropertyName.ToCamelCase()};"
: "");
_addedMembers.Add(property);
}
}
return builder;
}
private static SourceBuilder GenerateUpdateMethod(this SourceBuilder builder, MappingModel model)
{
foreach (var targetSourceType in model.MappedSourceTypes)
{
var sourceClassParameterName = targetSourceType.SourceTypeIdentifierName.ToCamelCase();
builder
.GenerateUpdaterMethodsXmlDocs(model, sourceClassParameterName, targetSourceType)
.WriteLine($"public void Update({targetSourceType.SourceType} {sourceClassParameterName})")
.WriteOpeningBracket()
.WriteAssignmentMethod(model, null, sourceClassParameterName, "context", true)
.WriteClosingBracket()
.WriteLine();
}
return builder;
}
private static SourceBuilder GenerateUpdaterMethodsXmlDocs(this SourceBuilder builder, MappingModel model, string sourceClassParameterName,
MappedSourceType mappedSourceType)
{
if (!model.Options.GenerateXmlDocument)
{
return builder;
}
builder
.WriteLine("/// <summary>")
.WriteLine($"/// Updates <see cref=\"{model.TypeIdentifierName}\"/> and sets its participating properties")
.WriteLine($"/// using the property values from <paramref name=\"{sourceClassParameterName}\"/>.")
.WriteLine("/// </summary>")
.WriteLine($"/// <param name=\"{sourceClassParameterName}\">The instance of <see cref=\"{mappedSourceType.SourceType}\"/> to use as source.</param>");
return builder;
}
private static SourceBuilder GenerateEnumerableJsonSourceTypeExtensionMethod(this SourceBuilder builder, MappingModel model)
{
foreach (var targetSourceType in model.MappedSourceTypes)
{
var sourceClassParameterName = targetSourceType.SourceTypeIdentifierName.ToCamelCase();
builder
.WriteLineIf(model.Options.SupportNullableStaticAnalysis, $"[return: NotNullIfNotNull(\"{sourceClassParameterName}\")]")
.WriteLine($"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static string ToJson(this IEnumerable<{targetSourceType.SourceType}{model.Options.NullableReferenceSyntax}> {sourceClassParameterName}List)")
.WriteOpeningBracket()
.WriteLine($"return {sourceClassParameterName} == null ? null : new {model.TypeIdentifierName}({sourceClassParameterName});")
.WriteClosingBracket();
}
return builder;
}
}
}

View File

@ -1,9 +1,9 @@
using System;
namespace MapTo.Extensions
{
internal static class EnumExtensions
{
internal static string ToLowercaseString(this Enum member) => member.ToString().ToLower();
}
using System;
namespace MapTo.Extensions
{
internal static class EnumExtensions
{
internal static string ToLowercaseString(this Enum member) => member.ToString().ToLower();
}
}

View File

@ -1,19 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace MapTo.Extensions
{
internal static class EnumerableExtensions
{
internal static void ForEach<T>(this IEnumerable<T> enumerable, Action<T> action)
{
foreach (var item in enumerable)
{
action(item);
}
}
internal static bool IsEmpty<T>(this IEnumerable<T> enumerable) => !enumerable.Any();
}
using System;
using System.Collections.Generic;
using System.Linq;
namespace MapTo.Extensions
{
internal static class EnumerableExtensions
{
internal static void ForEach<T>(this IEnumerable<T> enumerable, Action<T> action)
{
foreach (var item in enumerable)
{
action(item);
}
}
internal static bool IsEmpty<T>(this IEnumerable<T> enumerable) => !enumerable.Any();
}
}

View File

@ -1,51 +1,51 @@
using System;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Text;
namespace MapTo.Extensions
{
internal static class GeneratorExecutionContextExtensions
{
private const string PropertyNameSuffix = "MapTo_";
internal static T GetBuildGlobalOption<T>(this GeneratorExecutionContext context, string propertyName, T defaultValue = default!) where T : notnull
{
if (!context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(GetBuildPropertyName(propertyName), out var optionValue) || string.IsNullOrWhiteSpace(optionValue))
{
return defaultValue;
}
var type = typeof(T);
if (!type.IsEnum)
{
return (T)Convert.ChangeType(optionValue, type);
}
try
{
return (T)Enum.Parse(type, optionValue, true);
}
catch (Exception)
{
context.ReportDiagnostic(DiagnosticsFactory.ConfigurationParseError($"'{optionValue}' is not a valid value for {PropertyNameSuffix}{propertyName} property."));
return defaultValue;
}
}
internal static string GetBuildPropertyName(string propertyName) => $"build_property.{PropertyNameSuffix}{propertyName}";
internal static Compilation AddSource(this Compilation compilation, ref GeneratorExecutionContext context, SourceCode sourceCode)
{
var sourceText = SourceText.From(sourceCode.Text, Encoding.UTF8);
context.AddSource(sourceCode.HintName, sourceText);
// NB: https://github.com/dotnet/roslyn/issues/49753
// To be replaced after above issue is resolved.
var options = (CSharpParseOptions)((CSharpCompilation)compilation).SyntaxTrees[0].Options;
return compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(sourceText, options));
}
}
using System;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Text;
namespace MapTo.Extensions
{
internal static class GeneratorExecutionContextExtensions
{
private const string PropertyNameSuffix = "MapTo_";
internal static T GetBuildGlobalOption<T>(this GeneratorExecutionContext context, string propertyName, T defaultValue = default!) where T : notnull
{
if (!context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(GetBuildPropertyName(propertyName), out var optionValue) || string.IsNullOrWhiteSpace(optionValue))
{
return defaultValue;
}
var type = typeof(T);
if (!type.IsEnum)
{
return (T)Convert.ChangeType(optionValue, type);
}
try
{
return (T)Enum.Parse(type, optionValue, true);
}
catch (Exception)
{
context.ReportDiagnostic(DiagnosticsFactory.ConfigurationParseError($"'{optionValue}' is not a valid value for {PropertyNameSuffix}{propertyName} property."));
return defaultValue;
}
}
internal static string GetBuildPropertyName(string propertyName) => $"build_property.{PropertyNameSuffix}{propertyName}";
internal static Compilation AddSource(this Compilation compilation, ref GeneratorExecutionContext context, SourceCode sourceCode)
{
var sourceText = SourceText.From(sourceCode.Text, Encoding.UTF8);
context.AddSource(sourceCode.HintName, sourceText);
// NB: https://github.com/dotnet/roslyn/issues/49753
// To be replaced after above issue is resolved.
var options = (CSharpParseOptions)((CSharpCompilation)compilation).SyntaxTrees[0].Options;
return compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(sourceText, options));
}
}
}

View File

@ -1,131 +1,136 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace MapTo.Extensions
{
internal static class RoslynExtensions
{
public static IEnumerable<ITypeSymbol> GetBaseTypesAndThis(this ITypeSymbol type)
{
var current = type;
while (current != null)
{
yield return current;
current = current.BaseType;
}
}
public static IEnumerable<ISymbol> GetAllMembers(this ITypeSymbol type, bool includeBaseTypeMembers = true)
{
return includeBaseTypeMembers
? type.GetBaseTypesAndThis().SelectMany(t => t.GetMembers())
: type.GetMembers();
}
public static CompilationUnitSyntax GetCompilationUnit(this SyntaxNode syntaxNode) => syntaxNode.Ancestors().OfType<CompilationUnitSyntax>().Single();
public static string GetIdentifierName(this TypeDeclarationSyntax typeSyntax) => typeSyntax.Identifier.Text;
public static AttributeSyntax? GetAttribute(this TypeDeclarationSyntax typeDeclarationSyntax, string attributeName)
{
return typeDeclarationSyntax.AttributeLists
.SelectMany(al => al.Attributes)
.SingleOrDefault(a =>
(a.Name as IdentifierNameSyntax)?.Identifier.ValueText == attributeName ||
((a.Name as QualifiedNameSyntax)?.Right as IdentifierNameSyntax)?.Identifier.ValueText == attributeName);
}
public static bool HasAttribute(this ISymbol symbol, ITypeSymbol attributeSymbol) =>
symbol.GetAttributes().Any(a => a.AttributeClass?.Equals(attributeSymbol, SymbolEqualityComparer.Default) == true);
public static IEnumerable<AttributeData> GetAttributes(this ISymbol symbol, ITypeSymbol attributeSymbol) =>
symbol.GetAttributes().Where(a => a.AttributeClass?.Equals(attributeSymbol, SymbolEqualityComparer.Default) == true);
public static AttributeData? GetAttribute(this ISymbol symbol, ITypeSymbol attributeSymbol) =>
symbol.GetAttributes(attributeSymbol).FirstOrDefault();
public static string? GetNamespace(this TypeDeclarationSyntax typeDeclarationSyntax) => typeDeclarationSyntax
.Ancestors()
.OfType<NamespaceDeclarationSyntax>()
.FirstOrDefault()
?.Name
.ToString();
public static bool HasCompatibleTypes(this Compilation compilation, ISymbol source, ISymbol destination) =>
source.TryGetTypeSymbol(out var sourceType) && destination.TryGetTypeSymbol(out var destinationType) &&
(SymbolEqualityComparer.Default.Equals(destinationType, sourceType) || compilation.HasImplicitConversion(sourceType, destinationType));
public static bool TryGetTypeSymbol(this ISymbol symbol, [NotNullWhen(true)] out ITypeSymbol? typeSymbol)
{
switch (symbol)
{
case IPropertySymbol propertySymbol:
typeSymbol = propertySymbol.Type;
return true;
case IParameterSymbol parameterSymbol:
typeSymbol = parameterSymbol.Type;
return true;
default:
typeSymbol = null;
return false;
}
}
public static ITypeSymbol? GetTypeSymbol(this ISymbol symbol) => symbol.TryGetTypeSymbol(out var typeSymbol) ? typeSymbol : null;
public static IPropertySymbol? FindProperty(this IEnumerable<IPropertySymbol> properties, IPropertySymbol targetProperty)
{
return properties.SingleOrDefault(p =>
p.Name == targetProperty.Name &&
(p.NullableAnnotation != NullableAnnotation.Annotated ||
p.NullableAnnotation == NullableAnnotation.Annotated &&
targetProperty.NullableAnnotation == NullableAnnotation.Annotated));
}
public static INamedTypeSymbol GetTypeByMetadataNameOrThrow(this Compilation compilation, string fullyQualifiedMetadataName) =>
compilation.GetTypeByMetadataName(fullyQualifiedMetadataName) ?? throw new TypeLoadException($"Unable to find '{fullyQualifiedMetadataName}' type.");
public static bool IsGenericEnumerable(this Compilation compilation, ITypeSymbol typeSymbol) =>
typeSymbol is INamedTypeSymbol { IsGenericType: true } &&
compilation.GetSpecialType(SpecialType.System_Collections_Generic_IEnumerable_T).Equals(typeSymbol.OriginalDefinition, SymbolEqualityComparer.Default);
public static bool IsArray(this Compilation compilation, ITypeSymbol typeSymbol) => typeSymbol is IArrayTypeSymbol;
public static bool IsPrimitiveType(this ITypeSymbol type) => type.SpecialType is
SpecialType.System_String or
SpecialType.System_Boolean or
SpecialType.System_SByte or
SpecialType.System_Int16 or
SpecialType.System_Int32 or
SpecialType.System_Int64 or
SpecialType.System_Byte or
SpecialType.System_UInt16 or
SpecialType.System_UInt32 or
SpecialType.System_UInt64 or
SpecialType.System_Single or
SpecialType.System_Double or
SpecialType.System_Char or
SpecialType.System_Object;
public static SyntaxNode? GetSyntaxNode(this ISymbol symbol) =>
symbol.Locations.FirstOrDefault() is { } location ? location.SourceTree?.GetRoot().FindNode(location.SourceSpan) : null;
public static IEnumerable<INamedTypeSymbol> GetTypesByMetadataName(this Compilation compilation, string typeMetadataName)
{
return compilation.References
.Select(compilation.GetAssemblyOrModuleSymbol)
.OfType<IAssemblySymbol>()
.Select(assemblySymbol => assemblySymbol.GetTypeByMetadataName(typeMetadataName))
.Where(t => t != null)!;
}
public static bool TypeByMetadataNameExists(this Compilation compilation, string typeMetadataName) => GetTypesByMetadataName(compilation, typeMetadataName).Any();
}
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace MapTo.Extensions
{
internal static class RoslynExtensions
{
public static IEnumerable<ITypeSymbol> GetBaseTypesAndThis(this ITypeSymbol type)
{
var current = type;
while (current != null)
{
yield return current;
current = current.BaseType;
}
}
public static IEnumerable<ISymbol> GetAllMembers(this ITypeSymbol type, bool includeBaseTypeMembers = true)
{
return includeBaseTypeMembers
? type.GetBaseTypesAndThis().SelectMany(t => t.GetMembers())
: type.GetMembers();
}
public static CompilationUnitSyntax GetCompilationUnit(this SyntaxNode syntaxNode) => syntaxNode.Ancestors().OfType<CompilationUnitSyntax>().Single();
public static string GetIdentifierName(this TypeDeclarationSyntax typeSyntax) => typeSyntax.Identifier.Text;
public static AttributeSyntax? GetAttribute(this MemberDeclarationSyntax typeDeclarationSyntax, string attributeName)
{
var attributeLists = typeDeclarationSyntax.AttributeLists;
var selection = attributeLists
.SelectMany(al => al.Attributes)
.FirstOrDefault(x => x.Name.ToString().Contains(attributeName));
return selection;
}
public static bool HasAttribute(this ISymbol symbol, ITypeSymbol attributeSymbol) =>
symbol.GetAttributes().Any(a => a.AttributeClass?.Equals(attributeSymbol, SymbolEqualityComparer.Default) == true);
public static IEnumerable<AttributeData> GetAttributes(this ISymbol symbol, ITypeSymbol attributeSymbol) =>
symbol.GetAttributes().Where(a => a.AttributeClass?.Equals(attributeSymbol, SymbolEqualityComparer.Default) == true);
public static AttributeData? GetAttribute(this ISymbol symbol, ITypeSymbol attributeSymbol) =>
symbol.GetAttributes(attributeSymbol).FirstOrDefault();
public static string? GetNamespace(this MemberDeclarationSyntax typeDeclarationSyntax) => typeDeclarationSyntax
.Ancestors()
.OfType<NamespaceDeclarationSyntax>()
.FirstOrDefault()
?.Name
.ToString();
public static bool HasCompatibleTypes(this Compilation compilation, ISymbol source, ISymbol destination) =>
source.TryGetTypeSymbol(out var sourceType) && destination.TryGetTypeSymbol(out var destinationType) &&
(SymbolEqualityComparer.Default.Equals(destinationType, sourceType) || compilation.HasImplicitConversion(sourceType, destinationType));
public static bool TryGetTypeSymbol(this ISymbol symbol, [NotNullWhen(true)] out ITypeSymbol? typeSymbol)
{
switch (symbol)
{
case IPropertySymbol propertySymbol:
typeSymbol = propertySymbol.Type;
return true;
case IFieldSymbol fieldSymbol:
typeSymbol = fieldSymbol.Type;
return true;
case IParameterSymbol parameterSymbol:
typeSymbol = parameterSymbol.Type;
return true;
default:
typeSymbol = null;
return false;
}
}
public static ITypeSymbol? GetTypeSymbol(this ISymbol symbol) => symbol.TryGetTypeSymbol(out var typeSymbol) ? typeSymbol : null;
public static IPropertySymbol? FindProperty(this IEnumerable<IPropertySymbol> properties, IPropertySymbol targetProperty)
{
return properties.FirstOrDefault(p =>
p.Name == targetProperty.Name &&
(p.NullableAnnotation != NullableAnnotation.Annotated ||
p.NullableAnnotation == NullableAnnotation.Annotated &&
targetProperty.NullableAnnotation == NullableAnnotation.Annotated));
}
public static INamedTypeSymbol GetTypeByMetadataNameOrThrow(this Compilation compilation, string fullyQualifiedMetadataName) =>
compilation.GetTypeByMetadataName(fullyQualifiedMetadataName) ?? throw new TypeLoadException($"Unable to find '{fullyQualifiedMetadataName}' type.");
public static bool IsGenericEnumerable(this Compilation compilation, ITypeSymbol typeSymbol) =>
typeSymbol is INamedTypeSymbol { IsGenericType: true } &&
compilation.GetSpecialType(SpecialType.System_Collections_Generic_IEnumerable_T).Equals(typeSymbol.OriginalDefinition, SymbolEqualityComparer.Default);
public static bool IsArray(this Compilation compilation, ITypeSymbol typeSymbol) => typeSymbol is IArrayTypeSymbol;
public static bool IsPrimitiveType(this ITypeSymbol type) => type.SpecialType is
SpecialType.System_String or
SpecialType.System_Boolean or
SpecialType.System_SByte or
SpecialType.System_Int16 or
SpecialType.System_Int32 or
SpecialType.System_Int64 or
SpecialType.System_Byte or
SpecialType.System_UInt16 or
SpecialType.System_UInt32 or
SpecialType.System_UInt64 or
SpecialType.System_Single or
SpecialType.System_Double or
SpecialType.System_Char or
SpecialType.System_Object;
public static SyntaxNode? GetSyntaxNode(this ISymbol symbol) =>
symbol.Locations.FirstOrDefault() is { } location ? location.SourceTree?.GetRoot().FindNode(location.SourceSpan) : null;
public static IEnumerable<INamedTypeSymbol> GetTypesByMetadataName(this Compilation compilation, string typeMetadataName)
{
return compilation.References
.Select(compilation.GetAssemblyOrModuleSymbol)
.OfType<IAssemblySymbol>()
.Select(assemblySymbol => assemblySymbol.GetTypeByMetadataName(typeMetadataName))
.Where(t => t != null)!;
}
public static bool TypeByMetadataNameExists(this Compilation compilation, string typeMetadataName) => GetTypesByMetadataName(compilation, typeMetadataName).Any();
}
}

View File

@ -1,30 +1,30 @@
using System;
using System.Text;
namespace MapTo.Extensions
{
internal static class StringBuilderExtensions
{
public static StringBuilder PadLeft(this StringBuilder builder, int width)
{
for (var i = 0; i < width; i++)
{
builder.Append(" ");
}
return builder;
}
internal static StringBuilder AppendOpeningBracket(this StringBuilder builder, int indent = 0) => builder.AppendLine().PadLeft(indent).AppendFormat("{{{0}", Environment.NewLine);
internal static StringBuilder AppendClosingBracket(this StringBuilder builder, int indent = 0, bool padNewLine = true)
{
if (padNewLine)
{
builder.AppendLine();
}
return builder.PadLeft(indent).Append("}");
}
}
using System;
using System.Text;
namespace MapTo.Extensions
{
internal static class StringBuilderExtensions
{
public static StringBuilder PadLeft(this StringBuilder builder, int width)
{
for (var i = 0; i < width; i++)
{
builder.Append(" ");
}
return builder;
}
internal static StringBuilder AppendOpeningBracket(this StringBuilder builder, int indent = 0) => builder.AppendLine().PadLeft(indent).AppendFormat("{{{0}", Environment.NewLine);
internal static StringBuilder AppendClosingBracket(this StringBuilder builder, int indent = 0, bool padNewLine = true)
{
if (padNewLine)
{
builder.AppendLine();
}
return builder.PadLeft(indent).Append("}");
}
}
}

View File

@ -1,17 +1,17 @@
using System.Threading.Tasks;
namespace MapTo.Extensions
{
internal static class StringExtensions
{
public static string ToCamelCase(this string value) => string.IsNullOrWhiteSpace(value) ? value : $"{char.ToLower(value[0])}{value.Substring(1)}";
public static string ToSourceCodeString(this object? value) => value switch
{
null => "null",
string strValue => $"\"{strValue}\"",
char charValue => $"'{charValue}'",
_ => value.ToString()
};
}
using System.Threading.Tasks;
namespace MapTo.Extensions
{
internal static class StringExtensions
{
public static string ToCamelCase(this string value) => string.IsNullOrWhiteSpace(value) ? value : $"{char.ToLower(value[0])}{value.Substring(1)}";
public static string ToSourceCodeString(this object? value) => value switch
{
null => "null",
string strValue => $"\"{strValue}\"",
char charValue => $"'{charValue}'",
_ => value.ToString()
};
}
}

View File

@ -1,5 +1,5 @@
<Project>
<ItemGroup>
<CompilerVisibleProperty Include="MapTo_ConstructorAccessModifier" />
</ItemGroup>
<Project>
<ItemGroup>
<CompilerVisibleProperty Include="MapTo_ConstructorAccessModifier" />
</ItemGroup>
</Project>

View File

@ -1,74 +1,81 @@
using System;
using System.Collections.Generic;
using System.Linq;
using MapTo.Extensions;
using MapTo.Sources;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace MapTo
{
/// <summary>
/// MapTo source generator.
/// </summary>
[Generator]
public class MapToGenerator : ISourceGenerator
{
/// <inheritdoc />
public void Initialize(GeneratorInitializationContext context)
{
context.RegisterForSyntaxNotifications(() => new MapToSyntaxReceiver());
}
/// <inheritdoc />
public void Execute(GeneratorExecutionContext context)
{
try
{
var options = SourceGenerationOptions.From(context);
var compilation = context.Compilation
.AddSource(ref context, MapFromAttributeSource.Generate(options))
.AddSource(ref context, IgnorePropertyAttributeSource.Generate(options))
.AddSource(ref context, ITypeConverterSource.Generate(options))
.AddSource(ref context, MapTypeConverterAttributeSource.Generate(options))
.AddSource(ref context, MapPropertyAttributeSource.Generate(options))
.AddSource(ref context, MappingContextSource.Generate(options));
if (context.SyntaxReceiver is MapToSyntaxReceiver receiver && receiver.CandidateTypes.Any())
{
AddGeneratedMappingsClasses(context, compilation, receiver.CandidateTypes, options);
}
}
catch (Exception ex)
{
Console.WriteLine(ex);
throw;
}
}
private static void AddGeneratedMappingsClasses(GeneratorExecutionContext context, Compilation compilation, IEnumerable<TypeDeclarationSyntax> candidateTypes, SourceGenerationOptions options)
{
foreach (var typeDeclarationSyntax in candidateTypes)
{
var mappingContext = MappingContext.Create(compilation, options, typeDeclarationSyntax);
mappingContext.Diagnostics.ForEach(context.ReportDiagnostic);
if (mappingContext.Model is null)
{
continue;
}
var (source, hintName) = typeDeclarationSyntax switch
{
StructDeclarationSyntax => MapStructSource.Generate(mappingContext.Model),
ClassDeclarationSyntax => MapClassSource.Generate(mappingContext.Model),
RecordDeclarationSyntax => MapRecordSource.Generate(mappingContext.Model),
_ => throw new ArgumentOutOfRangeException()
};
context.AddSource(hintName, source);
}
}
}
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using MapTo.Extensions;
using MapTo.Sources;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace MapTo
{
/// <summary>
/// MapTo source generator.
/// </summary>
[Generator]
public class MapToGenerator : ISourceGenerator
{
/// <inheritdoc />
public void Initialize(GeneratorInitializationContext context)
{
context.RegisterForSyntaxNotifications(() => new MapToSyntaxReceiver());
}
/// <inheritdoc />
public void Execute(GeneratorExecutionContext context)
{
try
{
var options = SourceGenerationOptions.From(context);
var compilation = context.Compilation
.AddSource(ref context, UseUpdateAttributeSource.Generate(options))
.AddSource(ref context, JsonExtensionAttributeSource.Generate(options))
.AddSource(ref context, MapFromAttributeSource.Generate(options))
.AddSource(ref context, IgnoreMemberAttributeSource.Generate(options))
.AddSource(ref context, ITypeConverterSource.Generate(options))
.AddSource(ref context, MapTypeConverterAttributeSource.Generate(options))
.AddSource(ref context, MapPropertyAttributeSource.Generate(options))
.AddSource(ref context, MappingContextSource.Generate(options));
if (context.SyntaxReceiver is MapToSyntaxReceiver receiver && receiver.CandidateTypes.Any())
{
AddGeneratedMappingsClasses(context, compilation, receiver.CandidateTypes, options);
}
}
catch (Exception ex)
{
Console.WriteLine(ex);
throw;
}
}
private static void AddGeneratedMappingsClasses(GeneratorExecutionContext context, Compilation compilation, IEnumerable<TypeDeclarationSyntax> candidateTypes, SourceGenerationOptions options)
{
foreach (var typeDeclarationSyntax in candidateTypes)
{
var mappingContext = MappingContext.Create(compilation, options, typeDeclarationSyntax);
mappingContext.Diagnostics.ForEach(context.ReportDiagnostic);
if (mappingContext.Model is null)
{
continue;
}
var (source, hintName) = typeDeclarationSyntax switch
{
StructDeclarationSyntax => MapStructSource.Generate(mappingContext.Model),
ClassDeclarationSyntax => MapClassSource.Generate(mappingContext.Model),
RecordDeclarationSyntax => MapRecordSource.Generate(mappingContext.Model),
_ => throw new ArgumentOutOfRangeException()
};
context.AddSource(hintName, source);
}
}
}
}

View File

@ -1,39 +1,39 @@
using System.Collections.Generic;
using System.Linq;
using MapTo.Sources;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace MapTo
{
internal class MapToSyntaxReceiver : ISyntaxReceiver
{
public List<TypeDeclarationSyntax> CandidateTypes { get; } = new();
/// <inheritdoc />
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
if (syntaxNode is not TypeDeclarationSyntax { AttributeLists: { Count: >= 1 } attributes } typeDeclarationSyntax)
{
return;
}
var attributeSyntax = attributes
.SelectMany(a => a.Attributes)
.SingleOrDefault(a => a.Name is
IdentifierNameSyntax { Identifier: { ValueText: MapFromAttributeSource.AttributeName } } // For: [MapFrom]
or
QualifiedNameSyntax // For: [MapTo.MapFrom]
{
Left: IdentifierNameSyntax { Identifier: { ValueText: Constants.RootNamespace } },
Right: IdentifierNameSyntax { Identifier: { ValueText: MapFromAttributeSource.AttributeName } }
}
);
if (attributeSyntax is not null)
{
CandidateTypes.Add(typeDeclarationSyntax);
}
}
}
using System.Collections.Generic;
using System.Linq;
using MapTo.Sources;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace MapTo
{
internal class MapToSyntaxReceiver : ISyntaxReceiver
{
public List<TypeDeclarationSyntax> CandidateTypes { get; } = new();
/// <inheritdoc />
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
if (syntaxNode is not TypeDeclarationSyntax { AttributeLists: { Count: >= 1 } attributes } typeDeclarationSyntax)
{
return;
}
var attributeSyntax = attributes
.SelectMany(a => a.Attributes)
.FirstOrDefault(a => a.Name is
IdentifierNameSyntax { Identifier: { ValueText: MapFromAttributeSource.AttributeName } } // For: [MapFrom]
or
QualifiedNameSyntax // For: [MapTo.MapFrom]
{
Left: IdentifierNameSyntax { Identifier: { ValueText: Constants.RootNamespace } },
Right: IdentifierNameSyntax { Identifier: { ValueText: MapFromAttributeSource.AttributeName } }
}
);
if (attributeSyntax is not null)
{
CandidateTypes.Add(typeDeclarationSyntax);
}
}
}
}

View File

@ -0,0 +1,630 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using MapTo.Extensions;
using MapTo.Sources;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
#pragma warning disable CS8602
namespace MapTo
{
internal static class MappingContextExtensions
{
internal static ImmutableArray<MappedMember> GetReadOnlyMappedProperties(this ImmutableArray<MappedMember> mappedProperties) => mappedProperties.Where(p => p.isReadOnly).ToImmutableArray()!;
internal static ImmutableArray<MappedMember> GetWritableMappedProperties(this ImmutableArray<MappedMember> mappedProperties) => mappedProperties.Where(p => !p.isReadOnly).ToImmutableArray()!;
}
internal abstract class MappingContext
{
private readonly List<SymbolDisplayPart> _ignoredNamespaces;
protected MappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax)
{
_ignoredNamespaces = new();
Diagnostics = ImmutableArray<Diagnostic>.Empty;
Usings = ImmutableArray.Create("System", Constants.RootNamespace);
SourceGenerationOptions = sourceGenerationOptions;
TypeSyntax = typeSyntax;
Compilation = compilation;
IgnoreMemberAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(IgnoreMemberAttributeSource.FullyQualifiedName);
MapTypeConverterAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MapTypeConverterAttributeSource.FullyQualifiedName);
TypeConverterInterfaceTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(ITypeConverterSource.FullyQualifiedName);
MapPropertyAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MapPropertyAttributeSource.FullyQualifiedName);
MapFromAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MapFromAttributeSource.FullyQualifiedName);
UseUpdateAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(UseUpdateAttributeSource.FullyQualifiedName);
JsonExtensionAttributeSymbol = compilation.GetTypeByMetadataNameOrThrow(JsonExtensionAttributeSource.FullyQualifiedName);
MappingContextTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MappingContextSource.FullyQualifiedName);
AddUsingIfRequired(sourceGenerationOptions.SupportNullableStaticAnalysis, "System.Diagnostics.CodeAnalysis");
}
public ImmutableArray<Diagnostic> Diagnostics { get; private set; }
public MappingModel? Model { get; private set; }
protected Compilation Compilation { get; }
protected INamedTypeSymbol IgnoreMemberAttributeTypeSymbol { get; }
protected INamedTypeSymbol MapFromAttributeTypeSymbol { get; }
protected INamedTypeSymbol UseUpdateAttributeTypeSymbol { get; }
public static INamedTypeSymbol JsonExtensionAttributeSymbol { get; set; } = null!;
protected INamedTypeSymbol MappingContextTypeSymbol { get; }
protected INamedTypeSymbol MapPropertyAttributeTypeSymbol { get; }
protected INamedTypeSymbol MapTypeConverterAttributeTypeSymbol { get; }
protected SourceGenerationOptions SourceGenerationOptions { get; }
protected INamedTypeSymbol TypeConverterInterfaceTypeSymbol { get; }
protected TypeDeclarationSyntax TypeSyntax { get; }
protected ImmutableArray<string> Usings { get; private set; }
public static MappingContext Create(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax)
{
MappingContext context = typeSyntax switch
{
StructDeclarationSyntax => new StructMappingContext(compilation, sourceGenerationOptions, typeSyntax),
ClassDeclarationSyntax => new ClassMappingContext(compilation, sourceGenerationOptions, typeSyntax),
RecordDeclarationSyntax => new RecordMappingContext(compilation, sourceGenerationOptions, typeSyntax),
_ => throw new ArgumentOutOfRangeException()
};
context.Model = context.CreateMappingModel();
return context;
}
protected void AddDiagnostic(Diagnostic diagnostic)
{
Diagnostics = Diagnostics.Add(diagnostic);
}
protected void AddUsingIfRequired(ISymbol? namedTypeSymbol) =>
AddUsingIfRequired(namedTypeSymbol?.ContainingNamespace.IsGlobalNamespace == false, namedTypeSymbol?.ContainingNamespace);
protected void AddUsingIfRequired(bool condition, INamespaceSymbol? ns) =>
AddUsingIfRequired(condition && ns is not null && !_ignoredNamespaces.Contains(ns.ToDisplayParts().First()), ns?.ToDisplayString());
protected void AddUsingIfRequired(bool condition, string? ns)
{
if (ns is not null && condition && ns != TypeSyntax.GetNamespace() && !Usings.Contains(ns))
{
Usings = Usings.Add(ns);
}
}
protected IPropertySymbol? FindSourceProperty(IEnumerable<IPropertySymbol> sourceProperties, ISymbol property)
{
var propertyName = property
.GetAttribute(MapPropertyAttributeTypeSymbol)
?.NamedArguments
.FirstOrDefault(a => a.Key == MapPropertyAttributeSource.SourcePropertyNamePropertyName)
.Value.Value as string ?? property.Name;
return sourceProperties.FirstOrDefault(p => p.Name == propertyName);
}
protected IFieldSymbol? FindSourceField(IEnumerable<IFieldSymbol> sourceProperties, ISymbol property)
{
var propertyName = property
.GetAttribute(MapPropertyAttributeTypeSymbol)
?.NamedArguments
.FirstOrDefault(a => a.Key == MapPropertyAttributeSource.SourcePropertyNamePropertyName)
.Value.Value as string ?? property.Name;
return sourceProperties.FirstOrDefault(p => p.Name == propertyName);
}
protected abstract ImmutableArray<MappedMember> GetSourceMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass);
protected abstract ImmutableArray<MappedMember> GetTypeMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass);
protected abstract ImmutableArray<MappedMember> GetSourceMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass);
protected abstract ImmutableArray<MappedMember> GetTypeMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass);
protected ImmutableArray<INamedTypeSymbol> GetSourceTypeSymbol(TypeDeclarationSyntax typeDeclarationSyntax, SemanticModel? semanticModel = null)
{
var attributeData = typeDeclarationSyntax.GetAttribute(MapFromAttributeSource.AttributeName);
var sourceSymbol = GetSourceTypeSymbol(attributeData, semanticModel);
return sourceSymbol;
}
// we need two possible InamedTypeSymbol
protected ImmutableArray<INamedTypeSymbol> GetSourceTypeSymbol(SyntaxNode? attributeSyntax, SemanticModel? semanticModel = null)
{
if (attributeSyntax is null)
{
return new ImmutableArray<INamedTypeSymbol>(){};
}
semanticModel ??= Compilation.GetSemanticModel(attributeSyntax.SyntaxTree);
var descendentNodes = attributeSyntax
.DescendantNodes();
var sourceTypeExpressionSyntax = descendentNodes
.OfType<TypeOfExpressionSyntax>()
.ToImmutableArray();
// cast
var resultList = new List<INamedTypeSymbol>();
for (int i = 0; i < sourceTypeExpressionSyntax.Length; i++)
{
var sourceTypeExpression = sourceTypeExpressionSyntax[i];
if (semanticModel.GetTypeInfo(sourceTypeExpression.Type).Type is INamedTypeSymbol namedTypeSymbol)
{
resultList.Add(namedTypeSymbol);
}
}
return resultList.ToImmutableArray();
}
protected bool IsTypeInheritFromMappedBaseClass(SemanticModel semanticModel)
{
return TypeSyntax.BaseList is not null && TypeSyntax.BaseList.Types
.Select(t => semanticModel.GetTypeInfo(t.Type).Type)
.Any(t => t?.GetAttribute(MapFromAttributeTypeSymbol) != null);
}
protected bool IsTypeUpdatable()
{
return TypeSyntax.GetAttribute("UseUpdate") != null;
}
protected bool HasJsonExtension()
{
return TypeSyntax.GetAttribute("JsonExtension") != null;
}
protected virtual MappedMember? MapProperty(ISymbol sourceTypeSymbol, IReadOnlyCollection<IPropertySymbol> sourceProperties, ISymbol property)
{
var sourceProperty = FindSourceProperty(sourceProperties, property);
if (sourceProperty is null || !property.TryGetTypeSymbol(out var propertyType))
{
return null;
}
string? converterFullyQualifiedName = null;
var converterParameters = ImmutableArray<string>.Empty;
ITypeSymbol? mappedSourcePropertyType = null;
ITypeSymbol? enumerableTypeArgumentType = null;
if (!Compilation.HasCompatibleTypes(sourceProperty, property))
{
if (!TryGetMapTypeConverterForProperty(property, sourceProperty, out converterFullyQualifiedName, out converterParameters) &&
!TryGetNestedObjectMappings(property, out mappedSourcePropertyType, out enumerableTypeArgumentType))
{
return null;
}
}
AddUsingIfRequired(propertyType);
AddUsingIfRequired(enumerableTypeArgumentType);
AddUsingIfRequired(mappedSourcePropertyType);
INamedTypeSymbol? namedType;
var isEnumerable = IsEnumerable(property, out namedType);
return new MappedMember(
property.Name,
property.GetTypeSymbol().ToString(),
ToQualifiedDisplayName(propertyType) ?? propertyType.Name,
converterFullyQualifiedName,
converterParameters.ToImmutableArray(),
sourceProperty.Name,
ToQualifiedDisplayName(mappedSourcePropertyType),
ToQualifiedDisplayName(enumerableTypeArgumentType),
property,
namedType,
isEnumerable,
(property as IPropertySymbol).IsReadOnly);
;
}
protected virtual MappedMember? MapField(ISymbol sourceTypeSymbol, IReadOnlyCollection<IFieldSymbol> sourceProperties, ISymbol property)
{
var sourceProperty = FindSourceField(sourceProperties, property);
if (sourceProperty is null || !property.TryGetTypeSymbol(out var propertyType))
{
return null;
}
if (property is IFieldSymbol symbol)
{
if (symbol.AssociatedSymbol != null) return null;
}
string? converterFullyQualifiedName = null;
var converterParameters = ImmutableArray<string>.Empty;
ITypeSymbol? mappedSourcePropertyType = null;
ITypeSymbol? enumerableTypeArgumentType = null;
if (!Compilation.HasCompatibleTypes(sourceProperty, property))
{
if (!TryGetMapTypeConverterForField(property, sourceProperty, out converterFullyQualifiedName, out converterParameters) &&
!TryGetNestedObjectMappings(property, out mappedSourcePropertyType, out enumerableTypeArgumentType))
{
return null;
}
}
AddUsingIfRequired(propertyType);
AddUsingIfRequired(enumerableTypeArgumentType);
AddUsingIfRequired(mappedSourcePropertyType);
INamedTypeSymbol? namedType;
var isEnumerable = IsEnumerable(property, out namedType);
return new MappedMember(
property.Name,
property.GetTypeSymbol()?.ToString() ?? throw new InvalidOperationException(),
ToQualifiedDisplayName(propertyType) ?? propertyType.Name,
converterFullyQualifiedName,
converterParameters.ToImmutableArray(),
sourceProperty.Name,
ToQualifiedDisplayName(mappedSourcePropertyType),
ToQualifiedDisplayName(enumerableTypeArgumentType),
property,
namedType,
isEnumerable,
(property as IFieldSymbol).IsReadOnly);
;
}
protected virtual MappedMember? MapPropertySimple(ISymbol sourceTypeSymbol, ISymbol property)
{
if (!property.TryGetTypeSymbol(out var propertyType))
{
return null;
}
string? converterFullyQualifiedName = null;
var converterParameters = ImmutableArray<string>.Empty;
ITypeSymbol? mappedSourcePropertyType = null;
ITypeSymbol? enumerableTypeArgumentType = null;
AddUsingIfRequired(propertyType);
AddUsingIfRequired(enumerableTypeArgumentType);
AddUsingIfRequired(mappedSourcePropertyType);
INamedTypeSymbol? namedType;
var isEnumerable = IsEnumerable(property, out namedType);
return new MappedMember(
property.Name,
property.GetTypeSymbol().ToString(),
ToQualifiedDisplayName(propertyType) ?? propertyType.Name,
converterFullyQualifiedName,
converterParameters.ToImmutableArray(),
property.Name,
ToQualifiedDisplayName(mappedSourcePropertyType),
ToQualifiedDisplayName(enumerableTypeArgumentType),
property,
namedType,
isEnumerable,
#pragma warning disable CS8602
(property as IPropertySymbol).IsReadOnly);
#pragma warning restore CS8602
;
}
protected virtual MappedMember? MapFieldSimple(ISymbol sourceTypeSymbol, ISymbol property)
{
if (!property.TryGetTypeSymbol(out var propertyType))
{
return null;
}
if(property is IFieldSymbol symbol)
{
if (symbol.AssociatedSymbol != null) return null;
}
string? converterFullyQualifiedName = null;
var converterParameters = ImmutableArray<string>.Empty;
ITypeSymbol? mappedSourcePropertyType = null;
ITypeSymbol? enumerableTypeArgumentType = null;
AddUsingIfRequired(propertyType);
AddUsingIfRequired(enumerableTypeArgumentType);
AddUsingIfRequired(mappedSourcePropertyType);
INamedTypeSymbol? namedType;
var isEnumerable = IsEnumerable(property, out namedType);
return new MappedMember(
property.Name,
property.GetTypeSymbol().ToString(),
ToQualifiedDisplayName(propertyType) ?? propertyType.Name,
converterFullyQualifiedName,
converterParameters.ToImmutableArray(),
property.Name,
ToQualifiedDisplayName(mappedSourcePropertyType),
ToQualifiedDisplayName(enumerableTypeArgumentType),
property,
namedType,
isEnumerable,
(property as IFieldSymbol).IsReadOnly);
;
}
protected bool TryGetMapTypeConverterForProperty(ISymbol property, IPropertySymbol sourceProperty, out string? converterFullyQualifiedName,
out ImmutableArray<string> converterParameters)
{
converterFullyQualifiedName = null;
converterParameters = ImmutableArray<string>.Empty;
if (!Diagnostics.IsEmpty())
{
return false;
}
var typeConverterAttribute = property.GetAttribute(MapTypeConverterAttributeTypeSymbol);
if (typeConverterAttribute?.ConstructorArguments.First().Value is not INamedTypeSymbol converterTypeSymbol)
{
return false;
}
var baseInterface = GetTypeConverterBaseInterfaceForProperty(converterTypeSymbol, property, sourceProperty);
if (baseInterface is null)
{
AddDiagnostic(DiagnosticsFactory.InvalidTypeConverterGenericTypesError(property, sourceProperty));
return false;
}
converterFullyQualifiedName = converterTypeSymbol.ToDisplayString();
converterParameters = GetTypeConverterParameters(typeConverterAttribute);
return true;
}
protected bool TryGetMapTypeConverterForField(ISymbol property, IFieldSymbol sourceProperty, out string? converterFullyQualifiedName,
out ImmutableArray<string> converterParameters)
{
converterFullyQualifiedName = null;
converterParameters = ImmutableArray<string>.Empty;
if (!Diagnostics.IsEmpty())
{
return false;
}
var typeConverterAttribute = property.GetAttribute(MapTypeConverterAttributeTypeSymbol);
if (typeConverterAttribute?.ConstructorArguments.First().Value is not INamedTypeSymbol converterTypeSymbol)
{
return false;
}
var baseInterface = GetTypeConverterBaseInterfaceForField(converterTypeSymbol, property, sourceProperty);
if (baseInterface is null)
{
//AddDiagnostic(DiagnosticsFactory.InvalidTypeConverterGenericTypesError(property, null));
return false;
}
converterFullyQualifiedName = converterTypeSymbol.ToDisplayString();
converterParameters = GetTypeConverterParameters(typeConverterAttribute);
return true;
}
protected bool TryGetNestedObjectMappings(ISymbol property, out ITypeSymbol? mappedSourcePropertyType, out ITypeSymbol? enumerableTypeArgument)
{
mappedSourcePropertyType = null;
enumerableTypeArgument = null;
if (!Diagnostics.IsEmpty())
{
return false;
}
if (!property.TryGetTypeSymbol(out var propertyType))
{
AddDiagnostic(DiagnosticsFactory.NoMatchingPropertyTypeFoundError(property));
return false;
}
var mapFromAttribute = propertyType.GetAttribute(MapFromAttributeTypeSymbol);
if (mapFromAttribute is null &&
propertyType is INamedTypeSymbol namedTypeSymbol &&
!propertyType.IsPrimitiveType() &&
(Compilation.IsGenericEnumerable(propertyType) || propertyType.AllInterfaces.Any(i => Compilation.IsGenericEnumerable(i))))
{
enumerableTypeArgument = namedTypeSymbol.TypeArguments.First();
mapFromAttribute = enumerableTypeArgument.GetAttribute(MapFromAttributeTypeSymbol);
}
mappedSourcePropertyType = mapFromAttribute?.ConstructorArguments.First().Value as INamedTypeSymbol;
if (mappedSourcePropertyType is null && enumerableTypeArgument is null)
{
AddDiagnostic(DiagnosticsFactory.NoMatchingPropertyTypeFoundError(property));
}
return Diagnostics.IsEmpty();
}
protected bool IsEnumerable(ISymbol property, out INamedTypeSymbol? namedTypeSymbolResult)
{
if (!property.TryGetTypeSymbol(out var propertyType))
{
AddDiagnostic(DiagnosticsFactory.NoMatchingPropertyTypeFoundError(property));
namedTypeSymbolResult = null;
return false;
}
if (
propertyType is INamedTypeSymbol namedTypeSymbol &&
!propertyType.IsPrimitiveType() &&
(Compilation.IsGenericEnumerable(propertyType) || propertyType.AllInterfaces.Any(i => Compilation.IsGenericEnumerable(i))))
{
namedTypeSymbolResult = namedTypeSymbol;
return true;
}
namedTypeSymbolResult = null;
return false;
}
private static ImmutableArray<string> GetTypeConverterParameters(AttributeData typeConverterAttribute)
{
var converterParameter = typeConverterAttribute.ConstructorArguments.Skip(1).FirstOrDefault();
return converterParameter.IsNull
? ImmutableArray<string>.Empty
: converterParameter.Values.Where(v => v.Value is not null).Select(v => v.Value!.ToSourceCodeString()).ToImmutableArray();
}
private MappingModel? CreateMappingModel()
{
var semanticModel = Compilation.GetSemanticModel(TypeSyntax.SyntaxTree);
if (semanticModel.GetDeclaredSymbol(TypeSyntax) is not INamedTypeSymbol typeSymbol)
{
AddDiagnostic(DiagnosticsFactory.TypeNotFoundError(TypeSyntax.GetLocation(), TypeSyntax.Identifier.ValueText));
return null;
}
// We can have 2 sources...
var sourceTypeSymbols = GetSourceTypeSymbol(TypeSyntax, semanticModel);
// lets pick one for now, and then think what to do with the second one
if (sourceTypeSymbols.IsDefaultOrEmpty)
{
AddDiagnostic(DiagnosticsFactory.MapFromAttributeNotFoundError(TypeSyntax.GetLocation()));
return null;
}
var typeIdentifierName = TypeSyntax.GetIdentifierName();
var isTypeInheritFromMappedBaseClass = IsTypeInheritFromMappedBaseClass(semanticModel);
var isTypeUpdatable = true; //IsTypeUpdatable();
var hasJsonExtension = false; // HasJsonExtension();
List<MappedSourceType> mappedSourceTypes = new List<MappedSourceType>();
foreach (var sourceTypeSymbol in sourceTypeSymbols)
{
_ignoredNamespaces.Add(sourceTypeSymbol.ContainingNamespace.ToDisplayParts().First());
var sourceTypeIdentifierName = sourceTypeSymbol.Name;
var shouldGenerateSecondaryConstructor = ShouldGenerateSecondaryConstructor(semanticModel, sourceTypeSymbol);
var mappedProperties = GetSourceMappedProperties(typeSymbol, sourceTypeSymbol, isTypeInheritFromMappedBaseClass);
var mappedFields = GetSourceMappedFields(typeSymbol, sourceTypeSymbol, isTypeInheritFromMappedBaseClass);
AddUsingIfRequired(mappedProperties.Any(p => p.IsEnumerable), "System.Linq");
var allProperties = GetTypeMappedProperties(sourceTypeSymbol, typeSymbol , isTypeInheritFromMappedBaseClass);
var allFields = GetTypeMappedFields(sourceTypeSymbol, typeSymbol, isTypeInheritFromMappedBaseClass);
mappedSourceTypes.Add(new MappedSourceType(
sourceTypeSymbol.ContainingNamespace.ToDisplayString(),
sourceTypeIdentifierName,
sourceTypeSymbol.ToDisplayString(),
mappedProperties, mappedFields, allProperties, allFields, shouldGenerateSecondaryConstructor));
}
//var sourceTypeSymbol = sourceTypeSymbols[0];
// Pick first one to avoid errors. TODO: Make possible to use different source types
/*if (!mappedProperties.Any())
{
AddDiagnostic(DiagnosticsFactory.NoMatchingPropertyFoundError(TypeSyntax.GetLocation(), typeSymbol, sourceTypeSymbol));
return null;
}*/
return new MappingModel(
SourceGenerationOptions,
TypeSyntax.GetNamespace(),
TypeSyntax.Modifiers,
TypeSyntax.Keyword.Text,
typeIdentifierName,
isTypeUpdatable,
hasJsonExtension,
mappedSourceTypes.ToImmutableArray(),
isTypeInheritFromMappedBaseClass,
Usings);
}
private INamedTypeSymbol? GetTypeConverterBaseInterfaceForProperty(ITypeSymbol converterTypeSymbol, ISymbol property, IPropertySymbol sourceProperty)
{
if (!property.TryGetTypeSymbol(out var propertyType))
{
return null;
}
return converterTypeSymbol.AllInterfaces
.FirstOrDefault(i =>
i.TypeArguments.Length == 2 &&
SymbolEqualityComparer.Default.Equals(i.ConstructedFrom, TypeConverterInterfaceTypeSymbol) &&
SymbolEqualityComparer.Default.Equals(sourceProperty.Type, i.TypeArguments[0]) &&
SymbolEqualityComparer.Default.Equals(propertyType, i.TypeArguments[1]));
}
private INamedTypeSymbol? GetTypeConverterBaseInterfaceForField(ITypeSymbol converterTypeSymbol, ISymbol property, IFieldSymbol sourceProperty)
{
if (!property.TryGetTypeSymbol(out var propertyType))
{
return null;
}
return converterTypeSymbol.AllInterfaces
.FirstOrDefault(i =>
i.TypeArguments.Length == 2 &&
SymbolEqualityComparer.Default.Equals(i.ConstructedFrom, TypeConverterInterfaceTypeSymbol) &&
SymbolEqualityComparer.Default.Equals(sourceProperty.Type, i.TypeArguments[0]) &&
SymbolEqualityComparer.Default.Equals(propertyType, i.TypeArguments[1]));
}
private bool ShouldGenerateSecondaryConstructor(SemanticModel semanticModel, ISymbol sourceTypeSymbol)
{
var constructorSyntax = TypeSyntax.DescendantNodes()
.OfType<ConstructorDeclarationSyntax>()
.FirstOrDefault(c =>
c.ParameterList.Parameters.Count == 1 &&
SymbolEqualityComparer.Default.Equals(semanticModel.GetTypeInfo(c.ParameterList.Parameters.Single().Type!).ConvertedType, sourceTypeSymbol));
if (constructorSyntax is null)
{
// Secondary constructor is not defined.
return true;
}
if (constructorSyntax.Initializer?.ArgumentList.Arguments is not { Count: 2 } arguments ||
!SymbolEqualityComparer.Default.Equals(semanticModel.GetTypeInfo(arguments[0].Expression).ConvertedType, MappingContextTypeSymbol) ||
!SymbolEqualityComparer.Default.Equals(semanticModel.GetTypeInfo(arguments[1].Expression).ConvertedType, sourceTypeSymbol))
{
AddDiagnostic(DiagnosticsFactory.MissingConstructorArgument(constructorSyntax));
}
return false;
}
private string? ToQualifiedDisplayName(ISymbol? symbol)
{
if (symbol is null)
{
return null;
}
var containingNamespace = TypeSyntax.GetNamespace();
var symbolNamespace = symbol.ContainingNamespace.ToDisplayString();
return containingNamespace != symbolNamespace && _ignoredNamespaces.Contains(symbol.ContainingNamespace.ToDisplayParts().First())
? symbol.ToDisplayString()
: symbol.Name;
}
}
}

View File

@ -0,0 +1,222 @@
using System;
using System.Collections.Immutable;
using MapTo.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
namespace MapTo
{
internal enum AccessModifier
{
Public,
Internal,
Private
}
internal enum NullStaticAnalysisState
{
Default,
Enabled,
Disabled
}
internal record SourceCode(string Text, string HintName);
internal record MappedMember(
string Name,
string FullyQualifiedType,
string Type,
string? TypeConverter,
ImmutableArray<string> TypeConverterParameters,
string SourcePropertyName,
string? MappedSourcePropertyTypeName,
string? EnumerableTypeArgument,
ISymbol ActualSymbol,
INamedTypeSymbol? NamedTypeSymbol,
bool isEnumerable,
bool isReadOnly)
{
public bool IsEnumerable => EnumerableTypeArgument is not null;
}
internal record MappedSourceType
(
string SourceNamespace,
string SourceTypeIdentifierName,
string SourceTypeFullName,
ImmutableArray<MappedMember> SourceProperties,
ImmutableArray<MappedMember> SourceFields,
ImmutableArray<MappedMember> TypeProperties,
ImmutableArray<MappedMember> TypeFields,
bool GenerateSecondaryConstructor
)
{
public string SourceType => SourceTypeFullName;
}
internal record MappingModel(
SourceGenerationOptions Options,
string? Namespace,
SyntaxTokenList Modifiers,
string Type,
string TypeIdentifierName,
bool IsTypeUpdatable,
bool IsJsonExtension,
ImmutableArray<MappedSourceType> MappedSourceTypes,
bool HasMappedBaseClass,
ImmutableArray<string> Usings
);
internal interface IEfMethodsModel { }
internal record EfMethodsModel(
SourceGenerationOptions Options,
string Namespace,
string ContextTypeName,
string ContextFullType,
ImmutableArray<EfEntityDataModel> MethodsModels,
ImmutableArray<string> Usings
);
internal class EfEntityDataModel
{
public string PropertyName { get; set; }
public string EntityTypeFullName { get; set; }
public string EntityTypeIdentifierName { get; set; }
public EfEntityDataModel(string propertyName, string entityTypeFullName, string entityTypeIdentifierName)
{
PropertyName = propertyName;
EntityTypeFullName = entityTypeFullName;
EntityTypeIdentifierName = entityTypeIdentifierName;
}
}
internal class EfAddMethodsModel : EfEntityDataModel
{
public string CreateTypeFullName { get; set; }
public string CreateTypeIdentifierName { get; set; }
public string ReturnTypeFullName { get; set; }
public string ReturnTypeIdentifierName { get; set; }
public EfAddMethodsModel(EfEntityDataModel entity, string createTypeFullName,
string createTypeIdentifierName,
string returnTypeFullName,
string returnTypeIdentifierName) : base(entity.PropertyName, entity.EntityTypeFullName, entity.EntityTypeIdentifierName)
{
CreateTypeFullName = createTypeFullName;
CreateTypeIdentifierName = createTypeIdentifierName;
ReturnTypeIdentifierName = returnTypeIdentifierName;
ReturnTypeFullName = returnTypeFullName;
}
}
internal class EfGetOneByModel : EfEntityDataModel
{
public string ByParamPropertyName { get; set; }
public string ByParamFullTypeName { get; set; }
public string ByParamTypeName { get; set; }
public string ReturnTypeIdentifierName { get; set; }
public string ReturnTypeFullName { get; set; }
public EfGetOneByModel(EfEntityDataModel entity, string byParamPropertyName,
string byParamFullTypeName,
string returnTypeFullName,
string returnTypeIdentifierName) : base(entity.PropertyName, entity.EntityTypeFullName, entity.EntityTypeIdentifierName)
{
ByParamPropertyName = byParamPropertyName;
ByParamFullTypeName = byParamFullTypeName;
ReturnTypeIdentifierName = returnTypeIdentifierName;
ReturnTypeFullName = returnTypeFullName;
ByParamTypeName = returnTypeFullName;
}
}
internal class EfGetOneWithModel : EfEntityDataModel
{
public string ReturnTypeIdentifierName { get; set; }
public string ReturnTypeFullName { get; set; }
public EfGetOneWithModel(EfEntityDataModel entity, string returnTypeFullName,
string returnTypeIdentifierName) : base(entity.PropertyName, entity.EntityTypeFullName, entity.EntityTypeIdentifierName)
{
ReturnTypeIdentifierName = returnTypeIdentifierName;
ReturnTypeFullName = returnTypeFullName;
}
}
internal class EfGetManyModel : EfEntityDataModel
{
public string ReturnTypeIdentifierName { get; set; }
public string ReturnTypeFullName { get; set; }
public EfGetManyModel(EfEntityDataModel entity, string returnTypeFullName,
string returnTypeIdentifierName) : base(entity.PropertyName, entity.EntityTypeFullName, entity.EntityTypeIdentifierName)
{
ReturnTypeIdentifierName = returnTypeIdentifierName;
ReturnTypeFullName = returnTypeFullName;
}
}
internal class EfUpdateMethodsModel : EfEntityDataModel
{
public string UpdateTypeFullName;
public string UpdateTypeIdentifierName;
public string ReturnTypeFullName;
public string ReturnTypeIdentifierName;
public string KeyPropertyName;
public string KeyFullTypeName;
public EfUpdateMethodsModel(EfEntityDataModel entity,
string updateTypeFullName,
string updateTypeIdentifierName,
string returnTypeFullName,
string returnTypeIdentifierName,
string keyPropertyName,
string keyFullTypeName) : base(entity.PropertyName, entity.EntityTypeFullName, entity.EntityTypeIdentifierName)
{
UpdateTypeFullName = updateTypeFullName;
UpdateTypeIdentifierName = updateTypeIdentifierName;
ReturnTypeFullName= returnTypeFullName;
ReturnTypeIdentifierName = returnTypeIdentifierName;
KeyPropertyName = keyPropertyName;
KeyFullTypeName = keyFullTypeName;
}
}
internal record SourceGenerationOptions(
AccessModifier ConstructorAccessModifier,
AccessModifier GeneratedMethodsAccessModifier,
bool GenerateXmlDocument,
bool SupportNullableReferenceTypes,
bool SupportNullableStaticAnalysis)
{
internal static SourceGenerationOptions From(GeneratorExecutionContext context)
{
const string allowNullAttributeName = "System.Diagnostics.CodeAnalysis.AllowNullAttribute";
var supportNullableStaticAnalysis = context.GetBuildGlobalOption(propertyName: nameof(SupportNullableStaticAnalysis), NullStaticAnalysisState.Default);
var supportNullableReferenceTypes = context.Compilation.Options.NullableContextOptions is NullableContextOptions.Warnings or NullableContextOptions.Enable;
return new(
ConstructorAccessModifier: context.GetBuildGlobalOption(propertyName: nameof(ConstructorAccessModifier), AccessModifier.Public),
GeneratedMethodsAccessModifier: context.GetBuildGlobalOption(propertyName: nameof(GeneratedMethodsAccessModifier), AccessModifier.Public),
GenerateXmlDocument: context.GetBuildGlobalOption(propertyName: nameof(GenerateXmlDocument), true),
SupportNullableReferenceTypes: supportNullableReferenceTypes,
SupportNullableStaticAnalysis: supportNullableStaticAnalysis switch
{
NullStaticAnalysisState.Enabled => true,
NullStaticAnalysisState.Disabled => false,
_ => context.Compilation is CSharpCompilation { LanguageVersion: >= LanguageVersion.CSharp8 } cs && cs.TypeByMetadataNameExists(allowNullAttributeName)
}
);
}
public string NullableReferenceSyntax => SupportNullableReferenceTypes ? "?" : string.Empty;
}
}

View File

@ -0,0 +1,52 @@
using System.Collections.Immutable;
using System.Linq;
using MapTo.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace MapTo
{
internal class RecordMappingContext : MappingContext
{
internal RecordMappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax)
: base(compilation, sourceGenerationOptions, typeSyntax) { }
protected override ImmutableArray<MappedMember> GetSourceMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
{
throw new System.NotImplementedException();
}
protected override ImmutableArray<MappedMember> GetSourceMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
{
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray();
return typeSymbol.GetMembers()
.OfType<IMethodSymbol>()
.OrderByDescending(s => s.Parameters.Length)
.First(s => s.Name == ".ctor")
.Parameters
.Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol))
.Select(property => MapProperty(sourceTypeSymbol, sourceProperties, property))
.Where(mappedProperty => mappedProperty is not null)
.ToImmutableArray()!;
}
protected override ImmutableArray<MappedMember> GetTypeMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
{
throw new System.NotImplementedException();
}
protected override ImmutableArray<MappedMember> GetTypeMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
{
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray();
return typeSymbol.GetMembers()
.OfType<IMethodSymbol>()
.OrderByDescending(s => s.Parameters.Length)
.First(s => s.Name == ".ctor")
.Parameters
.Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol))
.Select(property => MapProperty(typeSymbol, sourceProperties, property))
.Where(mappedProperty => mappedProperty is not null)
.ToImmutableArray()!;
}
}
}

View File

@ -1,8 +1,8 @@
namespace MapTo.Sources
{
internal class Constants
{
internal const string RootNamespace = "MapTo";
internal const string GeneratedFilesHeader = "// <auto-generated />";
}
namespace MapTo.Sources
{
internal class Constants
{
internal const string RootNamespace = "MapTo";
internal const string GeneratedFilesHeader = "// <auto-generated />";
}
}

View File

@ -0,0 +1,60 @@
using static MapTo.Sources.Constants;
namespace MapTo.Sources
{
internal static class DictionaryToListAttributeSource
{
internal const string AttributeName = "DictionaryToList";
internal const string AttributeClassName = AttributeName + "Attribute";
internal const string FullyQualifiedName = RootNamespace + "." + AttributeClassName;
internal const string SourceMemberNameFieldOrPropertyName = "SourcePropertyName";
internal static SourceCode Generate(SourceGenerationOptions options)
{
using var builder = new SourceBuilder()
.WriteLine(GeneratedFilesHeader)
.WriteNullableContextOptionIf(options.SupportNullableReferenceTypes)
.WriteLine()
.WriteLine("using System;")
.WriteLine()
.WriteLine($"namespace {RootNamespace}")
.WriteOpeningBracket();
if (options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine("/// Specifies the mapping behavior of the annotated property.")
.WriteLine("/// </summary>")
.WriteLine("/// <remarks>")
.WriteLine($"/// {AttributeClassName} has a number of uses:")
.WriteLine("/// <list type=\"bullet\">")
.WriteLine("/// <item><description>By default properties with same name will get mapped. This attribute allows the names to be different.</description></item>")
.WriteLine("/// <item><description>Indicates that a property should be mapped when member serialization is set to opt-in.</description></item>")
.WriteLine("/// </list>")
.WriteLine("/// </remarks>");
}
builder
.WriteLine("[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = true)]")
.WriteLine($"public sealed class {AttributeClassName} : Attribute")
.WriteOpeningBracket();
if (options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine("/// Gets or sets the property name of the object to mapping from.")
.WriteLine("/// </summary>");
}
builder
.WriteLine($"public string{options.NullableReferenceSyntax} {SourceMemberNameFieldOrPropertyName} {{ get; set; }}")
.WriteClosingBracket() // class
.WriteClosingBracket(); // namespace
return new(builder.ToString(), $"{AttributeClassName}.g.cs");
}
}
}

View File

@ -1,58 +1,58 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis;
using static MapTo.Sources.Constants;
namespace MapTo.Sources
{
[SuppressMessage("ReSharper", "InconsistentNaming")]
internal static class ITypeConverterSource
{
internal const string InterfaceName = "ITypeConverter";
internal const string FullyQualifiedName = RootNamespace + "." + InterfaceName + "`2";
internal static SourceCode Generate(SourceGenerationOptions options)
{
using var builder = new SourceBuilder()
.WriteLine(GeneratedFilesHeader)
.WriteNullableContextOptionIf(options.SupportNullableReferenceTypes)
.WriteLine()
.WriteLine($"namespace {RootNamespace}")
.WriteOpeningBracket();
if (options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine("/// Converts the value of <typeparamref name=\"TSource\"/> to <typeparamref name=\"TDestination\"/>.")
.WriteLine("/// </summary>")
.WriteLine("/// <typeparam name=\"TSource\">The type to convert from.</typeparam>")
.WriteLine("/// <typeparam name=\"TDestination\">The type to convert to.</typeparam>");
}
builder
.WriteLine($"public interface {InterfaceName}<in TSource, out TDestination>")
.WriteOpeningBracket();
if (options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine("/// Converts the value of <paramref name=\"source\"/> object to <typeparamref name=\"TDestination\"/>.")
.WriteLine("/// </summary>")
.WriteLine("/// <param name=\"source\">The <see cref=\"TSource\"/> to convert.</param>")
.WriteLine($"/// <param name=\"converterParameters\">The parameter list passed to the <see cref=\"{MapTypeConverterAttributeSource.AttributeClassName}\"/></param>")
.WriteLine("/// <returns><typeparamref name=\"TDestination\"/> object.</returns>");
}
builder
.WriteLine($"TDestination Convert(TSource source, object[]{options.NullableReferenceSyntax} converterParameters);")
.WriteClosingBracket()
.WriteClosingBracket();
return new(builder.ToString(), $"{InterfaceName}.g.cs");
}
internal static string GetFullyQualifiedName(ITypeSymbol sourceType, ITypeSymbol destinationType) =>
$"{RootNamespace}.{InterfaceName}<{sourceType.ToDisplayString()}, {destinationType.ToDisplayString()}>";
}
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis;
using static MapTo.Sources.Constants;
namespace MapTo.Sources
{
[SuppressMessage("ReSharper", "InconsistentNaming")]
internal static class ITypeConverterSource
{
internal const string InterfaceName = "ITypeConverter";
internal const string FullyQualifiedName = RootNamespace + "." + InterfaceName + "`2";
internal static SourceCode Generate(SourceGenerationOptions options)
{
using var builder = new SourceBuilder()
.WriteLine(GeneratedFilesHeader)
.WriteNullableContextOptionIf(options.SupportNullableReferenceTypes)
.WriteLine()
.WriteLine($"namespace {RootNamespace}")
.WriteOpeningBracket();
if (options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine("/// Converts the value of <typeparamref name=\"TSource\"/> to <typeparamref name=\"TDestination\"/>.")
.WriteLine("/// </summary>")
.WriteLine("/// <typeparam name=\"TSource\">The type to convert from.</typeparam>")
.WriteLine("/// <typeparam name=\"TDestination\">The type to convert to.</typeparam>");
}
builder
.WriteLine($"public interface {InterfaceName}<in TSource, out TDestination>")
.WriteOpeningBracket();
if (options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine("/// Converts the value of <paramref name=\"source\"/> object to <typeparamref name=\"TDestination\"/>.")
.WriteLine("/// </summary>")
.WriteLine("/// <param name=\"source\">The <see cref=\"TSource\"/> to convert.</param>")
.WriteLine($"/// <param name=\"converterParameters\">The parameter list passed to the <see cref=\"{MapTypeConverterAttributeSource.AttributeClassName}\"/></param>")
.WriteLine("/// <returns><typeparamref name=\"TDestination\"/> object.</returns>");
}
builder
.WriteLine($"TDestination Convert(TSource source, object[]{options.NullableReferenceSyntax} converterParameters);")
.WriteClosingBracket()
.WriteClosingBracket();
return new(builder.ToString(), $"{InterfaceName}.g.cs");
}
internal static string GetFullyQualifiedName(ITypeSymbol sourceType, ITypeSymbol destinationType) =>
$"{RootNamespace}.{InterfaceName}<{sourceType.ToDisplayString()}, {destinationType.ToDisplayString()}>";
}
}

View File

@ -0,0 +1,36 @@
using static MapTo.Sources.Constants;
namespace MapTo.Sources
{
internal static class IgnoreMemberAttributeSource
{
internal const string AttributeName = "IgnoreMemberMapTo";
internal const string AttributeClassName = AttributeName + "Attribute";
internal const string FullyQualifiedName = RootNamespace + "." + AttributeClassName;
internal static SourceCode Generate(SourceGenerationOptions options)
{
var builder = new SourceBuilder()
.WriteLine(GeneratedFilesHeader)
.WriteLine("using System;")
.WriteLine()
.WriteLine($"namespace {RootNamespace}")
.WriteOpeningBracket();
if (options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine("/// Specifies that the annotated property should be excluded.")
.WriteLine("/// </summary>");
}
builder
.WriteLine("[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, Inherited = false, AllowMultiple = false)]")
.WriteLine($"public sealed class {AttributeClassName} : Attribute {{ }}")
.WriteClosingBracket();
return new(builder.ToString(), $"{AttributeClassName}.g.cs");
}
}
}

View File

@ -0,0 +1,40 @@
using static MapTo.Sources.Constants;
namespace MapTo.Sources
{
internal static class JsonExtensionAttributeSource
{
internal const string AttributeName = "JsonExtension";
internal const string AttributeClassName = AttributeName + "Attribute";
internal const string FullyQualifiedName = RootNamespace + "." + AttributeClassName;
internal static SourceCode Generate(SourceGenerationOptions options)
{
using var builder = new SourceBuilder()
.WriteLine(GeneratedFilesHeader)
.WriteLine("using System;")
.WriteLine()
.WriteLine($"namespace {RootNamespace}")
.WriteOpeningBracket();
if (options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine("/// Specifies that the annotated class has a json extension.")
.WriteLine("/// </summary>");
}
builder
.WriteLine("[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]")
.WriteLine($"public sealed class {AttributeName}Attribute : Attribute")
.WriteOpeningBracket();
builder
.WriteClosingBracket() // class
.WriteClosingBracket(); // namespace
return new(builder.ToString(), $"{AttributeName}Attribute.g.cs");
}
}
}

View File

@ -0,0 +1,14 @@
using MapTo.Extensions;
using System.Text;
using static MapTo.Sources.Constants;
namespace MapTo.Sources
{
internal static class MapClassSource
{
internal static SourceCode Generate(MappingModel model)
{
return model.GenerateStructOrClass("class");
}
}
}

View File

@ -1,65 +1,72 @@
using static MapTo.Sources.Constants;
namespace MapTo.Sources
{
internal static class MapFromAttributeSource
{
internal const string AttributeName = "MapFrom";
internal const string AttributeClassName = AttributeName + "Attribute";
internal const string FullyQualifiedName = RootNamespace + "." + AttributeClassName;
internal static SourceCode Generate(SourceGenerationOptions options)
{
using var builder = new SourceBuilder()
.WriteLine(GeneratedFilesHeader)
.WriteLine("using System;")
.WriteLine()
.WriteLine($"namespace {RootNamespace}")
.WriteOpeningBracket();
if (options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine("/// Specifies that the annotated class can be mapped from the provided <see cref=\"SourceType\"/>.")
.WriteLine("/// </summary>");
}
builder
.WriteLine("[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]")
.WriteLine($"public sealed class {AttributeName}Attribute : Attribute")
.WriteOpeningBracket();
if (options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine($"/// Initializes a new instance of the <see cref=\"{AttributeName}Attribute\"/> class with the specified <paramref name=\"sourceType\"/>.")
.WriteLine("/// </summary>")
.WriteLine("/// <param name=\"sourceType\">The type of to map from.</param>");
}
builder
.WriteLine($"public {AttributeName}Attribute(Type sourceType)")
.WriteOpeningBracket()
.WriteLine("SourceType = sourceType;")
.WriteClosingBracket()
.WriteLine();
if (options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine("/// Gets the type to map from.")
.WriteLine("/// </summary>");
}
builder
.WriteLine("public Type SourceType { get; }")
.WriteClosingBracket() // class
.WriteClosingBracket(); // namespace
return new(builder.ToString(), $"{AttributeName}Attribute.g.cs");
}
}
using static MapTo.Sources.Constants;
namespace MapTo.Sources
{
internal static class MapFromAttributeSource
{
internal const string AttributeName = "MapFrom";
internal const string AttributeClassName = AttributeName + "Attribute";
internal const string FullyQualifiedName = RootNamespace + "." + AttributeClassName;
internal static SourceCode Generate(SourceGenerationOptions options)
{
using var builder = new SourceBuilder()
.WriteLine(GeneratedFilesHeader)
.WriteLine("using System;")
.WriteLine()
.WriteLine($"namespace {RootNamespace}")
.WriteOpeningBracket();
if (options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine("/// Specifies that the annotated class can be mapped from the provided <see cref=\"SourceType\"/>.")
.WriteLine("/// </summary>");
}
builder
.WriteLine("[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]")
.WriteLine($"public sealed class {AttributeName}Attribute : Attribute")
.WriteOpeningBracket();
if (options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine($"/// Initializes a new instance of the <see cref=\"{AttributeName}Attribute\"/> class with the specified <paramref name=\"sourceType\"/>.")
.WriteLine("/// </summary>")
.WriteLine("/// <param name=\"sourceType\">The type of to map from.</param>");
}
builder
.WriteLine($"public {AttributeName}Attribute(Type sourceType)")
.WriteOpeningBracket()
.WriteLine("SourceType = new [] { sourceType };")
.WriteClosingBracket()
.WriteLine();
builder
.WriteLine($"public {AttributeName}Attribute(Type[] sourceType)")
.WriteOpeningBracket()
.WriteLine("SourceType = sourceType;")
.WriteClosingBracket()
.WriteLine();
if (options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine("/// Gets the type to map from.")
.WriteLine("/// </summary>");
}
builder
.WriteLine("public Type[] SourceType { get; }")
.WriteClosingBracket() // class
.WriteClosingBracket(); // namespace
return new(builder.ToString(), $"{AttributeName}Attribute.g.cs");
}
}
}

View File

@ -1,60 +1,61 @@
using static MapTo.Sources.Constants;
namespace MapTo.Sources
{
internal static class MapPropertyAttributeSource
{
internal const string AttributeName = "MapProperty";
internal const string AttributeClassName = AttributeName + "Attribute";
internal const string FullyQualifiedName = RootNamespace + "." + AttributeClassName;
internal const string SourcePropertyNamePropertyName = "SourcePropertyName";
internal static SourceCode Generate(SourceGenerationOptions options)
{
using var builder = new SourceBuilder()
.WriteLine(GeneratedFilesHeader)
.WriteNullableContextOptionIf(options.SupportNullableReferenceTypes)
.WriteLine()
.WriteLine("using System;")
.WriteLine()
.WriteLine($"namespace {RootNamespace}")
.WriteOpeningBracket();
if (options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine("/// Specifies the mapping behavior of the annotated property.")
.WriteLine("/// </summary>")
.WriteLine("/// <remarks>")
.WriteLine($"/// {AttributeClassName} has a number of uses:")
.WriteLine("/// <list type=\"bullet\">")
.WriteLine("/// <item><description>By default properties with same name will get mapped. This attribute allows the names to be different.</description></item>")
.WriteLine("/// <item><description>Indicates that a property should be mapped when member serialization is set to opt-in.</description></item>")
.WriteLine("/// </list>")
.WriteLine("/// </remarks>");
}
builder
.WriteLine("[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = true)]")
.WriteLine($"public sealed class {AttributeClassName} : Attribute")
.WriteOpeningBracket();
if (options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine("/// Gets or sets the property name of the object to mapping from.")
.WriteLine("/// </summary>");
}
builder
.WriteLine($"public string{options.NullableReferenceSyntax} {SourcePropertyNamePropertyName} {{ get; set; }}")
.WriteClosingBracket() // class
.WriteClosingBracket(); // namespace
return new(builder.ToString(), $"{AttributeClassName}.g.cs");
}
}
using static MapTo.Sources.Constants;
namespace MapTo.Sources
{
internal static class MapPropertyAttributeSource
{
internal const string AttributeName = "MapProperty";
internal const string AttributeClassName = AttributeName + "Attribute";
internal const string FullyQualifiedName = RootNamespace + "." + AttributeClassName;
internal const string SourcePropertyNamePropertyName = "SourcePropertyName";
internal static SourceCode Generate(SourceGenerationOptions options)
{
using var builder = new SourceBuilder()
.WriteLine(GeneratedFilesHeader)
.WriteNullableContextOptionIf(options.SupportNullableReferenceTypes)
.WriteLine()
.WriteLine("using System;")
.WriteLine()
.WriteLine($"namespace {RootNamespace}")
.WriteOpeningBracket();
if (options.GenerateXmlDocument)
{
builder
.WriteLine()
.WriteLine("/// <summary>")
.WriteLine("/// Specifies the mapping behavior of the annotated property.")
.WriteLine("/// </summary>")
.WriteLine("/// <remarks>")
.WriteLine($"/// {AttributeClassName} has a number of uses:")
.WriteLine("/// <list type=\"bullet\">")
.WriteLine("/// <item><description>By default properties with same name will get mapped. This attribute allows the names to be different.</description></item>")
.WriteLine("/// <item><description>Indicates that a property should be mapped when member serialization is set to opt-in.</description></item>")
.WriteLine("/// </list>")
.WriteLine("/// </remarks>");
}
builder
.WriteLine("[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = true)]")
.WriteLine($"public sealed class {AttributeClassName} : Attribute")
.WriteOpeningBracket();
if (options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine("/// Gets or sets the property name of the object to mapping from.")
.WriteLine("/// </summary>");
}
builder
.WriteLine($"public string{options.NullableReferenceSyntax} {SourcePropertyNamePropertyName} {{ get; set; }}")
.WriteClosingBracket() // class
.WriteClosingBracket(); // namespace
return new(builder.ToString(), $"{AttributeClassName}.g.cs");
}
}
}

View File

@ -0,0 +1,235 @@
using System;
using MapTo.Extensions;
using static MapTo.Sources.Constants;
namespace MapTo.Sources
{
internal static class MapRecordSource
{
internal static SourceCode Generate(MappingModel model)
{
using var builder = new SourceBuilder()
.WriteLine(GeneratedFilesHeader)
.WriteNullableContextOptionIf(model.Options.SupportNullableReferenceTypes)
.WriteUsings(model.Usings)
.WriteLine()
// Namespace declaration
.WriteLine($"namespace {model.Namespace}")
.WriteOpeningBracket()
// Class declaration
.WriteLine($"partial record {model.TypeIdentifierName}")
.WriteOpeningBracket();
foreach (var targetSourceType in model.MappedSourceTypes)
{
if (targetSourceType.GenerateSecondaryConstructor)
{
builder
.GenerateSecondaryConstructor(model)
.WriteLine();
}
}
// Class body
builder
.GeneratePrivateConstructor(model)
.WriteLine()
.GenerateFactoryMethod(model)
// End class declaration
.WriteClosingBracket()
.WriteLine()
// Extension class declaration
.GenerateSourceTypeExtensionClass(model)
// End namespace declaration
.WriteClosingBracket();
return new(builder.ToString(), $"{model.Namespace}.{model.TypeIdentifierName}.g.cs");
}
private static SourceBuilder GenerateSecondaryConstructor(this SourceBuilder builder, MappingModel model)
{
// grab first data from array
foreach (var targetSourceType in model.MappedSourceTypes)
{
var sourceClassParameterName = targetSourceType.SourceTypeIdentifierName.ToCamelCase();
if (model.Options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine($"/// Initializes a new instance of the <see cref=\"{model.TypeIdentifierName}\"/> class")
.WriteLine($"/// using the property values from the specified <paramref name=\"{sourceClassParameterName}\"/>.")
.WriteLine("/// </summary>")
.WriteLine($"/// <exception cref=\"ArgumentNullException\">{sourceClassParameterName} is null</exception>");
}
builder .WriteLine($"{model.Options.ConstructorAccessModifier.ToLowercaseString()} {model.TypeIdentifierName}({targetSourceType.SourceType} {sourceClassParameterName})")
.WriteLine($" : this(new {MappingContextSource.ClassName}(), {sourceClassParameterName}) {{ }}");
}
return builder;
}
private static SourceBuilder GeneratePrivateConstructor(this SourceBuilder builder, MappingModel model)
{
const string mappingContextParameterName = "context";
foreach (var targetSourceType in model.MappedSourceTypes)
{
var sourceClassParameterName = targetSourceType.SourceTypeIdentifierName.ToCamelCase();
builder
.WriteLine(
$"private protected {model.TypeIdentifierName}({MappingContextSource.ClassName} {mappingContextParameterName}, {targetSourceType.SourceType} {sourceClassParameterName})")
.Indent()
.Write(": this(").WriteProperties(model, sourceClassParameterName, mappingContextParameterName)
.WriteLine(")")
.Unindent()
.WriteOpeningBracket()
.WriteLine($"if ({mappingContextParameterName} == null) throw new ArgumentNullException(nameof({mappingContextParameterName}));")
.WriteLine($"if ({sourceClassParameterName} == null) throw new ArgumentNullException(nameof({sourceClassParameterName}));")
.WriteLine()
.WriteLine($"{mappingContextParameterName}.{MappingContextSource.RegisterMethodName}({sourceClassParameterName}, this);")
.WriteClosingBracket();
}
// End constructor declaration
return builder;
}
private static SourceBuilder WriteProperties(this SourceBuilder builder, MappingModel model, string sourceClassParameterName,
string mappingContextParameterName)
{
foreach (var targetSourceType in model.MappedSourceTypes)
{
for (var i = 0; i < targetSourceType.SourceProperties.Length; i++)
{
var property = targetSourceType.SourceProperties[i];
if (property.TypeConverter is null)
{
if (property.IsEnumerable)
{
builder.Write(
$"{property.Name}: {sourceClassParameterName}.{property.SourcePropertyName}.Select({mappingContextParameterName}.{MappingContextSource.MapMethodName}<{property.MappedSourcePropertyTypeName}, {property.EnumerableTypeArgument}>).ToList()");
}
else
{
builder.Write(property.MappedSourcePropertyTypeName is null
? $"{property.Name}: {sourceClassParameterName}.{property.SourcePropertyName}"
: $"{property.Name}: {mappingContextParameterName}.{MappingContextSource.MapMethodName}<{property.MappedSourcePropertyTypeName}, {property.Type}>({sourceClassParameterName}.{property.SourcePropertyName})");
}
}
else
{
var parameters = property.TypeConverterParameters.IsEmpty
? "null"
: $"new object[] {{ {string.Join(", ", property.TypeConverterParameters)} }}";
builder.Write(
$"{property.Name}: new {property.TypeConverter}().Convert({sourceClassParameterName}.{property.SourcePropertyName}, {parameters})");
}
if (i < targetSourceType.SourceProperties.Length - 1)
{
builder.Write(", ");
}
}
}
return builder;
}
private static SourceBuilder GenerateFactoryMethod(this SourceBuilder builder, MappingModel model)
{
foreach (var targetSourceType in model.MappedSourceTypes)
{
var sourceClassParameterName = targetSourceType.SourceTypeIdentifierName.ToCamelCase();
builder
.GenerateConvertorMethodsXmlDocs(model, sourceClassParameterName)
.WriteLineIf(model.Options.SupportNullableStaticAnalysis, $"[return: NotNullIfNotNull(\"{sourceClassParameterName}\")]")
.WriteLine(
$"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static {model.TypeIdentifierName}{model.Options.NullableReferenceSyntax} From({targetSourceType.SourceType}{model.Options.NullableReferenceSyntax} {sourceClassParameterName})")
.WriteOpeningBracket()
.WriteLine(
$"return {sourceClassParameterName} == null ? null : {MappingContextSource.ClassName}.{MappingContextSource.FactoryMethodName}<{targetSourceType.SourceType}, {model.TypeIdentifierName}>({sourceClassParameterName});")
.WriteClosingBracket();
}
return builder;
}
private static SourceBuilder GenerateConvertorMethodsXmlDocs(this SourceBuilder builder, MappingModel model, string sourceClassParameterName)
{
if (!model.Options.GenerateXmlDocument)
{
return builder;
}
foreach (var targetSourceType in model.MappedSourceTypes)
{
builder
.WriteLine("/// <summary>")
.WriteLine($"/// Creates a new instance of <see cref=\"{model.TypeIdentifierName}\"/> and sets its participating properties")
.WriteLine($"/// using the property values from <paramref name=\"{sourceClassParameterName}\"/>.")
.WriteLine("/// </summary>")
.WriteLine($"/// <param name=\"{sourceClassParameterName}\">The instance of <see cref=\"{targetSourceType.SourceType}\"/> to use as source.</param>")
.WriteLine(
$"/// <returns>A new instance of <see cred=\"{model.TypeIdentifierName}\"/> -or- <c>null</c> if <paramref name=\"{sourceClassParameterName}\"/> is <c>null</c>.</returns>");
}
return builder;
}
private static SourceBuilder GenerateSourceTypeExtensionClass(this SourceBuilder builder, MappingModel model)
{
foreach (var targetSourceType in model.MappedSourceTypes)
{
builder
.WriteLine(
$"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static partial class {targetSourceType.SourceTypeIdentifierName}To{model.TypeIdentifierName}Extensions")
.WriteOpeningBracket()
.GenerateSourceTypeExtensionMethod(model)
.WriteClosingBracket();
}
return builder;
}
private static SourceBuilder GenerateSourceTypeExtensionMethod(this SourceBuilder builder, MappingModel model)
{
foreach (var targetSourceType in model.MappedSourceTypes)
{
var sourceClassParameterName = targetSourceType.SourceTypeIdentifierName.ToCamelCase();
builder
.GenerateConvertorMethodsXmlDocs(model, sourceClassParameterName)
.WriteLineIf(model.Options.SupportNullableStaticAnalysis, $"[return: NotNullIfNotNull(\"{sourceClassParameterName}\")]")
.WriteLine(
$"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static {model.TypeIdentifierName}{model.Options.NullableReferenceSyntax} To{model.TypeIdentifierName}(this {targetSourceType.SourceType}{model.Options.NullableReferenceSyntax} {sourceClassParameterName})")
.WriteOpeningBracket()
.WriteLine($"return {sourceClassParameterName} == null ? null : new {model.TypeIdentifierName}({sourceClassParameterName});")
.WriteClosingBracket();
}
return builder;
}
}
}

View File

@ -0,0 +1,15 @@
using MapTo.Extensions;
using static MapTo.Sources.Constants;
using System.Collections.Generic;
using System.Text;
namespace MapTo.Sources
{
internal static class MapStructSource
{
internal static SourceCode Generate(MappingModel model)
{
return model.GenerateStructOrClass("struct");
}
}
}

View File

@ -1,84 +1,84 @@
using System;
using static MapTo.Sources.Constants;
namespace MapTo.Sources
{
internal static class MapTypeConverterAttributeSource
{
internal const string AttributeName = "MapTypeConverter";
internal const string AttributeClassName = AttributeName + "Attribute";
internal const string FullyQualifiedName = RootNamespace + "." + AttributeClassName;
internal const string ConverterPropertyName = "Converter";
internal const string ConverterParametersPropertyName = "ConverterParameters";
internal static SourceCode Generate(SourceGenerationOptions options)
{
using var builder = new SourceBuilder()
.WriteLine(GeneratedFilesHeader)
.WriteNullableContextOptionIf(options.SupportNullableReferenceTypes)
.WriteLine()
.WriteLine("using System;")
.WriteLine()
.WriteLine($"namespace {RootNamespace}")
.WriteOpeningBracket();
if (options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine("/// Specifies what type to use as a converter for the property this attribute is bound to.")
.WriteLine("/// </summary>");
}
builder
.WriteLine("[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false)]")
.WriteLine($"public sealed class {AttributeClassName} : Attribute")
.WriteOpeningBracket();
if (options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine($"/// Initializes a new instance of <see cref=\"{AttributeClassName}\"/>.")
.WriteLine("/// </summary>")
.WriteLine($"/// <param name=\"converter\">The <see cref=\"{ITypeConverterSource.InterfaceName}{{TSource,TDestination}}\" /> to be used to convert the source type.</param>")
.WriteLine("/// <param name=\"converterParameters\">The list of parameters to pass to the <paramref name=\"converter\"/> during the type conversion.</param>");
}
builder
.WriteLine($"public {AttributeClassName}(Type converter, object[]{options.NullableReferenceSyntax} converterParameters = null)")
.WriteOpeningBracket()
.WriteLine($"{ConverterPropertyName} = converter;")
.WriteLine($"{ConverterParametersPropertyName} = converterParameters;")
.WriteClosingBracket()
.WriteLine();
if (options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine($"/// Gets or sets the <see cref=\"{ITypeConverterSource.InterfaceName}{{TSource,TDestination}}\" /> to be used to convert the source type.")
.WriteLine("/// </summary>");
}
builder
.WriteLine($"public Type {ConverterPropertyName} {{ get; }}")
.WriteLine();
if (options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine($"/// Gets the list of parameters to pass to the <see cref=\"{ConverterPropertyName}\"/> during the type conversion.")
.WriteLine("/// </summary>");
}
builder
.WriteLine($"public object[]{options.NullableReferenceSyntax} {ConverterParametersPropertyName} {{ get; }}")
.WriteClosingBracket()
.WriteClosingBracket();
return new(builder.ToString(), $"{AttributeClassName}.g.cs");
}
}
using System;
using static MapTo.Sources.Constants;
namespace MapTo.Sources
{
internal static class MapTypeConverterAttributeSource
{
internal const string AttributeName = "MapTypeConverter";
internal const string AttributeClassName = AttributeName + "Attribute";
internal const string FullyQualifiedName = RootNamespace + "." + AttributeClassName;
internal const string ConverterPropertyName = "Converter";
internal const string ConverterParametersPropertyName = "ConverterParameters";
internal static SourceCode Generate(SourceGenerationOptions options)
{
using var builder = new SourceBuilder()
.WriteLine(GeneratedFilesHeader)
.WriteNullableContextOptionIf(options.SupportNullableReferenceTypes)
.WriteLine()
.WriteLine("using System;")
.WriteLine()
.WriteLine($"namespace {RootNamespace}")
.WriteOpeningBracket();
if (options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine("/// Specifies what type to use as a converter for the property this attribute is bound to.")
.WriteLine("/// </summary>");
}
builder
.WriteLine("[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false)]")
.WriteLine($"public sealed class {AttributeClassName} : Attribute")
.WriteOpeningBracket();
if (options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine($"/// Initializes a new instance of <see cref=\"{AttributeClassName}\"/>.")
.WriteLine("/// </summary>")
.WriteLine($"/// <param name=\"converter\">The <see cref=\"{ITypeConverterSource.InterfaceName}{{TSource,TDestination}}\" /> to be used to convert the source type.</param>")
.WriteLine("/// <param name=\"converterParameters\">The list of parameters to pass to the <paramref name=\"converter\"/> during the type conversion.</param>");
}
builder
.WriteLine($"public {AttributeClassName}(Type converter, object[]{options.NullableReferenceSyntax} converterParameters = null)")
.WriteOpeningBracket()
.WriteLine($"{ConverterPropertyName} = converter;")
.WriteLine($"{ConverterParametersPropertyName} = converterParameters;")
.WriteClosingBracket()
.WriteLine();
if (options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine($"/// Gets or sets the <see cref=\"{ITypeConverterSource.InterfaceName}{{TSource,TDestination}}\" /> to be used to convert the source type.")
.WriteLine("/// </summary>");
}
builder
.WriteLine($"public Type {ConverterPropertyName} {{ get; }}")
.WriteLine();
if (options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine($"/// Gets the list of parameters to pass to the <see cref=\"{ConverterPropertyName}\"/> during the type conversion.")
.WriteLine("/// </summary>");
}
builder
.WriteLine($"public object[]{options.NullableReferenceSyntax} {ConverterParametersPropertyName} {{ get; }}")
.WriteClosingBracket()
.WriteClosingBracket();
return new(builder.ToString(), $"{AttributeClassName}.g.cs");
}
}
}

View File

@ -1,115 +1,115 @@
using System.Collections.Generic;
using static MapTo.Sources.Constants;
namespace MapTo.Sources
{
internal static class MappingContextSource
{
internal const string ClassName = "MappingContext";
internal const string FullyQualifiedName = RootNamespace + "." + ClassName;
internal const string FactoryMethodName = "Create";
internal const string RegisterMethodName = "Register";
internal const string MapMethodName = "MapFromWithContext";
internal static SourceCode Generate(SourceGenerationOptions options)
{
var usings = new List<string> { "System", "System.Collections.Generic", "System.Reflection" };
using var builder = new SourceBuilder()
.WriteLine(GeneratedFilesHeader)
.WriteLine()
.WriteUsings(usings)
.WriteLine()
// Namespace declaration
.WriteLine($"namespace {RootNamespace}")
.WriteOpeningBracket()
// Class declaration
.WriteLine($"internal sealed class {ClassName}")
.WriteOpeningBracket()
.WriteLine("private readonly Dictionary<object, object> _cache;")
.WriteLine()
// Constructor
.WriteLine($"internal {ClassName}()")
.WriteOpeningBracket()
.WriteLine("_cache = new Dictionary<object, object>(1);")
.WriteClosingBracket()
.WriteLine()
// Factory
.WriteLine($"internal static TMapped {FactoryMethodName}<TOriginal, TMapped>(TOriginal original)")
.WriteOpeningBracket()
.WriteLine("if (original == null) throw new ArgumentNullException(nameof(original));")
.WriteLine()
.WriteLine("var context = new MappingContext();")
.WriteLine("var mapped = context.MapFromWithContext<TOriginal, TMapped>(original);")
.WriteLine()
.WriteLine("if (mapped == null)")
.WriteOpeningBracket()
.WriteLine("throw new InvalidOperationException();")
.WriteClosingBracket()
.WriteLine()
.WriteLine("return mapped;")
.WriteClosingBracket()
.WriteLine()
// MapFromWithContext method
.WriteLine($"internal TMapped MapFromWithContext<TOriginal, TMapped>(TOriginal original)")
.WriteOpeningBracket()
.WriteLine("if (original == null)")
.WriteOpeningBracket()
.WriteLine("return default(TMapped);")
.WriteClosingBracket()
.WriteLine()
.WriteLine("if (!TryGetValue<TOriginal, TMapped>(original, out var mapped))")
.WriteOpeningBracket()
.WriteLine("var instance = Activator.CreateInstance(typeof(TMapped), BindingFlags.Instance | BindingFlags.NonPublic, null, new object[] { this, original }, null);")
.WriteLine("if (instance != null)")
.WriteOpeningBracket()
.WriteLine("mapped = (TMapped)instance;")
.WriteClosingBracket()
.WriteClosingBracket()
.WriteLine()
.WriteLine("return mapped;")
.WriteClosingBracket()
.WriteLine()
// Register method
.WriteLine("internal void Register<TOriginal, TMapped>(TOriginal original, TMapped mapped)")
.WriteOpeningBracket()
.WriteLine("if (original == null) throw new ArgumentNullException(nameof(original));")
.WriteLine("if (mapped == null) throw new ArgumentNullException(nameof(mapped));")
.WriteLine()
.WriteLine("if (!_cache.ContainsKey(original))")
.WriteOpeningBracket()
.WriteLine("_cache.Add(original, mapped);")
.WriteClosingBracket()
.WriteClosingBracket()
.WriteLine()
// TryGetValue method
.WriteLine("private bool TryGetValue<TOriginal, TMapped>(TOriginal original, out TMapped mapped)")
.WriteOpeningBracket()
.WriteLine("if (original != null && _cache.TryGetValue(original, out var value))")
.WriteOpeningBracket()
.WriteLine("mapped = (TMapped)value;")
.WriteLine("return true;")
.WriteClosingBracket()
.WriteLine()
.WriteLine("mapped = default(TMapped);")
.WriteLine("return false;")
.WriteClosingBracket()
// End class declaration
.WriteClosingBracket()
// End namespace declaration
.WriteClosingBracket();
return new(builder.ToString(), $"{ClassName}.g.cs");
}
}
using System.Collections.Generic;
using static MapTo.Sources.Constants;
namespace MapTo.Sources
{
internal static class MappingContextSource
{
internal const string ClassName = "MappingContext";
internal const string FullyQualifiedName = RootNamespace + "." + ClassName;
internal const string FactoryMethodName = "Create";
internal const string RegisterMethodName = "Register";
internal const string MapMethodName = "MapFromWithContext";
internal static SourceCode Generate(SourceGenerationOptions options)
{
var usings = new List<string> { "System", "System.Collections.Generic", "System.Reflection" };
using var builder = new SourceBuilder()
.WriteLine(GeneratedFilesHeader)
.WriteLine()
.WriteUsings(usings)
.WriteLine()
// Namespace declaration
.WriteLine($"namespace {RootNamespace}")
.WriteOpeningBracket()
// Class declaration
.WriteLine($"internal sealed class {ClassName}")
.WriteOpeningBracket()
.WriteLine("private readonly Dictionary<object, object> _cache;")
.WriteLine()
// Constructor
.WriteLine($"internal {ClassName}()")
.WriteOpeningBracket()
.WriteLine("_cache = new Dictionary<object, object>(1);")
.WriteClosingBracket()
.WriteLine()
// Factory
.WriteLine($"internal static TMapped {FactoryMethodName}<TOriginal, TMapped>(TOriginal original)")
.WriteOpeningBracket()
.WriteLine("if (original == null) throw new ArgumentNullException(nameof(original));")
.WriteLine()
.WriteLine("var context = new MappingContext();")
.WriteLine("var mapped = context.MapFromWithContext<TOriginal, TMapped>(original);")
.WriteLine()
.WriteLine("if (mapped == null)")
.WriteOpeningBracket()
.WriteLine("throw new InvalidOperationException();")
.WriteClosingBracket()
.WriteLine()
.WriteLine("return mapped;")
.WriteClosingBracket()
.WriteLine()
// MapFromWithContext method
.WriteLine($"internal TMapped MapFromWithContext<TOriginal, TMapped>(TOriginal original)")
.WriteOpeningBracket()
.WriteLine("if (original == null)")
.WriteOpeningBracket()
.WriteLine("return default(TMapped);")
.WriteClosingBracket()
.WriteLine()
.WriteLine("if (!TryGetValue<TOriginal, TMapped>(original, out var mapped))")
.WriteOpeningBracket()
.WriteLine("var instance = Activator.CreateInstance(typeof(TMapped), BindingFlags.Instance | BindingFlags.NonPublic, null, new object[] { this, original }, null);")
.WriteLine("if (instance != null)")
.WriteOpeningBracket()
.WriteLine("mapped = (TMapped)instance;")
.WriteClosingBracket()
.WriteClosingBracket()
.WriteLine()
.WriteLine("return mapped;")
.WriteClosingBracket()
.WriteLine()
// Register method
.WriteLine("internal void Register<TOriginal, TMapped>(TOriginal original, TMapped mapped)")
.WriteOpeningBracket()
.WriteLine("if (original == null) throw new ArgumentNullException(nameof(original));")
.WriteLine("if (mapped == null) throw new ArgumentNullException(nameof(mapped));")
.WriteLine()
.WriteLine("if (!_cache.ContainsKey(original))")
.WriteOpeningBracket()
.WriteLine("_cache.Add(original, mapped);")
.WriteClosingBracket()
.WriteClosingBracket()
.WriteLine()
// TryGetValue method
.WriteLine("private bool TryGetValue<TOriginal, TMapped>(TOriginal original, out TMapped mapped)")
.WriteOpeningBracket()
.WriteLine("if (original != null && _cache.TryGetValue(original, out var value))")
.WriteOpeningBracket()
.WriteLine("mapped = (TMapped)value;")
.WriteLine("return true;")
.WriteClosingBracket()
.WriteLine()
.WriteLine("mapped = default(TMapped);")
.WriteLine("return false;")
.WriteClosingBracket()
// End class declaration
.WriteClosingBracket()
// End namespace declaration
.WriteClosingBracket();
return new(builder.ToString(), $"{ClassName}.g.cs");
}
}
}

View File

@ -1,36 +1,36 @@
using static MapTo.Sources.Constants;
namespace MapTo.Sources
{
internal static class IgnorePropertyAttributeSource
{
internal const string AttributeName = "IgnoreProperty";
internal const string AttributeClassName = AttributeName + "Attribute";
internal const string FullyQualifiedName = RootNamespace + "." + AttributeClassName;
internal static SourceCode Generate(SourceGenerationOptions options)
{
var builder = new SourceBuilder()
.WriteLine(GeneratedFilesHeader)
.WriteLine("using System;")
.WriteLine()
.WriteLine($"namespace {RootNamespace}")
.WriteOpeningBracket();
if (options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine("/// Specifies that the annotated property should be excluded.")
.WriteLine("/// </summary>");
}
builder
.WriteLine("[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]")
.WriteLine($"public sealed class {AttributeClassName} : Attribute {{ }}")
.WriteClosingBracket();
return new(builder.ToString(), $"{AttributeClassName}.g.cs");
}
}
using static MapTo.Sources.Constants;
namespace MapTo.Sources
{
internal static class ReadOnlyPropertyAttributeSource
{
internal const string AttributeName = "ReadOnlyProperty";
internal const string AttributeClassName = AttributeName + "Attribute";
internal const string FullyQualifiedName = RootNamespace + "." + AttributeClassName;
internal static SourceCode Generate(SourceGenerationOptions options)
{
var builder = new SourceBuilder()
.WriteLine(GeneratedFilesHeader)
.WriteLine("using System;")
.WriteLine()
.WriteLine($"namespace {RootNamespace}")
.WriteOpeningBracket();
if (options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine("/// Specifies that the annotated property should be excluded.")
.WriteLine("/// </summary>");
}
builder
.WriteLine("[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]")
.WriteLine($"public sealed class {AttributeClassName} : Attribute {{ }}")
.WriteClosingBracket();
return new(builder.ToString(), $"{AttributeClassName}.g.cs");
}
}
}

View File

@ -1,107 +1,107 @@
using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace MapTo.Sources
{
internal sealed class SourceBuilder : IDisposable
{
private readonly StringWriter _writer;
private readonly IndentedTextWriter _indentedWriter;
public SourceBuilder()
{
_writer = new StringWriter();
_indentedWriter = new IndentedTextWriter(_writer, new string(' ', 4));
}
/// <inheritdoc />
public void Dispose()
{
_writer.Dispose();
_indentedWriter.Dispose();
}
public SourceBuilder WriteLine(string? value = null)
{
if (string.IsNullOrWhiteSpace(value))
{
_indentedWriter.WriteLineNoTabs(string.Empty);
}
else
{
_indentedWriter.WriteLine(value);
}
return this;
}
public SourceBuilder Write(string? value = null)
{
_indentedWriter.Write(value);
return this;
}
public SourceBuilder WriteLineIf(bool condition, string? value)
{
if (condition)
{
WriteLine(value);
}
return this;
}
public SourceBuilder WriteNullableContextOptionIf(bool enabled) => WriteLineIf(enabled, "#nullable enable");
public SourceBuilder WriteOpeningBracket()
{
_indentedWriter.WriteLine("{");
_indentedWriter.Indent++;
return this;
}
public SourceBuilder WriteClosingBracket()
{
_indentedWriter.Indent--;
_indentedWriter.WriteLine("}");
return this;
}
public SourceBuilder WriteUsings(IEnumerable<string> usings)
{
foreach (var u in usings.OrderBy(s => s))
{
WriteUsing(u);
}
return this;
}
public SourceBuilder WriteUsing(string u)
{
WriteLine($"using {u};");
return this;
}
public SourceBuilder Indent()
{
_indentedWriter.Indent++;
return this;
}
public SourceBuilder Unindent()
{
_indentedWriter.Indent--;
return this;
}
/// <inheritdoc />
public override string ToString() => _writer.ToString();
}
using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace MapTo.Sources
{
internal sealed class SourceBuilder : IDisposable
{
private readonly StringWriter _writer;
private readonly IndentedTextWriter _indentedWriter;
public SourceBuilder()
{
_writer = new StringWriter();
_indentedWriter = new IndentedTextWriter(_writer, new string(' ', 4));
}
/// <inheritdoc />
public void Dispose()
{
_writer.Dispose();
_indentedWriter.Dispose();
}
public SourceBuilder WriteLine(string? value = null)
{
if (string.IsNullOrWhiteSpace(value))
{
_indentedWriter.WriteLineNoTabs(string.Empty);
}
else
{
_indentedWriter.WriteLine(value);
}
return this;
}
public SourceBuilder Write(string? value = null)
{
_indentedWriter.Write(value);
return this;
}
public SourceBuilder WriteLineIf(bool condition, string? value)
{
if (condition)
{
WriteLine(value);
}
return this;
}
public SourceBuilder WriteNullableContextOptionIf(bool enabled) => WriteLineIf(enabled, "#nullable enable");
public SourceBuilder WriteOpeningBracket()
{
_indentedWriter.WriteLine("{");
_indentedWriter.Indent++;
return this;
}
public SourceBuilder WriteClosingBracket()
{
_indentedWriter.Indent--;
_indentedWriter.WriteLine("}");
return this;
}
public SourceBuilder WriteUsings(IEnumerable<string> usings)
{
foreach (var u in usings.OrderBy(s => s))
{
WriteUsing(u);
}
return this;
}
public SourceBuilder WriteUsing(string u)
{
WriteLine($"using {u};");
return this;
}
public SourceBuilder Indent()
{
_indentedWriter.Indent++;
return this;
}
public SourceBuilder Unindent()
{
_indentedWriter.Indent--;
return this;
}
/// <inheritdoc />
public override string ToString() => _writer.ToString();
}
}

View File

@ -0,0 +1,40 @@
using static MapTo.Sources.Constants;
namespace MapTo.Sources
{
internal static class UseUpdateAttributeSource
{
internal const string AttributeName = "UseUpdate";
internal const string AttributeClassName = AttributeName + "Attribute";
internal const string FullyQualifiedName = RootNamespace + "." + AttributeClassName;
internal static SourceCode Generate(SourceGenerationOptions options)
{
using var builder = new SourceBuilder()
.WriteLine(GeneratedFilesHeader)
.WriteLine("using System;")
.WriteLine()
.WriteLine($"namespace {RootNamespace}")
.WriteOpeningBracket();
if (options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine("/// Specifies that the annotated class can be updatable.")
.WriteLine("/// </summary>");
}
builder
.WriteLine("[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]")
.WriteLine($"public sealed class {AttributeName}Attribute : Attribute")
.WriteOpeningBracket();
builder
.WriteClosingBracket() // class
.WriteClosingBracket(); // namespace
return new(builder.ToString(), $"{AttributeName}Attribute.g.cs");
}
}
}

View File

@ -0,0 +1,67 @@
using System.Collections.Immutable;
using System.Linq;
using MapTo.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace MapTo
{
internal class StructMappingContext : MappingContext
{
internal StructMappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax)
: base(compilation, sourceGenerationOptions, typeSyntax) { }
protected override ImmutableArray<MappedMember> GetSourceMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
{
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IFieldSymbol>().ToArray();
return typeSymbol
.GetAllMembers()
.OfType<IFieldSymbol>()
.Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol))
.Select(property => MapField(sourceTypeSymbol, sourceProperties, property))
.Where(mappedProperty => mappedProperty is not null)
.ToImmutableArray()!;
}
protected override ImmutableArray<MappedMember> GetSourceMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool hasInheritedClass)
{
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray();
return typeSymbol
.GetAllMembers()
.OfType<IPropertySymbol>()
.Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol))
.Select(property => MapProperty(sourceTypeSymbol, sourceProperties, property))
.Where(mappedProperty => mappedProperty is not null)
.ToImmutableArray()!;
}
protected override ImmutableArray<MappedMember> GetTypeMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
{
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IFieldSymbol>().ToArray();
return sourceTypeSymbol
.GetAllMembers()
.OfType<IFieldSymbol>()
.Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol))
.Select(property => MapFieldSimple(typeSymbol, property))
.Where(mappedProperty => mappedProperty is not null)
.ToImmutableArray()!;
}
protected override ImmutableArray<MappedMember> GetTypeMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool hasInheritedClass)
{
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray();
return sourceTypeSymbol
.GetAllMembers()
.OfType<IPropertySymbol>()
.Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol))
.Select(property => MapPropertySimple(typeSymbol, property))
.Where(mappedProperty => mappedProperty is not null)
.ToImmutableArray()!;
}
}
}

View File

@ -1,27 +0,0 @@
using System.Collections.Immutable;
using System.Linq;
using MapTo.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace MapTo
{
internal class ClassMappingContext : MappingContext
{
internal ClassMappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax)
: base(compilation, sourceGenerationOptions, typeSyntax) { }
protected override ImmutableArray<MappedProperty> GetMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
{
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray();
return typeSymbol
.GetAllMembers(!isInheritFromMappedBaseClass)
.OfType<IPropertySymbol>()
.Where(p => !p.HasAttribute(IgnorePropertyAttributeTypeSymbol))
.Select(property => MapProperty(sourceTypeSymbol, sourceProperties, property))
.Where(mappedProperty => mappedProperty is not null)
.ToImmutableArray()!;
}
}
}

View File

@ -1,340 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using MapTo.Extensions;
using MapTo.Sources;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace MapTo
{
internal abstract class MappingContext
{
private readonly List<SymbolDisplayPart> _ignoredNamespaces;
protected MappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax)
{
_ignoredNamespaces = new();
Diagnostics = ImmutableArray<Diagnostic>.Empty;
Usings = ImmutableArray.Create("System", Constants.RootNamespace);
SourceGenerationOptions = sourceGenerationOptions;
TypeSyntax = typeSyntax;
Compilation = compilation;
IgnorePropertyAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(IgnorePropertyAttributeSource.FullyQualifiedName);
MapTypeConverterAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MapTypeConverterAttributeSource.FullyQualifiedName);
TypeConverterInterfaceTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(ITypeConverterSource.FullyQualifiedName);
MapPropertyAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MapPropertyAttributeSource.FullyQualifiedName);
MapFromAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MapFromAttributeSource.FullyQualifiedName);
MappingContextTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MappingContextSource.FullyQualifiedName);
AddUsingIfRequired(sourceGenerationOptions.SupportNullableStaticAnalysis, "System.Diagnostics.CodeAnalysis");
}
public ImmutableArray<Diagnostic> Diagnostics { get; private set; }
public MappingModel? Model { get; private set; }
protected Compilation Compilation { get; }
protected INamedTypeSymbol IgnorePropertyAttributeTypeSymbol { get; }
protected INamedTypeSymbol MapFromAttributeTypeSymbol { get; }
protected INamedTypeSymbol MappingContextTypeSymbol { get; }
protected INamedTypeSymbol MapPropertyAttributeTypeSymbol { get; }
protected INamedTypeSymbol MapTypeConverterAttributeTypeSymbol { get; }
protected SourceGenerationOptions SourceGenerationOptions { get; }
protected INamedTypeSymbol TypeConverterInterfaceTypeSymbol { get; }
protected TypeDeclarationSyntax TypeSyntax { get; }
protected ImmutableArray<string> Usings { get; private set; }
public static MappingContext Create(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax)
{
MappingContext context = typeSyntax switch
{
ClassDeclarationSyntax => new ClassMappingContext(compilation, sourceGenerationOptions, typeSyntax),
RecordDeclarationSyntax => new RecordMappingContext(compilation, sourceGenerationOptions, typeSyntax),
_ => throw new ArgumentOutOfRangeException()
};
context.Model = context.CreateMappingModel();
return context;
}
protected void AddDiagnostic(Diagnostic diagnostic)
{
Diagnostics = Diagnostics.Add(diagnostic);
}
protected void AddUsingIfRequired(ISymbol? namedTypeSymbol) =>
AddUsingIfRequired(namedTypeSymbol?.ContainingNamespace.IsGlobalNamespace == false, namedTypeSymbol?.ContainingNamespace);
protected void AddUsingIfRequired(bool condition, INamespaceSymbol? ns) =>
AddUsingIfRequired(condition && ns is not null && !_ignoredNamespaces.Contains(ns.ToDisplayParts().First()), ns?.ToDisplayString());
protected void AddUsingIfRequired(bool condition, string? ns)
{
if (ns is not null && condition && ns != TypeSyntax.GetNamespace() && !Usings.Contains(ns))
{
Usings = Usings.Add(ns);
}
}
protected IPropertySymbol? FindSourceProperty(IEnumerable<IPropertySymbol> sourceProperties, ISymbol property)
{
var propertyName = property
.GetAttribute(MapPropertyAttributeTypeSymbol)
?.NamedArguments
.SingleOrDefault(a => a.Key == MapPropertyAttributeSource.SourcePropertyNamePropertyName)
.Value.Value as string ?? property.Name;
return sourceProperties.SingleOrDefault(p => p.Name == propertyName);
}
protected abstract ImmutableArray<MappedProperty> GetMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass);
protected INamedTypeSymbol? GetSourceTypeSymbol(TypeDeclarationSyntax typeDeclarationSyntax, SemanticModel? semanticModel = null) =>
GetSourceTypeSymbol(typeDeclarationSyntax.GetAttribute(MapFromAttributeSource.AttributeName), semanticModel);
protected INamedTypeSymbol? GetSourceTypeSymbol(SyntaxNode? attributeSyntax, SemanticModel? semanticModel = null)
{
if (attributeSyntax is null)
{
return null;
}
semanticModel ??= Compilation.GetSemanticModel(attributeSyntax.SyntaxTree);
var sourceTypeExpressionSyntax = attributeSyntax
.DescendantNodes()
.OfType<TypeOfExpressionSyntax>()
.SingleOrDefault();
return sourceTypeExpressionSyntax is not null ? semanticModel.GetTypeInfo(sourceTypeExpressionSyntax.Type).Type as INamedTypeSymbol : null;
}
protected bool IsTypeInheritFromMappedBaseClass(SemanticModel semanticModel)
{
return TypeSyntax.BaseList is not null && TypeSyntax.BaseList.Types
.Select(t => semanticModel.GetTypeInfo(t.Type).Type)
.Any(t => t?.GetAttribute(MapFromAttributeTypeSymbol) != null);
}
protected virtual MappedProperty? MapProperty(ISymbol sourceTypeSymbol, IReadOnlyCollection<IPropertySymbol> sourceProperties, ISymbol property)
{
var sourceProperty = FindSourceProperty(sourceProperties, property);
if (sourceProperty is null || !property.TryGetTypeSymbol(out var propertyType))
{
return null;
}
string? converterFullyQualifiedName = null;
var converterParameters = ImmutableArray<string>.Empty;
ITypeSymbol? mappedSourcePropertyType = null;
ITypeSymbol? enumerableTypeArgumentType = null;
if (!Compilation.HasCompatibleTypes(sourceProperty, property))
{
if (!TryGetMapTypeConverter(property, sourceProperty, out converterFullyQualifiedName, out converterParameters) &&
!TryGetNestedObjectMappings(property, out mappedSourcePropertyType, out enumerableTypeArgumentType))
{
return null;
}
}
AddUsingIfRequired(propertyType);
AddUsingIfRequired(enumerableTypeArgumentType);
AddUsingIfRequired(mappedSourcePropertyType);
return new MappedProperty(
property.Name,
ToQualifiedDisplayName(propertyType) ?? propertyType.Name,
converterFullyQualifiedName,
converterParameters.ToImmutableArray(),
sourceProperty.Name,
ToQualifiedDisplayName(mappedSourcePropertyType),
ToQualifiedDisplayName(enumerableTypeArgumentType));
}
protected bool TryGetMapTypeConverter(ISymbol property, IPropertySymbol sourceProperty, out string? converterFullyQualifiedName,
out ImmutableArray<string> converterParameters)
{
converterFullyQualifiedName = null;
converterParameters = ImmutableArray<string>.Empty;
if (!Diagnostics.IsEmpty())
{
return false;
}
var typeConverterAttribute = property.GetAttribute(MapTypeConverterAttributeTypeSymbol);
if (typeConverterAttribute?.ConstructorArguments.First().Value is not INamedTypeSymbol converterTypeSymbol)
{
return false;
}
var baseInterface = GetTypeConverterBaseInterface(converterTypeSymbol, property, sourceProperty);
if (baseInterface is null)
{
AddDiagnostic(DiagnosticsFactory.InvalidTypeConverterGenericTypesError(property, sourceProperty));
return false;
}
converterFullyQualifiedName = converterTypeSymbol.ToDisplayString();
converterParameters = GetTypeConverterParameters(typeConverterAttribute);
return true;
}
protected bool TryGetNestedObjectMappings(ISymbol property, out ITypeSymbol? mappedSourcePropertyType, out ITypeSymbol? enumerableTypeArgument)
{
mappedSourcePropertyType = null;
enumerableTypeArgument = null;
if (!Diagnostics.IsEmpty())
{
return false;
}
if (!property.TryGetTypeSymbol(out var propertyType))
{
AddDiagnostic(DiagnosticsFactory.NoMatchingPropertyTypeFoundError(property));
return false;
}
var mapFromAttribute = propertyType.GetAttribute(MapFromAttributeTypeSymbol);
if (mapFromAttribute is null &&
propertyType is INamedTypeSymbol namedTypeSymbol &&
!propertyType.IsPrimitiveType() &&
(Compilation.IsGenericEnumerable(propertyType) || propertyType.AllInterfaces.Any(i => Compilation.IsGenericEnumerable(i))))
{
enumerableTypeArgument = namedTypeSymbol.TypeArguments.First();
mapFromAttribute = enumerableTypeArgument.GetAttribute(MapFromAttributeTypeSymbol);
}
mappedSourcePropertyType = mapFromAttribute?.ConstructorArguments.First().Value as INamedTypeSymbol;
if (mappedSourcePropertyType is null && enumerableTypeArgument is null)
{
AddDiagnostic(DiagnosticsFactory.NoMatchingPropertyTypeFoundError(property));
}
return Diagnostics.IsEmpty();
}
private static ImmutableArray<string> GetTypeConverterParameters(AttributeData typeConverterAttribute)
{
var converterParameter = typeConverterAttribute.ConstructorArguments.Skip(1).FirstOrDefault();
return converterParameter.IsNull
? ImmutableArray<string>.Empty
: converterParameter.Values.Where(v => v.Value is not null).Select(v => v.Value!.ToSourceCodeString()).ToImmutableArray();
}
private MappingModel? CreateMappingModel()
{
var semanticModel = Compilation.GetSemanticModel(TypeSyntax.SyntaxTree);
if (semanticModel.GetDeclaredSymbol(TypeSyntax) is not INamedTypeSymbol typeSymbol)
{
AddDiagnostic(DiagnosticsFactory.TypeNotFoundError(TypeSyntax.GetLocation(), TypeSyntax.Identifier.ValueText));
return null;
}
var sourceTypeSymbol = GetSourceTypeSymbol(TypeSyntax, semanticModel);
if (sourceTypeSymbol is null)
{
AddDiagnostic(DiagnosticsFactory.MapFromAttributeNotFoundError(TypeSyntax.GetLocation()));
return null;
}
_ignoredNamespaces.Add(sourceTypeSymbol.ContainingNamespace.ToDisplayParts().First());
var typeIdentifierName = TypeSyntax.GetIdentifierName();
var sourceTypeIdentifierName = sourceTypeSymbol.Name;
var isTypeInheritFromMappedBaseClass = IsTypeInheritFromMappedBaseClass(semanticModel);
var shouldGenerateSecondaryConstructor = ShouldGenerateSecondaryConstructor(semanticModel, sourceTypeSymbol);
var mappedProperties = GetMappedProperties(typeSymbol, sourceTypeSymbol, isTypeInheritFromMappedBaseClass);
if (!mappedProperties.Any())
{
AddDiagnostic(DiagnosticsFactory.NoMatchingPropertyFoundError(TypeSyntax.GetLocation(), typeSymbol, sourceTypeSymbol));
return null;
}
AddUsingIfRequired(mappedProperties.Any(p => p.IsEnumerable), "System.Linq");
return new MappingModel(
SourceGenerationOptions,
TypeSyntax.GetNamespace(),
TypeSyntax.Modifiers,
TypeSyntax.Keyword.Text,
typeIdentifierName,
sourceTypeSymbol.ContainingNamespace.ToDisplayString(),
sourceTypeIdentifierName,
sourceTypeSymbol.ToDisplayString(),
mappedProperties,
isTypeInheritFromMappedBaseClass,
Usings,
shouldGenerateSecondaryConstructor);
}
private INamedTypeSymbol? GetTypeConverterBaseInterface(ITypeSymbol converterTypeSymbol, ISymbol property, IPropertySymbol sourceProperty)
{
if (!property.TryGetTypeSymbol(out var propertyType))
{
return null;
}
return converterTypeSymbol.AllInterfaces
.SingleOrDefault(i =>
i.TypeArguments.Length == 2 &&
SymbolEqualityComparer.Default.Equals(i.ConstructedFrom, TypeConverterInterfaceTypeSymbol) &&
SymbolEqualityComparer.Default.Equals(sourceProperty.Type, i.TypeArguments[0]) &&
SymbolEqualityComparer.Default.Equals(propertyType, i.TypeArguments[1]));
}
private bool ShouldGenerateSecondaryConstructor(SemanticModel semanticModel, ISymbol sourceTypeSymbol)
{
var constructorSyntax = TypeSyntax.DescendantNodes()
.OfType<ConstructorDeclarationSyntax>()
.SingleOrDefault(c =>
c.ParameterList.Parameters.Count == 1 &&
SymbolEqualityComparer.Default.Equals(semanticModel.GetTypeInfo(c.ParameterList.Parameters.Single().Type!).ConvertedType, sourceTypeSymbol));
if (constructorSyntax is null)
{
// Secondary constructor is not defined.
return true;
}
if (constructorSyntax.Initializer?.ArgumentList.Arguments is not { Count: 2 } arguments ||
!SymbolEqualityComparer.Default.Equals(semanticModel.GetTypeInfo(arguments[0].Expression).ConvertedType, MappingContextTypeSymbol) ||
!SymbolEqualityComparer.Default.Equals(semanticModel.GetTypeInfo(arguments[1].Expression).ConvertedType, sourceTypeSymbol))
{
AddDiagnostic(DiagnosticsFactory.MissingConstructorArgument(constructorSyntax));
}
return false;
}
private string? ToQualifiedDisplayName(ISymbol? symbol)
{
if (symbol is null)
{
return null;
}
var containingNamespace = TypeSyntax.GetNamespace();
var symbolNamespace = symbol.ContainingNamespace.ToDisplayString();
return containingNamespace != symbolNamespace && _ignoredNamespaces.Contains(symbol.ContainingNamespace.ToDisplayParts().First())
? symbol.ToDisplayString()
: symbol.Name;
}
}
}

View File

@ -1,84 +0,0 @@
using System;
using System.Collections.Immutable;
using MapTo.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
namespace MapTo
{
internal enum AccessModifier
{
Public,
Internal,
Private
}
internal enum NullStaticAnalysisState
{
Default,
Enabled,
Disabled
}
internal record SourceCode(string Text, string HintName);
internal record MappedProperty(
string Name,
string Type,
string? TypeConverter,
ImmutableArray<string> TypeConverterParameters,
string SourcePropertyName,
string? MappedSourcePropertyTypeName,
string? EnumerableTypeArgument)
{
public bool IsEnumerable => EnumerableTypeArgument is not null;
}
internal record MappingModel (
SourceGenerationOptions Options,
string? Namespace,
SyntaxTokenList Modifiers,
string Type,
string TypeIdentifierName,
string SourceNamespace,
string SourceTypeIdentifierName,
string SourceTypeFullName,
ImmutableArray<MappedProperty> MappedProperties,
bool HasMappedBaseClass,
ImmutableArray<string> Usings,
bool GenerateSecondaryConstructor
)
{
public string SourceType => SourceTypeFullName;
}
internal record SourceGenerationOptions(
AccessModifier ConstructorAccessModifier,
AccessModifier GeneratedMethodsAccessModifier,
bool GenerateXmlDocument,
bool SupportNullableReferenceTypes,
bool SupportNullableStaticAnalysis)
{
internal static SourceGenerationOptions From(GeneratorExecutionContext context)
{
const string allowNullAttributeName = "System.Diagnostics.CodeAnalysis.AllowNullAttribute";
var supportNullableStaticAnalysis = context.GetBuildGlobalOption(propertyName: nameof(SupportNullableStaticAnalysis), NullStaticAnalysisState.Default);
var supportNullableReferenceTypes = context.Compilation.Options.NullableContextOptions is NullableContextOptions.Warnings or NullableContextOptions.Enable;
return new(
ConstructorAccessModifier: context.GetBuildGlobalOption(propertyName: nameof(ConstructorAccessModifier), AccessModifier.Public),
GeneratedMethodsAccessModifier: context.GetBuildGlobalOption(propertyName: nameof(GeneratedMethodsAccessModifier), AccessModifier.Public),
GenerateXmlDocument: context.GetBuildGlobalOption(propertyName: nameof(GenerateXmlDocument), true),
SupportNullableReferenceTypes: supportNullableReferenceTypes,
SupportNullableStaticAnalysis: supportNullableStaticAnalysis switch
{
NullStaticAnalysisState.Enabled => true,
NullStaticAnalysisState.Disabled => false,
_ => context.Compilation is CSharpCompilation { LanguageVersion: >= LanguageVersion.CSharp8 } cs && cs.TypeByMetadataNameExists(allowNullAttributeName)
}
);
}
public string NullableReferenceSyntax => SupportNullableReferenceTypes ? "?" : string.Empty;
}
}

View File

@ -1,28 +0,0 @@
using System.Collections.Immutable;
using System.Linq;
using MapTo.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace MapTo
{
internal class RecordMappingContext : MappingContext
{
internal RecordMappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax)
: base(compilation, sourceGenerationOptions, typeSyntax) { }
protected override ImmutableArray<MappedProperty> GetMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
{
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray();
return typeSymbol.GetMembers()
.OfType<IMethodSymbol>()
.OrderByDescending(s => s.Parameters.Length)
.First(s => s.Name == ".ctor")
.Parameters
.Where(p => !p.HasAttribute(IgnorePropertyAttributeTypeSymbol))
.Select(property => MapProperty(sourceTypeSymbol, sourceProperties, property))
.Where(mappedProperty => mappedProperty is not null)
.ToImmutableArray()!;
}
}
}

View File

@ -1,206 +0,0 @@
using MapTo.Extensions;
using static MapTo.Sources.Constants;
namespace MapTo.Sources
{
internal static class MapClassSource
{
internal static SourceCode Generate(MappingModel model)
{
using var builder = new SourceBuilder()
.WriteLine(GeneratedFilesHeader)
.WriteNullableContextOptionIf(model.Options.SupportNullableReferenceTypes)
.WriteUsings(model.Usings)
.WriteLine()
// Namespace declaration
.WriteLine($"namespace {model.Namespace}")
.WriteOpeningBracket()
// Class declaration
.WriteLine($"partial class {model.TypeIdentifierName}")
.WriteOpeningBracket();
// Class body
if (model.GenerateSecondaryConstructor)
{
builder
.GenerateSecondaryConstructor(model)
.WriteLine();
}
builder
.GeneratePrivateConstructor(model)
.WriteLine()
.GenerateFactoryMethod(model)
.GenerateUpdateMethod(model)
// End class declaration
.WriteClosingBracket()
.WriteLine()
// Extension class declaration
.GenerateSourceTypeExtensionClass(model)
// End namespace declaration
.WriteClosingBracket();
return new(builder.ToString(), $"{model.Namespace}.{model.TypeIdentifierName}.g.cs");
}
private static SourceBuilder GenerateSecondaryConstructor(this SourceBuilder builder, MappingModel model)
{
var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase();
if (model.Options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine($"/// Initializes a new instance of the <see cref=\"{model.TypeIdentifierName}\"/> class")
.WriteLine($"/// using the property values from the specified <paramref name=\"{sourceClassParameterName}\"/>.")
.WriteLine("/// </summary>")
.WriteLine($"/// <exception cref=\"ArgumentNullException\">{sourceClassParameterName} is null</exception>");
}
return builder
.WriteLine($"{model.Options.ConstructorAccessModifier.ToLowercaseString()} {model.TypeIdentifierName}({model.SourceType} {sourceClassParameterName})")
.WriteLine($" : this(new {MappingContextSource.ClassName}(), {sourceClassParameterName}) {{ }}");
}
private static SourceBuilder GeneratePrivateConstructor(this SourceBuilder builder, MappingModel model)
{
var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase();
const string mappingContextParameterName = "context";
var baseConstructor = model.HasMappedBaseClass ? $" : base({mappingContextParameterName}, {sourceClassParameterName})" : string.Empty;
builder
.WriteLine($"private protected {model.TypeIdentifierName}({MappingContextSource.ClassName} {mappingContextParameterName}, {model.SourceType} {sourceClassParameterName}){baseConstructor}")
.WriteOpeningBracket()
.WriteLine($"if ({mappingContextParameterName} == null) throw new ArgumentNullException(nameof({mappingContextParameterName}));")
.WriteLine($"if ({sourceClassParameterName} == null) throw new ArgumentNullException(nameof({sourceClassParameterName}));")
.WriteLine()
.WriteLine($"{mappingContextParameterName}.{MappingContextSource.RegisterMethodName}({sourceClassParameterName}, this);")
.WriteLine().
WriteProperties( model, sourceClassParameterName, mappingContextParameterName);
// End constructor declaration
return builder.WriteClosingBracket();
}
private static SourceBuilder WriteProperties(this SourceBuilder builder, MappingModel model,
string? sourceClassParameterName, string mappingContextParameterName)
{
foreach (var property in model.MappedProperties)
{
if (property.TypeConverter is null)
{
if (property.IsEnumerable)
{
builder.WriteLine(
$"{property.Name} = {sourceClassParameterName}.{property.SourcePropertyName}.Select({mappingContextParameterName}.{MappingContextSource.MapMethodName}<{property.MappedSourcePropertyTypeName}, {property.EnumerableTypeArgument}>).ToList();");
}
else
{
builder.WriteLine(property.MappedSourcePropertyTypeName is null
? $"{property.Name} = {sourceClassParameterName}.{property.SourcePropertyName};"
: $"{property.Name} = {mappingContextParameterName}.{MappingContextSource.MapMethodName}<{property.MappedSourcePropertyTypeName}, {property.Type}>({sourceClassParameterName}.{property.SourcePropertyName});");
}
}
else
{
var parameters = property.TypeConverterParameters.IsEmpty
? "null"
: $"new object[] {{ {string.Join(", ", property.TypeConverterParameters)} }}";
builder.WriteLine(
$"{property.Name} = new {property.TypeConverter}().Convert({sourceClassParameterName}.{property.SourcePropertyName}, {parameters});");
}
}
return builder;
}
private static SourceBuilder GenerateFactoryMethod(this SourceBuilder builder, MappingModel model)
{
var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase();
return builder
.GenerateConvertorMethodsXmlDocs(model, sourceClassParameterName)
.WriteLineIf(model.Options.SupportNullableStaticAnalysis, $"[return: NotNullIfNotNull(\"{sourceClassParameterName}\")]")
.WriteLine($"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static {model.TypeIdentifierName}{model.Options.NullableReferenceSyntax} From({model.SourceType}{model.Options.NullableReferenceSyntax} {sourceClassParameterName})")
.WriteOpeningBracket()
.WriteLine($"return {sourceClassParameterName} == null ? null : {MappingContextSource.ClassName}.{MappingContextSource.FactoryMethodName}<{model.SourceType}, {model.TypeIdentifierName}>({sourceClassParameterName});")
.WriteClosingBracket();
}
private static SourceBuilder GenerateUpdateMethod(this SourceBuilder builder, MappingModel model)
{
var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase();
builder
.GenerateUpdaterMethodsXmlDocs(model, sourceClassParameterName)
.WriteLine($"public void {model.Options.NullableReferenceSyntax}Update({model.SourceType}{model.Options.NullableReferenceSyntax} {sourceClassParameterName})")
.WriteOpeningBracket()
.WriteProperties( model, sourceClassParameterName,"context" )
.WriteClosingBracket();
return builder;
}
private static SourceBuilder GenerateConvertorMethodsXmlDocs(this SourceBuilder builder, MappingModel model, string sourceClassParameterName)
{
if (!model.Options.GenerateXmlDocument)
{
return builder;
}
return builder
.WriteLine("/// <summary>")
.WriteLine($"/// Creates a new instance of <see cref=\"{model.TypeIdentifierName}\"/> and sets its participating properties")
.WriteLine($"/// using the property values from <paramref name=\"{sourceClassParameterName}\"/>.")
.WriteLine("/// </summary>")
.WriteLine($"/// <param name=\"{sourceClassParameterName}\">The instance of <see cref=\"{model.SourceType}\"/> to use as source.</param>")
.WriteLine($"/// <returns>A new instance of <see cred=\"{model.TypeIdentifierName}\"/> -or- <c>null</c> if <paramref name=\"{sourceClassParameterName}\"/> is <c>null</c>.</returns>");
}
private static SourceBuilder GenerateUpdaterMethodsXmlDocs(this SourceBuilder builder, MappingModel model, string sourceClassParameterName)
{
if (!model.Options.GenerateXmlDocument)
{
return builder;
}
return builder
.WriteLine("/// <summary>")
.WriteLine($"/// Updates <see cref=\"{model.TypeIdentifierName}\"/> and sets its participating properties")
.WriteLine($"/// using the property values from <paramref name=\"{sourceClassParameterName}\"/>.")
.WriteLine("/// </summary>")
.WriteLine($"/// <param name=\"{sourceClassParameterName}\">The instance of <see cref=\"{model.SourceType}\"/> to use as source.</param>");
}
private static SourceBuilder GenerateSourceTypeExtensionClass(this SourceBuilder builder, MappingModel model)
{
return builder
.WriteLine($"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static partial class {model.SourceTypeIdentifierName}To{model.TypeIdentifierName}Extensions")
.WriteOpeningBracket()
.GenerateSourceTypeExtensionMethod(model)
.WriteClosingBracket();
}
private static SourceBuilder GenerateSourceTypeExtensionMethod(this SourceBuilder builder, MappingModel model)
{
var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase();
return builder
.GenerateConvertorMethodsXmlDocs(model, sourceClassParameterName)
.WriteLineIf(model.Options.SupportNullableStaticAnalysis, $"[return: NotNullIfNotNull(\"{sourceClassParameterName}\")]")
.WriteLine($"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static {model.TypeIdentifierName}{model.Options.NullableReferenceSyntax} To{model.TypeIdentifierName}(this {model.SourceType}{model.Options.NullableReferenceSyntax} {sourceClassParameterName})")
.WriteOpeningBracket()
.WriteLine($"return {sourceClassParameterName} == null ? null : new {model.TypeIdentifierName}({sourceClassParameterName});")
.WriteClosingBracket();
}
}
}

View File

@ -1,191 +0,0 @@
using System;
using MapTo.Extensions;
using static MapTo.Sources.Constants;
namespace MapTo.Sources
{
internal static class MapRecordSource
{
internal static SourceCode Generate(MappingModel model)
{
using var builder = new SourceBuilder()
.WriteLine(GeneratedFilesHeader)
.WriteNullableContextOptionIf(model.Options.SupportNullableReferenceTypes)
.WriteUsings(model.Usings)
.WriteLine()
// Namespace declaration
.WriteLine($"namespace {model.Namespace}")
.WriteOpeningBracket()
// Class declaration
.WriteLine($"partial record {model.TypeIdentifierName}")
.WriteOpeningBracket();
// Class body
if (model.GenerateSecondaryConstructor)
{
builder
.GenerateSecondaryConstructor(model)
.WriteLine();
}
builder
.GeneratePrivateConstructor(model)
.WriteLine()
.GenerateFactoryMethod(model)
// End class declaration
.WriteClosingBracket()
.WriteLine()
// Extension class declaration
.GenerateSourceTypeExtensionClass(model)
// End namespace declaration
.WriteClosingBracket();
return new(builder.ToString(), $"{model.Namespace}.{model.TypeIdentifierName}.g.cs");
}
private static SourceBuilder GenerateSecondaryConstructor(this SourceBuilder builder, MappingModel model)
{
var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase();
if (model.Options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine($"/// Initializes a new instance of the <see cref=\"{model.TypeIdentifierName}\"/> class")
.WriteLine($"/// using the property values from the specified <paramref name=\"{sourceClassParameterName}\"/>.")
.WriteLine("/// </summary>")
.WriteLine($"/// <exception cref=\"ArgumentNullException\">{sourceClassParameterName} is null</exception>");
}
return builder
.WriteLine($"{model.Options.ConstructorAccessModifier.ToLowercaseString()} {model.TypeIdentifierName}({model.SourceType} {sourceClassParameterName})")
.WriteLine($" : this(new {MappingContextSource.ClassName}(), {sourceClassParameterName}) {{ }}");
}
private static SourceBuilder GeneratePrivateConstructor(this SourceBuilder builder, MappingModel model)
{
var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase();
const string mappingContextParameterName = "context";
builder
.WriteLine($"private protected {model.TypeIdentifierName}({MappingContextSource.ClassName} {mappingContextParameterName}, {model.SourceType} {sourceClassParameterName})")
.Indent()
.Write(": this(").
WriteProperties(model, sourceClassParameterName, mappingContextParameterName)
.WriteLine(")")
.Unindent()
.WriteOpeningBracket()
.WriteLine($"if ({mappingContextParameterName} == null) throw new ArgumentNullException(nameof({mappingContextParameterName}));")
.WriteLine($"if ({sourceClassParameterName} == null) throw new ArgumentNullException(nameof({sourceClassParameterName}));")
.WriteLine()
.WriteLine($"{mappingContextParameterName}.{MappingContextSource.RegisterMethodName}({sourceClassParameterName}, this);");
// End constructor declaration
return builder.WriteClosingBracket();
}
private static SourceBuilder WriteProperties(this SourceBuilder builder, MappingModel model, string sourceClassParameterName,
string mappingContextParameterName)
{
for (var i = 0; i < model.MappedProperties.Length; i++)
{
var property = model.MappedProperties[i];
if (property.TypeConverter is null)
{
if (property.IsEnumerable)
{
builder.Write(
$"{property.Name}: {sourceClassParameterName}.{property.SourcePropertyName}.Select({mappingContextParameterName}.{MappingContextSource.MapMethodName}<{property.MappedSourcePropertyTypeName}, {property.EnumerableTypeArgument}>).ToList()");
}
else
{
builder.Write(property.MappedSourcePropertyTypeName is null
? $"{property.Name}: {sourceClassParameterName}.{property.SourcePropertyName}"
: $"{property.Name}: {mappingContextParameterName}.{MappingContextSource.MapMethodName}<{property.MappedSourcePropertyTypeName}, {property.Type}>({sourceClassParameterName}.{property.SourcePropertyName})");
}
}
else
{
var parameters = property.TypeConverterParameters.IsEmpty
? "null"
: $"new object[] {{ {string.Join(", ", property.TypeConverterParameters)} }}";
builder.Write(
$"{property.Name}: new {property.TypeConverter}().Convert({sourceClassParameterName}.{property.SourcePropertyName}, {parameters})");
}
if (i < model.MappedProperties.Length - 1)
{
builder.Write(", ");
}
}
return builder;
}
private static SourceBuilder GenerateFactoryMethod(this SourceBuilder builder, MappingModel model)
{
var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase();
return builder
.GenerateConvertorMethodsXmlDocs(model, sourceClassParameterName)
.WriteLineIf(model.Options.SupportNullableStaticAnalysis, $"[return: NotNullIfNotNull(\"{sourceClassParameterName}\")]")
.WriteLine(
$"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static {model.TypeIdentifierName}{model.Options.NullableReferenceSyntax} From({model.SourceType}{model.Options.NullableReferenceSyntax} {sourceClassParameterName})")
.WriteOpeningBracket()
.WriteLine(
$"return {sourceClassParameterName} == null ? null : {MappingContextSource.ClassName}.{MappingContextSource.FactoryMethodName}<{model.SourceType}, {model.TypeIdentifierName}>({sourceClassParameterName});")
.WriteClosingBracket();
}
private static SourceBuilder GenerateConvertorMethodsXmlDocs(this SourceBuilder builder, MappingModel model, string sourceClassParameterName)
{
if (!model.Options.GenerateXmlDocument)
{
return builder;
}
return builder
.WriteLine("/// <summary>")
.WriteLine($"/// Creates a new instance of <see cref=\"{model.TypeIdentifierName}\"/> and sets its participating properties")
.WriteLine($"/// using the property values from <paramref name=\"{sourceClassParameterName}\"/>.")
.WriteLine("/// </summary>")
.WriteLine($"/// <param name=\"{sourceClassParameterName}\">The instance of <see cref=\"{model.SourceType}\"/> to use as source.</param>")
.WriteLine(
$"/// <returns>A new instance of <see cred=\"{model.TypeIdentifierName}\"/> -or- <c>null</c> if <paramref name=\"{sourceClassParameterName}\"/> is <c>null</c>.</returns>");
}
private static SourceBuilder GenerateSourceTypeExtensionClass(this SourceBuilder builder, MappingModel model)
{
return builder
.WriteLine(
$"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static partial class {model.SourceTypeIdentifierName}To{model.TypeIdentifierName}Extensions")
.WriteOpeningBracket()
.GenerateSourceTypeExtensionMethod(model)
.WriteClosingBracket();
}
private static SourceBuilder GenerateSourceTypeExtensionMethod(this SourceBuilder builder, MappingModel model)
{
var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase();
return builder
.GenerateConvertorMethodsXmlDocs(model, sourceClassParameterName)
.WriteLineIf(model.Options.SupportNullableStaticAnalysis, $"[return: NotNullIfNotNull(\"{sourceClassParameterName}\")]")
.WriteLine(
$"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static {model.TypeIdentifierName}{model.Options.NullableReferenceSyntax} To{model.TypeIdentifierName}(this {model.SourceType}{model.Options.NullableReferenceSyntax} {sourceClassParameterName})")
.WriteOpeningBracket()
.WriteLine($"return {sourceClassParameterName} == null ? null : new {model.TypeIdentifierName}({sourceClassParameterName});")
.WriteClosingBracket();
}
}
}

View File

@ -1,153 +0,0 @@
using MapTo.Extensions;
using static MapTo.Sources.Constants;
namespace MapTo.Sources
{
internal static class MapStructSource
{
internal static SourceCode Generate(MappingModel model)
{
using var builder = new SourceBuilder()
.WriteLine(GeneratedFilesHeader)
.WriteNullableContextOptionIf(model.Options.SupportNullableReferenceTypes)
.WriteUsings(model.Usings)
.WriteLine()
// Namespace declaration
.WriteLine($"namespace {model.Namespace}")
.WriteOpeningBracket()
// Class declaration
.WriteLine($"partial class {model.TypeIdentifierName}")
.WriteOpeningBracket();
// Class body
if (model.GenerateSecondaryConstructor)
{
builder
.GenerateSecondaryConstructor(model)
.WriteLine();
}
builder
.GeneratePrivateConstructor(model)
.WriteLine()
// End class declaration
.WriteClosingBracket()
.WriteLine()
// End namespace declaration
.WriteClosingBracket();
return new(builder.ToString(), $"{model.Namespace}.{model.TypeIdentifierName}.g.cs");
}
private static SourceBuilder GenerateSecondaryConstructor(this SourceBuilder builder, MappingModel model)
{
var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase();
if (model.Options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine($"/// Initializes a new instance of the <see cref=\"{model.TypeIdentifierName}\"/> class")
.WriteLine($"/// using the property values from the specified <paramref name=\"{sourceClassParameterName}\"/>.")
.WriteLine("/// </summary>")
.WriteLine($"/// <exception cref=\"ArgumentNullException\">{sourceClassParameterName} is null</exception>");
}
return builder
.WriteLine($"{model.Options.ConstructorAccessModifier.ToLowercaseString()} {model.TypeIdentifierName}({model.SourceType} {sourceClassParameterName})")
.WriteLine($" : this(new {MappingContextSource.ClassName}(), {sourceClassParameterName}) {{ }}");
}
private static SourceBuilder GeneratePrivateConstructor(this SourceBuilder builder, MappingModel model)
{
var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase();
const string mappingContextParameterName = "context";
var baseConstructor = model.HasMappedBaseClass ? $" : base({mappingContextParameterName}, {sourceClassParameterName})" : string.Empty;
builder
.WriteLine($"private protected {model.TypeIdentifierName}({MappingContextSource.ClassName} {mappingContextParameterName}, {model.SourceType} {sourceClassParameterName}){baseConstructor}")
.WriteOpeningBracket()
.WriteLine()
.WriteLine($"{mappingContextParameterName}.{MappingContextSource.RegisterMethodName}({sourceClassParameterName}, this);")
.WriteLine().
WriteProperties( model, sourceClassParameterName, mappingContextParameterName);
// End constructor declaration
return builder.WriteClosingBracket();
}
private static SourceBuilder WriteProperties(this SourceBuilder builder, MappingModel model,
string? sourceClassParameterName, string mappingContextParameterName)
{
foreach (var property in model.MappedProperties)
{
if (property.TypeConverter is null)
{
if (property.IsEnumerable)
{
builder.WriteLine(
$"{property.Name} = {sourceClassParameterName}.{property.SourcePropertyName}.Select({mappingContextParameterName}.{MappingContextSource.MapMethodName}<{property.MappedSourcePropertyTypeName}, {property.EnumerableTypeArgument}>).ToList();");
}
else
{
builder.WriteLine(property.MappedSourcePropertyTypeName is null
? $"{property.Name} = {sourceClassParameterName}.{property.SourcePropertyName};"
: $"{property.Name} = {mappingContextParameterName}.{MappingContextSource.MapMethodName}<{property.MappedSourcePropertyTypeName}, {property.Type}>({sourceClassParameterName}.{property.SourcePropertyName});");
}
}
else
{
var parameters = property.TypeConverterParameters.IsEmpty
? "null"
: $"new object[] {{ {string.Join(", ", property.TypeConverterParameters)} }}";
builder.WriteLine(
$"{property.Name} = new {property.TypeConverter}().Convert({sourceClassParameterName}.{property.SourcePropertyName}, {parameters});");
}
}
return builder;
}
private static SourceBuilder GenerateConvertorMethodsXmlDocs(this SourceBuilder builder, MappingModel model, string sourceClassParameterName)
{
if (!model.Options.GenerateXmlDocument)
{
return builder;
}
return builder
.WriteLine("/// <summary>")
.WriteLine($"/// Creates a new instance of <see cref=\"{model.TypeIdentifierName}\"/> and sets its participating properties")
.WriteLine($"/// using the property values from <paramref name=\"{sourceClassParameterName}\"/>.")
.WriteLine("/// </summary>")
.WriteLine($"/// <param name=\"{sourceClassParameterName}\">The instance of <see cref=\"{model.SourceType}\"/> to use as source.</param>")
.WriteLine($"/// <returns>A new instance of <see cred=\"{model.TypeIdentifierName}\"/> -or- <c>null</c> if <paramref name=\"{sourceClassParameterName}\"/> is <c>null</c>.</returns>");
}
private static SourceBuilder GenerateUpdaterMethodsXmlDocs(this SourceBuilder builder, MappingModel model, string sourceClassParameterName)
{
if (!model.Options.GenerateXmlDocument)
{
return builder;
}
return builder
.WriteLine("/// <summary>")
.WriteLine($"/// Updates <see cref=\"{model.TypeIdentifierName}\"/> and sets its participating properties")
.WriteLine($"/// using the property values from <paramref name=\"{sourceClassParameterName}\"/>.")
.WriteLine("/// </summary>")
.WriteLine($"/// <param name=\"{sourceClassParameterName}\">The instance of <see cref=\"{model.SourceType}\"/> to use as source.</param>");
}
}
}

View File

@ -1,37 +1,42 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
<RootNamespace>MapTo.Tests</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.1">
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.8.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.2.0" />
<PackageReference Update="Nerdbank.GitVersioning">
<Version>3.5.109</Version>
</PackageReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.0" />
<PackageReference Include="Shouldly" Version="4.0.3" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.0.3">
<PackageReference Include="coverlet.collector" Version="3.1.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\MapTo\MapTo.csproj" />
<ProjectReference Include="..\..\src\BlueWest.MapTo\BlueWest.MapTo.csproj" />
</ItemGroup>
</Project>

View File

@ -1,252 +1,252 @@
using System;
using System.Collections.Generic;
using System.Linq;
using MapTo.Extensions;
using MapTo.Sources;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Shouldly;
namespace MapTo.Tests
{
internal static class Common
{
internal const int Indent1 = 4;
internal const int Indent2 = Indent1 * 2;
internal const int Indent3 = Indent1 * 3;
internal static readonly Location IgnoreLocation = Location.None;
internal static readonly Dictionary<string, string> DefaultAnalyzerOptions = new()
{
[GeneratorExecutionContextExtensions.GetBuildPropertyName(nameof(SourceGenerationOptions.GenerateXmlDocument))] = "false"
};
internal static string GetSourceText(SourceGeneratorOptions? options = null)
{
const string ns = "Test";
options ??= new SourceGeneratorOptions();
var hasDifferentSourceNamespace = options.SourceClassNamespace != ns;
var builder = new SourceBuilder();
builder.WriteLine("//");
builder.WriteLine("// Test source code.");
builder.WriteLine("//");
builder.WriteLine();
options.Usings?.ForEach(s => builder.WriteLine($"using {s};"));
if (options.UseMapToNamespace)
{
builder.WriteLine($"using {Constants.RootNamespace};");
}
builder
.WriteLine($"using {options.SourceClassNamespace};")
.WriteLine()
.WriteLine();
builder
.WriteLine($"namespace {ns}")
.WriteOpeningBracket();
if (hasDifferentSourceNamespace && options.UseMapToNamespace)
{
builder
.WriteLine($"using {options.SourceClassNamespace};")
.WriteLine()
.WriteLine();
}
builder
.WriteLine(options.UseMapToNamespace ? "[MapFrom(typeof(Baz))]" : "[MapTo.MapFrom(typeof(Baz))]")
.WriteLine("public partial class Foo")
.WriteOpeningBracket();
for (var i = 1; i <= options.ClassPropertiesCount; i++)
{
builder.WriteLine(i % 2 == 0 ? $"public int Prop{i} {{ get; set; }}" : $"public int Prop{i} {{ get; }}");
}
options.PropertyBuilder?.Invoke(builder);
builder
.WriteClosingBracket()
.WriteClosingBracket()
.WriteLine()
.WriteLine();
builder
.WriteLine($"namespace {options.SourceClassNamespace}")
.WriteOpeningBracket()
.WriteLine("public class Baz")
.WriteOpeningBracket();
for (var i = 1; i <= options.SourceClassPropertiesCount; i++)
{
builder.WriteLine(i % 2 == 0 ? $"public int Prop{i} {{ get; set; }}" : $"public int Prop{i} {{ get; }}");
}
options.SourcePropertyBuilder?.Invoke(builder);
builder
.WriteClosingBracket()
.WriteClosingBracket();
return builder.ToString();
}
internal static string[] GetEmployeeManagerSourceText(
Func<string>? employeeClassSource = null,
Func<string>? managerClassSource = null,
Func<string>? employeeViewModelSource = null,
Func<string>? managerViewModelSource = null,
bool useDifferentViewModelNamespace = false)
{
return new[]
{
employeeClassSource?.Invoke() ?? DefaultEmployeeClassSource(),
managerClassSource?.Invoke() ?? DefaultManagerClassSource(),
employeeViewModelSource?.Invoke() ??
DefaultEmployeeViewModelSource(useDifferentViewModelNamespace),
managerViewModelSource?.Invoke() ?? DefaultManagerViewModelSource(useDifferentViewModelNamespace)
};
static string DefaultEmployeeClassSource() =>
@"
using System;
using System.Collections.Generic;
using System.Text;
namespace Test.Data.Models
{
public class Employee
{
public int Id { get; set; }
public string EmployeeCode { get; set; }
public Manager Manager { get; set; }
}
}".Trim();
static string DefaultManagerClassSource() =>
@"using System;
using System.Collections.Generic;
using System.Text;
namespace Test.Data.Models
{
public class Manager: Employee
{
public int Level { get; set; }
public IEnumerable<Employee> Employees { get; set; } = Array.Empty<Employee>();
}
}
".Trim();
static string DefaultEmployeeViewModelSource(bool useDifferentNamespace) => useDifferentNamespace
? @"
using MapTo;
using Test.Data.Models;
using Test.ViewModels2;
namespace Test.ViewModels
{
[MapFrom(typeof(Employee))]
public partial class EmployeeViewModel
{
public int Id { get; set; }
public string EmployeeCode { get; set; }
public ManagerViewModel Manager { get; set; }
}
}
".Trim()
: @"
using MapTo;
using Test.Data.Models;
namespace Test.ViewModels
{
[MapFrom(typeof(Employee))]
public partial class EmployeeViewModel
{
public int Id { get; set; }
public string EmployeeCode { get; set; }
public ManagerViewModel Manager { get; set; }
}
}
".Trim();
static string DefaultManagerViewModelSource(bool useDifferentNamespace) => useDifferentNamespace
? @"
using System;
using System.Collections.Generic;
using MapTo;
using Test.Data.Models;
using Test.ViewModels;
namespace Test.ViewModels2
{
[MapFrom(typeof(Manager))]
public partial class ManagerViewModel : EmployeeViewModel
{
public int Level { get; set; }
public IEnumerable<EmployeeViewModel> Employees { get; set; } = Array.Empty<EmployeeViewModel>();
}
}
".Trim()
: @"
using System;
using System.Collections.Generic;
using MapTo;
using Test.Data.Models;
namespace Test.ViewModels
{
[MapFrom(typeof(Manager))]
public partial class ManagerViewModel : EmployeeViewModel
{
public int Level { get; set; }
public IEnumerable<EmployeeViewModel> Employees { get; set; } = Array.Empty<EmployeeViewModel>();
}
}".Trim();
}
internal static PropertyDeclarationSyntax GetPropertyDeclarationSyntax(SyntaxTree syntaxTree, string targetPropertyName, string targetClass = "Foo")
{
return syntaxTree.GetRoot()
.DescendantNodes()
.OfType<ClassDeclarationSyntax>()
.Single(c => c.Identifier.ValueText == targetClass)
.DescendantNodes()
.OfType<PropertyDeclarationSyntax>()
.Single(p => p.Identifier.ValueText == targetPropertyName);
}
internal static IPropertySymbol GetSourcePropertySymbol(string propertyName, Compilation compilation, string targetClass = "Foo")
{
var syntaxTree = compilation.SyntaxTrees.First();
var propSyntax = GetPropertyDeclarationSyntax(syntaxTree, propertyName, targetClass);
var semanticModel = compilation.GetSemanticModel(syntaxTree);
return semanticModel.GetDeclaredSymbol(propSyntax).ShouldNotBeNull();
}
internal record SourceGeneratorOptions(
bool UseMapToNamespace = false,
string SourceClassNamespace = "Test.Models",
int ClassPropertiesCount = 3,
int SourceClassPropertiesCount = 3,
Action<SourceBuilder>? PropertyBuilder = null,
Action<SourceBuilder>? SourcePropertyBuilder = null,
IEnumerable<string>? Usings = null);
}
using System;
using System.Collections.Generic;
using System.Linq;
using MapTo.Extensions;
using MapTo.Sources;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Shouldly;
namespace MapTo.Tests
{
internal static class Common
{
internal const int Indent1 = 4;
internal const int Indent2 = Indent1 * 2;
internal const int Indent3 = Indent1 * 3;
internal static readonly Location IgnoreLocation = Location.None;
internal static readonly Dictionary<string, string> DefaultAnalyzerOptions = new()
{
[GeneratorExecutionContextExtensions.GetBuildPropertyName(nameof(SourceGenerationOptions.GenerateXmlDocument))] = "false"
};
internal static string GetSourceText(SourceGeneratorOptions? options = null)
{
const string ns = "Test";
options ??= new SourceGeneratorOptions();
var hasDifferentSourceNamespace = options.SourceClassNamespace != ns;
var builder = new SourceBuilder();
builder.WriteLine("//");
builder.WriteLine("// Test source code.");
builder.WriteLine("//");
builder.WriteLine();
options.Usings?.ForEach(s => builder.WriteLine($"using {s};"));
if (options.UseMapToNamespace)
{
builder.WriteLine($"using {Constants.RootNamespace};");
}
builder
.WriteLine($"using {options.SourceClassNamespace};")
.WriteLine()
.WriteLine();
builder
.WriteLine($"namespace {ns}")
.WriteOpeningBracket();
if (hasDifferentSourceNamespace && options.UseMapToNamespace)
{
builder
.WriteLine($"using {options.SourceClassNamespace};")
.WriteLine()
.WriteLine();
}
builder
.WriteLine(options.UseMapToNamespace ? "[MapFrom(typeof(Baz))]" : "[MapTo.MapFrom(typeof(Baz))]")
.WriteLine("public partial class Foo")
.WriteOpeningBracket();
for (var i = 1; i <= options.ClassPropertiesCount; i++)
{
builder.WriteLine(i % 2 == 0 ? $"public int Prop{i} {{ get; set; }}" : $"public int Prop{i} {{ get; }}");
}
options.PropertyBuilder?.Invoke(builder);
builder
.WriteClosingBracket()
.WriteClosingBracket()
.WriteLine()
.WriteLine();
builder
.WriteLine($"namespace {options.SourceClassNamespace}")
.WriteOpeningBracket()
.WriteLine("public class Baz")
.WriteOpeningBracket();
for (var i = 1; i <= options.SourceClassPropertiesCount; i++)
{
builder.WriteLine(i % 2 == 0 ? $"public int Prop{i} {{ get; set; }}" : $"public int Prop{i} {{ get; }}");
}
options.SourcePropertyBuilder?.Invoke(builder);
builder
.WriteClosingBracket()
.WriteClosingBracket();
return builder.ToString();
}
internal static string[] GetEmployeeManagerSourceText(
Func<string>? employeeClassSource = null,
Func<string>? managerClassSource = null,
Func<string>? employeeViewModelSource = null,
Func<string>? managerViewModelSource = null,
bool useDifferentViewModelNamespace = false)
{
return new[]
{
employeeClassSource?.Invoke() ?? DefaultEmployeeClassSource(),
managerClassSource?.Invoke() ?? DefaultManagerClassSource(),
employeeViewModelSource?.Invoke() ??
DefaultEmployeeViewModelSource(useDifferentViewModelNamespace),
managerViewModelSource?.Invoke() ?? DefaultManagerViewModelSource(useDifferentViewModelNamespace)
};
static string DefaultEmployeeClassSource() =>
@"
using System;
using System.Collections.Generic;
using System.Text;
namespace Test.Data.Models
{
public class Employee
{
public int Id { get; set; }
public string EmployeeCode { get; set; }
public Manager Manager { get; set; }
}
}".Trim();
static string DefaultManagerClassSource() =>
@"using System;
using System.Collections.Generic;
using System.Text;
namespace Test.Data.Models
{
public class Manager: Employee
{
public int Level { get; set; }
public IEnumerable<Employee> Employees { get; set; } = Array.Empty<Employee>();
}
}
".Trim();
static string DefaultEmployeeViewModelSource(bool useDifferentNamespace) => useDifferentNamespace
? @"
using MapTo;
using Test.Data.Models;
using Test.ViewModels2;
namespace Test.ViewModels
{
[MapFrom(typeof(Employee))]
public partial class EmployeeViewModel
{
public int Id { get; set; }
public string EmployeeCode { get; set; }
public ManagerViewModel Manager { get; set; }
}
}
".Trim()
: @"
using MapTo;
using Test.Data.Models;
namespace Test.ViewModels
{
[MapFrom(typeof(Employee))]
public partial class EmployeeViewModel
{
public int Id { get; set; }
public string EmployeeCode { get; set; }
public ManagerViewModel Manager { get; set; }
}
}
".Trim();
static string DefaultManagerViewModelSource(bool useDifferentNamespace) => useDifferentNamespace
? @"
using System;
using System.Collections.Generic;
using MapTo;
using Test.Data.Models;
using Test.ViewModels;
namespace Test.ViewModels2
{
[MapFrom(typeof(Manager))]
public partial class ManagerViewModel : EmployeeViewModel
{
public int Level { get; set; }
public IEnumerable<EmployeeViewModel> Employees { get; set; } = Array.Empty<EmployeeViewModel>();
}
}
".Trim()
: @"
using System;
using System.Collections.Generic;
using MapTo;
using Test.Data.Models;
namespace Test.ViewModels
{
[MapFrom(typeof(Manager))]
public partial class ManagerViewModel : EmployeeViewModel
{
public int Level { get; set; }
public IEnumerable<EmployeeViewModel> Employees { get; set; } = Array.Empty<EmployeeViewModel>();
}
}".Trim();
}
internal static PropertyDeclarationSyntax GetPropertyDeclarationSyntax(SyntaxTree syntaxTree, string targetPropertyName, string targetClass = "Foo")
{
return syntaxTree.GetRoot()
.DescendantNodes()
.OfType<ClassDeclarationSyntax>()
.Single(c => c.Identifier.ValueText == targetClass)
.DescendantNodes()
.OfType<PropertyDeclarationSyntax>()
.Single(p => p.Identifier.ValueText == targetPropertyName);
}
internal static IPropertySymbol GetSourcePropertySymbol(string propertyName, Compilation compilation, string targetClass = "Foo")
{
var syntaxTree = compilation.SyntaxTrees.First();
var propSyntax = GetPropertyDeclarationSyntax(syntaxTree, propertyName, targetClass);
var semanticModel = compilation.GetSemanticModel(syntaxTree);
return semanticModel.GetDeclaredSymbol(propSyntax).ShouldNotBeNull();
}
internal record SourceGeneratorOptions(
bool UseMapToNamespace = false,
string SourceClassNamespace = "Test.Models",
int ClassPropertiesCount = 3,
int SourceClassPropertiesCount = 3,
Action<SourceBuilder>? PropertyBuilder = null,
Action<SourceBuilder>? SourcePropertyBuilder = null,
IEnumerable<string>? Usings = null);
}
}

View File

@ -1,16 +1,16 @@
// ReSharper disable UnusedType.Global
// ReSharper disable CheckNamespace
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.ComponentModel;
namespace System.Runtime.CompilerServices
{
/// <summary>
/// Reserved to be used by the compiler for tracking metadata.
/// This class should not be used by developers in source code.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
internal static class IsExternalInit { }
// ReSharper disable UnusedType.Global
// ReSharper disable CheckNamespace
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.ComponentModel;
namespace System.Runtime.CompilerServices
{
/// <summary>
/// Reserved to be used by the compiler for tracking metadata.
/// This class should not be used by developers in source code.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
internal static class IsExternalInit { }
}

View File

@ -1,42 +1,42 @@
using System;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
namespace MapTo.Tests.Extensions
{
internal static class RoslynExtensions
{
internal static SyntaxTree? GetGeneratedSyntaxTree(this Compilation compilation, string className) =>
compilation.SyntaxTrees.SingleOrDefault(s => s.FilePath.EndsWith($"{className}.g.cs"));
internal static string PrintSyntaxTree(this Compilation compilation)
{
var builder = new StringBuilder();
return string.Join(
Environment.NewLine,
compilation.SyntaxTrees
.Reverse()
.Select((s, i) =>
{
builder
.Clear()
.AppendLine("----------------------------------------")
.AppendFormat("File Path: \"{0}\"", s.FilePath).AppendLine()
.AppendFormat("Index: \"{0}\"", i).AppendLine()
.AppendLine();
var lines = s.ToString().Split(Environment.NewLine);
var lineNumber = 0;
foreach (var line in lines)
{
builder.AppendFormat("{0:00}: {1}", lineNumber, line).AppendLine();
lineNumber++;
}
return builder.ToString();
}));
}
}
using System;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
namespace MapTo.Tests.Extensions
{
internal static class RoslynExtensions
{
internal static SyntaxTree? GetGeneratedSyntaxTree(this Compilation compilation, string className) =>
compilation.SyntaxTrees.FirstOrDefault(s => s.FilePath.EndsWith($"{className}.g.cs"));
internal static string PrintSyntaxTree(this Compilation compilation)
{
var builder = new StringBuilder();
return string.Join(
Environment.NewLine,
compilation.SyntaxTrees
.Reverse()
.Select((s, i) =>
{
builder
.Clear()
.AppendLine("----------------------------------------")
.AppendFormat("File Path: \"{0}\"", s.FilePath).AppendLine()
.AppendFormat("Index: \"{0}\"", i).AppendLine()
.AppendLine();
var lines = s.ToString().Split(Environment.NewLine);
var lineNumber = 0;
foreach (var line in lines)
{
builder.AppendFormat("{0:00}: {1}", lineNumber, line).AppendLine();
lineNumber++;
}
return builder.ToString();
}));
}
}
}

View File

@ -1,88 +1,88 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Shouldly;
using Xunit;
namespace MapTo.Tests.Extensions
{
internal static class ShouldlyExtensions
{
internal static void ShouldContainSource(this IEnumerable<SyntaxTree> syntaxTree, string typeName, string expectedSource, string? customMessage = null)
{
var syntax = syntaxTree
.Select(s => s.ToString().Trim())
.SingleOrDefault(s => s.Contains(typeName));
syntax.ShouldNotBeNullOrWhiteSpace();
syntax.ShouldBe(expectedSource, customMessage);
}
internal static void ShouldContainPartialSource(this IEnumerable<SyntaxTree> syntaxTree, string typeName, string expectedSource, string? customMessage = null)
{
var syntax = syntaxTree
.Select(s => s.ToString().Trim())
.SingleOrDefault(s => s.Contains(typeName));
syntax.ShouldNotBeNullOrWhiteSpace();
syntax.ShouldContainWithoutWhitespace(expectedSource, customMessage);
}
internal static void ShouldContainPartialSource(this SyntaxTree syntaxTree, string expectedSource, string? customMessage = null)
{
var syntax = syntaxTree.ToString();
syntax.ShouldNotBeNullOrWhiteSpace();
syntax.ShouldContainWithoutWhitespace(expectedSource, customMessage);
}
internal static void ShouldBeSuccessful(this IEnumerable<Diagnostic> diagnostics, Compilation? compilation = null, IEnumerable<string>? ignoreDiagnosticsIds = null)
{
var actual = diagnostics
.Where(d => (ignoreDiagnosticsIds is null || ignoreDiagnosticsIds.All(i => !d.Id.StartsWith(i) )) && (d.Severity is DiagnosticSeverity.Warning or DiagnosticSeverity.Error))
.Select(c => $"{c.Severity}: {c.Location.GetLineSpan()} - {c.GetMessage()}").ToArray();
if (!actual.Any())
{
return;
}
var builder = new StringBuilder();
builder.AppendLine("Failed");
foreach (var d in actual)
{
builder.AppendFormat("- {0}", d).AppendLine();
}
if (compilation is not null)
{
builder.AppendLine("Generated Sources:");
builder.AppendLine(compilation.PrintSyntaxTree());
}
Assert.False(true, builder.ToString());
}
internal static void ShouldNotBeSuccessful(this ImmutableArray<Diagnostic> diagnostics, Diagnostic expectedError)
{
var actualDiagnostics = diagnostics.SingleOrDefault(d => d.Id == expectedError.Id);
var compilationDiagnostics = actualDiagnostics == null ? diagnostics : diagnostics.Except(new[] { actualDiagnostics });
compilationDiagnostics.ShouldBeSuccessful();
Assert.NotNull(actualDiagnostics);
Assert.Equal(expectedError.Id, actualDiagnostics?.Id);
Assert.Equal(expectedError.Descriptor.Id, actualDiagnostics?.Descriptor.Id);
Assert.Equal(expectedError.Descriptor.Description, actualDiagnostics?.Descriptor.Description);
Assert.Equal(expectedError.Descriptor.Title, actualDiagnostics?.Descriptor.Title);
if (expectedError.Location != Location.None)
{
Assert.Equal(expectedError.Location, actualDiagnostics?.Location);
}
}
}
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Shouldly;
using Xunit;
namespace MapTo.Tests.Extensions
{
internal static class ShouldlyExtensions
{
internal static void ShouldContainSource(this IEnumerable<SyntaxTree> syntaxTree, string typeName, string expectedSource, string? customMessage = null)
{
var syntax = syntaxTree
.Select(s => s.ToString().Trim())
.FirstOrDefault(s => s.Contains(typeName));
syntax.ShouldNotBeNullOrWhiteSpace();
syntax.ShouldBe(expectedSource, customMessage);
}
internal static void ShouldContainPartialSource(this IEnumerable<SyntaxTree> syntaxTree, string typeName, string expectedSource, string? customMessage = null)
{
var syntax = syntaxTree
.Select(s => s.ToString().Trim())
.FirstOrDefault(s => s.Contains(typeName));
syntax.ShouldNotBeNullOrWhiteSpace();
syntax.ShouldContainWithoutWhitespace(expectedSource, customMessage);
}
internal static void ShouldContainPartialSource(this SyntaxTree syntaxTree, string expectedSource, string? customMessage = null)
{
var syntax = syntaxTree.ToString();
syntax.ShouldNotBeNullOrWhiteSpace();
syntax.ShouldContainWithoutWhitespace(expectedSource, customMessage);
}
internal static void ShouldBeSuccessful(this IEnumerable<Diagnostic> diagnostics, Compilation? compilation = null, IEnumerable<string>? ignoreDiagnosticsIds = null)
{
var actual = diagnostics
.Where(d => (ignoreDiagnosticsIds is null || ignoreDiagnosticsIds.All(i => !d.Id.StartsWith(i) )) && (d.Severity is DiagnosticSeverity.Warning or DiagnosticSeverity.Error))
.Select(c => $"{c.Severity}: {c.Location.GetLineSpan()} - {c.GetMessage()}").ToArray();
if (!actual.Any())
{
return;
}
var builder = new StringBuilder();
builder.AppendLine("Failed");
foreach (var d in actual)
{
builder.AppendFormat("- {0}", d).AppendLine();
}
if (compilation is not null)
{
builder.AppendLine("Generated Sources:");
builder.AppendLine(compilation.PrintSyntaxTree());
}
Assert.False(true, builder.ToString());
}
internal static void ShouldNotBeSuccessful(this ImmutableArray<Diagnostic> diagnostics, Diagnostic expectedError)
{
var actualDiagnostics = diagnostics.FirstOrDefault(d => d.Id == expectedError.Id);
var compilationDiagnostics = actualDiagnostics == null ? diagnostics : diagnostics.Except(new[] { actualDiagnostics });
compilationDiagnostics.ShouldBeSuccessful();
Assert.NotNull(actualDiagnostics);
Assert.Equal(expectedError.Id, actualDiagnostics?.Id);
Assert.Equal(expectedError.Descriptor.Id, actualDiagnostics?.Descriptor.Id);
Assert.Equal(expectedError.Descriptor.Description, actualDiagnostics?.Descriptor.Description);
Assert.Equal(expectedError.Descriptor.Title, actualDiagnostics?.Descriptor.Title);
if (expectedError.Location != Location.None)
{
Assert.Equal(expectedError.Location, actualDiagnostics?.Location);
}
}
}
}

View File

@ -1,79 +1,81 @@
using System.Linq;
using MapTo.Extensions;
using MapTo.Sources;
using MapTo.Tests.Extensions;
using MapTo.Tests.Infrastructure;
using Shouldly;
using Xunit;
using static MapTo.Tests.Common;
namespace MapTo.Tests
{
public class IgnorePropertyAttributeTests
{
[Fact]
public void VerifyIgnorePropertyAttribute()
{
// Arrange
const string source = "";
var expectedAttribute = $@"
{Constants.GeneratedFilesHeader}
using System;
namespace MapTo
{{
[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class IgnorePropertyAttribute : Attribute {{ }}
}}
".Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.ShouldContainSource(IgnorePropertyAttributeSource.AttributeName, expectedAttribute);
}
[Fact]
public void When_IgnorePropertyAttributeIsSpecified_Should_NotGenerateMappingsForThatProperty()
{
// Arrange
var source = GetSourceText(new SourceGeneratorOptions(
true,
PropertyBuilder: builder =>
{
builder
.WriteLine("[IgnoreProperty]")
.WriteLine("public int Prop4 { get; set; }");
},
SourcePropertyBuilder: builder => builder.WriteLine("public int Prop4 { get; set; }")));
var expectedResult = @"
partial class Foo
{
public Foo(Test.Models.Baz baz)
: this(new MappingContext(), baz) { }
private protected Foo(MappingContext context, Test.Models.Baz baz)
{
if (context == null) throw new ArgumentNullException(nameof(context));
if (baz == null) throw new ArgumentNullException(nameof(baz));
context.Register(baz, this);
Prop1 = baz.Prop1;
Prop2 = baz.Prop2;
Prop3 = baz.Prop3;
}
".Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult);
}
}
using System.Linq;
using MapTo.Extensions;
using MapTo.Sources;
using MapTo.Tests.Extensions;
using MapTo.Tests.Infrastructure;
using Shouldly;
using Xunit;
using static MapTo.Tests.Common;
namespace MapTo.Tests
{
public class IgnorePropertyAttributeTests
{
/*
[Fact]
public void VerifyIgnorePropertyAttribute()
{
// Arrange
const string source = "";
var expectedAttribute = $@"
{Constants.GeneratedFilesHeader}
using System;
namespace MapTo
{{
[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class IgnorePropertyAttribute : Attribute {{ }}
}}
".Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.ShouldContainSource(IgnorePropertyAttributeSource.AttributeName, expectedAttribute);
}
*/
[Fact]
public void When_IgnorePropertyAttributeIsSpecified_Should_NotGenerateMappingsForThatProperty()
{
// Arrange
var source = GetSourceText(new SourceGeneratorOptions(
true,
PropertyBuilder: builder =>
{
builder
.WriteLine("[IgnoreProperty]")
.WriteLine("public int Prop4 { get; set; }");
},
SourcePropertyBuilder: builder => builder.WriteLine("public int Prop4 { get; set; }")));
var expectedResult = @"
partial class Foo
{
public Foo(Test.Models.Baz baz)
: this(new MappingContext(), baz) { }
private protected Foo(MappingContext context, Test.Models.Baz baz)
{
if (context == null) throw new ArgumentNullException(nameof(context));
if (baz == null) throw new ArgumentNullException(nameof(baz));
context.Register(baz, this);
Prop1 = baz.Prop1;
Prop2 = baz.Prop2;
Prop3 = baz.Prop3;
}
".Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult);
}
}
}

View File

@ -1,64 +1,66 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using MapTo.Tests.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
namespace MapTo.Tests.Infrastructure
{
internal static class CSharpGenerator
{
internal static (Compilation compilation, ImmutableArray<Diagnostic> diagnostics) GetOutputCompilation(
string source,
bool assertCompilation = false,
IDictionary<string, string>? analyzerConfigOptions = null,
NullableContextOptions nullableContextOptions = NullableContextOptions.Disable,
LanguageVersion languageVersion = LanguageVersion.CSharp7_3) =>
GetOutputCompilation(
new[] { source },
assertCompilation,
analyzerConfigOptions,
nullableContextOptions,
languageVersion);
internal static (Compilation compilation, ImmutableArray<Diagnostic> diagnostics) GetOutputCompilation(
IEnumerable<string> sources,
bool assertCompilation = false,
IDictionary<string, string>? analyzerConfigOptions = null,
NullableContextOptions nullableContextOptions = NullableContextOptions.Disable,
LanguageVersion languageVersion = LanguageVersion.CSharp7_3)
{
var references = AppDomain.CurrentDomain.GetAssemblies()
.Where(a => !a.IsDynamic && !string.IsNullOrWhiteSpace(a.Location))
.Select(a => MetadataReference.CreateFromFile(a.Location))
.ToList();
var compilation = CSharpCompilation.Create(
$"{typeof(CSharpGenerator).Assembly.GetName().Name}.Dynamic",
sources.Select((source, index) => CSharpSyntaxTree.ParseText(source, path: $"Test{index:00}.g.cs", options: new CSharpParseOptions(languageVersion))),
references,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: nullableContextOptions));
if (assertCompilation)
{
// NB: fail tests when the injected program isn't valid _before_ running generators
compilation.GetDiagnostics().ShouldBeSuccessful();
}
var driver = CSharpGeneratorDriver.Create(
new[] { new MapToGenerator() },
optionsProvider: new TestAnalyzerConfigOptionsProvider(analyzerConfigOptions),
parseOptions: new CSharpParseOptions(languageVersion)
);
driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var generateDiagnostics);
generateDiagnostics.ShouldBeSuccessful(ignoreDiagnosticsIds: new[] { "MT" });
outputCompilation.GetDiagnostics().ShouldBeSuccessful(outputCompilation);
return (outputCompilation, generateDiagnostics);
}
}
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using MapTo.Tests.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
namespace MapTo.Tests.Infrastructure
{
internal static class CSharpGenerator
{
internal static (Compilation compilation, ImmutableArray<Diagnostic> diagnostics) GetOutputCompilation(
string source,
bool assertCompilation = false,
IDictionary<string, string>? analyzerConfigOptions = null,
NullableContextOptions nullableContextOptions = NullableContextOptions.Disable,
LanguageVersion languageVersion = LanguageVersion.CSharp7_3) =>
GetOutputCompilation(
new[] { source },
assertCompilation,
analyzerConfigOptions,
nullableContextOptions,
languageVersion);
internal static (Compilation compilation, ImmutableArray<Diagnostic> diagnostics) GetOutputCompilation(
IEnumerable<string> sources,
bool assertCompilation = false,
IDictionary<string, string>? analyzerConfigOptions = null,
NullableContextOptions nullableContextOptions = NullableContextOptions.Disable,
LanguageVersion languageVersion = LanguageVersion.CSharp7_3)
{
var references = AppDomain.CurrentDomain.GetAssemblies()
.Where(a => !a.IsDynamic && !string.IsNullOrWhiteSpace(a.Location))
.Select(a => MetadataReference.CreateFromFile(a.Location))
.ToList();
var compilation = CSharpCompilation.Create(
$"{typeof(CSharpGenerator).Assembly.GetName().Name}.Dynamic",
sources.Select((source, index) => CSharpSyntaxTree.ParseText(source, path: $"Test{index:00}.g.cs", options: new CSharpParseOptions(languageVersion))),
references,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: nullableContextOptions));
if (assertCompilation)
{
// NB: fail tests when the injected program isn't valid _before_ running generators
compilation.GetDiagnostics().ShouldBeSuccessful();
}
var driver = CSharpGeneratorDriver.Create(
new List<ISourceGenerator>() { new MapToGenerator()},
optionsProvider: new TestAnalyzerConfigOptionsProvider(analyzerConfigOptions),
parseOptions: new CSharpParseOptions(languageVersion)
);
driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var generateDiagnostics);
generateDiagnostics.ShouldBeSuccessful(ignoreDiagnosticsIds: new[] { "MT" });
outputCompilation.GetDiagnostics().ShouldBeSuccessful(outputCompilation);
return (outputCompilation, generateDiagnostics);
}
}
}

View File

@ -1,19 +1,19 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
namespace MapTo.Tests.Infrastructure
{
internal sealed class TestAnalyzerConfigOptions : AnalyzerConfigOptions
{
private readonly ImmutableDictionary<string, string> _backing;
public TestAnalyzerConfigOptions(IDictionary<string, string>? properties)
{
_backing = properties?.ToImmutableDictionary(KeyComparer) ?? ImmutableDictionary.Create<string, string>(KeyComparer);
}
public override bool TryGetValue(string key, out string? value) => _backing.TryGetValue(key, out value);
}
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
namespace MapTo.Tests.Infrastructure
{
internal sealed class TestAnalyzerConfigOptions : AnalyzerConfigOptions
{
private readonly ImmutableDictionary<string, string> _backing;
public TestAnalyzerConfigOptions(IDictionary<string, string>? properties)
{
_backing = properties?.ToImmutableDictionary(KeyComparer) ?? ImmutableDictionary.Create<string, string>(KeyComparer);
}
public override bool TryGetValue(string key, out string? value) => _backing.TryGetValue(key, out value);
}
}

View File

@ -1,24 +1,24 @@
using System;
using System.Collections.Generic;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
namespace MapTo.Tests.Infrastructure
{
internal sealed class TestAnalyzerConfigOptionsProvider : AnalyzerConfigOptionsProvider
{
public TestAnalyzerConfigOptionsProvider(IDictionary<string, string>? options)
{
GlobalOptions = new TestAnalyzerConfigOptions(options);
}
/// <inheritdoc />
public override AnalyzerConfigOptions GlobalOptions { get; }
/// <inheritdoc />
public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) => throw new NotImplementedException();
/// <inheritdoc />
public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) => throw new NotImplementedException();
}
using System;
using System.Collections.Generic;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
namespace MapTo.Tests.Infrastructure
{
internal sealed class TestAnalyzerConfigOptionsProvider : AnalyzerConfigOptionsProvider
{
public TestAnalyzerConfigOptionsProvider(IDictionary<string, string>? options)
{
GlobalOptions = new TestAnalyzerConfigOptions(options);
}
/// <inheritdoc />
public override AnalyzerConfigOptions GlobalOptions { get; }
/// <inheritdoc />
public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) => throw new NotImplementedException();
/// <inheritdoc />
public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) => throw new NotImplementedException();
}
}

View File

@ -1,202 +1,202 @@
using System;
using System.Collections.Generic;
using System.Linq;
using MapTo.Sources;
using MapTo.Tests.Extensions;
using MapTo.Tests.Infrastructure;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Xunit;
using static MapTo.Tests.Common;
namespace MapTo.Tests
{
public class MapPropertyTests
{
[Theory]
[InlineData(NullableContextOptions.Disable)]
[InlineData(NullableContextOptions.Enable)]
public void VerifyMapPropertyAttribute(NullableContextOptions nullableContextOptions)
{
// Arrange
const string source = "";
var nullableSyntax = nullableContextOptions == NullableContextOptions.Enable ? "?" : string.Empty;
var languageVersion = nullableContextOptions == NullableContextOptions.Enable ? LanguageVersion.CSharp8 : LanguageVersion.CSharp7_3;
var expectedInterface = $@"
{Constants.GeneratedFilesHeader}
{(nullableContextOptions == NullableContextOptions.Enable ? $"#nullable enable{Environment.NewLine}" : string.Empty)}
using System;
namespace MapTo
{{
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = true)]
public sealed class MapPropertyAttribute : Attribute
{{
public string{nullableSyntax} SourcePropertyName {{ get; set; }}
}}
}}
".Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, nullableContextOptions: nullableContextOptions, languageVersion: languageVersion);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.ShouldContainSource(MapPropertyAttributeSource.AttributeName, expectedInterface);
}
[Fact]
public void When_MapPropertyFound_Should_UseItToMapToSourceProperty()
{
// Arrange
var source = GetSourceText(new SourceGeneratorOptions(
true,
PropertyBuilder: builder =>
{
builder
.WriteLine("[MapProperty(SourcePropertyName = nameof(Baz.Prop3))]")
.WriteLine("public int Prop4 { get; set; }");
},
SourcePropertyBuilder: builder => builder.WriteLine("public int Prop4 { get; set; }")));
var expectedResult = @"
partial class Foo
{
public Foo(Test.Models.Baz baz)
: this(new MappingContext(), baz) { }
private protected Foo(MappingContext context, Test.Models.Baz baz)
{
if (context == null) throw new ArgumentNullException(nameof(context));
if (baz == null) throw new ArgumentNullException(nameof(baz));
context.Register(baz, this);
Prop1 = baz.Prop1;
Prop2 = baz.Prop2;
Prop3 = baz.Prop3;
Prop4 = baz.Prop3;
}
".Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult);
}
[Theory]
[MemberData(nameof(MapPropertyWithImplicitConversionFoundData))]
public void When_MapPropertyWithImplicitConversionFound_Should_UseItToMapToSourceProperty(string source, string expectedResult, LanguageVersion languageVersion)
{
// Arrange
source = source.Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, languageVersion: languageVersion);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult);
}
public static IEnumerable<object[]> MapPropertyWithImplicitConversionFoundData => new List<object[]>
{
new object[]
{
@"
namespace Test
{
using System.Collections.Generic;
public class InnerClass { public int Prop1 { get; set; } }
public class OuterClass
{
public int Id { get; set; }
public List<InnerClass> InnerProp { get; set; }
}
}
namespace Test.Models
{
using MapTo;
using System.Collections.Generic;
[MapFrom(typeof(Test.InnerClass))]
public partial class InnerClass { public int Prop1 { get; set; } }
[MapFrom(typeof(Test.OuterClass))]
public partial class OuterClass
{
public int Id { get; set; }
public IReadOnlyList<InnerClass> InnerProp { get; set; }
}
}
",
@"
private protected OuterClass(MappingContext context, Test.OuterClass outerClass)
{
if (context == null) throw new ArgumentNullException(nameof(context));
if (outerClass == null) throw new ArgumentNullException(nameof(outerClass));
context.Register(outerClass, this);
Id = outerClass.Id;
InnerProp = outerClass.InnerProp.Select(context.MapFromWithContext<Test.InnerClass, InnerClass>).ToList();
}
",
LanguageVersion.CSharp7_3
},
new object[]
{
@"
namespace Test
{
using System;
using System.Collections.Generic;
public class InnerClass
{
public int Id { get; set; }
public string Name { get; set; }
}
public class OuterClass
{
public int Id { get; set; }
public List<InnerClass> InnerClasses { get; set; }
public DateTime? SomeDate { get; set; }
}
}
namespace Test.Models
{
using MapTo;
using System;
using System.Collections.Generic;
[MapFrom(typeof(Test.InnerClass))]
public partial record InnerClass(int Id, string Name);
[MapFrom(typeof(Test.OuterClass))]
public partial record OuterClass(int Id, DateTime? SomeDate, IReadOnlyList<InnerClass> InnerClasses);
}
",
@"
private protected OuterClass(MappingContext context, Test.OuterClass outerClass)
: this(Id: outerClass.Id, SomeDate: outerClass.SomeDate, InnerClasses: outerClass.InnerClasses.Select(context.MapFromWithContext<Test.InnerClass, InnerClass>).ToList())
{
if (context == null) throw new ArgumentNullException(nameof(context));
if (outerClass == null) throw new ArgumentNullException(nameof(outerClass));
context.Register(outerClass, this);
}
",
LanguageVersion.CSharp9
}
};
}
using System;
using System.Collections.Generic;
using System.Linq;
using MapTo.Sources;
using MapTo.Tests.Extensions;
using MapTo.Tests.Infrastructure;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Xunit;
using static MapTo.Tests.Common;
namespace MapTo.Tests
{
public class MapPropertyTests
{
[Theory]
[InlineData(NullableContextOptions.Disable)]
[InlineData(NullableContextOptions.Enable)]
public void VerifyMapPropertyAttribute(NullableContextOptions nullableContextOptions)
{
// Arrange
const string source = "";
var nullableSyntax = nullableContextOptions == NullableContextOptions.Enable ? "?" : string.Empty;
var languageVersion = nullableContextOptions == NullableContextOptions.Enable ? LanguageVersion.CSharp8 : LanguageVersion.CSharp7_3;
var expectedInterface = $@"
{Constants.GeneratedFilesHeader}
{(nullableContextOptions == NullableContextOptions.Enable ? $"#nullable enable{Environment.NewLine}" : string.Empty)}
using System;
namespace MapTo
{{
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = true)]
public sealed class MapPropertyAttribute : Attribute
{{
public string{nullableSyntax} SourcePropertyName {{ get; set; }}
}}
}}
".Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, nullableContextOptions: nullableContextOptions, languageVersion: languageVersion);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.ShouldContainSource(MapPropertyAttributeSource.AttributeName, expectedInterface);
}
[Fact]
public void When_MapPropertyFound_Should_UseItToMapToSourceProperty()
{
// Arrange
var source = GetSourceText(new SourceGeneratorOptions(
true,
PropertyBuilder: builder =>
{
builder
.WriteLine("[MapProperty(SourcePropertyName = nameof(Baz.Prop3))]")
.WriteLine("public int Prop4 { get; set; }");
},
SourcePropertyBuilder: builder => builder.WriteLine("public int Prop4 { get; set; }")));
var expectedResult = @"
partial class Foo
{
public Foo(Test.Models.Baz baz)
: this(new MappingContext(), baz) { }
private protected Foo(MappingContext context, Test.Models.Baz baz)
{
if (context == null) throw new ArgumentNullException(nameof(context));
if (baz == null) throw new ArgumentNullException(nameof(baz));
context.Register(baz, this);
Prop1 = baz.Prop1;
Prop2 = baz.Prop2;
Prop3 = baz.Prop3;
Prop4 = baz.Prop3;
}
".Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult);
}
[Theory]
[MemberData(nameof(MapPropertyWithImplicitConversionFoundData))]
public void When_MapPropertyWithImplicitConversionFound_Should_UseItToMapToSourceProperty(string source, string expectedResult, LanguageVersion languageVersion)
{
// Arrange
source = source.Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, languageVersion: languageVersion);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult);
}
public static IEnumerable<object[]> MapPropertyWithImplicitConversionFoundData => new List<object[]>
{
new object[]
{
@"
namespace Test
{
using System.Collections.Generic;
public class InnerClass { public int Prop1 { get; set; } }
public class OuterClass
{
public int Id { get; set; }
public List<InnerClass> InnerProp { get; set; }
}
}
namespace Test.Models
{
using MapTo;
using System.Collections.Generic;
[MapFrom(typeof(Test.InnerClass))]
public partial class InnerClass { public int Prop1 { get; set; } }
[MapFrom(typeof(Test.OuterClass))]
public partial class OuterClass
{
public int Id { get; set; }
public IReadOnlyList<InnerClass> InnerProp { get; set; }
}
}
",
@"
private protected OuterClass(MappingContext context, Test.OuterClass outerClass)
{
if (context == null) throw new ArgumentNullException(nameof(context));
if (outerClass == null) throw new ArgumentNullException(nameof(outerClass));
context.Register(outerClass, this);
Id = outerClass.Id;
InnerProp = outerClass.InnerProp.Select(context.MapFromWithContext<Test.InnerClass, InnerClass>).ToList();
}
",
LanguageVersion.CSharp7_3
},
new object[]
{
@"
namespace Test
{
using System;
using System.Collections.Generic;
public class InnerClass
{
public int Id { get; set; }
public string Name { get; set; }
}
public class OuterClass
{
public int Id { get; set; }
public List<InnerClass> InnerClasses { get; set; }
public DateTime? SomeDate { get; set; }
}
}
namespace Test.Models
{
using MapTo;
using System;
using System.Collections.Generic;
[MapFrom(typeof(Test.InnerClass))]
public partial record InnerClass(int Id, string Name);
[MapFrom(typeof(Test.OuterClass))]
public partial record OuterClass(int Id, DateTime? SomeDate, IReadOnlyList<InnerClass> InnerClasses);
}
",
@"
private protected OuterClass(MappingContext context, Test.OuterClass outerClass)
: this(Id: outerClass.Id, SomeDate: outerClass.SomeDate, InnerClasses: outerClass.InnerClasses.Select(context.MapFromWithContext<Test.InnerClass, InnerClass>).ToList())
{
if (context == null) throw new ArgumentNullException(nameof(context));
if (outerClass == null) throw new ArgumentNullException(nameof(outerClass));
context.Register(outerClass, this);
}
",
LanguageVersion.CSharp9
}
};
}
}

View File

@ -1,283 +1,283 @@
using System.Linq;
using MapTo.Sources;
using MapTo.Tests.Extensions;
using MapTo.Tests.Infrastructure;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Xunit;
using static MapTo.Tests.Common;
namespace MapTo.Tests
{
public class MapTypeConverterTests
{
[Fact]
public void VerifyMapTypeConverterAttribute()
{
// Arrange
const string source = "";
var expectedInterface = $@"
{Constants.GeneratedFilesHeader}
using System;
namespace MapTo
{{
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false)]
public sealed class MapTypeConverterAttribute : Attribute
{{
public MapTypeConverterAttribute(Type converter, object[] converterParameters = null)
{{
Converter = converter;
ConverterParameters = converterParameters;
}}
public Type Converter {{ get; }}
public object[] ConverterParameters {{ get; }}
}}
}}
".Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.ShouldContainSource(MapTypeConverterAttributeSource.AttributeName, expectedInterface);
}
[Fact]
public void VerifyMapTypeConverterAttributeWithNullableOptionOn()
{
// Arrange
const string source = "";
var expectedInterface = $@"
{Constants.GeneratedFilesHeader}
#nullable enable
using System;
namespace MapTo
{{
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false)]
public sealed class MapTypeConverterAttribute : Attribute
{{
public MapTypeConverterAttribute(Type converter, object[]? converterParameters = null)
{{
Converter = converter;
ConverterParameters = converterParameters;
}}
public Type Converter {{ get; }}
public object[]? ConverterParameters {{ get; }}
}}
}}
".Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, nullableContextOptions: NullableContextOptions.Enable, languageVersion: LanguageVersion.CSharp8);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.ShouldContainSource(MapTypeConverterAttributeSource.AttributeName, expectedInterface);
}
[Fact]
public void VerifyTypeConverterInterface()
{
// Arrange
const string source = "";
var expectedInterface = $@"
{Constants.GeneratedFilesHeader}
namespace MapTo
{{
public interface ITypeConverter<in TSource, out TDestination>
{{
TDestination Convert(TSource source, object[] converterParameters);
}}
}}
".Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.ShouldContainSource(ITypeConverterSource.InterfaceName, expectedInterface);
}
[Fact]
public void VerifyTypeConverterInterfaceWithNullableOptionOn()
{
// Arrange
const string source = "";
var expectedInterface = $@"
{Constants.GeneratedFilesHeader}
#nullable enable
namespace MapTo
{{
public interface ITypeConverter<in TSource, out TDestination>
{{
TDestination Convert(TSource source, object[]? converterParameters);
}}
}}
".Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, nullableContextOptions: NullableContextOptions.Enable, languageVersion: LanguageVersion.CSharp8);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.ShouldContainSource(ITypeConverterSource.InterfaceName, expectedInterface);
}
[Fact]
public void When_FoundMatchingPropertyNameWithConverterType_ShouldUseTheConverterAndItsParametersToAssignProperties()
{
// Arrange
var source = GetSourceText(new SourceGeneratorOptions(
true,
PropertyBuilder: builder =>
{
builder
.WriteLine("[MapTypeConverter(typeof(Prop4Converter), new object[]{\"G\", 'C', 10})]")
.WriteLine("public string Prop4 { get; set; }");
},
SourcePropertyBuilder: builder => builder.WriteLine("public long Prop4 { get; set; }")));
source += @"
namespace Test
{
using MapTo;
public class Prop4Converter: ITypeConverter<long, string>
{
public string Convert(long source, object[] converterParameters) => source.ToString(converterParameters[0] as string);
}
}
";
const string expectedSyntax = "Prop4 = new Test.Prop4Converter().Convert(baz.Prop4, new object[] { \"G\", 'C', 10 });";
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedSyntax);
}
[Fact]
public void When_FoundMatchingPropertyNameWithConverterType_ShouldUseTheConverterToAssignProperties()
{
// Arrange
var source = GetSourceText(new SourceGeneratorOptions(
true,
PropertyBuilder: builder =>
{
builder
.WriteLine("[MapTypeConverter(typeof(Prop4Converter))]")
.WriteLine("public long Prop4 { get; set; }");
},
SourcePropertyBuilder: builder => builder.WriteLine("public string Prop4 { get; set; }")));
source += @"
namespace Test
{
using MapTo;
public class Prop4Converter: ITypeConverter<string, long>
{
public long Convert(string source, object[] converterParameters) => long.Parse(source);
}
}
";
const string expectedSyntax = "Prop4 = new Test.Prop4Converter().Convert(baz.Prop4, null);";
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedSyntax);
}
[Fact]
public void When_FoundMatchingPropertyNameWithDifferentImplicitlyConvertibleType_Should_GenerateTheProperty()
{
// Arrange
var source = GetSourceText(new SourceGeneratorOptions(
true,
PropertyBuilder: builder => { builder.WriteLine("public long Prop4 { get; set; }"); },
SourcePropertyBuilder: builder => builder.WriteLine("public int Prop4 { get; set; }")));
var expectedResult = @"
partial class Foo
{
public Foo(Test.Models.Baz baz)
: this(new MappingContext(), baz) { }
private protected Foo(MappingContext context, Test.Models.Baz baz)
{
if (context == null) throw new ArgumentNullException(nameof(context));
if (baz == null) throw new ArgumentNullException(nameof(baz));
context.Register(baz, this);
Prop1 = baz.Prop1;
Prop2 = baz.Prop2;
Prop3 = baz.Prop3;
Prop4 = baz.Prop4;
}
".Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult);
}
[Fact]
public void When_FoundMatchingPropertyNameWithIncorrectConverterType_ShouldReportError()
{
// Arrange
var source = GetSourceText(new SourceGeneratorOptions(
true,
PropertyBuilder: builder =>
{
builder
.WriteLine("[IgnoreProperty]")
.WriteLine("public long IgnoreMe { get; set; }")
.WriteLine("[MapTypeConverter(typeof(Prop4Converter))]")
.WriteLine("public long Prop4 { get; set; }");
},
SourcePropertyBuilder: builder => builder.WriteLine("public string Prop4 { get; set; }")));
source += @"
namespace Test
{
using MapTo;
public class Prop4Converter: ITypeConverter<string, int>
{
public int Convert(string source, object[] converterParameters) => int.Parse(source);
}
}
";
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
var expectedError = DiagnosticsFactory.InvalidTypeConverterGenericTypesError(GetSourcePropertySymbol("Prop4", compilation), GetSourcePropertySymbol("Prop4", compilation, "Baz"));
diagnostics.ShouldNotBeSuccessful(expectedError);
}
}
using System.Linq;
using MapTo.Sources;
using MapTo.Tests.Extensions;
using MapTo.Tests.Infrastructure;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Xunit;
using static MapTo.Tests.Common;
namespace MapTo.Tests
{
public class MapTypeConverterTests
{
[Fact]
public void VerifyMapTypeConverterAttribute()
{
// Arrange
const string source = "";
var expectedInterface = $@"
{Constants.GeneratedFilesHeader}
using System;
namespace MapTo
{{
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false)]
public sealed class MapTypeConverterAttribute : Attribute
{{
public MapTypeConverterAttribute(Type converter, object[] converterParameters = null)
{{
Converter = converter;
ConverterParameters = converterParameters;
}}
public Type Converter {{ get; }}
public object[] ConverterParameters {{ get; }}
}}
}}
".Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.ShouldContainSource(MapTypeConverterAttributeSource.AttributeName, expectedInterface);
}
[Fact]
public void VerifyMapTypeConverterAttributeWithNullableOptionOn()
{
// Arrange
const string source = "";
var expectedInterface = $@"
{Constants.GeneratedFilesHeader}
#nullable enable
using System;
namespace MapTo
{{
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false)]
public sealed class MapTypeConverterAttribute : Attribute
{{
public MapTypeConverterAttribute(Type converter, object[]? converterParameters = null)
{{
Converter = converter;
ConverterParameters = converterParameters;
}}
public Type Converter {{ get; }}
public object[]? ConverterParameters {{ get; }}
}}
}}
".Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, nullableContextOptions: NullableContextOptions.Enable, languageVersion: LanguageVersion.CSharp8);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.ShouldContainSource(MapTypeConverterAttributeSource.AttributeName, expectedInterface);
}
[Fact]
public void VerifyTypeConverterInterface()
{
// Arrange
const string source = "";
var expectedInterface = $@"
{Constants.GeneratedFilesHeader}
namespace MapTo
{{
public interface ITypeConverter<in TSource, out TDestination>
{{
TDestination Convert(TSource source, object[] converterParameters);
}}
}}
".Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.ShouldContainSource(ITypeConverterSource.InterfaceName, expectedInterface);
}
[Fact]
public void VerifyTypeConverterInterfaceWithNullableOptionOn()
{
// Arrange
const string source = "";
var expectedInterface = $@"
{Constants.GeneratedFilesHeader}
#nullable enable
namespace MapTo
{{
public interface ITypeConverter<in TSource, out TDestination>
{{
TDestination Convert(TSource source, object[]? converterParameters);
}}
}}
".Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, nullableContextOptions: NullableContextOptions.Enable, languageVersion: LanguageVersion.CSharp8);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.ShouldContainSource(ITypeConverterSource.InterfaceName, expectedInterface);
}
[Fact]
public void When_FoundMatchingPropertyNameWithConverterType_ShouldUseTheConverterAndItsParametersToAssignProperties()
{
// Arrange
var source = GetSourceText(new SourceGeneratorOptions(
true,
PropertyBuilder: builder =>
{
builder
.WriteLine("[MapTypeConverter(typeof(Prop4Converter), new object[]{\"G\", 'C', 10})]")
.WriteLine("public string Prop4 { get; set; }");
},
SourcePropertyBuilder: builder => builder.WriteLine("public long Prop4 { get; set; }")));
source += @"
namespace Test
{
using MapTo;
public class Prop4Converter: ITypeConverter<long, string>
{
public string Convert(long source, object[] converterParameters) => source.ToString(converterParameters[0] as string);
}
}
";
const string expectedSyntax = "Prop4 = new Test.Prop4Converter().Convert(baz.Prop4, new object[] { \"G\", 'C', 10 });";
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedSyntax);
}
[Fact]
public void When_FoundMatchingPropertyNameWithConverterType_ShouldUseTheConverterToAssignProperties()
{
// Arrange
var source = GetSourceText(new SourceGeneratorOptions(
true,
PropertyBuilder: builder =>
{
builder
.WriteLine("[MapTypeConverter(typeof(Prop4Converter))]")
.WriteLine("public long Prop4 { get; set; }");
},
SourcePropertyBuilder: builder => builder.WriteLine("public string Prop4 { get; set; }")));
source += @"
namespace Test
{
using MapTo;
public class Prop4Converter: ITypeConverter<string, long>
{
public long Convert(string source, object[] converterParameters) => long.Parse(source);
}
}
";
const string expectedSyntax = "Prop4 = new Test.Prop4Converter().Convert(baz.Prop4, null);";
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedSyntax);
}
[Fact]
public void When_FoundMatchingPropertyNameWithDifferentImplicitlyConvertibleType_Should_GenerateTheProperty()
{
// Arrange
var source = GetSourceText(new SourceGeneratorOptions(
true,
PropertyBuilder: builder => { builder.WriteLine("public long Prop4 { get; set; }"); },
SourcePropertyBuilder: builder => builder.WriteLine("public int Prop4 { get; set; }")));
var expectedResult = @"
partial class Foo
{
public Foo(Test.Models.Baz baz)
: this(new MappingContext(), baz) { }
private protected Foo(MappingContext context, Test.Models.Baz baz)
{
if (context == null) throw new ArgumentNullException(nameof(context));
if (baz == null) throw new ArgumentNullException(nameof(baz));
context.Register(baz, this);
Prop1 = baz.Prop1;
Prop2 = baz.Prop2;
Prop3 = baz.Prop3;
Prop4 = baz.Prop4;
}
".Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult);
}
[Fact]
public void When_FoundMatchingPropertyNameWithIncorrectConverterType_ShouldReportError()
{
// Arrange
var source = GetSourceText(new SourceGeneratorOptions(
true,
PropertyBuilder: builder =>
{
builder
.WriteLine("[IgnoreProperty]")
.WriteLine("public long IgnoreMe { get; set; }")
.WriteLine("[MapTypeConverter(typeof(Prop4Converter))]")
.WriteLine("public long Prop4 { get; set; }");
},
SourcePropertyBuilder: builder => builder.WriteLine("public string Prop4 { get; set; }")));
source += @"
namespace Test
{
using MapTo;
public class Prop4Converter: ITypeConverter<string, int>
{
public int Convert(string source, object[] converterParameters) => int.Parse(source);
}
}
";
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
var expectedError = DiagnosticsFactory.InvalidTypeConverterGenericTypesError(GetSourcePropertySymbol("Prop4", compilation), GetSourcePropertySymbol("Prop4", compilation, "Baz"));
diagnostics.ShouldNotBeSuccessful(expectedError);
}
}
}

View File

@ -1,102 +1,102 @@
using MapTo.Sources;
using MapTo.Tests.Extensions;
using MapTo.Tests.Infrastructure;
using Xunit;
using static MapTo.Tests.Common;
namespace MapTo.Tests
{
public class MappingContextTests
{
[Fact]
public void VerifyMappingContextSource()
{
// Arrange
const string source = "";
var expected = @"
// <auto-generated />
using System;
using System.Collections.Generic;
using System.Reflection;
namespace MapTo
{
internal sealed class MappingContext
{
private readonly Dictionary<object, object> _cache;
internal MappingContext()
{
_cache = new Dictionary<object, object>(1);
}
internal static TMapped Create<TOriginal, TMapped>(TOriginal original)
{
if (original == null) throw new ArgumentNullException(nameof(original));
var context = new MappingContext();
var mapped = context.MapFromWithContext<TOriginal, TMapped>(original);
if (mapped == null)
{
throw new InvalidOperationException();
}
return mapped;
}
internal TMapped MapFromWithContext<TOriginal, TMapped>(TOriginal original)
{
if (original == null)
{
return default(TMapped);
}
if (!TryGetValue<TOriginal, TMapped>(original, out var mapped))
{
var instance = Activator.CreateInstance(typeof(TMapped), BindingFlags.Instance | BindingFlags.NonPublic, null, new object[] { this, original }, null);
if (instance != null)
{
mapped = (TMapped)instance;
}
}
return mapped;
}
internal void Register<TOriginal, TMapped>(TOriginal original, TMapped mapped)
{
if (original == null) throw new ArgumentNullException(nameof(original));
if (mapped == null) throw new ArgumentNullException(nameof(mapped));
if (!_cache.ContainsKey(original))
{
_cache.Add(original, mapped);
}
}
private bool TryGetValue<TOriginal, TMapped>(TOriginal original, out TMapped mapped)
{
if (original != null && _cache.TryGetValue(original, out var value))
{
mapped = (TMapped)value;
return true;
}
mapped = default(TMapped);
return false;
}
}
}
".Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.ShouldContainSource(MappingContextSource.ClassName, expected);
}
}
using MapTo.Sources;
using MapTo.Tests.Extensions;
using MapTo.Tests.Infrastructure;
using Xunit;
using static MapTo.Tests.Common;
namespace MapTo.Tests
{
public class MappingContextTests
{
[Fact]
public void VerifyMappingContextSource()
{
// Arrange
const string source = "";
var expected = @"
// <auto-generated />
using System;
using System.Collections.Generic;
using System.Reflection;
namespace MapTo
{
internal sealed class MappingContext
{
private readonly Dictionary<object, object> _cache;
internal MappingContext()
{
_cache = new Dictionary<object, object>(1);
}
internal static TMapped Create<TOriginal, TMapped>(TOriginal original)
{
if (original == null) throw new ArgumentNullException(nameof(original));
var context = new MappingContext();
var mapped = context.MapFromWithContext<TOriginal, TMapped>(original);
if (mapped == null)
{
throw new InvalidOperationException();
}
return mapped;
}
internal TMapped MapFromWithContext<TOriginal, TMapped>(TOriginal original)
{
if (original == null)
{
return default(TMapped);
}
if (!TryGetValue<TOriginal, TMapped>(original, out var mapped))
{
var instance = Activator.CreateInstance(typeof(TMapped), BindingFlags.Instance | BindingFlags.NonPublic, null, new object[] { this, original }, null);
if (instance != null)
{
mapped = (TMapped)instance;
}
}
return mapped;
}
internal void Register<TOriginal, TMapped>(TOriginal original, TMapped mapped)
{
if (original == null) throw new ArgumentNullException(nameof(original));
if (mapped == null) throw new ArgumentNullException(nameof(mapped));
if (!_cache.ContainsKey(original))
{
_cache.Add(original, mapped);
}
}
private bool TryGetValue<TOriginal, TMapped>(TOriginal original, out TMapped mapped)
{
if (original != null && _cache.TryGetValue(original, out var value))
{
mapped = (TMapped)value;
return true;
}
mapped = default(TMapped);
return false;
}
}
}
".Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.ShouldContainSource(MappingContextSource.ClassName, expected);
}
}
}

View File

@ -1,85 +1,85 @@
using System.Linq;
using MapTo.Integration.Tests.Data.Models;
using MapTo.Integration.Tests.Data.ViewModels;
using Shouldly;
using Xunit;
namespace MapTo.Integration.Tests
{
public class CyclicReferenceTests
{
[Fact]
public void VerifySelfReference()
{
// Arrange
var manager = new Manager { Id = 1, EmployeeCode = "M001", Level = 100 };
manager.Manager = manager;
// Act
var result = manager.ToManagerViewModel();
// Assert
result.Id.ShouldBe(manager.Id);
result.EmployeeCode.ShouldBe(manager.EmployeeCode);
result.Level.ShouldBe(manager.Level);
result.Manager.ShouldBeSameAs(result);
}
[Fact]
public void VerifyNestedReference()
{
// Arrange
var manager1 = new Manager { Id = 100, EmployeeCode = "M001", Level = 100 };
var manager2 = new Manager { Id = 102, EmployeeCode = "M002", Level = 100 };
var employee1 = new Employee { Id = 200, EmployeeCode = "E001"};
var employee2 = new Employee { Id = 201, EmployeeCode = "E002"};
employee1.Manager = manager1;
employee2.Manager = manager2;
manager2.Manager = manager1;
// Act
var manager1ViewModel = manager1.ToManagerViewModel();
// Assert
manager1ViewModel.Id.ShouldBe(manager1.Id);
manager1ViewModel.Manager.ShouldBeNull();
manager1ViewModel.Employees.Count.ShouldBe(2);
manager1ViewModel.Employees[0].Id.ShouldBe(employee1.Id);
manager1ViewModel.Employees[0].Manager.ShouldBeSameAs(manager1ViewModel);
manager1ViewModel.Employees[1].Id.ShouldBe(manager2.Id);
manager1ViewModel.Employees[1].Manager.ShouldBeSameAs(manager1ViewModel);
}
[Fact]
public void VerifyNestedSelfReference()
{
// Arrange
var manager1 = new Manager { Id = 100, EmployeeCode = "M001", Level = 100 };
var manager3 = new Manager { Id = 101, EmployeeCode = "M003", Level = 100 };
var manager2 = new Manager { Id = 102, EmployeeCode = "M002", Level = 100 };
var employee1 = new Employee { Id = 200, EmployeeCode = "E001"};
var employee2 = new Employee { Id = 201, EmployeeCode = "E002"};
var employee3 = new Employee { Id = 202, EmployeeCode = "E003"};
employee1.Manager = manager1;
employee2.Manager = manager2;
employee3.Manager = manager3;
manager2.Manager = manager1;
manager3.Manager = manager2;
// Act
var manager3ViewModel = manager3.ToManagerViewModel();
// Assert
manager3ViewModel.Manager.ShouldNotBeNull();
manager3ViewModel.Manager.Id.ShouldBe(manager2.Id);
manager3ViewModel.Manager.Manager.Id.ShouldBe(manager1.Id);
manager3ViewModel.Employees.All(e => ReferenceEquals(e.Manager, manager3ViewModel)).ShouldBeTrue();
}
}
using System.Linq;
using MapTo.Integration.Tests.Data.Models;
using MapTo.Integration.Tests.Data.ViewModels;
using Shouldly;
using Xunit;
namespace MapTo.Integration.Tests
{
public class CyclicReferenceTests
{
[Fact]
public void VerifySelfReference()
{
// Arrange
var manager = new Manager { Id = 1, EmployeeCode = "M001", Level = 100 };
manager.Manager = manager;
// Act
var result = manager.ToManagerViewModel();
// Assert
result.Id.ShouldBe(manager.Id);
result.EmployeeCode.ShouldBe(manager.EmployeeCode);
result.Level.ShouldBe(manager.Level);
result.Manager.ShouldBeSameAs(result);
}
[Fact]
public void VerifyNestedReference()
{
// Arrange
var manager1 = new Manager { Id = 100, EmployeeCode = "M001", Level = 100 };
var manager2 = new Manager { Id = 102, EmployeeCode = "M002", Level = 100 };
var employee1 = new Employee { Id = 200, EmployeeCode = "E001"};
var employee2 = new Employee { Id = 201, EmployeeCode = "E002"};
employee1.Manager = manager1;
employee2.Manager = manager2;
manager2.Manager = manager1;
// Act
var manager1ViewModel = manager1.ToManagerViewModel();
// Assert
manager1ViewModel.Id.ShouldBe(manager1.Id);
manager1ViewModel.Manager.ShouldBeNull();
manager1ViewModel.Employees.Count.ShouldBe(2);
manager1ViewModel.Employees[0].Id.ShouldBe(employee1.Id);
manager1ViewModel.Employees[0].Manager.ShouldBeSameAs(manager1ViewModel);
manager1ViewModel.Employees[1].Id.ShouldBe(manager2.Id);
manager1ViewModel.Employees[1].Manager.ShouldBeSameAs(manager1ViewModel);
}
[Fact]
public void VerifyNestedSelfReference()
{
// Arrange
var manager1 = new Manager { Id = 100, EmployeeCode = "M001", Level = 100 };
var manager3 = new Manager { Id = 101, EmployeeCode = "M003", Level = 100 };
var manager2 = new Manager { Id = 102, EmployeeCode = "M002", Level = 100 };
var employee1 = new Employee { Id = 200, EmployeeCode = "E001"};
var employee2 = new Employee { Id = 201, EmployeeCode = "E002"};
var employee3 = new Employee { Id = 202, EmployeeCode = "E003"};
employee1.Manager = manager1;
employee2.Manager = manager2;
employee3.Manager = manager3;
manager2.Manager = manager1;
manager3.Manager = manager2;
// Act
var manager3ViewModel = manager3.ToManagerViewModel();
// Assert
manager3ViewModel.Manager.ShouldNotBeNull();
manager3ViewModel.Manager.Id.ShouldBe(manager2.Id);
manager3ViewModel.Manager.Manager.Id.ShouldBe(manager1.Id);
manager3ViewModel.Employees.All(e => ReferenceEquals(e.Manager, manager3ViewModel)).ShouldBeTrue();
}
}
}

View File

@ -1,29 +1,29 @@
namespace MapTo.Integration.Tests.Data.Models
{
public class Employee
{
private Manager _manager;
public int Id { get; set; }
public string EmployeeCode { get; set; }
public Manager Manager
{
get => _manager;
set
{
if (value == null)
{
_manager.Employees.Remove(this);
}
else
{
value.Employees.Add(this);
}
_manager = value;
}
}
}
namespace MapTo.Integration.Tests.Data.Models
{
public class Employee
{
private Manager _manager;
public int Id { get; set; }
public string EmployeeCode { get; set; }
public Manager Manager
{
get => _manager;
set
{
if (value == null)
{
_manager.Employees.Remove(this);
}
else
{
value.Employees.Add(this);
}
_manager = value;
}
}
}
}

View File

@ -1,11 +1,11 @@
using System.Collections.Generic;
namespace MapTo.Integration.Tests.Data.Models
{
public class Manager : Employee
{
public int Level { get; set; }
public List<Employee> Employees { get; set; } = new();
}
using System.Collections.Generic;
namespace MapTo.Integration.Tests.Data.Models
{
public class Manager : Employee
{
public int Level { get; set; }
public List<Employee> Employees { get; set; } = new();
}
}

View File

@ -1,14 +1,14 @@
using MapTo.Integration.Tests.Data.Models;
namespace MapTo.Integration.Tests.Data.ViewModels
{
[MapFrom(typeof(Employee))]
public partial class EmployeeViewModel
{
public int Id { get; set; }
public string EmployeeCode { get; set; }
public ManagerViewModel Manager { get; set; }
}
using MapTo.Integration.Tests.Data.Models;
namespace MapTo.Integration.Tests.Data.ViewModels
{
[MapFrom(typeof(Employee))]
public partial class EmployeeViewModel
{
public int Id { get; set; }
public string EmployeeCode { get; set; }
public ManagerViewModel Manager { get; set; }
}
}

View File

@ -1,13 +1,13 @@
using System.Collections.Generic;
using MapTo.Integration.Tests.Data.Models;
namespace MapTo.Integration.Tests.Data.ViewModels
{
[MapFrom(typeof(Manager))]
public partial class ManagerViewModel : EmployeeViewModel
{
public int Level { get; set; }
public List<EmployeeViewModel> Employees { get; set; } = new();
}
using System.Collections.Generic;
using MapTo.Integration.Tests.Data.Models;
namespace MapTo.Integration.Tests.Data.ViewModels
{
[MapFrom(typeof(Manager))]
public partial class ManagerViewModel : EmployeeViewModel
{
public int Level { get; set; }
public List<EmployeeViewModel> Employees { get; set; } = new();
}
}

View File

@ -1,26 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\MapTo\MapTo.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="Shouldly" Version="4.0.3" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.0.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\BlueWest.MapTo\BlueWest.MapTo.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="Shouldly" Version="4.0.3" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.0.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
</Project>

View File

@ -0,0 +1,59 @@
using System;
using MapTo;
namespace BlueWest.Data
{
public enum FinanceSymbol
{
BTC_EUR,
BTC_BUSD,
BTC_USD,
BTC_USDT,
LTC_EUR,
LTC_BUSD,
LTC_USDT
}
public enum FinanceTransactionType
{
Buy,
Sell
}
[JsonExtension]
[MapFrom(typeof(FinanceTransactionInsertDto))]
public partial struct FinanceTransaction
{
public readonly int Id;
public readonly int UserId;
public readonly FinanceTransactionType FinanceTransactionType;
public readonly FinanceSymbol FinanceSymbol;
public readonly double Amount; // To Buy
public readonly double Quantity; // Bought
public readonly double Fee;
public readonly DateTime DateTime;
public FinanceTransaction(int id, int userId, FinanceTransactionType financeTransactionType,
FinanceSymbol financeSymbol, double amount, double quantity, double fee, DateTime dateTime)
{
Id = id;
UserId = userId;
FinanceTransactionType = financeTransactionType;
FinanceSymbol = financeSymbol;
Amount = amount;
Quantity = quantity;
Fee = fee;
DateTime = dateTime;
}
}
}

View File

@ -0,0 +1,16 @@
using System;
namespace BlueWest.Data
{
public partial struct FinanceTransactionInsertDto
{
public readonly int UserId;
public readonly FinanceTransactionType FinanceTransactionType;
public readonly FinanceSymbol FinanceSymbol;
public readonly double Amount; // To Buy
public readonly double Quantity; // Bought
public readonly double Fee;
public readonly DateTime DateTime;
}
}

View File

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Text;
using MapTo;
namespace BlueWest.Data
{
[MapFrom(typeof(FinanceTransaction))]
partial struct FinanceTransactionReadDto
{
public readonly int UserId;
public readonly FinanceTransactionType FinanceTransactionType;
public readonly FinanceSymbol FinanceSymbol;
public readonly double Amount; // To Buy
public readonly double Quantity; // Bought
public readonly double Fee;
public readonly DateTime DateTime;
public readonly string ReadData;
}
}

View File

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MapTo;
using TestConsoleApp.ViewModels;
namespace TestConsoleApp.Data.Models
{
[MapFrom(typeof(CarReadDto))]
[UseUpdate]
partial class Car
{
public int Size { get; }
public int Id { get; }
public string Brand { get; }
public Car(int size, int id, string brand)
{
Size = size;
Id = id;
Brand = brand;
}
}
}

View File

@ -1,15 +1,21 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace TestConsoleApp.Data.Models
{
public class Employee
{
public int Id { get; set; }
public string EmployeeCode { get; set; }
public Manager Manager { get; set; }
}
}
using System;
using System.Collections.Generic;
using System.Text;
namespace TestConsoleApp.Data.Models
{
public class Employee
{
public int Id { get; }
public string EmployeeCode { get; }
public Employee(int id, string employeeCode)
{
Id = id;
EmployeeCode = employeeCode;
}
}
}

View File

@ -1,13 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace TestConsoleApp.Data.Models
{
public class Manager: Employee
{
public int Level { get; set; }
public IEnumerable<Employee> Employees { get; set; } = Array.Empty<Employee>();
}
}

View File

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TestConsoleApp.ViewModels;
using MapTo;
namespace TestConsoleApp.Data.Models
{
[MapFrom(typeof(MyStructViewModel))]
[UseUpdate]
public partial struct MyStruct
{
public int SomeInt { get; set; }
public string ReadOnlyString { get; }
public MyStruct(int someInt, string readOnlyString)
{
SomeInt = someInt;
ReadOnlyString = readOnlyString;
}
}
}

View File

@ -1,11 +0,0 @@
namespace TestConsoleApp.Data.Models
{
public class Profile
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string FullName => $"{FirstName} {LastName}";
}
}

View File

@ -1,13 +0,0 @@
using System;
namespace TestConsoleApp.Data.Models
{
public class User
{
public int Id { get; set; }
public DateTimeOffset RegisteredAt { get; set; }
public Profile Profile { get; set; }
}
}

View File

@ -0,0 +1,44 @@

using System.Collections.Generic;
using MapTo;
namespace BlueWest.Data
{
[MapFrom(typeof(UserUpdateDto))]
[JsonExtension]
public partial class User
{
public readonly int Id;
public string Name;
public string Address;
public string BTCAddress;
public string LTCAddress;
public double BTCAmount;
public double LTCAmount;
public readonly List<FinanceTransaction> FinanceTransactions;
public User(int id, string name, string address, string btcAddress, string ltcAddress, double btcAmount, double ltcAmount, List<FinanceTransaction> financeTransactions)
{
Id = id;
Name = name;
Address = address;
BTCAddress = btcAddress;
LTCAddress = ltcAddress;
BTCAmount = btcAmount;
LTCAmount = ltcAmount;
FinanceTransactions = financeTransactions;
}
public void AddTransaction(FinanceTransaction financeTransaction)
{
FinanceTransactions.Add(financeTransaction);
}
}
}

View File

@ -0,0 +1,16 @@
using System.Collections.Generic;
namespace BlueWest.Data
{
public class UserList
{
public List<User> Users;
public UserList(List<User> users)
{
Users = users;
}
public int Length => Users.Count;
}
}

View File

@ -0,0 +1,19 @@
using MapTo;
namespace BlueWest.Data
{
[MapFrom(typeof(User))]
public partial class UserUpdateDto
{
public string Name;
public string Address;
public string BTCAddress;
public string LTCAddress;
public double BTCAmount;
public double LTCAmount;
}
}

View File

@ -1,92 +1,32 @@
using System;
using MapTo;
using TestConsoleApp.Data.Models;
using TestConsoleApp.ViewModels;
using TestConsoleApp.ViewModels2;
namespace TestConsoleApp
{
internal class Program
{
private static void Main(string[] args)
{
//UserTest();
CyclicReferenceTest();
// EmployeeManagerTest();
Console.WriteLine("done");
}
private static void EmployeeManagerTest()
{
var manager1 = new Manager
{
Id = 1,
EmployeeCode = "M001",
Level = 100
};
var manager2 = new Manager
{
Id = 2,
EmployeeCode = "M002",
Level = 100,
Manager = manager1
};
var employee1 = new Employee
{
Id = 101,
EmployeeCode = "E101",
Manager = manager1
};
var employee2 = new Employee
{
Id = 102,
EmployeeCode = "E102",
Manager = manager2
};
manager1.Employees = new[] { employee1, manager2 };
manager2.Employees = new[] { employee2 };
manager1.ToManagerViewModel();
employee1.ToEmployeeViewModel();
}
private static ManagerViewModel CyclicReferenceTest()
{
var manager1 = new Manager
{
Id = 1,
EmployeeCode = "M001",
Level = 100
};
manager1.Manager = manager1;
return manager1.ToManagerViewModel();
}
private static void UserTest()
{
var user = new User
{
Id = 1234,
RegisteredAt = DateTimeOffset.Now,
Profile = new Profile
{
FirstName = "John",
LastName = "Doe"
}
};
var vm = user.ToUserViewModel();
Console.WriteLine("Key: {0}", vm.Key);
Console.WriteLine("RegisteredAt: {0}", vm.RegisteredAt);
Console.WriteLine("FirstName: {0}", vm.Profile.FirstName);
Console.WriteLine("LastName: {0}", vm.Profile.LastName);
}
}
using System;
using MapTo;
using TestConsoleApp.Data.Models;
using TestConsoleApp.ViewModels;
namespace TestConsoleApp
{
internal class Program
{
private static void Main(string[] args)
{
//UserTest();
// EmployeeManagerTest();
Console.WriteLine("done");
}
private static void EmployeeManagerTest()
{
var employee = new Employee(1, "hello");
}
}
}

View File

@ -1,18 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net471</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\MapTo\MapTo.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
<Import Project="..\..\src\MapTo\MapTo.props" />
<PropertyGroup>
<MapTo_ConstructorAccessModifier>Internal</MapTo_ConstructorAccessModifier>
</PropertyGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net471</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\BlueWest.MapTo\BlueWest.MapTo.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
<Import Project="..\..\src\BlueWest.MapTo\MapTo.props" />
<PropertyGroup>
<MapTo_ConstructorAccessModifier>Internal</MapTo_ConstructorAccessModifier>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MapTo;
using TestConsoleApp.Data.Models;
namespace TestConsoleApp.ViewModels
{
[MapFrom(typeof(Car))]
partial class CarReadDto
{
public int Size { get; }
public string Brand { get; }
public CarReadDto(int size, string brand)
{
Size = size;
Brand = brand;
}
}
}

View File

@ -1,16 +1,13 @@
using MapTo;
using TestConsoleApp.Data.Models;
using TestConsoleApp.ViewModels2;
namespace TestConsoleApp.ViewModels
{
[MapFrom(typeof(Employee))]
public partial class EmployeeViewModel
{
public int Id { get; set; }
public string EmployeeCode { get; set; }
public ManagerViewModel Manager { get; set; }
}
}
using MapTo;
using TestConsoleApp.Data.Models;
namespace TestConsoleApp.ViewModels
{
[MapFrom(typeof(Employee))]
public partial class EmployeeViewModel
{
public int Id { get; }
}
}

View File

@ -1,16 +0,0 @@
using System;
using System.Collections.Generic;
using MapTo;
using TestConsoleApp.Data.Models;
using TestConsoleApp.ViewModels;
namespace TestConsoleApp.ViewModels2
{
[MapFrom(typeof(Manager))]
public partial class ManagerViewModel : EmployeeViewModel
{
public int Level { get; set; }
public IEnumerable<EmployeeViewModel> Employees { get; set; } = Array.Empty<EmployeeViewModel>();
}
}

View File

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TestConsoleApp.Data.Models;
using MapTo;
namespace TestConsoleApp.ViewModels
{
[MapFrom(typeof(MyStruct))]
public partial struct MyStructViewModel
{
public int SomeInt { get; set; }
public MyStructViewModel(int someInt)
{
SomeInt = someInt;
}
}
}

View File

@ -1,13 +0,0 @@
using MapTo;
using TestConsoleApp.Data.Models;
namespace TestConsoleApp.ViewModels
{
[MapFrom(typeof(Profile))]
public partial class ProfileViewModel
{
public string FirstName { get; }
public string LastName { get; }
}
}

View File

@ -1,24 +0,0 @@
using System;
using MapTo;
using TestConsoleApp.Data.Models;
namespace TestConsoleApp.ViewModels
{
[MapFrom(typeof(User))]
public partial class UserViewModel
{
[MapProperty(SourcePropertyName = nameof(User.Id))]
[MapTypeConverter(typeof(IdConverter))]
public string Key { get; }
public DateTimeOffset RegisteredAt { get; set; }
// [IgnoreProperty]
public ProfileViewModel Profile { get; set; }
private class IdConverter : ITypeConverter<int, string>
{
public string Convert(int source, object[]? converterParameters) => $"{source:X}";
}
}
}

View File

@ -1,6 +1,6 @@
{
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json",
"version": "0.8",
"version": "0.9",
"semVer1NumericIdentifierPadding": 1,
"publicReleaseRefSpec": [
"^refs/heads/master$",