8

I have found some information regarding this but not enough for me to understand what the best practice for is for this scenario. I have your typicaly TPH setup with an abstract base class "Firm". I have several children "Small Firm", "Big Firm" etc inheriting from Firm. In reality I actually have different realistic classifications for firms but I am trying to keep it simple in this example. In the database as per TPH I have a single Firm table with a FirmTypeId column (int) that differentiates between all these types. Everything works great except I have a requirement to allow a user to change one type of firm into another. For example a user might have made a mistake when adding the firm, and would like to change it from Big Firm to Small Firm. Because entity framework does not allow exposing the discriminating database column to be exposed as a property, I don't believe there is a way to change one type into another via EF. Please correct me if I am wrong. The way I see it I have two options:

  1. Don't use TPH. Simply have a Firm Entity and go back to using .Where(FirmTypeId == something) to differentiate between the types.
  2. Execute SQL directly using context.ExecuteStoreCommand to update the FirmTypeId column of the database.

I've seen a post where people suggest that One of the tenets of OOP is that instances cannot change their type. Although that makes perfect sense to me, I just don't seem to be able to connect the dots. If we were to follow this rule, then the only time to use any kind of inheritance (TPH/TPT) is when one is sure that one type would never be converted into another. So a Small Firm will never become a Big Firm. I see suggestions that composition should be used instead. Even though it doesn't make sense to me (meaning I don't see how a Firm has a Big Firm, to me a Big Firm is a Firm), I can see how composition can be modeled in EF if the data is in multiple tables. However in a situation where I have a single table in the database it seems it's TPH or what I've described in #1 and #2 above.

e36M3
  • 5,952
  • 6
  • 36
  • 47
  • That "someone" may have been me. In class-based OO, objects can't change types. Ever. C# doesn't allow this. The EF certainly does not add such a feature to C#. The only thing which is different in the EF is that the object's lifetime is longer -- effectively, forever. – Craig Stuntz Nov 03 '10 at 20:22
  • Craig, it might have been you. I just edited the post to include a little more information on this as well. If you don't mind reviewing the last paragraph in my post and shedding some light on the correct approach. – e36M3 Nov 03 '10 at 20:29
  • What's the difference between a "Small Firm" and a "Big Firm", exactly? BTW I don't get notified of your replies unless you put @Craig in them. – Craig Stuntz Nov 03 '10 at 20:33
  • @Craig, In my example there are actually no difference in terms of having extra columns. Another example would be to say that I have an Icecreame table with a FlavorId that is a foreign key to a table that has different flavors in it. I thought in my scenario instead of having to constantly write .Where(FlavorId == 1) or .Where(FlavorId == 2) I would simply strongly type them through inheritance using TPH. Of course someone might decide to change the flavor of the icecreame on the administration web page, so my design is SOL. – e36M3 Nov 03 '10 at 20:42
  • I agree that an object should not change its type in OO but I don't see any problem if there is a business requirement for doing so. @Craig: can you please give an example of a problem that someone might be hitting down the road by changing an object type? – Morteza Manavi Nov 03 '10 at 20:54
  • @Morteza, amongst other things you've just broken concurrency. One user attempts to change a type whilst a second user updates the object. Instead of an (expected) concurrency error, the application throws a nasty mapping error. There is no "business requirement to change types," ever. *Business* requirements don't specify C# classes; that's a technical requirement. The "business requirement" specifies the business need, not the class layout. If the class layout won't support the business need, it's broken. – Craig Stuntz Nov 03 '10 at 20:57
  • @em36M3, none of this sounds like a good use for inheritance. In OOP, classes define the *shape* and *behavior* of an instance, not the state of its attributes. – Craig Stuntz Nov 03 '10 at 21:00
  • @Craig, Ok so it's fair to say that inheritance should never be used if you come up with a use case that requires the type to be changed? Because Chocolate Icecream is an Icecream, but we're saying it should be modeled as Icrecream has a Flavor, that happens to be Chocolate. If all of this is true, then (as it relates to ORMs) I see no realistic uses for TPH or TPT. Can you give a scenario where you would in fact use inheritance with an ORM? – e36M3 Nov 03 '10 at 21:48
  • @Craig: I have to say that I'm agree with you, everything you said well makes sense. Now, given that he needs to change the type of his objects that coming from *Firm* table, his best solution would be to implement "Conditional Mapping" inside his model: One table, Multiple Entities, each take the records from the table that match their condition. What do you think? Thanks. – Morteza Manavi Nov 04 '10 at 00:07
  • @E36M3 I don't use (EF mapped) inheritance very often. One place I do use it is in collecting payroll time cards. All time cards capture employee ID, hours, etc. But some are special – they're for a fundamentally different kind of time record – and capture quite a bit more data. Inheritance lets me show the common data in a single list but get the extra data when I need it. – Craig Stuntz Nov 04 '10 at 01:00
  • @Morteza to be fair creating 3 conditional mapping entities that map to the same table is pretty much the same solution as TPH but without the explicit inheritance part. Heck I would think that SQL generated would be identical both ways. It feels like a hack solution. In order to change one entity into another I would need the same direct sql against the table, I really don't think anything changes. I really wish someone would lay out best practices for inheritance as well as for conditional mappings. When and where should they really be used. To me it seems they are for read only data. – e36M3 Nov 04 '10 at 02:48

4 Answers4

4

I've ran into this problem in our project, where we have core DBContext and some "pluggable" modules with their own DBContexts, in which "module user" inherits "core (base) user". Hope that's understandable.

We also needed the ability to change (let's call it) User to Customer (and if needed also to another "inherited" Users at the same time, so that user can use all those modules.

Because of that we tried using TPT inheritance, instead of TPH - but TPH would work somehow too.

One way is to use custom stored procedure as suggested by many people...

Another way that came to my mind is to send custom insert/update query to DB. In TPT it would be:

private static bool UserToCustomer(User u, Customer c)
    {
        try
        {
            string sqlcommand = "INSERT INTO [dbo].[Customers] ([Id], [Email]) VALUES (" + u.Id + ", '" + c.Email + "')";
            var sqlconn = new SqlConnection(ConfigurationManager.ConnectionStrings["DBContext"].ConnectionString);
            sqlconn.Open();
            var sql = new SqlCommand(sqlcommand, sqlconn);
            var rows = sql.ExecuteNonQuery();
            sqlconn.Close();

            return rows == 1;
        }
        catch (Exception)
        {
            return false;
        }
    }

In this scenario Customer inherits User and has only string Email.

When using TPH the query would only change from INSERT ... VALUES ... to UPDATE ... SET ... WHERE [Id] = .... Dont forget to change Discriminator column too.

After next call dbcontext.Users.OfType<Customer> there is our original user, "converted" to customer.


Bottomline: I also tried solution from another question here, which included detaching original entity (user) from ObjectStateManager and making new entity (customer) state modified, then saving dbcontext.SaveChanges(). That didn't work for me (neither TPH nor TPT). Either because using separate DBContexts per module, or because EntityFramework 6(.1) ignores this. It can be found here.

Community
  • 1
  • 1
podvlada
  • 315
  • 2
  • 10
  • 1
    Seeing this after some time, it would be cleaner to use **SQL parameters** in query instead of concatenating strings. – podvlada Feb 17 '15 at 14:33
1

Yes, you got it all right. EF inheritance does not support this scenario. The best way to change a Firm type for an existing Firm is to use a stored procedure.

Please take a look at this post for more info:
Changing Inherited Types in Entity Framework

Community
  • 1
  • 1
Morteza Manavi
  • 33,026
  • 6
  • 100
  • 83
  • 1
    That's the conclusions that I've arrived at. At this point however I am not sure that it's the right way of solving this problem, it seems that if one has this problem than they approached the problem incorrectly to begin with (as per Craig) – e36M3 Nov 03 '10 at 20:43
0

Unless you explicitly want to use the polymorphic functionality of the relational inheritance, then why not look at a splitting strategy?

http://msdn.microsoft.com/en-us/data/ff657841.aspx

Slappy
  • 4,042
  • 2
  • 29
  • 41
  • I am not sure how this really helps me. In my situation there is a Type column that discriminates whether an entity is of type A, B, or C. They really don't have attributes of their own. It smells of inheritance to me, because I could possibly add a few more properties to each of these different types to better describe the specifics of their types. However it does seem that inheritance should not be used where there is a business requirements to allow changing one type into another. – e36M3 Nov 04 '10 at 12:11
  • Well you can split type on a flag using one of these methods, then by switching the flag essentially you switch the type. And like I say, inheritance is only mandatory if you want to use polymorphism. So you essentially have 2 different types with similar properties, differing only by one or two properties. – Slappy Nov 05 '10 at 05:10
0

EDIT: APOLOGIES, THIS IS AN EF 6.x ANSWER

I'm posting example code for completeness. In this scenario, I have a base Thing class. Then, sub-classes: ActiveThing and DeletedThing

My OData ThingsController, has a main GetThings which I intend to only expose ActiveThings, but, it's GetThing(ThingId) can still return either type of object. The Delete action performs a conversion from ActiveThing to DeletedThing much in the way requested by the OP, and much in the manner described in other answers. I'm using inline SQL (parameterized)

public class myDbModel:DbContext
{
    public myDbModel(): base("name=ThingDb"){}

    public DbSet<Thing> Things { get; set; }  //db table

    public DbSet<ActiveThing> ActiveThings { get; set; } // now my ThingsController 'GetThings' pulls from this

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
       //TPH (table-per-hierarchy):
      modelBuilder.Entity<Ross.Biz.ThingStatusLocation.Thing>()
        .Map<Ross.Biz.ThingStatusLocation.ActiveThing>(thg => thg.Requires("Discriminator").HasValue("A"))
        .Map<Ross.Biz.ThingStatusLocation.DeletedThing>(thg => thg.Requires("Discriminator").HasValue("D"));
    }

}

Here's my updated ThingsController.cs

public class ThingsController : ODataController
{
    private myDbModel db = new myDbModel();

    /// <summary>
    /// Only exposes ActiveThings (not DeletedThings)
    /// </summary>
    /// <returns></returns>
    [EnableQuery]
    public IQueryable<Thing> GetThings()
    {
        return db.ActiveThings;
    }

    public async Task<IHttpActionResult> Delete([FromODataUri] long key)
    {
        using (var context = new myDbModel())
        {
            using (var transaction = context.Database.BeginTransaction())
            {
                Thing thing = await db.Things.FindAsync(key);
                if (thing == null || thing is DeletedThing) // love the simple expressiveness here
                {
                    return NotFound();//was already deleted previously, so return NotFound status code
                }

                //soft delete: converts ActiveThing to DeletedThing via direct query to DB
                context.Database.ExecuteSqlCommand(
                    "UPDATE Things SET Discriminator='D', DeletedOn=@NowDate WHERE Id=@ThingId", 
                    new SqlParameter("@ThingId", key), 
                    new SqlParameter("@NowDate", DateTimeOffset.Now)
                    );

                context.ThingTransactionHistory.Add(new Ross.Biz.ThingStatusLocation.ThingTransactionHistory
                {
                    ThingId = thing.Id,
                    TransactionTime = DateTimeOffset.Now,
                    TransactionCode = "DEL",
                    UpdateUser = User.Identity.Name,
                    UpdateValue = "MARKED DELETED"
                });
                context.SaveChanges();
                transaction.Commit();
            }
        }

        return StatusCode(HttpStatusCode.NoContent);
    }
}
bkwdesign
  • 1,953
  • 2
  • 28
  • 50