9

How would I go about passing an entity type as a parameter in linq?

For e.g. The method will receive the entity name value as a string and I would like to pass the entity name to the below linq query. Is it possible to make the linq query generic ?

public ActionResult EntityRecords(string entityTypeName)
{
    var entityResults = context.<EntityType>.Tolist();
    return View(entityResults);
}

I would like to pass the Entity type as a parameter and return all property values.

Also, is it possible to filter results based the some property?

smolchanovsky
  • 1,775
  • 2
  • 15
  • 29
user793468
  • 4,898
  • 23
  • 81
  • 126
  • So ideally you want to dynamically generate the linq expression? Please clarify exactly what it is you are trying to do. – Nkosi Oct 22 '18 at 14:09
  • How will the name be passed. Fully qualified name? – Nkosi Oct 22 '18 at 14:15
  • Can you rephrase your question a little? because either you're asking something that has a simple answer, or is really difficult, i.e. using linq to dynamically build queries..? Passing parameters is easy, i.e. bool value = true; var er = context.Table.Where(r => r.Prop1 == value); – Phill Oct 22 '18 at 14:32
  • @Nkosi Basically, I want to make the Linq query generic for entity types, So I can pass in the entity name and linq will output the results – user793468 Oct 22 '18 at 14:36
  • @user793468 Show how you would expected to call your desired API. Should help clarify what it is you are asking. – Nkosi Oct 22 '18 at 14:37
  • @Phill I want to pass the entity type as the parameter, so make the linq query generic so whichever entity type is passed to the method, the linq query will output records for that entity – user793468 Oct 22 '18 at 14:38
  • @Nkosi I updated my question above, does this clear any confusions? – user793468 Oct 22 '18 at 14:46
  • It does clarify your intentions. It also raises another set of questions/concerns though. But i'll tackle those later – Nkosi Oct 22 '18 at 14:50
  • Have you considered using `OData`? That's exactly what it does, Accessing your model online via API. See https://learn.microsoft.com/en-us/aspnet/web-api/overview/odata-support-in-aspnet-web-api/odata-v4/create-an-odata-v4-endpoint – vendettamit Oct 22 '18 at 22:03

4 Answers4

4

Assuming your context class is looking like this:

public class MyContext : DbContext
{
    public DbSet<Entity1> Entity1 { get; set; }
    public DbSet<Entity2> Entity2 { get; set; }

    // and so on ...
}

simplest solution is to write method that looks like

private List<object> Selector(string entityTypeName)
{
  if (entityTypeName == "Entity1") 
    return context.Entity1.ToList();

  if (entityTypeName == "Entity2")
    return context.Entity2.ToList()

  // and so on  

  // you may add a custom message here, like "Unknown type"
  throw new Exception(); 
}

But we don't want to hardcode this stuff, so let create Selector dynamically with Linq.Expressions

Define a Func field within your controller:

private readonly Func<string, List<object>> selector;

Now you can create a factory for this member:

private Func<string, List<object>> SelectByType()
{
    var myContext = Expression.Constant(context);
    var entityTypeName = Expression.Parameter(typeof(string), "entityTypeName");

    var label = Expression.Label(typeof(List<object>));
    var body = Expression.Block(typeof(MyContext).GetProperties()
        .Where(p => typeof(IQueryable).IsAssignableFrom(p.PropertyType) && p.PropertyType.IsGenericType)
        .ToDictionary(
            k => Expression.Constant(k.PropertyType.GetGenericArguments().First().Name),
            v => Expression.Call(typeof(Enumerable), "ToList", new[] {typeof(object)}, Expression.Property(myContext, v.Name))
        )
        .Select(kv =>
            Expression.IfThen(Expression.Equal(kv.Key, entityTypeName),
              Expression.Return(label, kv.Value))
        )
        .Concat(new Expression[]
        {
            Expression.Throw(Expression.New(typeof(Exception))),
            Expression.Label(label, Expression.Constant(null, typeof(List<object>))),
        })
    );

    var lambda = Expression.Lambda<Func<string, List<object>>>(body, entityTypeName);
    return lambda.Compile();
}

and assign Func with it (somewhere in constructor)

selector = SelectByType();

Now you can use it like

public ActionResult EntityRecords(string entityTypeName)
{
    var entityResults = selector(entityTypeName);
    return View(entityResults);
}
Aleks Andreev
  • 7,016
  • 8
  • 29
  • 37
4

You have two options:

Option 1: You know the entity type at compile time

If you know the entity type at compile time, use a generic method:

public ActionResult EntityRecords<TEntity>()
{
    var entityResults = context.Set<TEntity>.ToList();
    return View(entityResults);
}

Usage:

public ActionResult UserRecords()
{
    return EntityRecords<User>();
}

Option 2: You know the entity type only at runtime

If you actually want to pass the entity type as a string, use the other overload of Set that takes a type:

public ActionResult EntityRecords(string entityType)
{
    var type = Type.GetType(entityType);
    var entityResults = context.Set(type).ToList();
    return View(entityResults);
}

This assumes that entityType is a fully qualified type name including assembly. See this answer for details.
If the entities are all inside the same assembly as the context - or in another well known assembly - you can use this code instead to get the entity type:

var type = context.GetType().Assembly.GetType(entityType);

This allows you to omit the assembly in the string, but it still requires the namespace.

Daniel Hilgarth
  • 171,043
  • 40
  • 335
  • 443
1

You can achieve what you want even if the context doesn't have DbSet properties (and if it does, that doesn't harm). It is by calling the DbContext.Set<TEntity>() method by reflection:

var nameSpace = "<the full namespace of your entity types here>";

// Get the entity type:
var entType = context.GetType().Assembly.GetType($"{nameSpace}.{entityTypeName}");

// Get the MethodInfo of DbContext.Set<TEntity>():
var setMethod = context.GetType().GetMethods().First(m => m.Name == "Set" && m.IsGenericMethod);
// Now we have DbContext.Set<>(), turn it into DbContext.Set<TEntity>()
var genset = setMethod.MakeGenericMethod(entType);

// Create the DbSet:
var dbSet = genset.Invoke(context, null);

// Call the generic static method Enumerable.ToList<TEntity>() on it:
var listMethod = typeof(Enumerable).GetMethod("ToList").MakeGenericMethod(entType);
var entityList = listMethod.Invoke(null, new[] { dbSet });

Now you've got your list of entities.

One remark: To get rid of some performance impact due to reflection you could cache some types and non-generic method infos.

Another remark: I don't think I would recommend this. As said in a comment: this raises a couple of concerns. For example: are you going to allow a client application to get all unfiltered data of any entity table? Whatever it is you're doing: handle with care.

Gert Arnold
  • 105,341
  • 31
  • 202
  • 291
0

In your example, it looks like you have a controller action that's taking the entity name as a parameter, so you won't be able to make your method generic. But you can use reflection and avoid the use of generics for the most part.

public ActionResult EntityRecords(string entityTypeName)
{
    var entityProperty = context.GetType().GetProperty(entityTypeName);
    var entityQueryObject = (IQueryable)entityProperty.GetValue(context);
    var entityResults = entityQueryObject.Cast<object>().ToList();
    return View(entityResults);
}

There are a few things to keep in mind, though:

  1. The assumption is that you've got a property on your context corresponding to the given entityTypeName argument. If entityTypeName is actually the type name instead of the property name, you'll need to do extra work to find the appropriate property.
  2. Your View will have to know what to do with a collection of objects where the type of the objects is not known at compile time. It'll probably have to use reflection to do whatever you intend for it to do.
  3. There may be some security concerns in a method like this. For example, if the user provides "Database" or "Configuration", you could end up exposing information like your connection string, which has nothing to do with the actual entities you've stored.

Also, is it possible to filter results based the some property?

Yes, and it will involve a similar use of reflection and/or dynamic. You could use a library like Dynamic LINQ to pass strings into LINQ-like method overloads (Where, Select, etc.).

public ActionResult EntityRecords(string entityTypeName, FilterOptions options)
{
    var entityProperty = context.GetType().GetProperty(entityTypeName);
    var entityQueryObject = entityProperty.GetValue(context);
    var entityResults = ApplyFiltersAndSuch((IQueryable)entityQueryObject);
    return View(entityResults);
}

private IEnumerable<object> ApplyFiltersAndSuch(IQueryable query, FilterOptions options)
{
    var dynamicFilterString = BuildDynamicFilterString(options);
    return query.Where(dynamicFilterString)
        // you can add .OrderBy... etc.
        .Cast<object>()
        .ToList();
}
StriplingWarrior
  • 151,543
  • 27
  • 246
  • 315
  • I am getting the following error at entityResults - Unable to cast the type 'Entity.Employee' to type 'System.Object'. LINQ to Entities only supports casting EDM primitive or enumeration types. – user793468 Oct 22 '18 at 17:07
  • You mention there being a security concern; how would you better design this to avoid such security concerns? – user793468 Oct 22 '18 at 17:07
  • @user793468: It depends on your security situation. You could, for example, have a white-list of `entityTypeName` values that the client is allowed to provide, and if they provide anything outside of that list of values they get blocked. You could set up an intermediate layer of DTOs that people are allowed to query against, to ensure that they're seeing only the properties which you intended to expose from this controller action. – StriplingWarrior Oct 22 '18 at 18:25
  • I only brought up the security issue because most of the time people reuse the EF context for a variety of data, but they only mean to expose certain parts of that data from specific endpoints. By taking unsanitized user input and using it to expose reflected property values, you'd be creating an endpoint from which someone could access all the data associated with your entire EF context. If that's what you intend, then great; if not, then you need to figure out a way to restrict things to only show them what they should see. – StriplingWarrior Oct 22 '18 at 18:28