Polymorphism in Entity Framework

2019-05-16 08:40发布

问题:

The concrete classes (BankAccount and CreditCard) are not visible on controller.

I'm stuck with this issue.

I'm using the example from this site:

http://weblogs.asp.net/manavi/archive/2010/12/28/inheritance-mapping-strategies-with-entity-framework-code-first-ctp5-part-2-table-per-type-tpt.aspx

The view

The CreateUser:

If the CreditCard was selected it should be associated to the User class.

The diagram

The code

UserController:

    [HttpPost]
    public ActionResult Create(User user)//The Watch above came from this user instance
    {
        if (ModelState.IsValid)
        {

            context.User.Add(user);
            context.SaveChanges();
            return RedirectToAction("Index");  
        }

        ViewBag.PossibleBillingDetail = context.BillingDetail;
        return View(user);
    }

User\_CreateOrEdit.cshtml:

User\Create.cshtml:

    @model TPTMVC.Models.User
    @using TPTMVC.Models;

<script src="http://ajax.microsoft.com/ajax/jQuery/jquery-1.5.js" type="text/javascript"></script> 
<script type="text/javascript">

    $(document).ready(function () {
        $('.divbank').hide();
        $('input[type=radio]').live('change', function () { updateweather(); });
    });

    function updateweather() {
        //alert();
        if ($('input[type=radio]:checked').val() == 'Bank') {
            $('.divcard').fadeOut(1000);
            $('.divcard').hide();
            $('.divbank').fadeIn(1000);
        }
        else {
            $('.divbank').fadeOut(1000);
            $('.divbank').hide();
            $('.divcard').fadeIn(1000);
            }

    }
        </script>
    <div id="json"></div>

@{
    ViewBag.Title = "Create";
}

<h2>Create</h2>

@using (Html.BeginForm())
{
@Html.ValidationSummary(true)
<fieldset>
    <legend>User</legend>

        @Html.Partial("_CreateOrEdit", Model)

        <div ='none' class="divcard">
            <div class="editor-label">
                @Html.LabelFor(model => ((CreditCard)model.billingDetail).ExpiryMonth)
            </div>
            <div class="editor-field">
                @Html.EditorFor(model => ((CreditCard)model.billingDetail).ExpiryMonth)
                @Html.ValidationMessageFor(model => ((CreditCard)model.billingDetail).ExpiryMonth)
            </div>

             <div class="editor-label">
                @Html.LabelFor(model => ((CreditCard)model.billingDetail).ExpiryYear)
            </div>
            <div class="editor-field">
                @Html.EditorFor(model => ((CreditCard)model.billingDetail).ExpiryYear)
                @Html.ValidationMessageFor(model => ((CreditCard)model.billingDetail).ExpiryYear)
            </div> 
        </div>

        <div='none' class="divbank">
            <div class="editor-label">
                @Html.LabelFor(model => ((BankAccount)model.billingDetail).BankName)
            </div>
            <div class="editor-field">
                @Html.EditorFor(model => ((BankAccount)model.billingDetail).BankName)
                @Html.ValidationMessageFor(model => ((BankAccount)model.billingDetail).BankName)
            </div>

             <div class="editor-label">
                @Html.LabelFor(model => ((BankAccount)model.billingDetail).Swift)
            </div>
            <div class="editor-field">
                @Html.EditorFor(model => ((BankAccount)model.billingDetail).Swift)
                @Html.ValidationMessageFor(model => ((BankAccount)model.billingDetail).Swift)
            </div> 
        </div>  
    <p>
        <input type="submit" value="Create" />
    </p>
</fieldset>
}

<div>
    @Html.ActionLink("Back to List", "Index")
</div>

Classes code:

namespace TPTMVC.Models{
public class BillingDetail
{
    [Key]
    [ForeignKey("user")]
    public int UserID { get; set; }
    public string Owner { get; set; }
    public string Number { get; set; }
    public virtual User user { get; set; }
}}

namespace TPTMVC.Models{
public class User
{
    public int UserId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public virtual BillingDetail billingDetail { get; set; }
}}
namespace TPTMVC.Models{
    [Table("BankAccounts")]
    public class BankAccount:BillingDetail
    {
        public string BankName { get; set; }
        public string Swift { get; set; }
    }}
namespace TPTMVC.Models{
[Table("CreditCards")]
public class CreditCard:BillingDetail
{
    public int CardType { get; set; }
    public string ExpiryMonth { get; set; }
    public string ExpiryYear { get; set; }
}}

The problem

When I click the create button, I get this result:

I selected a CreditCard but the result was BillingDetail. I tried to make a casting but I got a error, as you can see.

:(

Why only BillingDetail appear on UserController?

My first solution

[HttpPost]
        public ActionResult Create(User user, CreditCard card, BankAccount bank, String Radio)
        {
            //String teste=formCollection["Radio"];
            if (ModelState.IsValid)
            {
                switch (Radio)
                {
                    case "CredCard":
                        user.billingDetail = card;
                        break;
                    case "Bank":
                        user.billingDetail = bank;
                        break;
                }
                context.User.Add(user);
                context.SaveChanges();
                return RedirectToAction("Index");  
            }

            ViewBag.PossibleBillingDetail = context.BillingDetail;
            return View(user);
        }

回答1:

You are passing a User object to your View. This has a navigation property to BillingDetail which can be a CreditCard or a BankAccount. You cast it like this in the View (CreditCard)model and (BankAccount)model. It will work when your creating because you are casting an instance that is null, but it will cause a run-time error when you have a non-null instance because one of the casts will fail.

To fix that you can use model as CreditCard and model as BankAccount then check they are not null before you render the appropriate editors. But you'll need to work out what to do when your user wants to change the payment method.

When the form is returned to the controller, because your Create method signature takes a User parameter, the default ModelBinder knows that it should instantiate a User. It is quite capable of that, but it is not able to work out what to do with the values that appear in the FormCollection that relate to the BillingDetail.

With inheritance you can't rely on the default ModelBinder. You need to work out what suits you best. Here's some references I found useful:

Get an understanding of ModelBinding

Custom model binders - one person's opinion

The solution I went with - but look at all the other solutions here too!

Here's some example code from my project that should give you an idea:

public ActionResult CreateOrEdit(FormCollection values)
{
    //The FormCollection is either a Property or a Block
    BaseProperty model;
    if (values["PropertyTypeID"] != null)
    {
        //it must be a Property!
        Property property = new Property();
        TryUpdateModel(property);
        _Uow.PropertyRepository.InsertOrUpdate(property);
        model = property;
    }
    else
    {
        Block block = new Block();
        TryUpdateModel(block);
        _Uow.BlockRepository.InsertOrUpdate(block);
        model = block;
    }
    //etc....


回答2:

I think there are a few things wrong here:

  1. You are not adhering to separation of concerns. If the model diagram you provided is for your entities you shouldn't be using them as front-end models. Your data layer and view layer should have separate models -- this lets you decouple the way your data is designed versus what the user is interacting with.
  2. SO users please correct me if I'm wrong, but you can't return concrete server-side objects in webpage data. In this case you are attempting to cast BillingDetail, a c# class, into a model for your view and then return it with form submission. As far as I know you can only return plain data and form fields in a form submit. You can have your view model contain other view models and concrete classes, but you can only return plain fields and view models with plain fields in them.
  3. You are attempting to cast a base class into a derived class. This is possible when you have passed a derived class as a base class and then re-cast it somewhere else, but you can't take a pure base class and transform it into a more specific object. It's like trying to force a rectangle to be a square.

Solution wise you should do this:

  • Create 2 separate view models for CreditCard and BankAccount, each with their respective properties. (You should do the same for your User object so you adhere to SoC)
  • Populate your view using the model with the two new view models in lieu of BillingDetail.
  • When your form is submitted use your radio buttons as a conditional in your controller to determine which type of payment the user chose and then create the respective object, map the view model properties to the concrete object, add it to your user, and then save.


回答3:

Although I agree with Matt, it's usually a good idea to work with view models, the direct cause of your issue is in the line

ViewBag.PossibleBillingDetail = context.BillingDetail;

This also includes BankAccounts, so some BillingDetail objects can't be cast to CreditCard.

Replace the line by

ViewBag.PossibleBillingDetail = context.BillingDetail.OfType<CreditCard>();