-
Notifications
You must be signed in to change notification settings - Fork 0
EntityGraphCarParkExample
- Introduction
- Definition of car park classes
- Definition of a car park entity graph
- Instantiating and using the car park entity graph
- Copying and cloning the car park entity graph
- Using entity graph validation
On this page we will demonstrate the use of EntityGraph and its validation mechanism by means of a simple CarPark example. We will show how to define an entity graph for a collection of related entities and how validation rules can be defined that span multiple entities in the graph.
We start the example with defining the classes that we need to build a car park. CarPark
is a simple class that represents a collection of Cars. It is defined as follows:
public class CarPark
{
public ObservableCollection<Car> Cars { get; set; }
}
Car
is an abstract base class. Each car has an Id (we'll show later how to use validation to enforce uniqueness). Furthermore, a car has an engine, a collection of wheels, and a collection of doors. A car has a single owner:
public abstract class Car : INotifyPropertyChanged
{
private string _id;
public string Id
{
get => _id;
set
{
if (_id != value)
{
_id = value;
OnPropertyChanged();
}
}
}
public ObservableCollection<Wheel> Wheels { get; set; }
public ObservableCollection<Door> Doors { get; set; }
public Engine Engine { get; set; }
public Owner Owner { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Truck
is an instance of a Car
. It has six wheels and two doors. Furthermore, it has a diesel engine and it can have a trailer:
public class Truck : Car
{
public Truck()
{
Wheels = new ObservableCollection<Wheel>
{
new Wheel(),
new Wheel(),
new Wheel(),
new Wheel(),
new Wheel(),
new Wheel()
};
Doors = new ObservableCollection<Door>
{
new Door(),
new Door()
};
Engine = new Engine {EngineType = EngineType.Diesel};
}
private Trailer _trailer;
public Trailer Trailer
{
get => _trailer;
set
{
if (_trailer == value) return;
_trailer = value;
OnPropertyChanged();
}
}
}
PersonCar
is another instance of Car
, which has four wheels and five doors:
public class PersonCar : Car
{
public PersonCar()
{
Wheels = new ObservableCollection<Wheel>
{
new Wheel(),
new Wheel(),
new Wheel(),
new Wheel()
};
Doors = new ObservableCollection<Door>
{
new Door(),
new Door(),
new Door(),
new Door(),
new Door()
};
Engine = new Engine {EngineType = EngineType.Benzin};
}
}
Next, we define the class Engine
, which has a single property, EngineType
, that indicates the type of the engine:
public class Engine : INotifyPropertyChanged
{
private EngineType _engineType;
public EngineType EngineType
{
get => _engineType;
set
{
if (_engineType == value) return;
_engineType = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
EngineType
is a simple enum type with three elements:
public enum EngineType
{
Diesel,
Benzin,
Gaz
}
Next, we need (dummy) definitions for owner, wheels, doors, and trailer:
public class Owner { }
public class Wheel { }
public class Door { }
public class Trailer { }
Now that we have defined all entity classes for this example, we can define an entity graph that spans (a subset of) the associations between the entities. An entity graph is defined by its shape (an instance of the class EntityGraphShape
). A shape is defined in terms of a collection of edges. In this example we define an entity graph for CarPark
and include all associations except for the association from Car
to Owner
. We create the shape by extending CarPark
with the Shape
property:
public class CarPark
{
public ObservableCollection<Car> Cars { get; set; }
public static readonly EntityGraphShape Shape =
new EntityGraphShape()
.Edge<CarPark, Car>(carPark => carPark.Cars)
.Edge<Car, Wheel>(car => car.Wheels)
.Edge<Car, Door>(car => car.Doors)
.Edge<Car, Engine>(car => car.Engine)
.Edge<Truck, Trailer>(truck => truck.Trailer);
}
As you can see, an EntityGraphShape
object is constructed from a collection of strongly-typed edges. Each edge is a lambda expression that maps an entity type to an association (e.g., Car => Car.Engine
) or association collection (e.g., Car => car.Wheels
). This graph shape defines that all cars of a car park are included, that the wheels, doors, and engine of a car are included, and that the trailer of a truck is included.
Before instantiating a car park entity graph, we first instantiate a CarPark
object:
var truck = new Truck { Id = "1" };
var personCar = new PersonCar { Id = "2" };
var carPark = new CarPark
{
Cars = new ObservableCollection<Car> { truck, personCar };
}
For instantiating an entity graph we make use of entity graph extension methods defined in OpenSoftware.EntityGraph.Net
. Therefore we add the following using statement:
using OpenSoftware.EntityGraph.Net;
Now we are ready to instantiate an entity graph according to the graph shape that we defined earlier:
var graph = carPark.EntityGraph(CarPark.Shape);
Now we can use the graph, for instance, by iterating over its elements:
foreach(var car in graph.OfType<Car>())
{
Console.WriteLine(car.Id);
}
EntityGraph keeps track of entities being added or removed from the graph. This means that if a new car is added to the car park, the entity graph is extended:
Console.WriteLine(graph.OfType<Car>().Count(); // outputs 2
carPark.Cars.Add(new PersonCar());
Console.WriteLine(graph.OfType<Car>().Count(); // outputs 3
The entity graph API contains a copy method, which will duplicate the entities in an entity graph and their associations. Only the entities and associations in the entity graph are copied. Associations to entities outside the graph are left untouched. Only associations and properties are copied.
For example, lets create an owner for the truck and personCar in our initial carPark;
var owner = new Owner();
truck.Owner = owner;
personCar.Owner = owner;
If we now copy the car park as follows:
var copy = graph.Copy();
We get a new car park with a new truck and a new person car, both with new wheels, doors, and engines. However, the new cars have the same owner. In other words, because Car.Owner is not an edge in the entity graph, truck.Owner and personCar.owner are not copied.
Our first example of a validation rule is a rule that checks that a truck has only two doors:
public class TruckDoorsValidator : ValidationRule
{
public TruckDoorsValidator() :
base(InputOutput<Truck, IEnumerable<Door>>(truck => truck.Doors))
{ }
public ValidationResult Validate(IEnumerable<Door> doors)
{
if(doors.Count() > 2)
{
return new ValidationResult("Truck has max 2 doors.");
}
else
{
return ValidationResult.Success;
}
}
}
This class defines a validation rule, which is expressed in the method Validate
and a signature for that rule defined in the constructor. The signature consists of a single input-output parameter, which is a path expression that starts from an entity of type Truck
and ends at a property of type IEnumerable<Door>
. An input-output parameter indicates that the validation rule is triggered when the property is changed and that a validation error is set for this property when validation fails (we'll see an example of an input-only parameter in a minute).
To enable evaluation of this rule, we have to register it. We do this by setting the Validator
property of the entitygraph:
graph.Validator = new ValidationEngine
{
RulesProvider = new SimpleValidationRulesProvider<ValidationResult> {new TruckDoorsValidator()}
};
This initializes a new validation engine for the graph with a simple rules provider. A rules provider, as it name suggests, provides validation rules to the validation engine. You can create your own rules providers. For instance, to retrieve all validators from a given set of assemblies. The provider used in this example simply consumes a list of pre-defined validation rules. The validation engine is used by the entity graph to validate the controlled entities according to the rules of the rules provider when properties or collections change in the entity graph.
After a validation engine is instantiated, the entity graph will use it to validate its controlled entities.
var graph = carPark.EntityGraph(CarPark.Shape);
graph.Validator = new ValidationEngine
{
RulesProvider = new SimpleValidationRulesProvider<ValidationResult> {new TruckDoorsValidator()}
};
truck.Doors.Add(new Door());
Assert.IsTrue(truck.HasValidationErrors);
The previous validation example is performing a rather basic validation rule. So lets consider more advanced, cross entity, validation rule:
public class TruckEngineValidator : ValidationRule
{
public TruckEngineValidator() :
base(
InputOutput<Truck, Engine>(truck => truck.Engine),
InputOnly<Truck, EngineType>(truck => truck.Engine.EngineType)
)
{ }
public ValidationResult Validate(Engine engine, EngineType engineType)
{
if(engineType != EngineType.Diesel)
{
return new ValidationResult("Truck should have a diesel engine.");
}
else
{
return ValidationResult.Success;
}
}
}
This validator checks if a truck has a diesel engine. Two parameter expressions are used. The first is a path to a truck's engine, the second to the engine type of an engine. Observe that this validator is an example of cross-entity validation because two different entities are involved (Truck and Engine). The validator is invoked when either the Engine property changes or the type of an engine. In any case we will never put a validation error on the engine but only on the truck. The second input parameter is therefore an input-only parameter. It will trigger validation, but will never put the owning entity (Engine in the example) in a validation error state.
Observe that the Validate
method only performs validation and sets the Result
property to some error string in case of a validation error. The method does not attach validation errors to properties. It is the underlying validation engine that will distribute the validation errors across the properties of the involved entities according to the validation rule's signature:
Assert.IsFalse(truck.HasValidationErrors);
Assert.IsFalse(truck.Engine.HasValidationErrors);
truck.Engine.EngineType = EngineType.Benzin;
Assert.IsTrue(truck.HasValidationErrors);
Assert.IsFalse(truck.Engine.HasValidationErrors);
truck.Engine.EngineType = EngineType.Diesel;
Assert.IsFalse(truck.HasValidationErrors);
Assert.IsFalse(truck.Engine.HasValidationErrors);
The previous example demonstrated the use of cross-entity validation where the involved entities are associated (e.g., there is a relation between a truck and its engine). In the next example we show how cross-entity validation can also be used to put constraints on unrelated entities, which results in combinatorial validation of entities. The validation rule below puts a uniqueness constraint on car ids:
public class UniqIds : ValidationRule
{
public UniqIds()
: base(
InputOutput<Car, string>(car1 => car1.Id),
InputOutput<Car, string>(car2 => car2.Id)
)
{ }
public ValidationResult Validate(string carId1, string carId2)
{
if(carId1 == carId2)
{
return new ValidationResult("Car ids should be unique");
}
else
{
return ValidationResult.Success;
}
}
}
The signature of this rule consists of two path expressions from Car
to Id
. Since the lambda's arguments have different names (i.e., car1
and car2
), they can be bound to different Car
instances. This results in a computation of the different permutations of cars in the entity graph and evaluation of the validation rule for each of them. That is, if the Id if a car changes, all permutations that include that car are evaluated:
Assert.IsFalse(truck.HasValidationErrors);
Assert.IsFalse(personCar.HasValidationErrors);
truck.Id = personCar.Id; // make the truck id equal to personcar's id
Assert.IsTrue(truck.HasValidationErrors);
Assert.IsTrue(personCar.HasValidationErrors);
personCar.id = "1"; // change personcar's id to make unique
Assert.IsFalse(truck.HasValidationErrors);
Assert.IsFalse(personCar.HasValidationErrors);