Split AMP into multiple files

This commit is contained in:
honzapatCZ 2026-10-25 23:48:22 +01:00
parent 8a49d88153
commit c2fe430801
15 changed files with 1609 additions and 1117 deletions

263
AutoMapProperty.Tests/.gitignore vendored Normal file
View File

@ -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

View File

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
<ProjectReference Include="..\AutoMapProperty\AutoMapProperty.csproj" />
<ProjectReference Include="..\AutoMapPropertyHelper\AutoMapPropertyHelper.csproj" />
</ItemGroup>
</Project>

View File

@ -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<T>(IServiceProvider providers) where T : class => Activator.CreateInstance<T>();
public static T Create<T, TParent>(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<IPropertySymbol>(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);
}
}

View File

@ -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<Child>();
}
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());
}
}

View File

@ -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<string?>", "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<INamedTypeSymbol>(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);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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));
}
});
}
}

View File

@ -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<GeneratedType> 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<Func<").Append(classToGenerate.SourceContainer.Identifier).Append(", ").Append(classToGenerate.Name).Append(@">> 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<Func<").Append(classToGenerate.Name).Append(@", bool>> 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<Func<").Append(classToGenerate.SourceContainer.Identifier).Append(@", bool>> 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();
}
}

View File

@ -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<GeneratedType> otherClasses, string Key, ITypeSymbol FromType, ITypeSymbol ToType, bool allowProjectors, List<GeneratedType>? 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<GeneratedType> otherClasses, string Key, MappableProperty Value, bool allowProjectors, List<GeneratedType>? 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<GeneratedType>) GenerateFromExpression(List<GeneratedType> otherClasses, GeneratedType classToGenerate, SyntaxToken sourceName)
{
var projectors = new List<GeneratedType>();
var exprSb = new StringBuilder();
exprSb.Append(@"System.Linq.Expressions.Expression<Func<").Append(sourceName).Append(@", ").Append(classToGenerate.Name).Append(@">> 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<GeneratedType> 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<GeneratedType> 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();
}
}

View File

@ -0,0 +1,66 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace AutoMapProperty;
internal sealed class GeneratedType
{
public TypeDeclarationSyntax SourceContainer { get; }
public List<string> Usings { get; }
public string Namespace { get; }
public string Name { get; }
public string FullName => Namespace + "." + Name;
public List<GeneratedProperty> Properties { get; } = new();
public Dictionary<string, MappableProperty> MappableProperties { get; set; } = new();
public GeneratedType(TypeDeclarationSyntax sourceContainer, string name, string namespaceName, List<string> 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;
}
}

View File

@ -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<GeneratedType> Build(Compilation compilation, IEnumerable<PropertyDeclarationSyntax> properties,
SourceProductionContext context)
{
var types = new Dictionary<string, GeneratedType>();
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<IMethodSymbol>().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<string, GeneratedType> generated)
{
foreach (var property in type.GetMembers().OfType<IPropertySymbol>())
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)));
}

View File

@ -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<AttributeListSyntax> FilterAttributes(Compilation compilation, SyntaxList<AttributeListSyntax> lists)
{
var result = new List<AttributeListSyntax>();
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);
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,12 @@
namespace AutoMapPropertyHelper
{
public enum AutoMapNullability
{
/// <summary>Keep the source type, or the explicitly supplied replacement type, unchanged.</summary>
Preserve,
/// <summary>Add a nullable annotation or wrap a value type in Nullable&lt;T&gt;.</summary>
Nullable,
/// <summary>Remove a nullable annotation or unwrap Nullable&lt;T&gt;.</summary>
NonNullable
}
}

View File

@ -5,6 +5,12 @@ namespace AutoMapPropertyHelper
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)] [AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
public class AutoMapPropertyAttribute : System.Attribute public class AutoMapPropertyAttribute : System.Attribute
{ {
/// <summary>
/// Overrides the top-level nullability of the generated property after selecting its type.
/// Collection element and generic argument nullability are unchanged.
/// </summary>
public AutoMapNullability Nullability { get; set; } = AutoMapNullability.Preserve;
public AutoMapPropertyAttribute(string name) public AutoMapPropertyAttribute(string name)
{ {
} }