Split AMP into multiple files
This commit is contained in:
parent
8a49d88153
commit
c2fe430801
263
AutoMapProperty.Tests/.gitignore
vendored
Normal file
263
AutoMapProperty.Tests/.gitignore
vendored
Normal 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
|
||||
16
AutoMapProperty.Tests/AutoMapProperty.Tests.csproj
Normal file
16
AutoMapProperty.Tests/AutoMapProperty.Tests.csproj
Normal 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>
|
||||
70
AutoMapProperty.Tests/GeneratorTestCompilation.cs
Normal file
70
AutoMapProperty.Tests/GeneratorTestCompilation.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
198
AutoMapProperty.Tests/MappingRegressionTests.cs
Normal file
198
AutoMapProperty.Tests/MappingRegressionTests.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
142
AutoMapProperty.Tests/NullabilityTests.cs
Normal file
142
AutoMapProperty.Tests/NullabilityTests.cs
Normal 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
32
AutoMapProperty/AutoMapPropertyGenerator.cs
Normal file
32
AutoMapProperty/AutoMapPropertyGenerator.cs
Normal 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));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
264
AutoMapProperty/MappingEmitter.cs
Normal file
264
AutoMapProperty/MappingEmitter.cs
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
277
AutoMapProperty/MappingExpressionEmitter.cs
Normal file
277
AutoMapProperty/MappingExpressionEmitter.cs
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
66
AutoMapProperty/MappingModel.cs
Normal file
66
AutoMapProperty/MappingModel.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
123
AutoMapProperty/MappingModelBuilder.cs
Normal file
123
AutoMapProperty/MappingModelBuilder.cs
Normal 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)));
|
||||
}
|
||||
110
AutoMapProperty/PropertyDeclarationBuilder.cs
Normal file
110
AutoMapProperty/PropertyDeclarationBuilder.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
30
AutoMapProperty/TypeSymbolHelpers.cs
Normal file
30
AutoMapProperty/TypeSymbolHelpers.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
12
AutoMapPropertyHelper/AutoMapNullability.cs
Normal file
12
AutoMapPropertyHelper/AutoMapNullability.cs
Normal 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<T>.</summary>
|
||||
Nullable,
|
||||
/// <summary>Remove a nullable annotation or unwrap Nullable<T>.</summary>
|
||||
NonNullable
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,12 @@ namespace AutoMapPropertyHelper
|
|||
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
|
||||
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)
|
||||
{
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user