Linq 中嵌套 类 的通用层次结构过滤器

Generic hierarchy filter of nested classes in Linq

我正在编写一个包含如下嵌套结构的应用程序:

public class Country
{
    public string Name { get; set; }
    public List<State> States { get; set; } = new();
}

public class State
{
    public string Name { get; set; }
    public List<City> Cities { get; set; }
}

public class City
{
    public string Name { get; set; }
    public List<Shop> Shops { get; set; }
}

public class Shop
{
    public string Name { get; set; }
    public int Area { get; set; }
}

模拟数据:

private static List<Country> GenerateData()
    {
        List<Country> countries = new();
        
        Country country =  new()
        {
            Name = "USA", 
            States = new List<State>()
            {
                new State()
                {
                    Name = "Texas",
                    Cities = new List<City>()
                    {
                        new City()
                        {
                            Name = "Dallas",
                            Shops = new List<Shop>()
                            {
                                new Shop()
                                {
                                    Name = "Walmart",
                                    Area = 30000
                                },
                                new Shop()
                                {
                                    Name = "Walmart",
                                    Area = 40000
                                }
                            }
                        },
                        new City()
                        {
                            Name = "Austin",
                            Shops = new List<Shop>()
                            {
                                new Shop()
                                {
                                    Name = "Walmart",
                                    Area = 20000
                                }
                            }
                        }
                    }
                },
                new State()
                {
                    Name = "Alabama",
                    Cities = new List<City>()
                    {
                        new City()
                        {
                            Name = "Auburn",
                            Shops = new List<Shop>()
                            {
                                new Shop()
                                {
                                    Name = "MyShop",
                                    Area = 500
                                }
                            }
                        },
                        new City()
                        {
                            Name = "Dothan",
                            Shops = new List<Shop>()
                            {
                                new Shop()
                                {
                                    Name = "MyShop2",
                                    Area = 6000
                                }
                            }
                        }
                    }
                }
            } 
                
        };
        
        countries.Add(country);

        return countries;
    }

我的目标是像这样过滤这个嵌套结构:

搜索包含 “Dal” 的城市名称。结果应是从根到商店的完整层次结构。在那种情况下:

- USA
 - Texas
  - Dallas
   - Walmart (Area: 30000)
   - Walmart (Area: 40000)

另一个过滤器可能正在过滤商店名称,例如搜索 "MyShop2" 会得到:

- USA
 - Alabama
  - Dothan
   - MyShop2 (Area: 6000)

我对 linq 比较熟悉,所以对城市名称的过滤可能如下所示:

var result =
    from country in countries
    from state in country.States
    from city in state.Cities
    where city.Name.Contains("Dal", StringComparison.OrdinalIgnoreCase)
    select city; 

但那样的话,我只会获取城市和商店。如何在结果的顶部获取层次结构(国家和州)?

第二次搜索也是如此:

 var result =
     from country in countries
     from state in country.States
     from city in state.Cities
     from shop in city.Shops
     where shop.Name.Contains(nameFilter, StringComparison.OrdinalIgnoreCase)
     select shop;

这里只获取没有上面层级的店铺...

由于您需要 select 整个层次结构,因此您需要按最高节点(即国家/地区)对结果进行分组,然后从那里重建

var result =
    from country in countries
    from state in country.States
    from city in state.Cities
    from shop in city.Shops
    where shop.Name.Contains(nameFilter, StringComparison.OrdinalIgnoreCase)
    select new { country, state, city, shop } into p
    group p by p.country into g
    let shops = g.Select(x => x.shop)
    let cities = g.Select(x => x.city).Distinct()
    let states = g.Select(x => x.state).Distinct()
    select new Country()
    {
        Name = g.Key.Name,
        States = states.Select(s => new State()
        {
            Name = s.Name,
            Cities = s.Cities.Intersect(cities).Select(c => new City()
            {
                Name = c.Name,
                Shops = c.Shops.Intersect(shops).ToList()
            }).ToList()
        }).ToList()
    };

从某种意义上说,您在所显示的查询中具有相关路径

var result =
    from country in countries
    from state in country.States
    from city in state.Cities
    where city.Name.Contains("Dal", StringComparison.OrdinalIgnoreCase)
    select (country, state, city);

这将 select 三元组指定到给定城市的路径。现在你想把这样的三元组变成你的 class 结构,所以,好吧,我们就是这样做的。定义一个 Path class:

class FilteredPath
{
    public Country? Country { get; init; }

    public State? State { get; init; }

    public City? City { get; init; }

    public Shop? Shop { get; init; }
}

然后我们写了很多代码:

class FilteredBuilder
{
    private List<Country> _countries = new();

    public FilteredBuilder AddPath(FilteredPath path)
    {
        if (path.Country is null)
        {
            return this;
        }

        if (path.State is null)
        {
            AddFullCountry(path.Country);
        }
        else
        {
            Country country = GetOrCreateBy(_countries, c => c.Name == path.Country.Name);
            country.Name = path.Country.Name;

            AddPathTo(country, path);
        }

        return this;
    }

    private void AddFullCountry(Country country)
    {
        _countries.RemoveAll(c => c.Name == country.Name);
        _countries.Add(country);
    }
    private void AddFullState(Country country, State state)
    {
        country.States.RemoveAll(s => s.Name == state.Name);
        country.States.Add(state);
    }

    private void AddFullCity(State state, City city)
    {
        state.Cities.RemoveAll(c => c.Name == city.Name);
        state.Cities.Add(city);
    }

    private void AddFullShop(City city, Shop shop)
    {
        city.Shops.RemoveAll(s => s.Name == shop.Name && s.Area == shop.Area);
        city.Shops.Add(shop);
    }

    private void AddPathTo(Country country, FilteredPath path)
    {
        Debug.Assert(path.State is not null);
        if (path.City is null)
        {
            AddFullState(country, path.State);
        }
        else
        {
            State state = GetOrCreateBy(country.States, s => s.Name == path.State.Name);
            state.Name = path.State.Name;

            AddPathTo(state, path);
        }
    }

    private void AddPathTo(State state, FilteredPath path)
    {
        Debug.Assert(path.City is not null);
        if (path.Shop is null)
        {
            AddFullCity(state, path.City);
        }
        else
        {
            City city = GetOrCreateBy(state.Cities, s => s.Name == path.City.Name);
            city.Name = path.City.Name;

            AddPathTo(city, path);
        }
    }

    private void AddPathTo(City city, FilteredPath path)
    {
        Debug.Assert(path.Shop is not null);
        AddFullShop(city, path.Shop);
    }

    private static T GetOrCreateBy<T>(ICollection<T> source, Func<T, bool> predicate) where T : new()
    {
        T? result = source.FirstOrDefault(predicate);

        if (result is null)
        {
            result = new T();
            source.Add(result);
        }

        return result;
    }
        
    public List<Country> Build()
    {
        var result = _countries;
        _countries = new();
        return result;
    }
}

像这样启动它:

var result =
    from country in countries
    from state in country.States
    from city in state.Cities
    where city.Name.Contains("Dal", StringComparison.OrdinalIgnoreCase)
    select (country, state, city);

var paths = result.Select(r => new FilteredPath
{
    Country = r.country,
    State = r.state,
    City = r.city
});

var builder = new FilteredBuilder();

foreach (var path in paths)
{
    builder.AddPath(path);
}

var filtered = builder.Build();

如果你想在生产中使用它,需要考虑一些因素:

  1. 查找 Lists 效率低下。您可能希望每个实体都有一个辅助类型,这些实体将具有 Dictionary 以按名称查找其子项,然后在最后转换为您的原始模型。
  2. 肯定有办法让它更通用并避免代码重复,但是,引用 classic,“我没有时间让它变短”。

但是它确实适用于不同类型的查询。你在过滤状态?只需将 null 传递给 FilteredPathCityShop

工作演示:https://dotnetfiddle.net/IHIOZk