From 7914c76a233c70162351621264602662e6e7df02 Mon Sep 17 00:00:00 2001 From: JABE Date: Sat, 6 Jun 2020 11:18:39 +0800 Subject: [PATCH 1/2] Issue #403: Extensible support for data provider functions such as, but not limited to, UPPER() and LOWER(). --- .../DataProviderFunctionExtractorTest.cs | 111 +++++++++++++ ...oupParseExpressionForToUpperToLowerTest.cs | 114 +++++++++++++ .../RepoDb.UnitTests/RepoDb.UnitTests.csproj | 2 + .../DataProviderFunction.cs | 37 +++++ .../BaseDataProviderFunctionBuilder.cs | 150 ++++++++++++++++++ .../IDataProviderFunctionBuilder.cs | 19 +++ .../SqlServerFunctionBuilder.cs | 49 ++++++ .../DataProviderFunctionExtractor.cs | 82 ++++++++++ .../RepoDb/Extensions/QueryFieldExtension.cs | 11 +- RepoDb/RepoDb/Field.cs | 7 + RepoDb/RepoDb/QueryField.cs | 21 +++ RepoDb/RepoDb/QueryGroup.cs | 83 +++++++++- RepoDb/RepoDb/RepoDb.csproj | 6 +- 13 files changed, 685 insertions(+), 7 deletions(-) create mode 100644 RepoDb/RepoDb.Tests/RepoDb.UnitTests/DataProviderFunctions/DataProviderFunctionExtractorTest.cs create mode 100644 RepoDb/RepoDb.Tests/RepoDb.UnitTests/QueryGroups/QueryGroupParseExpressionForToUpperToLowerTest.cs create mode 100644 RepoDb/RepoDb/DataProviderFunctions/DataProviderFunction.cs create mode 100644 RepoDb/RepoDb/DataProviderFunctions/DataProviderFunctionBuilders/BaseDataProviderFunctionBuilder.cs create mode 100644 RepoDb/RepoDb/DataProviderFunctions/DataProviderFunctionBuilders/IDataProviderFunctionBuilder.cs create mode 100644 RepoDb/RepoDb/DataProviderFunctions/DataProviderFunctionBuilders/SqlServerFunctionBuilder.cs create mode 100644 RepoDb/RepoDb/DataProviderFunctions/DataProviderFunctionExtractor.cs diff --git a/RepoDb/RepoDb.Tests/RepoDb.UnitTests/DataProviderFunctions/DataProviderFunctionExtractorTest.cs b/RepoDb/RepoDb.Tests/RepoDb.UnitTests/DataProviderFunctions/DataProviderFunctionExtractorTest.cs new file mode 100644 index 000000000..62277738f --- /dev/null +++ b/RepoDb/RepoDb.Tests/RepoDb.UnitTests/DataProviderFunctions/DataProviderFunctionExtractorTest.cs @@ -0,0 +1,111 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using RepoDb.DataProviderFunctions; +using RepoDb.DataProviderFunctions.DataProviderFunctionBuilders; +using System; +using System.Linq.Expressions; +using RepoDb.Extensions; + +namespace RepoDb.UnitTests.DataProviderFunctions { + [TestClass] + public class DataProviderFunctionExtratorTest { + [TestMethod] + public void TestSingleCall() { + // arrange + var mockDataProviderFunctionBuilder = new Mock(); + mockDataProviderFunctionBuilder.Setup(b => b.IsDataProviderFunction("ToUpper")).Returns(true); + + Expression> inputExpr = s => "".ToUpper(); + + // act + var dataProviderFunctionExtractor = new DataProviderFunctionExtractor(mockDataProviderFunctionBuilder.Object); + dataProviderFunctionExtractor.Extract((MethodCallExpression)inputExpr.Body); + + // assert + Assert.AreEqual(1, dataProviderFunctionExtractor.ExtractedDataProviderFunctions.Count); + } + + [TestMethod] + public void TestChainedCallAllProviderFunctions() { + // arrange + var mockDataProviderFunctionBuilder = new Mock(); + mockDataProviderFunctionBuilder.Setup(b => b.IsDataProviderFunction("ToUpper")).Returns(true); + mockDataProviderFunctionBuilder.Setup(b => b.IsDataProviderFunction("ToLower")).Returns(true); + + Expression> inputExpr = s => "".ToUpper().ToLower(); + + // act + var dataProviderFunctionExtractor = new DataProviderFunctionExtractor(mockDataProviderFunctionBuilder.Object); + dataProviderFunctionExtractor.Extract((MethodCallExpression)inputExpr.Body); + + // assert + Assert.AreEqual(2, dataProviderFunctionExtractor.ExtractedDataProviderFunctions.Count); + } + + + [TestMethod] + public void TestChainedCallNotAllProviderFunctions() { + // arrange + var mockDataProviderFunctionBuilder = new Mock(); + mockDataProviderFunctionBuilder.Setup(b => b.IsDataProviderFunction("ToUpper")).Returns(true); + mockDataProviderFunctionBuilder.Setup(b => b.IsDataProviderFunction("ToLower")).Returns(true); + + Expression> inputExpr = s => "".ToUpper().Substring(0).ToLower(); + // act + var dataProviderFunctionExtractor = new DataProviderFunctionExtractor(mockDataProviderFunctionBuilder.Object); + dataProviderFunctionExtractor.Extract((MethodCallExpression)inputExpr.Body); + + // assert + Assert.AreEqual(2, dataProviderFunctionExtractor.ExtractedDataProviderFunctions.Count); + } + + + [TestMethod] + public void TestMemberExpression() { + // arrange + var mockDataProviderFunctionBuilder = new Mock(); + mockDataProviderFunctionBuilder.Setup(b => b.IsDataProviderFunction("ToUpper")).Returns(true); + mockDataProviderFunctionBuilder.Setup(b => b.IsDataProviderFunction("ToLower")).Returns(true); + + var testClass = new DataProviderFunctionExtractorTestClass(); + Expression> inputExpr = s => testClass.FirstName.ToUpper().Substring(0).ToLower(); + + // act + var dataProviderFunctionExtractor = new DataProviderFunctionExtractor(mockDataProviderFunctionBuilder.Object); + dataProviderFunctionExtractor.Extract((MethodCallExpression)inputExpr.Body); + + // assert + Assert.AreEqual(2, dataProviderFunctionExtractor.ExtractedDataProviderFunctions.Count); + Assert.IsNotNull(dataProviderFunctionExtractor.MemberExpression); + Assert.AreEqual("FirstName", dataProviderFunctionExtractor.MemberExpression.ToMember().Member.Name) ; + } + + [TestMethod] + public void TestMemberExpressionEmbeddedType() { + // arrange + var mockDataProviderFunctionBuilder = new Mock(); + mockDataProviderFunctionBuilder.Setup(b => b.IsDataProviderFunction("ToUpper")).Returns(true); + mockDataProviderFunctionBuilder.Setup(b => b.IsDataProviderFunction("ToLower")).Returns(true); + + var testClass = new DataProviderFunctionExtractorEmbeddedTestClass(); + Expression> inputExpr = s => testClass.EmbeddedStringType.FirstName.ToUpper().Substring(0).ToLower(); + + // act + var dataProviderFunctionExtractor = new DataProviderFunctionExtractor(mockDataProviderFunctionBuilder.Object); + dataProviderFunctionExtractor.Extract((MethodCallExpression)inputExpr.Body); + + // assert + Assert.AreEqual(2, dataProviderFunctionExtractor.ExtractedDataProviderFunctions.Count); + Assert.IsNotNull(dataProviderFunctionExtractor.MemberExpression); + Assert.AreEqual("FirstName", dataProviderFunctionExtractor.MemberExpression.ToMember().Member.Name) ; + } + } + + internal class DataProviderFunctionExtractorTestClass { + public string FirstName { get; set; } + } + + internal class DataProviderFunctionExtractorEmbeddedTestClass { + public DataProviderFunctionExtractorTestClass EmbeddedStringType { get; set; } + } +} diff --git a/RepoDb/RepoDb.Tests/RepoDb.UnitTests/QueryGroups/QueryGroupParseExpressionForToUpperToLowerTest.cs b/RepoDb/RepoDb.Tests/RepoDb.UnitTests/QueryGroups/QueryGroupParseExpressionForToUpperToLowerTest.cs new file mode 100644 index 000000000..9201da2fc --- /dev/null +++ b/RepoDb/RepoDb.Tests/RepoDb.UnitTests/QueryGroups/QueryGroupParseExpressionForToUpperToLowerTest.cs @@ -0,0 +1,114 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace RepoDb.UnitTests { + public partial class QueryGroupTest + { + [TestMethod] + public void TestQueryGroupParseExpressionStringContainsWithToUpperFromClassProperty() + { + // Setup + var @class = new QueryGroupTestExpressionClass + { + PropertyString = "A" + }; + var parsed = QueryGroup.Parse(c => c.PropertyString.ToUpper().Contains("A")); + + // Act + var actual = parsed.GetString(m_dbSetting); + var expected = "(UPPER([PropertyString]) LIKE @PropertyString)"; + + // Assert + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void TestQueryGroupParseExpressionStringContainsWithToLowerFromClassProperty() + { + // Setup + var @class = new QueryGroupTestExpressionClass + { + PropertyString = "A" + }; + var parsed = QueryGroup.Parse(c => c.PropertyString.ToLower().Contains("A")); + + // Act + var actual = parsed.GetString(m_dbSetting); + var expected = "(LOWER([PropertyString]) LIKE @PropertyString)"; + + // Assert + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void TestQueryGroupParseExpressionStringContainsWithToLowerToUpperFromClassProperty() + { + // Setup + var @class = new QueryGroupTestExpressionClass + { + PropertyString = "A" + }; + var parsed = QueryGroup.Parse(c => c.PropertyString.ToLower().ToUpper().Contains("A")); + + // Act + var actual = parsed.GetString(m_dbSetting); + var expected = "(UPPER(LOWER([PropertyString])) LIKE @PropertyString)"; + + // Assert + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void TestQueryGroupParseExpressionStringToUpperFromClassProperty() + { + // Setup + var @class = new QueryGroupTestExpressionClass + { + PropertyString = "A" + }; + var parsed = QueryGroup.Parse(c => c.PropertyString.ToUpper() == "A"); + + // Act + var actual = parsed.GetString(m_dbSetting); + var expected = "(UPPER([PropertyString]) = @PropertyString)"; + + // Assert + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void TestQueryGroupParseExpressionStringToLowerFromClassProperty() + { + // Setup + var @class = new QueryGroupTestExpressionClass + { + PropertyString = "A" + }; + var parsed = QueryGroup.Parse(c => c.PropertyString.ToLower() == "A"); + + // Act + var actual = parsed.GetString(m_dbSetting); + var expected = "(LOWER([PropertyString]) = @PropertyString)"; + + // Assert + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void TestQueryGroupParseExpressionStringToLowerToUpperFromClassProperty() + { + // Setup + var @class = new QueryGroupTestExpressionClass + { + PropertyString = "A" + }; + var parsed = QueryGroup.Parse(c => c.PropertyString.ToLower().ToUpper() != "A"); + + // Act + var actual = parsed.GetString(m_dbSetting); + var expected = "(UPPER(LOWER([PropertyString])) <> @PropertyString)"; + + // Assert + Assert.AreEqual(expected, actual); + } + } +} diff --git a/RepoDb/RepoDb.Tests/RepoDb.UnitTests/RepoDb.UnitTests.csproj b/RepoDb/RepoDb.Tests/RepoDb.UnitTests/RepoDb.UnitTests.csproj index 8b25d7ebc..3e6631568 100644 --- a/RepoDb/RepoDb.Tests/RepoDb.UnitTests/RepoDb.UnitTests.csproj +++ b/RepoDb/RepoDb.Tests/RepoDb.UnitTests/RepoDb.UnitTests.csproj @@ -83,6 +83,7 @@ + @@ -129,6 +130,7 @@ + diff --git a/RepoDb/RepoDb/DataProviderFunctions/DataProviderFunction.cs b/RepoDb/RepoDb/DataProviderFunctions/DataProviderFunction.cs new file mode 100644 index 000000000..1562c4087 --- /dev/null +++ b/RepoDb/RepoDb/DataProviderFunctions/DataProviderFunction.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Linq.Expressions; + +namespace RepoDb.DataProviderFunctions { + /// + /// An object that represents a data provider-level function that is intended + /// to be applied to the column/field itself during statement-generation. + /// + public class DataProviderFunction { + + /// + /// Name of server-side SQL function + /// + public readonly string Name; + + /// + /// Optional input arguments for the data provider function + /// + public readonly IEnumerable Arguments; + + /// + /// Creates a new instance of object. + /// + public DataProviderFunction(string functionName, IEnumerable arguments) : this(functionName) { + Arguments = arguments; + } + + + /// + /// Creates a new instance of object. + /// + public DataProviderFunction(string functionName) { + Name = functionName; + } + } + +} \ No newline at end of file diff --git a/RepoDb/RepoDb/DataProviderFunctions/DataProviderFunctionBuilders/BaseDataProviderFunctionBuilder.cs b/RepoDb/RepoDb/DataProviderFunctions/DataProviderFunctionBuilders/BaseDataProviderFunctionBuilder.cs new file mode 100644 index 000000000..542b93530 --- /dev/null +++ b/RepoDb/RepoDb/DataProviderFunctions/DataProviderFunctionBuilders/BaseDataProviderFunctionBuilder.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; + +namespace RepoDb.DataProviderFunctions.DataProviderFunctionBuilders { + + /// + /// Base class for building data provider functions + /// + public abstract class BaseDataProviderFunctionBuilder : IDataProviderFunctionBuilder { + + /// + /// + /// + public IEnumerable DataProviderFunctions { get; protected set; } + + /// + /// + /// + public string FieldName { get; protected set; } + + /// + /// Contains a dictionary of supported data provider functions: + /// key = function name + /// value = Decorator delegate that will transform input field. + /// + protected Dictionary, string>> Vocabulary = new Dictionary, string>>(); + + /// + /// Initializes Vocabulary + /// + protected BaseDataProviderFunctionBuilder() { + Vocabulary.Add("ToUpper", (fieldName, exprArgs) => string.Format("UPPER({0})", fieldName)); + Vocabulary.Add("ToLower", (fieldName, exprArgs) => string.Format("LOWER({0})", fieldName)); + /* Just a proof of concept for accommodating parameterized server-side functions + Vocabulary.Add("Substring", (fieldName, exprArgs) => string.Format("SUBSTRING({0}, {1},{2})", fieldName, + ((ConstantExpression)exprArgs[0]).Value, + ((ConstantExpression)exprArgs[1]).Value); + */ + } + + /// + /// Convenience ctor + /// + /// + /// + public BaseDataProviderFunctionBuilder(string fieldName, + IEnumerable dataProviderFunctions) : this() { + FieldName = fieldName; + DataProviderFunctions = dataProviderFunctions; + } + + /// + /// + /// + /// + public string Build() { + if (DataProviderFunctions?.Any() == false) { + return FieldName; + } + string fieldName = FieldName; + foreach (var dataProviderFunc in DataProviderFunctions) { + VerifyFunctionSupport(dataProviderFunc); + Decorate(ref fieldName, dataProviderFunc); + } + return fieldName; + } + + /// + /// + /// + /// + /// + public virtual bool IsDataProviderFunction(string functionName) { + return Vocabulary.ContainsKey(functionName); + } + + #region privates + private void VerifyFunctionSupport(DataProviderFunction dataProviderFunc) { + if (!IsDataProviderFunction(dataProviderFunc.Name)) { + // We shouldn't be here if IsDataProviderFunction was also used during extraction of functions. + throw new NotSupportedFunctionException(dataProviderFunc.Name); + } + } + + private void Decorate(ref string fieldName, DataProviderFunction dataProviderFunc) { + try { + fieldName = Vocabulary[dataProviderFunc.Name](fieldName, dataProviderFunc.Arguments); + } + catch (Exception exc) { + throw new DataProviderFunctionDecoratorException(fieldName, dataProviderFunc.Name, exc); + } + } + #endregion + } + + #region Exceptions + /// + /// + /// + public class NotSupportedFunctionException : NotSupportedException { + /// + /// + /// + public readonly string DataProviderFunctionName; + + /// + /// + /// + /// + public NotSupportedFunctionException(string dataProviderFunctionName) : base(string.Format("Function {0} not supported by DataProvider", + dataProviderFunctionName)) { + DataProviderFunctionName = dataProviderFunctionName; + } + } + + /// + /// + /// + public class DataProviderFunctionDecoratorException : ApplicationException { + /// + /// + /// + public readonly string FieldName; + /// + /// + /// + public readonly string DataProviderFunctionName; + /// + /// + /// + public readonly Exception Exception; + + /// + /// + /// + /// + /// + /// + public DataProviderFunctionDecoratorException(string fieldName, string dataProviderFunctionName, Exception exc) : + base(string.Format("Decorator for DataProviderFunction {0} threw an exception while being applied to field {1}: {2}", + dataProviderFunctionName, fieldName, exc.Message)) { + FieldName = fieldName; + DataProviderFunctionName = dataProviderFunctionName; + Exception = exc; + } + } +} + #endregion Exceptions \ No newline at end of file diff --git a/RepoDb/RepoDb/DataProviderFunctions/DataProviderFunctionBuilders/IDataProviderFunctionBuilder.cs b/RepoDb/RepoDb/DataProviderFunctions/DataProviderFunctionBuilders/IDataProviderFunctionBuilder.cs new file mode 100644 index 000000000..6679621bd --- /dev/null +++ b/RepoDb/RepoDb/DataProviderFunctions/DataProviderFunctionBuilders/IDataProviderFunctionBuilder.cs @@ -0,0 +1,19 @@ +namespace RepoDb.DataProviderFunctions.DataProviderFunctionBuilders { + /// + /// Builder interface for data provider functions + /// + public interface IDataProviderFunctionBuilder { + /// + /// Determines whether a given function name is a supported data provider function. + /// + /// + /// + bool IsDataProviderFunction(string functionName); + + /// + /// Builds data provider functions by applying decorators to a field + /// + /// + string Build(); + } +} \ No newline at end of file diff --git a/RepoDb/RepoDb/DataProviderFunctions/DataProviderFunctionBuilders/SqlServerFunctionBuilder.cs b/RepoDb/RepoDb/DataProviderFunctions/DataProviderFunctionBuilders/SqlServerFunctionBuilder.cs new file mode 100644 index 000000000..f1e9c5869 --- /dev/null +++ b/RepoDb/RepoDb/DataProviderFunctions/DataProviderFunctionBuilders/SqlServerFunctionBuilder.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; + +namespace RepoDb.DataProviderFunctions.DataProviderFunctionBuilders { + + /// + /// + /// + public class SqlServerFunctionBuilder : BaseDataProviderFunctionBuilder { + + /// + /// + /// + public SqlServerFunctionBuilder() : base() { + // Extend support for provider-specific capabilities/syntax by adding/updating entries in FunctionBuilders + // Vocabulary.Add(".NET function here...", ); + } + + /// + /// + /// + /// + /// + public SqlServerFunctionBuilder(string fieldName, + IEnumerable dataProviderFunctions) : base(fieldName, dataProviderFunctions) + { + } + } + + + /* + * + /// + /// POC for a project-specific function builder that can include a .NET extension method + server UDF pair + /// *** BUT can be a surface vector for SQL injection attacks *** + /// Otherwise, chosen DataProviderFunctionBuilder assembly can be loaded from configuration via Reflection + /// so that RepoDb clients can plug in their own function builder if desired. + /// /// + public class MyProjectSpecificFunctionBuilder : SqlServerFunctionBuilder { + + /// + /// + /// + public MyProjectSpecificFunctionBuilder() : base() { + Vocabulary.Add("ApplyServerUdf", (fieldName, exprArgs) => string.Format("dbo.fn_MyUdf({0})", fieldName)); + } + } + + */ +} \ No newline at end of file diff --git a/RepoDb/RepoDb/DataProviderFunctions/DataProviderFunctionExtractor.cs b/RepoDb/RepoDb/DataProviderFunctions/DataProviderFunctionExtractor.cs new file mode 100644 index 000000000..b467336b0 --- /dev/null +++ b/RepoDb/RepoDb/DataProviderFunctions/DataProviderFunctionExtractor.cs @@ -0,0 +1,82 @@ +using RepoDb.DataProviderFunctions.DataProviderFunctionBuilders; +using System.Collections.Generic; +using System.Linq.Expressions; + +namespace RepoDb.DataProviderFunctions { + /// + /// Visitor that traverses a MethodCall expression tree and extracts functions from the call-chain + /// that are classified as data-provider functions based on a configurable Vocabulary. + /// The extracted functions will then be translated to data-provider syntax during SQL-generation. + /// + public class DataProviderFunctionExtractor : ExpressionVisitor { + + private Stack m_dataProviderFunctions = new Stack(); + + /// + /// Given Extract({x.Customer.ToLower().Contains('a')}), MemberExpression should return { x.Customer } + /// + public Expression MemberExpression { get; protected set; } + + /// + /// Helper interface for classifying data provider-supported functions + /// + public IDataProviderFunctionBuilder DataProviderFunctionBuilder { get; protected set; } + + /// + /// Output for extracted data provider functions from input MethodCallExpression + /// + public IList ExtractedDataProviderFunctions; + + + /// + /// + /// + public DataProviderFunctionExtractor(IDataProviderFunctionBuilder dataProviderFunctionBuilder) { + ExtractedDataProviderFunctions = new List(); + DataProviderFunctionBuilder = dataProviderFunctionBuilder; + } + + /// + /// Entry point + /// + /// + public void Extract(MethodCallExpression node) { + ExtractedDataProviderFunctions.Clear(); + + if (node.Object is MemberExpression) { + MemberExpression = node.Object; + } + + Visit(node); + + while (m_dataProviderFunctions.Count > 0) { + ExtractedDataProviderFunctions.Add(m_dataProviderFunctions.Pop()); + } + } + + /// + /// Extract method calls (and any arguments) that are classified as data provider functions + /// + /// + /// + protected override Expression VisitMethodCall(MethodCallExpression node) { + if (DataProviderFunctionBuilder.IsDataProviderFunction(node.Method.Name)) { + m_dataProviderFunctions.Push(new DataProviderFunction(node.Method.Name, node.Arguments)); + } + return base.VisitMethodCall(node); + } + + + /// + /// See MemberExpression comments + /// + /// + /// + protected override Expression VisitMember(MemberExpression node) { + if (MemberExpression == null) { + MemberExpression = node; + } + return base.VisitMember(node); + } + } +} \ No newline at end of file diff --git a/RepoDb/RepoDb/Extensions/QueryFieldExtension.cs b/RepoDb/RepoDb/Extensions/QueryFieldExtension.cs index 0c1a28373..4570db36b 100644 --- a/RepoDb/RepoDb/Extensions/QueryFieldExtension.cs +++ b/RepoDb/RepoDb/Extensions/QueryFieldExtension.cs @@ -3,6 +3,7 @@ using System.Data; using System.Dynamic; using System.Linq; +using RepoDb.DataProviderFunctions.DataProviderFunctionBuilders; using RepoDb.Enumerations; using RepoDb.Interfaces; @@ -39,7 +40,9 @@ public static void ResetAll(this IEnumerable queryFields) internal static string AsField(this QueryField queryField, IDbSetting dbSetting) { - return queryField.Field.Name.AsField(dbSetting); + var result = queryField.Field.Name.AsField(dbSetting); + ApplyServerFunctionsIfNeeded(queryField, dbSetting, ref result); + return result; } // AsParameter @@ -142,5 +145,11 @@ internal static object AsObject(this IEnumerable queryFields) } return expandoObject; } + + private static void ApplyServerFunctionsIfNeeded(QueryField queryField, IDbSetting dbSetting, ref string fieldName) { + // TODO: Inject provider-specific IDataProviderFunctionBuilder + IDataProviderFunctionBuilder dataProviderFunctionBuilder = new SqlServerFunctionBuilder(fieldName, queryField.Field.DataProviderFunctions); + fieldName = dataProviderFunctionBuilder.Build(); + } } } diff --git a/RepoDb/RepoDb/Field.cs b/RepoDb/RepoDb/Field.cs index b1651b572..ba218200b 100644 --- a/RepoDb/RepoDb/Field.cs +++ b/RepoDb/RepoDb/Field.cs @@ -5,6 +5,7 @@ using System.Linq.Expressions; using System.Reflection; using RepoDb.Exceptions; +using RepoDb.DataProviderFunctions; namespace RepoDb { @@ -56,6 +57,12 @@ public Field(string name, /// public Type Type { get; set; } + + /// + /// Creates an enumerable of objects. + /// + public IEnumerable DataProviderFunctions { get; set; } = new List(); + #endregion #region Methods diff --git a/RepoDb/RepoDb/QueryField.cs b/RepoDb/RepoDb/QueryField.cs index 36d97a0f1..52802dc76 100644 --- a/RepoDb/RepoDb/QueryField.cs +++ b/RepoDb/RepoDb/QueryField.cs @@ -1,8 +1,10 @@ using RepoDb.Attributes; +using RepoDb.DataProviderFunctions; using RepoDb.Enumerations; using RepoDb.Exceptions; using RepoDb.Extensions; using System; +using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -116,6 +118,25 @@ internal QueryField(Field field, Parameter = new Parameter(field.Name, value, appendUnderscore); } + /// + /// Creates a new instance of object. + /// + /// The name of the field for the query expression. + /// The operation to be used for the query expression. + /// The value to be used for the query expression. + /// Data provider functions + public QueryField(string fieldName, + Operation operation, + object value, + IEnumerable dataProviderFunctions) + : this(fieldName, + operation, + value, + false) + { + Field.DataProviderFunctions = dataProviderFunctions; + } + #endregion #region Properties diff --git a/RepoDb/RepoDb/QueryGroup.cs b/RepoDb/RepoDb/QueryGroup.cs index 8ce59d349..5bb1b93c6 100644 --- a/RepoDb/RepoDb/QueryGroup.cs +++ b/RepoDb/RepoDb/QueryGroup.cs @@ -9,6 +9,8 @@ using System.Dynamic; using System.Data; using RepoDb.Interfaces; +using RepoDb.DataProviderFunctions; +using RepoDb.DataProviderFunctions.DataProviderFunctionBuilders; namespace RepoDb { @@ -764,7 +766,7 @@ private static QueryGroup Parse(BinaryExpression expression) // MethodCall else if (expression.Left.IsMethodCall()) { - leftQueryGroup = Parse(expression.Left.ToMethodCall(), false, isEqualsTo); + leftQueryGroup = Parse(expression.Left.ToMethodCall(), false, isEqualsTo, expression); } else { @@ -872,7 +874,8 @@ private static QueryGroup Parse(MemberExpression expression, private static QueryGroup Parse(MethodCallExpression expression, bool isNot, - bool isEqualsTo) + bool isEqualsTo, + BinaryExpression parentExpression = null) where TEntity : class { // Check methods for the 'Like', both 'Array.()' @@ -899,6 +902,11 @@ private static QueryGroup Parse(MethodCallExpression expression, return ParseContainsForArrayOrList(expression, isNot, isEqualsTo); } } + else if (expression.Object?.IsMethodCall() == true) + { + // Check for the (p => p.Property.ToUpper().Contains("A")) for LIKE + return ParseContainsOrStartsWithOrEndsWithForStringProperty(expression, isNot, isEqualsTo); + } else { // Check for the (array.Contains(p.Property)) or (new [] { value1, value2 }).Contains(p.Property)) @@ -917,7 +925,12 @@ private static QueryGroup Parse(MethodCallExpression expression, } } } - + else { + QueryGroup queryGroup; + if (TryParseDataProviderFunctions(expression, parentExpression, out queryGroup)){ + return queryGroup; + } + } // Return null if not supported return null; } @@ -1099,6 +1112,10 @@ private static QueryGroup ParseContainsOrStartsWithOrEndsWithForStringProperty(MethodCallExpression expression, + BinaryExpression parentExpression, + out QueryGroup queryGroup) + where TEntity : class + { + queryGroup = null; + + if (parentExpression == null) { + return false; + } + + // TODO: Inject IDataProviderFunctionBuilder dependency; hard-coded for now to SQL Server. + var dataProviderFunctionExtractor = new DataProviderFunctionExtractor(new SqlServerFunctionBuilder()); + dataProviderFunctionExtractor.Extract(expression); + + if (dataProviderFunctionExtractor.ExtractedDataProviderFunctions.Count == 0) + { + return false; + } + + // Get the value arg + var value = Convert.ToString(parentExpression.Right.GetValue()); + + // Make sure it has a value + if (string.IsNullOrEmpty(value)) + { + throw new NotSupportedException($"Expression '{expression.ToString()}' is currently not supported."); + } + + // Make sure it is a property info + var member = dataProviderFunctionExtractor.MemberExpression.ToMember().Member; + if (member.IsPropertyInfo() == false) + { + throw new NotSupportedException($"Expression '{expression.ToString()}' is currently not supported."); + } + + // Get the property + var property = member.ToPropertyInfo(); + + // Make sure the property is in the entity + if (PropertyCache.Get().FirstOrDefault(p => string.Equals(p.PropertyInfo.Name, property.Name, StringComparison.OrdinalIgnoreCase)) == null) + { + throw new InvalidExpressionException($"Invalid expression '{expression.ToString()}'. The property {property.Name} is not defined on a target type '{typeof(TEntity).FullName}'."); + } + + // Add to query fields; support = and != only for now. + var operation = parentExpression.NodeType == ExpressionType.Equal ? Operation.Equal : Operation.NotEqual; + var queryField = new QueryField(PropertyMappedNameCache.Get(property), + operation, + value, + dataProviderFunctionExtractor.ExtractedDataProviderFunctions); + + queryGroup = new QueryGroup(queryField.AsEnumerable()); + return true; + } private static string ConvertToLikeableValue(string methodName, string value) { diff --git a/RepoDb/RepoDb/RepoDb.csproj b/RepoDb/RepoDb/RepoDb.csproj index aeb85be65..705f20369 100644 --- a/RepoDb/RepoDb/RepoDb.csproj +++ b/RepoDb/RepoDb/RepoDb.csproj @@ -69,6 +69,11 @@ + + + + + @@ -268,7 +273,6 @@ - From 173b2e16812a39135cb82fcb55ae482de533b91b Mon Sep 17 00:00:00 2001 From: JABE Date: Sat, 6 Jun 2020 11:19:43 +0800 Subject: [PATCH 2/2] Issue #403: Minor refactor on Exception types --- .../BaseDataProviderFunctionBuilder.cs | 58 +------------------ .../DataProviderFunctionExtractor.cs | 5 +- .../DataProviderFunctionDecoratorException.cs | 36 ++++++++++++ .../NotSupportedFunctionException.cs | 22 +++++++ RepoDb/RepoDb/RepoDb.csproj | 2 + 5 files changed, 66 insertions(+), 57 deletions(-) create mode 100644 RepoDb/RepoDb/DataProviderFunctions/Exceptions/DataProviderFunctionDecoratorException.cs create mode 100644 RepoDb/RepoDb/DataProviderFunctions/Exceptions/NotSupportedFunctionException.cs diff --git a/RepoDb/RepoDb/DataProviderFunctions/DataProviderFunctionBuilders/BaseDataProviderFunctionBuilder.cs b/RepoDb/RepoDb/DataProviderFunctions/DataProviderFunctionBuilders/BaseDataProviderFunctionBuilder.cs index 542b93530..e2e945993 100644 --- a/RepoDb/RepoDb/DataProviderFunctions/DataProviderFunctionBuilders/BaseDataProviderFunctionBuilder.cs +++ b/RepoDb/RepoDb/DataProviderFunctions/DataProviderFunctionBuilders/BaseDataProviderFunctionBuilder.cs @@ -1,4 +1,5 @@ -using System; +using RepoDb.DataProviderFunctions.Exceptions; +using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; @@ -93,58 +94,5 @@ private void Decorate(ref string fieldName, DataProviderFunction dataProviderFun } } #endregion - } - - #region Exceptions - /// - /// - /// - public class NotSupportedFunctionException : NotSupportedException { - /// - /// - /// - public readonly string DataProviderFunctionName; - - /// - /// - /// - /// - public NotSupportedFunctionException(string dataProviderFunctionName) : base(string.Format("Function {0} not supported by DataProvider", - dataProviderFunctionName)) { - DataProviderFunctionName = dataProviderFunctionName; - } - } - - /// - /// - /// - public class DataProviderFunctionDecoratorException : ApplicationException { - /// - /// - /// - public readonly string FieldName; - /// - /// - /// - public readonly string DataProviderFunctionName; - /// - /// - /// - public readonly Exception Exception; - - /// - /// - /// - /// - /// - /// - public DataProviderFunctionDecoratorException(string fieldName, string dataProviderFunctionName, Exception exc) : - base(string.Format("Decorator for DataProviderFunction {0} threw an exception while being applied to field {1}: {2}", - dataProviderFunctionName, fieldName, exc.Message)) { - FieldName = fieldName; - DataProviderFunctionName = dataProviderFunctionName; - Exception = exc; - } - } + } } - #endregion Exceptions \ No newline at end of file diff --git a/RepoDb/RepoDb/DataProviderFunctions/DataProviderFunctionExtractor.cs b/RepoDb/RepoDb/DataProviderFunctions/DataProviderFunctionExtractor.cs index b467336b0..fb18cc0c3 100644 --- a/RepoDb/RepoDb/DataProviderFunctions/DataProviderFunctionExtractor.cs +++ b/RepoDb/RepoDb/DataProviderFunctions/DataProviderFunctionExtractor.cs @@ -4,9 +4,10 @@ namespace RepoDb.DataProviderFunctions { /// - /// Visitor that traverses a MethodCall expression tree and extracts functions from the call-chain + /// Visitor class that traverses a MethodCall expression tree and extracts functions from the call-chain /// that are classified as data-provider functions based on a configurable Vocabulary. - /// The extracted functions will then be translated to data-provider syntax during SQL-generation. + /// The extracted functions will then be translated to data-provider syntax during SQL-generation + /// but the translation itself is not part of this type's responsibility. /// public class DataProviderFunctionExtractor : ExpressionVisitor { diff --git a/RepoDb/RepoDb/DataProviderFunctions/Exceptions/DataProviderFunctionDecoratorException.cs b/RepoDb/RepoDb/DataProviderFunctions/Exceptions/DataProviderFunctionDecoratorException.cs new file mode 100644 index 000000000..697f18144 --- /dev/null +++ b/RepoDb/RepoDb/DataProviderFunctions/Exceptions/DataProviderFunctionDecoratorException.cs @@ -0,0 +1,36 @@ +using System; + +namespace RepoDb.DataProviderFunctions.Exceptions { + + /// + /// + /// + public class DataProviderFunctionDecoratorException : ApplicationException { + /// + /// + /// + public readonly string FieldName; + /// + /// + /// + public readonly string DataProviderFunctionName; + /// + /// + /// + public readonly Exception Exception; + + /// + /// + /// + /// + /// + /// + public DataProviderFunctionDecoratorException(string fieldName, string dataProviderFunctionName, Exception exc) : + base(string.Format("Decorator for DataProviderFunction {0} threw an exception while being applied to field {1}: {2}", + dataProviderFunctionName, fieldName, exc.Message)) { + FieldName = fieldName; + DataProviderFunctionName = dataProviderFunctionName; + Exception = exc; + } + } +} diff --git a/RepoDb/RepoDb/DataProviderFunctions/Exceptions/NotSupportedFunctionException.cs b/RepoDb/RepoDb/DataProviderFunctions/Exceptions/NotSupportedFunctionException.cs new file mode 100644 index 000000000..8f1fcc41d --- /dev/null +++ b/RepoDb/RepoDb/DataProviderFunctions/Exceptions/NotSupportedFunctionException.cs @@ -0,0 +1,22 @@ +using System; + +namespace RepoDb.DataProviderFunctions.Exceptions { + /// + /// + /// + public class NotSupportedFunctionException : NotSupportedException { + /// + /// + /// + public readonly string DataProviderFunctionName; + + /// + /// + /// + /// + public NotSupportedFunctionException(string dataProviderFunctionName) : base(string.Format("Function {0} not supported by DataProvider", + dataProviderFunctionName)) { + DataProviderFunctionName = dataProviderFunctionName; + } + } +} diff --git a/RepoDb/RepoDb/RepoDb.csproj b/RepoDb/RepoDb/RepoDb.csproj index 705f20369..0dca326fe 100644 --- a/RepoDb/RepoDb/RepoDb.csproj +++ b/RepoDb/RepoDb/RepoDb.csproj @@ -74,6 +74,8 @@ + +