Returning Domain Objects from Repositories on Joining tables

元气小坏坏 提交于 2020-04-30 11:22:21

问题


I have been reading that Repositories should return domain objects only. I am having difficulty with implementing this. I currently have API with Service Layer, Repository and I am using EF Core to access sql database.

If we consider User(Id, Name, address, PhoneNumber, Email, Username) and Orders (id, OrderDetails, UserId) as 2 domain objects. One Customer can have multiple Orders. I have created navigation property

public virtual User User{ get; set; }

and foreign Key.

Service layer needs to return DTO with OrderId, OrderDetails, CustomerId, CustomerName. What should the Repository return in this case? This is what i was trying:

public IEnumerable<Orders> GetOrders(int orderId)
        {
            var result = _context.Orders.Where(or=>or.Id=orderId)
                .Include(u => u.User)
                .ToList();
            return result;
        }

I am having trouble with Eager loading. I have tried to use include. I am using Database first. In the case of above, Navigation Properties are always retuned with NULL. The only way i was able to get data in to Navigation Properties was to enable lazy loading with proxies for the context. I think this will be a performance issue

Can anyone help with what i should return and why .Include is not working?


回答1:


The advice I give around the repository pattern is that repositories should return IQueryable<TEntity>, Not IEnumerable<TEntity>.

The purpose of a repository is to:

  • Make code easier to test.
  • Centralize common business rules.

The purpose of a repository should not be to:

  • Abstract EF away from your project.
  • Hide knowledge of your domain. (Entities)

If you're introducing a repository to hide the fact that the solution is depending on EF or hide the domain then you are sacrificing much of what EF can bring to the table for managing the interaction with your data or you are introducing a lot of unnecessary complexity into your solution to try and keep that capability. (filtering, sorting, paginating, selective eager loading, etc.)

Instead, by leveraging IQueryable and treating EF as a first-class citizen to your domain you can leverage EF to produce flexible and fast queries to get the data you need.

Given a Service where you want to " return DTO with OrderId, OrderDetails, CustomerId, CustomerName."

Step 1: Raw example, no repository...

Service code:

public OrderDto GetOrderById(int orderId)
{
    using (var context = new AppDbContext())
    {
        var order = context.Orders
            .Select(x => new OrderDto
            {
                OrderId = x.OrderId,
                OrderDetails = x.OrderDetails,
                CustomerId = x.Customer.CustomerId,
                CustomerName = x.Customer.Name
            }).Single(x => x.OrderId == orderId);
        return order;
    }    
}

This code can work perfectly fine, but it is coupled to the DbContext so it is hard to unit test. We may have additional business logic to consider that will need to apply to pretty much all queries such as if Orders have an "IsActive" state (soft delete) or the database serves multiple clients (multi-tenant). There will be a lot of queries in our controllers and would lead to the need for a lot of things like .Where(x => x.IsActive) included everywhere.

With the Repository pattern (IQueryable), unit of work:

public OrderDto GetOrderById(int orderId)
{
    using (var context = ContextScopeFactory.CreateReadOnly())
    {
        var order = OrderRepository.GetOrders()
            .Select(x => new OrderDto
            {
                OrderId = x.OrderId,
                OrderDetails = x.OrderDetails,
                CustomerId = x.Customer.CustomerId,
                CustomerName = x.Customer.Name
            }).Single(x => x.OrderId == orderId);
        return order;
    }    
}

Now at face value in the controller code above, this doesn't really look much different to the first raw example, but there are a few bits that make this testable and can help manage things like common criteria.

The repository code:

public class OrderRepository : IOrderRepository
{
    private readonly IAmbientContextScopeLocator _contextScopeLocator = null;

    public OrderRepository(IAmbientContextScopeLocator contextScopeLocator)
    {
        _contextScopeLocator = contextScopeLocator ?? throw new ArgumentNullException("contextScopeLocator");
    }

    private AppDbContext Context => return _contextScopeLocator.Get<AppDbContext>();

    IQueryable<Order> IOrderRepository.GetOrders()
    {
        return Context.Orders.Where(x => x.IsActive);
    }
}

This example uses Mehdime's DbContextScope for the unit of work, but can be adapted to others or an injected DbContext as long as it is lifetime scoped to the request. It also demonstrates a case with a very common filter criteria ("IsActive") that we might want to centralize across all queries.

In the above example we use a repository to return the orders as an IQueryable. The repository method is fully mock-able where the DbContextScopeFactory.CreateReadOnly call can be stubbed out, and the repository call can be mocked to return whatever data you want using a List<Order>().AsQueryable() for example. By returning IQueryable the calling code has full control over how the data will be consumed. Note that there is no need to worry about eager-loading the customer/user data. The query will not be executed until you perform the Single (or ToList etc.) call which results in very efficient queries. The repository class itself is kept very simple as there is no complexity about telling it what records and related data to include. We can adjust our query to add sorting, pagination, (Skip/Take) or get a Count or simply check if any data exists (Any) without adding functions etc. to the repository or having the overhead of loading the data just to do a simple check.

The most common objections I hear to having repositories return IQueryable are:

"It leaks. The callers need to know about EF and the entity structure." Yes, the callers need to know about EF limitations and the entity structure. However, many alternative approaches such as injecting expression trees for managing filtering, sorting, and eager loading require the same knowledge of the limitations of EF and the entity structure. For instance, injecting an expression to perform filtering still cannot include details that EF cannot execute. Completely abstracting away EF will result in a lot of similar but crippled methods in the repository and/or giving up a lot of the performance and capability that EF brings. If you adopt EF into your project it works a lot better when it is trusted as a first-class citizen within the project.

"As a maintainer of the domain layer, I can optimize code when the repositories are responsible for the criteria." I put this down to premature optimization. The repositories can enforce core-level filtering such as active state or tenancy, leaving the desired querying and retrieval up to the implementing code. It's true that you cannot predict or control how these resulting queries will look against your data source, but query optimization is something that is best done when considering real-world data use. The queries that EF generates reflect the data that is needed which can be further refined and the basis for what indexes will be most effective. The alternative is trying to predict what queries will be used and giving those limited selections to the services to consume with the intention of them requesting further refined "flavours". This often reverts to services running less efficient queries more often to get their data when it's more trouble to introduce new queries into the repositories.




回答2:


Repositories can return other types of objects, even primitive types like integers if you want to count some number of objects based on a criteria.

This is from the Domain Driven Design book:

They (Repositories) can also return symmary information, such as a count of how many instances (of Domain Object) meet some criteria. They can even return summary calculations, such as the total across all matching objects of some numerical attribute.

If you return somethings that isn't a Domain Objects, it's because you need some information about the Domain Objects, so you should only return immutable objects and primitive data types like integers.

If you make a query to get and objects with the intention of changing it after you get it, it should be a Domain Object.

If you need to do it place boundaries around your Domain Objects and organize them in Aggregates.

Here's a good article that explains how to decompose your model into aggregates: https://dddcommunity.org/library/vernon_2011/

In your case you can either compose the User and the Order entities in a single Aggreate or have them in separate Aggregates.

EDIT:

Example:

Here we will use Reference By Id and all Entities from different Aggregates will reference other entities from different Aggregates by Id.

We will have three Aggregates: User, Product and Order with one ValueObject OrderLineItem.

public class User {

    public Guid Id{ get; private set; }
    public string FirstName { get; private set; }
    public string LastName { get; private set; }
}

public class Product {

    public Guid Id { get; private set; }
    public string Name { get; private set; }
    public Money Price { get; private set; }
}

public class OrderLineItem {

    public Guid ProductId { get; private set; }
    public Quantity Quantity { get; private set; }
    // Copy the current price of the product here so future changes don't affect old orders
    public Money Price { get; private set; } 
}

public class Order {

    public Guid Id { get; private set; }
    public IEnumerable<OrderLineItem> LineItems { get; private set; }
}

Now if you do have to do heavy querying in your app you can create a ReadModel that will be created from the model above


public class OrderLineItemWithProductDetails {

    public Guid ProductId { get; private set; }
    public string ProductName { get; private set; }

    // other stuff quantity, price etc.
}

public class OrderWithUserDetails {

    public Guid Id { get; private set; }
    public string UserFirstName { get; private set; }
    public string UserLastName { get; private set; }
    public IEnumerable<OrderLineItemWithProductDetails > LineItems { get; private set; }
    // other stuff you will need

}

How you fill the ReadModel is a whole topic, so I can't cover all of it, but here are some pointers.

You said you will do a Join, so you're probably using RDBMS of some kind like PosteSQL or MySQL. You can do the Join in a special ReadModel Repository. If your data is in a single Database, you can just use a ReadModel Repository.


// SQL Repository, No ORM here
public class OrderReadModelRepository {

    public OrderWithUserDetails FindForUser(Guid userId) {

        // this is suppose to be an example, my SQL is a bit rusty so...
        string sql = @"SELECT * FROM orders AS o 
                    JOIN orderlineitems AS l
                    JOIN users AS u ON o.UserId = u.Id
                    JOIN products AS p ON p.id = l.ProductId
                    WHERE u.Id = userId";

        var resultSet = DB.Execute(sql);

        return CreateOrderWithDetailsFromResultSet(resultSet);
    }
}

// ORM based repository
public class OrderReadModelRepository {

    public IEnumerable<OrderWithUserDetails> FindForUser(Guid userId) {

        return ctx.Orders.Where(o => o.UserId == userId)
                         .Include("OrderLineItems")
                         .Include("Products")
                         .ToList();
    }
}

If it's not, well you will have to build it an keep it in a separate database. You can use DomainEvents to do that, but I wont go that far if you have a single SQL database.



来源:https://stackoverflow.com/questions/59807269/returning-domain-objects-from-repositories-on-joining-tables

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!