Как обобщить доступ к DbSet <TEntity>членам DbContext? - PullRequest
7 голосов
/ 21 марта 2012

У меня есть DbContext с несколькими членами следующих типов:

public DbSet<JobLevel> JobLevels { get; set; }
public DbSet<Country> Countries { get; set; }
public DbSet<Race> Races { get; set; }
public DbSet<Language> Languages { get; set; }
public DbSet<Title> Titles { get; set; }

Все это where T: IdNamePairBase, в котором есть только Id и Name членов.Я отчаянно пытаюсь найти общий интерфейс, с помощью которого можно получить доступ к любому из этих членов, чтобы обобщить следующий код контроллера MVC3 в один контроллер:

public ActionResult Edit(DropDownListModel model, Guid)
{
    var dbSet =  _dbContext.Countries;
    var newItems = model.Items.Where(i => i.IsNew && !i.IsDeleted).Select(i => new { i.Name });
    foreach (var item in newItems)
    {
        if (!string.IsNullOrWhiteSpace(item.Name))
        {
            var undead = ((IEnumerable<IdNamePairBase>)dbSet).FirstOrDefault(p => p.Name.ToLower() == item.Name.ToLower());
            if (undead != null)
            {
                // Assign new value to update to the new char. case if present.
                undead.Name = item.Name;
                undead.IsDeleted = false;
                _dbContext.SaveChanges();
                continue;
            }
            var newPair = new Country { Name = item.Name };
            dbSet.Add(newPair);
            _dbContext.SaveChanges();
        }
    }
    return RedirectToAction("Edit", new {listName = model.ListName});
}

Как я могу решить свою проблему, которая прямо сейчас янужен один контроллер для каждого из DbContext членов, как указано выше, посвященный DbSet<Country> Countries?

ЧАСТИЧНОЕ РЕШЕНИЕ: Вдоль строк, похожих на ответ GertArnold ниже, прежде чем я знал_dbContext.Set<T> все, что он выделяет, я реализовал этот метод в своем классе контекста, чтобы получить наборы определенного типа:

public IEnumerable<DbSet<T>> GetDbSetsByType<T>() where T : class
{
    //var flags = BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance;
    var props = GetType().GetProperties()
        .Where(p => p.PropertyType.IsGenericType && p.PropertyType.Name.StartsWith("DbSet"))
        .Where(p => p.PropertyType.GetGenericArguments().All(t => t == typeof(T)));
    return props.Select(p => (DbSet<T>)p.GetValue(this, null));
}

Ответы [ 3 ]

9 голосов
/ 26 марта 2012

Некоторое обобщение возможно при использовании

var dbSet = _dbContext.Set<T>

и помещение большей части вашего метода в метод с параметром универсального типа.

Однако где-то должен быть переключатель, чтобы решить, какой тип должен быть указан и какой тип создавать, потому что я думаю, что тип предоставляется как свойство модели (не так ли?). Так что это, вероятно, не будет выглядеть элегантно, но, вероятно, будет намного короче, с кодом DRY-er.

3 голосов
/ 28 сентября 2014

Я искал ответ на этот вопрос и обнаружил, что это легко сделать с помощью Managed Extensibility Framework. Ниже приведен более быстрый путь, однако MEF допускает гораздо более масштабируемый подход.

MEF позволяет создавать плагины динамического доступа из разрозненных сборок; однако его можно использовать для быстрого заполнения коллекций в одном приложении сборки. По сути, мы будем использовать его как безопасный способ отражения нашей сборки обратно в класс. Чтобы сделать это полностью функциональным, я также собираюсь внедрить шаблон стратегии в модель Entity Framework.

Добавьте ссылку на ваш проект, указывая на System.ComponentModel.Composition. Это даст доступ к библиотеке MEF.

Теперь нам нужно реализовать шаблон стратегии. Если у вас нет папки Interfaces, создайте ее и добавьте IEntity.cs, как показано ниже.

IEntity.cs

namespace Your.Project.Interfaces
{
    /// <summary>
    ///     Represents an entity used with Entity Framework Code First.
    /// </summary>
    public interface IEntity
    {
        /// <summary>
        ///     Gets or sets the identifier.
        /// </summary>
        /// <value>  
        ///     The identifier.
        /// </value>
        int Id { get; set; }
    }
}

Теперь каждый из вас должен реализовать этот интерфейс:

public class MyEntity : IEntity
{
    #region Implementation of IEntity

    /// <summary>
    ///     Gets or sets the identifier.
    /// </summary>
    /// <value>
    ///     The identifier.
    /// </value>
    public int Id { get; set; }

    #endregion

    // Other POCO properties...
}

Я считаю, что рекомендуется не создавать отдельные интерфейсы для каждого объекта, если только вы не работаете в среде с высоким уровнем тестирования. С практической точки зрения, интерфейсы должны использоваться только там, где необходим этот уровень абстракции; главным образом, когда более чем один конкретный класс унаследует, или когда работает с чрезмерно восторженным движком Inversion of Control. Если у вас есть интерфейсы для всего в вашей производственной модели, ваша архитектура, скорее всего, имеет серьезные недостатки. Во всяком случае, достаточно бессвязных.

Теперь, когда у нас есть все наши сущности, "стратегизированные", мы можем использовать MEF для их сравнения и заполнения коллекции в вашем контексте.

В вашем контексте добавьте новое свойство:

/// <summary>
///     Gets a dynamically populated list of DbSets within the context.
/// </summary>
/// <value>
///     A dynamically populated list of DbSets within the context.
/// </value>
[ImportMany(typeof(DbSet<IEntity>))]
public IEnumerable<DbSet<IEntity>> Sets { get; private set; }

Здесь [ImportMany(typeof(DbSet<IEntity>))] позволяет MEF заполнять коллекцию.

Затем добавьте соответствующий атрибут Export к каждому DbSet в контексте:

[Export(typeof(DbSet<IEntity>))]
public DbSet<MyEntity> MyEntities { get; set; }

Каждое из свойств Import ed и Export ed называется «деталью». Последняя часть головоломки состоит в том, чтобы составить эти части. Добавьте следующее в конструктор вашего контекста:

// Instantiate the Sets list.
Sets = new List<DbSet<IEntity>>();

// Create a new Types catalogue, to hold the exported parts.
var catalogue = new TypeCatalog(typeof (DbSet<IEntity>));

// Create a new Composition Container, to match all the importable and imported parts.
var container = new CompositionContainer(catalogue);

// Compose the exported and imported parts for this class.
container.ComposeParts(this); 

Теперь, если повезет, у вас должен быть динамически заполняемый список DbSets в вашем контексте.

Я использовал этот метод для упрощения усечения всех таблиц с помощью метода расширения.

/// <summary>
///     Provides extension methods for DbSet objects.
/// </summary>
public static class DbSetEx
{
    /// <summary>
    ///     Truncates the specified set.
    /// </summary>
    /// <typeparam name="TEntity">The type of the entity.</typeparam>
    /// <param name="set">The set.</param>
    /// <returns>The truncated set.</returns>
    public static DbSet<TEntity> Truncate<TEntity>(this DbSet<TEntity> set)
        where TEntity : class, IEntity
    {
        set.ToList().ForEach(p => set.Remove(p));
        return set;
    }
}

Я добавил метод в контекст для усечения всей базы данных.

/// <summary>
///     Truncates the database.
/// </summary>
public void TruncateDatabase()
{
    Sets.ToList().ForEach(s => s.Truncate());
    SaveChanges();
}

РЕДАКТИРОВАТЬ (капитальный ремонт):

Решение, приведенное выше, теперь устарело. Нужно немного подправить, чтобы это сработало. Для этого вам нужно импортировать DbSets во временную коллекцию DbSet типа «объект», а затем преобразовать эту коллекцию в DbSet требуемого типа интерфейса. Для базовых целей достаточно интерфейса IEntity.

    #region Dynamic Table List

    /// <summary>
    ///     Gets a dynamically populated list of DbSets within the context.
    /// </summary>
    /// <value>
    ///     A dynamically populated list of DbSets within the context.
    /// </value>
    public List<DbSet<IEntity>> Tables { get; private set; }

    /// <summary>
    ///     Gets a dynamically populated list of DbSets within the context.
    /// </summary>
    /// <value>
    ///     A dynamically populated list of DbSets within the context.
    /// </value>
    [ImportMany("Sets", typeof (DbSet<object>), AllowRecomposition = true)]
    private List<object> TableObjects { get; set; }

    /// <summary>
    ///     Composes the sets list.
    /// </summary>
    /// <remarks>
    ///     To make this work, you need to import the DbSets into a temporary collection of
    ///     DbSet of type "object", then cast this collection to DbSet of your required
    ///     interface type. For basic purposes, the IEntity interface will suffice.
    /// </remarks>
    private void ComposeSetsList()
    {
        // Instantiate the list of tables.
        Tables = new List<DbSet<IEntity>>();

        // Instantiate the MEF Import collection.
        TableObjects = new List<object>();

        // Create a new Types catalogue, to hold the exported parts.
        var catalogue = new TypeCatalog(typeof (DbSet<object>));

        // Create a new Composition Container, to match all the importable and imported parts.
        var container = new CompositionContainer(catalogue);

        // Compose the exported and imported parts for this class.
        container.ComposeParts(this);

        // Safe cast each DbSet<object> to the public list as DbSet<IEntity>.
        TableObjects.ForEach(p => Tables.Add(p as DbSet<IEntity>));
    }

    #endregion

Затем запустите фасад CompileSetsList() из конструктора (показаны лучшие практики для Web):

    public MvcApplicationContext()
    {
        // Enable verification of transactions for ExecuteSQL functions.
        Configuration.EnsureTransactionsForFunctionsAndCommands = true;

        // Disable lazy loading.
        Configuration.LazyLoadingEnabled = false;

        // Enable tracing of SQL queries.
        Database.Log = msg => Trace.WriteLine(msg);

        // Use MEF to compile a list of all sets within the context.
        ComposeSetsList();
    }

Затем просто украсьте свои DbSet <> как это:

    /// <summary>
    /// Gets or sets the job levels.
    /// </summary>
    /// <value>
    /// The job levels.
    /// </value>
    [Export("Sets", typeof(DbSet<object>))]
    public DbSet<JobLevel> JobLevels { get; set; }

Теперь все будет работать правильно.

3 голосов
/ 04 февраля 2014

Чтобы добавить ответ Герта Арнольда, я хочу отметить, что есть еще одна перегрузка метода в dbContext, которая возвращает общий DbSet из объекта типа:

var dbSet = dbContext.Set(typeof(T))

Если вы хотите добавить слепой объект, а затем создать объект, используя метод set.Create(), или, если у вас уже есть объект, созданный с помощью клавиши "new", вы можете преобразовать его с помощью (аналогично этот ответ )

var entity = dbSet.Create();
dbSet.Add(entity);
DbEntityEntry entry = context.Entry(entity);
entry.CurrentValues.SetValues(yourObject);
...