使用DTO在服务层和UI层之间传输数据

17

我已尝试数天来解决这个问题,但对于ASP.NET MVC这个特定主题的信息似乎非常少。我已经Google了数天,但并没有真正能够解决这个问题。

我有一个三层项目。业务层、数据访问层和UI/ Web层。在DAL中是dbcontext、repository和unit of work。在业务层中有一个包含所有接口和EF模型的域层。在业务层中还有一个服务层,其中包含用于EF模型的DTO以及访问存储库的通用存储库服务。图片应该可以帮助说明。

我的问题是,我就是无法弄清楚如何使用DTO来传输从业务层获取的数据。

我为DTO创建了服务类。我有一个ImageDTO和模型,以及图像锚点相同。我为每个DTO创建了一个服务类。因此我有一个图像服务和一个锚点服务。这些服务继承自存储库服务,并且目前实现了它们自己的服务。但这大概就是我所做到的了。由于这些服务具有通过IoC接收IUnitOfWork接口的构造函数,我几乎已经陷入困境。

如果我直接从UI引用服务,则一切正常,但我只是无法理解如何使用DTO来在服务层和UI层之间以及反之传输数据。

我的服务层:

业务/服务/DTO

public class AnchorDto
{
      public int Id { get; set; }
      public int x1 { get; set; }
      public int y1 { get; set; }
      public int x2 { get; set; }
      public int y2 { get; set; }
      public string description { get; set; }
      public int  imageId { get; set; }
      public int targetImageId { get; set; }

      public AnchorDto(int Id, int x1, int y1, int x2, int y2, string description, int imageId, int targetImageId)
      {
          // Just mapping input to the DTO 
      }
}

public class ImageDto
{
    public int Id { get; set; }
    public string name { get; set; }
    public string title { get; set; }
    public string description { get; set; }
    public virtual IList<AnchorDto> anchors { get; set; }

    public ImageDto(int Id, string name, string title, string description, IList<AnchorDto> anchors )
    {
        // Just mapping input to DTO
    }
}

业务/服务/服务

public class RepoService<TEntity> : IRepoService<TEntity> where TEntity : class
{
    private IRepository<TEntity> repo;

    public RepoService(IUnitOfWork repo)
    {
        this.repo = repo.GetRepository<TEntity>();
    }

    public IEnumerable<TEntity> Get(
        Expression<Func<TEntity, bool>> filter = null,
        Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
        string includeProperties = "")
        {
            return repo.Get(filter, orderBy, includeProperties);
        }

        public TEntity GetByID(object id)
        {
            return repo.GetByID(id);
        }

        public void Insert(TEntity entity)
        {
            repo.Insert(entity);
        }

        public void Delete(object id)
        {
            repo.Delete(id);
        }

        public void Delete(TEntity entityToDelete)
        {
            repo.Delete(entityToDelete);
        }

        public void Update(TEntity entityToUpdate)
        {
            repo.Update(entityToUpdate);
        }
    }

Image Service(图片服务),IImageService接口目前还是空的,直到我想明白需要实现什么为止。

public class ImageService : RepoService<ImageModel>, IImageService
{
    public ImageService(IUnitOfWork repo)
        : base(repo)
    {

    }
}

目前我的控制器没有真正工作,并且没有使用服务层,所以我决定不包括它们。一旦我解决了这个问题,我计划使用自动映射将DTO映射到ViewModels。

因此,请任何有足够知识的人给我那个我缺失的想法,这样我就能找出问题所在。

1个回答

47

您的服务应该接收DTO,将它们映射到业务实体并将它们发送到仓库。它还应该从仓库中检索业务实体,将其映射到DTO并将DTO作为响应返回。因此,您的业务实体永远不会从业务层中出现,只有DTO会。

然后您的UI / Web层应该不知道业务实体的存在。Web层只应该知道DTO。为了强制执行这个规则,非常重要的是您的UI层不使用服务实现类(应该是私有的),只使用接口。服务接口也不应该依赖于业务实体,只依赖于DTO。

因此,您需要基于DTO的服务接口,并且您的基础服务类需要另一个泛型参数来处理DTO。我喜欢为实体和DTO拥有一个基本类,以便它们可以声明为:

//Your UI\presentation layer will work with the interfaces (The inheriting ones) 
//so it is very important that there is no dependency
//on the business entities in the interface, just on the DTOs!
protected interface IRepoService<TDto> 
    where TDto: DTOBase
{
    //I'm just adding a couple of methods  but you get the idea
    TDto GetByID(object id);
    void Update(TDto entityToUpdateDto)
}

//This is the interface that will be used by your UI layer
public IImageService: IRepoService<ImageDTO>
{
}

//This class and the ones inheriting should never be used by your 
//presentation\UI layer because they depend on the business entities!
//(And it is a best practice to depend on interfaces, anyway)
protected abstract class RepoService<TEntity, TDto> : IRepoService<TDto> 
    where TEntity : EntityBase
    where TDto: DTOBase
{
    ... 
}

//This class should never be used by your service layer. 
//Your UI layer should always use IImageService
//You could have a different namespace like Service.Implementation and make sure
//it is not included by your UI layer
public class ImageService : RepoService<ImageModel, ImageDto>, IImageService
{
    ...
}

接下来,您需要一种方法在不实现映射的情况下将实体和DTO之间的映射添加到该基础服务中(因为它取决于每个具体的实体和DTO类)。您可以声明抽象方法来执行映射操作,并且需要在每个特定的服务(例如ImageService)上实现这些方法。基本的RepoService实现应如下所示:

public TDto GetByID(object id)
{
    //I'm writing it this way so its clear what the method is doing
    var entity = repo.GetByID(id);
    var dto = this.EntityToDto(entity);
    return dto;
}

public void Update(TDto entityToUpdateDto)
{
    var entity = this.DtoToEntity(entityToUpdateDto)
    repo.Update(entity);
}

//These methods will need to be implemented by every service like ImageService
protected abstract TEntity DtoToEntity(TDto dto);
protected abstract TDto EntityToDto(TEntity entity);

或者你可以声明映射服务,使用适当的映射服务向你的IOC添加依赖项(如果需要在不同的服务上进行相同的映射,则这样做更有意义)。RepoService的实现看起来像:

或者您可以声明映射服务,通过与应用程序提供的适当映射服务添加依赖项来实现。(如果需要在不同的服务上进行相同的映射,则这样做更有意义)。RepoService的实现如下:

private IRepository<TEntity> _repo;
private IDtoMappingService<TEntity, TDto> _mappingService;

public RepoService(IUnitOfWork repo, IDtoMappingService<TEntity, TDto> mapping)
{
    _repo = repo.GetRepository<TEntity>();
    _mappingService = mapping;
}

public TDto GetByID(object id)
{
    //I'm writing it this way so its clear what the method is doing
    var entity = repo.GetByID(id);
    var dto = _mappingService.EntityToDto(entity);
    return dto;
}

public void Update(TDto entityToUpdateDto)
{
    var entity = _mappingService.DtoToEntity(entityToUpdateDto)
    repo.Update(entity);
}

//You will need to create implementations of this interface for each 
//TEntity-TDto combination
//Then include them in your dependency injection configuration
public interface IDtoMappingService<TEntity, TDto>
    where TEntity : EntityBase
    where TDto: DTOBase
{
    public TEntity DtoToEntity(TDto dto);
    public TDto EntityToDto(TEntity entity);
}
在这两种情况下(抽象方法或映射服务),你可以手动实现实体和DTO之间的映射,也可以使用像Automapper这样的工具。但是当使用AutoMapper和实体框架时,你应该非常小心,尽管那是另一个话题!(Google一下并收集一些关于此主题的信息。作为第一个建议:在加载数据时注意执行对数据库的查询,以免加载更多不必要的数据或发送多个查询。在保存数据时,请注意你的集合和关系)
可能有点长,但希望能有所帮助!

您的示例中DTOBase / Tentity类包含什么?它只是一个带有id属性的抽象类吗?此外,它们将位于哪个层? - grimurd
如果它们不包含任何逻辑,则它们将是接口。但通常您会有像Id这样的公共属性,因此它们就是具有该公共属性的抽象类。DTOBase将位于服务层,而EntityBase将位于业务层。 - Daniel J.G.
1
很棒的回答。给了我很多启发! - Niklas Wulff
从干净架构和洋葱架构的角度来看,这意味着DTO也需要在核心项目中。这显然是不可取的。为什么不在UI和服务之间使用控制器呢? - liqSTAR
您的答案已经在这里链接:https://dev59.com/zmIj5IYBdhLWcg3wmWG1#33155561,并且一些评论认为它是完全错误的。我很困惑... - parsecer
1
@parsecer已经过去了10年,与此同时,对于服务或DTO等概念的理解发生了变化,架构风格也发展了!我的回答(我相信OP也是如此)假设控制器作为MVC模式的一部分拥有自己的模型。此外,SOA正流行,我将服务视为向客户端公开的API,而MVC应用程序就是其中之一。如果你能够将DTO/服务等概念抽象化/忽略化/转换成你当前的架构风格,并简单地考虑解耦层次,那么这个答案仍然可能作为代码结构的灵感而有用。 - Daniel J.G.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接