mercredi 20 mai 2020

How can I apply filtering to lists nested deep within an object graph? [closed]

I'm mapping an object graph from a data source to DTO objects to be returned in a Web API response but would like to apply filtering to objects nested deep within the graph. The data source is a tree structure and custom mappers are required to transform the data into a format similar to the following:

[{
  "divisionCode": "ABC123",
  "companyCode": "0123456789",
  "regions": [{
    // Objects to be filtered
    "stores": [{
      "name": "Store 1",
      "updatedDate": "2019-10-24T10:40:35Z"
    }, {
      "name": "Store 2",
      "updatedDate": "2020-01-09T14:30:02Z"
    }, {
      ...
    }]
  }],
  "updatedDate": "2019-09-11T06:57:15Z"
}, {
  ...
}]

I'd like to reuse an existing mapper interface, used throughout our project; so I've defined mappers, for the structure, implementing the existing interface:

public interface IMapper<TSource, TDestination> {
    TDestination Map(TSource src, TDestination dest);
}

For example:

internal class DivisionMapper : IMapper<Division, DivisionDto>
{
    private readonly IMapper<Region, RegionDto> regionMapper;

    public DivisionMapper(IMapper<Region, RegionDto> regionMapper)
    {
        this.regionMapper = regionMapper ?? throw new ArgumentNullException(nameof(regionMapper));
    }

    public DivisionDto Map(Division src, DivisionDto dest)
    {
        dest.DivisionCode = src.DivisionCode;

        if (src.Company is Company company)
        {
            dest.CompanyName = company.Name;
            dest.CompanyCode = company.CompanyCode;
        }

        this.MapRegions(src, dest);

        return dest;
    }

    private void MapRegions(Division src, DivisionDto dest)
    {
        foreach (var region in src.Regions)
        {
            var dto = this.regionMapper.Map(region, new RegionDto());

            dest.Regions.Add(dto);
        }
    }
}

And...

internal class RegionMapper : IMapper<LocationRegion, RegionDto>
{
    private readonly IMapper<Store, StoreDto> storeMapper;

    public RegionMapper(IMapper<Store, StoreDto> storeMapper)
    {
        this.storeMapper = storeMapper ?? throw new ArgumentNullException(nameof(storeMapper));
    }

    public RegionDto Map(Region src, RegionDto dest)
    {
        dest.Stores = src.Stores
            // Filtering to be applied
            .Where(x => updatedSince.HasValue ? x.UpdateDate >= updatedSince : true) 
            .Select(x => this.storeMapper.Map(x, new StoreDto()));

        return dest;
    }
}

The top level division mapper would then be injected into a respository class and used to map the data to the required format:

// Not sure of the best way to inject this parameter into the mapper!
public IEnumerable<DivisionDto> GetAll(DateTime? updatedSince = null)
{
    var results = new List<DivisionDto>();

    foreach (var division in this.GetDivisions())
    {
        var dto = this.divisionMapper.Map(division, new DivisionDto());

        results.Add(dto);
    }

    return results;
}

private IEnumerable<Division> GetDivisions() {
    // Get divisions from data source
}

The repository GetAll method would be called in the Web API controller action to create the response:

[HttpGet]
public HttpResponseMessage Index(DateTime? updatedSince = null) // Filter parameter
{
    var response = new Response<DivisionDto>();
    var divisions = this.divisionRepository.GetAll(updatedSince);

    if (divisions.Any())
    {
        response.AddRange(divisions);
    }

    return this.Request.CreateResponse(HttpStatusCode.OK, response);
}

The updatedSince parameter is passed to the Web API controller action and would ideally be used to filter the Stores list for each Region without having to map each first (there could be a significant number of stores).

How can I pass the updatedSince, or any other parameters, to the mappers? I've thought about using the Visitor Pattern to traverse the graph after the mapping, to filter out results, but I'd rather remove the result before performing the mapping.

Thanks for any help!

Aucun commentaire:

Enregistrer un commentaire