Neil
Neil

Reputation: 611

How do I use base classes across my view models and services in MVC?

Background

Using MVC in Asp.Net Core, I'm using the controller -> service -> view model -> view approach.

I want 2 services to share some base data and functionality.

My requirement is for one entity to be shared by 2 different user types. I.e. An admin user and a standard user.

The admin user will have access to additional properties (defined on the view model) and functionality (defined on the service) whereas the standard user won't.

In the example code below, Foo1 is the equivalent to an admin and Foo2 is a standard user.

Controller actions

private readonly IFoo1Service _foo1Service;
private readonly IFoo2Service _foo2Service;

public HomeController
    (IFoo1Service foo1Service,
    IFoo2Service foo2Service)
{
    _foo1Service = foo1Service;
    _foo2Service = foo2Service;
}

public IActionResult Foo1()
{
    Foo1ViewModel vm = _foo1Service.NewViewModel();
    return View(vm);
}

public IActionResult Foo2()
{
    Foo2ViewModel vm = _foo2Service.NewViewModel();
    return View(vm);
}

Services

public class Foo1Service : BaseFooService, IFoo1Service
{
    public Foo1ViewModel NewViewModel()
    {
        //*** LINE CAUSING THE ERROR ***
        //NewBaseFooViewModel returns a BaseFooViewModel
        //AS Foo1ViewModel derives from the BaseFooViewModel
        //I thought I could cast it
        Foo1ViewModel vm = (Foo1ViewModel) NewBaseFooViewModel();
        //set some defaults
        vm.Foo1Field1 = "Foo1Field1";
        vm.Foo1Field2 = "Foo1Field2";
        return vm;
    }

    public Foo1ViewModel GetViewModelFromEntity(Entity entity)
    {
        Foo1ViewModel vm = (Foo1ViewModel) GetBaseFooViewModelFromEntity(entity);
        vm.Foo1Field1 = entity.Foo1Field1;
        vm.Foo1Field2 = entity.Foo1Field2;
        return vm;
    }
}

public class Foo2Service : BaseFooService, IFoo2Service
{
    public Foo2ViewModel NewViewModel()
    {
        Foo2ViewModel vm = (Foo2ViewModel) NewBaseFooViewModel();
        return vm;
    }

    public Foo2ViewModel GetViewModelFromEntity(Entity entity)
    {
        Foo2ViewModel vm = (Foo2ViewModel) GetBaseFooViewModelFromEntity(entity);
        return vm;
    }
}

public class BaseFooService : IBaseFooService
{
    public BaseFooViewModel NewBaseFooViewModel()
    {
        return new BaseFooViewModel()
        {
            BaseFooField1 = "BaseFooField1",
            BaseFooField2 = "BaseFooField2",
            BaseFooField3 = "BaseFooField3"
        };
    }

    public BaseFooViewModel GetBaseFooViewModelFromEntity(Entity entity)
    {
        return new BaseFooViewModel()
        {
            BaseFooField1 = entity.BaseFooField1,
            BaseFooField2 = entity.BaseFooField2,
            BaseFooField3 = entity.BaseFooField3
        };
    }
}

Interfaces

public interface IFoo1Service : IBaseFooService
{
    Foo1ViewModel NewViewModel();
}

public interface IFoo2Service : IBaseFooService
{
    Foo2ViewModel NewViewModel();
}

public interface IBaseFooService
{
    BaseFooViewModel NewBaseFooViewModel();
}

View Models

public class Foo1ViewModel : BaseFooViewModel
{
    public string Foo1Field1 { get; set; }
    public string Foo1Field2 { get; set; }
}

public class Foo2ViewModel : BaseFooViewModel
{

}

public class BaseFooViewModel
{
    public string BaseFooField1 { get; set; }
    public string BaseFooField2 { get; set; }
    public string BaseFooField3 { get; set; }
}

Views

Foo1

@model  BaseServiceSample.ViewModels.Foo.Foo1ViewModel

<h1>Base foo fields</h1>
<p>@Model.BaseFooField1</p>
<p>@Model.BaseFooField2</p>
<p>@Model.BaseFooField3</p>

<h2>Foo1 fields</h2>
<p>@Model.Foo1Field1</p>
<p>@Model.Foo1Field2</p>

Foo2

@model BaseServiceSample.ViewModels.Foo.Foo2ViewModel

<h1>Base foo fields</h1>
<p>@Model.BaseFooField1</p>
<p>@Model.BaseFooField2</p>
<p>@Model.BaseFooField3</p>

Dependency injection in startup

services.AddScoped<IFoo1Service, Foo1Service>();
services.AddScoped<IFoo2Service, Foo2Service>();

Issue

The application compiles okay but I'm getting the error at run time:

InvalidCastException: Unable to cast object of type 'BaseServiceSample.ViewModels.Foo.BaseFooViewModel' to type 'BaseServiceSample.ViewModels.Foo.Foo1ViewModel'

See my comments within Foo1Service which are above the line in the code causing the error at run time.

I thought that if a class derived from a base class that it could be casted as the derived class but I'm probably confusing this with how model binding in MVC can work.

Question

How can I change my code so that it supports the basic requirement of a base view model and base service which manage the shared properties and functionality for 2 different user groups, but allowing a user group to extend those properties / functionality?

From my research it looks like I might need to use abstract classes or type arguments but I've not been able to get this working.

I've included the code sample without my attempts at providing type arguments to keep the code more simple and hopefully easier for someone to guide me on where I need to go with this.

By type arguments, I mean:

BaseFooService<T> : IBaseFooService<T> where T : class

Upvotes: 4

Views: 2513

Answers (1)

Scott Hannen
Scott Hannen

Reputation: 29222

This is the problem:

public Foo2ViewModel GetViewModelFromEntity(Entity entity)
{
    Foo2ViewModel vm = (Foo2ViewModel) GetBaseFooViewModelFromEntity(entity);
    return vm;
}

public BaseFooViewModel GetBaseFooViewModelFromEntity(Entity entity)
{
    return new BaseFooViewModel()
    {
        BaseFooField1 = entity.BaseFooField1,
        BaseFooField2 = entity.BaseFooField2,
        BaseFooField3 = entity.BaseFooField3
    };
}

GetBaseFooViewModelFromEntity returns a new BaseFooViewModel. It does not return a Foo2ViewModel. You can cast a Foo2ViewModel as its base class, but not the other way around.

I thought that if a class derived from a base class that it could be casted as the derived class

You can't cast any object of a base class to any derived type. The cast only works if the object actually is of that derived type (or something derived from the derived type.)

In other words, you can't do this because the BaseFooViewModel isn't a Foo2ViewModel:

var model = new BaseFooViewModel();
var fooModel = (Foo2ViewModel)model;

but you can do this because a Foo2ViewModel is always a BaseFooViewModel:

var fooModel = new Foo2ViewModel();
var model = (BaseFooViewModel)fooModel;

You can do this (but why would you?)

Foo2ViewModel fooModel = new Foo2ViewModel();
BaseFooViewModel model = (BaseFooViewModel)fooModel;
Foo2ViewModel thisWorks = (Foo2ViewModel)model;

It only works because the object always was a Foo2ViewModel, so it can be cast as either that or its base type.

Here's a suggestion to let the compiler guide you so that you get compiler errors instead of runtime errors. That will open up a can of worms in the form of more compiler errors, but the good news is that you can fix them all without running your code. Compiler errors are better than runtime errors.

The first step would be to make BaseFooModel abstract so that you can't create an instance of it. Presumably you only want to deal with classes like Foo1ViewModel, Foo2ViewModel. Making the base class abstract will ensure that you can only create the derived classes.

public abstract class BaseFooViewModel // Just added "abstract"
{
    public string BaseFooField1 { get; set; }
    public string BaseFooField2 { get; set; }
    public string BaseFooField3 { get; set; }
}

When you make BaseFooModel abstract, this won't compile:

new BaseFooViewModel()

...because you can't create an instance of an abstract class. You can only create instance of derived classes that aren't abstract.

Assuming that your derived classes don't have constructors with arguments, you could do this:

public TFooModel GetBaseFooViewModelFromEntity<TFooModel>(Entity entity)
    where TFooModel : BaseFooViewModel, new()
{
    return new TFooModel()
    {
        BaseFooField1 = entity.BaseFooField1,
        BaseFooField2 = entity.BaseFooField2,
        BaseFooField3 = entity.BaseFooField3
    };
}

Now you're telling the method which specific inherited class to create. The generic constraints - BaseFooViewModel, new() - specify that it must inherit from BaseFooViewModel, and it must have a default constructor so that the class can create a new instance of it.

It will create an instance of the class you specify, but because it "is" a BaseFooViewModel it can set the properties belonging to that base class.

Then you can call the method like this:

Foo2ViewModel vm = GetBaseFooViewModelFromEntity<Foo2ViewModel>(entity);

A different way of looking at it. Runtime casts like this:

Foo2ViewModel vm = (Foo2ViewModel) GetBaseFooViewModelFromEntity(entity);

are generally to be avoided. I say generally because there are plenty of exceptions. But when writing our own generic classes I'll go so far as to say that we should always avoid it. Instead it's good to write the code so that the declared type we have is the only type we care about.

In other words, if a method returns Foo or an argument is of type Foo then the only type we care about is Foo. We don't care about its base class, and we don't care if the object we get is actually something that inherits from Foo.

Here's a separate article that describes my personal experience with this a little bit. I call it the Generic Rabbit Hole of Madness.

Upvotes: 2

Related Questions