Можно ли вызвать метод для всех общих свойств в классе?

У меня есть класс с множеством свойств типа IDbSet<SomeClass>:

public InMemoryContext : IContext
{
    public IDbSet<ClassA> ClassASet { get; set; }
    public IDbSet<ClassB> ClassBSet { get; set; }
    [...]

    public void SaveChanges()
    {
        //TODO: this is relevant part
    }
}

Все эти свойства создаются в конструкторе:

public InMemoryContext
{
    ClassASet = new InMemoryDbSet<ClassA>();
    [...]
}

У InMemoryDbSet есть один подходящий метод для ответа на этот вопрос:

public class InMemoryDbSet<T> : IDbSet<T> where T : class
{
    public void SaveChanges()
    {
         [...]
    }
}

Я хотел бы получить все свойства, которые IDbSet, и вызвать SaveChanges() для них в цикле, поэтому мне не нужно повторять все имена наборов второй раз.

Я пробовал с отражением, но так как мои свойства общие, я не могу заставить его работать. Я попытался сделать ClassA и ClassB производными от одного и того же общего интерфейса, но все равно не повезло.

Можно ли это сделать без явного указания всех наборов? Что мне нужно изменить, чтобы добиться такого результата?

Я представляю себе псевдокод:

public void SaveChanges()
{
    foreach(var set in GetDbSetsFromClass(this))
    {
        set.SaveChanges();
    }
}

С размышлением я попробовал:

public void SaveChanges()
    {
        SaveChangesCalled = true;

        var properties = GetType().GetProperties().Where(p => p.PropertyType.IsInterface && typeof(IDbSet<IEntity>).IsAssignableFrom(p.PropertyType)).Cast<InMemoryDbSet<IEntity>>();

        foreach (var property in properties)
        {
            CallSaveChanges(property);
        }
    }

И список пуст. Public bool SaveChangesCalled {get; набор; }

    private static void CallSaveChanges<T>(InMemoryDbSet<T> set) where T : class
    {
        set.SaveChanges();
    }

РЕДАКТИРОВАТЬ: общедоступные свойства должны оставаться видимыми на верхнем уровне класса InMemoryContext, поскольку это ограничено интерфейсом, определяющим контекст EF.


person Episodex    schedule 15.09.2014    source источник
comment
Я пробовал с отражением - отражение должно работать, что вы пробовали?   -  person Jean Hominal    schedule 15.09.2014
comment
Есть ли в наборе SaveChanges метод? Не могу найти в документации? Что это за IDbSet<T> опубликуйте определение интерфейса.   -  person Sriram Sakthivel    schedule 15.09.2014
comment
InMemoryDbSet имеет, как показано в коде. IDbSet нет. Я думаю, это самая сложная часть.   -  person Episodex    schedule 15.09.2014


Ответы (2)


Ваш код не работает, потому что IDbSet<Class> не может быть назначен IDbSet<IEntity>, потому что IDbSet<T> не является ковариантным.

Вам нужно найти свойства, общее определение типа которых - IDbSet<>.

var properties = typeof (InMemoryContext)
    .GetProperties()
    .Where(p => p.PropertyType.IsGenericType &&
                p.PropertyType.GetGenericTypeDefinition() == typeof (IDbSet<>));

Чтобы вызвать ваш CallSaveChanges метод, вам понадобится следующее:

foreach(var proparty in properties)
{
    var value = property.GetValue(this);
    var entityType = property.PropertyType.GetGenericArguments().First();

    var callSaveChanges = this.GetType()
                              .GetMethod("CallSaveChanges", BindingFlags.NonPublic | BindingFlags.Static);

    var constructedCallSaveChanges = callSaveChanges.MakeGenericMethod(entityType );

    constructedCallSaveChanges.Invoke(null, BindingFlags.NonPublic | BindingFlags.Static, null, new object[]{ value }, CultureInfo.InvariantCulture);
}

При этом я не думаю, что этот сценарий требует использования отражения.

person dcastro    schedule 15.09.2014
comment
Я не ограничиваюсь размышлениями. Если есть способ получше, я приму его. - person Episodex; 15.09.2014
comment
Я пробовал ваше решение, и оно находит свойства, но как я могу вызвать SaveChanges() для них? Преобразование в InMemoryDbSet<IEntity> не работает (на самом деле, я бы предпочел вообще не использовать IEntity, если это возможно). В конце концов, метод SaveChanges() определен на InMemoryDbSet, независимо от его общего типа. - person Episodex; 15.09.2014
comment
@Episodex О, так есть не общий InMemoryDbSet? Просто переведите values в InMemoryDbSet, это должно сработать. - person dcastro; 15.09.2014
comment
Нет, извините за то, что сбил вас с толку, InMemoryDbSet является общим (с определением, подобным рассматриваемому), но я думаю, что мне все равно, какой тип T, если я хочу просто вызвать для него SaveChanges (). Что-то вроде IDbSet<>, но его нельзя использовать в кастинге. - person Episodex; 15.09.2014
comment
@Episodex К сожалению, вам придется перепрыгивать через обручи, чтобы это сработало. Я обновил свой ответ. - person dcastro; 15.09.2014
comment
Для меня это прекрасно, спасибо! Это необходимо, чтобы мои модульные тесты имели определение контекста DRY и могли имитировать SaveChanges() метод из EF. Даже если это означает пару волшебных строк и некоторое отражение - это нормально, это не производственный код, и он будет иметь огромное значение для тестов. - person Episodex; 15.09.2014
comment
Также возможно вызвать SaveChanges() на установке непосредственно в foreach цикле: property.GetType().GetMethod("SaveChanges", new Type[] {}).Invoke(property, null); - person Episodex; 15.09.2014

Для простого решения вы можете попробовать что-то вроде этого

public InMemoryContext
{
    public Dictionary<string, object> allSets = new Dictionary<string, object>();
    public InMemoryContext()
    {
        allSets.Add("classA", new InMemoryDbSet<ClassA>());
        allSets.Add("classB", new InMemoryDbSet<ClassB>());
        [...]
    }
    public IDbSet<ClassA> ClassASet { get
    {
        return allSets["classA"];
    }
    set
    {
        allSets["classA"] = Value
    }
    }
    [...]

    public void SaveChanges()
    {
    //TODO: this is relevant part
    foreach(var kvp in allSets)
    {
        kvp.Value.SaveChanges();
    }
    }
}

Некоторые вещи могут быть неправильными, потому что это просто не в моей голове ... но вы должны понять

person Nokdu    schedule 15.09.2014
comment
Я тоже думал об этом, но поскольку это InMemoryContext, он должен выглядеть в точности как контекст EF (он должен выполнять какой-то интерфейс). Я не ставил этот явный вопрос под сомнение, чтобы не добавить путаницы, извините. - person Episodex; 15.09.2014