diff --git a/BlueWest.Api/Controllers/CountryController.cs b/BlueWest.Api/Controllers/CountriesController.cs
similarity index 77%
rename from BlueWest.Api/Controllers/CountryController.cs
rename to BlueWest.Api/Controllers/CountriesController.cs
index 5358096..cb5f64b 100644
--- a/BlueWest.Api/Controllers/CountryController.cs
+++ b/BlueWest.Api/Controllers/CountriesController.cs
@@ -7,11 +7,15 @@ using Microsoft.AspNetCore.Mvc;
namespace BlueWest.WebApi.Controllers;
[ApiController]
[Route("[controller]")]
-public class CountryController : ControllerBase
+public class ContriesController : ControllerBase
{
private readonly CountriesDbContext _dbContext;
- public CountryController(CountriesDbContext dbContext)
+ ///
+ /// Controller responsible for handling country data in the Country table
+ ///
+ ///
+ public ContriesController(CountriesDbContext dbContext)
{
_dbContext = dbContext;
}
@@ -49,19 +53,20 @@ public class CountryController : ControllerBase
///
/// Updates a Country
///
- /// Payload with country data to update. Note that the Code is the primary key and can't be changed.
+ /// The country ISO 3166 code
+ /// Country payload data
///
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
- [HttpPut("countries/{country.Code}")]
- public ActionResult UpdateCountry(Country countryToUpdate)
+ [HttpPut("{countryCode}")]
+ public ActionResult UpdateCountry(int countryCode, CountryUpdate countryToUpdate)
{
- var country = _dbContext.Countries.FirstOrDefault(x => x.Code == countryToUpdate.Code);
+ var country = _dbContext.Countries.FirstOrDefault(x => x.Code == countryCode);
if (country != null)
{
- var updatedCountry = new Country(country.Code, countryToUpdate.StateName, countryToUpdate.TLD);
+ var updatedCountry = new Country(countryToUpdate, countryCode, null);
_dbContext.Countries.Update(updatedCountry);
return Ok(updatedCountry);
}
@@ -76,7 +81,7 @@ public class CountryController : ControllerBase
///
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- [HttpGet("countries")]
+ [HttpGet("")]
public ActionResult GetCountries()
{
var array = _dbContext.Countries;
@@ -92,11 +97,11 @@ public class CountryController : ControllerBase
///
/// Get Country by Id
///
- ///
+ /// ISO 3166-1 country numeric code
///
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- [HttpGet("countries/{countryId}", Name = nameof(GetCountryById))]
+ [HttpGet("{countryId}", Name = nameof(GetCountryById))]
public ActionResult GetCountryById(int countryId)
{
var array = _dbContext.Countries.FirstOrDefault(x => x.Code == countryId);
diff --git a/BlueWest.Api/Controllers/CurrencyController.cs b/BlueWest.Api/Controllers/CurrenciesController.cs
similarity index 73%
rename from BlueWest.Api/Controllers/CurrencyController.cs
rename to BlueWest.Api/Controllers/CurrenciesController.cs
index 78501b7..979ef0d 100644
--- a/BlueWest.Api/Controllers/CurrencyController.cs
+++ b/BlueWest.Api/Controllers/CurrenciesController.cs
@@ -1,3 +1,4 @@
+using System.Collections.Generic;
using System.Linq;
using BlueWest.Data;
using BlueWest.WebApi.MySQL;
@@ -27,15 +28,18 @@ public class CurrencyController : ControllerBase
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
- [HttpPut("countries/{Currency.Code}")]
- public ActionResult UpdateCurrency(Currency Currency)
+ [HttpPut("countries/{currencyNumber}")]
+ public ActionResult UpdateCurrency(int currencyNumber, CurrencyUpdate currencyToUpdate)
{
- var array = _dbContext.Currencies.FirstOrDefault(x => x.Code == Currency.Code);
+ var currency = _dbContext.Currencies.FirstOrDefault(x => x.Num ==currencyNumber);
- if (array != null)
+ if (currency != null)
{
- return Ok(array);
+ var updatedCurrency = new Currency(currencyToUpdate, currencyNumber, new List());
+ _dbContext.Update(updatedCurrency);
+ _dbContext.SaveChanges();
+ return Ok(updatedCurrency);
}
return new NotFoundResult();
diff --git a/BlueWest.Data/Finance/Currency/CountryCreate.cs b/BlueWest.Data/Finance/Currency/CountryCreate.cs
new file mode 100644
index 0000000..0e90e67
--- /dev/null
+++ b/BlueWest.Data/Finance/Currency/CountryCreate.cs
@@ -0,0 +1,6 @@
+namespace BlueWest.Data;
+
+public class CountryCreate
+{
+
+}
\ No newline at end of file
diff --git a/BlueWest.Data/Finance/Currency/CountryUpdate.cs b/BlueWest.Data/Finance/Currency/CountryUpdate.cs
new file mode 100644
index 0000000..5f79207
--- /dev/null
+++ b/BlueWest.Data/Finance/Currency/CountryUpdate.cs
@@ -0,0 +1,6 @@
+namespace BlueWest.Data;
+
+public class CountryUpdate
+{
+
+}
\ No newline at end of file
diff --git a/BlueWest.Data/Finance/Currency/CurrencyUpdate.cs b/BlueWest.Data/Finance/Currency/CurrencyUpdate.cs
new file mode 100644
index 0000000..58bc1f7
--- /dev/null
+++ b/BlueWest.Data/Finance/Currency/CurrencyUpdate.cs
@@ -0,0 +1,6 @@
+namespace BlueWest.Data;
+
+public class CurrencyUpdate
+{
+
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/.editorconfig b/include/BlueWest.MapTo/.editorconfig
new file mode 100644
index 0000000..aa6b134
--- /dev/null
+++ b/include/BlueWest.MapTo/.editorconfig
@@ -0,0 +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
diff --git a/include/BlueWest.MapTo/.github/workflows/build-and-test.yml b/include/BlueWest.MapTo/.github/workflows/build-and-test.yml
new file mode 100644
index 0000000..8066ca6
--- /dev/null
+++ b/include/BlueWest.MapTo/.github/workflows/build-and-test.yml
@@ -0,0 +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
diff --git a/include/BlueWest.MapTo/.github/workflows/publish-packages.yml b/include/BlueWest.MapTo/.github/workflows/publish-packages.yml
new file mode 100644
index 0000000..b95fc1b
--- /dev/null
+++ b/include/BlueWest.MapTo/.github/workflows/publish-packages.yml
@@ -0,0 +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/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}}
diff --git a/include/BlueWest.MapTo/.gitignore b/include/BlueWest.MapTo/.gitignore
new file mode 100644
index 0000000..290f4b9
--- /dev/null
+++ b/include/BlueWest.MapTo/.gitignore
@@ -0,0 +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
diff --git a/include/BlueWest.MapTo/Directory.Build.props b/include/BlueWest.MapTo/Directory.Build.props
new file mode 100644
index 0000000..b3e9068
--- /dev/null
+++ b/include/BlueWest.MapTo/Directory.Build.props
@@ -0,0 +1,9 @@
+
+
+
+
+ 3.4.22
+ all
+
+
+
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/LICENSE b/include/BlueWest.MapTo/LICENSE
new file mode 100644
index 0000000..841c74d
--- /dev/null
+++ b/include/BlueWest.MapTo/LICENSE
@@ -0,0 +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
+SOFTWARE.
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/MapTo.sln b/include/BlueWest.MapTo/MapTo.sln
new file mode 100644
index 0000000..35ef225
--- /dev/null
+++ b/include/BlueWest.MapTo/MapTo.sln
@@ -0,0 +1,34 @@
+
+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
diff --git a/include/BlueWest.MapTo/MapTo.sln.DotSettings b/include/BlueWest.MapTo/MapTo.sln.DotSettings
new file mode 100644
index 0000000..83b264c
--- /dev/null
+++ b/include/BlueWest.MapTo/MapTo.sln.DotSettings
@@ -0,0 +1,6 @@
+
+ True
+ True
+ True
+ True
+ True
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/README.md b/include/BlueWest.MapTo/README.md
new file mode 100644
index 0000000..0fe5fee
--- /dev/null
+++ b/include/BlueWest.MapTo/README.md
@@ -0,0 +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` 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
+ {
+ public string Convert(int source, object[] converterParameters) => $"{source:X}";
+ }
+}
+```
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/src/BlueWest.MapTo/BlueWest.MapTo.csproj b/include/BlueWest.MapTo/src/BlueWest.MapTo/BlueWest.MapTo.csproj
new file mode 100644
index 0000000..6061e68
--- /dev/null
+++ b/include/BlueWest.MapTo/src/BlueWest.MapTo/BlueWest.MapTo.csproj
@@ -0,0 +1,49 @@
+
+
+
+ netstandard2.0
+ enable
+ 9
+ An object to object mapping generator using Roslyn source generator.
+ true
+ true
+ MapTo
+ https://github.com/mrtaikandi/mapto
+ $(Version)
+ true
+ https://github.com/mrtaikandi/mapto
+ snupkg
+ MapTo
+
+
+
+ bin\Release\MapTo.xml
+
+
+
+
+ <_Parameter1>$(AssemblyName).Tests
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+ 3.5.109
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/include/BlueWest.MapTo/src/BlueWest.MapTo/ClassMappingContext.cs b/include/BlueWest.MapTo/src/BlueWest.MapTo/ClassMappingContext.cs
new file mode 100644
index 0000000..c8d427d
--- /dev/null
+++ b/include/BlueWest.MapTo/src/BlueWest.MapTo/ClassMappingContext.cs
@@ -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 GetSourceMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
+ {
+ var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType().ToArray();
+
+ return typeSymbol
+ .GetAllMembers()
+ .OfType()
+ .Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol))
+ .Select(property => MapField(sourceTypeSymbol, sourceProperties, property))
+ .Where(mappedProperty => mappedProperty is not null)
+ .ToImmutableArray()!;
+ }
+
+ protected override ImmutableArray GetSourceMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool hasInheritedClass)
+ {
+ var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType().ToArray();
+
+ return typeSymbol
+ .GetAllMembers()
+ .OfType()
+ .Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol))
+ .Select(property => MapProperty(sourceTypeSymbol, sourceProperties, property))
+ .Where(mappedProperty => mappedProperty is not null)
+ .ToImmutableArray()!;
+ }
+
+ protected override ImmutableArray GetTypeMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
+ {
+ var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType().ToArray();
+
+ return sourceTypeSymbol
+ .GetAllMembers()
+ .OfType()
+ .Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol))
+ .Select(property => MapFieldSimple(typeSymbol, property))
+ .Where(mappedProperty => mappedProperty is not null)
+ .ToImmutableArray()!;
+ }
+
+ protected override ImmutableArray GetTypeMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool hasInheritedClass)
+ {
+ var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType().ToArray();
+
+ return sourceTypeSymbol
+ .GetAllMembers()
+ .OfType()
+ .Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol))
+ .Select(property => MapPropertySimple(typeSymbol, property))
+ .Where(mappedProperty => mappedProperty is not null)
+ .ToImmutableArray()!;
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/src/BlueWest.MapTo/CompilerServices/IsExternalInit.cs b/include/BlueWest.MapTo/src/BlueWest.MapTo/CompilerServices/IsExternalInit.cs
new file mode 100644
index 0000000..72b53ee
--- /dev/null
+++ b/include/BlueWest.MapTo/src/BlueWest.MapTo/CompilerServices/IsExternalInit.cs
@@ -0,0 +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
+{
+ ///
+ /// Reserved to be used by the compiler for tracking metadata.
+ /// This class should not be used by developers in source code.
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ internal static class IsExternalInit { }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/src/BlueWest.MapTo/CompilerServices/NullableAttributes.cs b/include/BlueWest.MapTo/src/BlueWest.MapTo/CompilerServices/NullableAttributes.cs
new file mode 100644
index 0000000..3a00084
--- /dev/null
+++ b/include/BlueWest.MapTo/src/BlueWest.MapTo/CompilerServices/NullableAttributes.cs
@@ -0,0 +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
+ /// Specifies that null is allowed as an input even if the corresponding type disallows it.
+ [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property)]
+ internal sealed class AllowNullAttribute : Attribute { }
+
+ /// Specifies that null is disallowed as an input even if the corresponding type allows it.
+ [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property)]
+ internal sealed class DisallowNullAttribute : Attribute { }
+
+ /// Specifies that an output may be null even if the corresponding type disallows it.
+ [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue)]
+ internal sealed class MaybeNullAttribute : Attribute { }
+
+ ///
+ /// 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.
+ ///
+ [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue)]
+ internal sealed class NotNullAttribute : Attribute { }
+
+ ///
+ /// Specifies that when a method returns , the parameter may be null even if the
+ /// corresponding type disallows it.
+ ///
+ [AttributeUsage(AttributeTargets.Parameter)]
+ internal sealed class MaybeNullWhenAttribute : Attribute
+ {
+ /// Initializes the attribute with the specified return value condition.
+ ///
+ /// The return value condition. If the method returns this value, the associated parameter may be null.
+ ///
+ public MaybeNullWhenAttribute(bool returnValue)
+ {
+ ReturnValue = returnValue;
+ }
+
+ /// Gets the return value condition.
+ public bool ReturnValue { get; }
+ }
+
+ ///
+ /// Specifies that when a method returns , the parameter will not be null even if the
+ /// corresponding type allows it.
+ ///
+ [AttributeUsage(AttributeTargets.Parameter)]
+ internal sealed class NotNullWhenAttribute : Attribute
+ {
+ /// Initializes the attribute with the specified return value condition.
+ ///
+ /// The return value condition. If the method returns this value, the associated parameter will not be null.
+ ///
+ public NotNullWhenAttribute(bool returnValue)
+ {
+ ReturnValue = returnValue;
+ }
+
+ /// Gets the return value condition.
+ public bool ReturnValue { get; }
+ }
+
+ /// Specifies that the output will be non-null if the named parameter is non-null.
+ [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true)]
+ internal sealed class NotNullIfNotNullAttribute : Attribute
+ {
+ /// Initializes the attribute with the associated parameter name.
+ ///
+ /// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null.
+ ///
+ public NotNullIfNotNullAttribute(string parameterName)
+ {
+ ParameterName = parameterName;
+ }
+
+ /// Gets the associated parameter name.
+ public string ParameterName { get; }
+ }
+
+ /// Applied to a method that will never return under any circumstance.
+ [AttributeUsage(AttributeTargets.Method, Inherited = false)]
+ internal sealed class DoesNotReturnAttribute : Attribute { }
+
+ /// Specifies that the method will not return if the associated Boolean parameter is passed the specified value.
+ [AttributeUsage(AttributeTargets.Parameter)]
+ internal sealed class DoesNotReturnIfAttribute : Attribute
+ {
+ /// Initializes the attribute with the specified parameter value.
+ ///
+ /// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to
+ /// the associated parameter matches this value.
+ ///
+ public DoesNotReturnIfAttribute(bool parameterValue)
+ {
+ ParameterValue = parameterValue;
+ }
+
+ /// Gets the condition parameter value.
+ public bool ParameterValue { get; }
+ }
+#endif
+
+ ///
+ /// Specifies that the method or property will ensure that the listed field and property members have not-null
+ /// values.
+ ///
+ [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
+ internal sealed class MemberNotNullAttribute : Attribute
+ {
+ /// Initializes the attribute with a field or property member.
+ ///
+ /// The field or property member that is promised to be not-null.
+ ///
+ public MemberNotNullAttribute(string member)
+ {
+ Members = new[] { member };
+ }
+
+ /// Initializes the attribute with the list of field and property members.
+ ///
+ /// The list of field and property members that are promised to be not-null.
+ ///
+ public MemberNotNullAttribute(params string[] members)
+ {
+ Members = members;
+ }
+
+ /// Gets field or property member names.
+ public string[] Members { get; }
+ }
+
+ ///
+ /// 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.
+ ///
+ [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
+ internal sealed class MemberNotNullWhenAttribute : Attribute
+ {
+ /// Initializes the attribute with the specified return value condition and a field or property member.
+ ///
+ /// The return value condition. If the method returns this value, the associated parameter will not be null.
+ ///
+ ///
+ /// The field or property member that is promised to be not-null.
+ ///
+ public MemberNotNullWhenAttribute(bool returnValue, string member)
+ {
+ ReturnValue = returnValue;
+ Members = new[] { member };
+ }
+
+ /// Initializes the attribute with the specified return value condition and list of field and property members.
+ ///
+ /// The return value condition. If the method returns this value, the associated parameter will not be null.
+ ///
+ ///
+ /// The list of field and property members that are promised to be not-null.
+ ///
+ public MemberNotNullWhenAttribute(bool returnValue, params string[] members)
+ {
+ ReturnValue = returnValue;
+ Members = members;
+ }
+
+ /// Gets field or property member names.
+ public string[] Members { get; }
+
+ /// Gets the return value condition.
+ public bool ReturnValue { get; }
+ }
+}
+#endif
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/src/BlueWest.MapTo/DiagnosticsFactory.cs b/include/BlueWest.MapTo/src/BlueWest.MapTo/DiagnosticsFactory.cs
new file mode 100644
index 0000000..bb4fbeb
--- /dev/null
+++ b/include/BlueWest.MapTo/src/BlueWest.MapTo/DiagnosticsFactory.cs
@@ -0,0 +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 '{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);
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/src/BlueWest.MapTo/Extensions/CommonExtensions.cs b/include/BlueWest.MapTo/src/BlueWest.MapTo/Extensions/CommonExtensions.cs
new file mode 100644
index 0000000..edc2e23
--- /dev/null
+++ b/include/BlueWest.MapTo/src/BlueWest.MapTo/Extensions/CommonExtensions.cs
@@ -0,0 +1,81 @@
+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 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)
+ {
+ return 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 {model.SourceNamespace}")
+ .WriteComment($" SourceTypeFullName {model.SourceTypeFullName}")
+ .WriteComment($" SourceTypeIdentifierName {model.SourceTypeIdentifierName}");
+
+ }
+
+ internal static SourceBuilder WriteMappedProperties(this SourceBuilder builder, System.Collections.Immutable.ImmutableArray 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;
+ }
+
+ }
+}
diff --git a/include/BlueWest.MapTo/src/BlueWest.MapTo/Extensions/CommonSource.cs b/include/BlueWest.MapTo/src/BlueWest.MapTo/Extensions/CommonSource.cs
new file mode 100644
index 0000000..07f0413
--- /dev/null
+++ b/include/BlueWest.MapTo/src/BlueWest.MapTo/Extensions/CommonSource.cs
@@ -0,0 +1,303 @@
+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;
+
+namespace MapTo.Extensions
+{
+ internal static class CommonSource
+ {
+ internal static SourceCode GenerateStructOrClass(this MappingModel model, string structOrClass)
+ {
+ const bool writeDebugInfo = true;
+
+ using var builder = new SourceBuilder()
+ .WriteLine(GeneratedFilesHeader)
+ .WriteNullableContextOptionIf(model.Options.SupportNullableReferenceTypes)
+ .WriteUsings(model.Usings)
+ .WriteLine()
+
+ // Namespace declaration
+ .WriteLine($"namespace {model.Namespace}")
+ .WriteOpeningBracket();
+
+ if (writeDebugInfo)
+ builder
+ .WriteModelInfo(model)
+ .WriteLine()
+ .WriteComment("Type properties")
+ .WriteComment()
+ .WriteMappedProperties(model.TypeProperties)
+ .WriteLine()
+ .WriteComment("Source properties")
+ .WriteLine()
+ .WriteComment("Type fields")
+ .WriteComment()
+ .WriteMappedProperties(model.TypeFields)
+ .WriteLine()
+ .WriteComment("Source fields")
+ .WriteMappedProperties(model.SourceFields)
+ .WriteLine();
+
+ builder
+ // Class declaration
+ .WriteLine($"partial {structOrClass} {model.TypeIdentifierName}")
+ .WriteOpeningBracket()
+ .WriteLine()
+ // Class body
+ .GeneratePublicConstructor(model);
+
+ if (model.IsJsonExtension) builder.WriteToJsonMethod(model);
+ if (model.IsTypeUpdatable && model.TypeProperties.GetWritableMappedProperties().Length > 0) builder.GenerateUpdateMethod(model);
+ if (model.IsTypeUpdatable && model.TypeFields.GetWritableMappedProperties().Length > 0) builder.GenerateUpdateMethod(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)
+ {
+ var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase();
+ const string mappingContextParameterName = "context";
+
+ var baseConstructor = /*model.HasMappedBaseClass ? $" : base({mappingContextParameterName}, {sourceClassParameterName})" :*/ string.Empty;
+
+ var stringBuilder = new StringBuilder();
+
+ var otherProperties = new List();
+
+ foreach (var property in model.TypeProperties)
+ {
+ if (!model.SourceProperties.IsMappedProperty(property))
+ {
+ stringBuilder.Append(", ");
+ stringBuilder.Append($"{property.FullyQualifiedType} {property.SourcePropertyName.ToCamelCase()}");
+ otherProperties.Add(property);
+ }
+ }
+
+ foreach (var property in model.TypeFields)
+ {
+ if (!model.SourceFields.IsMappedProperty(property))
+ {
+ stringBuilder.Append(", ");
+ stringBuilder.Append($"{property.FullyQualifiedType} {property.SourcePropertyName.ToCamelCase()}");
+ otherProperties.Add(property);
+ }
+ }
+
+
+ var readOnlyPropertiesArguments = stringBuilder.ToString();
+
+ builder
+ .WriteLine($"public {model.TypeIdentifierName}({model.SourceType} {sourceClassParameterName}{readOnlyPropertiesArguments}){baseConstructor}")
+ .WriteOpeningBracket()
+ .WriteAssignmentMethod(model, otherProperties.ToArray().ToImmutableArray(), sourceClassParameterName, mappingContextParameterName, false);
+
+ // End constructor declaration
+ return builder.WriteClosingBracket();
+ }
+
+ private static bool IsMappedProperty(this System.Collections.Immutable.ImmutableArray 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 property in model.TypeProperties)
+ {
+ if (!property.isEnumerable)
+ HandlePropertyEnumerable(builder, property);
+ else
+ {
+ builder = WriteJsonField(builder, property);
+ }
+ }
+ foreach (var property in model.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;
+ builder.WriteCommentArray(symbol.Parameters, nameof(symbol.Parameters));
+ 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? otherProperties,
+ string? sourceClassParameterName, string mappingContextParameterName, bool fromUpdate)
+ {
+
+ foreach (var property in model.SourceProperties)
+ {
+ if (property.isReadOnly && fromUpdate) continue;
+
+ builder.WriteLine($"{property.Name} = {sourceClassParameterName}.{property.SourcePropertyName};");
+
+ }
+
+ foreach (var property in model.SourceFields)
+ {
+ if (property.isReadOnly && fromUpdate) continue;
+
+ builder.WriteLine($"{property.Name} = {sourceClassParameterName}.{property.SourcePropertyName};");
+
+ }
+
+ if (otherProperties == null) return builder;
+
+ foreach (var property in otherProperties)
+ {
+ builder.WriteLine(property.MappedSourcePropertyTypeName is null
+ ? $"{property.Name} = {property.SourcePropertyName.ToCamelCase()};"
+ : "");
+
+ }
+
+ return builder;
+
+ }
+
+
+ private static SourceBuilder GenerateUpdateMethod(this SourceBuilder builder, MappingModel model)
+ {
+ var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase();
+
+ builder
+ .GenerateUpdaterMethodsXmlDocs(model, sourceClassParameterName)
+ .WriteLine($"public void Update({model.SourceType} {sourceClassParameterName})")
+ .WriteOpeningBracket()
+ .WriteAssignmentMethod(model, null, sourceClassParameterName, "context", true)
+ .WriteClosingBracket();
+
+ return builder;
+ }
+
+ private static SourceBuilder GenerateUpdaterMethodsXmlDocs(this SourceBuilder builder, MappingModel model, string sourceClassParameterName)
+ {
+ if (!model.Options.GenerateXmlDocument)
+ {
+ return builder;
+ }
+
+ return builder
+ .WriteLine("/// ")
+ .WriteLine($"/// Updates and sets its participating properties")
+ .WriteLine($"/// using the property values from .")
+ .WriteLine("/// ")
+ .WriteLine($"/// The instance of to use as source.");
+ }
+
+ private static SourceBuilder GenerateEnumerableJsonSourceTypeExtensionMethod(this SourceBuilder builder, MappingModel model)
+ {
+ var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase();
+
+ return builder
+ .WriteLineIf(model.Options.SupportNullableStaticAnalysis, $"[return: NotNullIfNotNull(\"{sourceClassParameterName}\")]")
+ .WriteLine($"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static string ToJson(this IEnumerable<{model.SourceType}{model.Options.NullableReferenceSyntax}> {sourceClassParameterName}List)")
+ .WriteOpeningBracket()
+ .WriteLine($"return {sourceClassParameterName} == null ? null : new {model.TypeIdentifierName}({sourceClassParameterName});")
+ .WriteClosingBracket();
+ }
+ }
+}
diff --git a/include/BlueWest.MapTo/src/BlueWest.MapTo/Extensions/EnumExtensions.cs b/include/BlueWest.MapTo/src/BlueWest.MapTo/Extensions/EnumExtensions.cs
new file mode 100644
index 0000000..bf9120f
--- /dev/null
+++ b/include/BlueWest.MapTo/src/BlueWest.MapTo/Extensions/EnumExtensions.cs
@@ -0,0 +1,9 @@
+using System;
+
+namespace MapTo.Extensions
+{
+ internal static class EnumExtensions
+ {
+ internal static string ToLowercaseString(this Enum member) => member.ToString().ToLower();
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/src/BlueWest.MapTo/Extensions/EnumerableExtensions.cs b/include/BlueWest.MapTo/src/BlueWest.MapTo/Extensions/EnumerableExtensions.cs
new file mode 100644
index 0000000..60b0f18
--- /dev/null
+++ b/include/BlueWest.MapTo/src/BlueWest.MapTo/Extensions/EnumerableExtensions.cs
@@ -0,0 +1,19 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace MapTo.Extensions
+{
+ internal static class EnumerableExtensions
+ {
+ internal static void ForEach(this IEnumerable enumerable, Action action)
+ {
+ foreach (var item in enumerable)
+ {
+ action(item);
+ }
+ }
+
+ internal static bool IsEmpty(this IEnumerable enumerable) => !enumerable.Any();
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/src/BlueWest.MapTo/Extensions/GeneratorExecutionContextExtensions.cs b/include/BlueWest.MapTo/src/BlueWest.MapTo/Extensions/GeneratorExecutionContextExtensions.cs
new file mode 100644
index 0000000..266fb1b
--- /dev/null
+++ b/include/BlueWest.MapTo/src/BlueWest.MapTo/Extensions/GeneratorExecutionContextExtensions.cs
@@ -0,0 +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(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));
+ }
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/src/BlueWest.MapTo/Extensions/RoslynExtensions.cs b/include/BlueWest.MapTo/src/BlueWest.MapTo/Extensions/RoslynExtensions.cs
new file mode 100644
index 0000000..402996e
--- /dev/null
+++ b/include/BlueWest.MapTo/src/BlueWest.MapTo/Extensions/RoslynExtensions.cs
@@ -0,0 +1,135 @@
+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 GetBaseTypesAndThis(this ITypeSymbol type)
+ {
+ var current = type;
+ while (current != null)
+ {
+ yield return current;
+
+ current = current.BaseType;
+ }
+ }
+
+ public static IEnumerable 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().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 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()
+ .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 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 GetTypesByMetadataName(this Compilation compilation, string typeMetadataName)
+ {
+ return compilation.References
+ .Select(compilation.GetAssemblyOrModuleSymbol)
+ .OfType()
+ .Select(assemblySymbol => assemblySymbol.GetTypeByMetadataName(typeMetadataName))
+ .Where(t => t != null)!;
+ }
+
+ public static bool TypeByMetadataNameExists(this Compilation compilation, string typeMetadataName) => GetTypesByMetadataName(compilation, typeMetadataName).Any();
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/src/BlueWest.MapTo/Extensions/StringBuilderExtensions.cs b/include/BlueWest.MapTo/src/BlueWest.MapTo/Extensions/StringBuilderExtensions.cs
new file mode 100644
index 0000000..95aa64b
--- /dev/null
+++ b/include/BlueWest.MapTo/src/BlueWest.MapTo/Extensions/StringBuilderExtensions.cs
@@ -0,0 +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("}");
+ }
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/src/BlueWest.MapTo/Extensions/StringExtensions.cs b/include/BlueWest.MapTo/src/BlueWest.MapTo/Extensions/StringExtensions.cs
new file mode 100644
index 0000000..5579f36
--- /dev/null
+++ b/include/BlueWest.MapTo/src/BlueWest.MapTo/Extensions/StringExtensions.cs
@@ -0,0 +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()
+ };
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/src/BlueWest.MapTo/MapTo.props b/include/BlueWest.MapTo/src/BlueWest.MapTo/MapTo.props
new file mode 100644
index 0000000..4d9ec67
--- /dev/null
+++ b/include/BlueWest.MapTo/src/BlueWest.MapTo/MapTo.props
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/src/BlueWest.MapTo/MapToGenerator.cs b/include/BlueWest.MapTo/src/BlueWest.MapTo/MapToGenerator.cs
new file mode 100644
index 0000000..c9b12eb
--- /dev/null
+++ b/include/BlueWest.MapTo/src/BlueWest.MapTo/MapToGenerator.cs
@@ -0,0 +1,76 @@
+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
+{
+ ///
+ /// MapTo source generator.
+ ///
+ [Generator]
+ public class MapToGenerator : ISourceGenerator
+ {
+ ///
+ public void Initialize(GeneratorInitializationContext context)
+ {
+ context.RegisterForSyntaxNotifications(() => new MapToSyntaxReceiver());
+ }
+
+ ///
+ 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 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);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/src/BlueWest.MapTo/MapToSyntaxReceiver.cs b/include/BlueWest.MapTo/src/BlueWest.MapTo/MapToSyntaxReceiver.cs
new file mode 100644
index 0000000..03c35ac
--- /dev/null
+++ b/include/BlueWest.MapTo/src/BlueWest.MapTo/MapToSyntaxReceiver.cs
@@ -0,0 +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 CandidateTypes { get; } = new();
+
+ ///
+ 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);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/src/BlueWest.MapTo/MappingContext.cs b/include/BlueWest.MapTo/src/BlueWest.MapTo/MappingContext.cs
new file mode 100644
index 0000000..37cc076
--- /dev/null
+++ b/include/BlueWest.MapTo/src/BlueWest.MapTo/MappingContext.cs
@@ -0,0 +1,613 @@
+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 static class MappingContextExtensions
+ {
+ internal static ImmutableArray GetReadOnlyMappedProperties(this ImmutableArray mappedProperties) => mappedProperties.Where(p => p.isReadOnly).ToImmutableArray()!;
+ internal static ImmutableArray GetWritableMappedProperties(this ImmutableArray mappedProperties) => mappedProperties.Where(p => !p.isReadOnly).ToImmutableArray()!;
+ }
+
+ internal abstract class MappingContext
+ {
+ private readonly List _ignoredNamespaces;
+
+ protected MappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax)
+ {
+ _ignoredNamespaces = new();
+ Diagnostics = ImmutableArray.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 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; }
+
+ 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 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 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 IFieldSymbol? FindSourceField(IEnumerable 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 GetSourceMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass);
+ protected abstract ImmutableArray GetTypeMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass);
+
+
+ protected abstract ImmutableArray GetSourceMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass);
+ protected abstract ImmutableArray GetTypeMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass);
+
+
+ protected ImmutableArray GetSourceTypeSymbol(TypeDeclarationSyntax typeDeclarationSyntax, SemanticModel? semanticModel = null) =>
+ GetSourceTypeSymbol(typeDeclarationSyntax.GetAttribute(MapFromAttributeSource.AttributeName), semanticModel);
+
+ // we need two possible InamedTypeSymbol
+ protected ImmutableArray GetSourceTypeSymbol(SyntaxNode? attributeSyntax, SemanticModel? semanticModel = null)
+ {
+ if (attributeSyntax is null)
+ {
+ return new ImmutableArray(){};
+ }
+
+ semanticModel ??= Compilation.GetSemanticModel(attributeSyntax.SyntaxTree);
+ var sourceTypeExpressionSyntax = attributeSyntax
+ .DescendantNodes()
+ .OfType()
+ .ToImmutableArray();
+
+ // cast
+ var resultList = new List();
+ 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 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.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 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.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(),
+ 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.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 IPropertySymbol).IsReadOnly);
+ ;
+ }
+
+ 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.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 converterParameters)
+ {
+ converterFullyQualifiedName = null;
+ converterParameters = ImmutableArray.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 converterParameters)
+ {
+ converterFullyQualifiedName = null;
+ converterParameters = ImmutableArray.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 GetTypeConverterParameters(AttributeData typeConverterAttribute)
+ {
+ var converterParameter = typeConverterAttribute.ConstructorArguments.Skip(1).FirstOrDefault();
+ return converterParameter.IsNull
+ ? ImmutableArray.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 sourceTypeSymbol = sourceTypeSymbols[0];
+
+ // Pick first one to avoid errors. TODO: Make possible to use different source types
+
+ _ignoredNamespaces.Add(sourceTypeSymbol.ContainingNamespace.ToDisplayParts().First());
+
+ var typeIdentifierName = TypeSyntax.GetIdentifierName();
+ var sourceTypeIdentifierName = sourceTypeSymbol.Name;
+ var isTypeInheritFromMappedBaseClass = IsTypeInheritFromMappedBaseClass(semanticModel);
+ var isTypeUpdatable = IsTypeUpdatable();
+ var hasJsonExtension = HasJsonExtension();
+ var shouldGenerateSecondaryConstructor = ShouldGenerateSecondaryConstructor(semanticModel, sourceTypeSymbol);
+
+ var mappedProperties = GetSourceMappedProperties(typeSymbol, sourceTypeSymbol, isTypeInheritFromMappedBaseClass);
+ var mappedFields = GetSourceMappedFields(typeSymbol, sourceTypeSymbol, isTypeInheritFromMappedBaseClass);
+
+ /*if (!mappedProperties.Any())
+ {
+ AddDiagnostic(DiagnosticsFactory.NoMatchingPropertyFoundError(TypeSyntax.GetLocation(), typeSymbol, sourceTypeSymbol));
+ return null;
+ }*/
+
+ AddUsingIfRequired(mappedProperties.Any(p => p.IsEnumerable), "System.Linq");
+
+ var allProperties = GetTypeMappedProperties(sourceTypeSymbol, typeSymbol , isTypeInheritFromMappedBaseClass);
+ var allFields = GetTypeMappedFields(sourceTypeSymbol, typeSymbol, isTypeInheritFromMappedBaseClass);
+
+ return new MappingModel(
+ SourceGenerationOptions,
+ TypeSyntax.GetNamespace(),
+ TypeSyntax.Modifiers,
+ TypeSyntax.Keyword.Text,
+ typeIdentifierName,
+ sourceTypeSymbol.ContainingNamespace.ToDisplayString(),
+ sourceTypeIdentifierName,
+ sourceTypeSymbol.ToDisplayString(),
+ isTypeUpdatable,
+ hasJsonExtension,
+ mappedProperties,
+ allProperties,
+ mappedFields,
+ allFields,
+ isTypeInheritFromMappedBaseClass,
+ Usings,
+ shouldGenerateSecondaryConstructor);
+ }
+
+
+
+ private INamedTypeSymbol? GetTypeConverterBaseInterfaceForProperty(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 INamedTypeSymbol? GetTypeConverterBaseInterfaceForField(ITypeSymbol converterTypeSymbol, ISymbol property, IFieldSymbol 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()
+ .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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/src/BlueWest.MapTo/Models.cs b/include/BlueWest.MapTo/src/BlueWest.MapTo/Models.cs
new file mode 100644
index 0000000..e10dcbf
--- /dev/null
+++ b/include/BlueWest.MapTo/src/BlueWest.MapTo/Models.cs
@@ -0,0 +1,94 @@
+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 TypeConverterParameters,
+ string SourcePropertyName,
+ string? MappedSourcePropertyTypeName,
+ string? EnumerableTypeArgument,
+ ISymbol ActualSymbol,
+ INamedTypeSymbol? NamedTypeSymbol,
+ bool isEnumerable,
+ bool isReadOnly)
+ {
+ 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,
+ bool IsTypeUpdatable,
+ bool IsJsonExtension,
+ ImmutableArray SourceProperties,
+ ImmutableArray TypeProperties,
+ ImmutableArray SourceFields,
+ ImmutableArray TypeFields,
+ bool HasMappedBaseClass,
+ ImmutableArray 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;
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/src/BlueWest.MapTo/RecordMappingContext.cs b/include/BlueWest.MapTo/src/BlueWest.MapTo/RecordMappingContext.cs
new file mode 100644
index 0000000..c56fe2b
--- /dev/null
+++ b/include/BlueWest.MapTo/src/BlueWest.MapTo/RecordMappingContext.cs
@@ -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 GetSourceMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ protected override ImmutableArray GetSourceMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
+ {
+ var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType().ToArray();
+ return typeSymbol.GetMembers()
+ .OfType()
+ .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 GetTypeMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ protected override ImmutableArray GetTypeMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
+ {
+ var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType().ToArray();
+ return typeSymbol.GetMembers()
+ .OfType()
+ .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()!;
+ }
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/src/BlueWest.MapTo/Sources/Constants.cs b/include/BlueWest.MapTo/src/BlueWest.MapTo/Sources/Constants.cs
new file mode 100644
index 0000000..66e80c2
--- /dev/null
+++ b/include/BlueWest.MapTo/src/BlueWest.MapTo/Sources/Constants.cs
@@ -0,0 +1,8 @@
+namespace MapTo.Sources
+{
+ internal class Constants
+ {
+ internal const string RootNamespace = "MapTo";
+ internal const string GeneratedFilesHeader = "// ";
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/src/BlueWest.MapTo/Sources/DictionaryToListAttributeSource.cs b/include/BlueWest.MapTo/src/BlueWest.MapTo/Sources/DictionaryToListAttributeSource.cs
new file mode 100644
index 0000000..4429fd8
--- /dev/null
+++ b/include/BlueWest.MapTo/src/BlueWest.MapTo/Sources/DictionaryToListAttributeSource.cs
@@ -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("/// ")
+ .WriteLine("/// Specifies the mapping behavior of the annotated property.")
+ .WriteLine("/// ")
+ .WriteLine("/// ")
+ .WriteLine($"/// {AttributeClassName} has a number of uses:")
+ .WriteLine("/// ")
+ .WriteLine("/// By default properties with same name will get mapped. This attribute allows the names to be different. ")
+ .WriteLine("/// Indicates that a property should be mapped when member serialization is set to opt-in. ")
+ .WriteLine("///
")
+ .WriteLine("/// ");
+ }
+
+ builder
+ .WriteLine("[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = true)]")
+ .WriteLine($"public sealed class {AttributeClassName} : Attribute")
+ .WriteOpeningBracket();
+
+ if (options.GenerateXmlDocument)
+ {
+ builder
+ .WriteLine("/// ")
+ .WriteLine("/// Gets or sets the property name of the object to mapping from.")
+ .WriteLine("/// ");
+ }
+
+ builder
+ .WriteLine($"public string{options.NullableReferenceSyntax} {SourceMemberNameFieldOrPropertyName} {{ get; set; }}")
+ .WriteClosingBracket() // class
+ .WriteClosingBracket(); // namespace
+
+
+ return new(builder.ToString(), $"{AttributeClassName}.g.cs");
+ }
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/src/BlueWest.MapTo/Sources/ITypeConverterSource.cs b/include/BlueWest.MapTo/src/BlueWest.MapTo/Sources/ITypeConverterSource.cs
new file mode 100644
index 0000000..3bb41d5
--- /dev/null
+++ b/include/BlueWest.MapTo/src/BlueWest.MapTo/Sources/ITypeConverterSource.cs
@@ -0,0 +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("/// ")
+ .WriteLine("/// Converts the value of to .")
+ .WriteLine("/// ")
+ .WriteLine("/// The type to convert from. ")
+ .WriteLine("/// The type to convert to. ");
+ }
+
+ builder
+ .WriteLine($"public interface {InterfaceName}")
+ .WriteOpeningBracket();
+
+ if (options.GenerateXmlDocument)
+ {
+ builder
+ .WriteLine("/// ")
+ .WriteLine("/// Converts the value of object to .")
+ .WriteLine("/// ")
+ .WriteLine("/// The to convert.")
+ .WriteLine($"/// The parameter list passed to the ")
+ .WriteLine("/// object. ");
+ }
+
+ 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()}>";
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/src/BlueWest.MapTo/Sources/IgnoreMemberAttributeSource.cs b/include/BlueWest.MapTo/src/BlueWest.MapTo/Sources/IgnoreMemberAttributeSource.cs
new file mode 100644
index 0000000..502dfa4
--- /dev/null
+++ b/include/BlueWest.MapTo/src/BlueWest.MapTo/Sources/IgnoreMemberAttributeSource.cs
@@ -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("/// ")
+ .WriteLine("/// Specifies that the annotated property should be excluded.")
+ .WriteLine("/// ");
+ }
+
+ 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");
+ }
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/src/BlueWest.MapTo/Sources/JsonExtensionAttributeSource.cs b/include/BlueWest.MapTo/src/BlueWest.MapTo/Sources/JsonExtensionAttributeSource.cs
new file mode 100644
index 0000000..d556fe5
--- /dev/null
+++ b/include/BlueWest.MapTo/src/BlueWest.MapTo/Sources/JsonExtensionAttributeSource.cs
@@ -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("/// ")
+ .WriteLine("/// Specifies that the annotated class has a json extension.")
+ .WriteLine("/// ");
+ }
+
+ 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");
+ }
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/src/BlueWest.MapTo/Sources/MapClassSource.cs b/include/BlueWest.MapTo/src/BlueWest.MapTo/Sources/MapClassSource.cs
new file mode 100644
index 0000000..437926b
--- /dev/null
+++ b/include/BlueWest.MapTo/src/BlueWest.MapTo/Sources/MapClassSource.cs
@@ -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");
+ }
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/src/BlueWest.MapTo/Sources/MapFromAttributeSource.cs b/include/BlueWest.MapTo/src/BlueWest.MapTo/Sources/MapFromAttributeSource.cs
new file mode 100644
index 0000000..70c48ef
--- /dev/null
+++ b/include/BlueWest.MapTo/src/BlueWest.MapTo/Sources/MapFromAttributeSource.cs
@@ -0,0 +1,65 @@
+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("/// ")
+ .WriteLine("/// Specifies that the annotated class can be mapped from the provided .")
+ .WriteLine("/// ");
+ }
+
+ builder
+ .WriteLine("[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]")
+ .WriteLine($"public sealed class {AttributeName}Attribute : Attribute")
+ .WriteOpeningBracket();
+
+ if (options.GenerateXmlDocument)
+ {
+ builder
+ .WriteLine("/// ")
+ .WriteLine($"/// Initializes a new instance of the class with the specified .")
+ .WriteLine("/// ")
+ .WriteLine("/// The type of to map from.");
+ }
+
+ builder
+ .WriteLine($"public {AttributeName}Attribute(Type sourceType)")
+ .WriteOpeningBracket()
+ .WriteLine("SourceType = sourceType;")
+ .WriteClosingBracket()
+ .WriteLine();
+
+ if (options.GenerateXmlDocument)
+ {
+ builder
+ .WriteLine("/// ")
+ .WriteLine("/// Gets the type to map from.")
+ .WriteLine("/// ");
+ }
+
+ builder
+ .WriteLine("public Type SourceType { get; }")
+ .WriteClosingBracket() // class
+ .WriteClosingBracket(); // namespace
+
+ return new(builder.ToString(), $"{AttributeName}Attribute.g.cs");
+ }
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/src/BlueWest.MapTo/Sources/MapPropertyAttributeSource.cs b/include/BlueWest.MapTo/src/BlueWest.MapTo/Sources/MapPropertyAttributeSource.cs
new file mode 100644
index 0000000..4c18cae
--- /dev/null
+++ b/include/BlueWest.MapTo/src/BlueWest.MapTo/Sources/MapPropertyAttributeSource.cs
@@ -0,0 +1,60 @@
+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("/// Specifies the mapping behavior of the annotated property.")
+ .WriteLine("/// ")
+ .WriteLine("/// ")
+ .WriteLine($"/// {AttributeClassName} has a number of uses:")
+ .WriteLine("/// ")
+ .WriteLine("/// By default properties with same name will get mapped. This attribute allows the names to be different. ")
+ .WriteLine("/// Indicates that a property should be mapped when member serialization is set to opt-in. ")
+ .WriteLine("///
")
+ .WriteLine("/// ");
+ }
+
+ builder
+ .WriteLine("[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = true)]")
+ .WriteLine($"public sealed class {AttributeClassName} : Attribute")
+ .WriteOpeningBracket();
+
+ if (options.GenerateXmlDocument)
+ {
+ builder
+ .WriteLine("/// ")
+ .WriteLine("/// Gets or sets the property name of the object to mapping from.")
+ .WriteLine("/// ");
+ }
+
+ builder
+ .WriteLine($"public string{options.NullableReferenceSyntax} {SourcePropertyNamePropertyName} {{ get; set; }}")
+ .WriteClosingBracket() // class
+ .WriteClosingBracket(); // namespace
+
+
+ return new(builder.ToString(), $"{AttributeClassName}.g.cs");
+ }
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/src/BlueWest.MapTo/Sources/MapRecordSource.cs b/include/BlueWest.MapTo/src/BlueWest.MapTo/Sources/MapRecordSource.cs
new file mode 100644
index 0000000..baa48b5
--- /dev/null
+++ b/include/BlueWest.MapTo/src/BlueWest.MapTo/Sources/MapRecordSource.cs
@@ -0,0 +1,191 @@
+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("/// ")
+ .WriteLine($"/// Initializes a new instance of the class")
+ .WriteLine($"/// using the property values from the specified .")
+ .WriteLine("/// ")
+ .WriteLine($"/// {sourceClassParameterName} is null ");
+ }
+
+ 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.SourceProperties.Length; i++)
+ {
+ var property = model.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 < model.SourceProperties.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("/// ")
+ .WriteLine($"/// Creates a new instance of and sets its participating properties")
+ .WriteLine($"/// using the property values from .")
+ .WriteLine("/// ")
+ .WriteLine($"/// The instance of to use as source.")
+ .WriteLine(
+ $"/// A new instance of -or- null if is null . ");
+ }
+
+ 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();
+ }
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/src/BlueWest.MapTo/Sources/MapStructSource.cs b/include/BlueWest.MapTo/src/BlueWest.MapTo/Sources/MapStructSource.cs
new file mode 100644
index 0000000..5a8b60e
--- /dev/null
+++ b/include/BlueWest.MapTo/src/BlueWest.MapTo/Sources/MapStructSource.cs
@@ -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");
+ }
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/src/BlueWest.MapTo/Sources/MapTypeConverterAttributeSource.cs b/include/BlueWest.MapTo/src/BlueWest.MapTo/Sources/MapTypeConverterAttributeSource.cs
new file mode 100644
index 0000000..6d2a6a3
--- /dev/null
+++ b/include/BlueWest.MapTo/src/BlueWest.MapTo/Sources/MapTypeConverterAttributeSource.cs
@@ -0,0 +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("/// ")
+ .WriteLine("/// Specifies what type to use as a converter for the property this attribute is bound to.")
+ .WriteLine("/// ");
+ }
+
+ builder
+ .WriteLine("[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false)]")
+ .WriteLine($"public sealed class {AttributeClassName} : Attribute")
+ .WriteOpeningBracket();
+
+ if (options.GenerateXmlDocument)
+ {
+ builder
+ .WriteLine("/// ")
+ .WriteLine($"/// Initializes a new instance of .")
+ .WriteLine("/// ")
+ .WriteLine($"/// The to be used to convert the source type.")
+ .WriteLine("/// The list of parameters to pass to the during the type conversion.");
+ }
+
+ 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("/// ")
+ .WriteLine($"/// Gets or sets the to be used to convert the source type.")
+ .WriteLine("/// ");
+ }
+
+ builder
+ .WriteLine($"public Type {ConverterPropertyName} {{ get; }}")
+ .WriteLine();
+
+ if (options.GenerateXmlDocument)
+ {
+ builder
+ .WriteLine("/// ")
+ .WriteLine($"/// Gets the list of parameters to pass to the during the type conversion.")
+ .WriteLine("/// ");
+ }
+
+ builder
+ .WriteLine($"public object[]{options.NullableReferenceSyntax} {ConverterParametersPropertyName} {{ get; }}")
+ .WriteClosingBracket()
+ .WriteClosingBracket();
+
+ return new(builder.ToString(), $"{AttributeClassName}.g.cs");
+ }
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/src/BlueWest.MapTo/Sources/MappingContextSource.cs b/include/BlueWest.MapTo/src/BlueWest.MapTo/Sources/MappingContextSource.cs
new file mode 100644
index 0000000..43dbc73
--- /dev/null
+++ b/include/BlueWest.MapTo/src/BlueWest.MapTo/Sources/MappingContextSource.cs
@@ -0,0 +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 { "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 _cache;")
+ .WriteLine()
+
+ // Constructor
+ .WriteLine($"internal {ClassName}()")
+ .WriteOpeningBracket()
+ .WriteLine("_cache = new Dictionary(1);")
+ .WriteClosingBracket()
+ .WriteLine()
+
+ // Factory
+ .WriteLine($"internal static TMapped {FactoryMethodName}(TOriginal original)")
+ .WriteOpeningBracket()
+ .WriteLine("if (original == null) throw new ArgumentNullException(nameof(original));")
+ .WriteLine()
+ .WriteLine("var context = new MappingContext();")
+ .WriteLine("var mapped = context.MapFromWithContext(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 original)")
+ .WriteOpeningBracket()
+ .WriteLine("if (original == null)")
+ .WriteOpeningBracket()
+ .WriteLine("return default(TMapped);")
+ .WriteClosingBracket()
+ .WriteLine()
+ .WriteLine("if (!TryGetValue(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 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 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");
+ }
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/src/BlueWest.MapTo/Sources/ReadOnlyPropertyAttributeSource.cs b/include/BlueWest.MapTo/src/BlueWest.MapTo/Sources/ReadOnlyPropertyAttributeSource.cs
new file mode 100644
index 0000000..babb02f
--- /dev/null
+++ b/include/BlueWest.MapTo/src/BlueWest.MapTo/Sources/ReadOnlyPropertyAttributeSource.cs
@@ -0,0 +1,36 @@
+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("/// ")
+ .WriteLine("/// Specifies that the annotated property should be excluded.")
+ .WriteLine("/// ");
+ }
+
+ builder
+ .WriteLine("[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]")
+ .WriteLine($"public sealed class {AttributeClassName} : Attribute {{ }}")
+ .WriteClosingBracket();
+
+ return new(builder.ToString(), $"{AttributeClassName}.g.cs");
+ }
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/src/BlueWest.MapTo/Sources/SourceBuilder.cs b/include/BlueWest.MapTo/src/BlueWest.MapTo/Sources/SourceBuilder.cs
new file mode 100644
index 0000000..cb270c6
--- /dev/null
+++ b/include/BlueWest.MapTo/src/BlueWest.MapTo/Sources/SourceBuilder.cs
@@ -0,0 +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));
+ }
+
+ ///
+ 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 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;
+ }
+
+ ///
+ public override string ToString() => _writer.ToString();
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/src/BlueWest.MapTo/Sources/UseUpdateAttributeSource.cs b/include/BlueWest.MapTo/src/BlueWest.MapTo/Sources/UseUpdateAttributeSource.cs
new file mode 100644
index 0000000..befca4a
--- /dev/null
+++ b/include/BlueWest.MapTo/src/BlueWest.MapTo/Sources/UseUpdateAttributeSource.cs
@@ -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("/// ")
+ .WriteLine("/// Specifies that the annotated class can be updatable.")
+ .WriteLine("/// ");
+ }
+
+ 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");
+ }
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/src/BlueWest.MapTo/StructMappingContext.cs b/include/BlueWest.MapTo/src/BlueWest.MapTo/StructMappingContext.cs
new file mode 100644
index 0000000..707bdc9
--- /dev/null
+++ b/include/BlueWest.MapTo/src/BlueWest.MapTo/StructMappingContext.cs
@@ -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 GetSourceMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
+ {
+ var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType().ToArray();
+
+ return typeSymbol
+ .GetAllMembers()
+ .OfType()
+ .Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol))
+ .Select(property => MapField(sourceTypeSymbol, sourceProperties, property))
+ .Where(mappedProperty => mappedProperty is not null)
+ .ToImmutableArray()!;
+ }
+
+ protected override ImmutableArray GetSourceMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool hasInheritedClass)
+ {
+ var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType().ToArray();
+
+ return typeSymbol
+ .GetAllMembers()
+ .OfType()
+ .Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol))
+ .Select(property => MapProperty(sourceTypeSymbol, sourceProperties, property))
+ .Where(mappedProperty => mappedProperty is not null)
+ .ToImmutableArray()!;
+ }
+
+ protected override ImmutableArray GetTypeMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
+ {
+ var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType().ToArray();
+
+ return sourceTypeSymbol
+ .GetAllMembers()
+ .OfType()
+ .Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol))
+ .Select(property => MapFieldSimple(typeSymbol, property))
+ .Where(mappedProperty => mappedProperty is not null)
+ .ToImmutableArray()!;
+ }
+
+ protected override ImmutableArray GetTypeMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool hasInheritedClass)
+ {
+ var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType().ToArray();
+
+ return sourceTypeSymbol
+ .GetAllMembers()
+ .OfType()
+ .Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol))
+ .Select(property => MapPropertySimple(typeSymbol, property))
+ .Where(mappedProperty => mappedProperty is not null)
+ .ToImmutableArray()!;
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/test/BlueWest.MapTo.Tests/BlueWest.MapTo.Tests.csproj b/include/BlueWest.MapTo/test/BlueWest.MapTo.Tests/BlueWest.MapTo.Tests.csproj
new file mode 100644
index 0000000..ec7dd15
--- /dev/null
+++ b/include/BlueWest.MapTo/test/BlueWest.MapTo.Tests/BlueWest.MapTo.Tests.csproj
@@ -0,0 +1,42 @@
+
+
+
+ net6.0
+
+ false
+
+ enable
+
+ MapTo.Tests
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+ 3.5.109
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
diff --git a/include/BlueWest.MapTo/test/BlueWest.MapTo.Tests/Common.cs b/include/BlueWest.MapTo/test/BlueWest.MapTo.Tests/Common.cs
new file mode 100644
index 0000000..34270e4
--- /dev/null
+++ b/include/BlueWest.MapTo/test/BlueWest.MapTo.Tests/Common.cs
@@ -0,0 +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 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? employeeClassSource = null,
+ Func? managerClassSource = null,
+ Func? employeeViewModelSource = null,
+ Func? 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 Employees { get; set; } = Array.Empty();
+ }
+}
+".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 Employees { get; set; } = Array.Empty();
+ }
+}
+".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 Employees { get; set; } = Array.Empty();
+ }
+}".Trim();
+ }
+
+ internal static PropertyDeclarationSyntax GetPropertyDeclarationSyntax(SyntaxTree syntaxTree, string targetPropertyName, string targetClass = "Foo")
+ {
+ return syntaxTree.GetRoot()
+ .DescendantNodes()
+ .OfType()
+ .Single(c => c.Identifier.ValueText == targetClass)
+ .DescendantNodes()
+ .OfType()
+ .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? PropertyBuilder = null,
+ Action? SourcePropertyBuilder = null,
+ IEnumerable? Usings = null);
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/test/BlueWest.MapTo.Tests/CompilerServices/IsExternalInit.cs b/include/BlueWest.MapTo/test/BlueWest.MapTo.Tests/CompilerServices/IsExternalInit.cs
new file mode 100644
index 0000000..72b53ee
--- /dev/null
+++ b/include/BlueWest.MapTo/test/BlueWest.MapTo.Tests/CompilerServices/IsExternalInit.cs
@@ -0,0 +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
+{
+ ///
+ /// Reserved to be used by the compiler for tracking metadata.
+ /// This class should not be used by developers in source code.
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ internal static class IsExternalInit { }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/test/BlueWest.MapTo.Tests/Extensions/RoslynExtensions.cs b/include/BlueWest.MapTo/test/BlueWest.MapTo.Tests/Extensions/RoslynExtensions.cs
new file mode 100644
index 0000000..cddafeb
--- /dev/null
+++ b/include/BlueWest.MapTo/test/BlueWest.MapTo.Tests/Extensions/RoslynExtensions.cs
@@ -0,0 +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();
+ }));
+ }
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/test/BlueWest.MapTo.Tests/Extensions/ShouldlyExtensions.cs b/include/BlueWest.MapTo/test/BlueWest.MapTo.Tests/Extensions/ShouldlyExtensions.cs
new file mode 100644
index 0000000..2b9dd87
--- /dev/null
+++ b/include/BlueWest.MapTo/test/BlueWest.MapTo.Tests/Extensions/ShouldlyExtensions.cs
@@ -0,0 +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, 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, 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 diagnostics, Compilation? compilation = null, IEnumerable? 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 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);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/test/BlueWest.MapTo.Tests/IgnorePropertyAttributeTests.cs b/include/BlueWest.MapTo/test/BlueWest.MapTo.Tests/IgnorePropertyAttributeTests.cs
new file mode 100644
index 0000000..cbbad37
--- /dev/null
+++ b/include/BlueWest.MapTo/test/BlueWest.MapTo.Tests/IgnorePropertyAttributeTests.cs
@@ -0,0 +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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/test/BlueWest.MapTo.Tests/Infrastructure/CSharpGenerator.cs b/include/BlueWest.MapTo/test/BlueWest.MapTo.Tests/Infrastructure/CSharpGenerator.cs
new file mode 100644
index 0000000..872f37c
--- /dev/null
+++ b/include/BlueWest.MapTo/test/BlueWest.MapTo.Tests/Infrastructure/CSharpGenerator.cs
@@ -0,0 +1,64 @@
+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 diagnostics) GetOutputCompilation(
+ string source,
+ bool assertCompilation = false,
+ IDictionary? analyzerConfigOptions = null,
+ NullableContextOptions nullableContextOptions = NullableContextOptions.Disable,
+ LanguageVersion languageVersion = LanguageVersion.CSharp7_3) =>
+ GetOutputCompilation(
+ new[] { source },
+ assertCompilation,
+ analyzerConfigOptions,
+ nullableContextOptions,
+ languageVersion);
+
+ internal static (Compilation compilation, ImmutableArray diagnostics) GetOutputCompilation(
+ IEnumerable sources,
+ bool assertCompilation = false,
+ IDictionary? 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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/test/BlueWest.MapTo.Tests/Infrastructure/TestAnalyzerConfigOptions.cs b/include/BlueWest.MapTo/test/BlueWest.MapTo.Tests/Infrastructure/TestAnalyzerConfigOptions.cs
new file mode 100644
index 0000000..5cfa68f
--- /dev/null
+++ b/include/BlueWest.MapTo/test/BlueWest.MapTo.Tests/Infrastructure/TestAnalyzerConfigOptions.cs
@@ -0,0 +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 _backing;
+
+ public TestAnalyzerConfigOptions(IDictionary? properties)
+ {
+ _backing = properties?.ToImmutableDictionary(KeyComparer) ?? ImmutableDictionary.Create(KeyComparer);
+ }
+
+ public override bool TryGetValue(string key, out string? value) => _backing.TryGetValue(key, out value);
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/test/BlueWest.MapTo.Tests/Infrastructure/TestAnalyzerConfigOptionsProvider.cs b/include/BlueWest.MapTo/test/BlueWest.MapTo.Tests/Infrastructure/TestAnalyzerConfigOptionsProvider.cs
new file mode 100644
index 0000000..9c80539
--- /dev/null
+++ b/include/BlueWest.MapTo/test/BlueWest.MapTo.Tests/Infrastructure/TestAnalyzerConfigOptionsProvider.cs
@@ -0,0 +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? options)
+ {
+ GlobalOptions = new TestAnalyzerConfigOptions(options);
+ }
+
+ ///
+ public override AnalyzerConfigOptions GlobalOptions { get; }
+
+ ///
+ public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) => throw new NotImplementedException();
+
+ ///
+ public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) => throw new NotImplementedException();
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/test/BlueWest.MapTo.Tests/MapPropertyTests.cs b/include/BlueWest.MapTo/test/BlueWest.MapTo.Tests/MapPropertyTests.cs
new file mode 100644
index 0000000..ced2108
--- /dev/null
+++ b/include/BlueWest.MapTo/test/BlueWest.MapTo.Tests/MapPropertyTests.cs
@@ -0,0 +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 MapPropertyWithImplicitConversionFoundData => new List
+ {
+ 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 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 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).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 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 InnerClasses);
+}
+",
+ @"
+ private protected OuterClass(MappingContext context, Test.OuterClass outerClass)
+ : this(Id: outerClass.Id, SomeDate: outerClass.SomeDate, InnerClasses: outerClass.InnerClasses.Select(context.MapFromWithContext).ToList())
+ {
+ if (context == null) throw new ArgumentNullException(nameof(context));
+ if (outerClass == null) throw new ArgumentNullException(nameof(outerClass));
+
+ context.Register(outerClass, this);
+ }
+",
+ LanguageVersion.CSharp9
+ }
+ };
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/test/BlueWest.MapTo.Tests/MapToTests.cs b/include/BlueWest.MapTo/test/BlueWest.MapTo.Tests/MapToTests.cs
new file mode 100644
index 0000000..3ad3fe9
--- /dev/null
+++ b/include/BlueWest.MapTo/test/BlueWest.MapTo.Tests/MapToTests.cs
@@ -0,0 +1,601 @@
+using System.Collections.Generic;
+using System.Linq;
+using MapTo.Sources;
+using MapTo.Tests.Extensions;
+using MapTo.Tests.Infrastructure;
+using Shouldly;
+using Xunit;
+using static MapTo.Extensions.GeneratorExecutionContextExtensions;
+using static MapTo.Tests.Common;
+
+namespace MapTo.Tests
+{
+ public class MapToTests
+ {
+ private static readonly string ExpectedAttribute = $@"{Constants.GeneratedFilesHeader}
+using System;
+
+namespace MapTo
+{{
+ [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
+ public sealed class MapFromAttribute : Attribute
+ {{
+ public MapFromAttribute(Type sourceType)
+ {{
+ SourceType = sourceType;
+ }}
+
+ public Type SourceType {{ get; }}
+ }}
+}}";
+
+ [Fact]
+ public void VerifyMapToAttribute()
+ {
+ // Arrange
+ const string source = "";
+
+ // Act
+ var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
+
+ // Assert
+ diagnostics.ShouldBeSuccessful();
+ compilation.SyntaxTrees.ShouldContainSource(MapFromAttributeSource.AttributeClassName, ExpectedAttribute);
+ }
+
+ [Fact]
+ public void When_FoundMatchingPropertyNameWithDifferentTypes_Should_ReportError()
+ {
+ // Arrange
+ var source = GetSourceText(new SourceGeneratorOptions(
+ true,
+ PropertyBuilder: builder => { builder.WriteLine("public string Prop4 { get; set; }"); },
+ SourcePropertyBuilder: builder => builder.WriteLine("public int Prop4 { get; set; }")));
+
+ // Act
+ var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
+
+ // Assert
+ var expectedError = DiagnosticsFactory.NoMatchingPropertyTypeFoundError(GetSourcePropertySymbol("Prop4", compilation));
+
+ diagnostics.ShouldNotBeSuccessful(expectedError);
+ }
+
+ [Fact]
+ public void When_MappingsModifierOptionIsSetToInternal_Should_GenerateThoseMethodsWithInternalAccessModifier()
+ {
+ // Arrange
+ var source = GetSourceText();
+ var configOptions = new Dictionary
+ {
+ [GetBuildPropertyName(nameof(SourceGenerationOptions.GeneratedMethodsAccessModifier))] = "Internal",
+ [GetBuildPropertyName(nameof(SourceGenerationOptions.GenerateXmlDocument))] = "false"
+ };
+
+ var expectedExtension = @"
+ internal static partial class BazToFooExtensions
+ {
+ internal static Foo ToFoo(this Test.Models.Baz baz)
+ {
+ return baz == null ? null : new Foo(baz);
+ }
+ }".Trim();
+
+ var expectedFactory = @"
+ internal static Foo From(Test.Models.Baz baz)
+ {
+ return baz == null ? null : MappingContext.Create(baz);
+ }".Trim();
+
+ // Act
+ var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: configOptions);
+
+ // Assert
+ diagnostics.ShouldBeSuccessful();
+
+ var syntaxTree = compilation.SyntaxTrees.Last().ToString();
+ syntaxTree.ShouldContain(expectedFactory);
+ syntaxTree.ShouldContain(expectedExtension);
+ }
+
+ [Fact]
+ public void When_MapToAttributeFound_Should_GenerateTheClass()
+ {
+ // Arrange
+ const string source = @"
+using MapTo;
+
+namespace Test
+{
+ [MapFrom(typeof(Baz))]
+ public partial class Foo
+ {
+ public int Prop1 { get; set; }
+ }
+
+ public class Baz
+ {
+ public int Prop1 { get; set; }
+ }
+}
+";
+
+ const string expectedResult = @"
+//
+using MapTo;
+using System;
+
+namespace Test
+{
+ partial class Foo
+ {
+ public Foo(Test.Baz baz)
+ : this(new MappingContext(), baz) { }
+
+ private protected Foo(MappingContext context, Test.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;
+ }
+";
+
+ // Act
+ var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
+
+ // Assert
+ diagnostics.ShouldBeSuccessful();
+ compilation.SyntaxTrees.Last().ToString().ShouldStartWith(expectedResult.Trim());
+ }
+
+ [Fact]
+ public void When_MapToAttributeFoundWithoutMatchingProperties_Should_ReportError()
+ {
+ // Arrange
+ const string source = @"
+using MapTo;
+
+namespace Test
+{
+ [MapFrom(typeof(Baz))]
+ public partial class Foo { }
+
+ public class Baz { public int Prop1 { get; set; } }
+}
+";
+
+ // Act
+ var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source);
+
+ // Assert
+ var fooType = compilation.GetTypeByMetadataName("Test.Foo");
+ fooType.ShouldNotBeNull();
+
+ var bazType = compilation.GetTypeByMetadataName("Test.Baz");
+ bazType.ShouldNotBeNull();
+
+ var expectedDiagnostic = DiagnosticsFactory.NoMatchingPropertyFoundError(fooType.Locations.Single(), fooType, bazType);
+ var error = diagnostics.FirstOrDefault(d => d.Id == expectedDiagnostic.Id);
+ error.ShouldNotBeNull();
+ }
+
+ [Fact]
+ public void When_MapToAttributeWithNamespaceFound_Should_GenerateTheClass()
+ {
+ // Arrange
+ const string source = @"
+namespace Test
+{
+ [MapTo.MapFrom(typeof(Baz))]
+ public partial class Foo { public int Prop1 { get; set; } }
+
+ public class Baz { public int Prop1 { get; set; } }
+}
+";
+
+ const string expectedResult = @"
+//
+using MapTo;
+using System;
+
+namespace Test
+{
+ partial class Foo
+ {
+ public Foo(Test.Baz baz)
+ : this(new MappingContext(), baz) { }
+
+ private protected Foo(MappingContext context, Test.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;
+ }
+";
+
+ // Act
+ var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
+
+ // Assert
+ diagnostics.ShouldBeSuccessful();
+ compilation.SyntaxTrees.Last().ToString().ShouldStartWith(expectedResult.Trim());
+ }
+
+ [Fact]
+ public void When_NoMapToAttributeFound_Should_GenerateOnlyTheAttribute()
+ {
+ // Arrange
+ const string source = "";
+ var expectedTypes = new[]
+ {
+ //IgnorePropertyAttributeSource.AttributeName,
+ MapFromAttributeSource.AttributeName,
+ ITypeConverterSource.InterfaceName,
+ MapPropertyAttributeSource.AttributeName
+ };
+
+ // Act
+ var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source);
+
+ // Assert
+ diagnostics.ShouldBeSuccessful();
+ compilation.SyntaxTrees
+ .Select(s => s.ToString())
+ .Where(s => !string.IsNullOrWhiteSpace(s.ToString()))
+ .All(s => expectedTypes.Any(s.Contains))
+ .ShouldBeTrue();
+ }
+
+ [Fact]
+ public void When_SourceTypeHasDifferentNamespace_Should_NotAddToUsings()
+ {
+ // Arrange
+ var source = GetSourceText(new SourceGeneratorOptions(SourceClassNamespace: "Bazaar"));
+
+ const string expectedResult = @"
+//
+using MapTo;
+using System;
+
+namespace Test
+{
+";
+
+ // Act
+ var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source);
+
+ // Assert
+ diagnostics.ShouldBeSuccessful();
+ compilation.SyntaxTrees.Last().ToString().ShouldStartWith(expectedResult.Trim());
+ }
+
+ [Fact]
+ public void When_SourceTypeHasMatchingProperties_Should_CreateConstructorAndAssignSrcToDest()
+ {
+ // Arrange
+ var source = GetSourceText();
+
+ const string 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;
+ }
+";
+
+ // Act
+ var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
+
+ // Assert
+ diagnostics.ShouldBeSuccessful();
+ compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult.Trim());
+ }
+
+ [Fact]
+ public void When_SourceTypeHasMatchingProperties_Should_CreateFromStaticMethod()
+ {
+ // Arrange
+ var source = GetSourceText();
+
+ const string expectedResult = @"
+ public static Foo From(Test.Models.Baz baz)
+ {
+ return baz == null ? null : MappingContext.Create(baz);
+ }
+";
+
+ // Act
+ var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source);
+
+ // Assert
+ diagnostics.ShouldBeSuccessful();
+ compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult.Trim());
+ }
+
+ [Fact]
+ public void When_SourceTypeHasMatchingProperties_Should_GenerateToExtensionMethodOnSourceType()
+ {
+ // Arrange
+ var source = GetSourceText();
+
+ const string expectedResult = @"
+ public static partial class BazToFooExtensions
+ {
+ public static Foo ToFoo(this Test.Models.Baz baz)
+ {
+ return baz == null ? null : new Foo(baz);
+ }
+ }
+";
+
+ // Act
+ var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
+
+ // Assert
+ diagnostics.ShouldBeSuccessful();
+ compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult.Trim());
+ }
+
+ [Fact]
+ public void When_HasNestedObjectPropertyTypeHasMapFromAttribute_Should_UseContinueToMap()
+ {
+ // Arrange
+ var source = GetSourceText(new SourceGeneratorOptions(
+ SourceClassNamespace: "Test",
+ PropertyBuilder: b => b.WriteLine("public B InnerProp1 { get; }"),
+ SourcePropertyBuilder: b => b.WriteLine("public A InnerProp1 { get; }")));
+
+ source += @"
+namespace Test
+{
+ public class A { public int Prop1 { get; } }
+
+ [MapTo.MapFrom(typeof(A))]
+ public partial class B { public int Prop1 { get; }}
+}
+".Trim();
+
+ var expectedResult = @"
+ partial class Foo
+ {
+ public Foo(Test.Baz baz)
+ : this(new MappingContext(), baz) { }
+
+ private protected Foo(MappingContext context, Test.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;
+ InnerProp1 = context.MapFromWithContext(baz.InnerProp1);
+ }
+".Trim();
+
+ // Act
+ var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
+
+ // Assert
+ diagnostics.ShouldBeSuccessful();
+ compilation.SyntaxTrees.ToArray()[^2].ShouldContainPartialSource(expectedResult);
+ }
+
+ [Fact]
+ public void When_HasNestedObjectPropertyTypeDoesNotHaveMapFromAttribute_Should_ReportError()
+ {
+ // Arrange
+ var source = GetSourceText(new SourceGeneratorOptions(
+ SourceClassNamespace: "Test",
+ PropertyBuilder: b => b.WriteLine("public FooInner1 InnerProp1 { get; }"),
+ SourcePropertyBuilder: b => b.WriteLine("public BazInner1 InnerProp1 { get; }")));
+
+ source += @"
+namespace Test
+{
+ public class FooInner1 { public int Prop1 { get; } }
+
+ public partial class BazInner1 { public int Prop1 { get; }}
+}
+".Trim();
+
+ // Act
+ var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
+
+ // Assert
+ var expectedError = DiagnosticsFactory.NoMatchingPropertyTypeFoundError(GetSourcePropertySymbol("InnerProp1", compilation));
+ diagnostics.ShouldNotBeSuccessful(expectedError);
+ }
+
+ [Fact]
+ public void When_HasNestedObjectPropertyTypeHasMapFromAttributeToDifferentType_Should_ReportError()
+ {
+ // Arrange
+ var source = GetSourceText(new SourceGeneratorOptions(
+ SourceClassNamespace: "Test",
+ PropertyBuilder: b => b.WriteLine("public FooInner1 InnerProp1 { get; }"),
+ SourcePropertyBuilder: b => b.WriteLine("public BazInner1 InnerProp1 { get; }")));
+
+ source += @"
+namespace Test
+{
+ public class FooInner1 { public int Prop1 { get; } }
+
+ public class FooInner2 { public int Prop1 { get; } }
+
+ [MapTo.MapFrom(typeof(FooInner2))]
+ public partial class BazInner1 { public int Prop1 { get; }}
+}
+".Trim();
+
+ // Act
+ var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
+
+ // Assert
+ var expectedError = DiagnosticsFactory.NoMatchingPropertyTypeFoundError(GetSourcePropertySymbol("InnerProp1", compilation));
+ diagnostics.ShouldNotBeSuccessful(expectedError);
+ }
+
+ [Fact]
+ public void When_SourceTypeEnumerableProperties_Should_CreateConstructorAndAssignSrcToDest()
+ {
+ // Arrange
+ var source = GetSourceText(new SourceGeneratorOptions(
+ Usings: new[] { "System.Collections.Generic"},
+ PropertyBuilder: builder => builder.WriteLine("public IEnumerable Prop4 { get; }"),
+ SourcePropertyBuilder: builder => builder.WriteLine("public IEnumerable Prop4 { get; }")));
+
+ const string 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;
+ }
+";
+
+ // Act
+ var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
+
+ // Assert
+ diagnostics.ShouldBeSuccessful();
+ compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult.Trim());
+ }
+
+ [Fact]
+ public void When_DestinationTypeHasBaseClass_Should_CallBaseConstructor()
+ {
+ // Arrange
+ var sources = GetEmployeeManagerSourceText();
+
+ const string expectedResult = @"
+private protected ManagerViewModel(MappingContext context, Test.Data.Models.Manager manager) : base(context, manager)
+{
+ if (context == null) throw new ArgumentNullException(nameof(context));
+ if (manager == null) throw new ArgumentNullException(nameof(manager));
+
+ context.Register(manager, this);
+
+ Level = manager.Level;
+ Employees = manager.Employees.Select(context.MapFromWithContext).ToList();
+}
+";
+
+ // Act
+ var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(sources, analyzerConfigOptions: DefaultAnalyzerOptions);
+
+ // Assert
+ diagnostics.ShouldBeSuccessful();
+ compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult);
+ }
+
+ [Fact]
+ public void When_SourceTypeHasEnumerablePropertiesWithMapFromAttribute_Should_CreateANewEnumerableWithMappedObjects()
+ {
+ // Arrange
+ var sources = GetEmployeeManagerSourceText();
+
+ const string expectedResult = @"
+//
+using MapTo;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Test.ViewModels
+{
+ partial class ManagerViewModel
+ {
+ public ManagerViewModel(Test.Data.Models.Manager manager)
+ : this(new MappingContext(), manager) { }
+
+ private protected ManagerViewModel(MappingContext context, Test.Data.Models.Manager manager) : base(context, manager)
+ {
+ if (context == null) throw new ArgumentNullException(nameof(context));
+ if (manager == null) throw new ArgumentNullException(nameof(manager));
+
+ context.Register(manager, this);
+
+ Level = manager.Level;
+ Employees = manager.Employees.Select(context.MapFromWithContext).ToList();
+ }
+";
+
+ // Act
+ var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(sources, analyzerConfigOptions: DefaultAnalyzerOptions);
+
+ // Assert
+ diagnostics.ShouldBeSuccessful();
+ compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult);
+ }
+
+ [Fact]
+ public void When_SourceTypeHasEnumerablePropertiesWithMapFromAttributeInDifferentNamespaces_Should_CreateANewEnumerableWithMappedObjectsAndImportNamespace()
+ {
+ // Arrange
+ var sources = GetEmployeeManagerSourceText(useDifferentViewModelNamespace: true);
+
+ const string expectedResult = @"
+using MapTo;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Test.ViewModels2
+{
+ partial class ManagerViewModel
+ {
+ public ManagerViewModel(Test.Data.Models.Manager manager)
+ : this(new MappingContext(), manager) { }
+
+ private protected ManagerViewModel(MappingContext context, Test.Data.Models.Manager manager) : base(context, manager)
+ {
+ if (context == null) throw new ArgumentNullException(nameof(context));
+ if (manager == null) throw new ArgumentNullException(nameof(manager));
+
+ context.Register(manager, this);
+
+ Level = manager.Level;
+ Employees = manager.Employees.Select(context.MapFromWithContext).ToList();
+ }
+";
+
+ // Act
+ var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(sources, analyzerConfigOptions: DefaultAnalyzerOptions);
+
+ // Assert
+ diagnostics.ShouldBeSuccessful();
+ compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult);
+ }
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/test/BlueWest.MapTo.Tests/MapTypeConverterTests.cs b/include/BlueWest.MapTo/test/BlueWest.MapTo.Tests/MapTypeConverterTests.cs
new file mode 100644
index 0000000..fe20ca5
--- /dev/null
+++ b/include/BlueWest.MapTo/test/BlueWest.MapTo.Tests/MapTypeConverterTests.cs
@@ -0,0 +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
+ {{
+ 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
+ {{
+ 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
+ {
+ 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
+ {
+ 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
+ {
+ 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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/test/BlueWest.MapTo.Tests/MappedClassesTests.cs b/include/BlueWest.MapTo/test/BlueWest.MapTo.Tests/MappedClassesTests.cs
new file mode 100644
index 0000000..c89ce35
--- /dev/null
+++ b/include/BlueWest.MapTo/test/BlueWest.MapTo.Tests/MappedClassesTests.cs
@@ -0,0 +1,562 @@
+using System.Collections.Generic;
+using System.Linq;
+using MapTo.Tests.Extensions;
+using MapTo.Tests.Infrastructure;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Shouldly;
+using Xunit;
+using Xunit.Abstractions;
+using static MapTo.Tests.Common;
+
+namespace MapTo.Tests
+{
+ public class MappedClassesTests
+ {
+ private readonly ITestOutputHelper _output;
+
+ public MappedClassesTests(ITestOutputHelper output)
+ {
+ _output = output;
+ }
+
+ [Theory]
+ [MemberData(nameof(SecondaryConstructorCheckData))]
+ public void When_SecondaryConstructorExists_Should_NotGenerateOne(string source, LanguageVersion languageVersion)
+ {
+ // Arrange
+ source = source.Trim();
+
+ // Act
+ var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, languageVersion: languageVersion);
+
+ // Assert
+ diagnostics.ShouldBeSuccessful();
+ compilation
+ .GetGeneratedSyntaxTree("DestinationClass")
+ .ShouldNotBeNull()
+ .GetRoot()
+ .DescendantNodes()
+ .OfType()
+ .Count()
+ .ShouldBe(1);
+ }
+
+ public static IEnumerable SecondaryConstructorCheckData => new List
+ {
+ new object[]
+ {
+ @"
+using MapTo;
+namespace Test.Data.Models
+{
+ public class SourceClass { public string Prop1 { get; set; } }
+
+ [MapFrom(typeof(SourceClass))]
+ public partial class DestinationClass
+ {
+ public DestinationClass(SourceClass source) : this(new MappingContext(), source) { }
+ public string Prop1 { get; set; }
+ }
+}
+",
+ LanguageVersion.CSharp7_3
+ },
+ new object[]
+ {
+ @"
+using MapTo;
+namespace Test.Data.Models
+{
+ public record SourceClass(string Prop1);
+
+ [MapFrom(typeof(SourceClass))]
+ public partial record DestinationClass(string Prop1)
+ {
+ public DestinationClass(SourceClass source) : this(new MappingContext(), source) { }
+ }
+}
+",
+ LanguageVersion.CSharp9
+ }
+ };
+
+ [Theory]
+ [MemberData(nameof(SecondaryCtorWithoutPrivateCtorData))]
+ public void When_SecondaryConstructorExistsButDoNotReferencePrivateConstructor_Should_ReportError(string source, LanguageVersion languageVersion)
+ {
+ // Arrange
+ source = source.Trim();
+
+ // Act
+ var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, languageVersion: languageVersion);
+
+ // Assert
+ var constructorSyntax = compilation.SyntaxTrees
+ .First()
+ .GetRoot()
+ .DescendantNodes()
+ .OfType()
+ .Single();
+
+ diagnostics.ShouldNotBeSuccessful(DiagnosticsFactory.MissingConstructorArgument(constructorSyntax));
+ }
+
+ public static IEnumerable SecondaryCtorWithoutPrivateCtorData => new List
+ {
+ new object[]
+ {
+ @"
+using MapTo;
+namespace Test.Data.Models
+{
+ public class SourceClass { public string Prop1 { get; set; } }
+
+ [MapFrom(typeof(SourceClass))]
+ public partial class DestinationClass
+ {
+ public DestinationClass(SourceClass source) { }
+ public string Prop1 { get; set; }
+ }
+}
+",
+ LanguageVersion.CSharp7_3
+ },
+ new object[]
+ {
+ @"
+using MapTo;
+namespace Test.Data.Models
+{
+ public record SourceClass(string Prop1);
+
+ [MapFrom(typeof(SourceClass))]
+ public partial record DestinationClass(string Prop1)
+ {
+ public DestinationClass(SourceClass source) : this(""invalid"") { }
+ }
+}
+",
+ LanguageVersion.CSharp9
+ }
+ };
+
+ [Fact]
+ public void When_PropertyNameIsTheSameAsClassName_Should_MapAccordingly()
+ {
+ // Arrange
+ var source = @"
+namespace Sale
+{
+ public class Sale { public Sale Prop1 { get; set; } }
+}
+
+namespace SaleModel
+{
+ using MapTo;
+ using Sale;
+
+ [MapFrom(typeof(Sale))]
+ public partial class SaleModel
+ {
+ [MapProperty(SourcePropertyName = nameof(global::Sale.Sale.Prop1))]
+ public Sale Sale { get; set; }
+ }
+}
+".Trim();
+
+ // Act
+ var (_, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
+
+ // Assert
+ diagnostics.ShouldBeSuccessful();
+ }
+
+ [Theory]
+ [MemberData(nameof(SameSourceAndDestinationTypeNameData))]
+ public void When_SourceAndDestinationNamesAreTheSame_Should_MapAccordingly(string source, LanguageVersion languageVersion)
+ {
+ // Arrange
+ source = source.Trim();
+
+ // Act
+ var (_, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, languageVersion: languageVersion);
+
+ // Assert
+ diagnostics.ShouldBeSuccessful();
+ }
+
+ public static IEnumerable SameSourceAndDestinationTypeNameData => new List
+ {
+ new object[]
+ {
+ @"
+namespace Test
+{
+ public class TypeName { public int Prop2 { get; set; } }
+}
+
+namespace Test2
+{
+ using MapTo;
+
+ [MapFrom(typeof(Test.TypeName))]
+ public partial class TypeName
+ {
+ [MapProperty(SourcePropertyName=""Prop2"")]
+ public int Prop1 { get; set; }
+ }
+}",
+ LanguageVersion.CSharp7_3
+ },
+ new object[]
+ {
+ @"
+namespace Test
+{
+ public record TypeName(int Prop2);
+}
+
+namespace Test2
+{
+ using MapTo;
+
+ [MapFrom(typeof(Test.TypeName))]
+ public partial record TypeName([MapProperty(SourcePropertyName=""Prop2"")] int Prop1);
+}",
+ LanguageVersion.CSharp9
+ },
+ new object[]
+ {
+ @"
+namespace Test
+{
+ using System.Collections.Generic;
+
+ public class SourceType2 { public int Id { get; set; } }
+ public class SourceType
+ {
+ public int Id { get; set; }
+ public List Prop1 { get; set; }
+ }
+}
+
+namespace Test2
+{
+ using MapTo;
+ using System.Collections.Generic;
+
+ [MapFrom(typeof(Test.SourceType2))]
+ public partial class SourceType2 { public int Id { get; set; } }
+
+ [MapFrom(typeof(Test.SourceType))]
+ public partial class SourceType
+ {
+ public int Id { get; set; }
+ public IReadOnlyList Prop1 { get; set; }
+ }
+}",
+ LanguageVersion.CSharp7_3
+ },
+ new object[]
+ {
+ @"
+namespace Test
+{
+ using System.Collections.Generic;
+
+ public record SourceType(int Id, List Prop1);
+ public record SourceType2(int Id);
+}
+
+namespace Test2
+{
+ using MapTo;
+ using System.Collections.Generic;
+
+ [MapFrom(typeof(Test.SourceType2))]
+ public partial record SourceType2(int Id);
+
+ [MapFrom(typeof(Test.SourceType))]
+ public partial record SourceType(int Id, IReadOnlyList Prop1);
+}",
+ LanguageVersion.CSharp9
+ },
+ new object[]
+ {
+ @"
+namespace Test
+{
+ using System.Collections.Generic;
+
+ public record SourceType1(int Id);
+ public record SourceType2(int Id, List Prop1);
+}
+
+namespace Test
+{
+ using MapTo;
+ using System.Collections.Generic;
+
+ [MapFrom(typeof(Test.SourceType1))]
+ public partial record SourceType3(int Id);
+
+ [MapFrom(typeof(Test.SourceType2))]
+ public partial record SourceType4(int Id, IReadOnlyList Prop1);
+}",
+ LanguageVersion.CSharp9
+ }
+ };
+
+ [Theory]
+ [MemberData(nameof(VerifyMappedTypesData))]
+ public void VerifyMappedTypes(string[] sources, LanguageVersion languageVersion)
+ {
+ // Arrange
+ // Act
+ var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(sources, analyzerConfigOptions: DefaultAnalyzerOptions, languageVersion: languageVersion);
+
+ // Assert
+ diagnostics.ShouldBeSuccessful();
+ _output.WriteLine(compilation.PrintSyntaxTree());
+ }
+
+ public static IEnumerable VerifyMappedTypesData => new List
+ {
+ new object[] { new[] { MainSourceClass, NestedSourceClass, MainDestinationClass, NestedDestinationClass }, LanguageVersion.CSharp7_3 },
+ new object[] { new[] { MainSourceRecord, NestedSourceRecord, MainDestinationRecord, NestedDestinationRecord }, LanguageVersion.CSharp9 },
+ new object[]
+ {
+ new[]
+ {
+ @"
+namespace Test.Classes.Classes1
+{
+ public class Class1
+ {
+ public int Id { get; set; }
+ public string Name { get; set; }
+ }
+}",
+ @"
+using System;
+using System.Collections.Generic;
+using Test.Classes.Classes1;
+
+namespace Test.Classes.Classes2
+{
+ public class Class2
+ {
+ public int Id { get; set; }
+ public List Genres { get; set; }
+ public DateTime? ReleaseDate { get; set; }
+ }
+}",
+ @"
+using MapTo;
+using System;
+using System.Collections.Generic;
+using TC = Test.Classes;
+
+namespace Tests.Records
+{
+ [MapFrom(typeof(Test.Classes.Classes1.Class1))]
+ public partial record Class1(int Id, string Name);
+
+ [MapFrom(typeof(Test.Classes.Classes2.Class2))]
+ public partial record Class2(int Id, IReadOnlyList Genres);
+}"
+ },
+ LanguageVersion.CSharp9
+ }
+ };
+
+ [Fact]
+ public void VerifySelfReferencingRecords()
+ {
+ // Arrange
+ var source = @"
+namespace Tests.Data.Models
+{
+ using System.Collections.Generic;
+
+ public record Employee(int Id, string EmployeeCode, Manager Manager);
+
+ public record Manager(int Id, string EmployeeCode, Manager Manager, int Level, List Employees) : Employee(Id, EmployeeCode, Manager);
+}
+
+namespace Tests.Data.ViewModels
+{
+ using System.Collections.Generic;
+ using Tests.Data.Models;
+ using MapTo;
+
+ [MapFrom(typeof(Employee))]
+ public partial record EmployeeViewModel(int Id, string EmployeeCode, ManagerViewModel Manager);
+
+ [MapFrom(typeof(Manager))]
+ public partial record ManagerViewModel(int Id, string EmployeeCode, ManagerViewModel Manager, int Level, List Employees) : EmployeeViewModel(Id, EmployeeCode, Manager);
+}
+".Trim();
+
+ // Act
+ var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, languageVersion: LanguageVersion.CSharp9);
+
+ // Assert
+ diagnostics.ShouldBeSuccessful();
+ _output.WriteLine(compilation.PrintSyntaxTree());
+ }
+
+ [Fact]
+ public void VerifySystemNamespaceConflict()
+ {
+ // Arrange
+ var source = @"
+namespace Test
+{
+ public record SomeRecord(int Id);
+}
+
+namespace Test.Models
+{
+ using MapTo;
+
+ [MapFrom(typeof(Test.SomeRecord))]
+ public partial record SomeRecordModel(int Id);
+}
+
+namespace Test.System
+{
+ public interface IMyInterface { }
+}
+".Trim();
+
+ // Act
+ var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, languageVersion: LanguageVersion.CSharp9);
+
+ // Assert
+ diagnostics.ShouldBeSuccessful();
+ _output.WriteLine(compilation.PrintSyntaxTree());
+ }
+
+ private static string MainSourceClass => @"
+using System;
+
+namespace Test.Data.Models
+{
+ public class User
+ {
+ public int Id { get; set; }
+
+ public DateTimeOffset RegisteredAt { get; set; }
+
+ public Profile Profile { get; set; }
+ }
+}
+".Trim();
+
+ private static string NestedSourceClass => @"
+namespace Test.Data.Models
+{
+ public class Profile
+ {
+ public string FirstName { get; set; }
+
+ public string LastName { get; set; }
+
+ public string FullName => $""{FirstName} {LastName}"";
+ }
+}
+".Trim();
+
+ private static string MainDestinationClass => @"
+using System;
+using MapTo;
+using Test.Data.Models;
+
+namespace Test.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
+ {
+ public string Convert(int source, object[] converterParameters) => $""{source:X}"";
+ }
+ }
+}
+".Trim();
+
+ private static string NestedDestinationClass => @"
+using MapTo;
+using Test.Data.Models;
+
+namespace Test.ViewModels
+{
+ [MapFrom(typeof(Profile))]
+ public partial class ProfileViewModel
+ {
+ public string FirstName { get; }
+
+ public string LastName { get; }
+ }
+}
+".Trim();
+
+ private static string MainSourceRecord => BuildSourceRecord("public record User(int Id, DateTimeOffset RegisteredAt, Profile Profile);");
+
+ private static string MainDestinationRecord => BuildDestinationRecord(@"
+[MapFrom(typeof(User))]
+public partial record UserViewModel(
+ [MapProperty(SourcePropertyName = nameof(User.Id))]
+ [MapTypeConverter(typeof(UserViewModel.IdConverter))]
+ string Key,
+ DateTimeOffset RegisteredAt,
+ Profile Profile)
+{
+ private class IdConverter : ITypeConverter
+ {
+ public string Convert(int source, object[] converterParameters) => $""{source:X}"";
+ }
+}");
+
+ private static string NestedSourceRecord => BuildSourceRecord("public record Profile(string FirstName, string LastName) { public string FullName => $\"{FirstName} {LastName}\"; }");
+
+ private static string NestedDestinationRecord => BuildDestinationRecord("[MapFrom(typeof(Profile))] public partial record ProfileViewModel(string FirstName, string LastName);");
+
+ private static string BuildSourceRecord(string record)
+ {
+ return $@"
+using System;
+
+namespace RecordTest.Data.Models
+{{
+ {record}
+}}
+".Trim();
+ }
+
+ private static string BuildDestinationRecord(string record)
+ {
+ return $@"
+using System;
+using MapTo;
+using RecordTest.Data.Models;
+
+namespace RecordTest.ViewModels
+{{
+ {record}
+}}
+".Trim();
+ }
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/test/BlueWest.MapTo.Tests/MappingContextTests.cs b/include/BlueWest.MapTo/test/BlueWest.MapTo.Tests/MappingContextTests.cs
new file mode 100644
index 0000000..70981fa
--- /dev/null
+++ b/include/BlueWest.MapTo/test/BlueWest.MapTo.Tests/MappingContextTests.cs
@@ -0,0 +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 = @"
+//
+
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+
+namespace MapTo
+{
+ internal sealed class MappingContext
+ {
+ private readonly Dictionary _cache;
+
+ internal MappingContext()
+ {
+ _cache = new Dictionary(1);
+ }
+
+ internal static TMapped Create(TOriginal original)
+ {
+ if (original == null) throw new ArgumentNullException(nameof(original));
+
+ var context = new MappingContext();
+ var mapped = context.MapFromWithContext(original);
+
+ if (mapped == null)
+ {
+ throw new InvalidOperationException();
+ }
+
+ return mapped;
+ }
+
+ internal TMapped MapFromWithContext(TOriginal original)
+ {
+ if (original == null)
+ {
+ return default(TMapped);
+ }
+
+ if (!TryGetValue(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 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 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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/test/MapTo.Integration.Tests/CyclicReferenceTests.cs b/include/BlueWest.MapTo/test/MapTo.Integration.Tests/CyclicReferenceTests.cs
new file mode 100644
index 0000000..071b42c
--- /dev/null
+++ b/include/BlueWest.MapTo/test/MapTo.Integration.Tests/CyclicReferenceTests.cs
@@ -0,0 +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();
+ }
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/test/MapTo.Integration.Tests/Data/Models/Employee.cs b/include/BlueWest.MapTo/test/MapTo.Integration.Tests/Data/Models/Employee.cs
new file mode 100644
index 0000000..65a17fd
--- /dev/null
+++ b/include/BlueWest.MapTo/test/MapTo.Integration.Tests/Data/Models/Employee.cs
@@ -0,0 +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;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/test/MapTo.Integration.Tests/Data/Models/Manager.cs b/include/BlueWest.MapTo/test/MapTo.Integration.Tests/Data/Models/Manager.cs
new file mode 100644
index 0000000..3ad79fb
--- /dev/null
+++ b/include/BlueWest.MapTo/test/MapTo.Integration.Tests/Data/Models/Manager.cs
@@ -0,0 +1,11 @@
+using System.Collections.Generic;
+
+namespace MapTo.Integration.Tests.Data.Models
+{
+ public class Manager : Employee
+ {
+ public int Level { get; set; }
+
+ public List Employees { get; set; } = new();
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/test/MapTo.Integration.Tests/Data/ViewModels/EmployeeViewModel.cs b/include/BlueWest.MapTo/test/MapTo.Integration.Tests/Data/ViewModels/EmployeeViewModel.cs
new file mode 100644
index 0000000..424da90
--- /dev/null
+++ b/include/BlueWest.MapTo/test/MapTo.Integration.Tests/Data/ViewModels/EmployeeViewModel.cs
@@ -0,0 +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; }
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/test/MapTo.Integration.Tests/Data/ViewModels/ManagerViewModel.cs b/include/BlueWest.MapTo/test/MapTo.Integration.Tests/Data/ViewModels/ManagerViewModel.cs
new file mode 100644
index 0000000..63a2c9c
--- /dev/null
+++ b/include/BlueWest.MapTo/test/MapTo.Integration.Tests/Data/ViewModels/ManagerViewModel.cs
@@ -0,0 +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 Employees { get; set; } = new();
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/test/MapTo.Integration.Tests/MapTo.Integration.Tests.csproj b/include/BlueWest.MapTo/test/MapTo.Integration.Tests/MapTo.Integration.Tests.csproj
new file mode 100644
index 0000000..0cf37be
--- /dev/null
+++ b/include/BlueWest.MapTo/test/MapTo.Integration.Tests/MapTo.Integration.Tests.csproj
@@ -0,0 +1,26 @@
+
+
+
+ net5.0
+ false
+
+
+
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
diff --git a/include/BlueWest.MapTo/test/TestConsoleApp/Data/FinanceTransaction.cs b/include/BlueWest.MapTo/test/TestConsoleApp/Data/FinanceTransaction.cs
new file mode 100644
index 0000000..8105767
--- /dev/null
+++ b/include/BlueWest.MapTo/test/TestConsoleApp/Data/FinanceTransaction.cs
@@ -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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/test/TestConsoleApp/Data/FinanceTransactionInsertDto.cs b/include/BlueWest.MapTo/test/TestConsoleApp/Data/FinanceTransactionInsertDto.cs
new file mode 100644
index 0000000..ea81b87
--- /dev/null
+++ b/include/BlueWest.MapTo/test/TestConsoleApp/Data/FinanceTransactionInsertDto.cs
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/test/TestConsoleApp/Data/FinanceTransactionReadDto.cs b/include/BlueWest.MapTo/test/TestConsoleApp/Data/FinanceTransactionReadDto.cs
new file mode 100644
index 0000000..30ba600
--- /dev/null
+++ b/include/BlueWest.MapTo/test/TestConsoleApp/Data/FinanceTransactionReadDto.cs
@@ -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;
+ }
+}
diff --git a/include/BlueWest.MapTo/test/TestConsoleApp/Data/Models/Car.cs b/include/BlueWest.MapTo/test/TestConsoleApp/Data/Models/Car.cs
new file mode 100644
index 0000000..4b812c6
--- /dev/null
+++ b/include/BlueWest.MapTo/test/TestConsoleApp/Data/Models/Car.cs
@@ -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;
+ }
+ }
+}
diff --git a/include/BlueWest.MapTo/test/TestConsoleApp/Data/Models/Employee.cs b/include/BlueWest.MapTo/test/TestConsoleApp/Data/Models/Employee.cs
new file mode 100644
index 0000000..e7c7118
--- /dev/null
+++ b/include/BlueWest.MapTo/test/TestConsoleApp/Data/Models/Employee.cs
@@ -0,0 +1,21 @@
+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;
+ }
+
+ }
+}
diff --git a/include/BlueWest.MapTo/test/TestConsoleApp/Data/Models/MyStruct.cs b/include/BlueWest.MapTo/test/TestConsoleApp/Data/Models/MyStruct.cs
new file mode 100644
index 0000000..8d09d44
--- /dev/null
+++ b/include/BlueWest.MapTo/test/TestConsoleApp/Data/Models/MyStruct.cs
@@ -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;
+ }
+
+ }
+}
diff --git a/include/BlueWest.MapTo/test/TestConsoleApp/Data/User.cs b/include/BlueWest.MapTo/test/TestConsoleApp/Data/User.cs
new file mode 100644
index 0000000..ed36a9a
--- /dev/null
+++ b/include/BlueWest.MapTo/test/TestConsoleApp/Data/User.cs
@@ -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 FinanceTransactions;
+
+ public User(int id, string name, string address, string btcAddress, string ltcAddress, double btcAmount, double ltcAmount, List 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);
+ }
+
+ }
+}
+
+
diff --git a/include/BlueWest.MapTo/test/TestConsoleApp/Data/UserList.cs b/include/BlueWest.MapTo/test/TestConsoleApp/Data/UserList.cs
new file mode 100644
index 0000000..78de267
--- /dev/null
+++ b/include/BlueWest.MapTo/test/TestConsoleApp/Data/UserList.cs
@@ -0,0 +1,16 @@
+using System.Collections.Generic;
+
+namespace BlueWest.Data
+{
+ public class UserList
+ {
+ public List Users;
+
+ public UserList(List users)
+ {
+ Users = users;
+ }
+
+ public int Length => Users.Count;
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/test/TestConsoleApp/Data/UserUpdateDto.cs b/include/BlueWest.MapTo/test/TestConsoleApp/Data/UserUpdateDto.cs
new file mode 100644
index 0000000..957beb9
--- /dev/null
+++ b/include/BlueWest.MapTo/test/TestConsoleApp/Data/UserUpdateDto.cs
@@ -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;
+
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/test/TestConsoleApp/Program.cs b/include/BlueWest.MapTo/test/TestConsoleApp/Program.cs
new file mode 100644
index 0000000..43517ac
--- /dev/null
+++ b/include/BlueWest.MapTo/test/TestConsoleApp/Program.cs
@@ -0,0 +1,32 @@
+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");
+
+
+
+
+ }
+
+
+
+ }
+}
\ No newline at end of file
diff --git a/include/BlueWest.MapTo/test/TestConsoleApp/TestConsoleApp.csproj b/include/BlueWest.MapTo/test/TestConsoleApp/TestConsoleApp.csproj
new file mode 100644
index 0000000..814b5e3
--- /dev/null
+++ b/include/BlueWest.MapTo/test/TestConsoleApp/TestConsoleApp.csproj
@@ -0,0 +1,18 @@
+
+
+
+ Exe
+ net471
+ latest
+ enable
+
+
+
+
+
+
+
+
+ Internal
+
+
diff --git a/include/BlueWest.MapTo/test/TestConsoleApp/ViewModels/CarReadDto.cs b/include/BlueWest.MapTo/test/TestConsoleApp/ViewModels/CarReadDto.cs
new file mode 100644
index 0000000..75df73c
--- /dev/null
+++ b/include/BlueWest.MapTo/test/TestConsoleApp/ViewModels/CarReadDto.cs
@@ -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;
+ }
+ }
+}
diff --git a/include/BlueWest.MapTo/test/TestConsoleApp/ViewModels/EmployeeViewModel.cs b/include/BlueWest.MapTo/test/TestConsoleApp/ViewModels/EmployeeViewModel.cs
new file mode 100644
index 0000000..8750e6c
--- /dev/null
+++ b/include/BlueWest.MapTo/test/TestConsoleApp/ViewModels/EmployeeViewModel.cs
@@ -0,0 +1,13 @@
+using MapTo;
+using TestConsoleApp.Data.Models;
+
+namespace TestConsoleApp.ViewModels
+{
+ [MapFrom(typeof(Employee))]
+ public partial class EmployeeViewModel
+ {
+ public int Id { get; }
+
+
+ }
+}
diff --git a/include/BlueWest.MapTo/test/TestConsoleApp/ViewModels/MyStructViewModel.cs b/include/BlueWest.MapTo/test/TestConsoleApp/ViewModels/MyStructViewModel.cs
new file mode 100644
index 0000000..b542044
--- /dev/null
+++ b/include/BlueWest.MapTo/test/TestConsoleApp/ViewModels/MyStructViewModel.cs
@@ -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;
+ }
+ }
+}
diff --git a/include/BlueWest.MapTo/version.json b/include/BlueWest.MapTo/version.json
new file mode 100644
index 0000000..e11d992
--- /dev/null
+++ b/include/BlueWest.MapTo/version.json
@@ -0,0 +1,14 @@
+{
+ "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json",
+ "version": "0.9",
+ "semVer1NumericIdentifierPadding": 1,
+ "publicReleaseRefSpec": [
+ "^refs/heads/master$",
+ "^refs/heads/v\\d+(?:\\.\\d+)?$"
+ ],
+ "cloudBuild": {
+ "buildNumber": {
+ "enabled": true
+ }
+ }
+}
\ No newline at end of file