diff --git a/AutoMapProperty.Tests/.gitignore b/AutoMapProperty.Tests/.gitignore
new file mode 100644
index 0000000..10913e0
--- /dev/null
+++ b/AutoMapProperty.Tests/.gitignore
@@ -0,0 +1,263 @@
+es (213 sloc) 4.2 KB
+
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+
+# User-specific files
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+
+# Visual Studio 2015 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# DNX
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.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
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding add-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# 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
+# TODO: 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
+# The packages folder can be ignored because of Package Restore
+**/packages/*
+# except build/, which is used as an MSBuild target.
+!**/packages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/packages/repositories.config
+# NuGet v3's project.json files produces more ignoreable 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
+
+# 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
+node_modules/
+orleans.codegen.cs
+
+# 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
+
+# SQL Server files
+*.mdf
+*.ldf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# 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/
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+# CodeRush
+.cr/
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
\ No newline at end of file
diff --git a/AutoMapProperty.Tests/AutoMapProperty.Tests.csproj b/AutoMapProperty.Tests/AutoMapProperty.Tests.csproj
new file mode 100644
index 0000000..9694b44
--- /dev/null
+++ b/AutoMapProperty.Tests/AutoMapProperty.Tests.csproj
@@ -0,0 +1,16 @@
+
+
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
diff --git a/AutoMapProperty.Tests/GeneratorTestCompilation.cs b/AutoMapProperty.Tests/GeneratorTestCompilation.cs
new file mode 100644
index 0000000..139ffbe
--- /dev/null
+++ b/AutoMapProperty.Tests/GeneratorTestCompilation.cs
@@ -0,0 +1,70 @@
+using AutoMapPropertyHelper;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using System.Reflection;
+using Xunit;
+
+namespace AutoMapProperty.Tests;
+
+internal sealed class GeneratorTestCompilation
+{
+ private static readonly MetadataReference[] References = ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")!)
+ .Split(Path.PathSeparator)
+ .Append(typeof(AutoMapPropertyAttribute).Assembly.Location)
+ .Append(typeof(LinqKit.PredicateBuilder).Assembly.Location)
+ .Distinct()
+ .Select(path => MetadataReference.CreateFromFile(path))
+ .ToArray();
+
+ private const string Support = """
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using AutoMapPropertyHelper;
+
+ namespace AutoMapProperty
+ {
+ public static class AutoMapPropertyUtils
+ {
+ public static T Create(IServiceProvider providers) where T : class => Activator.CreateInstance();
+ public static T Create(IServiceProvider providers, TParent parent, string name)
+ where T : class, new() where TParent : class => new T();
+ }
+ }
+ """;
+
+ public Compilation Output { get; }
+ public GeneratorDriverRunResult RunResult { get; }
+
+ public GeneratorTestCompilation(string source)
+ {
+ var compilation = CSharpCompilation.Create("MappingTests_" + Guid.NewGuid().ToString("N"),
+ new[] { CSharpSyntaxTree.ParseText(Support + "\n" + source) }, References,
+ new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: NullableContextOptions.Enable));
+
+ GeneratorDriver driver = CSharpGeneratorDriver.Create(new AutoMapPropertyGenerator());
+ driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var output, out _);
+ Output = output;
+ RunResult = driver.GetRunResult();
+ Assert.All(RunResult.Results, result => Assert.Null(result.Exception));
+ }
+
+ public void AssertCompiles()
+ {
+ var errors = Output.GetDiagnostics().Concat(RunResult.Diagnostics)
+ .Where(x => x.Severity == DiagnosticSeverity.Error).ToArray();
+ Assert.True(errors.Length == 0, string.Join(Environment.NewLine, errors.Select(x => x.ToString())));
+ }
+
+ public IPropertySymbol Property(string type, string property) =>
+ Assert.IsAssignableFrom(Assert.Single(Output.GetTypeByMetadataName(type)!.GetMembers(property)));
+
+ public object? Evaluate()
+ {
+ AssertCompiles();
+ using var stream = new MemoryStream();
+ var result = Output.Emit(stream);
+ Assert.True(result.Success, string.Join(Environment.NewLine, result.Diagnostics));
+ return Assembly.Load(stream.ToArray()).GetType("Fixtures.Check")!.GetMethod("Run")!.Invoke(null, null);
+ }
+}
diff --git a/AutoMapProperty.Tests/MappingRegressionTests.cs b/AutoMapProperty.Tests/MappingRegressionTests.cs
new file mode 100644
index 0000000..68ef447
--- /dev/null
+++ b/AutoMapProperty.Tests/MappingRegressionTests.cs
@@ -0,0 +1,198 @@
+using Microsoft.CodeAnalysis;
+using Xunit;
+
+namespace AutoMapProperty.Tests;
+
+public class MappingRegressionTests
+{
+ [Fact]
+ public void NullableNestedDtosMapNullAndNonNullValues()
+ {
+ var compilation = new GeneratorTestCompilation("""
+ namespace Fixtures
+ {
+ public class Child
+ {
+ [AutoMapProperty("Response")]
+ public int Value { get; set; }
+ }
+ public class Parent
+ {
+ [AutoMapProperty("Response", typeof(ChildResponse), Nullability = AutoMapNullability.Nullable)]
+ public Child Child { get; set; } = new Child();
+ }
+ public static class Check
+ {
+ public static bool Run()
+ {
+ var parent = new Parent { Child = new Child { Value = 42 } };
+ var dto = new ParentResponse().ApplyFrom(null, parent);
+ if (dto.Child.Value != 42 || dto.GetProjectorFrom(null).Compile()(parent).Child.Value != 42)
+ return false;
+ dto.Child = null;
+ dto.ApplyTo(null, parent);
+ if (parent.Child != null || new ParentResponse().ApplyFrom(null, parent).Child != null ||
+ dto.GetProjectorFrom(null).Compile()(parent).Child != null)
+ return false;
+ if (!ParentResponse.EqualExpression(null, parent).Compile()(dto) ||
+ !dto.Equals(new ParentResponse { Child = null }))
+ return false;
+ dto.Child = new ChildResponse { Value = 17 };
+ dto.ApplyTo(null, parent);
+ return parent.Child.Value == 17;
+ }
+ }
+ }
+ """);
+ Assert.Equal(NullableAnnotation.Annotated, compilation.Property("Fixtures.ParentResponse", "Child").Type.NullableAnnotation);
+ Assert.Equal(true, compilation.Evaluate());
+ }
+
+ [Fact]
+ public void InheritedGeneratedPropertiesParticipateInMappings()
+ {
+ var compilation = new GeneratorTestCompilation("""
+ namespace Fixtures
+ {
+ public class Base
+ {
+ [AutoMapProperty("Request", Nullability = AutoMapNullability.NonNullable)]
+ public string? Name { get; set; }
+ }
+ public class Derived : Base
+ {
+ [AutoMapProperty("Request")]
+ public int Value { get; set; }
+ }
+ public partial class DerivedRequest : BaseRequest { }
+ public static class Check
+ {
+ public static bool Run()
+ {
+ var entity = new Derived { Name = "name", Value = 42 };
+ var dto = new DerivedRequest().ApplyFrom(null, entity);
+ var projected = dto.GetProjectorFrom(null).Compile()(entity);
+ dto.Name = "changed";
+ dto.ApplyTo(null, entity);
+ return entity.Name == "changed" && projected.Name == "name" && projected.Value == 42;
+ }
+ }
+ }
+ """);
+ Assert.Equal(true, compilation.Evaluate());
+ }
+
+ [Fact]
+ public void CustomConvertersReceiveOverriddenNullableValues()
+ {
+ var compilation = new GeneratorTestCompilation("""
+ namespace Fixtures
+ {
+ public class Entity
+ {
+ [AutoMapProperty("Request", typeof(string), nameof(Parse), nameof(Format), Nullability = AutoMapNullability.Nullable)]
+ public int Value { get; set; }
+ public static int Parse(IServiceProvider providers, Entity entity, string? value) => value == null ? 0 : int.Parse(value);
+ public static string Format(IServiceProvider providers, Entity entity, int value) => value.ToString();
+ }
+ public static class Check
+ {
+ public static bool Run()
+ {
+ var entity = new Entity { Value = 42 };
+ var dto = new EntityRequest().ApplyFrom(null, entity);
+ if (dto.Value != "42" || dto.GetProjectorFrom(null).Compile()(entity).Value != "42")
+ return false;
+ dto.Value = null;
+ dto.ApplyTo(null, entity);
+ return entity.Value == 0;
+ }
+ }
+ }
+ """);
+ Assert.Equal(true, compilation.Evaluate());
+ }
+
+ [Fact]
+ public void FiltersMappingAttributesFromSharedListsAndKeepsValidationAttributes()
+ {
+ var compilation = new GeneratorTestCompilation("""
+ namespace Fixtures
+ {
+ public class Entity
+ {
+ [System.ComponentModel.DataAnnotations.Required, AutoMapProperty("Response", Nullability = AutoMapNullability.Nullable)]
+ public string Value { get; set; } = "initial";
+ }
+ }
+ """);
+ compilation.AssertCompiles();
+ var attribute = Assert.Single(compilation.Property("Fixtures.EntityResponse", "Value").GetAttributes());
+ Assert.Equal("RequiredAttribute", attribute.AttributeClass!.Name);
+ }
+
+ [Fact]
+ public void NestedCollectionsAndArrayTypeReplacementsKeepMappingAndInitializerBehavior()
+ {
+ var compilation = new GeneratorTestCompilation("""
+ namespace Fixtures
+ {
+ public class Child
+ {
+ [AutoMapProperty("Response")]
+ public int Value { get; set; }
+ }
+ public class Parent
+ {
+ [AutoMapProperty("Response", typeof(ChildResponse[]), Nullability = AutoMapNullability.Nullable)]
+ public Child[] Children { get; set; } = Array.Empty();
+ }
+ public static class Check
+ {
+ public static bool Run()
+ {
+ var parent = new Parent { Children = new[] { new Child { Value = 42 } } };
+ var dto = new ParentResponse().ApplyFrom(null, parent);
+ if (dto.Children[0].Value != 42 || dto.GetProjectorFrom(null).Compile()(parent).Children[0].Value != 42)
+ return false;
+ dto.Children[0].Value = 17;
+ dto.ApplyTo(null, parent);
+ if (parent.Children[0].Value != 17)
+ return false;
+ dto.Children = null;
+ dto.ApplyTo(null, parent);
+ return parent.Children.Length == 0;
+ }
+ }
+ }
+ """);
+ Assert.Equal(true, compilation.Evaluate());
+ }
+
+ [Fact]
+ public void SupportsInterfacesComputedPropertiesAndNullTypeArguments()
+ {
+ var compilation = new GeneratorTestCompilation("""
+ namespace Fixtures
+ {
+ public interface ISource
+ {
+ [AutoMapProperty("Request", Nullability = AutoMapNullability.Nullable)]
+ string Name { get; }
+ }
+ public class Entity
+ {
+ [AutoMapProperty("Response", null, Nullability = AutoMapNullability.NonNullable)]
+ public string? Name => "name";
+ }
+ public static class Check
+ {
+ public static bool Run() => new EntityResponse().ApplyFrom(null, new Entity()).Name == "name";
+ }
+ }
+ """);
+ Assert.Equal(NullableAnnotation.Annotated, compilation.Property("Fixtures.ISourceRequest", "Name").Type.NullableAnnotation);
+ Assert.Equal(NullableAnnotation.NotAnnotated, compilation.Property("Fixtures.EntityResponse", "Name").Type.NullableAnnotation);
+ Assert.Equal(true, compilation.Evaluate());
+ }
+}
diff --git a/AutoMapProperty.Tests/NullabilityTests.cs b/AutoMapProperty.Tests/NullabilityTests.cs
new file mode 100644
index 0000000..cb3509f
--- /dev/null
+++ b/AutoMapProperty.Tests/NullabilityTests.cs
@@ -0,0 +1,142 @@
+using Microsoft.CodeAnalysis;
+using Xunit;
+
+namespace AutoMapProperty.Tests;
+
+public class NullabilityTests
+{
+ [Theory]
+ [InlineData("string", "Nullable", true)]
+ [InlineData("string?", "NonNullable", false)]
+ [InlineData("string?", "Preserve", true)]
+ [InlineData("string", "Preserve", false)]
+ [InlineData("int", "Nullable", true)]
+ [InlineData("int?", "Nullable", true)]
+ [InlineData("int?", "NonNullable", false)]
+ [InlineData("int", "NonNullable", false)]
+ [InlineData("int?", "Preserve", true)]
+ [InlineData("DateTime", "Nullable", true)]
+ [InlineData("DateTime?", "NonNullable", false)]
+ [InlineData("string[]", "Nullable", true)]
+ [InlineData("string[]?", "NonNullable", false)]
+ [InlineData("ICollection", "Nullable", true)]
+ public void OverridesTopLevelNullability(string type, string mode, bool nullable)
+ {
+ var compilation = new GeneratorTestCompilation($$"""
+ namespace Fixtures
+ {
+ public class Entity
+ {
+ [AutoMapProperty("Response", Nullability = AutoMapNullability.{{mode}})]
+ public {{type}} Value { get; set; }
+ }
+ }
+ """);
+ compilation.AssertCompiles();
+ var generatedType = compilation.Property("Fixtures.EntityResponse", "Value").Type;
+ Assert.Equal(nullable, generatedType.NullableAnnotation == NullableAnnotation.Annotated ||
+ generatedType is INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T });
+ if (generatedType is INamedTypeSymbol { Name: "ICollection" } collection)
+ Assert.Equal(NullableAnnotation.Annotated, collection.TypeArguments[0].NullableAnnotation);
+ }
+
+ [Fact]
+ public void OverridesAreIndependentForEachDtoAndAppliedAfterTypeReplacement()
+ {
+ var compilation = new GeneratorTestCompilation("""
+ namespace Fixtures
+ {
+ public class Entity
+ {
+ [AutoMapProperty("Request", typeof(long), Nullability = AutoMapNullability.Nullable)]
+ [AutoMapProperty("Response", typeof(long?), Nullability = AutoMapNullability.NonNullable)]
+ [AutoMapProperty("Copy")]
+ public int Value { get; set; }
+ }
+ }
+ """);
+ compilation.AssertCompiles();
+ var request = Assert.IsAssignableFrom(compilation.Property("Fixtures.EntityRequest", "Value").Type);
+ Assert.Equal(SpecialType.System_Nullable_T, request.OriginalDefinition.SpecialType);
+ Assert.Equal(SpecialType.System_Int64, request.TypeArguments[0].SpecialType);
+ Assert.Equal(SpecialType.System_Int64, compilation.Property("Fixtures.EntityResponse", "Value").Type.SpecialType);
+ Assert.Equal(SpecialType.System_Int32, compilation.Property("Fixtures.EntityCopy", "Value").Type.SpecialType);
+ }
+
+ [Fact]
+ public void NullableValuesMapInBothDirectionsAndThroughProjectors()
+ {
+ var compilation = new GeneratorTestCompilation("""
+ namespace Fixtures
+ {
+ public class Entity
+ {
+ [AutoMapProperty("Request", Nullability = AutoMapNullability.Nullable)]
+ public int Value { get; set; }
+ }
+ public static class Check
+ {
+ public static bool Run()
+ {
+ var entity = new Entity { Value = 42 };
+ var dto = new EntityRequest().ApplyFrom(null, entity);
+ var projected = dto.GetProjectorFrom(null).Compile()(entity);
+ dto.Value = 17;
+ dto.ApplyTo(null, entity);
+ dto.Value = null;
+ try { dto.ApplyTo(null, entity); }
+ catch (InvalidOperationException) { return entity.Value == 17 && projected.Value == 42; }
+ return false;
+ }
+ }
+ }
+ """);
+ Assert.Equal(true, compilation.Evaluate());
+ }
+
+ [Fact]
+ public void ValueTypeNullabilityOverridesKeepValidInitializers()
+ {
+ var compilation = new GeneratorTestCompilation("""
+ namespace Fixtures
+ {
+ public class Entity
+ {
+ [AutoMapProperty("Request", Nullability = AutoMapNullability.Nullable)]
+ public DateTime Date { get; set; } = new DateTime(2026, 1, 2);
+ [AutoMapProperty("Request", Nullability = AutoMapNullability.NonNullable)]
+ public int? Count { get; set; } = null;
+ [AutoMapProperty("Request", Nullability = AutoMapNullability.NonNullable)]
+ public int? Total { get; set; } = default(int?);
+ }
+ public static class Check
+ {
+ public static bool Run()
+ {
+ var dto = new EntityRequest();
+ return dto.Date == new DateTime(2026, 1, 2) && dto.Count == 0 && dto.Total == 0;
+ }
+ }
+ }
+ """);
+ Assert.Equal(true, compilation.Evaluate());
+ }
+
+ [Fact]
+ public void InvalidNullabilityReportsPropertyDiagnostic()
+ {
+ var compilation = new GeneratorTestCompilation("""
+ namespace Fixtures
+ {
+ public class Entity
+ {
+ [AutoMapProperty("Response", Nullability = (AutoMapNullability)99)]
+ public string Value { get; set; }
+ }
+ }
+ """);
+ var diagnostic = Assert.Single(compilation.RunResult.Diagnostics);
+ Assert.Equal("NEJ23", diagnostic.Id);
+ Assert.True(diagnostic.Location.IsInSource);
+ }
+}
diff --git a/AutoMapProperty/AutoMapProperty.cs b/AutoMapProperty/AutoMapProperty.cs
deleted file mode 100644
index 581c704..0000000
--- a/AutoMapProperty/AutoMapProperty.cs
+++ /dev/null
@@ -1,1117 +0,0 @@
-using Microsoft.CodeAnalysis;
-using Microsoft.CodeAnalysis.CSharp;
-using Microsoft.CodeAnalysis.CSharp.Syntax;
-using Microsoft.CodeAnalysis.Text;
-using System;
-using System.Collections.Generic;
-using System.Collections.Immutable;
-using System.Diagnostics;
-using System.Linq;
-using System.Net;
-using System.Reflection;
-using System.Reflection.Metadata.Ecma335;
-using System.Text;
-using System.Text.RegularExpressions;
-
-namespace AutoMapProperty
-{
- [Generator]
- public class AutoMapPropertyGenerator : IIncrementalGenerator
- {
- public void Initialize(IncrementalGeneratorInitializationContext context)
- {
- IncrementalValuesProvider classDeclarations = context.SyntaxProvider
- .CreateSyntaxProvider(IsSyntaxTargetForGeneration, GetSemanticTargetForGeneration)
- .Where(m => m is not null)!;
-
-
- IncrementalValueProvider<(Compilation, ImmutableArray)> compilationAndClasses
- = context.CompilationProvider.Combine(classDeclarations.Collect());
-
- context.RegisterSourceOutput(compilationAndClasses,
- static (spc, source) => Execute(source.Item1, source.Item2, spc));
- }
-
-
- static bool IsSyntaxTargetForGeneration(SyntaxNode node, CancellationToken ct)
- => node is PropertyDeclarationSyntax m && m.AttributeLists.Count > 0;
-
- private const string AutoMapPropertyAttributeName = "AutoMapPropertyHelper.AutoMapPropertyAttribute";
-
- static PropertyDeclarationSyntax? GetSemanticTargetForGeneration(GeneratorSyntaxContext context, CancellationToken ct)
- {
- // we know the node is a MethodDeclarationSyntax thanks to IsSyntaxTargetForGeneration
- var propertyDeclarationSyntax = (PropertyDeclarationSyntax)context.Node;
-
- // loop through all the attributes on the method
- foreach (AttributeListSyntax attributeListSyntax in propertyDeclarationSyntax.AttributeLists)
- {
- foreach (AttributeSyntax attributeSyntax in attributeListSyntax.Attributes)
- {
- if (context.SemanticModel.GetSymbolInfo(attributeSyntax).Symbol is not IMethodSymbol attributeSymbol)
- {
- // weird, we couldn't get the symbol, ignore it
- continue;
- }
-
- INamedTypeSymbol attributeContainingTypeSymbol = attributeSymbol.ContainingType;
- string fullName = attributeContainingTypeSymbol.ToDisplayString();
-
- // Is the attribute theAutoMapPropertyAttributeName attribute?
- if (fullName == AutoMapPropertyAttributeName)
- {
- // return the parent class of the method
- return propertyDeclarationSyntax;
- }
- }
- }
-
- // we didn't find the attribute we were looking for
- return null;
- }
-
-
- public class ClassToGenerate
- {
- ///
- /// Class or Interface
- ///
- public TypeDeclarationSyntax SourceContainer;
-
- public List Usings;
- public string Namespace;
- public string Name;
- public string FullName => Namespace + "." + Name;
-
- public record struct ProperyGenerationInfo(PropertyDeclarationSyntax prop, ITypeSymbol propTypeSymbol, bool customFrom, IMethodSymbol? customFromMethod, bool customTo, IMethodSymbol? customToMethod);
- public List Properties;
-
- public struct MappableTypes
- {
- public ITypeSymbol FromType;
- public ITypeSymbol ToType;
-
- public ClassToGenerate OurTargetType;
- public IPropertySymbol FromProperty;
- public bool FromIsReadOnly => FromProperty.SetMethod == null;
-
- public bool CustomFrom;
- public IMethodSymbol? CustomFromMethod;
- public bool CustomTo;
- public IMethodSymbol? CustomToMethod;
- public MappableTypes(ITypeSymbol from, ITypeSymbol to, IPropertySymbol fromProperty, bool customFrom, IMethodSymbol? customFromMethod, bool customTo, IMethodSymbol? customToMethod, ClassToGenerate? isOurType)
- {
- FromType = from;
- ToType = to;
- FromProperty = fromProperty;
- CustomFrom = customFrom;
- CustomFromMethod = customFromMethod;
- CustomTo = customTo;
- CustomToMethod = customToMethod;
- OurTargetType = isOurType;
- }
- }
-
- public Dictionary MappableProperties = new Dictionary();
-
- public ClassToGenerate(TypeDeclarationSyntax sourceClass, string name, string namespaceName, List usings, List properties)
- {
- SourceContainer = sourceClass;
-
- Namespace = namespaceName;
- Name = name;
- Properties = properties;
- Usings = usings;
- }
- }
-
-
- private static void Execute(Compilation compilation, ImmutableArray properties, SourceProductionContext context)
- {
- if (properties.IsDefaultOrEmpty)
- {
- // nothing to do yet
- return;
- }
-
- IEnumerable distinctClasses = properties.Distinct();
- List enumsToGenerate = GetTypesToGenerate(compilation, distinctClasses, context);
-
- // If there were errors in the EnumDeclarationSyntax, we won't create an
- // EnumToGenerate for it, so make sure we have something to generate
- foreach (ClassToGenerate classToGenerate in enumsToGenerate)
- {
- //throw new Exception(classToGenerate.Name);
- string result = GenerateExtensionClass(enumsToGenerate, classToGenerate);
- context.AddSource(classToGenerate.Name + ".g.cs", SourceText.From(result, Encoding.UTF8));
- }
- }
-
- static List GetTypesToGenerate(Compilation compilation, IEnumerable props, SourceProductionContext context)
- {
- // Create a list to hold our output
- var classesToGenerate = new Dictionary();
- // Get the semantic representation of our marker attribute
- INamedTypeSymbol? propAttribute = compilation.GetTypeByMetadataName(AutoMapPropertyAttributeName);
-
- if (propAttribute == null)
- {
- // If this is null, the compilation couldn't find the marker attribute type
- // which suggests there's something very wrong! Bail out..
- return classesToGenerate.Values.ToList();
- }
-
- foreach (PropertyDeclarationSyntax propSyntax in props)
- {
- // stop if we're asked to
- context.CancellationToken.ThrowIfCancellationRequested();
-
- // Get the semantic representation of the enum syntax
- SemanticModel semanticModel = compilation.GetSemanticModel(propSyntax.Parent!.SyntaxTree);
-
- //Get the parent class or interface
- TypeDeclarationSyntax PropParentDecl = (TypeDeclarationSyntax)propSyntax.Parent!;
-
- if (PropParentDecl is not ClassDeclarationSyntax && PropParentDecl is not InterfaceDeclarationSyntax)
- continue;
-
- if (semanticModel.GetDeclaredSymbol(PropParentDecl) is not INamedTypeSymbol classSymbol)
- continue;
-
- if (semanticModel.GetDeclaredSymbol(propSyntax) is not IPropertySymbol propSymbol)
- continue;
-
- var root = PropParentDecl.SyntaxTree.GetCompilationUnitRoot();
-
- string className = classSymbol.Name;
-
- foreach (AttributeData attr in propSymbol.GetAttributes())
- {
- if (!propAttribute.Equals(attr.AttributeClass, SymbolEqualityComparer.Default))
- {
- // This isn't the [EnumExtensions] attribute
- continue;
- }
-
- //get the autogen name
- string dataName = (string)attr.ConstructorArguments[0].Value!;
-
- //the final name
- string name = className + dataName;
- if (!classesToGenerate.ContainsKey(name))
- {
- //Copy all usings
- List usings = root.Usings.Select(x => x.ToString()).ToList();
- string ns = classSymbol.ContainingNamespace.ToString();
- usings.Add("using " + ns + ";");
-
- //Create the carrier type
- classesToGenerate[name] = new ClassToGenerate(PropParentDecl, name, ns, usings, new List());
- }
-
- PropertyDeclarationSyntax? mem = propSyntax;
- try
- {
- var newList = GetModifiedAttributeList(compilation, propSyntax.AttributeLists);
- /*
- context.ReportDiagnostic(Diagnostic.Create(
- new DiagnosticDescriptor("NEJ23", "Error", "New list on: {1} {0}", "NEJ", DiagnosticSeverity.Warning, true), null, string.Join(", ", newList.Select(x => x.ToString())), propSymbol.Name)
- );*/
-
- //Modify attributes to not include our generation attribute
- mem = propSyntax.WithAttributeLists(newList);
- }
- catch (Exception ex)
- {
- context.ReportDiagnostic(Diagnostic.Create(
- new DiagnosticDescriptor("NEJ23", "Error", "Error processing {0}", "NEJ", DiagnosticSeverity.Error, true), null, propSymbol.Name)
- );
-
- context.ReportDiagnostic(Diagnostic.Create(
- new DiagnosticDescriptor("NEJ23", "Error", "{0}", "NEJ", DiagnosticSeverity.Error, true), null, ex.Message)
- );
-
- context.ReportDiagnostic(Diagnostic.Create(
- new DiagnosticDescriptor("NEJ23", "Error", "{0}", "NEJ", DiagnosticSeverity.Error, true), null, ex.StackTrace)
- );
- }
-
- var typeSymbol = propSymbol.Type;
-
- //If we have a special overwrite type, replace it
- if (attr.ConstructorArguments.Count() >= 2)
- {
- INamedTypeSymbol dataTypename = attr?.ConstructorArguments[1].Value as INamedTypeSymbol;
- mem = mem.WithType(SyntaxFactory.ParseTypeName(dataTypename.ToDisplayString() + " "));
-
- var oldConcreteType = GetConcreteType(propSymbol.Type);
- var newConcreteType = GetConcreteType(dataTypename);
-
- if (mem.Initializer != null && !SymbolEqualityComparer.Default.Equals(oldConcreteType, newConcreteType))
- {
- var rewriter = new TypeReplacer(oldConcreteType.Name, newConcreteType.Name);
- var newInitializer = (EqualsValueClauseSyntax)rewriter.Visit(mem.Initializer);
- mem = mem.WithInitializer(newInitializer);
- }
-
- typeSymbol = dataTypename;
- }
-
-
- //if the property is readonly, remove the readonly modifier
- if (mem.Modifiers.Any(x => x.IsKind(SyntaxKind.ReadOnlyKeyword)))
- {
- mem = mem.WithModifiers(SyntaxFactory.TokenList(mem.Modifiers.Where(x => !x.IsKind(SyntaxKind.ReadOnlyKeyword))));
- }
-
- //if property has non automatic(backing field) getter or setter, remove them
- //add the default backing field getter and setter
- mem = mem.WithAccessorList(SyntaxFactory.AccessorList(SyntaxFactory.List(new[]
- {
- SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration).WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)),
- SyntaxFactory.AccessorDeclaration(SyntaxKind.SetAccessorDeclaration).WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken))
- })));
-
- //if property has initializer or expression body, remove them
- //mem = mem.WithInitializer(null);
- mem = mem.WithExpressionBody(null);
-
- //if property is abstract remove the abstract modifier
- if (mem.Modifiers.Any(x => x.IsKind(SyntaxKind.AbstractKeyword)))
- {
- mem = mem.WithModifiers(SyntaxFactory.TokenList(mem.Modifiers.Where(x => !x.IsKind(SyntaxKind.AbstractKeyword))));
- }
-
- //now the has trailing semicolon, which is wrong
- if (mem.Initializer == null)
- {
- mem = mem.WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.None));
- }
- else
- {
- mem = mem.WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken));
- }
-
- //if we dont want to generate converter
- bool customFrom = false;
- IMethodSymbol? customFromMethod = null;
- bool customTo = false;
- IMethodSymbol? customToMethod = null;
- if (attr.ConstructorArguments.Count() >= 3 && attr.ConstructorArguments[2].Value is not null)
- {
- customTo = true;
- customToMethod = ProcessCustomAttribute(context, classSymbol, attr.ConstructorArguments[2]);
- }
- if (attr.ConstructorArguments.Count() >= 4 && attr.ConstructorArguments[3].Value is not null)
- {
- customFrom = true;
- customFromMethod = ProcessCustomAttribute(context, classSymbol, attr.ConstructorArguments[3]);
- }
- //Add the property to the carrier type
- classesToGenerate[name].Properties.Add(
- new ClassToGenerate.ProperyGenerationInfo(mem, typeSymbol, customFrom, customFromMethod, customTo, customToMethod)
- );
- }
- }
-
- foreach (ClassToGenerate cls in classesToGenerate.Values)
- {
- ProcessClassToGenerateVariations(context, compilation, classesToGenerate, cls);
- }
-
- return classesToGenerate.Values.ToList();
- }
-
- public static IMethodSymbol? ProcessCustomAttribute(SourceProductionContext cont, INamedTypeSymbol classSymbol, TypedConstant attr)
- {
- var arg = attr.Value as string;
- //get all methods of the declaring parent type
- var methods = classSymbol.GetMembers().OfType();
- //get the method with the same name as the attribute argument
- var method = methods.FirstOrDefault(x => x.Name == arg);
- if (method == null)
- {
- cont.ReportDiagnostic(Diagnostic.Create(
- new DiagnosticDescriptor("NEJ10", "No method found", "Couldn't find method {0} on {1}", "NEJ", DiagnosticSeverity.Error, true), null, arg, classSymbol.Name)
- );
- return null;
- }
-
- //check if the method is static
- if (!method.IsStatic)
- {
- cont.ReportDiagnostic(Diagnostic.Create(
- new DiagnosticDescriptor("NEJ10", "Method not static", "Method {0} on {1} is not static", "NEJ", DiagnosticSeverity.Error, true), null, arg, classSymbol.Name)
- );
- return null;
- }
-
- return method;
- }
-
- public record struct PropertyGenInfo(ITypeSymbol type, IPropertySymbol? symbol = null, bool customFrom = false, IMethodSymbol? customFromMethod = null, bool customTo = false, IMethodSymbol? customToMethod = null);
-
- public static void ProcessClassToGenerateVariations(SourceProductionContext context, Compilation compilation, Dictionary classesToGenerate, ClassToGenerate cls)
- {
- var possibleCandidates = new List<(string, PropertyGenInfo)>();
- //check if the newName already exists in compilation
-
- var sym = compilation.GetTypesByMetadataName(cls.FullName).FirstOrDefault();
-
- if (compilation.GetTypesByMetadataName(cls.FullName).FirstOrDefault() is INamedTypeSymbol symb)
- {
- possibleCandidates.AddRange(GetSymbolsForTypedSymbol(compilation, symb, classesToGenerate)
- .Select(x => (x.Item1, x.Item2)));
- }
- else
- {
- possibleCandidates.AddRange(GetInfoFromClassToGenerate(compilation, cls));
- }
-
- //get properties on the source class
- var comp = compilation.GetSemanticModel(cls.SourceContainer.SyntaxTree);
- var symbol = comp.GetDeclaredSymbol(cls.SourceContainer)!;
- var sourceCandidates = GetSymbolsForTypedSymbol(compilation, symbol, classesToGenerate);
-
- //only add possible candidates that are also in sourceCanddidates
- //check if there are multiples if so report a diagnostic
- var source = sourceCandidates
- .Where(x => !string.IsNullOrWhiteSpace(x.Item1))
- //Remove entries with duplicate names
- .GroupBy(x => x.Item1)
- .Select(x => x.First());
-
- var possible = possibleCandidates
- .Where(x => !string.IsNullOrWhiteSpace(x.Item1))
- //Remove entries with duplicate names
- .GroupBy(x => x.Item1)
- .Select(x => x.First());
-
- var sourceNames = source.ToDictionary(x => x.Item1, x => x.Item2);
- var possibleNames = possible.ToDictionary(x => x.Item1, x => x.Item2);
- cls.MappableProperties = possibleNames.Keys.Where(x => sourceNames.Keys.Contains(x))
- .ToDictionary(x => x, x =>
- {
- var concreteType = GetConcreteType(possibleNames[x].type);
- var fullName = GetFullString(concreteType);
- var ourType = classesToGenerate.FirstOrDefault(y => y.Value.FullName == fullName).Value;
-
- //could not be resolved due to missing namespace, lets be optimistic
- if (ourType == null && string.IsNullOrEmpty(concreteType.ContainingNamespace?.Name))
- {
- ourType = classesToGenerate.FirstOrDefault(y => y.Value.Name == fullName).Value;
- }
- /*
- if (!isOurType && !fullName.StartsWith("System"))
- {
- Console.WriteLine(x + "'s type: " + fullName + " is not in our types");
- Console.WriteLine(concreteType.ContainingNamespace.Name);
- Console.WriteLine(concreteType.Name);
- }*/
-
- return new ClassToGenerate.MappableTypes(
- sourceNames[x].type,
- possibleNames[x].type,
- sourceNames[x].symbol,
- possibleNames[x].customFrom,
- possibleNames[x].customFromMethod,
- possibleNames[x].customTo,
- possibleNames[x].customToMethod,
- ourType);
- });
- //c.MappableProperties = c.MappableProperties.ToDictionary(x => x.Item1, x => x.Item2).Select(x => (x.Key, x.Value)).ToList();
- /*context.ReportDiagnostic(
- Diagnostic.Create(
- new DiagnosticDescriptor(
- "NEJ01", "Report candidates vs real", "{0} had: {1} possible candidates and:{2} source values, but in the end there were: {3} values",
- "NEJ", DiagnosticSeverity.Warning, true), null, cls.Name, possibleCandidates.Count, sourceCandidates.Count, cls.MappableProperties.Count));*/
-
- }
-
- static ITypeSymbol GetConcreteType(ITypeSymbol symb)
- {
- //if it's array, dicionary, etc. then we weant to check the underlying type
- if (symb is IArrayTypeSymbol arr)
- {
- //Console.WriteLine(symb.GetFullMetadataName() + " is array: " + GetFullString(arr.ElementType));
- return GetConcreteType(arr.ElementType);
- }
- //also check for ICollection and IEnumerable
- if (symb.Name == "ICollection" || symb.Name == "IEnumerable")
- {
- //if it's generic, then we want to check the underlying type
- if (symb is INamedTypeSymbol gen)
- {
- //Console.WriteLine(symb.GetFullMetadataName() + " is generic: " + GetFullString(gen.TypeArguments[0]));
- return GetConcreteType(gen.TypeArguments[0]);
- }
- }
-
- return symb;
- }
-
- static SymbolDisplayFormat displayFormat = new SymbolDisplayFormat(
- typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
- genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters,
- miscellaneousOptions: SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier);
-
-
- public static List<(string, PropertyGenInfo)> GetSymbolsForTypedSymbol(Compilation comp, INamedTypeSymbol cls, Dictionary ctg)
- {
- var result = new List<(string, PropertyGenInfo)>();
-
- result.AddRange(cls.GetMembers().OfType().Select(x => (x.Name.Trim(), new PropertyGenInfo(x.Type, x))));
- //add from to be generated symbols
- result.AddRange(GetSymbolsFromCurrentGeneration(comp, cls.Name.Trim(), ctg));
-
- //add parent symbols
- if (cls.BaseType != null)
- result.AddRange(GetSymbolsForTypedSymbol(comp, cls.BaseType, ctg));
- return result;
- }
- public static List<(string, PropertyGenInfo)> GetSymbolsFromCurrentGeneration(Compilation comp, string className, Dictionary currentClassesToBeGenerated)
- {
- if (currentClassesToBeGenerated.ContainsKey(className))
- {
- var c = currentClassesToBeGenerated[className];
-
- return GetInfoFromClassToGenerate(comp, c);
- }
- return new List<(string, PropertyGenInfo)>();
- }
- public static List<(string, PropertyGenInfo)> GetInfoFromClassToGenerate(Compilation comp, ClassToGenerate cls)
- {
- return cls.Properties.Select(x =>
- {
- var name = x.prop.Identifier.ToFullString().Trim();
- return (name,
- new PropertyGenInfo(x.propTypeSymbol, (IPropertySymbol?)null, x.customFrom, x.customFromMethod, x.customTo, x.customToMethod));
- })
- .ToList();
- }
-
- #region Attribute_Editing
- public static SyntaxList GetModifiedAttributeList(Compilation com, SyntaxList attListList)
- {
- if (attListList == null)
- return attListList;
- if (attListList.Count <= 0)
- return attListList;
-
- var toRemove = new List();
-
- foreach (var node in attListList.ToList().Where(x => x != null && x.Attributes != null && x.Attributes.Count > 0))
- {
- var atts = GetModifiedAttributes(com, node.Attributes);
- node.WithAttributes(atts);
-
- if (atts.Count <= 0)
- toRemove.Add(node);
- }
- return new SyntaxList(attListList.Where(x => !toRemove.Contains(x)));
- }
- public static SeparatedSyntaxList GetModifiedAttributes(Compilation com, SeparatedSyntaxList attList)
- {
- var toRemove = new List();
- foreach (var x in attList.ToList().Where(x => x != null).Where(x => IsOurAttribute(com, x)))
- {
- toRemove.Add(x);
- }
- var newList = SyntaxFactory.SeparatedList(attList.Where(x => !toRemove.Contains(x)));
- return newList;
- }
- public static bool IsOurAttribute(Compilation com, AttributeSyntax syntax)
- {
- if (com.GetSemanticModel(syntax.SyntaxTree).GetSymbolInfo(syntax).Symbol is not IMethodSymbol attrSymbol)
- return syntax.ToString().Contains(AutoMapPropertyAttributeName);
-
- var t = com.GetTypeByMetadataName(AutoMapPropertyAttributeName);
- if (t == null)
- throw new Exception("AutoMapPropertyAttributeName is null");
-
- return attrSymbol.ContainingSymbol.Equals(t, SymbolEqualityComparer.Default);
- }
- #endregion
-
- public static string GetFullMethodName(IMethodSymbol method)
- {
- var sb = new StringBuilder();
- sb.Append(method.ContainingType.ToDisplayString()).Append(".").Append(method.Name);
- return sb.ToString();
- }
-
- public static string GetProjectorName(ClassToGenerate cls)
- {
- return cls.Name.ToLower();
- }
- public static string GeneratePropertyFrom(List otherClasses, string Key, ITypeSymbol FromType, ITypeSymbol ToType, bool allowProjectors, List? requiredProjectors, bool nullable = true, string prefix = "")
- {
- var sb = new StringBuilder();
- sb.Append(Key).Append(@" = ");
-
- var concreteToType = GetConcreteType(ToType);
- var concreteFromType = GetConcreteType(FromType);
-
- bool isArray = !SymbolEqualityComparer.Default.Equals(concreteToType, ToType);
-
- if (isArray)
- {
- sb.Append("(").Append(ToType).Append(@")(source").Append(prefix).Append(".").Append(Key).Append(" != null ? ");
- sb.Append("source").Append(prefix).Append(".").Append(Key);
- sb.Append(nullable ? "?" : "");
- if (!SymbolEqualityComparer.Default.Equals(concreteToType, concreteFromType))
- {
- var otherCls = otherClasses.FirstOrDefault(x => x.Name == concreteToType.Name);
- if (otherCls != null && allowProjectors)
- {
- sb.Append(@".Select(x => ").Append(GetProjectorName(otherCls)).Append(@".Invoke(x))");
- sb.Append(nullable ? "?" : "");
- requiredProjectors!.Add(otherCls);
- }
- else
- {
- sb.Append(@".Select(x => new ").Append(GetFullString(concreteToType)).Append(@"().ApplyFrom(providers, x))");
- sb.Append(nullable ? "?" : "");
- }
- }
-
- //Add the appropriate ToHashSet or ToSet or ToCollection or ToList
- if (ToType.Name.Contains("Set"))
- sb.Append(@".ToHashSet()");
- else if (ToType.TypeKind == TypeKind.Array)
- sb.Append(@".ToArray()");
- else
- sb.Append(@".ToList()");
-
- sb.Append(" : ");
- if (ToType.Name.Contains("Set"))
- sb.Append("new HashSet<").Append(GetFullString(concreteToType)).Append(">()");
- else if (ToType.TypeKind == TypeKind.Array)
- sb.Append("Array.Empty<").Append(GetFullString(concreteToType)).Append(">()");
- else
- sb.Append("new List<").Append(GetFullString(concreteToType)).Append(">()");
-
- sb.Append(")");
- }
- else
- {
- if (!otherClasses.Select(x => x.Name).Contains(ToType.Name))
- sb.Append("(").Append(ToType).Append(@")source").Append(prefix).Append(".").Append(Key);
- else
- {
- sb.Append("source").Append(prefix).Append(".").Append(Key).Append(" == null ? null : ");
- var classToGenerate = otherClasses.First(x => x.Name == ToType.Name);
- sb.Append(@"new ").Append(classToGenerate.Name).Append(@"()
- {");
- foreach (var prop in classToGenerate.MappableProperties)
- {
- var property = GenerateCorrectFromProperty(otherClasses, prop.Key, prop.Value, allowProjectors, requiredProjectors, false, prefix + "." + Key);
- if (property == null)
- continue;
-
- sb.Append(@"
- ").Append(property).Append(@",");
- }
- sb.Append(@"
- }");
- }
- }
-
- return sb.ToString();
- }
-
- public static string GetFullString(ITypeSymbol symbol)
- {
- return symbol.ToDisplayString(format: displayFormat);
- }
-
- public static string? GenerateCorrectFromProperty(List otherClasses, string Key, ClassToGenerate.MappableTypes Value, bool allowProjectors, List? requiredProjectors, bool nullable = true, string prefix = "")
- {
- if (!Value.CustomFrom)
- {
- return GeneratePropertyFrom(otherClasses, Key, Value.FromType, Value.ToType, allowProjectors, requiredProjectors, nullable, prefix);
- }
- else
- {
- if (Value.CustomFromMethod == null)
- return null;
-
- StringBuilder sb = new StringBuilder();
- sb.Append(Key).Append(" = (").Append(GetFullString(Value.ToType)).Append(@")").Append(GetFullMethodName(Value.CustomFromMethod)).Append(@"(providers, source").Append(prefix).Append(", source").Append(prefix).Append(".").Append(Key).Append(")");
- return sb.ToString();
- }
- }
-
- private static (string, List) GenerateFromExpression(List otherClasses, ClassToGenerate classToGenerate, SyntaxToken sourceName)
- {
- var projectors = new List();
- var exprSb = new StringBuilder();
- exprSb.Append(@"System.Linq.Expressions.Expression> expr = (").Append(sourceName).Append(" source)=>new ").Append(classToGenerate.Name).Append(@"()
- {");
- foreach (var prop in classToGenerate.MappableProperties)
- {
- var property = GenerateCorrectFromProperty(otherClasses, prop.Key, prop.Value, true, projectors, false);
- if (property == null)
- continue;
-
- exprSb.Append(@"
- ").Append(property).Append(@",");
- }
- exprSb.Append(@"
- };
- return expr.Expand();
- ");
-
- return (exprSb.ToString(), projectors);
- }
-
- public static string? GeneratePropertyTo(ClassToGenerate classToGenerate, List otherClasses, string Key, ITypeSymbol FromType, ITypeSymbol ToType, bool nullable = true)
- {
-
-
- var concreteToType = GetConcreteType(ToType);
- var concreteFromType = GetConcreteType(FromType);
-
- bool isArray = !SymbolEqualityComparer.Default.Equals(FromType, concreteFromType);
-
- if (concreteFromType.IsAbstract)
- return null;
-
- var sb = new StringBuilder();
-
- if (isArray)
- {
- sb.Append(Key).Append("?.Any(); source.");
- }
-
- sb.Append(Key).Append(@" =");
-
- if (isArray)
- {
- sb.Append("(").Append(FromType).Append(@")(this.").Append(Key).Append(" != null ? ");
- sb.Append("this.").Append(Key);
- sb.Append(nullable ? "?" : "");
- if (!SymbolEqualityComparer.Default.Equals(concreteToType, concreteFromType))
- {
- sb.Append(@".Select(x => (").Append(GetFullString(concreteFromType)).Append(@")x.ApplyTo(providers, (").Append(GetFullString(concreteFromType)).Append(@")(AutoMapProperty.AutoMapPropertyUtils.Create<").Append(GetFullString(concreteFromType)).Append(",").Append(classToGenerate.SourceContainer.Identifier).Append(">(providers, source, \"").Append(Key).Append("\"))))");
- sb.Append(nullable ? "?" : "");
- }
-
- //Add the appropriate ToHashSet or ToSet or ToCollection or ToList
- if (FromType.Name.Contains("Set"))
- sb.Append(@".ToHashSet()");
- else if (FromType.TypeKind == TypeKind.Array)
- sb.Append(@".ToArray()");
- else
- sb.Append(@".ToList()");
-
- sb.Append(" : ");
- if (FromType.Name.Contains("Set"))
- sb.Append("new HashSet<").Append(GetFullString(concreteFromType)).Append(">()");
- else if (FromType.TypeKind == TypeKind.Array)
- sb.Append("Array.Empty<").Append(GetFullString(concreteFromType)).Append(">()");
- else
- sb.Append("new List<").Append(GetFullString(concreteFromType)).Append(">()");
-
- sb.Append(")");
- }
- else
- {
- if (otherClasses.Select(x => x.Name).Contains(ToType.Name))
- sb.Append("this.").Append(Key).Append(".ApplyTo(providers, (").Append(GetFullString(FromType)).Append(@")(source.").Append(Key).Append(" != null ? source.").Append(Key).Append(" : AutoMapProperty.AutoMapPropertyUtils.Create<").Append(GetFullString(FromType)).Append(",").Append(classToGenerate.SourceContainer.Identifier).Append(">(providers, source, \"").Append(Key).Append("\")))");
- else
- sb.Append("(").Append(GetFullString(FromType)).Append(@")this.").Append(Key);
- }
- return sb.ToString();
- }
- public static string? GenerateCorrectToProperty(ClassToGenerate classToGenerate, List otherClasses, string Key, ClassToGenerate.MappableTypes Value, bool nullable = true)
- {
- var retSb = new StringBuilder();
- retSb.Append("source.");
- if (!Value.CustomTo)
- {
- if (Value.FromIsReadOnly)
- return null;
- var generated = GeneratePropertyTo(classToGenerate, otherClasses, Key, Value.FromType, Value.ToType, nullable);
- if (generated == null)
- return null;
- retSb.Append(generated);
- }
- else
- {
- if (Value.CustomToMethod == null)
- return null;
-
- //if it's readonly we are probably just mutating some other data based of of this one
- if (Value.FromIsReadOnly)
- {
- var callerSb = new StringBuilder();
- callerSb.Append(GetFullMethodName(Value.CustomToMethod)).Append(@"(providers, source, this.").Append(Key).Append(");");
- return callerSb.ToString();
- }
-
- StringBuilder sb = new StringBuilder();
- sb.Append(Key).Append(" = (").Append(GetFullString(Value.FromType)).Append(@")").Append(GetFullMethodName(Value.CustomToMethod)).Append(@"(providers, source, this.").Append(Key).Append(")");
- retSb.Append(sb.ToString());
- }
- retSb.Append(";");
- return retSb.ToString();
- }
-
- public static string? GenerateEqualityOperator(string name, ClassToGenerate.MappableTypes Value, string? prefix = null)
- {
- var prefixedName = prefix == null ? name : prefix + "." + name;
- //if the type is also being generated we actually want to use the generated EqualityExpression
- if (Value.OurTargetType != null)
- {
- var concrecteType = GetConcreteType(Value.ToType);
- //cannot process collections/arrays
- if (SymbolEqualityComparer.Default.Equals(concrecteType, Value.ToType))
- {
- var childSb = new StringBuilder();
-
- foreach (var prop in Value.OurTargetType.MappableProperties)
- {
- var op = GenerateEqualityOperator(prop.Key, prop.Value, prefixedName);
- if (string.IsNullOrEmpty(op))
- {
- continue;
- }
- childSb.Append(@"
- ").Append(op).Append(" && ");
- }
- childSb.Append(@"true");
- return childSb.ToString();
- }
- else
- return null;
- }
-
- if (!SymbolEqualityComparer.Default.Equals(Value.FromType, Value.ToType))
- {
- return null;
- }
-
- var sb = new StringBuilder();
- sb.Append("item1.").Append(prefixedName).Append(" == item2.").Append(prefixedName);
-
- return sb.ToString();
- }
-
- public static string GenerateExtensionClass(List otherClasses, ClassToGenerate classToGenerate)
- {
- var sb = new StringBuilder();
-
- foreach (var usi in classToGenerate.Usings)
- {
- sb.AppendLine(usi);
- }
-
- sb.Append(@"
-using LinqKit;
-#nullable enable
-
-#pragma warning disable CS8600
-#pragma warning disable CS8601
-#pragma warning disable CS8602
-#pragma warning disable CS8603
-#pragma warning disable CS8604
-#pragma warning disable CS8618
-#pragma warning disable CS8625
-
-#pragma warning disable CS0109
-#pragma warning disable CS8767
-#pragma warning disable CS0659
-
-namespace ").Append(classToGenerate.Namespace).Append(@"
-{
- [System.CodeDom.Compiler.GeneratedCode(""AutoMapProperty"", ""1.0.0"")]
- public partial ").Append(classToGenerate.SourceContainer is ClassDeclarationSyntax ? "class " : "interface ").Append(classToGenerate.Name);
-
- if (classToGenerate.SourceContainer is ClassDeclarationSyntax)//&& !classToGenerate.SourceContainer.Modifiers.Any(x => x.IsKind(SyntaxKind.AbstractKeyword)))
- {
- sb.Append(": IAutomappedAttribute<").Append(classToGenerate.SourceContainer.Identifier).Append(",").Append(classToGenerate.Name).Append(">");
- sb.Append(", System.Collections.Generic.IEqualityComparer<").Append(classToGenerate.Name).Append(">");
- }
- sb.Append(@"
- {");
- foreach (var prop in classToGenerate.Properties)
- {
- sb.Append(@"
- ").Append(prop.prop.ToString());
- }
- if (classToGenerate.SourceContainer is ClassDeclarationSyntax cds)
- {
- sb.Append(@"
-
- public static implicit operator ").Append(classToGenerate.Name).Append(@"(").Append(classToGenerate.SourceContainer.Identifier).Append(@" source)
- {
- var dat = new ").Append(classToGenerate.Name).Append(@"();
- return dat.ApplyFrom(null, source);
- }");
- sb.Append(@"
- object? IAutomappedAttribute.ApplyFrom(System.IServiceProvider? providers, object source)
- {
- if(source is ").Append(classToGenerate.SourceContainer.Identifier).Append(@" src)
- return ApplyFrom(providers, src);
- return null;
- }
- public new ").Append(classToGenerate.Name).Append(@" ApplyFrom(System.IServiceProvider? providers, ").Append(classToGenerate.SourceContainer.Identifier).Append(@" source)
- {
- if(source == null)
- return null;
- {");
- foreach (var prop in classToGenerate.MappableProperties)
- {
- var property = GenerateCorrectFromProperty(otherClasses, prop.Key, prop.Value, false, null);
- if (property == null)
- continue;
-
- sb.Append(@"
- ").Append(property).Append(@";");
- }
- sb.Append(@"
- }
- return this;
- }");
- sb.Append(@"
- public new System.Linq.Expressions.Expression> GetProjectorFrom(System.IServiceProvider providers) {
-
- ");
- var (expr, projectors) = GenerateFromExpression(otherClasses, classToGenerate, classToGenerate.SourceContainer.Identifier);
-
- foreach (var projector in projectors.Distinct())
- {
- sb.Append("var ").Append(GetProjectorName(projector)).Append(@" = new ").Append(projector.Name).Append(@"().GetProjectorFrom(providers);
- ");
- }
- sb.Append(expr);
- sb.Append(@"
- }");
-
- sb.Append(@"
- public static new System.Linq.Expressions.Expression> EqualExpression(System.IServiceProvider providers,").Append(classToGenerate.SourceContainer.Identifier).Append(@" item1) => (item2)=>
- (");
- foreach (var prop in classToGenerate.MappableProperties)
- {
- var op = GenerateEqualityOperator(prop.Key, prop.Value);
- if (string.IsNullOrEmpty(op))
- {
- continue;
- }
- sb.Append(@"
- ").Append(op).Append(" && ");
- }
- sb.Append(@"true
- );");
-
- sb.Append(@"
- public static new System.Linq.Expressions.Expression> EqualExpression(System.IServiceProvider providers,").Append(classToGenerate.Name).Append(@" item1) => (item2)=>
- (");
- foreach (var prop in classToGenerate.MappableProperties)
- {
- var op = GenerateEqualityOperator(prop.Key, prop.Value);
- if (string.IsNullOrEmpty(op))
- {
- continue;
- }
- sb.Append(@"
- ").Append(op).Append(" && ");
- }
- sb.Append(@"true
- );");
-
- sb.Append(@"
- public override bool Equals(object? obj){
- if(obj == null)
- return false;
-
- if (ReferenceEquals(obj, this))
- return true;
-
- ").Append(classToGenerate.Name).Append(@" item2 = obj as ").Append(classToGenerate.Name).Append(@";
- var item1 = this;
-
- return (");
- foreach (var prop in classToGenerate.MappableProperties)
- {
- var op = GenerateEqualityOperator(prop.Key, prop.Value);
- if (string.IsNullOrEmpty(op))
- {
- continue;
- }
- sb.Append(@"
- ").Append(op).Append(" && ");
- }
- sb.Append(@"true
- );
- }");
- sb.Append(@"
- public bool Equals(").Append(classToGenerate.Name).Append(" item1, ").Append(classToGenerate.Name).Append(@" item2){
-
- if (item1 is null || item2 is null)
- {
- return false;
- }
-
- if (ReferenceEquals(item1, item2))
- return true;
-
- return (");
- foreach (var prop in classToGenerate.MappableProperties)
- {
- var op = GenerateEqualityOperator(prop.Key, prop.Value);
- if (string.IsNullOrEmpty(op))
- {
- continue;
- }
- sb.Append(@"
- ").Append(op).Append(" && ");
- }
- sb.Append(@"true
- );
- }");
-
- sb.Append(@"
- public int GetHashCode(").Append(classToGenerate.Name).Append(@" obj)
- {
- return obj.GetHashCode();
- }
- public override int GetHashCode()
- {
- return 7;
- }
-
- object IAutomappedAttribute.ApplyTo(System.IServiceProvider? providers, object source)
- {
- if(source == null)
- return null;
- if(source is ").Append(classToGenerate.SourceContainer.Identifier).Append(@" src)
- return ApplyTo(providers, src);
- return null;
- }
- public new ").Append(classToGenerate.SourceContainer.Identifier).Append(@" ApplyTo(System.IServiceProvider? providers, ").Append(classToGenerate.SourceContainer.Identifier).Append(@" source)
- {
- if(source == null)
- return null;
- {");
- foreach (var prop in classToGenerate.MappableProperties)
- {/*
- if (prop.Value.FromType.IsAbstract)
- continue;*/
- var property = GenerateCorrectToProperty(classToGenerate, otherClasses, prop.Key, prop.Value);
- if (property == null)
- continue;
- sb.Append(@"
- ").Append(property);
- }
- sb.Append(@"
- }
- return source;
- }
- Type IAutomappedAttribute.GetSourceType() => typeof(").Append(classToGenerate.SourceContainer.Identifier).Append(@");
- ");
- sb.Append(@"
-
- ");
-
- var abstr = cds.Modifiers.Any(x => x.IsKind(SyntaxKind.AbstractKeyword));
- if (!abstr)
- {
-
- sb.Append(@"
- public ").Append(classToGenerate.SourceContainer.Identifier).Append(@" Create(System.IServiceProvider? providers = null)
- {
- var dat = AutoMapProperty.AutoMapPropertyUtils.Create<").Append(classToGenerate.SourceContainer.Identifier).Append(@">(providers);
- return ApplyTo(providers, dat);
- }
- public static explicit operator ").Append(classToGenerate.SourceContainer.Identifier).Append(@"(").Append(classToGenerate.Name).Append(@" source)
- {
- return source.Create(null);
- }");
- }
- }
-
- sb.Append(@"
- }
-}
-#pragma warning restore CS8600
-#pragma warning restore CS8601
-#pragma warning restore CS8602
-#pragma warning restore CS8603
-#pragma warning restore CS8604
-#pragma warning restore CS8618
-#pragma warning restore CS8625
-
-#pragma warning restore CS0109
-#pragma warning restore CS8767
-#pragma warning restore CS0659
-
-#nullable disable
-");
-
- return sb.ToString();
- }
-
- private class TypeReplacer : CSharpSyntaxRewriter
- {
- private readonly string _oldTypeName;
- private readonly string _newTypeName;
-
- public TypeReplacer(string oldTypeName, string newTypeName)
- {
- _oldTypeName = oldTypeName;
- _newTypeName = newTypeName;
- }
-
- public override SyntaxNode? VisitIdentifierName(IdentifierNameSyntax node)
- {
- if (node.Identifier.ValueText == _oldTypeName)
- {
- return SyntaxFactory.IdentifierName(_newTypeName).WithTriviaFrom(node);
- }
- return base.VisitIdentifierName(node);
- }
- }
- }
-
- public static class Utils
- {
- public static string GetFullMetadataName(this ISymbol s)
- {
- if (s == null || IsRootNamespace(s))
- {
- return string.Empty;
- }
-
- var sb = new StringBuilder(s.MetadataName);
- var last = s;
-
- s = s.ContainingSymbol;
-
- while (!IsRootNamespace(s))
- {
- if (s is ITypeSymbol && last is ITypeSymbol)
- {
- sb.Insert(0, '+');
- }
- else
- {
- sb.Insert(0, '.');
- }
-
- sb.Insert(0, s.OriginalDefinition.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat));
- //sb.Insert(0, s.MetadataName);
- s = s.ContainingSymbol;
- }
-
- return sb.ToString();
- }
-
- private static bool IsRootNamespace(ISymbol symbol)
- {
- INamespaceSymbol s = null;
- return ((s = symbol as INamespaceSymbol) != null) && s.IsGlobalNamespace;
- }
- }
-}
diff --git a/AutoMapProperty/AutoMapPropertyGenerator.cs b/AutoMapProperty/AutoMapPropertyGenerator.cs
new file mode 100644
index 0000000..f220219
--- /dev/null
+++ b/AutoMapProperty/AutoMapPropertyGenerator.cs
@@ -0,0 +1,32 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Text;
+using System.Text;
+
+namespace AutoMapProperty;
+
+[Generator]
+public class AutoMapPropertyGenerator : IIncrementalGenerator
+{
+ internal const string AttributeName = "AutoMapPropertyHelper.AutoMapPropertyAttribute";
+
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ var properties = context.SyntaxProvider.ForAttributeWithMetadataName(
+ AttributeName,
+ static (node, _) => node is PropertyDeclarationSyntax,
+ static (context, _) => (PropertyDeclarationSyntax)context.TargetNode);
+
+ context.RegisterSourceOutput(context.CompilationProvider.Combine(properties.Collect()),
+ static (context, input) =>
+ {
+ var types = MappingModelBuilder.Build(input.Left, input.Right, context);
+ foreach (var type in types)
+ {
+ context.CancellationToken.ThrowIfCancellationRequested();
+ context.AddSource(type.Name + ".g.cs",
+ SourceText.From(MappingEmitter.GenerateExtensionClass(types, type), Encoding.UTF8));
+ }
+ });
+ }
+}
diff --git a/AutoMapProperty/MappingEmitter.cs b/AutoMapProperty/MappingEmitter.cs
new file mode 100644
index 0000000..7468dfc
--- /dev/null
+++ b/AutoMapProperty/MappingEmitter.cs
@@ -0,0 +1,264 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using System.Text;
+using static AutoMapProperty.MappingExpressionEmitter;
+
+namespace AutoMapProperty;
+
+internal static class MappingEmitter
+{
+ public static string GenerateExtensionClass(List otherClasses, GeneratedType classToGenerate)
+ {
+ var sb = new StringBuilder();
+
+ foreach (var usi in classToGenerate.Usings)
+ {
+ sb.AppendLine(usi);
+ }
+
+ sb.Append(@"
+using LinqKit;
+#nullable enable
+
+#pragma warning disable CS8600
+#pragma warning disable CS8601
+#pragma warning disable CS8602
+#pragma warning disable CS8603
+#pragma warning disable CS8604
+#pragma warning disable CS8618
+#pragma warning disable CS8625
+
+#pragma warning disable CS0109
+#pragma warning disable CS8767
+#pragma warning disable CS0659
+
+namespace ").Append(classToGenerate.Namespace).Append(@"
+{
+ [System.CodeDom.Compiler.GeneratedCode(""AutoMapProperty"", ""1.0.0"")]
+ public partial ").Append(classToGenerate.SourceContainer is ClassDeclarationSyntax ? "class " : "interface ").Append(classToGenerate.Name);
+
+ if (classToGenerate.SourceContainer is ClassDeclarationSyntax)
+ {
+ sb.Append(": IAutomappedAttribute<").Append(classToGenerate.SourceContainer.Identifier).Append(",").Append(classToGenerate.Name).Append(">");
+ sb.Append(", System.Collections.Generic.IEqualityComparer<").Append(classToGenerate.Name).Append(">");
+ }
+ sb.Append(@"
+ {");
+ foreach (var prop in classToGenerate.Properties)
+ {
+ sb.Append(@"
+ ").Append(prop.Declaration.ToString());
+ }
+ if (classToGenerate.SourceContainer is ClassDeclarationSyntax cds)
+ {
+ sb.Append(@"
+
+ public static implicit operator ").Append(classToGenerate.Name).Append(@"(").Append(classToGenerate.SourceContainer.Identifier).Append(@" source)
+ {
+ var dat = new ").Append(classToGenerate.Name).Append(@"();
+ return dat.ApplyFrom(null, source);
+ }");
+ sb.Append(@"
+ object? IAutomappedAttribute.ApplyFrom(System.IServiceProvider? providers, object source)
+ {
+ if(source is ").Append(classToGenerate.SourceContainer.Identifier).Append(@" src)
+ return ApplyFrom(providers, src);
+ return null;
+ }
+ public new ").Append(classToGenerate.Name).Append(@" ApplyFrom(System.IServiceProvider? providers, ").Append(classToGenerate.SourceContainer.Identifier).Append(@" source)
+ {
+ if(source == null)
+ return null;
+ {");
+ foreach (var prop in classToGenerate.MappableProperties)
+ {
+ var property = GenerateCorrectFromProperty(otherClasses, prop.Key, prop.Value, false, null);
+ if (property == null)
+ continue;
+
+ sb.Append(@"
+ ").Append(property).Append(@";");
+ }
+ sb.Append(@"
+ }
+ return this;
+ }");
+ sb.Append(@"
+ public new System.Linq.Expressions.Expression> GetProjectorFrom(System.IServiceProvider providers) {
+
+ ");
+ var (expr, projectors) = GenerateFromExpression(otherClasses, classToGenerate, classToGenerate.SourceContainer.Identifier);
+
+ foreach (var projector in projectors.Distinct())
+ {
+ sb.Append("var ").Append(GetProjectorName(projector)).Append(@" = new ").Append(projector.Name).Append(@"().GetProjectorFrom(providers);
+ ");
+ }
+ sb.Append(expr);
+ sb.Append(@"
+ }");
+
+ sb.Append(@"
+ public static new System.Linq.Expressions.Expression> EqualExpression(System.IServiceProvider providers,").Append(classToGenerate.SourceContainer.Identifier).Append(@" item1) => (item2)=>
+ (");
+ foreach (var prop in classToGenerate.MappableProperties)
+ {
+ var op = GenerateEqualityOperator(prop.Key, prop.Value);
+ if (string.IsNullOrEmpty(op))
+ {
+ continue;
+ }
+ sb.Append(@"
+ ").Append(op).Append(" && ");
+ }
+ sb.Append(@"true
+ );");
+
+ sb.Append(@"
+ public static new System.Linq.Expressions.Expression> EqualExpression(System.IServiceProvider providers,").Append(classToGenerate.Name).Append(@" item1) => (item2)=>
+ (");
+ foreach (var prop in classToGenerate.MappableProperties)
+ {
+ var op = GenerateEqualityOperator(prop.Key, prop.Value);
+ if (string.IsNullOrEmpty(op))
+ {
+ continue;
+ }
+ sb.Append(@"
+ ").Append(op).Append(" && ");
+ }
+ sb.Append(@"true
+ );");
+
+ sb.Append(@"
+ public override bool Equals(object? obj){
+ if(obj == null)
+ return false;
+
+ if (ReferenceEquals(obj, this))
+ return true;
+
+ ").Append(classToGenerate.Name).Append(@" item2 = obj as ").Append(classToGenerate.Name).Append(@";
+ var item1 = this;
+
+ return (");
+ foreach (var prop in classToGenerate.MappableProperties)
+ {
+ var op = GenerateEqualityOperator(prop.Key, prop.Value);
+ if (string.IsNullOrEmpty(op))
+ {
+ continue;
+ }
+ sb.Append(@"
+ ").Append(op).Append(" && ");
+ }
+ sb.Append(@"true
+ );
+ }");
+ sb.Append(@"
+ public bool Equals(").Append(classToGenerate.Name).Append(" item1, ").Append(classToGenerate.Name).Append(@" item2){
+
+ if (item1 is null || item2 is null)
+ {
+ return false;
+ }
+
+ if (ReferenceEquals(item1, item2))
+ return true;
+
+ return (");
+ foreach (var prop in classToGenerate.MappableProperties)
+ {
+ var op = GenerateEqualityOperator(prop.Key, prop.Value);
+ if (string.IsNullOrEmpty(op))
+ {
+ continue;
+ }
+ sb.Append(@"
+ ").Append(op).Append(" && ");
+ }
+ sb.Append(@"true
+ );
+ }");
+
+ sb.Append(@"
+ public int GetHashCode(").Append(classToGenerate.Name).Append(@" obj)
+ {
+ return obj.GetHashCode();
+ }
+ public override int GetHashCode()
+ {
+ return 7;
+ }
+
+ object IAutomappedAttribute.ApplyTo(System.IServiceProvider? providers, object source)
+ {
+ if(source == null)
+ return null;
+ if(source is ").Append(classToGenerate.SourceContainer.Identifier).Append(@" src)
+ return ApplyTo(providers, src);
+ return null;
+ }
+ public new ").Append(classToGenerate.SourceContainer.Identifier).Append(@" ApplyTo(System.IServiceProvider? providers, ").Append(classToGenerate.SourceContainer.Identifier).Append(@" source)
+ {
+ if(source == null)
+ return null;
+ {");
+ foreach (var prop in classToGenerate.MappableProperties)
+ {
+ var property = GenerateCorrectToProperty(classToGenerate, otherClasses, prop.Key, prop.Value);
+ if (property == null)
+ continue;
+ sb.Append(@"
+ ").Append(property);
+ }
+ sb.Append(@"
+ }
+ return source;
+ }
+ Type IAutomappedAttribute.GetSourceType() => typeof(").Append(classToGenerate.SourceContainer.Identifier).Append(@");
+ ");
+ sb.Append(@"
+
+ ");
+
+ var abstr = cds.Modifiers.Any(x => x.IsKind(SyntaxKind.AbstractKeyword));
+ if (!abstr)
+ {
+
+ sb.Append(@"
+ public ").Append(classToGenerate.SourceContainer.Identifier).Append(@" Create(System.IServiceProvider? providers = null)
+ {
+ var dat = AutoMapProperty.AutoMapPropertyUtils.Create<").Append(classToGenerate.SourceContainer.Identifier).Append(@">(providers);
+ return ApplyTo(providers, dat);
+ }
+ public static explicit operator ").Append(classToGenerate.SourceContainer.Identifier).Append(@"(").Append(classToGenerate.Name).Append(@" source)
+ {
+ return source.Create(null);
+ }");
+ }
+ }
+
+ sb.Append(@"
+ }
+}
+#pragma warning restore CS8600
+#pragma warning restore CS8601
+#pragma warning restore CS8602
+#pragma warning restore CS8603
+#pragma warning restore CS8604
+#pragma warning restore CS8618
+#pragma warning restore CS8625
+
+#pragma warning restore CS0109
+#pragma warning restore CS8767
+#pragma warning restore CS0659
+
+#nullable disable
+");
+
+ return sb.ToString();
+ }
+
+}
diff --git a/AutoMapProperty/MappingExpressionEmitter.cs b/AutoMapProperty/MappingExpressionEmitter.cs
new file mode 100644
index 0000000..5d8074d
--- /dev/null
+++ b/AutoMapProperty/MappingExpressionEmitter.cs
@@ -0,0 +1,277 @@
+using Microsoft.CodeAnalysis;
+using System.Text;
+using static AutoMapProperty.TypeSymbolHelpers;
+
+namespace AutoMapProperty;
+
+internal static class MappingExpressionEmitter
+{
+ public static string GetFullMethodName(IMethodSymbol method)
+ {
+ var sb = new StringBuilder();
+ sb.Append(method.ContainingType.ToDisplayString()).Append(".").Append(method.Name);
+ return sb.ToString();
+ }
+
+ public static string GetProjectorName(GeneratedType cls)
+ {
+ return cls.Name.ToLower();
+ }
+ public static string GeneratePropertyFrom(List otherClasses, string Key, ITypeSymbol FromType, ITypeSymbol ToType, bool allowProjectors, List? requiredProjectors, bool useNullConditionalAccess = true, string prefix = "")
+ {
+ var sb = new StringBuilder();
+ sb.Append(Key).Append(@" = ");
+
+ var concreteToType = GetConcreteType(ToType);
+ var concreteFromType = GetConcreteType(FromType);
+
+ bool isArray = !SymbolEqualityComparer.Default.Equals(concreteToType, ToType);
+
+ if (isArray)
+ {
+ sb.Append("(").Append(ToType).Append(@")(source").Append(prefix).Append(".").Append(Key).Append(" != null ? ");
+ sb.Append("source").Append(prefix).Append(".").Append(Key);
+ sb.Append(useNullConditionalAccess ? "?" : "");
+ if (!SymbolEqualityComparer.Default.Equals(concreteToType, concreteFromType))
+ {
+ var otherCls = otherClasses.FirstOrDefault(x => x.Name == concreteToType.Name);
+ if (otherCls != null && allowProjectors)
+ {
+ sb.Append(@".Select(x => ").Append(GetProjectorName(otherCls)).Append(@".Invoke(x))");
+ sb.Append(useNullConditionalAccess ? "?" : "");
+ requiredProjectors!.Add(otherCls);
+ }
+ else
+ {
+ sb.Append(@".Select(x => new ").Append(GetFullString(concreteToType)).Append(@"().ApplyFrom(providers, x))");
+ sb.Append(useNullConditionalAccess ? "?" : "");
+ }
+ }
+
+ //Add the appropriate ToHashSet or ToSet or ToCollection or ToList
+ if (ToType.Name.Contains("Set"))
+ sb.Append(@".ToHashSet()");
+ else if (ToType.TypeKind == TypeKind.Array)
+ sb.Append(@".ToArray()");
+ else
+ sb.Append(@".ToList()");
+
+ sb.Append(" : ");
+ if (ToType.Name.Contains("Set"))
+ sb.Append("new HashSet<").Append(GetFullString(concreteToType)).Append(">()");
+ else if (ToType.TypeKind == TypeKind.Array)
+ sb.Append("Array.Empty<").Append(GetFullString(concreteToType)).Append(">()");
+ else
+ sb.Append("new List<").Append(GetFullString(concreteToType)).Append(">()");
+
+ sb.Append(")");
+ }
+ else
+ {
+ if (!otherClasses.Select(x => x.Name).Contains(ToType.Name))
+ sb.Append("(").Append(ToType).Append(@")source").Append(prefix).Append(".").Append(Key);
+ else
+ {
+ sb.Append("source").Append(prefix).Append(".").Append(Key).Append(" == null ? null : ");
+ var classToGenerate = otherClasses.First(x => x.Name == ToType.Name);
+ sb.Append(@"new ").Append(classToGenerate.Name).Append(@"()
+ {");
+ foreach (var prop in classToGenerate.MappableProperties)
+ {
+ var property = GenerateCorrectFromProperty(otherClasses, prop.Key, prop.Value, allowProjectors, requiredProjectors, false, prefix + "." + Key);
+ if (property == null)
+ continue;
+
+ sb.Append(@"
+ ").Append(property).Append(@",");
+ }
+ sb.Append(@"
+ }");
+ }
+ }
+
+ return sb.ToString();
+ }
+
+ public static string? GenerateCorrectFromProperty(List otherClasses, string Key, MappableProperty Value, bool allowProjectors, List? requiredProjectors, bool useNullConditionalAccess = true, string prefix = "")
+ {
+ if (!Value.CustomFrom)
+ {
+ return GeneratePropertyFrom(otherClasses, Key, Value.FromType, Value.ToType, allowProjectors, requiredProjectors, useNullConditionalAccess, prefix);
+ }
+ else
+ {
+ if (Value.CustomFromMethod == null)
+ return null;
+
+ StringBuilder sb = new StringBuilder();
+ sb.Append(Key).Append(" = (").Append(GetFullString(Value.ToType)).Append(@")").Append(GetFullMethodName(Value.CustomFromMethod)).Append(@"(providers, source").Append(prefix).Append(", source").Append(prefix).Append(".").Append(Key).Append(")");
+ return sb.ToString();
+ }
+ }
+
+ public static (string, List) GenerateFromExpression(List otherClasses, GeneratedType classToGenerate, SyntaxToken sourceName)
+ {
+ var projectors = new List();
+ var exprSb = new StringBuilder();
+ exprSb.Append(@"System.Linq.Expressions.Expression> expr = (").Append(sourceName).Append(" source)=>new ").Append(classToGenerate.Name).Append(@"()
+ {");
+ foreach (var prop in classToGenerate.MappableProperties)
+ {
+ var property = GenerateCorrectFromProperty(otherClasses, prop.Key, prop.Value, true, projectors, false);
+ if (property == null)
+ continue;
+
+ exprSb.Append(@"
+ ").Append(property).Append(@",");
+ }
+ exprSb.Append(@"
+ };
+ return expr.Expand();
+ ");
+
+ return (exprSb.ToString(), projectors);
+ }
+
+ public static string? GeneratePropertyTo(GeneratedType classToGenerate, List otherClasses, string Key, ITypeSymbol FromType, ITypeSymbol ToType, bool useNullConditionalAccess = true)
+ {
+ var concreteToType = GetConcreteType(ToType);
+ var concreteFromType = GetConcreteType(FromType);
+
+ bool isArray = !SymbolEqualityComparer.Default.Equals(FromType, concreteFromType);
+
+ if (concreteFromType.IsAbstract)
+ return null;
+
+ var sb = new StringBuilder();
+
+ if (isArray)
+ {
+ sb.Append(Key).Append("?.Any(); source.");
+ }
+
+ sb.Append(Key).Append(@" =");
+
+ if (isArray)
+ {
+ sb.Append("(").Append(FromType).Append(@")(this.").Append(Key).Append(" != null ? ");
+ sb.Append("this.").Append(Key);
+ sb.Append(useNullConditionalAccess ? "?" : "");
+ if (!SymbolEqualityComparer.Default.Equals(concreteToType, concreteFromType))
+ {
+ sb.Append(@".Select(x => (").Append(GetFullString(concreteFromType)).Append(@")x.ApplyTo(providers, (").Append(GetFullString(concreteFromType)).Append(@")(AutoMapProperty.AutoMapPropertyUtils.Create<").Append(GetFullString(concreteFromType)).Append(",").Append(classToGenerate.SourceContainer.Identifier).Append(">(providers, source, \"").Append(Key).Append("\"))))");
+ sb.Append(useNullConditionalAccess ? "?" : "");
+ }
+
+ //Add the appropriate ToHashSet or ToSet or ToCollection or ToList
+ if (FromType.Name.Contains("Set"))
+ sb.Append(@".ToHashSet()");
+ else if (FromType.TypeKind == TypeKind.Array)
+ sb.Append(@".ToArray()");
+ else
+ sb.Append(@".ToList()");
+
+ sb.Append(" : ");
+ if (FromType.Name.Contains("Set"))
+ sb.Append("new HashSet<").Append(GetFullString(concreteFromType)).Append(">()");
+ else if (FromType.TypeKind == TypeKind.Array)
+ sb.Append("Array.Empty<").Append(GetFullString(concreteFromType)).Append(">()");
+ else
+ sb.Append("new List<").Append(GetFullString(concreteFromType)).Append(">()");
+
+ sb.Append(")");
+ }
+ else
+ {
+ if (otherClasses.Select(x => x.Name).Contains(ToType.Name))
+ {
+ if (ToType.NullableAnnotation == NullableAnnotation.Annotated)
+ sb.Append("this.").Append(Key).Append(" == null ? null : ");
+ sb.Append("this.").Append(Key).Append(".ApplyTo(providers, (").Append(GetFullString(FromType)).Append(@")(source.").Append(Key).Append(" != null ? source.").Append(Key).Append(" : AutoMapProperty.AutoMapPropertyUtils.Create<").Append(GetFullString(FromType)).Append(",").Append(classToGenerate.SourceContainer.Identifier).Append(">(providers, source, \"").Append(Key).Append("\")))");
+ }
+ else
+ sb.Append("(").Append(GetFullString(FromType)).Append(@")this.").Append(Key);
+ }
+ return sb.ToString();
+ }
+ public static string? GenerateCorrectToProperty(GeneratedType classToGenerate, List otherClasses, string Key, MappableProperty Value, bool useNullConditionalAccess = true)
+ {
+ var retSb = new StringBuilder();
+ retSb.Append("source.");
+ if (!Value.CustomTo)
+ {
+ if (Value.SourceIsReadOnly)
+ return null;
+ var generated = GeneratePropertyTo(classToGenerate, otherClasses, Key, Value.FromType, Value.ToType, useNullConditionalAccess);
+ if (generated == null)
+ return null;
+ retSb.Append(generated);
+ }
+ else
+ {
+ if (Value.CustomToMethod == null)
+ return null;
+
+ //if it's readonly we are probably just mutating some other data based of of this one
+ if (Value.SourceIsReadOnly)
+ {
+ var callerSb = new StringBuilder();
+ callerSb.Append(GetFullMethodName(Value.CustomToMethod)).Append(@"(providers, source, this.").Append(Key).Append(");");
+ return callerSb.ToString();
+ }
+
+ StringBuilder sb = new StringBuilder();
+ sb.Append(Key).Append(" = (").Append(GetFullString(Value.FromType)).Append(@")").Append(GetFullMethodName(Value.CustomToMethod)).Append(@"(providers, source, this.").Append(Key).Append(")");
+ retSb.Append(sb.ToString());
+ }
+ retSb.Append(";");
+ return retSb.ToString();
+ }
+
+ public static string? GenerateEqualityOperator(string name, MappableProperty Value, string? prefix = null)
+ {
+ var prefixedName = prefix == null ? name : prefix + "." + name;
+ //if the type is also being generated we actually want to use the generated EqualityExpression
+ if (Value.NestedType != null)
+ {
+ var concreteType = GetConcreteType(Value.ToType);
+ //cannot process collections/arrays
+ if (SymbolEqualityComparer.Default.Equals(concreteType, Value.ToType))
+ {
+ var childSb = new StringBuilder();
+
+ foreach (var prop in Value.NestedType.MappableProperties)
+ {
+ var op = GenerateEqualityOperator(prop.Key, prop.Value, prefixedName);
+ if (string.IsNullOrEmpty(op))
+ {
+ continue;
+ }
+ childSb.Append(@"
+ ").Append(op).Append(" && ");
+ }
+ childSb.Append(@"true");
+ if (Value.FromType.NullableAnnotation == NullableAnnotation.Annotated ||
+ Value.ToType.NullableAnnotation == NullableAnnotation.Annotated)
+ {
+ return "(item1." + prefixedName + " == null ? item2." + prefixedName +
+ " == null : item2." + prefixedName + " != null && (" + childSb + "))";
+ }
+ return childSb.ToString();
+ }
+ else
+ return null;
+ }
+
+ if (!SymbolEqualityComparer.Default.Equals(Value.FromType, Value.ToType))
+ {
+ return null;
+ }
+
+ var sb = new StringBuilder();
+ sb.Append("item1.").Append(prefixedName).Append(" == item2.").Append(prefixedName);
+
+ return sb.ToString();
+ }
+
+}
diff --git a/AutoMapProperty/MappingModel.cs b/AutoMapProperty/MappingModel.cs
new file mode 100644
index 0000000..74bc3cb
--- /dev/null
+++ b/AutoMapProperty/MappingModel.cs
@@ -0,0 +1,66 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace AutoMapProperty;
+
+internal sealed class GeneratedType
+{
+ public TypeDeclarationSyntax SourceContainer { get; }
+ public List Usings { get; }
+ public string Namespace { get; }
+ public string Name { get; }
+ public string FullName => Namespace + "." + Name;
+ public List Properties { get; } = new();
+ public Dictionary MappableProperties { get; set; } = new();
+
+ public GeneratedType(TypeDeclarationSyntax sourceContainer, string name, string namespaceName, List usings)
+ {
+ SourceContainer = sourceContainer;
+ Name = name;
+ Namespace = namespaceName;
+ Usings = usings;
+ }
+}
+
+internal record struct GeneratedProperty(
+ PropertyDeclarationSyntax Declaration,
+ ITypeSymbol Type,
+ bool CustomFrom,
+ IMethodSymbol? CustomFromMethod,
+ bool CustomTo,
+ IMethodSymbol? CustomToMethod);
+
+internal record struct PropertyCandidate(
+ ITypeSymbol Type,
+ IPropertySymbol? Symbol = null,
+ bool CustomFrom = false,
+ IMethodSymbol? CustomFromMethod = null,
+ bool CustomTo = false,
+ IMethodSymbol? CustomToMethod = null);
+
+internal sealed class MappableProperty
+{
+ public ITypeSymbol FromType { get; }
+ public ITypeSymbol ToType { get; }
+ public IPropertySymbol FromProperty { get; }
+ public bool SourceIsReadOnly => FromProperty.SetMethod == null;
+ public GeneratedType? NestedType { get; }
+ public bool CustomFrom { get; }
+ public IMethodSymbol? CustomFromMethod { get; }
+ public bool CustomTo { get; }
+ public IMethodSymbol? CustomToMethod { get; }
+
+ public MappableProperty(ITypeSymbol fromType, ITypeSymbol toType, IPropertySymbol fromProperty,
+ bool customFrom, IMethodSymbol? customFromMethod, bool customTo, IMethodSymbol? customToMethod,
+ GeneratedType? ourTargetType)
+ {
+ FromType = fromType;
+ ToType = toType;
+ FromProperty = fromProperty;
+ CustomFrom = customFrom;
+ CustomFromMethod = customFromMethod;
+ CustomTo = customTo;
+ CustomToMethod = customToMethod;
+ NestedType = ourTargetType;
+ }
+}
diff --git a/AutoMapProperty/MappingModelBuilder.cs b/AutoMapProperty/MappingModelBuilder.cs
new file mode 100644
index 0000000..211c671
--- /dev/null
+++ b/AutoMapProperty/MappingModelBuilder.cs
@@ -0,0 +1,123 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using static AutoMapProperty.TypeSymbolHelpers;
+
+namespace AutoMapProperty;
+
+internal static class MappingModelBuilder
+{
+ private static readonly DiagnosticDescriptor InvalidProperty = new(
+ "NEJ23", "Invalid mapped property", "Error processing {0}: {1}", "NEJ", DiagnosticSeverity.Error, true);
+ private static readonly DiagnosticDescriptor InvalidConverter = new(
+ "NEJ10", "Invalid mapping method", "Method {0} on {1} must exist and be static", "NEJ", DiagnosticSeverity.Error, true);
+
+ public static List Build(Compilation compilation, IEnumerable properties,
+ SourceProductionContext context)
+ {
+ var types = new Dictionary();
+ foreach (var property in properties.Distinct())
+ {
+ context.CancellationToken.ThrowIfCancellationRequested();
+ if (property.Parent is not TypeDeclarationSyntax container ||
+ (container is not ClassDeclarationSyntax && container is not InterfaceDeclarationSyntax))
+ continue;
+
+ var semanticModel = compilation.GetSemanticModel(property.SyntaxTree);
+ if (semanticModel.GetDeclaredSymbol(container) is not INamedTypeSymbol sourceType ||
+ semanticModel.GetDeclaredSymbol(property) is not IPropertySymbol sourceProperty)
+ continue;
+
+ foreach (var attribute in sourceProperty.GetAttributes().Where(x =>
+ x.AttributeClass?.ToDisplayString() == AutoMapPropertyGenerator.AttributeName))
+ {
+ if (attribute.ConstructorArguments.Length == 0 || attribute.ConstructorArguments[0].Value is not string suffix)
+ continue;
+
+ var name = sourceType.Name + suffix;
+ if (!types.TryGetValue(name, out var target))
+ {
+ var namespaceName = sourceType.ContainingNamespace.ToDisplayString();
+ var usings = property.SyntaxTree.GetCompilationUnitRoot().Usings.Select(x => x.ToString()).ToList();
+ usings.Add("using " + namespaceName + ";");
+ target = new GeneratedType(container, name, namespaceName, usings);
+ types.Add(name, target);
+ }
+
+ try
+ {
+ var (declaration, type) = PropertyDeclarationBuilder.Build(compilation, property, sourceProperty, attribute);
+ var customTo = GetConverter(2);
+ var customFrom = GetConverter(3);
+ target.Properties.Add(new GeneratedProperty(declaration, type,
+ customFrom.Enabled, customFrom.Method, customTo.Enabled, customTo.Method));
+ }
+ catch (ArgumentException exception)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(InvalidProperty, property.GetLocation(), sourceProperty.Name, exception.Message));
+ }
+
+ (bool Enabled, IMethodSymbol? Method) GetConverter(int index)
+ {
+ if (attribute.ConstructorArguments.Length <= index || attribute.ConstructorArguments[index].Value is not string methodName)
+ return (false, null);
+ var method = sourceType.GetMembers(methodName).OfType().FirstOrDefault();
+ if (method == null || !method.IsStatic)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(InvalidConverter, property.GetLocation(), methodName, sourceType.Name));
+ return (true, null);
+ }
+ return (true, method);
+ }
+ }
+ }
+
+ foreach (var target in types.Values)
+ {
+ var source = (INamedTypeSymbol)compilation.GetSemanticModel(target.SourceContainer.SyntaxTree)
+ .GetDeclaredSymbol(target.SourceContainer)!;
+ var sourceProperties = GetProperties(source, types).GroupBy(x => x.Name).ToDictionary(x => x.Key, x => x.First().Info);
+ var existingTarget = compilation.GetTypeByMetadataName(target.FullName);
+ var targetProperties = existingTarget == null ? GetGeneratedProperties(target) : GetProperties(existingTarget, types);
+
+ foreach (var candidate in targetProperties.GroupBy(x => x.Name).Select(x => x.First()))
+ {
+ if (!sourceProperties.TryGetValue(candidate.Name, out var from) || from.Symbol == null)
+ continue;
+
+ var to = candidate.Info;
+ var concrete = GetConcreteType(to.Type);
+ var nested = types.Values.FirstOrDefault(x => x.FullName == GetFullString(concrete.WithNullableAnnotation(NullableAnnotation.NotAnnotated)));
+ if (nested == null && string.IsNullOrEmpty(concrete.ContainingNamespace?.Name))
+ nested = types.Values.FirstOrDefault(x => x.Name == concrete.Name);
+
+ target.MappableProperties[candidate.Name] = new MappableProperty(from.Type, to.Type, from.Symbol,
+ to.CustomFrom, to.CustomFromMethod, to.CustomTo, to.CustomToMethod, nested);
+ }
+ }
+ return types.Values.ToList();
+ }
+
+ private static IEnumerable<(string Name, PropertyCandidate Info)> GetProperties(INamedTypeSymbol type,
+ Dictionary generated)
+ {
+ foreach (var property in type.GetMembers().OfType())
+ yield return (property.Name, new PropertyCandidate(property.Type, property));
+
+ if (generated.TryGetValue(type.Name, out var target))
+ {
+ foreach (var property in GetGeneratedProperties(target))
+ yield return property;
+ }
+
+ if (type.BaseType != null)
+ {
+ foreach (var property in GetProperties(type.BaseType, generated))
+ yield return property;
+ }
+ }
+
+ private static IEnumerable<(string Name, PropertyCandidate Info)> GetGeneratedProperties(GeneratedType target) =>
+ target.Properties.Select(x => (x.Declaration.Identifier.ValueText,
+ new PropertyCandidate(x.Type, null, x.CustomFrom, x.CustomFromMethod, x.CustomTo, x.CustomToMethod)));
+}
diff --git a/AutoMapProperty/PropertyDeclarationBuilder.cs b/AutoMapProperty/PropertyDeclarationBuilder.cs
new file mode 100644
index 0000000..0bb990f
--- /dev/null
+++ b/AutoMapProperty/PropertyDeclarationBuilder.cs
@@ -0,0 +1,110 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using static AutoMapProperty.TypeSymbolHelpers;
+
+namespace AutoMapProperty;
+
+internal static class PropertyDeclarationBuilder
+{
+ public static (PropertyDeclarationSyntax Declaration, ITypeSymbol Type) Build(
+ Compilation compilation, PropertyDeclarationSyntax source, IPropertySymbol symbol, AttributeData attribute)
+ {
+ var type = attribute.ConstructorArguments.Length >= 2 &&
+ attribute.ConstructorArguments[1].Value is ITypeSymbol replacement
+ ? replacement
+ : symbol.Type;
+
+ var nullability = attribute.NamedArguments.FirstOrDefault(x => x.Key == "Nullability").Value.Value;
+ type = ApplyNullability(compilation, type, nullability is int value ? value : 0);
+
+ var declaration = source.WithAttributeLists(FilterAttributes(compilation, source.AttributeLists));
+ if (!SymbolEqualityComparer.IncludeNullability.Equals(type, symbol.Type))
+ {
+ declaration = declaration.WithType(SyntaxFactory.ParseTypeName(GetFullString(type)).WithTriviaFrom(source.Type));
+ var oldType = GetConcreteType(UnwrapNullableValueType(symbol.Type));
+ var newType = GetConcreteType(UnwrapNullableValueType(type));
+ if (declaration.Initializer != null && !SymbolEqualityComparer.Default.Equals(oldType, newType))
+ {
+ declaration = declaration.WithInitializer((EqualsValueClauseSyntax?)
+ new TypeReplacer(oldType.Name, newType.Name).Visit(declaration.Initializer));
+ }
+ }
+
+ if (declaration.Initializer != null && type.IsValueType &&
+ !SymbolEqualityComparer.Default.Equals(symbol.Type, UnwrapNullableValueType(symbol.Type)) &&
+ SymbolEqualityComparer.Default.Equals(type, UnwrapNullableValueType(type)))
+ {
+ var nullableType = SyntaxFactory.NullableType(SyntaxFactory.ParseTypeName(GetFullString(type)));
+ var initializer = SyntaxFactory.ParenthesizedExpression(SyntaxFactory.CastExpression(nullableType,
+ SyntaxFactory.ParenthesizedExpression(declaration.Initializer.Value)));
+ declaration = declaration.WithInitializer(declaration.Initializer.WithValue(SyntaxFactory.InvocationExpression(
+ SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, initializer,
+ SyntaxFactory.IdentifierName("GetValueOrDefault")))));
+ }
+
+ declaration = declaration
+ .WithModifiers(SyntaxFactory.TokenList(source.Modifiers.Where(x =>
+ !x.IsKind(SyntaxKind.ReadOnlyKeyword) && !x.IsKind(SyntaxKind.AbstractKeyword))))
+ .WithAccessorList(SyntaxFactory.AccessorList(SyntaxFactory.List(new[]
+ {
+ SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration)
+ .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)),
+ SyntaxFactory.AccessorDeclaration(SyntaxKind.SetAccessorDeclaration)
+ .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken))
+ })))
+ .WithExpressionBody(null)
+ .WithSemicolonToken(SyntaxFactory.Token(declaration.Initializer == null ? SyntaxKind.None : SyntaxKind.SemicolonToken));
+
+ return (declaration.NormalizeWhitespace(), type);
+ }
+
+ private static ITypeSymbol ApplyNullability(Compilation compilation, ITypeSymbol type, int nullability)
+ {
+ // Attribute enums are read by value so the analyzer does not depend on the runtime helper assembly.
+ if (nullability == 0)
+ return type;
+ if (nullability != 1 && nullability != 2)
+ throw new ArgumentException("Nullability must be Preserve, Nullable or NonNullable.");
+
+ if (type is INamedTypeSymbol named && named.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T)
+ return nullability == 1 ? type : named.TypeArguments[0];
+
+ if (type.IsValueType && nullability == 1)
+ return compilation.GetSpecialType(SpecialType.System_Nullable_T).Construct(type);
+
+ return type.WithNullableAnnotation(nullability == 1 ? NullableAnnotation.Annotated : NullableAnnotation.NotAnnotated);
+ }
+
+ private static SyntaxList FilterAttributes(Compilation compilation, SyntaxList lists)
+ {
+ var result = new List();
+ foreach (var list in lists)
+ {
+ var attributes = list.Attributes.Where(attribute =>
+ compilation.GetSemanticModel(attribute.SyntaxTree).GetSymbolInfo(attribute).Symbol is not IMethodSymbol method ||
+ method.ContainingType.ToDisplayString() != AutoMapPropertyGenerator.AttributeName);
+ var filtered = SyntaxFactory.SeparatedList(attributes);
+ if (filtered.Count > 0)
+ result.Add(list.WithAttributes(filtered));
+ }
+ return SyntaxFactory.List(result);
+ }
+
+ private sealed class TypeReplacer : CSharpSyntaxRewriter
+ {
+ private readonly string oldName;
+ private readonly string newName;
+
+ public TypeReplacer(string oldName, string newName)
+ {
+ this.oldName = oldName;
+ this.newName = newName;
+ }
+
+ public override SyntaxNode? VisitIdentifierName(IdentifierNameSyntax node) =>
+ node.Identifier.ValueText == oldName
+ ? SyntaxFactory.IdentifierName(newName).WithTriviaFrom(node)
+ : base.VisitIdentifierName(node);
+ }
+}
diff --git a/AutoMapProperty/TypeSymbolHelpers.cs b/AutoMapProperty/TypeSymbolHelpers.cs
new file mode 100644
index 0000000..1ac63bd
--- /dev/null
+++ b/AutoMapProperty/TypeSymbolHelpers.cs
@@ -0,0 +1,30 @@
+using Microsoft.CodeAnalysis;
+
+namespace AutoMapProperty;
+
+internal static class TypeSymbolHelpers
+{
+ private static readonly SymbolDisplayFormat DisplayFormat = new(
+ typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
+ genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters,
+ miscellaneousOptions: SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier);
+
+ public static string GetFullString(ITypeSymbol symbol) => symbol.ToDisplayString(DisplayFormat);
+
+ public static ITypeSymbol UnwrapNullableValueType(ITypeSymbol symbol) =>
+ symbol is INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T } nullable
+ ? nullable.TypeArguments[0]
+ : symbol;
+
+ public static ITypeSymbol GetConcreteType(ITypeSymbol symbol)
+ {
+ if (symbol is IArrayTypeSymbol array)
+ return GetConcreteType(array.ElementType);
+
+ if (symbol is INamedTypeSymbol named && named.IsGenericType &&
+ (named.Name == "ICollection" || named.Name == "IEnumerable"))
+ return GetConcreteType(named.TypeArguments[0]);
+
+ return symbol;
+ }
+}
diff --git a/AutoMapPropertyHelper/AutoMapNullability.cs b/AutoMapPropertyHelper/AutoMapNullability.cs
new file mode 100644
index 0000000..af14851
--- /dev/null
+++ b/AutoMapPropertyHelper/AutoMapNullability.cs
@@ -0,0 +1,12 @@
+namespace AutoMapPropertyHelper
+{
+ public enum AutoMapNullability
+ {
+ /// Keep the source type, or the explicitly supplied replacement type, unchanged.
+ Preserve,
+ /// Add a nullable annotation or wrap a value type in Nullable<T>.
+ Nullable,
+ /// Remove a nullable annotation or unwrap Nullable<T>.
+ NonNullable
+ }
+}
diff --git a/AutoMapPropertyHelper/AutoMapPropertyAttribute.cs b/AutoMapPropertyHelper/AutoMapPropertyAttribute.cs
index 47e85e9..97274ee 100644
--- a/AutoMapPropertyHelper/AutoMapPropertyAttribute.cs
+++ b/AutoMapPropertyHelper/AutoMapPropertyAttribute.cs
@@ -5,6 +5,12 @@ namespace AutoMapPropertyHelper
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
public class AutoMapPropertyAttribute : System.Attribute
{
+ ///
+ /// Overrides the top-level nullability of the generated property after selecting its type.
+ /// Collection element and generic argument nullability are unchanged.
+ ///
+ public AutoMapNullability Nullability { get; set; } = AutoMapNullability.Preserve;
+
public AutoMapPropertyAttribute(string name)
{
}