Однозначно идентифицирующий анонимный метод с параметрами тела Func

Я пытаюсь написать какой-то волшебный код для обработки кеширования, которое может или не может быть возможным. По сути, идея состоит в том, чтобы иметь класс CacheManager со статическим методом, который принимает Func для выполнения в качестве параметра. В теле статического метода он сможет выполнить этот Func и кэшировать результаты, используя ключ кеша, который однозначно идентифицирует внутренние компоненты переданного Func (анонимный метод с 0 или более параметрами). Последующие вызовы этого статического метода с теми же предоставленными аргументами приведут к тому же ключу кеширования и вернут кешированные результаты.

Мне нужен способ однозначной идентификации переданной анонимной функции.

Изменить: Expression предоставил ответ после того, как я скорректировал синтаксис анонимной функции.

Меня беспокоит влияние на производительность компиляции выражения во время выполнения. Учитывая, что это попытка поддержки кеширования для повышения производительности, было бы глупо, если бы компиляция занимала какое-либо значительное количество времени. Есть предположения?

Базовый репозиторий для тестирования:

public class Product
{
    public int ID { get; set; }
    public string Name { get; set; }
}

public class ProductRepository
{
    private List<Product> products { get; set; }

    public ProductRepository()
    {
        products = new List<Product>() { new Product() { ID = 1, Name = "Blue Lightsaber" }, new Product() { ID = 2, Name = "Green Lightsaber" }, new Product() { ID = 3, Name = "Red Lightsaber" } };
    }

    public Product GetByID(int productID)
    {
        return products.SingleOrDefault(p => p.ID == productID);
    }
}

CacheManager:

public class CacheManager
{
    public static TResult Get<TResult>(Expression<Func<TResult>> factory)
    {
        if (factory == null) throw new ArgumentNullException("factory");

        var methodCallExpression = factory.Body as MethodCallExpression;
        if (methodCallExpression == null) throw new ArgumentException("factory must contain a single MethodCallExpression.");

        string cacheKey = "|Repository:" + methodCallExpression.Method.DeclaringType.FullName + "|Method:" + methodCallExpression.Method.Name + "|Args";
        foreach (var arg in methodCallExpression.Arguments)
        {
            cacheKey += ":" + (arg is ConstantExpression ? ((ConstantExpression)arg).Value : Expression.Lambda(arg).Compile().DynamicInvoke());
        }

        if (HttpContext.Current.Cache[cacheKey] == null)
        {
            HttpContext.Current.Cache[cacheKey] = factory.Compile().Invoke();
        }
        return (TResult)HttpContext.Current.Cache[cacheKey];
    }
}

Использование:

ProductRepository productRepository = new ProductRepository();
int productID = 1;
Product product;

// From repo
product = CacheManager.Get<Product>(() => productRepository.GetByID(1));

// From cache
product = CacheManager.Get<Product>(() => productRepository.GetByID(productID));

person benmccallum    schedule 15.01.2013    source источник
comment
Ограничено ли каким-либо образом тело действия? Или это может быть любой произвольный код?   -  person Daniel Hilgarth    schedule 15.01.2013
comment
Это должно было стать моей следующей проблемой ... возможно ли вообще что-то там принудить ?? Возможно, я мог бы наложить ограничение на тип TInstance. Было бы неплохо обеспечить, чтобы тело действия было единственным оператором присваивания моему аргументу out, чтобы уменьшить вероятность передачи чего-то изменчивого в качестве действия (при условии, что это синтаксис, необходимый для достижения окончательного решения).   -  person benmccallum    schedule 15.01.2013
comment
Вы не можете принудительно применить его во время компиляции, вы можете применить его только во время выполнения с помощью выражений. Одна мысль: почему такой странный синтаксис с присвоением переменной и тем же параметром, что и выходной параметр? Почему бы просто не использовать Func<TRepository, TResult> и не заменить GetValue на эту подпись: TResult GetValue<TRepository, TResult>(TRepository repository, Func<TRepository, TResult> valueFactory)? Это действительно сработает, а ваш текущий код - нет.   -  person Daniel Hilgarth    schedule 15.01.2013
comment
Текущий код действительно правильно назначает значение и работает, но я согласен, что это странно. Я рассмотрю ваше предложение об использовании Func и вскоре обновлю свой вопрос. На самом деле я обдумывал это, но не исследовал его полностью. Спасибо за помощь!   -  person benmccallum    schedule 15.01.2013
comment
Я обновил свой вопрос вашими предложениями. Смотрится намного красивее. Но у меня все еще та же проблема. Как получить ключ кеша из аргумента Func<TRepository, TResult>? Если я заключу его в Expression, я получу доступ к Body, но я не могу передать Func как Body, потому что выражение lamba с телом оператора не может быть преобразовано в дерево выражений. Кроме того, даже если бы я мог обернуть его, мне бы пришлось вызвать Compile () для Expression перед его вызовом; который, как я полагаю, имеет некоторые накладные расходы, делающие его менее производительным (хотя его нужно будет скомпилировать только при промахе кеша). Мысли?   -  person benmccallum    schedule 15.01.2013
comment
Не используйте тело оператора. Используйте это: (productsRepositoryInstance) => productsRepositoryInstance.GetByID(1). Я думаю, вы не сможете обойтись без вызова Compile, если вы не хотите сравнивать код IL, чтобы получить ключ кеширования. Однако использование кода IL может быть альтернативой: когда вы не используете выражение, вы не можете вызвать Method.GetMethodBody().GetILasByteArray() для делегата. Я предполагаю, что этот массив байтов можно использовать в качестве ключа. Однако я не уверен, насколько эффективен ключ такого размера.   -  person Daniel Hilgarth    schedule 15.01.2013
comment
Спасибо еще раз. Я обновил вопрос кодом, который выполняет задание и не требует компиляции Expression в IL, если ключ кеша не является промахом кеша. Вы видите какие-либо нерешенные проблемы? Что вы думаете о кешировании скомпилированного Func? Учитывая, насколько маленьким будет тело Expression и что Expression нужно компилировать только при промахах в кеше, накладные расходы, вероятно, незначительны. Какую еще обработку исключений я мог бы добавить? В частности, о преобразовании Expression в MethodCallExpression? Или просто позволить среде выполнения генерировать собственное исключение? Ваше здоровье!   -  person benmccallum    schedule 15.01.2013
comment
Выглядит неплохо. Еще два момента, которые следует учитывать: (1) Вы должны проверить, что MethodCallExpression действительно вызывает метод в репозитории, который передается в качестве параметра делегату (2) (arg as ConstantExpression).Value выдаст NullReferenceException, если arg не является ConstantExpression. Код, выдающий NullReferenceException, считается ошибочным. Он должен выдать ArgumentException, сообщающий вызывающему, что все параметры должны быть константами. Что, кстати, кажется странным требованием. А что насчет r => r.GetByID(id)?   -  person Daniel Hilgarth    schedule 15.01.2013
comment
Да, вы правы, я неправильно понял определение ConstantExpression. Я добавлю проверку переданного экземпляра, а также подумаю, как получить значение аргумента другим способом.   -  person benmccallum    schedule 15.01.2013
comment
Я решил проблему 1, упомянутую выше, полностью удалив параметр репозитория. Проблема 2 немного сложна, потому что мне нужно учитывать выражения в качестве параметров в анонимном методе. Я попытался найти решение, и оно работает при передаче констант и переменных, как показано в коде использования. Использование сейчас довольно емкое. Я не думаю, что когда-либо смогу полностью заблокировать и ограничить то, что передается для Expression, поэтому я просто постараюсь сделать все возможное, чтобы проверить.   -  person benmccallum    schedule 16.01.2013


Ответы (1)


Похоже, вы хотите сделать что-то вроде мемоизации. Мемоизация - это метод хранения результатов, которые уже были вычислены, что, по-видимому, является вашим требованием.

Лучший способ сделать это - создать новый объект функции, который отличается от оригинала тем, что он будет хранить результаты в словаре и выполнять поиск по предоставленным параметрам. Эта новая функция будет обрабатывать кеширование и добавит в кеш в случае промаха.

Этот класс создаст мемоизированные версии функций одного аргумента:

public static class memofactory
{
    public static Func<In, Out> Memoize<In, Out>(Func<In, Out> BaseFunction)
    {
        Dictionary<In,Out> ResultsDictionary = new Dictionary<In, Out>();

        return Input =>
            {
                Out rval;
                try
                {
                    rval = ResultsDictionary[Input];
                    Console.WriteLine("Cache hit"); // tracing message
                }
                catch (KeyNotFoundException)
                {
                    Console.WriteLine("Cache miss"); // tracing message
                    rval = BaseFunction(Input);
                    ResultsDictionary[Input] = rval;
                }
                return rval;
            };
    }
}

Использование, основанное на вашем примере:

        ProductRepository productRepository = new ProductRepository();
        int productID = 1;
        Product product;

        Func<int, Product> MemoizedGetById = memofactory.Memoize<int, Product>(productRepository.GetByID);

        // From repo
        product = MemoizedGetById(1);

        // From cache
        product = MemoizedGetById(productID);
person Brian B    schedule 16.01.2013
comment
Спасибо, Брайан, я никогда не слышал о концепции Memoize. Похоже на то, что мне нужно. Я предполагаю, что ограничение в вашем примере заключается в том, что он может обрабатывать ввод Func только с одним параметром, хотя вы можете сделать много переопределений Memoize, принимающих до параметров In9. - person benmccallum; 16.01.2013
comment
Я рад, что это помогло. Да, вам придется переопределить Memoize для обработки большего количества параметров, и вам придется упаковать параметры в кортеж или структуру для использования в качестве ключей к словарю. Если мой ответ решил вашу проблему, примите его, если я могу что-то уточнить, дайте мне знать. - person Brian B; 16.01.2013