Using the EntityCollection<T> class

Adapter contains a general purpose, generic, EntityCollection<T> class. The EntityCollection<T> class is located in the HelperClasses namespace in the database generic project. This class is used to work on more than one entity at the same time and it is used to retrieve more than one entity of the same type from the database. This section describes the functionality bundled in the EntityCollection<T> class and how to utilize that functionality in your code.

The EntityCollection<T> class derives from the base class EntityCollectionBase2, which is a class in the ORMSupportClasses assembly. Besides the generic EntityCollection<T> class, the generated code also contains a non-generic EntityCollection class, which derives from the also non-generic class EntityCollectionNonGeneric and is provided for backwards compatibility.

Entity retrieval into an entity collection object

Entity collection objects can be filled with entities retrieved from the database using several ways. Below we'll walk you through the ones you will use the most.

Using a query

Querying for a set of entities and fetch them into an EntityCollection<T> can be done in several ways. Below we'll fetch all Customer instances which match the filter Country=='Germany'. This is illustrated using three different queries: one using our low-level native Api, one using our QuerySpec API and one using Linq to LLBLGen Pro. When using QuerySpec or Linq to LLBLGen Pro, be sure to have the correct namespaces included in your code file.

High-level APIs (recommended):

var customers = new EntityCollection<CustomerEntity>();
using(var adapter = new DataAccessAdapter())
{
    var qf = new QueryFactory();
    var q = qf.Customer.Where(CustomerFields.Country == "Germany");
    adapter.FetchQuery(q, customers);
}
Dim customers As New EntityCollection(Of CustomerEntity)()
Using adapter As New DataAccessAdapter()
    Dim qf As new QueryFactory()
    Dim q As qf.Customer.Where(CustomerFields.Country = "Germany")
    adapter.FetchQuery(q, customers)
End Using
IEntityCollection2 customers;
using(var adapter = new DataAccessAdapter())
{
    var metaData = new LinqMetaData(adapter);
    var q = from c in metaData.Customer where c.Country == "Germany" select c;
    // use the Execute method to fill the collection. ToList() would re-enumerate
    // the data into a List<T> and not return an IEntityCollection2.
    // ToList() of course works too, Execute is mentioned here to illustrate how
    // to obtain the data into an EntityCollection<T>.
    customers = ((ILLBLGenProQuery)q).Execute<EntityCollection<CustomerEntity>>();
}
Dim customers As IEntityCollection2
Using adapter As New DataAccessAdapter()
    Dim metaData As new LinqMetaData(adapter)
    Dim q = From c in metaData.Customer Where c.Country = "Germany" Select c
    ' use the Execute method to fill the collection. ToList() would re-enumerate
    ' the data into a List(Of T) and not return an IEntityCollection2.
    ' ToList() of course works too, Execute is mentioned here to illustrate how
    ' to obtain the data into an EntityCollection(Of T).
    customers = CType(q, ILLBLGenProQuery).Execute(Of EntityCollection(Of CustomerEntity))()
End Using

Low-level API:

EntityCollection<CustomerEntity> customers = new EntityCollection<CustomerEntity>();
RelationPredicateBucket filter = new RelationPredicateBucket(CustomerFields.Country=="Germany");
using(DataAccessAdapter adapter = new DataAccessAdapter())
{
    adapter.FetchEntityCollection(customers, filter);
}
Dim customers As New EntityCollection(Of CustomerEntity)()
Dim filter As New RelationPredicateBucket(CustomerFields.Country="Germany")
Using adapter As new DataAccessAdapter()
    adapter.FetchEntityCollection(customers, filter)
End Using

In the above examples, the collection fetched is created outside the using block with the adapter. For Linq and QuerySpec the fetch methods which return typed collection objects are used. See for alternatives and other ways to fetch collections using a query the sections for the particular query system, e.g. Linq to LLBLGen Pro or QuerySpec.

Another way to fetch a set of entities is by using a related entity as a filter. For example, if you want to retrieve all Order entities for the Customer entity with pk value CHOPS into the Customer's Orders collection, you can use the following code. In the following example, the myCustomer object contains the already fetched Customer entity with pk value CHOPS.  There are no Linq examples given as Linq can't directly fetch into an existing container/collection.

// Uses EXISTS query.
using(var adapter = new DataAccessAdapter())
{
    var qf = new QueryFactory();
    var q = qf.Order
                .Where(qf.Customer
                   .CorrelatedOver(OrderEntity.Relations.CustomerEntityUsingCustomerId)
                   .Contains(myCustomer));
    adapter.FetchQuery(q, myCustomer.Orders);
}

// or, using direct FK filter:
using(var adapter = new DataAccessAdapter())
{
    var qf = new QueryFactory();
    var q = qf.Order.Where(OrderFields.CustomerId == myCustomer.CustomerId);
    adapter.FetchQuery(q, myCustomer.Orders);
}

// or, using a join
using(var adapter = new DataAccessAdapter())
{
    var qf = new QueryFactory();
    var q = qf.Order
                .From(QueryTarget.InnerJoin(OrderEntity.Relations.CustomerEntityUsingCustomerId))
                .Where(CustomerFields.CustomerId==myCustomer.CustomerId);
    adapter.FetchQuery(q, myCustomer.Orders);
}
' Uses EXISTS query.
Using adapter As New DataAccessAdapter()
    Dim qf As new QueryFactory()
    Dim q = qf.Order _
                .Where(qf.Customer _
                    .CorrelatedOver(OrderEntity.Relations.CustomerEntityUsingCustomerId) _
                    .Contains(myCustomer))
    adapter.FetchQuery(q, myCustomer.Orders)
End Using

' or, using direct FK filter:
Using adapter As New DataAccessAdapter()
    Dim qf As new QueryFactory()
    Dim q = qf.Order.Where(OrderFields.CustomerId = myCustomer.CustomerId)
    adapter.FetchQuery(q, myCustomer.Orders)
End Using

' or, using a join
Using adapter As New DataAccessAdapter()
    Dim qf As new QueryFactory()
    Dim q = qf.Order _
                .From(QueryTarget.InnerJoin(OrderEntity.Relations.CustomerEntityUsingCustomerId)) _
                .Where(CustomerFields.CustomerId=myCustomer.CustomerId)
    adapter.FetchQuery(q, myCustomer.Orders)
End Using
// Will use JOIN query.
using(DataAccessAdapter adapter = new DataAccessAdapter())
{
    // Use helper method to produce RelationPredicateBucket with relation + predicate.
    // You adjust this object with additional filters or create your own.
    adapter.FetchEntityCollection(myCustomer.Orders, myCustomer.GetRelationInfoOrders());
}
Using adapter As New DataAccessAdapter()
    ' Use helper method to produce RelationPredicateBucket with relation + predicate.
    ' You adjust this object with additional filters or create your own.
    adapter.FetchEntityCollection(myCustomer.Orders, myCustomer.GetRelationInfoOrders())
End Using

If Order is in an inheritance hierarchy, the fetch is polymorphic. This means that if the Customer entity, in this case Customer with pk value CHOPS, has references to instances of different derived types of Order, every instance in customer.Orders is of the type it represents, which effectively means that not every instance in Orders is of the same type. See for more information about polymorphic fetchs also Polymorphic fetches.

Using a prefetch path

An alternative way to retrieve a set of entities can be by using a Prefetch Path, which is the mechanism in LLBLGen Pro to use eager loading: fetch related entities together with the main entity or entities to fetch. See for more information about Prefetch Paths and how to use them: Prefetch Paths.

Entity data manipulation using collection classes

When you have fetched a set of entities in a collection and for example have bound this collection to a grid, the user probably has altered one or more entity's fields in the collection. When you want to save these changes to the persistent storage, the easiest way to do so is to use the SaveEntityCollection() method of the DataAccessAdapter class which traverses all entity class instances inside the collection and, if the object is dirty, (which means, it's been changed and should be updated in the persistent storage) it is saved.

The complete save action is done in a single transaction if no transaction is currently available, otherwise the action is performed inside the existing transaction. (See for more information about transactions the section Transactions).

Deleting one or more entities from the persistent storage

To delete a set of entities from the persistent storage, you can use the DeleteEntityCollection() method of the DataAccessAdapter object to achieve your goal. This method works with the objects inside the collection and deletes them one by one from the persistent storage inside a transaction if no transaction is currently available, otherwise the action is performed inside the existing transaction. (See for more information about transactions the section Transactions).

Client side filtering and sorting

The EntityCollection<T> class doesn't implement IBindingList, as the EntityCollection<T> class uses the EntityView2<T> class to bind to grids and other controls and let these views do the filtering and sorting of the entity collection data. It's recommended you use an EntityView2<T> class to sort and filter an EntityCollection<T> if you want to sort / filter in-place.

See for more information about EntityView2<T> classes: Generated code - using entity views with entity collections. As EntityCollection<T> implements IEnumerable<T> you can also use Linq to Objects to sort and filter the entities inside the collection. With both methods (EntityView2 and Linq to Objects) the data inside the collection isn't altered nor sorted.

Hierarchical projections of entity collections

LLBLGen Pro allows you to create projections of the full graph of all the entities inside a given entity collection onto a DataSet or a Dictionary object. A hierarchical projection is a projection where all entities in the entity collection plus all their related entities and so on, are grouped together per entity type.

Say you have the following graph in memory: a set of CustomerEntity instances, which contain each a set of OrderEntity instances and each OrderEntity instance refers to an EmployeeEntity instance. This projection functionality is implemented on the EntityCollection<T>, in the method CreateHierarchicalProjection.

With LLBLGen Pro it's possible to project this graph onto a DataSet which will result in per entity type a new DataTable object with all instances of that entity type (and the data relations setup correctly). You can also project it onto a Dictionary with per entity type an entity collection which contains the entities of that type.

Projections are defined in instances of the IViewProjectionData interface which is implemented in the ViewProjectionData class. This class combines per type projections (as shown below in the example) which are then used as one projection on the complete graph.

By default, when projecting to a DataSet, only the entity types which have instances in the graph get a DataTable in the resulting DataSet. If you want to have a DataSet where there are always an expected number of DataTable instances (so for entities which aren't in the graph, they're empty), you can pre-create the DataSet and pass the pre-created DataSet to the projection routine.

LLBLGen Pro's runtime library contains helper routines to produce an empty DataSet with empty DataTables, the correct columns and the proper DataRelation objects setup based on a prefetch path specified. Please consult the LLBLGen Pro Reference Manual for the GeneralUtils.ProduceEmptyDataSet and GeneralUtils.ProduceEmptyDataTable methods.

Examples

The following examples will show you both projections (to DataSet and to Dictionary) of the earlier described graph of customers - Orders - Employees. The examples will first fetch the complete graph of customers, orders and employees and will then create a projection of that graph. Usage of custom projections per property and additional filters is also shown by the examples.

Please refer to the LLBLGen Pro reference manual for details about the generic ViewProjectionData class and its constructors. The queries are using the low-level API, but you can also use Linq to LLBLGen Pro or QuerySpec for these queries.

Projection to DataSet

EntityCollection<CustomerEntity> customers = new EntityCollection<CustomerEntity>();
PrefetchPath2 path = new PrefetchPath2(EntityType.CustomerEntity);
path.Add(CustomerEntity.PrefetchPathOrders).SubPath.Add(OrderEntity.PrefetchPathEmployees);
using(DataAccessAdapter adapter = new DataAccessAdapter())
{
    adapter.FetchEntityCollection(customers, null, path);
}

// setup projections per type.
List<IEntityPropertyProjector> customerProjections = EntityFields2.ConvertToProjectors(
        EntityFieldsFactory.CreateEntityFieldsObject(EntityType.CustomerEntity));
// add an additional projector so the destination DataTable will have an additional column called 'IsNew' with
// the value of the IsNew property of the customer entities.
customerProjections.Add(new EntityPropertyProjector(new EntityProperty("IsNew"), "IsNew"));
List<IEntityPropertyProjector> orderProjections = EntityFields2.ConvertToProjectors(
        EntityFieldsFactory.CreateEntityFieldsObject(EntityType2.OrderEntity));
List<IEntityPropertyProjector> employeeProjections = EntityFields.ConvertToProjectors(
        EntityFieldsFactory.CreateEntityFieldsObject(EntityType2.EmployeeEntity));

List<IViewProjectionData> projectionData = new List<IViewProjectionData>();
// create the customer projection information. Specify a filter so only customers from Germany
// are projected.
projectionData.Add(new ViewProjectionData<CustomerEntity>(
        customerProjections, (CustomerFields.Country == "Germany"), true));
projectionData.Add(new ViewProjectionData<OrderEntity>(orderProjections, null, false));
projectionData.Add(new ViewProjectionData<EmployeeEntity>(employeeProjections));

DataSet result = new DataSet("projectionResult");
customers.CreateHierarchicalProjection(projectionData, result);
Dim customers As New EntityCollection(Of CustomerEntity)()
Dim path As New PrefetchPath2(EntityType.CustomerEntity)
path.Add(CustomerEntity.PrefetchPathOrders).SubPath.Add(OrderEntity.PrefetchPathEmployees)
Using adapter As New DataAccessAdapter()
    adapter.FetchEntityCollection(customers, Nothing, path)
End Using

' setup projections per type.
Dim customerProjections As List(Of IEntityPropertyProjector) = EntityFields2.ConvertToProjectors( _
        EntityFieldsFactory.CreateEntityFieldsObject(EntityType.CustomerEntity))
' add an additional projector so the destination DataTable will have an additional column called 'IsNew' with
' the value of the IsNew property of the customer entities.
customerProjections.Add(New EntityPropertyProjector(New EntityProperty("IsNew"), "IsNew"))
Dim orderProjections  As List(Of IEntityPropertyProjector) = EntityFields2.ConvertToProjectors( _
        EntityFieldsFactory.CreateEntityFieldsObject(EntityType.OrderEntity))
Dim employeeProjections As List(Of IEntityPropertyProjector) = EntityFields2.ConvertToProjectors( _
        EntityFieldsFactory.CreateEntityFieldsObject(EntityType.EmployeeEntity))

Dim projectionData As New List(Of IViewProjectionData)()
' create the customer projection information. Specify a filter so only customers from Germany
' are projected.
projectionData.Add(New ViewProjectionData(Of CustomerEntity)( _
        customerProjections, (CustomerFields.Country = "Germany"), True))
projectionData.Add(New ViewProjectionData(Of OrderEntity)(orderProjections, Nothing, False))
projectionData.Add(New ViewProjectionData(Of EmployeeEntity)(employeeProjections))

Dim result As New DataSet("projectionResult")
customers.CreateHierarchicalProjection(projectionData, result)

The same projectors as used with the projection to the DataSet are usable with a projection to a Dictionary, which is almost equal to the DataSet example.

Projection to Dictionary

EntityCollection<CustomerEntity> customers = new EntityCollection<CustomerEntity>();
PrefetchPath2 path = new PrefetchPath2(EntityType.CustomerEntity);
path.Add(CustomerEntity.PrefetchPathOrders).SubPath.Add(OrderEntity.PrefetchPathEmployees);
using(DataAccessAdapter adapter = new DataAccessAdapter())
{
    adapter.FetchEntityCollection(customers, null, path);
}

// setup projections per type.
List<IEntityPropertyProjector> customerProjections = EntityFields2.ConvertToProjectors(
        EntityFieldsFactory.CreateEntityFieldsObject(EntityType.CustomerEntity));
// add an additional projector so the destination DataTable will have an additional column called 'IsNew' with
// the value of the IsNew property of the customer entities.
customerProjections.Add(new EntityPropertyProjector(new EntityProperty("IsNew"), "IsNew"));
List<IEntityPropertyProjector> orderProjections = EntityFields2.ConvertToProjectors(
        EntityFieldsFactory.CreateEntityFieldsObject(EntityType2.OrderEntity));
List<IEntityPropertyProjector> employeeProjections = EntityFields.ConvertToProjectors(
        EntityFieldsFactory.CreateEntityFieldsObject(EntityType2.EmployeeEntity));

List<IViewProjectionData> projectionData = new List<IViewProjectionData>();
// create the customer projection information. Specify a filter so only customers from Germany
// are projected.
projectionData.Add(new ViewProjectionData<CustomerEntity>(
        customerProjections, (CustomerFields.Country == "Germany"), true));
projectionData.Add(new ViewProjectionData<OrderEntity>(orderProjections, null, false));
projectionData.Add(new ViewProjectionData<EmployeeEntity>(employeeProjections));

Dictionary<Type, IEntityCollection> projectionResults = new Dictionary<Type, IEntityCollection>();
customers.CreateHierarchicalProjection(projectionData, projectionResults);
Dim customers As New EntityCollection(Of CustomerEntity)()
Dim path As New PrefetchPath2(EntityType.CustomerEntity)
path.Add(CustomerEntity.PrefetchPathOrders).SubPath.Add(OrderEntity.PrefetchPathEmployees)
Using adapter As New DataAccessAdapter()
    adapter.FetchEntityCollection(customers, Nothing, path)
End Using

' setup projections per type.
Dim customerProjections As List(Of IEntityPropertyProjector) = EntityFields2.ConvertToProjectors( _
        EntityFieldsFactory.CreateEntityFieldsObject(EntityType.CustomerEntity))
' add an additional projector so the destination DataTable will have an additional column called 'IsNew' with
' the value of the IsNew property of the customer entities.
customerProjections.Add(New EntityPropertyProjector(New EntityProperty("IsNew"), "IsNew"))
Dim orderProjections  As List(Of IEntityPropertyProjector) = EntityFields2.ConvertToProjectors( _
        EntityFieldsFactory.CreateEntityFieldsObject(EntityType.OrderEntity))
Dim employeeProjections As List(Of IEntityPropertyProjector) = EntityFields2.ConvertToProjectors( _
        EntityFieldsFactory.CreateEntityFieldsObject(EntityType.EmployeeEntity))

Dim projectionData As New List(Of IViewProjectionData)()
' create the customer projection information. Specify a filter so only customers from Germany
' are projected.
projectionData.Add(New ViewProjectionData(Of CustomerEntity)( _
        customerProjections, (CustomerFields.Country = "Germany"), True))
projectionData.Add(New ViewProjectionData(Of OrderEntity)(orderProjections, Nothing, False))
projectionData.Add(New ViewProjectionData(Of EmployeeEntity)(employeeProjections))

Dim projectionResults As New Dictionary(Of Type, IEntityCollection)()
customers.CreateHierarchicalProjection(projectionData, projectionResults)
Info

If you just want a structure with per entity type a collection with all the instances of that type in the entity graph, please use the routine ObjectGraphUtils.ProduceCollectionsPerTypeFromGraph. The ObjectGraphUtils class is located in the ORMSupportClasses namespace and contains a variety of routines working on entity graphs. Please see the LLBLGen Pro reference manual for details on this class and this method.

Tracking entity remove actions

Removing an entity from a collection by calling entitycollection.Remove(toRemove) or entitycollection.RemoveAt(index) is an ambiguous action: do you want to remove the entity from the collection to further process the entities left, or do you want to get rid of the entity completely, both in-memory and also in the database? This is the reason why LLBLGen Pro doesn't perform deletes on the database automatically if you remove an entity from a collection, you have to explicitly specify what entities to remove.

Tracking which entities are removed from an entity collection so they can be removed from the database can be a bit cumbersome if the collection is bound to e.g .a grid. To overcome this, LLBLGen Pro has a feature which makes an entity collection track the entities removed from it by using another entity collection. This way, you can keep track of which entities are removed from the entity collection and pass them on to a Unit of work object for persistence in one transaction together with the rest of the entities which have changed.

The extra collection is necessary because an entity which is removed from the collection isn't there anymore, so it can't be referred to by the collection itself. To enable removal tracking in an entity collection, set its RemovedEntitiesTracker property to a collection into which you want to track the removed entities from the collection. This collection can then be added to a UnitOfWork2 object for deletion by using the method unitofwork2.AddCollectionForDelete(collectionWithEntitiesToDelete) or you can delete the entities by calling DataAccessAdapter.DeleteEntityCollection(collectionWithEntitiesToDelete).

The following example illustrates this.

// First fetch all customers from Germany with their orders. 
EntityCollection<CustomerEntity> customers = new EntityCollection<CustomerEntity>();
PrefetchPath2 path = new PrefetchPath2(EntityType.CustomerEntity);
path.Add(CustomerEntity.PrefetchPathOrders);
using(DataAccessAdapter adapter = new DataAccessAdapter())
{
    adapter.FetchEntityCollection(customers, 
        new RelationPredicateBucket(CustomerFields.Country == "Germany"), 
        path);
}

// we now will add a tracker collection to the orders collection of customer 0.
EntityCollection<OrderEntity> tracker = new EntityCollection<OrderEntity>();
customers[0].Orders.RemovedEntitiesTracker = tracker;

// after this, we can do this:
customers[0].Orders.Remove(myOrder);

// and myOrder is removed from the in-memory collection customers[0].Orders
// and it's placed in 'tracker'. We can now delete the entities in tracker
// by using a UnitOfWork2 object or by calling adapter.DeleteEntityCollection(tracker).
' First fetch all customers from Germany with their orders. 
Dim customers As New EntityCollection(Of CustomerEntity)()
Dim path As New PrefetchPath2(EntityType.CustomerEntity)
path.Add(CustomerEntity.PrefetchPathOrders)
Using adapter As New DataAccessAdapter()
    adapter.FetchEntityCollection(customers, _
        new RelationPredicateBucket(CustomerFields.Country = "Germany"), _
        path)
End Using

' we now will add a tracker collection to the orders collection of customer 0.
Dim tracker As New EntityCollection(Of OrderEntity)()
customers(0).Orders.RemovedEntitiesTracker = tracker

' after this, we can do this:
customers(0).Orders.Remove(myOrder)

' and myOrder is removed from the in-memory collection customers[0].Orders
' and it's placed in 'tracker'. We can now delete the entities in tracker
' by using a UnitOfWork2 object or by calling adapter.DeleteEntityCollection(tracker).
Info

The Clear() method has two overloads: Clear() and Clear(bool). The Clear() method doesn't do tracking removal of removed entities, because Clear is often used to clean up a collection and not to remove entities from the database, so to avoid false positives and the deletion of entities which weren't suppose to be deleted, removal tracking isn't available for this overload. The Clear(bool) overload however allows you to have Clear add the entities removed from the collection to a contained RemovedEntitiesTracker, by passing true to the Clear(bool) overload.