diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c99aba..d31a288 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,9 @@ -# 1.0.2 (2019-05-02) +# 1.1.0 (2019-05-05) + +## Features +* **_find:** IQueryable methods that are not supported by CouchDB are evaluated in-memory using the IEnumerable counterpart, if possible. + +# 1.0.2 (2019-05-02) ## Bug Fixes * **_find:** Boolean member expressions converted to binary expressions in Where (Fix [#PR36](https://github.com/matteobortolazzo/couchdb-net/pull/36)). diff --git a/LATEST_CHANGE.md b/LATEST_CHANGE.md index f89f4b3..2128c8f 100644 --- a/LATEST_CHANGE.md +++ b/LATEST_CHANGE.md @@ -1,2 +1,2 @@ -## Bug Fixes -* **_find:** Boolean member expression converted to binary expression in Where (Fix [#PR36](https://github.com/matteobortolazzo/couchdb-net/pull/36)). +## Features +* **_find:** IQueryable methods that are not supported by CouchDB are evaluated in-memory using the IEnumerable counterpart, if possible. diff --git a/README.md b/README.md index 7c7a0d1..9e82e0e 100644 --- a/README.md +++ b/README.md @@ -179,6 +179,31 @@ If the Where method is not called in the expression, it will at an empty selecto | execution_stats | IncludeExecutionStats() | | conflicts | IncludeConflicts() | +### Composite methods + +Some methods that are not directly supported by CouchDB are converted to a composition of supported ones. + +| Input | Output | +|:----------------------------------|:--------------------------------------| +| Min(r => r.Age) | OrderBy(r => r.Age).Take(1) | +| Max(r => r.Age) | OrderByDescending(r => r.Age).Take(1) | +| Single() | Take(1) | +| SingleOrDefault() | Take(1) | +| Single(r => r.Age == 19) | Where(r => r.Age == 19).Take(1) | +| SingleOrDefault(r => r.Age == 19) | Where(r => r.Age == 19).Take(1) | + +**WARN**: Do not call a method twice, for example: `Where(func).Single(func)` won't work. + +**WARN**: Since Max and Min use **sort**, an *index* must be created. + + +### All other IQueryables + +IQueryable methods that are not natively supported by CouchDB are evaluated in-memory using the IEnumerable counterpart, if possible. + +For example: `All` `Any` `Avg` `Count` `DefaultIfEmpty` `ElementAt` `ElementAtOrDefault` `GroupBy` `Last` `Reverse` `SelectMany` `Sum` + + ## Client operations ```csharp diff --git a/src/CouchDB.Driver/CouchClientAuthentication.cs b/src/CouchDB.Driver/CouchClientAuthentication.cs index 8483238..29d1317 100644 --- a/src/CouchDB.Driver/CouchClientAuthentication.cs +++ b/src/CouchDB.Driver/CouchClientAuthentication.cs @@ -4,6 +4,7 @@ using Flurl.Http; using Nito.AsyncEx; using System; +using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Text.RegularExpressions; @@ -25,7 +26,7 @@ protected virtual void OnBeforeCall(HttpCall httpCall) case AuthenticationType.None: break; case AuthenticationType.Basic: - httpCall.FlurlRequest.WithBasicAuth(_settings.Username, _settings.Password); + httpCall.FlurlRequest = httpCall.FlurlRequest.WithBasicAuth(_settings.Username, _settings.Password); break; case AuthenticationType.Cookie: var isTokenExpired = @@ -35,7 +36,7 @@ protected virtual void OnBeforeCall(HttpCall httpCall) { AsyncContext.Run(() => LoginAsync()); } - httpCall.FlurlRequest.EnableCookies().WithCookie("AuthSession", _cookieToken); + httpCall.FlurlRequest = httpCall.FlurlRequest.EnableCookies().WithCookie("AuthSession", _cookieToken); break; default: throw new NotSupportedException($"Authentication of type {_settings.AuthenticationType} is not supported."); @@ -55,7 +56,7 @@ private async Task LoginAsync() _cookieCreationDate = DateTime.Now; - if (response.Headers.TryGetValues("Set-Cookie", out var values)) + if (response.Headers.TryGetValues("Set-Cookie", out IEnumerable values)) { var dirtyToken = values.First(); var regex = new Regex(@"^AuthSession=(.+); Version=1; .*Path=\/; HttpOnly$"); diff --git a/src/CouchDB.Driver/CouchDB.Driver.csproj b/src/CouchDB.Driver/CouchDB.Driver.csproj index 81ba7a8..d674d4d 100644 --- a/src/CouchDB.Driver/CouchDB.Driver.csproj +++ b/src/CouchDB.Driver/CouchDB.Driver.csproj @@ -9,14 +9,13 @@ https://github.com/matteobortolazzo/couchdb-net https://github.com/matteobortolazzo/couchdb-ne couchdb,driver,nosql,netstandard,pouchdb,xamarin - Complete rewrite. -IQueryable support. -All selectors support. + http://couchdb.apache.org/image/couch@2x.png Library en + 1.1.0 diff --git a/src/CouchDB.Driver/CouchQueryProvider.cs b/src/CouchDB.Driver/CouchQueryProvider.cs index 4c2c51a..910fa96 100644 --- a/src/CouchDB.Driver/CouchQueryProvider.cs +++ b/src/CouchDB.Driver/CouchQueryProvider.cs @@ -1,5 +1,5 @@ using CouchDB.Driver.DTOs; -using CouchDB.Driver.ExpressionVisitors; +using CouchDB.Driver.CompositeExpressionsEvaluator; using CouchDB.Driver.Helpers; using CouchDB.Driver.Settings; using CouchDB.Driver.Types; @@ -29,49 +29,51 @@ public CouchQueryProvider(IFlurlClient flurlClient, CouchSettings settings, stri public override string GetQueryText(Expression expression) { - return Translate(expression); + return Translate(ref expression); } - public override object Execute(Expression e, bool completeResponse) + public override object Execute(Expression expression, bool completeResponse) { - MethodInfo _filterMethodInfo = null; - Expression[] _filteringExpressions = Array.Empty(); - if (e is MethodCallExpression m) + // Remove from the expressions tree all IQueryable methods not supported by CouchDB and put them into the list + var unsupportedMethodCallExpressions = new List(); + expression = RemoveUnsupportedMethodExpressions(expression, out var hasUnsupportedMethods, unsupportedMethodCallExpressions); + + var body = Translate(ref expression); + Type elementType = TypeSystem.GetElementType(expression.Type); + + // Create generic GetCouchList method and invoke it, sending the request to CouchDB + MethodInfo method = typeof(CouchQueryProvider).GetMethod(nameof(CouchQueryProvider.GetCouchList)); + MethodInfo generic = method.MakeGenericMethod(elementType); + var result = generic.Invoke(this, new[] { body }); + + // If no unsupported methods, return the result + if (!hasUnsupportedMethods) { - if ( - m.Method.Name == "First" || - m.Method.Name == "FirstOrDefault" || - m.Method.Name == "Last" || - m.Method.Name == "LastOrDefault" || - m.Method.Name == "Single" || - m.Method.Name == "SingleOrDefault") - { - _filterMethodInfo = m.Method; - _filteringExpressions = m.Arguments.Skip(1).ToArray(); - e = m.Arguments[0]; - } + return result; } - var body = Translate(e); - Type elementType = TypeSystem.GetElementType(e.Type); - - MethodInfo method = typeof(CouchQueryProvider).GetMethod(nameof(CouchQueryProvider.GetCouchListOrFiltered)); - MethodInfo generic = method.MakeGenericMethod(elementType); - var result = generic.Invoke(this, new[] { body, (object)_filterMethodInfo, _filteringExpressions }); + // For every unsupported method expression, execute it on the result + foreach (MethodCallExpression inMemoryCall in unsupportedMethodCallExpressions) + { + result = InvokeUnsupportedMethodCallExpression(result, inMemoryCall); + } return result; } - private string Translate(Expression e) + private string Translate(ref Expression e) { - e = Evaluator.PartialEval(e); - var whereVisitor = new WhereExpressionVisitor(); + e = Local.PartialEval(e); + var whereVisitor = new BoolMemberToConstantEvaluator(); e = whereVisitor.Visit(e); + var pretranslator = new QueryPretranslator(); + e = pretranslator.Visit(e); + return new QueryTranslator(_settings).Translate(e); } - - public object GetCouchListOrFiltered(string body, MethodInfo filteringMethodInfo, Expression[] filteringExpressions) - { + + public object GetCouchList(string body) + { FindResult result = _flurlClient .Request(_connectionString) .AppendPathSegments(_db, "_find") @@ -80,49 +82,116 @@ public object GetCouchListOrFiltered(string body, MethodInfo filteringMethodI .SendRequest(); var couchList = new CouchList(result.Docs.ToList(), result.Bookmark, result.ExecutionStats); + return couchList; + } - if (filteringMethodInfo == null) + private Expression RemoveUnsupportedMethodExpressions(Expression expression, out bool hasUnsupportedMethods, IList unsupportedMethodCallExpressions) + { + if (unsupportedMethodCallExpressions == null) { - return couchList; + throw new ArgumentNullException(nameof(unsupportedMethodCallExpressions)); } - var filteringMethods = typeof(Enumerable).GetMethods() - .Where(m => - m.Name == filteringMethodInfo.Name && - m.GetParameters().Length - 1 == filteringExpressions.Length) - .OrderBy(m => m.GetParameters().Length).ToList(); - - - var invokeParameter = new object[filteringExpressions.Length + 1]; - invokeParameter[0] = couchList; - - bool IsRightOverload(MethodInfo m) + // Search for method calls to run in-memory, + // Once one is found all method calls after that must run in-memory. + // The expression to translate in JSON ends with the last not in-memory call. + bool IsUnsupportedMethodCallExpression(Expression ex) { - ParameterInfo[] parameters = m.GetParameters(); - for (var i = 0; i < filteringExpressions.Length; i++) + if (ex is MethodCallExpression m) { - var lamdaExpression = filteringExpressions[i] as UnaryExpression; - if (lamdaExpression == null) + Expression nextCall = m.Arguments[0]; + // Check if the next expression is unsupported + var isUnsupported = IsUnsupportedMethodCallExpression(nextCall); + if (isUnsupported) { - return false; + unsupportedMethodCallExpressions.Add(m); + return isUnsupported; } - - if (lamdaExpression.Operand.Type != parameters[i + 1].ParameterType) + // If the next call is supported and the current is in the composite list + if (QueryTranslator.CompositeQueryableMethods.Contains(m.Method.Name)) { - return false; + unsupportedMethodCallExpressions.Add(m); + return true; + } + // If the next call is supported and the current is not in the supported list + if (!QueryTranslator.NativeQueryableMethods.Contains(m.Method.Name)) + { + unsupportedMethodCallExpressions.Add(m); + expression = nextCall; + return true; } - invokeParameter[i + 1] = lamdaExpression.Operand; } - return true; + return false; } - MethodInfo rightOverload = filteringMethods.Single(IsRightOverload); + hasUnsupportedMethods = IsUnsupportedMethodCallExpression(expression); + return expression; + } - MethodInfo enumerableGenericFilteringMethod = rightOverload.MakeGenericMethod(typeof(T)); + private object InvokeUnsupportedMethodCallExpression(object result, MethodCallExpression methodCallExpression) + { + MethodInfo queryableMethodInfo = methodCallExpression.Method; + Expression[] queryableMethodArguments = methodCallExpression.Arguments.ToArray(); + // Since Max and Min are not map 1 to 1 from Queryable to Enumerable + // they need to be handled differently + MethodInfo FindEnumerableMethod() + { + if (queryableMethodInfo.Name == nameof(Queryable.Max) || queryableMethodInfo.Name == nameof(Queryable.Min)) + { + return FindEnumerableMinMax(queryableMethodInfo); + } + return typeof(Enumerable).GetMethods().Single(enumerableMethodInfo => + { + return + queryableMethodInfo.Name == enumerableMethodInfo.Name && + ReflectionComparator.IsCompatible(queryableMethodInfo, enumerableMethodInfo); + }); + } - var filtered = enumerableGenericFilteringMethod.Invoke(null, invokeParameter); + // Find the equivalent method in Enumerable + MethodInfo enumarableMethodInfo = FindEnumerableMethod(); + + // Add the list as first parameter of the call + var invokeParameter = new List { result }; + // Convert everty other parameter expression to real values + IEnumerable enumerableParameters = queryableMethodArguments.Skip(1).Select(GetArgumentValueFromExpression); + // Add the other parameter to the complete list + invokeParameter.AddRange(enumerableParameters); + + Type[] requestedGenericParameters = enumarableMethodInfo.GetGenericMethodDefinition().GetGenericArguments(); + Type[] genericParameters = queryableMethodInfo.GetGenericArguments(); + Type[] usableParameters = genericParameters.Take(requestedGenericParameters.Length).ToArray(); + MethodInfo enumarableGenericMethod = enumarableMethodInfo.MakeGenericMethod(usableParameters); + var filtered = enumarableGenericMethod.Invoke(null, invokeParameter.ToArray()); return filtered; } + + private object GetArgumentValueFromExpression(Expression e) + { + if (e is ConstantExpression c) + { + return c.Value; + } + if (e is UnaryExpression u && u.Operand is LambdaExpression l) + { + return l.Compile(); + } + throw new NotImplementedException($"Expression of type {e.NodeType} not supported."); + } + + private static MethodInfo FindEnumerableMinMax(MethodInfo queryableMethodInfo) + { + Type[] genericParams = queryableMethodInfo.GetGenericArguments(); + MethodInfo finalMethodInfo = typeof(Enumerable).GetMethods().Single(enumerableMethodInfo => + { + Type[] enumerableArguments = enumerableMethodInfo.GetGenericArguments(); + return + enumerableMethodInfo.Name == queryableMethodInfo.Name && + enumerableArguments.Length == genericParams.Length - 1 && + enumerableMethodInfo.ReturnType == genericParams[1]; + }); + return finalMethodInfo; + } } } diff --git a/src/CouchDB.Driver/ExpressionVisitors/WhereExpressionVisitor.cs b/src/CouchDB.Driver/ExpressionVisitors/BoolMemberToConstantEvaluator.cs similarity index 83% rename from src/CouchDB.Driver/ExpressionVisitors/WhereExpressionVisitor.cs rename to src/CouchDB.Driver/ExpressionVisitors/BoolMemberToConstantEvaluator.cs index 2d09c3c..6714356 100644 --- a/src/CouchDB.Driver/ExpressionVisitors/WhereExpressionVisitor.cs +++ b/src/CouchDB.Driver/ExpressionVisitors/BoolMemberToConstantEvaluator.cs @@ -2,15 +2,15 @@ using System.Linq.Expressions; using System.Reflection; -namespace CouchDB.Driver.ExpressionVisitors +namespace CouchDB.Driver.CompositeExpressionsEvaluator { - internal class WhereExpressionVisitor : ExpressionVisitor + internal class BoolMemberToConstantEvaluator : ExpressionVisitor { private bool _visitingWhereMethod; protected override Expression VisitMethodCall(MethodCallExpression m) { - _visitingWhereMethod = m.Method.Name == "Where" && m.Method.DeclaringType == typeof(Queryable); + _visitingWhereMethod = m.Method.Name == nameof(Queryable.Where) && m.Method.DeclaringType == typeof(Queryable); if (_visitingWhereMethod) { Expression result = base.VisitMethodCall(m); @@ -22,7 +22,7 @@ protected override Expression VisitMethodCall(MethodCallExpression m) protected override Expression VisitBinary(BinaryExpression expression) { - if (expression.Right is ConstantExpression c && c.Type == typeof(bool) && + if (_visitingWhereMethod && expression.Right is ConstantExpression c && c.Type == typeof(bool) && (expression.NodeType == ExpressionType.Equal || expression.NodeType == ExpressionType.NotEqual)) { return expression; diff --git a/src/CouchDB.Driver/ExpressionVisitors/CompositeExpressionsEvaluator.cs b/src/CouchDB.Driver/ExpressionVisitors/CompositeExpressionsEvaluator.cs new file mode 100644 index 0000000..73db91b --- /dev/null +++ b/src/CouchDB.Driver/ExpressionVisitors/CompositeExpressionsEvaluator.cs @@ -0,0 +1,50 @@ +using System; +using System.Linq; +using System.Linq.Expressions; + +namespace CouchDB.Driver.CompositeExpressionsEvaluator +{ + public class QueryPretranslator : ExpressionVisitor + { + protected override Expression VisitMethodCall(MethodCallExpression node) + { + Type[] genericArgs = node.Method.GetGenericArguments(); + var numberOfParameters = node.Method.GetParameters().Length; + + // Return an expression representing Queryable.Take(1) + MethodCallExpression GetTakeOneExpression(Expression previousExpression) + { + return Expression.Call(typeof(Queryable), nameof(Queryable.Take), genericArgs.Take(1).ToArray(), previousExpression, Expression.Constant(1)); + } + + // Min(e => e.P) == OrderBy(e => e.P).Take(1) + Min + if (node.Method.Name == nameof(Queryable.Min) && numberOfParameters == 2) + { + MethodCallExpression orderByDesc = Expression.Call(typeof(Queryable), nameof(Queryable.OrderBy), genericArgs, node.Arguments[0], node.Arguments[1]); + return GetTakeOneExpression(orderByDesc); + } + // Max(e => e.P) == OrderByDescending(e => e.P).Take(1) + Max + if (node.Method.Name == nameof(Queryable.Max) && numberOfParameters == 2) + { + MethodCallExpression orderBy = Expression.Call(typeof(Queryable), nameof(Queryable.OrderByDescending), genericArgs, node.Arguments[0], node.Arguments[1]); + return GetTakeOneExpression(orderBy); + } + // Single and SingleOrDefault have the same behaviour + if (node.Method.Name == nameof(Queryable.First) || node.Method.Name == nameof(Queryable.FirstOrDefault)) + { + // First() == Take(1) + First + if (numberOfParameters == 1) + { + return GetTakeOneExpression(node.Arguments[0]); + } + // First(e => e.P) == Where(e => e.P).Take(1) + First + else if (numberOfParameters == 2) + { + MethodCallExpression whereExpression = Expression.Call(typeof(Queryable), nameof(Queryable.Where), genericArgs, node.Arguments[0], node.Arguments[1]); + return GetTakeOneExpression(whereExpression); + } + } + return base.VisitMethodCall(node); + } + } +} diff --git a/src/CouchDB.Driver/Helpers/Evaluator.cs b/src/CouchDB.Driver/ExpressionVisitors/LocalExpressionEvaluator.cs similarity index 91% rename from src/CouchDB.Driver/Helpers/Evaluator.cs rename to src/CouchDB.Driver/ExpressionVisitors/LocalExpressionEvaluator.cs index 34c15f4..196e5ea 100644 --- a/src/CouchDB.Driver/Helpers/Evaluator.cs +++ b/src/CouchDB.Driver/ExpressionVisitors/LocalExpressionEvaluator.cs @@ -4,9 +4,10 @@ using System.Linq; using System.Linq.Expressions; -namespace CouchDB.Driver.Helpers +#pragma warning disable IDE0058 // Expression value is never used +namespace CouchDB.Driver.CompositeExpressionsEvaluator { - internal static class Evaluator + internal static class Local { /// /// Performs evaluation & replacement of independent sub-trees @@ -26,17 +27,15 @@ public static Expression PartialEval(Expression expression, FuncA new tree with sub-trees evaluated and replaced. public static Expression PartialEval(Expression expression) { - return PartialEval(expression, Evaluator.CanBeEvaluatedLocally); + return PartialEval(expression, Local.CanBeEvaluatedLocally); } private static bool CanBeEvaluatedLocally(Expression expression) { if (expression is MethodCallExpression c) { - return - c.Method.Name != nameof(Queryable.Where) && - c.Method.Name != nameof(Queryable.Skip) && - c.Method.Name != nameof(Queryable.Take) && + return !QueryTranslator.CompositeQueryableMethods.Contains(c.Method.Name) && + !QueryTranslator.NativeQueryableMethods.Contains(c.Method.Name) && c.Method.Name != nameof(QueryableExtensions.WithReadQuorum) && c.Method.Name != nameof(QueryableExtensions.WithoutIndexUpdate) && c.Method.Name != nameof(QueryableExtensions.UseBookmark) && @@ -134,3 +133,4 @@ public override Expression Visit(Expression expression) } } } +#pragma warning restore IDE0058 // Expression value is never used diff --git a/src/CouchDB.Driver/Helpers/MicrosecondEpochConverter.cs b/src/CouchDB.Driver/Helpers/MicrosecondEpochConverter.cs index 4d53824..6950c38 100644 --- a/src/CouchDB.Driver/Helpers/MicrosecondEpochConverter.cs +++ b/src/CouchDB.Driver/Helpers/MicrosecondEpochConverter.cs @@ -2,11 +2,10 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; +#pragma warning disable CA1812 // Avoid uninstantiated internal classes namespace CouchDB.Driver.Helpers { -#pragma warning disable CA1812 // Avoid uninstantiated internal classes internal class MicrosecondEpochConverter : DateTimeConverterBase -#pragma warning restore CA1812 // Avoid uninstantiated internal classes { private static readonly DateTime _epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); @@ -17,8 +16,10 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { - if (reader.Value == null) { return null; } - return _epoch.AddMilliseconds((long)reader.Value / 1000d); + return reader.Value != null ? + (object)_epoch.AddMilliseconds((long)reader.Value / 1000d) : + null; } } -} \ No newline at end of file +} +#pragma warning restore CA1812 // Avoid uninstantiated internal classes \ No newline at end of file diff --git a/src/CouchDB.Driver/Helpers/ReflectionComparator.cs b/src/CouchDB.Driver/Helpers/ReflectionComparator.cs new file mode 100644 index 0000000..b5b4b6b --- /dev/null +++ b/src/CouchDB.Driver/Helpers/ReflectionComparator.cs @@ -0,0 +1,102 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; + +namespace CouchDB.Driver.Helpers +{ + internal static class ReflectionComparator + { + public static bool IsCompatible(MethodInfo sourceMethodInfo, MethodInfo targetMethodInfo) + { + // If return types are not compatible + if (!IsCompatible(sourceMethodInfo.ReturnType, targetMethodInfo.ReturnType)) + { + return false; + } + + ParameterInfo[] sourceParametersInfo = sourceMethodInfo.GetParameters(); + ParameterInfo[] targetParametersInfo = targetMethodInfo.GetParameters(); + + // If different number of parameters + if (sourceParametersInfo.Length != targetParametersInfo.Length) + { + return false; + } + + // Checks if all parameters are compatibles + for (var i = 0; i < sourceParametersInfo.Length; i++) + { + ParameterInfo sourceParameterInfo = sourceParametersInfo[i]; + ParameterInfo targetParameterInfo = targetParametersInfo[i]; + if (!IsCompatible(sourceParameterInfo, targetParameterInfo)) + { + return false; + } + } + + return true; + } + + public static bool IsCompatible(ParameterInfo sourceParameterInfo, ParameterInfo targetParameterInfo) + { + // If parameters types are LambdaExpression, take the generic argument value + Type sourceParameterType = sourceParameterInfo.ParameterType; + if (sourceParameterType.BaseType == typeof(LambdaExpression)) + { + sourceParameterType = sourceParameterType.GenericTypeArguments.First(); + } + Type targetParameterType = targetParameterInfo.ParameterType; + if (targetParameterType.BaseType == typeof(LambdaExpression)) + { + targetParameterType = targetParameterType.GenericTypeArguments.First(); + } + + // If parameters types are not compatible + if (!IsCompatible(sourceParameterType, targetParameterType)) + { + return false; + } + return true; + } + + public static bool IsCompatible(Type sourceType, Type targetType) + { + // If the target type is a generic parameter, no other check needed + if (targetType.IsGenericParameter) + { + return true; + } + + // If the target type is generic + if (targetType.IsGenericType) + { + Type[] sourceGenericParameters = sourceType.GetGenericArguments(); + Type[] targetGenericParameters = targetType.GetGenericArguments(); + + // If different number of generic parameters + if (sourceGenericParameters.Length != targetGenericParameters.Length) + { + return false; + } + + // Checks if all generic parameters are compatibles + for (var i = 0; i < sourceGenericParameters.Length; i++) + { + Type sourceGenericParameter = sourceGenericParameters[i]; + Type targetGenericParameter = targetGenericParameters[i]; + if (!IsCompatible(sourceGenericParameter, targetGenericParameter)) + { + return false; + } + } + + // If all generic parameters are compatible + return true; + } + + // If the target is not generic, just check if types are different + return sourceType == targetType; + } + } +} diff --git a/src/CouchDB.Driver/QueryTranslator.cs b/src/CouchDB.Driver/QueryTranslator.cs index d42baf0..3c197ca 100644 --- a/src/CouchDB.Driver/QueryTranslator.cs +++ b/src/CouchDB.Driver/QueryTranslator.cs @@ -4,6 +4,7 @@ using System.Reflection; using System.Text; +#pragma warning disable IDE0058 // Expression value is never used namespace CouchDB.Driver { internal partial class QueryTranslator : ExpressionVisitor @@ -50,3 +51,4 @@ protected override Expression VisitLambda(Expression l) } } } +#pragma warning restore IDE0058 // Expression value is never used diff --git a/src/CouchDB.Driver/Security/CouchSecurity.cs b/src/CouchDB.Driver/Security/CouchSecurity.cs index 69d45eb..7394a2f 100644 --- a/src/CouchDB.Driver/Security/CouchSecurity.cs +++ b/src/CouchDB.Driver/Security/CouchSecurity.cs @@ -1,4 +1,6 @@ -using CouchDB.Driver.Helpers; +using CouchDB.Driver.DTOs; +using CouchDB.Driver.Exceptions; +using CouchDB.Driver.Helpers; using Flurl.Http; using System; using System.Threading.Tasks; @@ -39,11 +41,17 @@ public async Task SetInfoAsync(CouchSecurityInfo info) throw new ArgumentNullException(nameof(info)); } - await _newRequest() + OperationResult result = await _newRequest() .AppendPathSegment("_security") .PutJsonAsync(info) + .ReceiveJson() .SendRequestAsync() .ConfigureAwait(false); + + if (!result.Ok) + { + throw new CouchDeleteException(); + } } } } diff --git a/src/CouchDB.Driver/Settings/CaseType.cs b/src/CouchDB.Driver/Settings/CaseType.cs index 35ae1bf..5653635 100644 --- a/src/CouchDB.Driver/Settings/CaseType.cs +++ b/src/CouchDB.Driver/Settings/CaseType.cs @@ -23,11 +23,7 @@ internal virtual string Convert(string str) } public override bool Equals(object obj) { - if (!(obj is CaseType item)) - { - return false; - } - return Value == item.Value; + return obj is CaseType item && Value == item.Value; } public override int GetHashCode() { diff --git a/src/CouchDB.Driver/Translators/BinaryExpressionTranslator.cs b/src/CouchDB.Driver/Translators/BinaryExpressionTranslator.cs index efdfdfa..370dcbd 100644 --- a/src/CouchDB.Driver/Translators/BinaryExpressionTranslator.cs +++ b/src/CouchDB.Driver/Translators/BinaryExpressionTranslator.cs @@ -2,6 +2,7 @@ using System.Linq.Expressions; using System.Reflection; +#pragma warning disable IDE0058 // Expression value is never used namespace CouchDB.Driver { internal partial class QueryTranslator @@ -139,3 +140,4 @@ private void VisitBinaryConditionOperator(BinaryExpression b) } } } +#pragma warning restore IDE0058 // Expression value is never used \ No newline at end of file diff --git a/src/CouchDB.Driver/Translators/ConstantExpressionTranslator.cs b/src/CouchDB.Driver/Translators/ConstantExpressionTranslator.cs index b528523..55fe0c1 100644 --- a/src/CouchDB.Driver/Translators/ConstantExpressionTranslator.cs +++ b/src/CouchDB.Driver/Translators/ConstantExpressionTranslator.cs @@ -1,11 +1,11 @@ using Newtonsoft.Json; using System; using System.Collections; -using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Linq.Expressions; +#pragma warning disable IDE0058 // Expression value is never used namespace CouchDB.Driver { internal partial class QueryTranslator @@ -67,7 +67,7 @@ private void HandleConstant(object constant) private void VisitIEnumerable(IEnumerable list) { _sb.Append("["); - bool needsComma = false; + var needsComma = false; foreach (var item in list) { if (needsComma) @@ -81,3 +81,4 @@ private void VisitIEnumerable(IEnumerable list) } } } +#pragma warning restore IDE0058 // Expression value is never used \ No newline at end of file diff --git a/src/CouchDB.Driver/Translators/MemberExpressionTranslator.cs b/src/CouchDB.Driver/Translators/MemberExpressionTranslator.cs index b692988..64e873f 100644 --- a/src/CouchDB.Driver/Translators/MemberExpressionTranslator.cs +++ b/src/CouchDB.Driver/Translators/MemberExpressionTranslator.cs @@ -1,27 +1,32 @@ -using CouchDB.Driver.Extensions; -using CouchDB.Driver.Settings; -using CouchDB.Driver.Types; -using Humanizer; -using Newtonsoft.Json; +using Newtonsoft.Json; using System.Collections.Generic; using System.Linq.Expressions; using System.Reflection; +#pragma warning disable IDE0058 // Expression value is never used namespace CouchDB.Driver { internal partial class QueryTranslator { protected override Expression VisitMember(MemberExpression m) { - PropertyCaseType caseType = _settings.PropertiesCase; + string GetPropertyName(MemberInfo memberInfo) + { + var jsonPropertyAttributes = memberInfo.GetCustomAttributes(typeof(JsonPropertyAttribute), true); + JsonPropertyAttribute jsonProperty = jsonPropertyAttributes.Length > 0 ? jsonPropertyAttributes[0] as JsonPropertyAttribute : null; + + return jsonProperty != null ? + jsonProperty.PropertyName : + _settings.PropertiesCase.Convert(memberInfo.Name); + } - var members = new List { m.Member.GetCouchPropertyName(caseType) }; + var members = new List { GetPropertyName(m.Member) }; - var currentExpression = m.Expression; + Expression currentExpression = m.Expression; while (currentExpression is MemberExpression cm) { - members.Add(cm.Member.GetCouchPropertyName(caseType)); + members.Add(GetPropertyName(cm.Member)); currentExpression = cm.Expression; } @@ -33,3 +38,4 @@ protected override Expression VisitMember(MemberExpression m) } } } +#pragma warning restore IDE0058 // Expression value is never used \ No newline at end of file diff --git a/src/CouchDB.Driver/Translators/MethodCallExpressionTranslator.cs b/src/CouchDB.Driver/Translators/MethodCallExpressionTranslator.cs index 5d9d0ae..c9ac325 100644 --- a/src/CouchDB.Driver/Translators/MethodCallExpressionTranslator.cs +++ b/src/CouchDB.Driver/Translators/MethodCallExpressionTranslator.cs @@ -10,6 +10,26 @@ namespace CouchDB.Driver { internal partial class QueryTranslator { + internal static List NativeQueryableMethods { get; } = new List + { + nameof(Queryable.Where), + nameof(Queryable.OrderBy), + nameof(Queryable.ThenBy), + nameof(Queryable.OrderByDescending), + nameof(Queryable.ThenByDescending), + nameof(Queryable.Skip), + nameof(Queryable.Take), + nameof(Queryable.Select) + }; + + internal static List CompositeQueryableMethods { get; } = new List + { + nameof(Queryable.Max), + nameof(Queryable.Min), + nameof(Queryable.First), + nameof(Queryable.FirstOrDefault) + }; + private static Expression StripQuotes(Expression e) { while (e.NodeType == ExpressionType.Quote) @@ -237,17 +257,20 @@ private Expression VisitSelectMethod(MethodCallExpression m) _sb.Append("\"fields\":["); var lambda = (LambdaExpression)StripQuotes(m.Arguments[1]); - if (!(lambda.Body is NewExpression n)) + if (lambda.Body is NewExpression n) { - throw new NotSupportedException($"The expression of type {lambda.Body.GetType()} is not supported in the Select method."); + foreach (Expression a in n.Arguments) + { + Visit(a); + _sb.Append(","); + } + _sb.Length--; } - - foreach (Expression a in n.Arguments) + else { - Visit(a); - _sb.Append(","); + throw new NotSupportedException($"The expression of type {lambda.Body.GetType()} is not supported in the Select method."); } - _sb.Length--; + _sb.Append("],"); return m; diff --git a/src/CouchDB.Driver/Translators/UnaryExpressionTranslator.cs b/src/CouchDB.Driver/Translators/UnaryExpressionTranslator.cs index 9ba23ca..1440642 100644 --- a/src/CouchDB.Driver/Translators/UnaryExpressionTranslator.cs +++ b/src/CouchDB.Driver/Translators/UnaryExpressionTranslator.cs @@ -1,7 +1,7 @@ -using CouchDB.Driver.Extensions; -using System; +using System; using System.Linq.Expressions; +#pragma warning disable IDE0058 // Expression value is never used namespace CouchDB.Driver { internal partial class QueryTranslator @@ -40,3 +40,4 @@ protected override Expression VisitUnary(UnaryExpression u) } } +#pragma warning restore IDE0058 // Expression value is never used diff --git a/tests/CouchDB.Driver.UnitTests/Database_Tests.cs b/tests/CouchDB.Driver.UnitTests/Database_Tests.cs index b75ae03..30c15bb 100644 --- a/tests/CouchDB.Driver.UnitTests/Database_Tests.cs +++ b/tests/CouchDB.Driver.UnitTests/Database_Tests.cs @@ -277,6 +277,8 @@ public async Task SecurityInfo_Put() { using (var httpTest = new HttpTest()) { + httpTest.RespondWithJson(new { ok = true }); + var securityInfo = new CouchSecurityInfo(); securityInfo.Admins.Names.Add("user1"); diff --git a/tests/CouchDB.Driver.UnitTests/SupportByCombination_Tests.cs b/tests/CouchDB.Driver.UnitTests/SupportByCombination_Tests.cs new file mode 100644 index 0000000..f01156b --- /dev/null +++ b/tests/CouchDB.Driver.UnitTests/SupportByCombination_Tests.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using CouchDB.Driver.UnitTests.Models; +using Flurl.Http.Testing; +using Xunit; + +namespace CouchDB.Driver.UnitTests +{ + public class SupportByCombination_Tests + { + private readonly CouchDatabase _rebels; + private readonly Rebel _mainRebel; + private readonly List _rebelsList; + private object _response; + + public SupportByCombination_Tests() + { + var client = new CouchClient("http://localhost"); + _rebels = client.GetDatabase(); + _mainRebel = new Rebel + { + Id = Guid.NewGuid().ToString(), + Name = "Luke", + Age = 19, + Skills = new List { "Force" } + }; + _rebelsList = new List + { + _mainRebel + }; + _response = new + { + Docs = _rebelsList + }; + } + + [Fact] + public async Task Max() + { + using (var httpTest = new HttpTest()) + { + httpTest.RespondWithJson(_response); + var result = _rebels.AsQueryable().Max(r => r.Age); + Assert.Equal(_mainRebel.Age, result); + } + } + + [Fact] + public async Task Min() + { + using (var httpTest = new HttpTest()) + { + httpTest.RespondWithJson(_response); + var result = _rebels.AsQueryable().Min(r => r.Age); + Assert.Equal(_mainRebel.Age, result); + } + } + + [Fact] + public async Task First() + { + using (var httpTest = new HttpTest()) + { + httpTest.RespondWithJson(_response); + var result = _rebels.AsQueryable().First(); + Assert.Equal(_mainRebel.Age, result.Age); + } + } + + [Fact] + public async Task First_Expr() + { + using (var httpTest = new HttpTest()) + { + httpTest.RespondWithJson(_response); + var result = _rebels.AsQueryable().First(r => r.Age == 19); + Assert.Equal(_mainRebel.Age, result.Age); + } + } + + [Fact] + public async Task FirstOrDefault() + { + using (var httpTest = new HttpTest()) + { + httpTest.RespondWithJson(new { Docs = Array.Empty() }); + var result = _rebels.AsQueryable().FirstOrDefault(); + Assert.Null(result); + } + } + + [Fact] + public async Task FirstOrDefault_Expr() + { + using (var httpTest = new HttpTest()) + { + httpTest.RespondWithJson(new { Docs = Array.Empty() }); + var result = _rebels.AsQueryable().FirstOrDefault(r => r.Age == 20); + Assert.Null(result); + } + } + } +} diff --git a/tests/CouchDB.Driver.UnitTests/UnsupportedMethods_Tests.cs b/tests/CouchDB.Driver.UnitTests/UnsupportedMethods_Tests.cs new file mode 100644 index 0000000..ca65da2 --- /dev/null +++ b/tests/CouchDB.Driver.UnitTests/UnsupportedMethods_Tests.cs @@ -0,0 +1,211 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using CouchDB.Driver.UnitTests.Models; +using Flurl.Http.Testing; +using Xunit; + +namespace CouchDB.Driver.UnitTests +{ + public class UnsupportedMethods_Tests + { + private readonly CouchDatabase _rebels; + private readonly Rebel _mainRebel; + private readonly List _rebelsList; + private object _response; + + public UnsupportedMethods_Tests() + { + var client = new CouchClient("http://localhost"); + _rebels = client.GetDatabase(); + _mainRebel = new Rebel + { + Id = Guid.NewGuid().ToString(), + Name = "Luke", + Age = 19, + Skills = new List { "Force" } + }; + _rebelsList = new List + { + _mainRebel + }; + _response = new + { + Docs = _rebelsList + }; + } + + [Fact] + public async Task All() + { + using (var httpTest = new HttpTest()) + { + httpTest.RespondWithJson(_response); + var result = _rebels.AsQueryable().All(r => r.Name == _mainRebel.Name); + Assert.True(result); + } + } + [Fact] + public async Task Any() + { + using (var httpTest = new HttpTest()) + { + httpTest.RespondWithJson(_response); + var result = _rebels.AsQueryable().Any(); + Assert.True(result); + } + } + [Fact] + public async Task Any_Selector() + { + using (var httpTest = new HttpTest()) + { + httpTest.RespondWithJson(_response); + var result = _rebels.AsQueryable().Any(r => r.Name == _mainRebel.Name); + Assert.True(result); + } + } + + [Fact] + public async Task Avg_Expr() + { + using (var httpTest = new HttpTest()) + { + httpTest.RespondWithJson(_response); + var result = _rebels.AsQueryable().Average(r => r.Age); + Assert.Equal(19, result); + } + } + + [Fact] + public async Task Count() + { + using (var httpTest = new HttpTest()) + { + httpTest.RespondWithJson(_response); + var result = _rebels.AsQueryable().Count(); + Assert.Equal(1, result); + } + } + + [Fact] + public async Task CountExpr() + { + using (var httpTest = new HttpTest()) + { + httpTest.RespondWithJson(_response); + var result = _rebels.AsQueryable().Count(r => r.Age == 19); + Assert.Equal(1, result); + } + } + + [Fact] + public async Task DefaultIfEmpty() + { + using (var httpTest = new HttpTest()) + { + httpTest.RespondWithJson(_response); + var result = _rebels.Where(c => c.Name == "Luce").DefaultIfEmpty(_mainRebel); + Assert.Equal(_mainRebel, result.First()); + } + } + + [Fact] + public async Task ElementAt() + { + using (var httpTest = new HttpTest()) + { + httpTest.RespondWithJson(_response); + var result = _rebels.AsQueryable().ElementAt(0); + Assert.Equal(_mainRebel, result); + } + } + [Fact] + public async Task ElementAtOrDefault() + { + using (var httpTest = new HttpTest()) + { + httpTest.RespondWithJson(_response); + var result = _rebels.AsQueryable().ElementAtOrDefault(2); + Assert.Null(result); + } + } + + [Fact] + public async Task GroupBy() + { + using (var httpTest = new HttpTest()) + { + httpTest.RespondWithJson(_response); + var result = _rebels.AsQueryable().GroupBy(c => c.Id); + Assert.Equal(1, result.Count()); + } + } + + [Fact] + public async Task Last() + { + using (var httpTest = new HttpTest()) + { + httpTest.RespondWithJson(_response); + var result = _rebels.AsQueryable().Last(); + Assert.Equal(_mainRebel, result); + } + } + [Fact] + public async Task Last_Expr() + { + using (var httpTest = new HttpTest()) + { + httpTest.RespondWithJson(_response); + var result = _rebels.AsQueryable().Last(c => c.Name == _mainRebel.Name); + Assert.Equal(_mainRebel, result); + } + } + + [Fact] + public async Task LongCount() + { + using (var httpTest = new HttpTest()) + { + httpTest.RespondWithJson(_response); + var result = _rebels.AsQueryable().LongCount(r => r.Age == 19); + Assert.Equal(1, result); + } + } + + [Fact] + public async Task Reverse() + { + using (var httpTest = new HttpTest()) + { + httpTest.RespondWithJson(_response); + var result = _rebels.AsQueryable().Reverse(); + Assert.Equal(_mainRebel, result.First()); + } + } + + [Fact] + public async Task SelectMany() + { + using (var httpTest = new HttpTest()) + { + httpTest.RespondWithJson(_response); + var result = _rebels.AsQueryable().SelectMany(r => r.Skills).ToList(); + Assert.Single(result); + } + } + + [Fact] + public async Task Sum() + { + using (var httpTest = new HttpTest()) + { + httpTest.RespondWithJson(_response); + var result = _rebels.AsQueryable().Sum(r => r.Age); + Assert.Equal(19, result); + } + } + } +} diff --git a/tests/CouchDB.Driver.UnitTests/_Models/Rebel.cs b/tests/CouchDB.Driver.UnitTests/_Models/Rebel.cs index 833fb3d..7c7160c 100644 --- a/tests/CouchDB.Driver.UnitTests/_Models/Rebel.cs +++ b/tests/CouchDB.Driver.UnitTests/_Models/Rebel.cs @@ -15,5 +15,14 @@ public class Rebel : CouchDocument public Guid Guid { get; set; } public List Skills { get; set; } public List Battles { get; set; } + + public override bool Equals(object obj) + { + if (obj is Rebel r) + { + return r.Id == Id; + } + return base.Equals(obj); + } } }