How to Concat Classes with Inheritance and same Table in Database with Linq and EntityFramework 6?

asp.net asp.net-mvc-5 c# entity-framework-6 linq

Question

I have a project written in , and and I have the following classes:

public class Profession
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }  
    public string Description { get; set; }
}

public class Person
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }  
    public string Name { get; set; }
}

public class Individuals : Person
{
    public string CPF { get; set; }
    public int Profession_Id { get; set; }
    [ForeignKey("Profession_Id")]
    public Profession Profession { get; set; }
}

public class Corporate : Person
{
    public string CNPJ { get; set; }
}

And my DbContext class is:

public class Context : DbContext
{
    public Context() : base("AppConnectionString") { }

    public DbSet<Profession> Professions { get; set; }
    public DbSet<Person> People { get; set; }
    public DbSet<Individuals> Individuals { get; set; }
    public DbSet<Corporate> Corporations { get; set; }
}

At running my application, the database is generated with the following structure:
The Table People and columns Id, Name, CNPJ, CPF and Discriminator. And Table Professions with Id and Description columns.

This is my example project:

public class Program
{
    public static void Main(string[] args) {
        var context = new Context();
        GenerateDatabase(context);

        // display the data

        System.Console.ReadKey();
    }

    private static void GenerateDatabase(Context context) 
    {
        if (!context.Database.Exists()) 
        {
            context.Database.Create();

            var p1 = new Profession { Description = "Profission 1" };
            var p2 = new Profession { Description = "Profission 2" };
            context.Professions.Add(p1);
            context.Professions.Add(p2);

            context.SaveChanges();

            context.Individuals.Add(new Individuals { Name = "People 1", CPF = "CPF Teste 1", Profession_Id = p1.Id });
            context.Individuals.Add(new Individuals { Name = "People 2", CPF = "CPF Teste 2", Profession_Id = p2.Id });
            context.Corporations.Add(new Corporate { Name = "Person 1", CNPJ = "CNPJ Teste 1" });
            context.Corporations.Add(new Corporate { Name = "Person 2", CNPJ = "CNPJ Teste 2" });

            context.SaveChanges();
        }
    }
}

Well, in the Main method I have the following code snippet to display data:

var items = context.People.OfType<Individuals>();
foreach (var item in items)
    System.Console.WriteLine(item.Id.ToString() + " | " + item.Name + " | " item.Profession.Description + " | " + item.GetType().Name);

var items2 = context.People.OfType<Corporation>();
foreach (var item in items2)
    System.Console.WriteLine(items2.Id.ToString() + " - " + items2.Name);

I need a concatenation to sort results.
In this example the sorting by the Name property of the class does not have a correct result.

+----+----------+--------------+--------------+
| ID | Name     | Profession   | Discriminator|
+----+----------+--------------+--------------+
|  1 | Person 1 | Profission 1 | Individuals  |
|  2 | Person 2 | Profission 1 | Individuals  |
|  3 | Person 1 |              | Corporate    |
|  4 | Person 2 |              | Corporate    |
+----+----------+--------------+--------------+

I need to gather the records of the two type to display in a query page. So was trying to Concat, as told to do a Union all.

Soon, I tested something like:

var items = context.People.OfType<Individuals>().Include(x => x.Profession).Concat<Individuals>(
    context.People.OfType<Corporate>().Select(x => new Corporate
    {
        Id = x.Id,
        Name = x.Name,
        CNPJ = x.CNPJ
    }));

items = items.OrderBy(x => x.Name);

But this is wrong and doesn´t compile!

How can this be done using Queryable?

1
0
4/22/2014 12:58:25 AM

Accepted Answer

The first-level solution is to cast both Individuals and Corporate back to the base class, like this:

var items = 
    context.Individuals.Select(x => (Person)x)
    .Concat(context.Corporations.Select(x => (Person)x))
    .OrderBy(x => x.Name);

This will give you an enumeration of Person objects, which could be either sub-class: Individuals or Corporate. Casting to the common base class solves the compatibility issue, but now you're restricted to using only those properties available on the base class. You'll need to do type checks on each item to figure out which concrete type it is if you need to access properties specific to one or other sub-class (the Profession reference on an Individuals object for instance).

Simple display:

foreach (var person in items)
{
    Console.WriteLine("{0,-6}{1,-12}{2}", person.Id, person.Name, person.GetType().Name);
}

With the data above this should display:

1     Person 1    Individuals
3     Person 1    Corporate
2     Person 2    Individuals
4     Person 2    Corporate

As discussed in the comments, the casting of the objects to the Person type gets done before the Include() statement in my original query, which causes an error at runtime when Entity Frameworks can't find the Profession property of the People class.

I cam see three ways to deal with this:

  1. Let EF handle it as a lazy load.

  2. Add an operation that forces the data into memory before the conversion

  3. Use another type for the results.

The first option is to not include the Profession property in the query at all and let Entity Frameworks do the work for you. This is the lazy method for quick-n-dirty queries that you're expecting to return only a handful of records. Whenever you reference the Profession property of an Individuals object for the first time EF will query the database server for it. For large collections of records this is A Bad Idea. If you had 1000 Individuals records, that's an extra 1000 SQL queries that have to be run.

Second option looks like this:

var items = 
    context.Individuals.Include("Profession").ToArray().Select(x => (Person)x)
    .Concat(context.Corporations.Select(x => (Person)x)
    .OrderBy(x => x.Name);

The ToArray operation in the midst of the expression will tell EF to stop building the query and go grab the data, returning a Individuals[] array. Unfortunately this is pretty inefficient for large data sets.

And the final option:

var items =
    context.Individuals.Select(x => new 
        { 
            Id = x.Id, 
            Name = x.Name, 
            Profession = x.Profession.Description, 
            Type = "Individual" 
        })
    .Concat(context.Corporations.Select(x => new 
        { 
            Id = x.Id, 
            Name = x.Name, 
            Profession = (string)null, 
            Type = "Corporation" 
        }))
    .OrderBy(x => x.Name).ThenBy(x => x.Id);

I've used compatible anonymous types - same field names and types in the same order - but you could also use a specific result record structure or class. Use anonymous types if all the processing is happening locally, or result struct/class if you'll be passing the information outside of the method.

In general I think that the last one is the best of the 3. Use an intermediate type to store the results and you'll get a much better result than if you play around with type casting, forcing data into memory, etc. You also get a much more efficient query, since only those fields you request in your queries will be returned from the server. Great when you only want two columns from a table with lots and lots of columns. When you get to a table design with 50+ columns it makes a lot of difference.

Incidentally, my original idea of printing person.GetType().Name was a bit broken. EF subclasses your classes, so the types - and therefore the names - you get out will be ugly mashups. The third option solves this by explicitly setting the values.

0
4/22/2014 2:56:17 AM


Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow