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();
如果你想在生产中使用它,需要考虑一些因素:
- 查找
List
s 效率低下。您可能希望每个实体都有一个辅助类型,这些实体将具有 Dictionary
以按名称查找其子项,然后在最后转换为您的原始模型。
- 肯定有办法让它更通用并避免代码重复,但是,引用 classic,“我没有时间让它变短”。
但是它确实适用于不同类型的查询。你在过滤状态?只需将 null
传递给 FilteredPath
的 City
和 Shop
。
我正在编写一个包含如下嵌套结构的应用程序:
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();
如果你想在生产中使用它,需要考虑一些因素:
- 查找
List
s 效率低下。您可能希望每个实体都有一个辅助类型,这些实体将具有Dictionary
以按名称查找其子项,然后在最后转换为您的原始模型。 - 肯定有办法让它更通用并避免代码重复,但是,引用 classic,“我没有时间让它变短”。
但是它确实适用于不同类型的查询。你在过滤状态?只需将 null
传递给 FilteredPath
的 City
和 Shop
。