0

I have the following snippet.

public async Task<ActionResult> Register(RegisterViewModel model)
{
    if (!ModelState.IsValid) return View(model as LocalRegisterViewModel);
        var user = new User
    {
        UserId = model.Username,
        Password = null,
        Email = model.Email,
        AccType = model.AccountType
    };

    var modelAsLocalRegisterViewModel = model as LocalRegisterViewModel;
    if (modelAsLocalRegisterViewModel != null)
        user.Password = modelAsLocalRegisterViewModel.Password;
    //...
}

The classes looks as follows.

public class RegisterViewModel
{
    public string Username { get; set; }
    public string Email { get; set; }
    public int AccountType { get; set; }
}

public interface IInternalPassword
{
    string Password { get; set; }
    string ConfirmPassword { get; set; }
}

public class LocalRegisterViewModel : RegisterViewModel, IInternalPassword
{
    public string Password { get; set; }
    public string ConfirmPassword { get; set; }
}

The LocalRegisterViewModel is passed to the controller as follows from a cshtml page.

@model  LocalRegisterViewModel

@{
    ViewBag.Title = "Register";
    Layout = "~/Views/Shared/_LayoutAnonymous.cshtml";
}

<h2>@ViewBag.Title.</h2>

@using (Html.BeginForm("Register", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" }))

My problem is that, modelAsLocalRegisterViewModel is null after the safe cast.

    var modelAsLocalRegisterViewModel = model as LocalRegisterViewModel;
    if (modelAsLocalRegisterViewModel != null)
        user.Password = modelAsLocalRegisterViewModel.Password;

Can someone look into this and tell me why?

EDIT

Seems like my questioning style is bad. So let me clarify my exact intention as well. The Register action I have written is intended to serve multiple ViewModels, each having some additional info. So what I have done is writing a parent which carries the common attributes and extending that to get the added attributes. For an instance, I pass an instance of a LocalRegisterViewModel to the controller, so that it will first execute the common functionality and if the instance passed is of type LocalRegisterViewModel if will carry out the extended functionality. That's why I need to check the passed RegisteredViewModel is also of type LocalRegisterViewModel.

EDIT 2 This is not trying to assign a base class instance to a derived class reference. It's a fact that following is completely valid in C#.

class Program
{
    static void Test(Parent p)
    {
        var c = p as Child;
        Console.WriteLine(c == null ? "Can't do it!" : "Can do it!");
        Console.WriteLine(c.GetType().ToString());
    }

    static void Main(string[] args)
    {
        var c = new Child();
        Test(c);
    }
}

public class Parent
{
}

public class Child : Parent
{
}
Romeo Sierra
  • 1,666
  • 1
  • 17
  • 35
  • 1
    If you don't expect cast to fail - then don't use `as` operator, use `(LocalRegisterViewModel)model`. It will throw exception stating what exactly cannot be cast to what. – Evk Feb 20 '18 at 07:52
  • Pardon me for the incomplete question. I have just noticed the missing parts. I have updated it. – Romeo Sierra Feb 20 '18 at 07:55
  • I think `model` can never be `RegisteredViewModel`. It's parameter of asp.net mvc controller action, and it's bound from http request. So it will always be of type `RegisteredViewModel`, never of any other (child) type. – Evk Feb 20 '18 at 08:09
  • @Evk Any references that you can quote? – Romeo Sierra Feb 20 '18 at 08:11
  • No, I just cannot see how it can happen. Asp.net model binding doesn't work like this. – Evk Feb 20 '18 at 08:12
  • If the parameter is of type `RegisterViewModel` then the object will be of type `RegisterViewModel`. If you want it to be of type `LocalRegisterViewModel` then use `public async Task Register(LocalRegisterViewModel model)`. – mjwills Feb 20 '18 at 08:17
  • 2
    Your confusion I think comes from thinking that you somehow "calling" controller method, "passing" your model there. It's not exactly true. When you submit your form, it will post all values to server. Those values might include all properties of `LocalRegisterViewModel `, such as `Password`. When request comes to server, ASP.NET sees that related action (`Register`) accepts parameter of type `RegisterViewModel`. It will then create instance of that type and fill its parameters, ignoring all the rest (such as password). Http request does not contain information about "model" type. – Evk Feb 20 '18 at 08:39
  • 1
    @Romeo How would ASP.NET know which type to use? We're doing something like you want to do but on our WebAPI project, but we had to write JsonConverters to look at our own type field on the inbound object in order to decide which type to instantiate. It isn't done automagically. You need a model binder in MVC iirc. – ProgrammingLlama Feb 20 '18 at 08:41
  • @Evk Your comment is the answer for my question. :) Can you post it as an answer please? – Romeo Sierra Feb 20 '18 at 08:53
  • `This is not trying to assign a base class instance to a derived class reference.` That is **exactly** what it is. – mjwills Feb 20 '18 at 09:27

2 Answers2

2

Right; so if:

var modelAsLocalRegisterViewModel = model as LocalRegisterViewModel;

gives null, then there are exactly 2 options:

  • model is null
  • model is something, but something other than LocalRegisterViewModel

So: you'll need to look at model and find out what it is. We can't tell you that: it isn't in the code shown. But string typeName = model?.GetType()?.Name; should tell you which; it'll return either null or the name of the type that model is.


With the recent edit, we can see that model is a RegisterViewModel; but: it sounds like it isn't a LocalRegisterViewModel. Since there is an inheritance tree, it sounds like model is either the base-type (RegisterViewModel) or a different sub-type unrelated to LocalRegisterViewModel.

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • Improved the question further. Mind having another look? – Romeo Sierra Feb 20 '18 at 08:16
  • 1
    @RomeoSierra you still haven't shown us what is calling `public async Task Register(RegisterViewModel model)`; if that is a top-level "action" on the controller, then MVC won't know that you actually wanted a `LocalRegisterViewModel`, so it won't **be** a `LocalRegisterViewModel` - it will be simply a `RegisterViewModel`. If it isn't MVC doing this: then please show us the code where you pass an object to that method, so we can see **what you are passing**. – Marc Gravell Feb 20 '18 at 08:25
  • Yes it is an action of a controller. So what you are suggesting is that, no matter what subtype I pass into the controller, it will always accept it as of type `RegisterViewModel` isn't it? – Romeo Sierra Feb 20 '18 at 08:34
  • 1
    Yes, unless you build a custom model binder that is correct @RomeoSierra. – mjwills Feb 20 '18 at 08:39
  • @RomeoSierra where did you pass it into the controller? – Marc Gravell Feb 20 '18 at 15:39
  • @MarcGravell I have been passing `RegisterViewModel` in the action definition. But in my view, what is bound to it is a `LocalRegisterViewModel` which is a child of `RegisterViewModel`. Got my problem figured out. It is a problem with how I understood the fundamental semantics of ASP.NET MVC... – Romeo Sierra Feb 21 '18 at 07:50
  • @RomeoSierra so again: it is a `RegisterViewModel` in the view; now: what is it that you are expecting to make it *actually* be a `LocalRegisterViewModel`? please be explicit; because without a good reason: it is going to be a `RegisterViewModel` - do you have a reason to expect the incoming value to *actually* be something different? – Marc Gravell Feb 21 '18 at 09:02
  • Yup! I got the problem there figured out. Thank you very much for your efforts... :) – Romeo Sierra Feb 22 '18 at 04:38
2

Your confusion I think comes from thinking that you are "calling" controller method Register() from cshtml page, and "passing" your model there. It's not exactly true.

When you submit your form, it will post all inputs to server, to the specified url. Those inputs might include properties of LocalRegisterViewModel, such as Password. Request body might look like this:

{"email": "my@email.com", "password": "bla" }

When request comes to server, ASP.NET looks for controller action matching given url. It sees that matching action is Register() and this action accepts parameter of type RegisterViewModel. Now it tries to bind that model (fill its properties from http request). It has absolutely no idea that there are additional values, such as Password, in incoming request.

So asp.net will create instance of RegisterViewModel and fill its properties, ignoring all the rest (such as password), because there is no information in request itself about which C# type it should be parsed into.

Evk
  • 98,527
  • 8
  • 141
  • 191