Re-Evaluate value of constant FieldExpression on each invoke

Viewed 139

Setup

I've got a method that I want to invoke periodically. The method has two DateTime parameter.

public static object PrintDateTimes(DateTime fromDate, DateTime toDate)
{
    Console.WriteLine(
        string.Format("PrintDateTimes:\t{0} - {1} :: {2}", fromDate.ToLongTimeString(), toDate.ToLongTimeString(), Program.StartDateTimeString));
    return 0;
}

The class Item provides the method Refresh and the property callback to store a method that's supposed to be invoked.

public class Item
{
    public Expression<Func<object>> callback { get; set;  }
    
    public void Refresh()
    {
        this.Refresh(this.callback);
    }

    public void Refresh(Expression<Func<object>> callback)
    {
        MethodCallExpression methodBody = (MethodCallExpression)callback.Body;
        bool fromDateFound = false;
        bool toDateFound = false;

        foreach (var arg in methodBody.Arguments) {

            if (arg.Type == typeof(DateTime)) {

                if (fromDateFound && !toDateFound) {
                    
                    DateTime fromDate = Convert.ToDateTime(Expression.Lambda(arg).Compile().DynamicInvoke());

                    Console.WriteLine("newFromDate\t" + fromDate);
                    
                    toDateFound = true;

                } else if (!fromDateFound) {

                    DateTime toDate = Convert.ToDateTime(Expression.Lambda(arg).Compile().DynamicInvoke());

                    fromDateFound = true;

                    Console.WriteLine("newToDate\t" + toDate);
                }
            }
        }

        callback.Compile().Invoke();

        Console.WriteLine();
    }

The main method looks like this

public static string StartDateTimeString;

public static void Main()
{
    DateTime now = DateTime.Now;
    DateTime fromDate = DateTime.Now.AddHours(-0.5);
    DateTime toDate = DateTime.Now.AddHours(1);

    Program.StartDateTimeString = DateTime.Now.ToLongTimeString();

    Item item = new Item();

    // this is important
    AssignCallback(fromDate, toDate, item);

    timer = new Timer(timerCallback, item, Timeout.Infinite, Timeout.Infinite);

    timer.Change(0, Timeout.Infinite);
    while (true) ;
}

public static void AssignCallback(DateTime fromDate, DateTime toDate, Item item)
{
    item.callback = () => Program.PrintDateTimes(fromDate, toDate);
}

and finally the timer callback

private static void timerCallback(object state)
{
    Item i = state as Item;

    i.Refresh();

    Console.WriteLine("Refresh at:\t" + DateTime.Now.ToLongTimeString());

    timer.Change(TimeSpan.FromSeconds(3), Timeout.InfiniteTimeSpan);
}

Problem

This is the output when executing the code (and waiting for a few seconds)

Refresh at:     10:53:58
newToDate       10:23:58
newFromDate     11:53:58
PrintDateTimes: 10:23:58 - 11:53:58 :: 10:53:58

Refresh at:     10:54:01
newToDate       10:23:58
newFromDate     11:53:58
PrintDateTimes: 10:23:58 - 11:53:58 :: 10:53:58

Refresh at:     10:54:04
newToDate       10:23:58
newFromDate     11:53:58
PrintDateTimes: 10:23:58 - 11:53:58 :: 10:53:58

Refresh at:     10:54:07
newToDate       10:23:58
newFromDate     11:53:58
PrintDateTimes: 10:23:58 - 11:53:58 :: 10:53:58

So far so good, however that's not the desired output. When invoking the callback, the DateTime values of the initial call will be used - but I want them to be evaluated again on every invoke, not just passed with their old value.

The desired output would be this (note the seconds)

Refresh at:     10:53:58
newToDate       10:23:58
newFromDate     11:53:58
PrintDateTimes: 10:23:58 - 11:53:58 :: 10:53:58

Refresh at:     10:54:01
newToDate       10:23:01
newFromDate     11:53:01
PrintDateTimes: 10:23:01 - 11:53:01 :: 10:53:58

Refresh at:     10:54:04
newToDate       10:23:04
newFromDate     11:53:04
PrintDateTimes: 10:23:04 - 11:53:04 :: 10:53:58

Refresh at:     10:54:07
newToDate       10:23:07
newFromDate     11:53:07
PrintDateTimes: 10:23:07 - 11:53:07 :: 10:53:58

What I've figured out

I've noticed that the type of arg in foreach (var arg in methodBody.Arguments) { is a FieldExpression which - afaik - means it's constant.

If I remove the call to AssignCallback and assign the callback directly, so item.callback = () => PrintDateTimes(DateTime.Now.AddHours(-0.5), DateTime.Now.AddHours(1)); it actually works.

Probably because then arg will be of the type InstanceMethodCallExpressionN, so no constant. Seems like the will be evaluated on each invoke rather than being passsed with a constant value.

Question

Is there a way to re-evaluate those parameter on each call, so that the values will be updated correctly?

2 Answers

If i follow your example correctly, your Refresh method doesn't seem to update the values - it picks whatever expression you assigned to it and reevaluates it over and over.

So what it seems you are missing is the actual update to fromDate, toDate in your timerCallback. I assume you have reasons to use an expression there, but if not - a simple delegate might actually do the job with a bit less code (and performance penalty):

public class Item
{
    public Func<DateTime, DateTime, object> callbackFunc { get; set; } // to illustrate alternative point
    public Expression<Func<object>> callback { get; set; }

    public void Refresh()
    {
        Refresh(callback);
    }

    public void RefreshFunc(DateTime fromDate, DateTime toDate) // alternative execution path - see timerCallback()
    {
        callbackFunc(fromDate, toDate);
    }
    

    public void Refresh(Expression<Func<object>> callback)
    {
        //your code with no change where
    }
}

class Program
{
    public static object PrintDateTimes(DateTime fromDate, DateTime toDate)
    {
        Console.WriteLine($"PrintDateTimes:\t{fromDate.ToLongTimeString()} - {toDate.ToLongTimeString()} :: {Program.StartDateTimeString}");
        return 0;
    }

    public static string StartDateTimeString;
    private static Timer timer;

    public static void Main()
    {
        DateTime now = DateTime.Now;
        DateTime fromDate = now.AddHours(-0.5);
        DateTime toDate = now.AddHours(1);

        Program.StartDateTimeString = now.ToLongTimeString();

        Item item = new Item();

        // this is important
        AssignCallback(fromDate, toDate, item);

        timer = new Timer(timerCallback, item, Timeout.Infinite, Timeout.Infinite);

        timer.Change(0, Timeout.Infinite);
        while (true) ;
    }

    public static void AssignCallback(DateTime fromDate, DateTime toDate, Item item)
    {
        item.callback = () => Program.PrintDateTimes(fromDate, toDate);
    }
    private static void timerCallback(object state)
    {
        Item i = state as Item;
        // update the dates
        DateTime now = DateTime.Now;
        DateTime fromDate = now.AddHours(-0.5);
        DateTime toDate = now.AddHours(1);
        // call refresh delegate - you proably don't even need to wrap it in a method: i.callbackFunc(toDate, fromDate)
        i.RefreshFunc(fromDate, toDate);

        // or use your old method, but update the expression so it gets new values
        //AssignCallback(fromDate, toDate, i); // i'm not sure if you can do this in your actual code, but this basically reassigns the expression so it works
        //i.Refresh();

        Console.WriteLine("Refresh at:\t" + now.ToLongTimeString());

        timer.Change(TimeSpan.FromSeconds(3), Timeout.InfiniteTimeSpan);
    }
}

Here

public static void AssignCallback(DateTime fromDate, DateTime toDate, Item item)
{
    item.callback = () => Program.PrintDateTimes(fromDate, toDate);
}

Local variables are captured inside lambda, which means they are promoted by compiler to fields of some compiler-generated class. That's why you have FieldExpression, and it's not constant. However, you do not update those (field-promoted) variables fromDate and toDate anywhere, so their value stays constant between executions indeed.

If you remove AssignCallback and do the same directly inside main:

item.callback = () => Program.PrintDateTimes(fromDate, toDate);

Then it's still FieldExpression, but you can update fromDate and toDate, and this change will be reflected. For example:

while (true) {
    Console.ReadKey();
    fromDate = DateTime.Now.AddHours(-0.5);
    toDate = DateTime.Now.AddHours(1);
}

Then each time you press the button in console - local (captured) variables will be updated, and next invocation of your callback will reflect that.

There is no other way to "refresh" those dates if you pass them directly as DateTime. You cannot know it was initialized as DateTime.Now.AddHours(-0.5), all you have is (already calculated) DateTime value. In your case it's better to make it explicit that your callback expects not DateTime value, but a way to get such value. For example:

public static void AssignCallback(Func<DateTime> fromDate, Func<DateTime> toDate, Item item) {
    item.callback = () => Program.PrintDateTimes(fromDate(), toDate());
}

And then:

public static void Main() {
    Func<DateTime> fromDate = () => DateTime.Now.AddHours(-0.5);
    Func<DateTime> toDate = () => DateTime.Now.AddHours(1);

    Program.StartDateTimeString = DateTime.Now.ToLongTimeString();

    Item item = new Item();

    // this is important
    AssignCallback(fromDate, toDate, item);

    timer = new Timer(timerCallback, item, Timeout.Infinite, Timeout.Infinite);

    timer.Change(0, Timeout.Infinite);
    Console.ReadKey();
}

Then it's quite clear what's going on at least.

Related