Add nullable reference types support.
This commit is contained in:
parent
cfb0debf82
commit
f7a755332e
|
@ -1,20 +1,11 @@
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using MapTo.Extensions;
|
using MapTo.Extensions;
|
||||||
using Microsoft.CodeAnalysis;
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp;
|
||||||
|
|
||||||
namespace MapTo
|
namespace MapTo
|
||||||
{
|
{
|
||||||
internal record SourceGenerationOptions(
|
internal record SourceCode(string Text, string HintName);
|
||||||
AccessModifier ConstructorAccessModifier,
|
|
||||||
AccessModifier GeneratedMethodsAccessModifier,
|
|
||||||
bool GenerateXmlDocument)
|
|
||||||
{
|
|
||||||
internal static SourceGenerationOptions From(GeneratorExecutionContext context) => new(
|
|
||||||
context.GetBuildGlobalOption<AccessModifier>(nameof(ConstructorAccessModifier)),
|
|
||||||
context.GetBuildGlobalOption<AccessModifier>(nameof(GeneratedMethodsAccessModifier)),
|
|
||||||
context.GetBuildGlobalOption(nameof(GenerateXmlDocument), true)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal record MappedProperty(string Name, string? TypeConverter, ImmutableArray<string> TypeConverterParameters);
|
internal record MappedProperty(string Name, string? TypeConverter, ImmutableArray<string> TypeConverterParameters);
|
||||||
|
|
||||||
|
@ -28,4 +19,25 @@ namespace MapTo
|
||||||
string SourceClassFullName,
|
string SourceClassFullName,
|
||||||
ImmutableArray<MappedProperty> MappedProperties
|
ImmutableArray<MappedProperty> MappedProperties
|
||||||
);
|
);
|
||||||
|
|
||||||
|
internal record SourceGenerationOptions(
|
||||||
|
AccessModifier ConstructorAccessModifier,
|
||||||
|
AccessModifier GeneratedMethodsAccessModifier,
|
||||||
|
bool GenerateXmlDocument,
|
||||||
|
bool SupportNullableReferenceTypes)
|
||||||
|
{
|
||||||
|
internal static SourceGenerationOptions From(GeneratorExecutionContext context)
|
||||||
|
{
|
||||||
|
var compilationOptions = (context.Compilation as CSharpCompilation)?.Options;
|
||||||
|
|
||||||
|
return new(
|
||||||
|
context.GetBuildGlobalOption<AccessModifier>(nameof(ConstructorAccessModifier)),
|
||||||
|
context.GetBuildGlobalOption<AccessModifier>(nameof(GeneratedMethodsAccessModifier)),
|
||||||
|
context.GetBuildGlobalOption(nameof(GenerateXmlDocument), true),
|
||||||
|
compilationOptions is not null && (compilationOptions.NullableContextOptions == NullableContextOptions.Warnings || compilationOptions.NullableContextOptions == NullableContextOptions.Enable)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string NullableReferenceSyntax => SupportNullableReferenceTypes ? "?" : string.Empty;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -14,6 +14,7 @@ namespace MapTo.Sources
|
||||||
{
|
{
|
||||||
using var builder = new SourceBuilder()
|
using var builder = new SourceBuilder()
|
||||||
.WriteLine(GeneratedFilesHeader)
|
.WriteLine(GeneratedFilesHeader)
|
||||||
|
.WriteNullableContextOptionIf(options.SupportNullableReferenceTypes)
|
||||||
.WriteLine()
|
.WriteLine()
|
||||||
.WriteLine($"namespace {RootNamespace}")
|
.WriteLine($"namespace {RootNamespace}")
|
||||||
.WriteOpeningBracket();
|
.WriteOpeningBracket();
|
||||||
|
@ -44,7 +45,7 @@ namespace MapTo.Sources
|
||||||
}
|
}
|
||||||
|
|
||||||
builder
|
builder
|
||||||
.WriteLine("TDestination Convert(TSource source, object[] converterParameters);")
|
.WriteLine($"TDestination Convert(TSource source, object[]{options.NullableReferenceSyntax} converterParameters);")
|
||||||
.WriteClosingBracket()
|
.WriteClosingBracket()
|
||||||
.WriteClosingBracket();
|
.WriteClosingBracket();
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,8 @@ namespace MapTo.Sources
|
||||||
{
|
{
|
||||||
using var builder = new SourceBuilder()
|
using var builder = new SourceBuilder()
|
||||||
.WriteLine(GeneratedFilesHeader)
|
.WriteLine(GeneratedFilesHeader)
|
||||||
|
.WriteNullableContextOptionIf(model.Options.SupportNullableReferenceTypes)
|
||||||
|
.WriteLine()
|
||||||
.WriteUsings(model)
|
.WriteUsings(model)
|
||||||
.WriteLine()
|
.WriteLine()
|
||||||
|
|
||||||
|
@ -90,7 +92,7 @@ namespace MapTo.Sources
|
||||||
|
|
||||||
return builder
|
return builder
|
||||||
.GenerateConvertorMethodsXmlDocs(model, sourceClassParameterName)
|
.GenerateConvertorMethodsXmlDocs(model, sourceClassParameterName)
|
||||||
.WriteLine($"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static {model.ClassName} From({model.SourceClassFullName} {sourceClassParameterName})")
|
.WriteLine($"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static {model.ClassName} From({model.SourceClassFullName}{model.Options.NullableReferenceSyntax} {sourceClassParameterName})")
|
||||||
.WriteOpeningBracket()
|
.WriteOpeningBracket()
|
||||||
.WriteLine($"return {sourceClassParameterName} == null ? null : new {model.ClassName}({sourceClassParameterName});")
|
.WriteLine($"return {sourceClassParameterName} == null ? null : new {model.ClassName}({sourceClassParameterName});")
|
||||||
.WriteClosingBracket();
|
.WriteClosingBracket();
|
||||||
|
@ -127,7 +129,7 @@ namespace MapTo.Sources
|
||||||
|
|
||||||
return builder
|
return builder
|
||||||
.GenerateConvertorMethodsXmlDocs(model, sourceClassParameterName)
|
.GenerateConvertorMethodsXmlDocs(model, sourceClassParameterName)
|
||||||
.WriteLine($"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static {model.ClassName} To{model.ClassName}(this {model.SourceClassFullName} {sourceClassParameterName})")
|
.WriteLine($"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static {model.ClassName} To{model.ClassName}(this {model.SourceClassFullName}{model.Options.NullableReferenceSyntax} {sourceClassParameterName})")
|
||||||
.WriteOpeningBracket()
|
.WriteOpeningBracket()
|
||||||
.WriteLine($"return {sourceClassParameterName} == null ? null : new {model.ClassName}({sourceClassParameterName});")
|
.WriteLine($"return {sourceClassParameterName} == null ? null : new {model.ClassName}({sourceClassParameterName});")
|
||||||
.WriteClosingBracket();
|
.WriteClosingBracket();
|
||||||
|
|
|
@ -12,9 +12,11 @@ namespace MapTo.Sources
|
||||||
internal const string ConverterParametersPropertyName = "ConverterParameters";
|
internal const string ConverterParametersPropertyName = "ConverterParameters";
|
||||||
|
|
||||||
internal static SourceCode Generate(SourceGenerationOptions options)
|
internal static SourceCode Generate(SourceGenerationOptions options)
|
||||||
{
|
{
|
||||||
using var builder = new SourceBuilder()
|
using var builder = new SourceBuilder()
|
||||||
.WriteLine(GeneratedFilesHeader)
|
.WriteLine(GeneratedFilesHeader)
|
||||||
|
.WriteNullableContextOptionIf(options.SupportNullableReferenceTypes)
|
||||||
|
.WriteLine()
|
||||||
.WriteLine("using System;")
|
.WriteLine("using System;")
|
||||||
.WriteLine()
|
.WriteLine()
|
||||||
.WriteLine($"namespace {RootNamespace}")
|
.WriteLine($"namespace {RootNamespace}")
|
||||||
|
@ -44,7 +46,7 @@ namespace MapTo.Sources
|
||||||
}
|
}
|
||||||
|
|
||||||
builder
|
builder
|
||||||
.WriteLine($"public {AttributeClassName}(Type converter, object[] converterParameters = null)")
|
.WriteLine($"public {AttributeClassName}(Type converter, object[]{options.NullableReferenceSyntax} converterParameters = null)")
|
||||||
.WriteOpeningBracket()
|
.WriteOpeningBracket()
|
||||||
.WriteLine($"{ConverterPropertyName} = converter;")
|
.WriteLine($"{ConverterPropertyName} = converter;")
|
||||||
.WriteLine($"{ConverterParametersPropertyName} = converterParameters;")
|
.WriteLine($"{ConverterParametersPropertyName} = converterParameters;")
|
||||||
|
@ -72,7 +74,7 @@ namespace MapTo.Sources
|
||||||
}
|
}
|
||||||
|
|
||||||
builder
|
builder
|
||||||
.WriteLine($"public object[] {ConverterParametersPropertyName} {{ get; }}")
|
.WriteLine($"public object[]{options.NullableReferenceSyntax} {ConverterParametersPropertyName} {{ get; }}")
|
||||||
.WriteClosingBracket()
|
.WriteClosingBracket()
|
||||||
.WriteClosingBracket();
|
.WriteClosingBracket();
|
||||||
|
|
||||||
|
|
|
@ -22,18 +22,32 @@ namespace MapTo.Sources
|
||||||
_indentedWriter.Dispose();
|
_indentedWriter.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
public SourceBuilder WriteLine(string value)
|
public SourceBuilder WriteLine(string? value = null)
|
||||||
{
|
{
|
||||||
_indentedWriter.WriteLine(value);
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
_indentedWriter.WriteLineNoTabs(string.Empty);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_indentedWriter.WriteLine(value);
|
||||||
|
}
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SourceBuilder WriteLine()
|
public SourceBuilder WriteLineIf(bool condition, string? value)
|
||||||
{
|
{
|
||||||
_indentedWriter.WriteLineNoTabs(string.Empty);
|
if (condition)
|
||||||
|
{
|
||||||
|
WriteLine(value);
|
||||||
|
}
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public SourceBuilder WriteNullableContextOptionIf(bool enabled) => WriteLineIf(enabled, "#nullable enable");
|
||||||
|
|
||||||
public SourceBuilder WriteOpeningBracket()
|
public SourceBuilder WriteOpeningBracket()
|
||||||
{
|
{
|
||||||
_indentedWriter.WriteLine("{");
|
_indentedWriter.WriteLine("{");
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
namespace MapTo.Sources
|
|
||||||
{
|
|
||||||
internal record SourceCode(string Text, string HintName);
|
|
||||||
}
|
|
|
@ -11,7 +11,7 @@ namespace MapTo.Tests.Infrastructure
|
||||||
{
|
{
|
||||||
internal static class CSharpGenerator
|
internal static class CSharpGenerator
|
||||||
{
|
{
|
||||||
internal static (Compilation compilation, ImmutableArray<Diagnostic> diagnostics) GetOutputCompilation(string source, bool assertCompilation = false, IDictionary<string, string> analyzerConfigOptions = null)
|
internal static (Compilation compilation, ImmutableArray<Diagnostic> diagnostics) GetOutputCompilation(string source, bool assertCompilation = false, IDictionary<string, string> analyzerConfigOptions = null, NullableContextOptions nullableContextOptions = NullableContextOptions.Disable)
|
||||||
{
|
{
|
||||||
var syntaxTree = CSharpSyntaxTree.ParseText(source);
|
var syntaxTree = CSharpSyntaxTree.ParseText(source);
|
||||||
var references = AppDomain.CurrentDomain.GetAssemblies()
|
var references = AppDomain.CurrentDomain.GetAssemblies()
|
||||||
|
@ -19,25 +19,29 @@ namespace MapTo.Tests.Infrastructure
|
||||||
.Select(a => MetadataReference.CreateFromFile(a.Location))
|
.Select(a => MetadataReference.CreateFromFile(a.Location))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var compilation = CSharpCompilation.Create($"{typeof(CSharpGenerator).Assembly.GetName().Name}.Dynamic", new[] { syntaxTree }, references, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
|
var compilation = CSharpCompilation.Create(
|
||||||
|
$"{typeof(CSharpGenerator).Assembly.GetName().Name}.Dynamic",
|
||||||
|
new[] { syntaxTree },
|
||||||
|
references,
|
||||||
|
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: nullableContextOptions));
|
||||||
|
|
||||||
if (assertCompilation)
|
if (assertCompilation)
|
||||||
{
|
{
|
||||||
// NB: fail tests when the injected program isn't valid _before_ running generators
|
// NB: fail tests when the injected program isn't valid _before_ running generators
|
||||||
compilation.GetDiagnostics().ShouldBeSuccessful();
|
compilation.GetDiagnostics().ShouldBeSuccessful();
|
||||||
}
|
}
|
||||||
|
|
||||||
var driver = CSharpGeneratorDriver.Create(
|
var driver = CSharpGeneratorDriver.Create(
|
||||||
new[] { new MapToGenerator() },
|
new[] { new MapToGenerator() },
|
||||||
optionsProvider: new TestAnalyzerConfigOptionsProvider(analyzerConfigOptions)
|
optionsProvider: new TestAnalyzerConfigOptionsProvider(analyzerConfigOptions)
|
||||||
);
|
);
|
||||||
|
|
||||||
driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var generateDiagnostics);
|
driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var generateDiagnostics);
|
||||||
|
|
||||||
var diagnostics = outputCompilation.GetDiagnostics()
|
var diagnostics = outputCompilation.GetDiagnostics()
|
||||||
.Where(d => d.Severity >= DiagnosticSeverity.Warning)
|
.Where(d => d.Severity >= DiagnosticSeverity.Warning)
|
||||||
.Select(c => $"{c.Severity}: {c.Location.GetLineSpan().StartLinePosition} - {c.GetMessage()} [in \"{c.Location.SourceTree?.FilePath}\"]").ToArray();
|
.Select(c => $"{c.Severity}: {c.Location.GetLineSpan().StartLinePosition} - {c.GetMessage()} [in \"{c.Location.SourceTree?.FilePath}\"]").ToArray();
|
||||||
|
|
||||||
if (diagnostics.Any())
|
if (diagnostics.Any())
|
||||||
{
|
{
|
||||||
Assert.False(diagnostics.Any(), $@"Failed:
|
Assert.False(diagnostics.Any(), $@"Failed:
|
||||||
|
|
|
@ -286,6 +286,7 @@ namespace Test
|
||||||
|
|
||||||
const string expectedResult = @"
|
const string expectedResult = @"
|
||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Test
|
namespace Test
|
||||||
|
@ -355,6 +356,7 @@ namespace Test
|
||||||
|
|
||||||
const string expectedResult = @"
|
const string expectedResult = @"
|
||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Test
|
namespace Test
|
||||||
|
@ -404,6 +406,7 @@ namespace Test
|
||||||
|
|
||||||
const string expectedResult = @"
|
const string expectedResult = @"
|
||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Test
|
namespace Test
|
||||||
|
@ -514,6 +517,32 @@ namespace MapTo
|
||||||
compilation.SyntaxTrees.ShouldContainSource(ITypeConverterSource.InterfaceName, expectedInterface);
|
compilation.SyntaxTrees.ShouldContainSource(ITypeConverterSource.InterfaceName, expectedInterface);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void VerifyTypeConverterInterfaceWithNullableOptionOn()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
const string source = "";
|
||||||
|
var expectedInterface = $@"
|
||||||
|
{Constants.GeneratedFilesHeader}
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace MapTo
|
||||||
|
{{
|
||||||
|
public interface ITypeConverter<in TSource, out TDestination>
|
||||||
|
{{
|
||||||
|
TDestination Convert(TSource source, object[]? converterParameters);
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
".Trim();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, nullableContextOptions: NullableContextOptions.Enable);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
diagnostics.ShouldBeSuccessful();
|
||||||
|
compilation.SyntaxTrees.ShouldContainSource(ITypeConverterSource.InterfaceName, expectedInterface);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void VerifyMapTypeConverterAttribute()
|
public void VerifyMapTypeConverterAttribute()
|
||||||
{
|
{
|
||||||
|
@ -521,6 +550,7 @@ namespace MapTo
|
||||||
const string source = "";
|
const string source = "";
|
||||||
var expectedInterface = $@"
|
var expectedInterface = $@"
|
||||||
{Constants.GeneratedFilesHeader}
|
{Constants.GeneratedFilesHeader}
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace MapTo
|
namespace MapTo
|
||||||
|
@ -548,6 +578,43 @@ namespace MapTo
|
||||||
diagnostics.ShouldBeSuccessful();
|
diagnostics.ShouldBeSuccessful();
|
||||||
compilation.SyntaxTrees.ShouldContainSource(MapTypeConverterAttributeSource.AttributeName, expectedInterface);
|
compilation.SyntaxTrees.ShouldContainSource(MapTypeConverterAttributeSource.AttributeName, expectedInterface);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void VerifyMapTypeConverterAttributeWithNullableOptionOn()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
const string source = "";
|
||||||
|
var expectedInterface = $@"
|
||||||
|
{Constants.GeneratedFilesHeader}
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace MapTo
|
||||||
|
{{
|
||||||
|
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
|
||||||
|
public sealed class MapTypeConverterAttribute : Attribute
|
||||||
|
{{
|
||||||
|
public MapTypeConverterAttribute(Type converter, object[]? converterParameters = null)
|
||||||
|
{{
|
||||||
|
Converter = converter;
|
||||||
|
ConverterParameters = converterParameters;
|
||||||
|
}}
|
||||||
|
|
||||||
|
public Type Converter {{ get; }}
|
||||||
|
|
||||||
|
public object[]? ConverterParameters {{ get; }}
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
".Trim();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, nullableContextOptions: NullableContextOptions.Enable);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
diagnostics.ShouldBeSuccessful();
|
||||||
|
compilation.SyntaxTrees.ShouldContainSource(MapTypeConverterAttributeSource.AttributeName, expectedInterface);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void When_FoundMatchingPropertyNameWithDifferentImplicitlyConvertibleType_Should_GenerateTheProperty()
|
public void When_FoundMatchingPropertyNameWithDifferentImplicitlyConvertibleType_Should_GenerateTheProperty()
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
using TestConsoleApp.ViewModels;
|
using TestConsoleApp.ViewModels;
|
||||||
using VM = TestConsoleApp.ViewModels;
|
|
||||||
using Data = TestConsoleApp.Data.Models;
|
|
||||||
|
|
||||||
var userViewModel = VM.User.From(new Data.User());
|
namespace TestConsoleApp
|
||||||
var userViewModel2 = new Data.User().ToUserViewModel();
|
{
|
||||||
|
internal class Program
|
||||||
|
{
|
||||||
|
private static void Main(string[] args)
|
||||||
|
{
|
||||||
|
var userViewModel = User.From(new Data.Models.User());
|
||||||
|
var userViewModel2 = new Data.Models.User().ToUserViewModel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,14 +2,16 @@
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net5.0</TargetFramework>
|
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||||
|
<LangVersion>8</LangVersion>
|
||||||
|
<Nullable>annotations</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\src\MapTo\MapTo.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
|
<ProjectReference Include="..\..\src\MapTo\MapTo.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Import Project="..\..\src\MapTo\MapTo.props"/>
|
<Import Project="..\..\src\MapTo\MapTo.props" />
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<MapTo_ConstructorAccessModifier>Internal</MapTo_ConstructorAccessModifier>
|
<MapTo_ConstructorAccessModifier>Internal</MapTo_ConstructorAccessModifier>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
|
@ -16,7 +16,7 @@ namespace TestConsoleApp.ViewModels
|
||||||
private class LastNameConverter : ITypeConverter<long, string>
|
private class LastNameConverter : ITypeConverter<long, string>
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string Convert(long source, object[] converterParameters) => $"{source} :: With Type Converter";
|
public string Convert(long source, object[]? converterParameters) => $"{source} :: With Type Converter";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue