Как мне использовать базовые классы в моих моделях просмотра и сервисах в MVC?

#c# #asp.net-core #asp.net-core-mvc

#c# #asp.net-core #asp.net-core-mvc

Вопрос:

Справочная информация

Использование MVC в Asp.Net В основном, я использую подход controller -> service -> view model -> view.

Я хочу, чтобы 2 сервиса совместно использовали некоторые базовые данные и функциональность.

Мое требование заключается в том, чтобы одна сущность была общей для двух разных типов пользователей. Т.е. пользователя admin и обычного пользователя.

Пользователь-администратор будет иметь доступ к дополнительным свойствам (определенным в модели представления) и функциональности (определенной в службе), в то время как обычный пользователь не будет.

В приведенном ниже примере кода 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();
}
  

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>
  

Внедрение зависимостей при запуске

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

Проблема

Приложение компилируется нормально, но я получаю ошибку во время выполнения:

Исключение InvalidCastException: невозможно привести объект типа ‘BaseServiceSample.ViewModels.Foo.BaseFooViewModel’ для ввода ‘BaseServiceSample.ViewModels.Foo.Foo1ViewModel’

Смотрите мои комментарии в Foo1Service, которые находятся над строкой в коде, вызывающей ошибку во время выполнения.

Я думал, что если класс является производным от базового класса, то его можно было бы использовать как производный класс, но я, вероятно, путаю это с тем, как может работать привязка модели в MVC.

Вопрос

Как я могу изменить свой код так, чтобы он поддерживал основные требования базовой модели представления и базовой службы, которые управляют общими свойствами и функциональностью для двух разных групп пользователей, но позволяют группе пользователей расширять эти свойства / функциональность?

Из моих исследований похоже, что мне может понадобиться использовать абстрактные классы или аргументы типа, но я не смог заставить это работать.

Я включил пример кода без моих попыток предоставить аргументы типа, чтобы сделать код более простым и, надеюсь, кому-то будет легче указать мне, куда мне нужно идти с этим.

Под аргументами типа я подразумеваю:

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

Комментарии:

1. вы пробовали Foo1ViewModel vm = new BaseFooViewModel(); ?

2. @AkbarBadhusha NewBaseFooViewModel() — это метод в BaseFooService, который выполняет некоторую настройку, а не опечатку. Тем не менее, я попытался изменить это в соответствии с вашим предложением, и я получаю ту же ошибку, но при компиляции решения, а не во время выполнения, что имеет смысл, поскольку приведения больше нет (Foo1ViewModel).

3. «если класс является производным от базового класса, то он может быть приведен как производный класс» — я думаю, именно здесь возникает путаница. Как правило, вам не нужно будет приводить базовый класс к производному классу. Это работает, только если объект определенно относится к этому определенному производному типу. (И если это так, то зачем вам нужно его приводить?) Обычно это происходит другим путем — объект приводится в качестве своего базового типа. Но это обычно тоже неявно.

4. @ScottHannen вы уловили мое замешательство. Я использовал производные модели представления при передаче их в действия контроллера и надеялся, что смогу сделать то же самое при создании их из сервисов. Однако я понимаю, что действия контроллера используют привязку модели, а не архитектуру типа класса.

Ответ №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 . Вы можете привести a Foo2ViewModel в качестве базового класса, но не наоборот.

Я подумал, что если класс является производным от базового класса, то он может быть приведен как производный класс

Вы не можете преобразовать какой-либо объект базового класса в какой-либо производный тип. Приведение работает, только если объект на самом деле имеет этот производный тип (или что-то производное от производного типа.)

Другими словами, вы не можете этого сделать, потому что BaseFooViewModel это не Foo2ViewModel :

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

но вы можете это сделать, потому что a Foo2ViewModel всегда BaseFooViewModel :

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

Вы можете это сделать (но зачем вам это?)

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

Это работает только потому, что объект всегда был Foo2ViewModel , поэтому он может быть приведен либо к этому, либо к его базовому типу.

Вот предложение, позволяющее компилятору направлять вас, чтобы вы получали ошибки компилятора вместо ошибок времени выполнения. Это приведет к появлению червей в виде большего количества ошибок компилятора, но хорошей новостью является то, что вы можете исправить их все, не запуская свой код. Ошибки компилятора лучше, чем ошибки времени выполнения.

Первым шагом было бы сделать BaseFooModel абстрактным, чтобы вы не могли создать его экземпляр. Предположительно, вы хотите иметь дело только с такими классами, как Foo1ViewModel , Foo2ViewModel . Создание абстрактного базового класса гарантирует, что вы сможете создавать только производные классы.

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

Когда вы создаете BaseFooModel abstract, это не будет компилироваться:

 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 , и у него должен быть конструктор по умолчанию, чтобы класс мог создать его новый экземпляр.

Это создаст экземпляр указанного вами класса, но поскольку он «является» a BaseFooViewModel , он может устанавливать свойства, принадлежащие этому базовому классу.

Затем вы можете вызвать метод следующим образом:

 Foo2ViewModel vm = GetBaseFooViewModelFromEntity<Foo2ViewModel>(entity);
  

Другой взгляд на это. Во время выполнения приведения выполняются следующим образом:

 Foo2ViewModel vm = (Foo2ViewModel) GetBaseFooViewModelFromEntity(entity);
  

их, как правило, следует избегать. Я говорю в целом, потому что есть много исключений. Но при написании наших собственных универсальных классов я зайду так далеко, что скажу, что мы всегда должны избегать этого. Вместо этого полезно написать код так, чтобы объявленный тип, который у нас есть, был единственным типом, который нас интересует.

Другими словами, если метод возвращает значение типа Foo или аргумент имеет тип Foo , то единственный тип, о котором мы заботимся, это Foo . Нас не волнует его базовый класс, и нам все равно, является ли получаемый нами объект на самом деле чем-то, что наследуется от Foo .

Вот отдельная статья, в которой немного описывается мой личный опыт работы с этим. Я называю это общей кроличьей норой безумия.

Комментарии:

1. спасибо за подробный ответ. Я просто играл в пинг-понг с тех пор, как увидел ваш ответ! Также очень интересная статья, ссылка на все блоки lego задела за живое. Я успешно применил ваше предложение в своем примере приложения, и оно сработало как по волшебству! Теперь перейдем к более сложной задаче внедрения этого в мое реальное приложение!