I have a need for customizing creation of a collection, with quite complicated relationships between the objects within it, and I can't figure out how to do it correctly.
For the sake of this issue, let's assume I'm working on a todo app. It has Items and SubItems, and the items have a week number indicating when they should be done:
public class Item {
public string Name { get; set; }
public int Week { get; set; }
public ICollection<SubItem> SubItems { get; set; }
}
public class SubItem {
public string Name { get; set; }
public Item Parent { get; set; }
}
Now, because this is what data usually looks like in the actual application, I want to create a collection of Items that has the following properties:
There are items that have the same name, but different weeks
There are items that have the same week but different name
There are sub-items that have the same name, but different parents
In order to do this, I've created a TodoItemSpecimenBuilder : ISpecimenBuilder which starts its Create method like this:
var type = (request as PropertyInfo)?.PropertyType ?? request as Type;
if (type == null || !typeof(IEnumerable<Item>).IsAssignableFrom(type))
{
return new NoSpecimen();
}
// build up the actual collection
return BuildActualCollection();
However, when I run tests with this specimen builder included in my context, I get lots (maybe 20 or 30) hits on the return statement before I enter even my setup code, and the first time I try to actually CreateMany<Item>(), it blows up with a cast exception because it can't cast OmitSpecimen to Item.
What am I doing wrong here?
Full sample code, compilable after installing NUnit and AutoFixture:
public class TodoList
{
public ICollection<Item> Tasks { get; set; }
}
public class Item
{
public string Name { get; set; }
public Week Week { get; set; }
public ICollection<SubItem> SubItems { get; set; }
public int ItemId { get; set; }
public TodoList TodoList { get; set; }
}
public class SubItem
{
public Item Item { get; set; }
public string Name { get; set; }
public int SortOrder { get; set; }
public string HelpText { get; set; }
}
public class Week
{
public int WeekId { get; set; }
}
public class ItemCollectionSpecimenBuilder : ISpecimenBuilder
{
public object Create(object request, ISpecimenContext context)
{
if (!IsApplicable(request))
{
return new NoSpecimen();
}
var items = new List<Item>(3);
var week1 = context.Create<Week>();
var week2 = context.Create<Week>();
items.Add(CreateItem(context, week1));
items.Add(CreateItem(context, week1));
items.Add(CreateItem(context, week2));
items.GroupBy(t => t.Week).ToList().ForEach(ConfigureNames);
ConfigureSubItems(context, items);
return items;
}
private static bool IsApplicable(object request)
{
bool IsManyItemsType(Type type) => typeof(IEnumerable<Item>).IsAssignableFrom(type);
bool IsItemsType(Type type) => type != null && typeof(Item) == type;
switch (request)
{
case PropertyInfo pInfo:
return IsManyItemsType(pInfo.PropertyType);
case Type type:
return IsManyItemsType(type);
case MultipleRequest multipleRequest:
if (!(multipleRequest.Request is SeededRequest seededRequest))
{
return false;
}
return IsItemsType(seededRequest.Request as Type);
default:
return false;
}
}
private static Item CreateItem(ISpecimenContext context, Week week)
{
var item = context.Create<Item>();
item.Week = week;
return item;
}
private static void ConfigureNames(IEnumerable<Item> items)
{
string name = null;
foreach (var item in items)
{
if (name == null)
{
name = item.Name;
}
else
{
item.Name = name;
}
}
}
private static void ConfigureSubItems(ISpecimenContext context, IEnumerable<Item> items)
{
foreach (var group in items.GroupBy(item => item.Week.WeekId))
{
var subItemTemplates = context.CreateMany<SubItem>().ToList();
foreach (var item in group)
{
item.SubItems.Clear();
foreach (var subItem in context.CreateMany<SubItem>().Zip(subItemTemplates,
(model, subItem) =>
{
subItem.Item = item;
subItem.Name = model.Name;
subItem.SortOrder = model.SortOrder;
subItem.HelpText = model.HelpText;
return subItem;
}))
{
item.SubItems.Add(subItem);
}
}
}
}
}
[TestFixture]
public class AutoFixtureSpecimenBuilderTests
{
private static void TestCreationOfTasks(Func<IFixture, ICollection<Item>> creator)
{
var fixture = new Fixture();
fixture.Customizations.Add(new ItemCollectionSpecimenBuilder());
fixture.Behaviors.OfType<ThrowingRecursionBehavior>().ToList()
.ForEach(b => fixture.Behaviors.Remove(b));
fixture.Behaviors.Add(new OmitOnRecursionBehavior());
var tasks = creator(fixture);
Assert.AreEqual(3, tasks.Count);
Assert.AreEqual(2, tasks.GroupBy(t => t.Week).Count());
Assert.IsTrue(tasks.GroupBy(t => t.Week).Select(g => g.Select(t => t.Name).Distinct()).All(distinctNames => distinctNames.Count() == 1));
var task = tasks.GroupBy(t => t.Week).OrderBy(g => g.Count()).First().OrderBy(t => t.ItemId).First();
}
[Test]
public void CreateMany() => TestCreationOfTasks(fixture => fixture.CreateMany<Item>().ToList());
[Test]
public void CreateWithProperty() => TestCreationOfTasks(fixture => fixture.Create<TodoList>().Tasks);
[Test]
public void CreateAsList() => TestCreationOfTasks(fixture => fixture.Create<IList<Item>>());
}
I can't think of any particularly good way to address this issue. The problem is that Item is a recursive (tree-like) data structure, and while AutoFixture does have some support for such, it's not easily extensible.
When you create an ISpecimenBuilder, you tell AutoFixture that this object is going to handle requests for particular objects. This means that you can no longer use the context to request those objects, because that'll recurse back into the same builder, causing an infinite recursion.
So, one option is to build up the objects 'by hand' from within the builder. You can still request all other types, but you'll have to avoid requesting objects that cause recursion.
Another option is to add a post-processor. Here's a proof of concept:
public class ItemCollectionSpecimenCommand : ISpecimenCommand
{
public void Execute(object specimen, ISpecimenContext context)
{
var #is = specimen as IEnumerable<Item>;
if (#is == null)
return;
var items = #is.ToList();
if (items.Count < 3)
return;
var week1 = context.Create<Week>();
var week2 = context.Create<Week>();
items[0].Week = week1;
items[1].Week = week1;
items[2].Week = week2;
items.GroupBy(t => t.Week).ToList().ForEach(ConfigureNames);
}
private static void ConfigureNames(IEnumerable<Item> items)
{
string name = null;
foreach (var item in items)
{
if (name == null)
name = item.Name;
else
item.Name = name;
}
}
}
You can configure your fixture like this:
var fixture = new Fixture();
fixture.Customizations.Add(
SpecimenBuilderNodeFactory.CreateTypedNode(
typeof(IEnumerable<Item>),
new Postprocessor(
new EnumerableRelay(),
new CompositeSpecimenCommand(
new AutoPropertiesCommand(),
new ItemCollectionSpecimenCommand()))));
fixture.Behaviors.OfType<ThrowingRecursionBehavior>().ToList()
.ForEach(b => fixture.Behaviors.Remove(b));
fixture.Behaviors.Add(new OmitOnRecursionBehavior());
This'll pass the repro tests CreateWithProperty and CreateAsList, but not CreateMany.
For various (historical) reasons, the way that CreateMany works is quite different from the way that something like Create<IList<>> works. If you really need this to work for CreateMany as well, I'll see what I can do, but I can't promise that this'll be possible at all.
After having looked at this repro for a few hours, this is the best I can come up with. I haven't really used AutoFixture for a year or two now, so it's possible that I'm simply out of shape, and that a better solution is available... I just can't think of it...
Related
I need to merge data from tables in the database with data based on some logic from third-party sources. I implemented this logic via hashset, for which I overloaded the GetHashCode and Equals methods for entities. Now I don't understand how I can save the result of work in the database via DbSet, with subsequent data loading and subsequent merging (the task of merging/supplementing is periodic)
The directories are quite voluminous, so working through hashset speeds up the process.
class Program
{
private class DummyDbContext { public void SaveChangesAsync() { }}
static void Main(string[] args)
{
var dbContext = new DummyDbContext(); // TODO: Get from DI
// TODO: I don't know how to do it yet with HashSets
var currentFactories = LoadCurrentFactoriesFromDb(dbContext);
var currentProducts = LoadCurrentProductsFromDb(dbContext);
var thirdPartyData = GetThirdPartyData();
foreach (var data in thirdPartyData)
{
/*
In reality, the logic is more complicated, because some data transformation is required.
Some data may be missing. That is why comparing two objects is not quite easy (see the method Product.Equals)
*/
var factory = new Factory(data.otherFactory.Name);
var product = new Product(data.otherProduct.Property1, data.otherProduct.Property2, factory);
if (currentFactories.TryGetValue(factory, out var existedFactory))
factory = existedFactory;
else
currentFactories.Add(factory);
if (currentProducts.TryGetValue(product, out var existedProduct))
{
if (!existedProduct.Factory.Equals(factory))
throw new InvalidOperationException(); // TODO:
product = existedProduct;
factory.Products.Add(product); // TODO:
}
else
currentProducts.Add(product);
}
// **how to implement the saving of combined directories, in hashsets, in the database ?**
dbContext.SaveChangesAsync();
}
private static IEnumerable<(ThirdPartyFactory otherFactory, ThirdPartyProduct otherProduct)> GetThirdPartyData()
{
return new (ThirdPartyFactory otherFactory, ThirdPartyProduct otherProduct)[]
{
( new ThirdPartyFactory () {Name = "SomeFactory"}, new ThirdPartyProduct() {Property1 = "ProductName1"}),
( new ThirdPartyFactory () {Name = "SomeFactory"}, new ThirdPartyProduct() {Property1 = "ProductName2"}),
( new ThirdPartyFactory () {Name = "SomeFactory"}, new ThirdPartyProduct() {Property2 = "Property1"})
};
}
private static HashSet<Factory> LoadCurrentFactoriesFromDb(DummyDbContext context)
{
// DbContext.DbSet<Factory>.GetAll()
return new HashSet<Factory>();
}
private static HashSet<Product> LoadCurrentProductsFromDb(DummyDbContext context)
{
// DbContext.DbSet<Product>.GetAll()
return new HashSet<Product>();
}
}
public class Product
{
public Product(string property1, string property2, Factory factory)
{
Property1 = property1;
Property2 = property2;
Factory = factory;
}
public long Id { get; set; }
public string Property1 { get; }
public string Property2 { get; }
public Factory Factory { get; }
public override bool Equals(object? obj)
{
if (obj == null)
return false;
var product = (Product) obj;
return (string.IsNullOrWhiteSpace(Property1) && string.IsNullOrWhiteSpace(product.Property1)
|| string.CompareOrdinal(this.Property1, product.Property1) == 0)
&& (string.IsNullOrWhiteSpace(Property2) && string.IsNullOrWhiteSpace(product.Property2)
|| string.CompareOrdinal(this.Property2, product.Property2) == 0);
}
public override int GetHashCode()
{
return HashCode.Combine(Property1, Property2).GetHashCode();
}
}
public class Factory
{
public Factory(string name)
{
Name = name;
}
public long Id { get; set; }
public string Name { get; }
public HashSet<Product> Products { get; set; }
}
public class ThirdPartyProduct
{
public string Property1 { get; set; }
public string Property2 { get; set; }
}
public class ThirdPartyFactory
{
public string Name { get; set; }
}
Is it possible to implement this ? Or do I need to convert data from DbSet to HashSet and then back ? But won't I lose information about entities inside the context during such transformations ?
I'm having problems trying to get Should().BeEquivalentTo() to work with types that derive from a base class and implement a collection interface:
public class Entity
{
public string Id {get; set;}
public string Name {get; set;}
}
public class Derived : Entity, ICollection<Entity>
{
private List<Entity> m_Children = new List<Entity>();
public string Description { get; set; }
public int Count => ((ICollection<Entity>)m_Children).Count;
public bool IsReadOnly => ((ICollection<Entity>)m_Children).IsReadOnly;
public void Add(Entity item)
{
((ICollection<Entity>)m_Children).Add(item);
}
public void Clear()
{
((ICollection<Entity>)m_Children).Clear();
}
public bool Contains(Entity item)
{
return ((ICollection<Entity>)m_Children).Contains(item);
}
public void CopyTo(Entity[] array, int arrayIndex)
{
((ICollection<Entity>)m_Children).CopyTo(array, arrayIndex);
}
public IEnumerator<Entity> GetEnumerator()
{
return ((ICollection<Entity>)m_Children).GetEnumerator();
}
public bool Remove(Entity item)
{
return ((ICollection<Entity>)m_Children).Remove(item);
}
IEnumerator IEnumerable.GetEnumerator()
{
return ((ICollection<Entity>)m_Children).GetEnumerator();
}
}
The Test
[TestMethod]
public void EquivalenceTest()
{
var expected = new Derived
{
Id = "123",
Name = "abc",
Description = "def"
};
var actual = new Derived
{
Id = "121",
Name = "xyz",
Description = "def"
};
actual.Should().BeEquivalentTo(expected); // This succeeds, but should fail
}
The call to BeEquivalentTo seems to be ignoring the properties that are defined in the object, and only treating the object as a collection.
How can I get the framework to check the properties and the contents of the collection?
Edit
It seems like this is a known issue
Does anyone know of a workaround?
It's a known issue when comparing classes that implements IEnumerable and have extra properties to be compared.
Here's a way to hack the comparison.
public class Entity : IEnumerable<int>
{
private int[] ints = new[] { 1 };
public int Id { get; set; }
public string Name { get; set; }
public IEnumerator<int> GetEnumerator() => ((IEnumerable<int>)ints).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable<int>)ints).GetEnumerator();
}
[TestMethod]
public void EquivalenceTest()
{
var expected = new Entity
{
Id = 1,
Name = "abc",
};
var actual = new Entity
{
Id = 1,
Name = "abc",
};
actual.Should().BeEquivalentTo(expected, opt => opt
.Using<Entity>(e =>
e.Subject.Should().Match<Entity>(f => f.Name == e.Expectation.Name)
.And.Subject.Should().Match<Entity>(f => f.Id == e.Expectation.Id)
.And.Subject.Should().BeEquivalentTo(e.Expectation)
)
.WhenTypeIs<Entity>());
}
So right now I am trying to design a new hire program that grants access to active directory groups, generates documents with their information and location.
Right now I am doing this with an enumeration, with a switch statement that sets the details on the ViewModel like this:
case CaneRidgeSettings.Departments.SCSC:
Model.ScannerFolder = #"scan1\Supply Chain Service Center\" + Model.UserId;
Model.ExtensionRanges = "list station 8000 to-ext 8349";
Model.AdministrativeAssistant = Loader.SCSCAdminAssistant;
Model.DuoCode = "Franklin TN - 8175";
Model.PrinterSelectedIndex = (int)CaneRidgeSettings.PrinterGroups.Cane_Ridge_5th_Floor_West;
return await find.FindNextComputer("800SCSC");
The problem I have with this design is that if I ever add more departments to this building, I have to manually update this switch. So I tried a few things around this such as a dictionary, but it didn't seem to bind to a combo-box very well (even when implementing my own INotifyCollectionChanged).
So instead I created an interface that contains this information, for simplicity and length lets just say the interface does this:
public interface IDepartmentInfo
{
string DepartmentName { get; }
List<string> ActiveDirectoryGroups { get; }
string AdministrativeAssistant { get; }
string Floor { get; }
}
I then created a new class that implements this interface
public class SCSC : IDepartmentInfo
{
public string DepartmentName { get; } = "Shared Services";
public List<string> ActiveDirectoryGroups { get; } = new List<string>() {"Example_AD_GRP","Domain_Users"};
public string AdministrativeAssistant { get; } = "Lisa_Smith#outlook.com";
public string Floor { get; } = "5th Floor East";
public override string ToString() => DepartmentName;
}
Then, on my main Building Class I have an observable collection that expects an IDepartmentInfo and initializes those departments
public class CaneRidgeBuilding : IBuilding
{
public ObservableCollection<IDepartmentInfo> Departments { get; set; } = new ObservableCollection<IDepartmentInfo>() {new SCSC(), new ARS()};
public override string ToString()
{
return "CaneRidge";
}
}
On my View Model I implemented a few properties, mainly the BuildingSelectedIndex and the DepartmentSelectedIndex.
I also have an IDepartmentInfo property that notifies when it is changed because it is databound to several labels on my UI.
public class MainWindowViewModel : BindableBase
{
public ObservableCollection<IBuilding> Buildings { get; set; } = new ObservableCollection<IBuilding>() { new CaneRidgeBuilding() };
private ObservableCollection<IDepartmentInfo> _departmentInfos = new ObservableCollection<IDepartmentInfo>();
public ObservableCollection<IDepartmentInfo> DepartmentInfos
{
get { return _departmentInfos; }
set { SetProperty(ref _departmentInfos, value); }
}
private int _buildingIndex = -1;
public int BuildingIndex
{
get { return _buildingIndex; }
set
{
SetProperty(ref _buildingIndex, value);
SetDepartments();
}
}
private void SetDepartments()
{
if (BuildingIndex != -1)
DepartmentInfos = Buildings[BuildingIndex].Departments;
}
private int _departmentIndex = -1;
public int DepartmentIndex
{
get { return _departmentIndex; }
set
{
SetProperty(ref _departmentIndex, value);
LoadDepartmentSettings();
}
}
private IDepartmentInfo _departmentInformation;
public IDepartmentInfo DepartmentInformation
{
get { return _departmentInformation; }
set { SetProperty(ref _departmentInformation, value); }
}
private void LoadDepartmentSettings()
{
if (DepartmentIndex != -1)
DepartmentInformation = DepartmentInfos[DepartmentIndex];
}
private string _title = "Prism Application";
public string Title
{
get { return _title; }
set { SetProperty(ref _title, value); }
}
public MainWindowViewModel()
{
}
}
And it works exactly the way I want it to, however to problem I am running into now is how would I handle dependency injection? If I have 10 departments implementing IDepartmentInfo, how exactly could I pass this to an observable collection?
Because the moment I introduce a new building, if I tell Unity to resolve all IDepartmentInfos, what is going to happen is I'll get every single department even if it doesn't belong to CaneRidge.
If I split the departments to each building, then I run into issues where I can't easily load the departments into the ViewModel, because it is expecting an IDepartmentInfo collection. If I limited it to just one type of collection, then it wouldn't work.
Am I over-complicating things?
Here is an idea.
Custom attribute
Introduce a BuilingAttribute so each IDepartmentInfo implementation can declare Type of the building it belongs to (allow multiple if one department can belong to multiple buildings, I got the idea it can't).
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class BuildingAttribute : Attribute
{
public Type BuildingType { get; private set; }
public BuildingAttribute(Type buildingType)
{
this.BuildingType = buildingType;
}
}
DepartmentInfo Collection Factory
An interface that knows how to create a collection of DepartmentInfo for each building Type.
public interface IDepartmentInfoCollectionFactory
{
void RegisterDepartment<T>(Func<IDepartmentInfo> departmentCreator) where T : class, IBuilding;
ObservableCollection<IDepartmentInfo> GetDepartments<T>() where T : class, IBuilding;
}
And the implementation (will be registered as singleton).
public class DepartmentInfoCollectionFactory : IDepartmentInfoCollectionFactory
{
private readonly Dictionary<Type, List<Func<IDepartmentInfo>>> departmentCreators =
new Dictionary<Type, List<Func<IDepartmentInfo>>>();
void IDepartmentInfoCollectionFactory.RegisterDepartment<T>(Func<IDepartmentInfo> departmentCreator)
{
Type buildingType = typeof(T);
if (!this.departmentCreators.ContainsKey(buildingType))
this.departmentCreators.Add(buildingType, new List<Func<IDepartmentInfo>>());
if (!this.departmentCreators[buildingType].Contains(departmentCreator))
this.departmentCreators[buildingType].Add(departmentCreator);
}
ObservableCollection<IDepartmentInfo> IDepartmentInfoCollectionFactory.GetDepartments<T>()
{
Type buildingType = typeof(T);
if (!this.departmentCreators.ContainsKey(buildingType))
throw new InvalidOperationException(
string.Format("No departments have been registered for {0}.", buildingType.ToString()));
ObservableCollection<IDepartmentInfo> departmentInfos = new ObservableCollection<IDepartmentInfo>();
foreach(Func<IDepartmentInfo> creator in this.departmentCreators[buildingType])
{
departmentInfos.Add(creator());
}
return departmentInfos;
}
}
Configuring the factory, so it knows how to create IDepartmentInfo collections.
protected override void ConfigureContainer()
{
Container.RegisterType<IDepartmentInfoCollectionFactory, DepartmentInfoCollectionFactory>(
new ContainerControlledLifetimeManager());
this.ConfigureDepartmentInfoCollectionFactory(Container.Resolve<IDepartmentInfoCollectionFactory>());
}
private void ConfigureDepartmentInfoCollectionFactory(IDepartmentInfoCollectionFactory factory)
{
// Types implementing IDepartmentInfo
var deptInfoTypes = AppDomain.CurrentDomain
.GetAssemblies()
.SelectMany(s => s.GetTypes())
.Where(t => typeof(IDepartmentInfo).IsAssignableFrom(t) && !t.IsInterface);
foreach(Type type in deptInfoTypes)
{
// Get collection of BuildingAttribute for the type
var buildingAttributes = type.GetCustomAttributes(typeof(BuildingAttribute), false)
.OfType<BuildingAttribute>();
if (buildingAttributes.Count() < 1)
throw new InvalidOperationException(
string.Format("The type {0} didn't declare BuildingArgument.", type.ToString()));
var buildingType = buildingAttributes.First().BuildingType;
if (buildingType == null || !buildingType.GetInterfaces().Contains(typeof(IBuilding)))
throw new InvalidOperationException(
string.Format("{0}: BuildingType is not an IBuilding.", type.ToString()));
var registerMethod = typeof(IDepartmentInfoCollectionFactory).GetMethod("RegisterDepartment")
.MakeGenericMethod(new Type[] { buildingType });
registerMethod.Invoke(factory, new object[]
{
new Func<IDepartmentInfo>(() => (IDepartmentInfo)Container.Resolve(type))
});
}
}
Inject the factory.
public class FooBuilding : IBuilding
{
private IDepartmentInfoCollectionFactory factory;
private readonly ObservableCollection<IDepartmentInfo> departmentInfos;
public string Name { get; } = "FooBuilding";
public ObservableCollection<IDepartmentInfo> DepartmentInfos
{
get { return this.departmentInfos; }
}
public FooBuilding(IDepartmentInfoCollectionFactory factory)
{
this.factory = factory;
this.departmentInfos = factory.GetDepartments<FooBuilding>();
}
}
Adding new department
It doesn't require any editing, just create new class with the attribute.
[Building(typeof(FooBuilding))]
public class BarDepartment : IDepartmentInfo
{
public string Name { get; } = "Bar department";
}
I was able to figure out how to inject different buildings and departments, probably not the best way
EDIT: Updated it to use reflection to make it less maintenance
protected override void ConfigureContainer()
{
base.ConfigureContainer();
Container.RegisterTypes(AllClasses.FromLoadedAssemblies()
.Where(type => typeof(IDepartment).IsAssignableFrom(type)), WithMappings.FromAllInterfaces, WithName.TypeName, WithLifetime.None);
ObservableCollection<IBuilding> Buildings = new ObservableCollection<IBuilding>()
{
Container.Resolve<Building1>(new ParameterOverride("departments",GetDepartmentCollection("Building1"))),
Container.Resolve<Building2>(new ParameterOverride("departments",GetDepartmentCollection("Building2")))
};
Container.RegisterInstance(typeof(ObservableCollection<IBuilding>), Buildings,
new ExternallyControlledLifetimeManager());
}
private ObservableCollection<IDepartment> GetDepartmentCollection(string buildingName)
{
var departments = new List<IDepartment>();
foreach (var registration in Container.Registrations.Where( s => s.MappedToType.Namespace.Contains(buildingName)))
{
departments.Add((IDepartment)Container.Resolve(registration.MappedToType));
}
return new ObservableCollection<IDepartment>(departments);
}
Now I am able to completely eliminate the enumeration and it can be extended in the future without breaking any code or requiring me to change anything.
So I am using reflection to loop through the properties of one object and populating the values on a different object with properties of the same name. This works great but the problem comes when the property type is a collection. I want to be able to loop through each of the objects in the source collection and populate the same list with objects in the source collection.
public class SourceMessage
{
public string Name { get; set; }
public int Version { get; set; }
public IList<ValueDefinition> Values { get; set; }
}
public class ValueDefinition
{
public string Name { get; set; }
public string Value { get; set; }
}
public class TargetObject
{
public TargetObject()
{
Values = new List<TargetValueDefinition>();
}
public string Name { get; set; }
public int Version { get; set; }
public IList<TargetValueDefinition> Values { get; set; }
}
public class TargetValueDefinition
{
public string Name { get; set; }
public string Value { get; set; }
}
Then I use Reflection to populate the target from the source.
public static void PopulateFromMessage<T, TS>(ref T targetEntity, TS message)
{
var sourceType = typeof(TS);
var targetType = typeof(T);
foreach (var targetPropInfo in targetType.GetProperties())
{
if (sourceType.GetProperty(targetPropInfo.Name) != null)
{
var obj = sourceType.GetProperty(targetPropInfo.Name);
if (obj.PropertyType.Namespace == "System.Collections.Generic")
{
//var x = targetType.GetProperty(targetPropInfo.Name);
//PopulateFromMessage(ref x, sourceType.GetProperty(targetPropInfo.Name));
continue;
}
targetPropInfo.SetValue(targetEntity, sourceType.GetProperty(targetPropInfo.Name).GetValue(message), null);
}
}
}
So calling this would be like this:
private void DenormalizeMessage(SourceMessage message)
{
var newTargetObject = new TargetObject();
PopulateFromMessage(ref newTargetObject , message);
}
I can identify when the property is a collection but am uncertain of how to create new TargetValueDefinitions and populate them with the values from ValueDefinitions. In the end it is pretty much a copy of the SourceMessage in the form of a TargetObject.
This all stems from receiving messages and transforming them into objects with the same property names.
If your problem is iterating through items contained inside a single property when it is a collection, then the key would be to read the property value into a dynamic variable and not an object variable that is by default, this way you could use a foreach for it.
dynamic propVal = inputProperty.GetValue(item);
foreach (var subItem in propVal)
{
//do your stuff
}
Disclaimer: This is extremely unsafe to do and makes a lot of assumptions but it should puth you on the right path.
Change you method to this:
public static void PopulateFromMessage<T, TS>(T targetEntity, TS message)
{
var sourceType = typeof (TS);
var targetType = typeof (T);
foreach (var targetPropInfo in targetType.GetProperties())
{
if (targetPropInfo.PropertyType.IsGenericType)
{
if (targetPropInfo.PropertyType.GetGenericTypeDefinition() == typeof(IList<>))
{
var originalList = sourceType.GetProperty(targetPropInfo.Name).GetValue(message) as IList;
if (originalList != null)
{
var argumentType = targetPropInfo.PropertyType.GetGenericArguments();
var listType = typeof (List<>);
var concreteType = listType.MakeGenericType(argumentType);
var newList = Activator.CreateInstance(concreteType) as IList;
foreach (var original in originalList)
{
var targetValue = Activator.CreateInstance(argumentType[0]);
// do this yourself. Here we're converting ValueDefinition to TargetValueDefinition
// targetValue.Fill(original);
}
targetPropInfo.SetValue(targetEntity, newList);
}
}
}
else
{
if (sourceType.GetProperty(targetPropInfo.Name) != null)
{
var obj = sourceType.GetProperty(targetPropInfo.Name);
if (obj.PropertyType.Namespace == "System.Collections.Generic")
{
//var x = targetType.GetProperty(targetPropInfo.Name);
//PopulateFromMessage(ref x, sourceType.GetProperty(targetPropInfo.Name));
continue;
}
targetPropInfo.SetValue(targetEntity, sourceType.GetProperty(targetPropInfo.Name).GetValue(message), null);
}
}
}
}
You should create a interface for each class (implement the methods and properties on interface) and implement it in each class. After, in function PopulateFromMessage should specify the interface allowed in method, with this you can use directly the properties of class with T and TS generic types.
I am building a treeview with a list of ScanItem. The class of ScanItem is actually:
public class ScanItem
{
public string FullPath { get; set; }
public string Name
{
get
{
return Path.GetFileName(FullPath);
}
}
public DateTime ModifiedDate { get; set; }
public DateTime CreatedDate { get; set; }
public FileAttributes Attributes { get; set; }
public bool IsDirectory { get; set; }
public string Extension
{
get
{
if (IsDirectory)
return "Folder";
else
return Path.GetExtension(Name);
}
}
public UInt64 Size { get; set; }
}
In order for me to create a treeview I needed to create two other classes in order to distinguish the folders and files in my treeview:
public class ScanFile : ScanItem
{
}
public class ScanDir : ScanItem
{
public List<ScanItem> Items { get; set; }
public ScanDir()
{
Items = new List<ScanItem>();
}
}
Note that the class ScanFile is just like the ScanItem and the ScanDir class has an extra property called Items and will contain a list of items of itself.
So if I where to iterate through this direcotory (C:\Temp):
my List will actually contain:
note that if I expand one ScanDir object I will get another List:
in order to populate the following treeview:
So I was able to populate this list using recursion by searching for files and directories in a specific path.
I just wanted to explain my situation because there are several places in the internet that enable you to filter a treeview and that is what I actually want to do. But it will be nice if I can iterate through each item in List and then remove it if some criteria is not met:
I have actually tried using the following recursive method to filter my results.
public List<ScanItem> search(List<ScanItem> items)
{
var filter = new List<ScanItem>();
foreach (var item in items)
{
if (!item.FullPath.Contains("stringIwantToLookFor")) continue;
filter.Add(item);
if (item.IsDirectory)
{
search(((ScanDir)item).Items);
}
}
return filter;
}
I think that if an item is found I need to add all the parent root directories and that's why it does not work. The reason why I want to build my own recursion method is because I want to be able to filter the treeview based on spesific criteria.
EDIT:
In other words if I want to have all the items that contain "X.txt" in my listview I want to just see:
I would do it like this: create public abstract ScanItem Seach(string s) on your ScanItem. You can then call it with the string you want to search for.
The actual implementation would look like this:
ScanFile:
public override ScanItem Seach(string s)
{
if (Name.Contains(s))
return this;
return null;
}
ScanDir:
public override ScanItem Seach(string s)
{
var results = Items.Select(i => i.Seach(s)).Where(i => i != null).ToList();
if (results.Any())
{
var result = (ScanDir)MemberwiseClone();
result.Items = results;
return result;
}
return null;
}
The implementation in ScanFile is easy: if the file matches, return it, else return null. In ScanDir, call Search on all child items recursively. If any of them returned non-null, create a copy of the current object and set the Items of the copy only to those that matched. If none matched, return null.
Note that this will search only through the names of files, not directories. But if you want to do that, such modification is going to be straight-forward.
You should treat the directories a little different because now, if the root directory does not meet the criteria the routine will exit immediately.
Try this: change your ScanItem a little:
public class ScanItem {
...
public virtual bool IsDirectory { get; }
...
}
add this to your scanFile:
public class ScanFile : ScanItem {
public override bool IsDirectory {
get { return false; }
}
}
and this to your scanDir:
public class ScanDir : ScanItem {
public List<ScanItem> Items { get; set; }
public ScanDir() {
Items = new List<ScanItem>();
}
public ScanDir CopyWithoutChildren() {
return new ScanDir() {
FullPath = this.FullPath,
ModifiedDate = this.ModifiedDate,
CreatedDate = this.CreatedDate,
Attributes = this.Attributes,
Size = this.Size
};
}
public override bool IsDirectory {
get { return true; }
}
}
Now do the filtering on the files, omitting empty directories:
public List<ScanItem> search(List<ScanItem> items) {
var filter = new List<ScanItem>();
foreach(var item in items) {
if(item.IsDirectory) {
List<ScanItem> potential = search(((ScanDir)item).Items);
if(potential.Count > 0) {
ScanDir dir = ((ScanDir)item).CopyWithoutChildren();
dir.Items.AddRange(potential);
filter.Add(dir);
}
} else {
if(!item.FullPath.Contains("stringIwantToLookFor")) continue;
filter.Add(item);
}
}
return filter;
}
I didn't test it, but I guess that should do what you want.
I realized my comment to your post might not have been descriptive enough, so I've written some C#-ish pseudocode to demonstrate what I was getting at.
Here's an example of using the Visitor pattern to implement search in a polymorphic, loosely-coupled way:
interface FilesystemVistor
{
void Visit (FilesystemItem item);
}
interface FilesystemItem
{
void Accept(FilesystemVistor visitor);
string Name;
}
class Directory : FilesystemItem
{
private FilesystemItem[] _children;
public void Accept(FilesystemVistor visitor) {
visitor.Visit(this);
foreach(FilesystemItem item in _children)
{
visitor.Visit(item);
}
}
}
class File : FilesystemItem
{
public void Accept(FilesystemVistor visitor) {
visitor.Visit(this);
}
}
class FilesystemSearcher : FilesystemVistor
{
private List<string> _results;
public void Visit(FilesystemItem item) {
if (item.Name == "Foo") { _results.Add(item.Name); }
}
}
This "visitor pattern"-based design will allow you to implement any kind of search without having the search algorithm having to "know" anything about the structure of the file system and the file system doesn't need an extra property like "IsDirectory" to expose its implementation details.
so If I am looking for the files that contain foo this method will populate the files that contain foo in the list 'newList' . I would have to set that list equal to a new list before calling that method. I am obviously missing basic implementation such as changing foo for a parameter etc. I am also missing to remove the empty directories I am working on that.
private List<ScanDir> history = new List<ScanDir>();
private ScanDir LastDir;
private List<ScanItem> newList = new List<ScanItem>();
public void Search(List<ScanItem> allItems) //adds files that contain foo
{
bool updateLastDir = false;
foreach(ScanItem s in allItems)
{
if (updateLastDir)
{
history = (from a in history
select a).Distinct().ToList();
LastDir = null;
for (int i = history.Count - 1; i >= 0; i--)
{
if (history[i].FullPath == Directory.GetParent(s.FullPath).ToString())
{
LastDir = history[i];
break;
}
}
updateLastDir = false;
}
if (s.IsDirectory)
{
var temp = new ScanDir { FullPath = s.FullPath, IsDirectory = true, comparePath = s.comparePath, Attributes = s.Attributes };
if (LastDir == null)
{
newList.Add(temp);
}
else
{
LastDir.Items.Add(temp);
}
LastDir = temp;
history.Add(LastDir);
Search(((ScanDir)s).Items);
history.RemoveAt(history.Count - 1);
updateLastDir = true;
}
else
{
if (s.Name.Contains("Foo")) // then add it
{
if (LastDir == null)
newList.Add(s);
else
LastDir.Items.Add(s);
}
}
}
}