This project has moved and is read-only. For the latest updates, please go here.
3
Vote

Losing scope ability on attempted reuse of func

description

Hey all.
So this a weird one cause the doc under "performance" illustrates otherwise. I'm using ScopeCompile to pre-compile my query and the resulting Func is cached away, very similarly to how the Doc example shows. The only symbol registered is one called "scope" and it's a "dynamic" type. All ok so far. Now remember, my variable called "scope" of type "dynamic' is registered as a symbol into the CompiledExpression instance, which I later use to create the function using ScopeCompile. And that seems to be where the problem is cause it ties these two things together. At this point, all I've done with the variable scope is create it of type dynamic, instantiated as ExpandoObject. Ok, so now to use the function to actually perform an evaluation. I cached both the resulting func from ScopeCompile and the scope variable. So I grab both of these from cache. So far so good. I cast scope to IDictionary<string, object> and start filling it with data. The keys of course correspond to the "scope.{variable}" variables that are scattered in my expression. The values are incoming data value that I want to use against my already-compiled expression. After I'm done, I take the func I got from cache and invoke it, sending in the scope variable, which now has data. Everything works perfectly. So you wonder what's the problem? Well, I need to do this concurrently for multiple users, each running different data against the same query. Sounds easy, except that because I am caching both the Func and the scope var that I registered, I have to sync-lock it so it's all thread-safe. What I'd love to do is very simply have a new dynamic scope variable for each client that calls this routine and each one gets its own data, but no-joy. I HAVE TO use the one that I registered when I did the ScopeCompile. That exact reference seems to have been joined at the hip with the func that is returned from ScopeCompile. Attempting to create a new scope variable and using that is resulting in an error telling me that it cannot find one of my fields in the instance of ExpandoObject. Which is weird cause the instance that I'm sending into the Func DOES have the field it is telling me it cannot find, which means that internally it is looking at the scope variable instance that I originally registered with the expression. Is it a big deal that I just keep that same instance, clear the dictionary-cast of it, and refill it with new data? No, as long as I put a Lock around it cause it is being called from multiple threads. But I'd like to get around this by just having a unique scope var injected into the already compiled expression at the time of evaluation.
Any thoughts?

Here's some code:
This is the routine that performs the pre-compilation.
Notice it stores the evaluator and scope variables in the queryset object using the SetExpressionDefinition method:
        dynamic scope = new ExpandoObject();
        string expStr = expression.ToString();
        string scopeModExp = expStr.Replace("dataField", "scope.dataField").Replace("queryPart", "scope.queryPart").Replace("customFunc", "scope.customFunc");
        CompiledExpression<bool> exp = new CompiledExpression<bool>()
        {
            StringToParse = scopeModExp,
            TypeRegistry = new TypeRegistry()
        };
        exp.TypeRegistry.RegisterDefaultTypes();
        exp.TypeRegistry.RegisterSymbol("scope", scope);
        Func<object, bool> evaluator = exp.ScopeCompile();

        // Note, scope reference is compiled into expression.
        // This means that this same reference needs to be stored and used for this evaluator.
        // Casting to a IDictionary and clearing is fine, but resetting the scope variable is invalid.
        querySet.SetExpressionDefinition(evaluator, scope);

        StorePreCompiledQuerySetInCache(querySet);
And here's the routine that performs an evaluation:
Notice I am obtaining the scope from the cached Expression property and going from there.
The line commented out, that says "var scopeDict" is what I would like to do. Note that the only difference in what works and what doesn't is the one that works grabs the scope variable originally registered in the expression and casts it to a Dictionary for data-filling. The one that doesn't creates a new scope variable.
        ExpressionDefinition expression = querySet.Expression;

        lock (expression)
        {
            // this way is invalid
            //dynamic scope = new ExpandoObject();
            //var scopeDict = scope as IDictionary<string, object>;

            // this way is valid
            dynamic scope = querySet.Expression.Scope;
            var scopeDict = expression.Scope as IDictionary<string, object>;
            scopeDict.Clear();

            int part = 0;

            foreach (QueryPart queryPart in querySet.QueryParts)
            {
                part++;
                string queryPartVar = "queryPart" + part.ToString();
                string dataFieldVar = "dataField" + part.ToString();
                string customFuncVar = "customFunc" + part.ToString();

                DataField dataField = dataFields.Where(fld => fld.Name == queryPart.FieldName).FirstOrDefault();

                scopeDict.Add(dataFieldVar, dataField);
                scopeDict.Add(queryPartVar, queryPart);

                if (queryPart.ConditionalOperator.ToString() == "CUSTOM")
                {
                    IQueryPartFunction queryPartFunction = _Self.QueryPartFunctions.FirstOrDefault(item => item.FunctionName == queryPart.CustomFunctionName);
                    if (queryPartFunction != null)
                        scopeDict.Add(customFuncVar, queryPartFunction);
                }
            }

            // evaluate expression with scope data
            var expressionResult = expression.Evaluator(scope); // scope can change and only this line needs to be recalled

            return expressionResult;
        }

comments