Skip to content

Commit

Permalink
feat: queryable support for user implemented mappings (#348)
Browse files Browse the repository at this point in the history
  • Loading branch information
latonz authored Apr 17, 2023
1 parent 74d0b52 commit a58f56f
Show file tree
Hide file tree
Showing 15 changed files with 265 additions and 80 deletions.
2 changes: 1 addition & 1 deletion docs/docs/02-configuration/11-queryable-projections.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ such mappings have several limitations:
- Object factories are not applied
- Constructors with unmatched optional parameters are ignored
- `ThrowOnPropertyMappingNullMismatch` is ignored
- User implemented mappings are not supported
- Enum mappings do not support the `ByName` strategy
- Reference handling is not supported
- Nullable reference types are disabled
- User implemented mapping methods need to follow expression tree [limitations](https://learn.microsoft.com/en-us/dotnet/csharp/advanced-topics/expression-trees/#limitations).

:::

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ namespace Riok.Mapperly.Descriptors;
public class InlineExpressionMappingBuilderContext : MappingBuilderContext
{
private readonly MappingCollection _inlineExpressionMappings;
private readonly MappingBuilderContext _parentContext;

public InlineExpressionMappingBuilderContext(MappingBuilderContext ctx, ITypeSymbol sourceType, ITypeSymbol targetType)
: this(ctx, (ctx.FindMapping(sourceType, targetType) as IUserMapping)?.Method, sourceType, targetType)
Expand All @@ -26,6 +27,7 @@ private InlineExpressionMappingBuilderContext(
ITypeSymbol target)
: base(ctx, userSymbol, source, target)
{
_parentContext = ctx;
_inlineExpressionMappings = new MappingCollection();
}

Expand All @@ -36,6 +38,7 @@ private InlineExpressionMappingBuilderContext(
ITypeSymbol target)
: base(ctx, userSymbol, source, target)
{
_parentContext = ctx;
_inlineExpressionMappings = ctx._inlineExpressionMappings;
}

Expand All @@ -53,12 +56,28 @@ and not MappingConversionType.Dictionary
/// <summary>
/// Tries to find an existing mapping for the provided types.
/// The nullable annotation of reference types is ignored and always set to non-nullable.
/// Only inline expression mappings and user implemented mappings are considered.
/// </summary>
/// <param name="sourceType">The source type.</param>
/// <param name="targetType">The target type.</param>
/// <returns>The <see cref="ITypeMapping"/> if a mapping was found or <c>null</c> if none was found.</returns>
public override ITypeMapping? FindMapping(ITypeSymbol sourceType, ITypeSymbol targetType)
=> _inlineExpressionMappings.Find(sourceType, targetType);
{
if (_inlineExpressionMappings.Find(sourceType, targetType) is { } mapping)
return mapping;

// user implemented mappings are also taken into account
// this works as long as the user implemented methods
// follow the expression tree limitations:
// https://learn.microsoft.com/en-us/dotnet/csharp/advanced-topics/expression-trees/#limitations
if (_parentContext.FindMapping(sourceType, targetType) is UserImplementedMethodMapping userMapping)
{
_inlineExpressionMappings.Add(userMapping);
return userMapping;
}

return null;
}

/// <summary>
/// Existing target instance mappings are not supported.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace Riok.Mapperly.IntegrationTests.Dto
{
public class TestObjectDtoManuallyMappedProjection
{
public TestObjectDtoManuallyMappedProjection(int magicIntValue)
{
MagicIntValue = magicIntValue;
}

public int MagicIntValue { get; }

public string? StringValue { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public TestObjectDtoProjection(int ctorValue)

public string EnumStringValue { get; set; } = string.Empty;

public TestEnumDtoByValue EnumReverseStringValue { get; set; }
public TestEnumDtoByName EnumReverseStringValue { get; set; }

public InheritanceSubObjectDto? SubObject { get; set; }

Expand All @@ -61,5 +61,7 @@ public TestObjectDtoProjection(int ctorValue)
public DateOnly DateTimeValueTargetDateOnly { get; set; }

public TimeOnly DateTimeValueTargetTimeOnly { get; set; }

public TestObjectDtoManuallyMappedProjection? ManuallyMapped { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,13 @@ public static partial class ProjectionMapper
[MapperIgnoreSource(nameof(TestObjectProjection.IgnoredStringValue))]
[MapProperty(nameof(TestObjectProjection.RenamedStringValue), nameof(TestObjectDtoProjection.RenamedStringValue2))]
private static partial TestObjectDtoProjection ProjectToDto(this TestObjectProjection testObject);

private static TestObjectDtoManuallyMappedProjection? MapManual(string str)
{
return new TestObjectDtoManuallyMappedProjection(100)
{
StringValue = str,
};
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,7 @@ public class TestObjectProjection
public DateTime DateTimeValueTargetDateOnly { get; set; }

public DateTime DateTimeValueTargetTimeOnly { get; set; }

public String ManuallyMapped { get; set; } = "fooBar";
}
}
93 changes: 66 additions & 27 deletions test/Riok.Mapperly.IntegrationTests/ProjectionMapperTest.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
using System;
using System.Threading.Tasks;
using FluentAssertions;
using Riok.Mapperly.IntegrationTests.Mapper;
using Riok.Mapperly.IntegrationTests.Models;
using VerifyXunit;
using Xunit;
#if NET7_0_OR_GREATER
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
#endif

Expand All @@ -22,38 +23,76 @@ public Task SnapshotGeneratedSource()

#if NET7_0_OR_GREATER
[Fact]
public void ProjectionShouldTranslateToQuery()
public async Task ProjectionShouldTranslateToQuery()
{
using var ctx = new ProjectionDbContext();
var query = ctx.Objects.ProjectToDto().ToQueryString();
query.Should().Be(
"""
SELECT "o"."CtorValue", "o"."IntValue", "o"."IntInitOnlyValue", "o"."RequiredValue", "o"."StringValue", "o"."RenamedStringValue", "i"."IdValue", CASE
WHEN "i0"."IdValue" IS NOT NULL THEN "i0"."IdValue"
ELSE 0
END, CASE
WHEN "t"."IntValue" IS NOT NULL THEN "t"."IntValue"
ELSE 0
END, "t"."IntValue" IS NOT NULL, "t"."IntValue", "t0"."IntValue" IS NOT NULL, "t0"."IntValue", COALESCE("o"."StringNullableTargetNotNullable", ''), "o0"."IntValue", "o0"."CtorValue", "o0"."DateTimeValueTargetDateOnly", "o0"."DateTimeValueTargetTimeOnly", "o0"."EnumName", "o0"."EnumRawValue", "o0"."EnumReverseStringValue", "o0"."EnumStringValue", "o0"."EnumValue", "o0"."FlatteningIdValue", "o0"."IgnoredIntValue", "o0"."IgnoredStringValue", "o0"."IntInitOnlyValue", "o0"."NestedNullableIntValue", "o0"."NestedNullableTargetNotNullableIntValue", "o0"."NullableFlatteningIdValue", "o0"."NullableUnflatteningIdValue", "o0"."RecursiveObjectIntValue", "o0"."RenamedStringValue", "o0"."RequiredValue", "o0"."StringNullableTargetNotNullable", "o0"."StringValue", "o0"."SubObjectSubIntValue", "o0"."UnflatteningIdValue", "i0"."IdValue", "i1"."SubIntValue", "t1"."IntValue", "t1"."TestObjectProjectionIntValue", "t2"."IntValue", CAST("o"."EnumValue" AS INTEGER), CAST("o"."EnumName" AS INTEGER), CAST("o"."EnumRawValue" AS INTEGER), "o"."EnumStringValue", "o"."EnumReverseStringValue", "i1"."SubIntValue" IS NOT NULL, "i1"."BaseIntValue", "o"."DateTimeValueTargetDateOnly", "o"."DateTimeValueTargetTimeOnly"
FROM "Objects" AS "o"
INNER JOIN "IdObject" AS "i" ON "o"."FlatteningIdValue" = "i"."IdValue"
LEFT JOIN "IdObject" AS "i0" ON "o"."NullableFlatteningIdValue" = "i0"."IdValue"
LEFT JOIN "TestObjectNested" AS "t" ON "o"."NestedNullableIntValue" = "t"."IntValue"
LEFT JOIN "TestObjectNested" AS "t0" ON "o"."NestedNullableTargetNotNullableIntValue" = "t0"."IntValue"
LEFT JOIN "Objects" AS "o0" ON "o"."IntValue" = "o0"."RecursiveObjectIntValue"
LEFT JOIN "InheritanceSubObject" AS "i1" ON "o"."SubObjectSubIntValue" = "i1"."SubIntValue"
LEFT JOIN "TestObjectNested" AS "t1" ON "o"."IntValue" = "t1"."TestObjectProjectionIntValue"
LEFT JOIN "TestObjectNested" AS "t2" ON "o"."IntValue" = "t2"."TestObjectProjectionIntValue"
ORDER BY "o"."IntValue", "i"."IdValue", "i0"."IdValue", "t"."IntValue", "t0"."IntValue", "o0"."IntValue", "i1"."SubIntValue", "t1"."IntValue"
""");
await using var connection = new SqliteConnection("Data Source=:memory:");
await connection.OpenAsync();

var options = new DbContextOptionsBuilder()
.UseSqlite(connection)
.Options;

await using var ctx = new ProjectionDbContext(options);
await ctx.Database.EnsureCreatedAsync();
ctx.Objects.Add(CreateObject());
await ctx.SaveChangesAsync();

var query = ctx.Objects.ProjectToDto();
await Verifier
.Verify(query.ToQueryString(), "sql")
.UseTextForParameters("query");

var objects = await query.ToListAsync();
await Verifier
.Verify(objects)
.UseTextForParameters("result");
}

private TestObjectProjection CreateObject()
{
return new TestObjectProjection
{
RequiredValue = 10,
EnumName = TestEnum.Value10,
EnumReverseStringValue = nameof(TestEnum.Value10),
EnumValue = TestEnum.Value20,
IntValue = 100,
EnumRawValue = TestEnum.Value30,
EnumStringValue = TestEnum.Value10,
Flattening = new IdObject { IdValue = 10 },
CtorValue = 2,
IgnoredIntValue = 3,
IntInitOnlyValue = 4,
StringNullableTargetNotNullable = "fooBar",
DateTimeValueTargetTimeOnly = new DateTime(2018, 11, 29, 10, 11, 12),
DateTimeValueTargetDateOnly = new DateTime(2018, 11, 29, 10, 11, 12),
RenamedStringValue = "fooBar2",
UnflatteningIdValue = 7,
StringValue = "fooBar3",
IgnoredStringValue = "fooBar4",
NullableFlattening = new IdObject { IdValue = 20 },
SubObject = new InheritanceSubObject
{
BaseIntValue = 10,
SubIntValue = 20,
},
RecursiveObject = new TestObjectProjection
{
RequiredValue = -1,
EnumName = TestEnum.Value10,
EnumReverseStringValue = nameof(TestEnum.Value10),
EnumValue = TestEnum.Value20,
},
};
}

class ProjectionDbContext : DbContext
{
public DbSet<TestObjectProjection> Objects { get; set; } = null!;
public ProjectionDbContext(DbContextOptions options) : base(options)
{
}

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.UseSqlite("Data Source=:memory:");
public DbSet<TestObjectProjection> Objects { get; set; } = null!;

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ public static partial class ProjectionMapper
public static partial global::System.Linq.IQueryable<global::Riok.Mapperly.IntegrationTests.Dto.TestObjectDtoProjection> ProjectToDto(this global::System.Linq.IQueryable<global::Riok.Mapperly.IntegrationTests.Models.TestObjectProjection> q)
{
#nullable disable
return System.Linq.Queryable.Select(q, x => new global::Riok.Mapperly.IntegrationTests.Dto.TestObjectDtoProjection(x.CtorValue) { IntValue = x.IntValue, IntInitOnlyValue = x.IntInitOnlyValue, RequiredValue = x.RequiredValue, StringValue = x.StringValue, RenamedStringValue2 = x.RenamedStringValue, FlatteningIdValue = x.Flattening.IdValue, NullableFlatteningIdValue = x.NullableFlattening != null ? x.NullableFlattening.IdValue : default, NestedNullableIntValue = x.NestedNullable != null ? x.NestedNullable.IntValue : default, NestedNullable = x.NestedNullable != null ? new global::Riok.Mapperly.IntegrationTests.Dto.TestObjectNestedDto() { IntValue = x.NestedNullable.IntValue } : default, NestedNullableTargetNotNullable = x.NestedNullableTargetNotNullable != null ? new global::Riok.Mapperly.IntegrationTests.Dto.TestObjectNestedDto() { IntValue = x.NestedNullableTargetNotNullable.IntValue } : new global::Riok.Mapperly.IntegrationTests.Dto.TestObjectNestedDto(), StringNullableTargetNotNullable = x.StringNullableTargetNotNullable ?? "", SourceTargetSameObjectType = x.SourceTargetSameObjectType, NullableReadOnlyObjectCollection = x.NullableReadOnlyObjectCollection != null ? global::System.Linq.Enumerable.ToArray(global::System.Linq.Enumerable.Select(x.NullableReadOnlyObjectCollection, x1 => new global::Riok.Mapperly.IntegrationTests.Dto.TestObjectNestedDto() { IntValue = x1.IntValue })) : default, EnumValue = (global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue)x.EnumValue, EnumName = (global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByName)x.EnumName, EnumRawValue = (byte)x.EnumRawValue, EnumStringValue = (string)x.EnumStringValue.ToString(), EnumReverseStringValue = (global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue)System.Enum.Parse(typeof(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue), x.EnumReverseStringValue, false), SubObject = x.SubObject != null ? new global::Riok.Mapperly.IntegrationTests.Dto.InheritanceSubObjectDto() { SubIntValue = x.SubObject.SubIntValue, BaseIntValue = x.SubObject.BaseIntValue } : default, DateTimeValueTargetDateOnly = global::System.DateOnly.FromDateTime(x.DateTimeValueTargetDateOnly), DateTimeValueTargetTimeOnly = global::System.TimeOnly.FromDateTime(x.DateTimeValueTargetTimeOnly) });
return System.Linq.Queryable.Select(q, x => new global::Riok.Mapperly.IntegrationTests.Dto.TestObjectDtoProjection(x.CtorValue) { IntValue = x.IntValue, IntInitOnlyValue = x.IntInitOnlyValue, RequiredValue = x.RequiredValue, StringValue = x.StringValue, RenamedStringValue2 = x.RenamedStringValue, FlatteningIdValue = x.Flattening.IdValue, NullableFlatteningIdValue = x.NullableFlattening != null ? x.NullableFlattening.IdValue : default, NestedNullableIntValue = x.NestedNullable != null ? x.NestedNullable.IntValue : default, NestedNullable = x.NestedNullable != null ? new global::Riok.Mapperly.IntegrationTests.Dto.TestObjectNestedDto() { IntValue = x.NestedNullable.IntValue } : default, NestedNullableTargetNotNullable = x.NestedNullableTargetNotNullable != null ? new global::Riok.Mapperly.IntegrationTests.Dto.TestObjectNestedDto() { IntValue = x.NestedNullableTargetNotNullable.IntValue } : new global::Riok.Mapperly.IntegrationTests.Dto.TestObjectNestedDto(), StringNullableTargetNotNullable = x.StringNullableTargetNotNullable ?? "", SourceTargetSameObjectType = x.SourceTargetSameObjectType, NullableReadOnlyObjectCollection = x.NullableReadOnlyObjectCollection != null ? global::System.Linq.Enumerable.ToArray(global::System.Linq.Enumerable.Select(x.NullableReadOnlyObjectCollection, x1 => new global::Riok.Mapperly.IntegrationTests.Dto.TestObjectNestedDto() { IntValue = x1.IntValue })) : default, EnumValue = (global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue)x.EnumValue, EnumName = (global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByName)x.EnumName, EnumRawValue = (byte)x.EnumRawValue, EnumStringValue = (string)x.EnumStringValue.ToString(), EnumReverseStringValue = (global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByName)System.Enum.Parse(typeof(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByName), x.EnumReverseStringValue, false), SubObject = x.SubObject != null ? new global::Riok.Mapperly.IntegrationTests.Dto.InheritanceSubObjectDto() { SubIntValue = x.SubObject.SubIntValue, BaseIntValue = x.SubObject.BaseIntValue } : default, DateTimeValueTargetDateOnly = global::System.DateOnly.FromDateTime(x.DateTimeValueTargetDateOnly), DateTimeValueTargetTimeOnly = global::System.TimeOnly.FromDateTime(x.DateTimeValueTargetTimeOnly), ManuallyMapped = MapManual(x.ManuallyMapped) });
#nullable enable
}

Expand Down Expand Up @@ -53,9 +53,10 @@ public static partial class ProjectionMapper
target.EnumName = (global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByName)testObject.EnumName;
target.EnumRawValue = (byte)testObject.EnumRawValue;
target.EnumStringValue = MapToString(testObject.EnumStringValue);
target.EnumReverseStringValue = MapToTestEnumDtoByValue(testObject.EnumReverseStringValue);
target.EnumReverseStringValue = MapToTestEnumDtoByName(testObject.EnumReverseStringValue);
target.DateTimeValueTargetDateOnly = global::System.DateOnly.FromDateTime(testObject.DateTimeValueTargetDateOnly);
target.DateTimeValueTargetTimeOnly = global::System.TimeOnly.FromDateTime(testObject.DateTimeValueTargetTimeOnly);
target.ManuallyMapped = MapManual(testObject.ManuallyMapped);
return target;
}

Expand All @@ -77,14 +78,14 @@ private static string MapToString(global::Riok.Mapperly.IntegrationTests.Models.
};
}

private static global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue MapToTestEnumDtoByValue(string source)
private static global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByName MapToTestEnumDtoByName(string source)
{
return source switch
{
nameof(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue.DtoValue1) => global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue.DtoValue1,
nameof(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue.DtoValue2) => global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue.DtoValue2,
nameof(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue.DtoValue3) => global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue.DtoValue3,
_ => (global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue)System.Enum.Parse(typeof(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue), source, false),
nameof(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByName.Value10) => global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByName.Value10,
nameof(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByName.Value20) => global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByName.Value20,
nameof(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByName.Value30) => global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByName.Value30,
_ => (global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByName)System.Enum.Parse(typeof(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByName), source, false),
};
}

Expand All @@ -96,4 +97,4 @@ private static string MapToString(global::Riok.Mapperly.IntegrationTests.Models.
return target;
}
}
}
}
Loading

0 comments on commit a58f56f

Please # to comment.