Files
Umbraco-CMS/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/Querying/ExpressionTests.cs
Andy Butland dca70a0bd0 Dependencies: Updates to .NET 10 RC and NPoco 6.1 (#20184)
* 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>
2025-09-19 08:59:03 +00:00

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