Reputation: 611
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
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