How to generalise access to DbSet<TEntity> members of a DbContext?

空扰寡人 提交于 2019-12-04 04:03:53

Some generalization is possible by using

var dbSet = _dbContext.Set<T>

and putting most of your method in a method with a generics type parameter.

However, there should be a switch somewhere to decide which type should be specified and which type to create, because I think the type is supplied as a property of the model (is it?). So it probably won't really look elegant, but probably be a lot shorter, with DRY-er code.

yoel halb

To add on Gert Arnold's answer, I want to note that there is another method overload on the dbContext that returns a general DbSet from a type object:

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

If you want to add blind an object, then create the object using the set.Create() method, or if you already have an object created with the "new" keyowrd, you can convert it by using (similar to this answer)

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

I've been looking for an answer to this question and I've found that it is easy to do using the Managed Extensibility Framework. There is a quicker way at the bottom of this post, however MEF allows for a much more scalable approach.

MEF allows you to build dynamic access plugins from disparate Assemblies; however it can be used to quickly populate Collections within a single assembly application.In essence, we'll be using it as a safe way of reflecting our assembly back into the class. In order to make this fully functional, I'm also going to implement the Strategy Pattern to the Entity Framework Model.

Add a reference to your project, pointing to System.ComponentModel.Composition. This will give access to the MEF library.

Now, we need to implement the Strategy Pattern. If you don't have an Interfaces folder, create one, and add IEntity.cs, as below.

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; }
    }
}

Now, each of you concrete entities need to implement this Interface:

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...
}

I find that it is best practice, not to create individual interfaces for each entity, unless you're working in a high testing environment. Pragmatically, interfaces should only be used where that level of abstraction is needed; mainly when more than one concrete class will inherit, or when working with an over-enthusiastic Inversion of Control engine. If you have interfaces for everything in your production model, your architecture more than likely, has major flaws. Anyway, enough of the rambling.

Now that we have all of our entities "strategised", we can use MEF to collate them and populate a collection within your context.

Within your context, add a new property:

/// <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; }

The [ImportMany(typeof(DbSet<IEntity>))] here, allows MEF to populate the collection.

Next, add the corresponding Export attribute to each DbSet within the context:

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

Each of the Imported and Exported properties is known as a "part". The final piece to the puzzle is to compose those parts. Add the following to your context's constructor:

// 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); 

Now, with any luck, you should have a dynamically populated list of DbSets, within your context.

I have used this method to allow easy truncating of all tables via an extension method.

/// <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;
    }
}

I have added a method to the context to truncate the whole database.

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

EDIT (Overhaul):

The solution above has now been depreciated. Some tweeking as had to be done to get this to work now. 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.

    #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

Next, run the CompileSetsList() facade from the constructor (with best practices for Web shown):

    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();
    }

Then, just decorate your DbSet<>s like this:

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

Now it will work properly.