N 层架构 c# mvc ViewModels
N tier architecture c# mvc ViewModels
假设我有这样的表:
Users
------
UserId
Email
...
People
------
PersonId
UserId
Name
Country
...
对应机型:
public class User{
...
}
public class Person{
...
}
我想在我的 MVC 应用程序中有一个视图显示所有人,包括他们的电子邮件地址。
我的解决方案在不同的项目中构建,例如:
Comp.App.Services
Comp.App.Dal
Comp.App.Web
理想情况下,我在我的控制器中创建一个视图模型,以便稍后像这样填充我的视图:
public class PersonListItemViewModel{
public string Name { get; set; }
public string Country { get; set; }
public string Email { get; set; }
}
现在是问题。我想在我的服务层中通过 1 个查询获取数据,但服务层必须不知道视图模型,并且查询不应该 return 任何额外的字段。只有我在视图模型中指定的字段。
我为 N 层应用程序编写了许多最佳实践,但我似乎无法弄清楚如何在不让我的服务层了解视图模型的情况下实现它。这似乎不正确。我一直假设我的服务层只能知道领域模型,但在现实世界的应用程序中,我总是会遇到这样的问题,我想将查询限制在例如只有 2 个字段。如果我的服务只讨论领域模型,这是无法完成的。
我对这种方法有什么误解吗?
PS。我在我的 Dal 项目中使用 Dapper。
编辑:
它不是 Converting DTOs to View Models 的副本
.我知道自动映射器。让我问这个问题有点不同。当我有一个名为 GetPeopleWithEmail() 的方法时,我的服务 return 应该做什么???
根据我现在在答案中阅读的内容,它应该 return 一个 DTO,并在我的控制器中将 DTO 转换为 ViewModel。正确吗?
如果您希望确保您的 ViewModel 位于表示端而域模型位于存储库端,那么我会选择逻辑层。
网络层:
- 视图:接受视图模型
- 控制器:与 Viewmodel 和 WebLogic 一起工作
- WebLogic:适用于 Viewmodel、Service 和 Domain 模型
应用层:
- 服务:适用于领域模型和
- BusinessLogic:与存储库和领域模型一起工作
- 存储库:与数据存储一起工作
我强烈建议使用一些依赖注入,例如 Unity 来帮助管理依赖关系。
比如在我上面的结构中,SomeController会接受SomeWebLogic,SomeWebLogic会接受ISomeService。 SomeWebLogic 将调用 ISomeService 将 SomeDomainModel 转换为 SomeViewModel,最终将由 SomeController 使用。
在应用层这边,SomeService 会接受 SomeBusinessLogic,SomeBusinessLogic 会接受 ISomeRepository。
在您的示例中 - 使用我建议的结构:
--网络层--
PersonView.cshtml:
@model List<PersonListItemViewModel>;
@foreach(var person in model)
{
//some html binding
}
PersonController.cs:
public ActionResult PersonView()
{
var personWebLogic = new PersonWebLogic(); //would suggest DI instead
var personsModelList = personWebLogic.GetPersons();
return View(personsModelList );
}
PersonWebLogic.cs
public List<PersonListItemViewModel> GetPersons()
{
var personService = new PersonService(); //suggest DI here again
var people = personService.GetPeople(); //returns a list of domain models
var personsViewModelList = new List<PersonListItemViewModel>();
foreach(var person in people)
{
//use some custom function to convert PersonDomainModel to PersonListItemViewModel
personalsViewModel.Add(MapPersonDomainToPersonView(person));
}
return personsViewModelList;
}
--应用层--
个人服务
public List<Person> GetPeople()
{
var personLogic = new PersonLogic(); //use DI for this
return personLogic.GetPeople(); //return type will be dependent on your service architecture
}
个人逻辑
public List<Person> GetPeople()
{
var personRepostitory = new PersonRepository(); //DI...
var personDataTable = personRepository.GetAllPeople(); //return type will vary on your repository structure
//Custom function to map to person list from data table, this could be in repo, all depends on your desired structure
return MapPersonDataTableToPersonList(personDataTable);
}
个人资料库
public DataTable GetAllPeople()
{
var database = GetDatabase();
var dataTable = ...//call db to get person datatable
return dataTable;
}
[...] and the query should not return any extra field. Only the fields
i specified in my viewmodel.
实际上返回某个模型、域对象或域层中具有 10、20 或 100 个属性的任何对象的实例并不意味着必须设置所有属性。
另一方面,同样的规则可以应用于数据传输对象 (DTO)。
通常你使用 JSON 序列化格式和默认 ASP.NET Web API JSON 序列化与 JSON.NET 库一起工作,它支持 DataContractAttribute
和 DataMemberAttribute
属性。
最终,您可以使用具有 20 个属性的 DTO class 从 API 通过网络发送对象,但只有具有非默认值的对象才会被发送序列化:
[DataContract]
public class Dto
{
[DataMember(EmitDefaultValue = false)]
public string PropertyA { get; set; }
}
如果实例化 Dto
并且没有设置 属性,序列化结果将只是 {}
(一个空白文字对象)。现在您可以将此规则外推到大型 DTO,并且可以确保您不会通过网络传输不需要的属性。 或者换句话说,您可以使用序列化属性从同一个 class!.
中生成许多不同的 DTO
在 n 层场景中工作时,您应该关心正在设置哪些属性,而不是有多少属性公开一些 class。用例 A 可以设置 3 个属性,用例 B 可以设置与用例 A 不同的其他 2 个属性。也许用例 C 设置其他属性加上用例 A 和 B 设置的属性。
有时事情变得更难,你不能用一个 class 来统治它们,然后你实现许多数据传输对象来涵盖不同的用例,你实现了属性的子集具体客户端视图需要。
这里很重要的一点是ViewModel != DTO
。 视图模型 为视图提供行为和数据,而 DTO 只是一个对象,用于传输某些服务提供的数据子集,以优化网络性能和使用,并避免将不相关的数据发送到其他层。
按照DDD原则(domain-driven设计)域实体可以是root和child .根实体是独立的,可以包含几个依赖children.
根实体存储库 应该从数据库加载整个实体,包括所有必要的 children。您可以在存储库的代码中优化查询。
public class PersonRepository
{
public Person GetById(int id)
{
// reads Person data and related User data
// builds Person object and returns it
}
}
然后您可以使用根 object 来构建您的 view/model:
PersonListItemViewModel CreateViewModel(Person person)
{
return new PersonListItemViewModel
{
Name = person.Name,
Country = person.Country,
Email = person.User.Email,
};
}
该服务可以 return 所有人通过像 All() 这样的方法 return 是一个 IQueryable。然后你可以用它来 select 像
这样的自定义投影
db.People.All().Select(p => new PersonListItemViewModel()
{
Name = p.Name,
Country = p.Country,
Email = db.Users.FirstOrDefault(u => u.UserId == p.UserId).Email
});
假设我有这样的表:
Users
------
UserId
Email
...
People
------
PersonId
UserId
Name
Country
...
对应机型:
public class User{
...
}
public class Person{
...
}
我想在我的 MVC 应用程序中有一个视图显示所有人,包括他们的电子邮件地址。
我的解决方案在不同的项目中构建,例如:
Comp.App.Services
Comp.App.Dal
Comp.App.Web
理想情况下,我在我的控制器中创建一个视图模型,以便稍后像这样填充我的视图:
public class PersonListItemViewModel{
public string Name { get; set; }
public string Country { get; set; }
public string Email { get; set; }
}
现在是问题。我想在我的服务层中通过 1 个查询获取数据,但服务层必须不知道视图模型,并且查询不应该 return 任何额外的字段。只有我在视图模型中指定的字段。
我为 N 层应用程序编写了许多最佳实践,但我似乎无法弄清楚如何在不让我的服务层了解视图模型的情况下实现它。这似乎不正确。我一直假设我的服务层只能知道领域模型,但在现实世界的应用程序中,我总是会遇到这样的问题,我想将查询限制在例如只有 2 个字段。如果我的服务只讨论领域模型,这是无法完成的。
我对这种方法有什么误解吗?
PS。我在我的 Dal 项目中使用 Dapper。
编辑: 它不是 Converting DTOs to View Models 的副本 .我知道自动映射器。让我问这个问题有点不同。当我有一个名为 GetPeopleWithEmail() 的方法时,我的服务 return 应该做什么???
根据我现在在答案中阅读的内容,它应该 return 一个 DTO,并在我的控制器中将 DTO 转换为 ViewModel。正确吗?
如果您希望确保您的 ViewModel 位于表示端而域模型位于存储库端,那么我会选择逻辑层。
网络层:
- 视图:接受视图模型
- 控制器:与 Viewmodel 和 WebLogic 一起工作
- WebLogic:适用于 Viewmodel、Service 和 Domain 模型
应用层:
- 服务:适用于领域模型和
- BusinessLogic:与存储库和领域模型一起工作
- 存储库:与数据存储一起工作
我强烈建议使用一些依赖注入,例如 Unity 来帮助管理依赖关系。
比如在我上面的结构中,SomeController会接受SomeWebLogic,SomeWebLogic会接受ISomeService。 SomeWebLogic 将调用 ISomeService 将 SomeDomainModel 转换为 SomeViewModel,最终将由 SomeController 使用。
在应用层这边,SomeService 会接受 SomeBusinessLogic,SomeBusinessLogic 会接受 ISomeRepository。
在您的示例中 - 使用我建议的结构:
--网络层--
PersonView.cshtml:
@model List<PersonListItemViewModel>;
@foreach(var person in model)
{
//some html binding
}
PersonController.cs:
public ActionResult PersonView()
{
var personWebLogic = new PersonWebLogic(); //would suggest DI instead
var personsModelList = personWebLogic.GetPersons();
return View(personsModelList );
}
PersonWebLogic.cs
public List<PersonListItemViewModel> GetPersons()
{
var personService = new PersonService(); //suggest DI here again
var people = personService.GetPeople(); //returns a list of domain models
var personsViewModelList = new List<PersonListItemViewModel>();
foreach(var person in people)
{
//use some custom function to convert PersonDomainModel to PersonListItemViewModel
personalsViewModel.Add(MapPersonDomainToPersonView(person));
}
return personsViewModelList;
}
--应用层--
个人服务
public List<Person> GetPeople()
{
var personLogic = new PersonLogic(); //use DI for this
return personLogic.GetPeople(); //return type will be dependent on your service architecture
}
个人逻辑
public List<Person> GetPeople()
{
var personRepostitory = new PersonRepository(); //DI...
var personDataTable = personRepository.GetAllPeople(); //return type will vary on your repository structure
//Custom function to map to person list from data table, this could be in repo, all depends on your desired structure
return MapPersonDataTableToPersonList(personDataTable);
}
个人资料库
public DataTable GetAllPeople()
{
var database = GetDatabase();
var dataTable = ...//call db to get person datatable
return dataTable;
}
[...] and the query should not return any extra field. Only the fields i specified in my viewmodel.
实际上返回某个模型、域对象或域层中具有 10、20 或 100 个属性的任何对象的实例并不意味着必须设置所有属性。
另一方面,同样的规则可以应用于数据传输对象 (DTO)。
通常你使用 JSON 序列化格式和默认 ASP.NET Web API JSON 序列化与 JSON.NET 库一起工作,它支持 DataContractAttribute
和 DataMemberAttribute
属性。
最终,您可以使用具有 20 个属性的 DTO class 从 API 通过网络发送对象,但只有具有非默认值的对象才会被发送序列化:
[DataContract]
public class Dto
{
[DataMember(EmitDefaultValue = false)]
public string PropertyA { get; set; }
}
如果实例化 Dto
并且没有设置 属性,序列化结果将只是 {}
(一个空白文字对象)。现在您可以将此规则外推到大型 DTO,并且可以确保您不会通过网络传输不需要的属性。 或者换句话说,您可以使用序列化属性从同一个 class!.
在 n 层场景中工作时,您应该关心正在设置哪些属性,而不是有多少属性公开一些 class。用例 A 可以设置 3 个属性,用例 B 可以设置与用例 A 不同的其他 2 个属性。也许用例 C 设置其他属性加上用例 A 和 B 设置的属性。
有时事情变得更难,你不能用一个 class 来统治它们,然后你实现许多数据传输对象来涵盖不同的用例,你实现了属性的子集具体客户端视图需要。
这里很重要的一点是ViewModel != DTO
。 视图模型 为视图提供行为和数据,而 DTO 只是一个对象,用于传输某些服务提供的数据子集,以优化网络性能和使用,并避免将不相关的数据发送到其他层。
按照DDD原则(domain-driven设计)域实体可以是root和child .根实体是独立的,可以包含几个依赖children.
根实体存储库 应该从数据库加载整个实体,包括所有必要的 children。您可以在存储库的代码中优化查询。
public class PersonRepository
{
public Person GetById(int id)
{
// reads Person data and related User data
// builds Person object and returns it
}
}
然后您可以使用根 object 来构建您的 view/model:
PersonListItemViewModel CreateViewModel(Person person)
{
return new PersonListItemViewModel
{
Name = person.Name,
Country = person.Country,
Email = person.User.Email,
};
}
该服务可以 return 所有人通过像 All() 这样的方法 return 是一个 IQueryable。然后你可以用它来 select 像
这样的自定义投影db.People.All().Select(p => new PersonListItemViewModel()
{
Name = p.Name,
Country = p.Country,
Email = db.Users.FirstOrDefault(u => u.UserId == p.UserId).Email
});