如何在MVC中的视图模型和服务中使用基类?

时间:2019-03-06 11:03:15

标签: c# asp.net-core asp.net-core-mvc

背景

在Asp.Net Core中使用MVC,我正在使用控制器->服务->视图模型->视图方法。

我希望2个服务共享一些基本数据和功能。

我的要求是,一个实体要由2种不同的用户类型共享。即管理员用户和标准用户。

管理员用户将有权访问其他属性(在视图模型上定义)和功能(在服务上定义),而标准用户则不能。

在下面的示例代码中,Foo1等同于管理员,而Foo2是标准用户。

控制器动作

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);
}

服务

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
        };
    }
}

接口

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

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

public interface IBaseFooService
{
    BaseFooViewModel NewBaseFooViewModel();
}

查看模型

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; }
}

观看次数

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>

启动时的依赖注入

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

问题

应用程序可以编译,但是在运行时出现错误:

  

InvalidCastException:无法转换类型的对象   键入“ BaseServiceSample.ViewModels.Foo.BaseFooViewModel”   'BaseServiceSample.ViewModels.Foo.Foo1ViewModel'

请参阅我在Foo1Service中的注释,这些注释在代码中在运行时导致错误的行上方。

我认为,如果从基类派生的类可以将其强制转换为派生类,但我可能会将其与MVC中的模型绑定如何工作混淆。

问题

如何更改代码,使其支持基本视图模型和基本服务的基本要求,这些基本视图模型和基本服务管理2个不同用户组的共享属性和功能,但允许用户组扩展这些属性/功能? / p>

从我的研究看来,我可能需要使用抽象类或类型自变量,但我无法使它正常工作。

我包含了代码示例,但没有尝试提供类型参数来使代码更简单,希望有人可以引导我了解我该如何去做。

通过类型自变量,我的意思是:

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

1 个答案:

答案 0 :(得分:1)

这是问题所在

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返回一个新的BaseFooViewModel。它不返回Foo2ViewModel。您可以将Foo2ViewModel作为其基类,但反之亦然。

  

我认为,如果从基类派生的类可以将其强制转换为派生类

您不能将基类的任何对象转换为任何派生类型。仅当对象实际上是该派生类型(或从派生类型派生的对象)时,才能进行强制转换。

换句话说,您无法执行此操作,因为BaseFooViewModel不是Foo2ViewModel

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

但是您可以这样做,因为Foo2ViewModel始终是BaseFooViewModel

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

您可以执行此操作(但是为什么呢?)

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

它只能工作,因为对象始终是Foo2ViewModel,因此可以将其强制转换为该类型或其基类型。

这里建议让编译器指导您,使您得到编译器错误而不是运行时错误。这将以更多的编译器错误的形式打开蠕虫的一罐,但是好消息是您可以在不运行代码的情况下修复所有蠕虫。编译器错误胜于运行时错误。

第一步是使BaseFooModel抽象,以便您不能创建它的实例。大概您只想处理Foo1ViewModelFoo2ViewModel之类的类。将基类抽象化将确保您只能创建派生类。

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

BaseFooModel设为抽象时,它将不会编译:

new BaseFooViewModel()

...因为您无法创建抽象类的实例。您只能创建不是抽象的派生类的实例。

假设您的派生类没有带参数的构造函数,则可以执行以下操作:

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

现在,您要告诉该方法创建哪个特定的继承类。通用约束-BaseFooViewModel, new()-指定它必须继承自BaseFooViewModel,并且必须具有默认构造函数,以便类可以创建它的新实例。

它将创建您指定的类的实例,但是由于它“是” BaseFooViewModel,因此可以设置属于该基类的属性。

然后您可以像这样调用方法:

Foo2ViewModel vm = GetBaseFooViewModelFromEntity<Foo2ViewModel>(entity);

一种不同的看待方式。运行时类型如下:

Foo2ViewModel vm = (Foo2ViewModel) GetBaseFooViewModelFromEntity(entity);

通常应被避免。我之所以说是因为有很多例外。但是当编写我们自己的泛型类时,我会说我们应该始终避免使用它。相反,最好编写代码,以便我们拥有的声明类型是我们关心的唯一类型。

换句话说,如果方法返回Foo或参数的类型为Foo,那么我们关心的唯一类型是Foo。我们不在乎其基类,也不在乎我们获取的对象是否实际上是从Foo继承的对象。

这是另一篇文章,介绍了我的个人经历。我称之为Generic Rabbit Hole of Madness