diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d177df0..143ce55e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# v1.2.24 (27 November 2022) +- [#621](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/621) - Fix Join on inherited class [bug] contributed by [StefH](https://github.com/StefH) +- [#646](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/646) - Add more unittests for issue 645 contributed by [StefH](https://github.com/StefH) +- [#647](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/647) - Support nullable notation "xxx?" in As expression [feature] contributed by [StefH](https://github.com/StefH) +- [#614](https://github.com/zzzprojects/System.Linq.Dynamic.Core/issues/614) - Join problem with inherited entities [bug] + # v1.2.23 (12 November 2022) - [#644](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/644) - Add support for .NET 7 and EF Core 7 [feature] contributed by [StefH](https://github.com/StefH) diff --git a/Generate-ReleaseNotes.bat b/Generate-ReleaseNotes.bat index 4d61cd78..a4e98cd5 100644 --- a/Generate-ReleaseNotes.bat +++ b/Generate-ReleaseNotes.bat @@ -1,5 +1,5 @@ rem https://github.com/StefH/GitHubReleaseNotes -SET version=v1.2.23 +SET version=v1.2.24 GitHubReleaseNotes --output CHANGELOG.md --exclude-labels invalid question documentation wontfix --language en --version %version% --token %GH_TOKEN% diff --git a/src-console/ConsoleAppEF6_InMemory/Program.cs b/src-console/ConsoleAppEF6_InMemory/Program.cs index c223bc1a..a924708f 100644 --- a/src-console/ConsoleAppEF6_InMemory/Program.cs +++ b/src-console/ConsoleAppEF6_InMemory/Program.cs @@ -18,13 +18,25 @@ static async Task Main(string[] args) await using (var context = new TestContextEF6()) { context.Products.Add(new ProductDynamic { NullableInt = 1, Dict = new Dictionary { { "Name", "test" } } }); + context.Products.Add(new ProductDynamic { NullableInt = 2, Dict = new Dictionary { { "Name1", "test1" } } }); await context.SaveChangesAsync(); } + await using (var context = new TestContextEF6()) + { + var intType = typeof(int).FullName; + + var a1 = context.Products.Select($"\"{intType}\"(Key)").ToDynamicArray(); + Console.WriteLine("a1 {0}", string.Join(",", a1)); + + var a2 = context.Products.Select($"\"{intType}\"?(Key)").ToDynamicArray(); + Console.WriteLine("a2 {0}", string.Join(",", a2)); + } + await using (var context = new TestContextEF6()) { var resultsNormal = context.Products.Where(p => p.Dict["Name"] == "test").ToListAsync(); - + var results1 = await context.Products.Where("Dict.Name == @0", "test").ToListAsync(); Console.WriteLine("results1:"); foreach (var result in results1) diff --git a/src/System.Linq.Dynamic.Core/AnyOfTypes/AnyOfTypes.g.cs b/src/System.Linq.Dynamic.Core/AnyOfTypes/AnyOfTypes.g.cs new file mode 100644 index 00000000..840b4ddd --- /dev/null +++ b/src/System.Linq.Dynamic.Core/AnyOfTypes/AnyOfTypes.g.cs @@ -0,0 +1,16 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by https://github.com/StefH/AnyOf. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace AnyOfTypes +{ + internal enum AnyOfType + { + Undefined = 0, First, Second, Third, Fourth, Fifth, Sixth, Seventh, Eighth, Ninth, Tenth + } +} \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core/AnyOfTypes/AnyOf_2.g.cs b/src/System.Linq.Dynamic.Core/AnyOfTypes/AnyOf_2.g.cs new file mode 100644 index 00000000..c2a62b99 --- /dev/null +++ b/src/System.Linq.Dynamic.Core/AnyOfTypes/AnyOf_2.g.cs @@ -0,0 +1,156 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by https://github.com/StefH/AnyOf. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +using System; +using System.Diagnostics; +using System.Collections.Generic; + +namespace AnyOfTypes +{ + [DebuggerDisplay("{_thisType}, AnyOfType = {_currentType}; Type = {_currentValueType?.Name}; Value = '{ToString()}'")] + internal struct AnyOf + { + private readonly string _thisType => $"AnyOf<{typeof(TFirst).Name}, {typeof(TSecond).Name}>"; + private readonly int _numberOfTypes; + private readonly object _currentValue; + private readonly Type _currentValueType; + private readonly AnyOfType _currentType; + + private readonly TFirst _first; + private readonly TSecond _second; + + public readonly AnyOfType[] AnyOfTypes => new[] { AnyOfType.First, AnyOfType.Second }; + public readonly Type[] Types => new[] { typeof(TFirst), typeof(TSecond) }; + public bool IsUndefined => _currentType == AnyOfType.Undefined; + public bool IsFirst => _currentType == AnyOfType.First; + public bool IsSecond => _currentType == AnyOfType.Second; + + public static implicit operator AnyOf(TFirst value) => new AnyOf(value); + + public static implicit operator TFirst(AnyOf @this) => @this.First; + + public AnyOf(TFirst value) + { + _numberOfTypes = 2; + _currentType = AnyOfType.First; + _currentValue = value; + _currentValueType = typeof(TFirst); + _first = value; + _second = default; + } + + public TFirst First + { + get + { + Validate(AnyOfType.First); + return _first; + } + } + + public static implicit operator AnyOf(TSecond value) => new AnyOf(value); + + public static implicit operator TSecond(AnyOf @this) => @this.Second; + + public AnyOf(TSecond value) + { + _numberOfTypes = 2; + _currentType = AnyOfType.Second; + _currentValue = value; + _currentValueType = typeof(TSecond); + _second = value; + _first = default; + } + + public TSecond Second + { + get + { + Validate(AnyOfType.Second); + return _second; + } + } + + private void Validate(AnyOfType desiredType) + { + if (desiredType != _currentType) + { + throw new InvalidOperationException($"Attempting to get {desiredType} when {_currentType} is set"); + } + } + + public AnyOfType CurrentType + { + get + { + return _currentType; + } + } + + public object CurrentValue + { + get + { + return _currentValue; + } + } + + public Type CurrentValueType + { + get + { + return _currentValueType; + } + } + + public override int GetHashCode() + { + var fields = new object[] + { + _numberOfTypes, + _currentValue, + _currentType, + _first, + _second, + typeof(TFirst), + typeof(TSecond), + }; + return HashCodeCalculator.GetHashCode(fields); + } + + private bool Equals(AnyOf other) + { + return _currentType == other._currentType && + _numberOfTypes == other._numberOfTypes && + EqualityComparer.Default.Equals(_currentValue, other._currentValue) && + EqualityComparer.Default.Equals(_first, other._first) && + EqualityComparer.Default.Equals(_second, other._second); + } + + public static bool operator ==(AnyOf obj1, AnyOf obj2) + { + return obj1.Equals(obj2); + } + + public static bool operator !=(AnyOf obj1, AnyOf obj2) + { + return !obj1.Equals(obj2); + } + + public override bool Equals(object obj) + { + return obj is AnyOf o && Equals(o); + } + + public override string ToString() + { + return IsUndefined ? null : $"{_currentValue}"; + } + } +} \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core/AnyOfTypes/HashCodeCalculator.g.cs b/src/System.Linq.Dynamic.Core/AnyOfTypes/HashCodeCalculator.g.cs new file mode 100644 index 00000000..8305eaca --- /dev/null +++ b/src/System.Linq.Dynamic.Core/AnyOfTypes/HashCodeCalculator.g.cs @@ -0,0 +1,30 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by https://github.com/StefH/AnyOf. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +using System.Collections.Generic; +using System.Linq; + +namespace AnyOfTypes +{ + // Code is based on https://github.com/Informatievlaanderen/hashcode-calculator + internal static class HashCodeCalculator + { + public static int GetHashCode(IEnumerable hashFieldValues) + { + const int offset = unchecked((int)2166136261); + const int prime = 16777619; + + static int HashCodeAggregator(int hashCode, object value) => value == null + ? (hashCode ^ 0) * prime + : (hashCode ^ value.GetHashCode()) * prime; + + return hashFieldValues.Aggregate(offset, HashCodeAggregator); + } + } +} \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index 7feddedc..38f7a7fd 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -10,6 +10,7 @@ using System.Linq.Dynamic.Core.Validation; using System.Linq.Expressions; using System.Reflection; +using AnyOfTypes; namespace System.Linq.Dynamic.Core.Parser { @@ -734,7 +735,7 @@ private Expression ParseUnary() private Expression ParsePrimary() { - Expression expr = ParsePrimaryStart(); + var expr = ParsePrimaryStart(); _expressionHelper.WrapConstantExpression(ref expr); while (true) @@ -757,6 +758,7 @@ private Expression ParsePrimary() break; } } + return expr; } @@ -766,38 +768,55 @@ private Expression ParsePrimaryStart() { case TokenId.Identifier: return ParseIdentifier(); + case TokenId.StringLiteral: - return ParseStringLiteral(); + var expressionOrType = ParseStringLiteral(false); + return expressionOrType.IsFirst ? expressionOrType.First : ParseTypeAccess(expressionOrType.Second, false); + case TokenId.IntegerLiteral: return ParseIntegerLiteral(); + case TokenId.RealLiteral: return ParseRealLiteral(); + case TokenId.OpenParen: return ParseParenExpression(); + default: throw ParseError(Res.ExpressionExpected); } } - private Expression ParseStringLiteral() + private AnyOf ParseStringLiteral(bool forceParseAsString) { _textParser.ValidateToken(TokenId.StringLiteral); - string result = StringParser.ParseString(_textParser.CurrentToken.Text); + var stringValue = StringParser.ParseString(_textParser.CurrentToken.Text); if (_textParser.CurrentToken.Text[0] == '\'') { - if (result.Length > 1) + if (stringValue.Length > 1) { throw ParseError(Res.InvalidCharacterLiteral); } _textParser.NextToken(); - return ConstantExpressionHelper.CreateLiteral(result[0], result); + return ConstantExpressionHelper.CreateLiteral(stringValue[0], stringValue); } _textParser.NextToken(); - return ConstantExpressionHelper.CreateLiteral(result, result); + + if (_parsingConfig.SupportFullTypeCastingUsingDoubleQuotes && !forceParseAsString && stringValue.Length > 2 && stringValue.Contains('.')) + { + // Try to resolve this string as a type + var type = _typeFinder.FindTypeByName(stringValue, null, false); + if (type is { }) + { + return type; + } + } + + return ConstantExpressionHelper.CreateLiteral(stringValue, stringValue); } private Expression ParseIntegerLiteral() @@ -844,7 +863,7 @@ private Expression ParseIdentifier() { if (value is Type typeValue) { - return ParseTypeAccess(typeValue); + return ParseTypeAccess(typeValue, true); } switch (value) @@ -1476,10 +1495,13 @@ private Expression ParseLambdaInvocation(LambdaExpression lambda) return Expression.Invoke(lambda, args); } - private Expression ParseTypeAccess(Type type) + private Expression ParseTypeAccess(Type type, bool getNext) { int errorPos = _textParser.CurrentToken.Pos; - _textParser.NextToken(); + if (getNext) + { + _textParser.NextToken(); + } if (_textParser.CurrentToken.Id == TokenId.Question) { @@ -1496,7 +1518,16 @@ private Expression ParseTypeAccess(Type type) bool shorthand = _textParser.CurrentToken.Id == TokenId.StringLiteral; if (_textParser.CurrentToken.Id == TokenId.OpenParen || shorthand) { - Expression[] args = shorthand ? new[] { ParseStringLiteral() } : ParseArgumentList(); + Expression[] args; + if (shorthand) + { + var expressionOrType = ParseStringLiteral(true); + args = new[] { expressionOrType.First }; + } + else + { + args = ParseArgumentList(); + } // If only 1 argument and // - the arg is ConstantExpression, return the conversion diff --git a/src/System.Linq.Dynamic.Core/ParsingConfig.cs b/src/System.Linq.Dynamic.Core/ParsingConfig.cs index da6c2a03..e7177079 100644 --- a/src/System.Linq.Dynamic.Core/ParsingConfig.cs +++ b/src/System.Linq.Dynamic.Core/ParsingConfig.cs @@ -107,7 +107,7 @@ public IQueryableAnalyzer QueryableAnalyzer /// Determines if the context keywords (it, parent, and root) are valid and usable inside a Dynamic Linq string expression. /// Does not affect the usability of the equivalent context symbols ($, ^ and ~). /// - /// Default value is true. + /// Default value is false. /// public bool AreContextKeywordsEnabled { get; set; } = true; @@ -117,7 +117,7 @@ public IQueryableAnalyzer QueryableAnalyzer /// /// Remark: when this setting is set to 'true', make sure to supply this ParsingConfig as first parameter on the extension methods. /// - /// Default value is false. + /// Default value is false. /// public bool EvaluateGroupByAtDatabase { get; set; } @@ -125,28 +125,28 @@ public IQueryableAnalyzer QueryableAnalyzer /// Use Parameterized Names in generated dynamic SQL query. /// See https://github.com/graeme-hill/gblog/blob/master/source_content/articles/2014.139_entity-framework-dynamic-queries-and-parameterization.mkd /// - /// Default value is false. + /// Default value is false. /// public bool UseParameterizedNamesInDynamicQuery { get; set; } = false; /// /// Allows the New() keyword to evaluate any available Type. /// - /// Default value is false. + /// Default value is false. /// public bool AllowNewToEvaluateAnyType { get; set; } = false; /// /// Renames the (Typed)ParameterExpression empty Name to a the correct supplied name from `it`. /// - /// Default value is false. + /// Default value is false. /// public bool RenameParameterExpression { get; set; } = false; /// /// Prevents any System.Linq.Expressions.ParameterExpression.Name value from being empty by substituting a random 16 character word. /// - /// Default value is false. + /// Default value is false. /// public bool RenameEmptyParameterExpressionNames { get; set; } @@ -155,7 +155,7 @@ public IQueryableAnalyzer QueryableAnalyzer /// this flag to disable this behaviour and have parsing fail when parsing an expression /// where a member access on a non existing member happens. /// - /// Default value is false. + /// Default value is false. /// public bool DisableMemberAccessToIndexAccessorFallback { get; set; } = false; @@ -164,7 +164,7 @@ public IQueryableAnalyzer QueryableAnalyzer /// Use this flag to use the CustomTypeProvider to resolve types by a simple name like "Employee" instead of "MyDatabase.Entities.Employee". /// Note that a first matching type is returned and this functionality needs to scan all types from all assemblies, so use with caution. /// - /// Default value is false. + /// Default value is false. /// public bool ResolveTypesBySimpleName { get; set; } = false; @@ -179,7 +179,7 @@ public IQueryableAnalyzer QueryableAnalyzer /// By default DateTime (like 'Fri, 10 May 2019 11:03:17 GMT') is parsed as local time. /// Use this flag to parse all DateTime strings as UTC. /// - /// Default value is false. + /// Default value is false. /// public bool DateTimeIsParsedAsUTC { get; set; } = false; @@ -198,8 +198,18 @@ public IQueryableAnalyzer QueryableAnalyzer /// /// When using the NullPropagating function np(...), use a "default value" for non-nullable value types instead of "null value". /// - /// Default value is false. + /// Default value is false. /// public bool NullPropagatingUseDefaultValueForNonNullableValueTypes { get; set; } = false; + + /// + /// Support casting to a full type using double quotes. + /// + /// var result = queryable.Select($"\"System.DateTime\"(LastUpdate)"); + /// + /// + /// Default value is true. + /// + public bool SupportFullTypeCastingUsingDoubleQuotes { get; set; } = true; } -} +} \ No newline at end of file diff --git a/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs b/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs index dc3724cb..68c79c64 100644 --- a/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs @@ -384,15 +384,15 @@ public void DynamicExpressionParser_ParseLambda_UseParameterizedNamesInDynamicQu var expressionAsString = expression.ToString(); // Assert - var queriedProp = typeof(SimpleValuesModel).GetProperty(propName, BindingFlags.Instance | BindingFlags.Public); + var queriedProp = typeof(SimpleValuesModel).GetProperty(propName, BindingFlags.Instance | BindingFlags.Public)!; var queriedPropType = queriedProp.PropertyType; var queriedPropUnderlyingType = Nullable.GetUnderlyingType(queriedPropType); Check.That(expressionAsString).IsEqualTo($"Param_0 => (Param_0.{propName} == {ExpressionString.NullableConversion($"value(System.Linq.Dynamic.Core.Parser.WrappedValue`1[{queriedPropUnderlyingType}]).Value")})"); - dynamic constantExpression = ((MemberExpression)(((expression.Body as BinaryExpression).Right as UnaryExpression).Operand)).Expression as ConstantExpression; + dynamic constantExpression = (ConstantExpression)((MemberExpression)((UnaryExpression)((BinaryExpression)expression.Body).Right).Operand).Expression; object wrapperObj = constantExpression.Value; - var propertyInfo = wrapperObj.GetType().GetProperty("Value", BindingFlags.Instance | BindingFlags.Public); + var propertyInfo = wrapperObj.GetType().GetProperty("Value", BindingFlags.Instance | BindingFlags.Public)!; var value = propertyInfo.GetValue(wrapperObj); value.Should().Be(Convert.ChangeType(valueString, Nullable.GetUnderlyingType(queriedPropType) ?? queriedPropType, culture)); diff --git a/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs b/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs index aa845375..f5537a6c 100644 --- a/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs @@ -286,6 +286,74 @@ public void ExpressionTests_Cast_To_Enum_Using_DynamicLinqType() Assert.Equal(expectedResult.ToArray(), result.ToDynamicArray()); } + [Fact] + public void ExpressionTests_Cast_To_FullTypeDateTime_Using_DynamicLinqType() + { + // Arrange + var list = new List + { + new SimpleValuesModel { DateTime = DateTime.Now } + }; + + // Act + var expectedResult = list.Select(x => x.DateTime); + var result = list.AsQueryable().Select($"\"{typeof(DateTime).FullName}\"(DateTime)"); + + // Assert + Assert.Equal(expectedResult.ToArray(), result.ToDynamicArray()); + } + + [Fact] + public void ExpressionTests_Cast_To_FullTypeDateTimeNullable_Using_DynamicLinqType() + { + // Arrange + var list = new List + { + new SimpleValuesModel { DateTime = DateTime.Now } + }; + + // Act + var expectedResult = list.Select(x => (DateTime?)x.DateTime); + var result = list.AsQueryable().Select($"\"{typeof(DateTime).FullName}\"?(DateTime)"); + + // Assert + Assert.Equal(expectedResult.ToArray(), result.ToDynamicArray()); + } + + [Fact] + public void ExpressionTests_Cast_To_FullTypeEnum_Using_DynamicLinqType() + { + // Arrange + var list = new List + { + new SimpleValuesModel { EnumValueDynamicLinqType = SimpleValuesModelEnumAsDynamicLinqType.A } + }; + + // Act + var expectedResult = list.Select(x => x.EnumValueDynamicLinqType); + var result = list.AsQueryable().Select($"\"{typeof(SimpleValuesModelEnumAsDynamicLinqType).FullName}\"(EnumValueDynamicLinqType)"); + + // Assert + Assert.Equal(expectedResult.ToArray(), result.ToDynamicArray()); + } + + [Fact] + public void ExpressionTests_Cast_To_FullTypeNullableEnum_Using_DynamicLinqType() + { + // Arrange + var list = new List + { + new SimpleValuesModel { EnumValueDynamicLinqType = SimpleValuesModelEnumAsDynamicLinqType.A } + }; + + // Act + var expectedResult = list.Select(x => (SimpleValuesModelEnumAsDynamicLinqType?)x.EnumValueDynamicLinqType); + var result = list.AsQueryable().Select($"\"{typeof(SimpleValuesModelEnumAsDynamicLinqType).FullName}\"?(EnumValueDynamicLinqType)"); + + // Assert + Assert.Equal(expectedResult.ToArray(), result.ToDynamicArray()); + } + [Fact] public void ExpressionTests_Cast_To_Enum_Using_CustomTypeProvider() { diff --git a/test/System.Linq.Dynamic.Core.Tests/Helpers/Models/SimpleValuesModel.cs b/test/System.Linq.Dynamic.Core.Tests/Helpers/Models/SimpleValuesModel.cs index c431f380..63a74d73 100644 --- a/test/System.Linq.Dynamic.Core.Tests/Helpers/Models/SimpleValuesModel.cs +++ b/test/System.Linq.Dynamic.Core.Tests/Helpers/Models/SimpleValuesModel.cs @@ -32,5 +32,7 @@ public class SimpleValuesModel public SimpleValuesModelEnum EnumValue { get; set; } public SimpleValuesModelEnumAsDynamicLinqType EnumValueDynamicLinqType { get; set; } + + public DateTime DateTime { get; set; } } } \ No newline at end of file diff --git a/version.xml b/version.xml index 165a71ac..1e9f7b0b 100644 --- a/version.xml +++ b/version.xml @@ -1,5 +1,5 @@ - 23 + 24-preview-02