* Update to .NET 10 RC 1. * Update NPoco to 6.1.0 and resolve binary breaking changes. * Resolved behavioural breaking changes (need to use List<string> over string[] for Contains). * Update dependency on NPoco.SqlServer. * Further fixes from manual testing. * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fixed comment typos. * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fixed nullability issue. * Fix database creation issue * Fix missing to list --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: nikolajlauridsen <nikolajlauridsen@protonmail.ch>
260 lines
12 KiB
C#
260 lines
12 KiB
C#
// Copyright (c) Umbraco.
|
|
// See LICENSE for more details.
|
|
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using System.Linq.Expressions;
|
|
using Moq;
|
|
using NUnit.Framework;
|
|
using Umbraco.Cms.Core.Models;
|
|
using Umbraco.Cms.Core.Models.Membership;
|
|
using Umbraco.Cms.Core.Persistence;
|
|
using Umbraco.Cms.Core.PropertyEditors;
|
|
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
|
|
using Umbraco.Cms.Infrastructure.Persistence.Querying;
|
|
using Umbraco.Cms.Infrastructure.Serialization;
|
|
using Umbraco.Cms.Tests.UnitTests.TestHelpers;
|
|
|
|
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Persistence.Querying;
|
|
|
|
[TestFixture]
|
|
public class ExpressionTests : BaseUsingSqlSyntax
|
|
{
|
|
[Test]
|
|
public void Equals_Claus_With_Two_Entity_Values()
|
|
{
|
|
var dataType = new DataType(
|
|
new VoidEditor(Mock.Of<IDataValueEditorFactory>()),
|
|
new SystemTextConfigurationEditorJsonSerializer(new DefaultJsonSerializerEncoderFactory()))
|
|
{ Id = 12345 };
|
|
Expression<Func<PropertyType, bool>> predicate = p => p.DataTypeId == dataType.Id;
|
|
var modelToSqlExpressionHelper = new ModelToSqlExpressionVisitor<PropertyType>(SqlContext.SqlSyntax, Mappers);
|
|
var result = modelToSqlExpressionHelper.Visit(predicate);
|
|
|
|
Debug.Print("Model to Sql ExpressionHelper: \n" + result);
|
|
|
|
Assert.AreEqual("([cmsPropertyType].[dataTypeId] = @0)", result);
|
|
Assert.AreEqual(12345, modelToSqlExpressionHelper.GetSqlParameters()[0]);
|
|
}
|
|
|
|
[Test]
|
|
public void Can_Query_With_Content_Type_Alias()
|
|
{
|
|
// Arrange
|
|
Expression<Func<IMedia, bool>> predicate = content => content.ContentType.Alias == "Test";
|
|
var modelToSqlExpressionHelper = new ModelToSqlExpressionVisitor<IContent>(SqlContext.SqlSyntax, Mappers);
|
|
var result = modelToSqlExpressionHelper.Visit(predicate);
|
|
|
|
Debug.Print("Model to Sql ExpressionHelper: \n" + result);
|
|
|
|
Assert.AreEqual("([cmsContentType].[alias] = @0)", result);
|
|
Assert.AreEqual("Test", modelToSqlExpressionHelper.GetSqlParameters()[0]);
|
|
}
|
|
|
|
[Test]
|
|
public void Can_Query_With_Content_Type_Aliases_IEnumerable()
|
|
{
|
|
// Arrange - Contains is IEnumerable.Contains extension method
|
|
var aliases = new List<string> { "Test1", "Test2" };
|
|
Expression<Func<IMedia, bool>> predicate = content => aliases.Contains(content.ContentType.Alias);
|
|
var modelToSqlExpressionHelper = new ModelToSqlExpressionVisitor<IContent>(SqlContext.SqlSyntax, Mappers);
|
|
var result = modelToSqlExpressionHelper.Visit(predicate);
|
|
|
|
Debug.Print("Model to Sql ExpressionHelper: \n" + result);
|
|
|
|
Assert.AreEqual("[cmsContentType].[alias] IN (@1,@2)", result);
|
|
Assert.AreEqual("Test1", modelToSqlExpressionHelper.GetSqlParameters()[1]);
|
|
Assert.AreEqual("Test2", modelToSqlExpressionHelper.GetSqlParameters()[2]);
|
|
}
|
|
|
|
[Test]
|
|
public void Can_Query_With_Content_Type_Aliases_List()
|
|
{
|
|
// Arrange - Contains is List.Contains instance method
|
|
var aliases = new List<string> { "Test1", "Test2" };
|
|
Expression<Func<IMedia, bool>> predicate = content => aliases.Contains(content.ContentType.Alias);
|
|
var modelToSqlExpressionHelper = new ModelToSqlExpressionVisitor<IContent>(SqlContext.SqlSyntax, Mappers);
|
|
var result = modelToSqlExpressionHelper.Visit(predicate);
|
|
|
|
Debug.Print("Model to Sql ExpressionHelper: \n" + result);
|
|
|
|
Assert.AreEqual("[cmsContentType].[alias] IN (@1,@2)", result);
|
|
Assert.AreEqual("Test1", modelToSqlExpressionHelper.GetSqlParameters()[1]);
|
|
Assert.AreEqual("Test2", modelToSqlExpressionHelper.GetSqlParameters()[2]);
|
|
}
|
|
|
|
[Test]
|
|
public void CachedExpression_Can_Verify_Path_StartsWith_Predicate_In_Same_Result()
|
|
{
|
|
// Arrange
|
|
|
|
// use a single cached expression for multiple expressions and ensure the correct output
|
|
// is done for both of them.
|
|
var cachedExpression = new CachedExpression();
|
|
|
|
Expression<Func<IContent, bool>> predicate1 = content => content.Path.StartsWith("-1");
|
|
cachedExpression.Wrap(predicate1);
|
|
var modelToSqlExpressionHelper1 = new ModelToSqlExpressionVisitor<IContent>(SqlContext.SqlSyntax, Mappers);
|
|
var result1 = modelToSqlExpressionHelper1.Visit(cachedExpression);
|
|
Assert.AreEqual("upper([umbracoNode].[path]) LIKE upper(@0)", result1);
|
|
Assert.AreEqual("-1%", modelToSqlExpressionHelper1.GetSqlParameters()[0]);
|
|
|
|
Expression<Func<IContent, bool>> predicate2 = content => content.Path.StartsWith("-1,123,97");
|
|
cachedExpression.Wrap(predicate2);
|
|
var modelToSqlExpressionHelper2 = new ModelToSqlExpressionVisitor<IContent>(SqlContext.SqlSyntax, Mappers);
|
|
var result2 = modelToSqlExpressionHelper2.Visit(cachedExpression);
|
|
Assert.AreEqual("upper([umbracoNode].[path]) LIKE upper(@0)", result2);
|
|
Assert.AreEqual("-1,123,97%", modelToSqlExpressionHelper2.GetSqlParameters()[0]);
|
|
}
|
|
|
|
[Test]
|
|
public void Can_Verify_Path_StartsWith_Predicate_In_Same_Result()
|
|
{
|
|
// Arrange
|
|
Expression<Func<IContent, bool>> predicate = content => content.Path.StartsWith("-1");
|
|
var modelToSqlExpressionHelper = new ModelToSqlExpressionVisitor<IContent>(SqlContext.SqlSyntax, Mappers);
|
|
var result = modelToSqlExpressionHelper.Visit(predicate);
|
|
|
|
Assert.AreEqual("upper([umbracoNode].[path]) LIKE upper(@0)", result);
|
|
Assert.AreEqual("-1%", modelToSqlExpressionHelper.GetSqlParameters()[0]);
|
|
}
|
|
|
|
[Test]
|
|
public void Can_Verify_ParentId_StartsWith_Predicate_In_Same_Result()
|
|
{
|
|
// Arrange
|
|
Expression<Func<IContent, bool>> predicate = content => content.ParentId == -1;
|
|
var modelToSqlExpressionHelper = new ModelToSqlExpressionVisitor<IContent>(SqlContext.SqlSyntax, Mappers);
|
|
var result = modelToSqlExpressionHelper.Visit(predicate);
|
|
|
|
Debug.Print("Model to Sql ExpressionHelper: \n" + result);
|
|
|
|
Assert.AreEqual("([umbracoNode].[parentId] = @0)", result);
|
|
Assert.AreEqual(-1, modelToSqlExpressionHelper.GetSqlParameters()[0]);
|
|
}
|
|
|
|
[Test]
|
|
public void Equals_Operator_For_Value_Gets_Escaped()
|
|
{
|
|
Expression<Func<IUser, bool>> predicate = user => user.Username == "hello@world.com";
|
|
var modelToSqlExpressionHelper = new ModelToSqlExpressionVisitor<IUser>(SqlContext.SqlSyntax, Mappers);
|
|
var result = modelToSqlExpressionHelper.Visit(predicate);
|
|
|
|
Debug.Print("Model to Sql ExpressionHelper: \n" + result);
|
|
|
|
Assert.AreEqual("([umbracoUser].[userLogin] = @0)", result);
|
|
Assert.AreEqual("hello@world.com", modelToSqlExpressionHelper.GetSqlParameters()[0]);
|
|
}
|
|
|
|
[Test]
|
|
public void Equals_Method_For_Value_Gets_Escaped()
|
|
{
|
|
Expression<Func<IUser, bool>> predicate = user => user.Username.Equals("hello@world.com");
|
|
var modelToSqlExpressionHelper = new ModelToSqlExpressionVisitor<IUser>(SqlContext.SqlSyntax, Mappers);
|
|
var result = modelToSqlExpressionHelper.Visit(predicate);
|
|
|
|
Debug.Print("Model to Sql ExpressionHelper: \n" + result);
|
|
|
|
Assert.AreEqual("upper([umbracoUser].[userLogin]) = upper(@0)", result);
|
|
Assert.AreEqual("hello@world.com", modelToSqlExpressionHelper.GetSqlParameters()[0]);
|
|
}
|
|
|
|
[Test]
|
|
public void Sql_Replace_Mapped()
|
|
{
|
|
Expression<Func<IUser, bool>> predicate = user => user.Username.Replace("@world", "@test") == "hello@test.com";
|
|
var modelToSqlExpressionHelper = new ModelToSqlExpressionVisitor<IUser>(SqlContext.SqlSyntax, Mappers);
|
|
var result = modelToSqlExpressionHelper.Visit(predicate);
|
|
|
|
Debug.Print("Model to Sql ExpressionHelper: \n" + result);
|
|
|
|
Assert.AreEqual("(replace([umbracoUser].[userLogin], @1, @2) = @0)", result);
|
|
Assert.AreEqual("hello@test.com", modelToSqlExpressionHelper.GetSqlParameters()[0]);
|
|
Assert.AreEqual("@world", modelToSqlExpressionHelper.GetSqlParameters()[1]);
|
|
Assert.AreEqual("@test", modelToSqlExpressionHelper.GetSqlParameters()[2]);
|
|
}
|
|
|
|
[Test]
|
|
public void Sql_In()
|
|
{
|
|
// This unit test reveals a breaking change for .NET 10 and NPoco.
|
|
// When a List<string> is used with Contains in an expression, we call bool List<string>.Contains(string), which NPoco translates to SQL IN.
|
|
// However, if we use string[] with Contains, we call the extension method bool ReadOnlySpan.Contains<string>(string) which
|
|
// blows up in ExpressionVisitorBase as the method name of "op_Implicit" is unrecognized.
|
|
// As such we need to ensure we use a List<string and not string[] here (and anywhere we call Contains).
|
|
|
|
//var userNames = new[] { "hello@world.com", "blah@blah.com" };
|
|
var userNames = new List<string> { "hello@world.com", "blah@blah.com" };
|
|
|
|
Expression<Func<IUser, bool>> predicate = user => userNames.Contains(user.Username);
|
|
var modelToSqlExpressionHelper = new ModelToSqlExpressionVisitor<IUser>(SqlContext.SqlSyntax, Mappers);
|
|
var result = modelToSqlExpressionHelper.Visit(predicate);
|
|
|
|
Debug.Print("Model to Sql ExpressionHelper: \n" + result);
|
|
|
|
Assert.AreEqual("[umbracoUser].[userLogin] IN (@1,@2)", result);
|
|
Assert.AreEqual("hello@world.com", modelToSqlExpressionHelper.GetSqlParameters()[1]);
|
|
Assert.AreEqual("blah@blah.com", modelToSqlExpressionHelper.GetSqlParameters()[2]);
|
|
}
|
|
|
|
private string GetSomeValue(string s) => "xx" + s + "xx";
|
|
|
|
private class Foo
|
|
{
|
|
public string Value { get; set; }
|
|
}
|
|
|
|
[Test]
|
|
public void StartsWith()
|
|
{
|
|
Expression<Func<UserDto, object>> predicate = user => user.Login.StartsWith("aaaaa");
|
|
var modelToSqlExpressionHelper = new PocoToSqlExpressionVisitor<UserDto>(SqlContext, null);
|
|
var result = modelToSqlExpressionHelper.Visit(predicate);
|
|
|
|
Console.WriteLine(result);
|
|
Assert.AreEqual("upper([umbracoUser].[userLogin]) LIKE upper(@0)", result);
|
|
|
|
predicate = user => user.Login.StartsWith(GetSomeValue("aaaaa"));
|
|
modelToSqlExpressionHelper = new PocoToSqlExpressionVisitor<UserDto>(SqlContext, null);
|
|
result = modelToSqlExpressionHelper.Visit(predicate);
|
|
|
|
Console.WriteLine(result);
|
|
Assert.AreEqual("upper([umbracoUser].[userLogin]) LIKE upper(@0)", result);
|
|
|
|
predicate = user => user.Login.StartsWith(GetSomeValue("aaaaa"));
|
|
modelToSqlExpressionHelper = new PocoToSqlExpressionVisitor<UserDto>(SqlContext, null);
|
|
result = modelToSqlExpressionHelper.Visit(predicate);
|
|
|
|
Console.WriteLine(result);
|
|
Assert.AreEqual("upper([umbracoUser].[userLogin]) LIKE upper(@0)", result);
|
|
|
|
var foo = new Foo { Value = "aaaaa" };
|
|
predicate = user => user.Login.StartsWith(foo.Value);
|
|
modelToSqlExpressionHelper = new PocoToSqlExpressionVisitor<UserDto>(SqlContext, null);
|
|
result = modelToSqlExpressionHelper.Visit(predicate);
|
|
|
|
Console.WriteLine(result);
|
|
Assert.AreEqual("upper([umbracoUser].[userLogin]) LIKE upper(@0)", result);
|
|
|
|
// below does not work, we want to output
|
|
// LIKE concat([group].[name], ',%')
|
|
// and how would we get the comma there? we'd have to parse .StartsWith(group.Name + ',')
|
|
// which is going to be quite complicated => no
|
|
|
|
// how can we do user.Login.StartsWith(other.Value) ?? to use in WHERE or JOIN.ON clauses ??
|
|
//// Expression<Func<UserDto, UserGroupDto, object>> predicate2 = (user, group) => user.Login.StartsWith(group.Name);
|
|
//// var modelToSqlExpressionHelper2 = new PocoToSqlExpressionVisitor<UserDto, UserGroupDto>(SqlContext, null, null);
|
|
//// var result2 = modelToSqlExpressionHelper2.Visit(predicate2); // fails, for now
|
|
|
|
//// Console.WriteLine(result2);
|
|
|
|
Expression<Func<UserDto, UserGroupDto, object>> predicate3 = (user, group) =>
|
|
SqlExtensionsStatics.SqlText<bool>(user.Login, group.Name, (n1, n2) => $"({n1} LIKE concat({n2}, ',%'))");
|
|
var modelToSqlExpressionHelper3 = new PocoToSqlExpressionVisitor<UserDto, UserGroupDto>(SqlContext, null, null);
|
|
var result3 = modelToSqlExpressionHelper3.Visit(predicate3);
|
|
|
|
Console.WriteLine(result3);
|
|
}
|
|
}
|