Content

  1. Introduction
  2. Definition of car park classes
  3. Definition of a car park entity graph
  4. Instantiating and using the car park entity graph
  5. Copying and cloning the car park entity graph
  6. Using entity graph validation

Introduction

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.

Definition of car park classes

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
{
    public string _id;
    [DataMember]
    public string Id
    {
        get { return _id; }
        set
        {
            if(_id != value)
            {
                _id = value;
                RaisePropertyChanged("Id");
            }
        }
    }
    public Engine Engine { get; set; }
    public ObservableCollection<Wheel> Wheels { get; set; }
    public ObservableCollection<Door> Doors { get; set; }
    public Owner Owner { get; set; }
}

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 };
    }
    public Trailer Trailer { get; set; }
}

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
{
    private EngineType _engineType;
    [DataMember]
    public EngineType EngineType
    {
        get { return _engineType; }
        set
        {
            if(_engineType != value)
            {
                _engineType = value;
                RaisePropertyChanged("EngineType");
            }
        }
    }
}

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 { }

Back to top

Definition of a car park entity graph

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 : Entity
{
    private static 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);
    public static EntityGraphShape Shape { get { return _shape; } }
    public ObservableCollection<Car> Cars { get; set; }
}

As you can see, an EntityGraphShape object is constructed from a collection of strongly-types 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.Engine). 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.

Back to top

Instantiating and using the car park entity graph

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 the entity graph extension methods defined in RiaServicesContrib.DomainServices.Client. Therefore we add the following using statement:

using RiaServicesContrib.DomainServices.Client;

Now we are ready to instantiate an entitygraph 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

Back to top

Copying and cloning the car park entity graph

The entity graph API contains a clone and a copy method, which will duplicate the entities in an entity graph and their associations. The difference between the two is that Clone will copy the values for keys, while Copy doesn't. The result is that a clone is really identical to its source (if you save it to a database, it will complain that an entity with the same key already exists), while a copy has its own identity and can be stored in the database. Both have their typical use cases.

Both for cloning and for copying, only the entities and associations in the entity graph are copied/cloned. Associations to entities outside the graph are left untouched. For non-association properties, only those marked with the [DataMember] attribute are copied/cloned.

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 (or clone) 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 (or cloned).

Back to top

Using entity graph validation

Single entity validation

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. This can be by registering its type, or by registering the assembly that contains this validator. in the latter case, all validators contained in the assembly registered:

MEFValidationRules.RegisterAssembly(typeof(CarPark).Assembly);

Once a car park entity graph is instantiated validation will be performed automatically:

var graph = carPark.EntityGraph(CarPark.Shape);
truck.Doors.Add(new Door());
Assert.IsTrue(truck.HasValidationErrors);

Back to top

Cross entity validation

The previous validation example does not add a lot compared to the standard RIA validation. So lets consider another, 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 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 (Tuck 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 niput 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);

Back to top

Combinatorial validation

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);

Like registering validation rules you can also unregister validation rules. This can also per type (using MEFValidationRules.UnregisterType) or per assembly (using MEFValidationRules.UnregisterAssembly). This will stop evaluating the corresponding validation rules.
Back to top

Last edited Jun 26, 2011 at 8:55 PM by MdeJ, version 17

Comments

No comments yet.