Introduction
Kerosene ORM
is dynamic, configuration-less and self-adaptive ORM library that overcomes many of the limitations and nuances found in other ORM solutions. It fully supports POCO objects without requiring any mapping or configuration files, polluting their code with attributes, or requiring them to inherit from any ORM-specific base class – so promoting a clear separation of concerns. It does not use any kind of conventions either, so we can write our classes and name their members and the columns and tables in our databases the way we want, not the way someone has decided for us. It gives us back full control on what SQL code will be executed: just what we write, no more, no less. It follows the Repository and Unit Of Work patterns, albeit implemented in a dynamic fashion which allows us to gain great flexibility.
All these characteristics make Kerosene ORM
an ideal solution for iterative and agile development scenarios and, even more, where a number of disparate teams is involved, from developers to IT and database responsible ones.
This article explores the Entity Maps operational mode of Kerosene ORM
. Please refer to the introductory article Kerosene ORM Introductory Article for more context information as this article elaborates on the concepts introduced in it.
The business scenario
In the discussions that follow we are going to use basically the same business scenario as the one used in the introductory article. Remember that we were dealing with a minimalist HR system composed by three tables:
The Regions
table maintains the hierarchy of regions our company uses, and its ParentId
column maintains what region the current one belongs to, or null if it is a top-most one. The Countries
table maintains the countries where our company has operations, and its RegionId
column contains the region that country belong to. Finally the Employees
table maintains our employees, and contain both a CountryId
column (that must not be null) and a ManagerId
one that, if it is not null, identifies the manager of the current employee.
Preliminary Concepts
As discussed in the introductory article the Kerosene ORM
Entity Maps operational mode takes care of dynamically map the records obtained from the database to instances of our business-level POCO classes, tracking their contents, state and dependencies. Remember that Kerosene ORM
does not require us to write external configuration or mapping files, using any kind of pre-canned conventions, polluting our classes with attributes, or needing them to inherit from any ORM specific base class.
Even more, Kerosene ORM
will try figure out by default what tables to use and how to perform the mapping among their columns and the members of our POCO classes. Only if we want to use non-standard table or column names, or if we want to define navigational properties and dependencies, then we need to provide some extra information to Kerosene ORM
in the form of a custom map.
The only caveat to bear in mind is that our POCO types have to be classes, not structs or any other kind of objects.
Repositories
A repository is an object that implements the IDataRepository
interface, used by Kerosene ORM
to maintain a view on the state and contents of the underlying database, what maps have been explicitly or implicitly registered into it to put in correspondence tables and POCO classes, and to access the cache of managed entities. Repositories also implement the Repository and Unit Of Work patterns.
The easiest way of obtaining a repository if by using the Create()
method of the static RepositoryFactory
class, as follows:
using Kerosene.ORM.Maps;
...
var link = ...;
var repo = RepositoryFactory.Create(link);
Here the link object passed to that method can be obtained by any of the mechanisms discussed in the introductory article. Remember that this object will manage the physical connection with the underlying database, opening, closing and disposing it as needed – our applications need not to bother with these details.
Simple (Table) Maps
Once we have obtained a repository instance we can just start using it without any further ceremony. Let’s suppose that we have laid down our Region
POCO class as a representation of the columns in the database we are interested at:
public class Region
{
public string Id { get; set; }
public string Name { get; set; }
public string ParentId { get; set; }
}
Now, to retrieve the list of regions in our database we can just write:
var cmd = repo.Query<region>();
var list = cmd.ToList();
</region>
We have not written any configuration or mapping files. We have not written any map. We have not polluted our domain-level code with any attributes or ORM related stuff. Actually, we have not even told Kerosene ORM
the name of the table to use for these entities.
By default Kerosene ORM
maps operate in a Simple (or Table) mode in which the members in the POCO class are automatically mapped to columns whose name match the name of those members. If such names are not case sensitive in the database Kerosene ORM
will follow the database rule and won’t enforce case sensitiveness when finding those matches. If a match is not found the corresponding member, or column, is not taken into consideration.
In this scenario the first time the type of a POCO class is used Kerosene ORM
will try to find a suitable table in the database using some educated guesses based upon the name of that type. If such table is found, then a “weak” map is created on our behalf using such direct correspondence rule among the members of our type and the columns found in that table.
These maps are said to be “weak” because if we register an explicit map for that type, and such weak map was registered into the repository, then it is discarded. The reason is because a given type can be registered only once in a given repository.
We can also filter what entities to retrieve using any logic that fits into the domain problem we are trying to solve:
var cmd = repo.Where<employee>(x => x.Id == "007");
var emp = cmd.First();
</employee>
Note that we are not constrained to use a pre-canned set of FindXXX()
methods, but rather we can use any logic we need. As we will see below Kerosene ORM
provides a wide range of methods in its Query commands to support non-conventional query scenarios where, for instance, and even in this POCO world, we can query or join from several tables simultaneously.
To persists into the database a new entity we just need to use the Insert()
method with the affected entity:
var emp = new Employee() { Id = "007", CountryId = "uk" };
...
var cmd = repo.Insert(emp);
cmd.Submit();
...
repo.ExecuteChanges();
Kerosene ORM
uses the Unit Of Work pattern so we have to create the command associated with the entity, submit that command and, when we are done with all the changes we may need in between, including other change operations, execute them all as a single unit.
We can now update our entity:
emp.FirstName = "James";
emp.LastName = "Bond";
...
repo.UpdateNow(emp);
Kerosene ORM
will automatically find the changes experimented by our entities since the last time their contents were obtained from the database, and will create an update command containing only those changes. In this example we have executed the update operation using the UpdateNow()
method: it is basically a handy way to submit and execute the command, along with any other submitted ones, in just one call. There are also similar InsertNow()
and DeleteNow()
methods available.
Finally, we can delete our entity by using:
repo.Delete(obj).Submit();
repo.ExecuteChanges();
or just:
repo.DeleteNow(obj);
If, for any reasons, we are not happy with the changes we have annotated into a repository we can discard them all by using the repository DiscardChanges()
method:
repo.DiscardChanges();
When invoking this method all the pending operations are disposed.
Read-Only columns
Kerosene ORM
is prepared to identify what columns are read-only ones in the database. As a safety net, even when they are used in a given map, they are not going to be persisted back into the database despite how their corresponding members were used in our domain model. This information is obtained from the database and Kerosene ORM
provides no mechanism to circumvent it.
Entity Constructors
Other ORM solutions require that our business classes have a parameterless constructor – Kerosene ORM
does not. Remember that its philosophy it to impose no restrictions to the way we may want to develop and architect our POCO classes.
If our POCO class has such a constructor it will be captured and used for performance reasons. If not then Kerosene ORM
will create in memory an un-initialized object without invoking any constructor but, at least, we will have an instance ready to be used.
Creating a custom map.
When we need to use a table name that Kerosene ORM
cannot figure out automatically, when our POCO class contains members that should not be mapped for whatever reasons, when the contents of those members are to be obtained by querying the database, performing complex calculations, or even accessing external systems, or when we use navigational properties in our POCO class and we want to take these dependencies into consideration, then we need to create a custom map.
The recommended way of proceeding is by creating a class that inherits from the DataMap<T>
<t> base one, and perform those customizations in its constructor:
public class RegionMap : DataMap<Region><region>
{
public RegionMap(DataRepository repo) : base(repo, x => x.Regions)
{ ... }
}
</region>
The DataMap<T>
constructor takes two arguments. The first one is the repository where our new map instance will be registered into, and the objects returned by the RepositoryFactory
class can be casted into DataRepository
instances safely. The second one is a dynamic lambda expression that resolves into the name of the primary table in the database.
Discriminators
It may happen our table is used to maintain records that we will map to different POCO types. For instance suppose that we have in our domain the Employee
class, but also the Director
one to represent those employees without a manager associated to them. We can use the Discriminator
property of the map to express this condition:
public class Director : Employee { ... }
public class DirectorMap : DataMap<Director>
{
public DirectorMap(DataRepository repo) : base(repo, x => x.Employees)
{
Discriminator = x => x.ManagerId == null;
...
}
}
This property has the Func<dynamic, object>
signature and if it is not null will be parsed and injected as part of the WHERE clauses when needed.
Row versions
If our table has a column used to keep track of the version of the row we can tell the map to take this column into consideration when executing update or delete operations, by using its VersionColumn
property:
VersionColumn.SetName(x => x.MyVersionColumnName);
Even if there is not a corresponding member in our POCO class Kerosene ORM
will keep track of the last value retrieved from the database, and will compare it with the most up-to-date one before executing those operations. If the value has changed then a ChangedException
exception will be thrown.
Note that we have not had to specify the type of the values maintained by that column. By default Kerosene ORM
will use an agnostic normalized string representation to perform those comparisons. If for whatever reasons you would like to modify how these values are compared you can set the ValueToString
property of the VersionColumn
one, which is a delegate that takes the object representing the value and returns a string representation:
VersionColumn.ValueToString = x => x.ToString();
Note that modifying this property is not needed as the default mechanism will suffice in almost all possible scenarios.
Excluding Columns
It may happen our POCO class has a member whose name matches the one of a column in the database but, for whatever reasons, we don’t want that column-member combination to participate in the map. Easy, we just need to tell the map this by adding an entry into its Columns
collection, and specifying that this column has to be excluded:
Columns.Add(x => x.MyColumn).SetExcluded(true);
Eager and Lazy members
Eager and Lazy members are those whose contents are to be obtained not from the primary table but by querying the database, performing complex calculations, or even accessing external systems if we need so. They are also used to express dependencies.
For instance, let’s suppose that our Country
class has a BusinessValue
member whose contents we want to populate when the record entity is loaded from the database (yes, we can also use its getter, but let me progress with this as an example). It may involve quite convoluted operations, querying other databases, or accessing external systems. We just need to tell the map how to proceed when completing that value as follows:
Members.Add(x => x.BusinessValue)
.OnComplete((rec, obj) => {
obj.BusinessValue = ...;
});
We have used the Add()
method of the map’s Members
collection to add a new entry for the member we want the map to complete when needed. Its OnComplete()
method is a delegate that takes two arguments: the first one is the last record obtained from the primary table (that we can use to get the values of some columns that might be relevant for us), and the second one is a reference to the host entity itself.
If the BusinessValue
member is a virtual property with at least an accessible getter or setter then the member is said to be a “Lazy” one. If it is a field or a non-virtual property then it is said to be an “Eager” one. The contents of the eager members are populated just after the primary record is obtained from the database, whereas the contents of the lazy ones are populated only when their getters are used.
In the general case Lazy members are preferred over Eager ones. The reason if that, when using dependencies, eager members will try to load those dependencies into memory, potentially cascading and loading the complete object’s graph, and experimenting a delay while these operations complete. On the flip side we may have no access to the source code of our POCO class (for instance when it is defined in an external assembly we are not allowed to modify), and for these scenarios eager members are handy.
In any case we are free to mix Simple, Lazy and Eager members as we wish or need.
Dependencies and Navigational properties
Let’s now suppose that we want to use dependencies and navigational properties in our POCO classes. We could have written our Region
class as follows:
public class Region
{
public string Id { get; set; }
public string Name { get; set; }
virtual public Region Parent { get; set; }
virtual public List<region> Childs { get; private set; }
virtual public List<country> Countries { get; private set; }
}
</country></region>
We are using virtual properties here, so Lazy members, but we could have used Eager ones instead without any changes in the discussions that follow.
The Countries
property will maintain the list of countries that belong to our region: it is “Child” dependency. We can tell this fact to the map as follows:
Members.Add(x => x.Countries)
.OnComplete((rec, obj) =>
{
obj.Countries.Clear();
obj.Countries.AddRange(
Repository.Where<country>(x => x.RegionId == obj.Id).ToList());
})
.SetDependencyMode(MemberDependencyMode.Child);
</country>
In our already-known OnComplete()
method we are instructing the map to firstly clear that list, for sanity reasons, and then to query the database to obtain its most up-to-date contents. And because we are defining this dependency inside the map’s constructor we can use its Repository
property for simplicity. Finally we are setting the dependency mode to “Child”.
Our Region
POCO class also has a Parent
property that is a reference to the parent region of the current one. We can define this dependency as follows:
Members.Add(x => x.Parent)
.WithColumn(x => x.ParentId, col =>
{
col.OnWriteRecord(
obj => { return obj.Parent == null ? null : obj.Parent.Id; });
})
.OnComplete((rec, obj) =>
{
obj.Parent = Repository.FindNow<region>(x => x.Id == rec["ParentId"]);
})
.SetDependencyMode(MemberDependencyMode.Parent);
</region>
The main difference is that we want to be sure that the ParentId
column is taken into consideration for the map, because is the one used in the database to reference the parent record. In order to do that we use the WithColumn()
method that takes two arguments: the first one is a dynamic lambda expression that resolves into the name of that column, and the second one is a delegate that takes the new column added to the internal collection and let us customize how it behaves.
In our case we don’t need to tell the map how to read the value of that column, because it will do so automatically using its name (and this value will be stored in the record maintained in the metadata associated with each entity). We just need to tell the map how to persist back the value of that column when needed: we use the OnWriteRecord()
method that has to return the value to write back into the database. In the example it is enough to return null if no parent reference is used, or the value of its Id
property otherwise.
See also how we have written its OnComplete()
method: for performance reasons instead of querying the database we have used the FindNow()
method that will try to find in the in-memory cache a valid entity and, only if it is not found there, go to the database to find it. This method will return null if no entity is found in the cache or in the database, which is precisely what we want in this example. Finally we are setting its dependency mode to “Parent”.
Cascading Dependencies and Aggregate Roots
Only those dependencies whose mode is set to “Child” or to “Parent” are cascaded when executing a change operation (Insert, Delete or Update). For instance, when this is the case Kerosene ORM
will insert parent dependencies that were not persisted yet into the database, or will make sure that child ones are deleted before deleting its parent entity.
Actually this feature let us work naturally with aggregate roots from our C# code without needing to pollute our domain-level code with ORM operations. For instance:
var root = repo.Query<region>.Where(...).First();
...
root.Countries.RemoveAt(0);
root.Countries.Add(new Country() { Id = "ZZZ" });
...
repo.UpdateNow(root);
</region>
In this example we are removing and adding entries into the Countries
property at our domain level. Only later, when we are done with all the changes we need, we just need to persist back the hosting entity and Kerosene ORM
will find out what changes it has experimented and materialize those changes in the database.
Kerosene ORM
injects into each managed entity a metadata package that, among other purposes, is used to keep track of the state and changes the entity may experiment. The internals of this mechanism are discussed later in this document. When a given dependency is a collection-alike one this metadata will keep the original contents of that collection and, when the time comes, Kerosene ORM
will compare the original ones against the current contents. If it is not a collection-alike one then the state of the current entity is used to decide how to proceed.
Advanced Concepts
This section explores further the Entity Maps operational mode discussing a number of advanced concepts and internals of Kerosene ORM
. In order to do so we are going to expand a bit our business scenario incorporating two additional tables: Talents
, that maintain the talents our HR friends are interested at, and EmployeeTalents
that basically is a join table among employees and the talents assigned to them:
Non-Conventional queries
Let’s suppose our Talent
POCO class is written down as follows:
public class Talent
{
public string Id { get; set; }
public string Description { get; set; }
virtual public List<Employee> Employees { get; }
...
}
Here we have and Employees
property being a list (set by the class constructor) that we want to populate: we need to find all the employees whose Id
appear associated to the talent Id
in the join table. We can define in this case a dependency as follows:
Members.Add(x => x.Employees)
.OnComplete((rec, obj) => {
obj.Employees.Clear();
obj.Employees.AddRange(
Repository
.Where<employee>(x => x.Emp.Id == x.Temp.EmployeeId)
.MasterAlias(x => x.Emp)
.From(x =>
x(Repository.Where<employeetalent>(y => y.TalentId == obj.Id))
.As(x.Temp))
.ToList()
);
})
.SetDependencyMode(MemberDependencyMode.Parent);
</employeetalent></employee>
The interesting thing to note here is that we are firstly querying from the EmployeeTalents
table for those records whose talent Id
match the Id
of our talent hosting entity, and then finding their related employees by injecting these results into the FROM clause of the main query.
As we are using several tables simultaneously and they all have their respective Id
column we need to provide aliases to disambiguate among them: this is easy with the inner query as we can use the As()
virtual extension method but… how can we do it within the main mapped query?
The answer is the MasterAlias()
method whose argument is a dynamic lambda expression that resolves into the alias to use with the primary table for this query only. It doesn’t matter what the name of that primary table is (and remember that Kerosene ORM
might have found it out automatically): its alias will be the one we are specifying.
With this two aliases, the Emp
and Temp
ones, it is now very easy to write the main WHERE clause we need to find the employees we are interested at. Just take a minute to review the code in the example and the above explanations because it sounds more complex than what it really is. Other ORM solutions take a different approach and try to automate this scenario (some of them struggle to solve it by the way), but we will be constrained by their assumptions and rules.
This unique Kerosene ORM
approach is an advanced feature that you can use or not, but that put a lot more power on your hands – indeed, Kerosene ORM supports, even in this POCO world, Join()
, GroupBy()
, and Having()
methods, as well as basically any logic we may need to incorporate to solve our business problem. Yes, you need to write some SQL-alike code but it does also give us the opportunity to optimize that code and not to use the fat one produced automatically.
Map Validation
We now know that when a map instance is created it will be automatically registered into the repository used in its constructor. We can customize it inside constructor of the derived class, which is the recommended approach, or by using its methods and properties directly. It will remain in a non-validated state while we are customizing it.
Now, as soon as the map is used for any interesting purpose it will be validated: its structure and rules will be checked against the database and if any inconsistency is found the corresponding exception will be thrown. Once a map is validated it becomes locked and cannot be customized any further.
As said this validation takes place automatically when needed. It may happen that, for whatever reasons, you want to lock and validate your map – in this case you can invoke its Validate()
method to do so. By the way this method can be called as many times as needed without any side effects if the map was already validated. Anyhow, the map’s IsValidated
property will return true if it has been validated, or false otherwise.
Repository Maps Collection
Each repository carry a Maps
property maintaining the collection of maps registered into it. You can use this collection, or the several GetMap()
method overrides, to find what map is associated with a given POCO type.
Note that registration of maps is based upon a one-to-one correspondence between the POCO type and the type of the entities managed by the registered map. So a map registered for the Employee
POCO class, and a map registered for the Manager
POCO class, even if the later inherits from the former, are considered different maps.
When a map is disposed it is removed from its repository. Similarly repositories have the ClearMaps()
method that will remove and dispose all maps registered into it.
Finally, repositories also have the RetrieveMap()
method that will return either a registered map for the given type, or if no one was registered, will create a new one for that type. If no arguments are used the name of the primary table will be proposed by Kerosene ORM
using a number of educated guesses and pluralization rules, and the map returned will be a “weak” one.
This method is provided in case we want not to create a custom map class and rather we want an instance to customize using its properties and methods. In this case it is recommended that its IsWeakMap
and its IsValidated
properties are used accordingly.
Its Table
property maintains the name of the primary table the map is associated with, either the one we have specified or the one found automatically by Kerosene ORM.
Identity Columns
In purity Kerosene ORM
does not require primary key columns in the primary table. It just need a way to univocally identify what record to associate with a given entity and, for this, if no primary key columns are defined, it will try to find unique valued ones. If neither primary key columns not unique valued ones exist in the table then Kerosene ORM
will throw an exception when validating the map.
Note that we have not to identify which ones are those identity columns: Kerosene ORM
will find them out automatically from the database’s metadata.
Managed Entities and Metadata
Kerosene ORM
does not require us to decorate our POCO classes with any attributes, and they have not to inherit from any ORM-specific one. What it does instead is to inject into each POCO instance it manages a package of metadata to keep track of its state, the latest record read from or persisted to the database, and the state of its dependencies, among other things.
This package is an object that implements the IMetaEntity
interface that can be obtained using the Locate()
method of the static EntityFactory
class:
var obj = ...;
var meta = EntityFactory.Locate(obj);
Note that this method will throw an exception if the object used as its argument is not a class. Value types, enumerations, or structs, are not considered as valid Kerosene ORM
entities.
This metadata object has only two public properties: the first one, Entity
, is a reference back to the entity the metadata is associated with; the second one, State
, is an enumeration that gives as back the state of this underlying entity.
The value of the Entity
property can also be null. This situation can happen when we have obtained the metadata reference and, after a while, if the underlying entity is used no longer, it may have been collected by the CLR garbage collector. Indeed, to avoid locking the entities in memory the metadata package just holds a weak reference to them.
The value of the State
property can be Detached
if we have just created our entity and Kerosene ORM
has not yet used it, Collected
if the underlying entity has been collected by the CLR, Ready
if it has been read from or persisted to the database, or ToInsert
, ToUpdate
or ToDelete
if the corresponding pending operation has been submitted.
For performance reasons, instead of maintaining any kind of list or similar structure, what Kerosene ORM
does is injecting that metadata package into the CLR descriptor associated with any object it manages, in the form of a run-time attribute. Yes, IMetaData
instances internally inherit from the Attribute
class.
Entities Collector
Kerosene ORM
does not, internally, keep track of the entities themselves but rather of the metadata packages associated with them which, in turn, just maintain a weak reference back to the original entities. This way it permits those entities to be collected by the CLR garbage collector when they are needed any longer.
But this also means that a number of metadata packages will remain with no associated entities. Kerosene ORM
repositories implement an internal collector that fires periodically to perform the cleaning of these zombie packages. Note that this feature is not part of the IDataRepository
interface but is provided by the concrete DataRepository
instances.
In the general case you don’t need to interact with this mechanism. But it may happen that you want to disable it for debug purposes and, for these scenarios, you can use the following methods:
repo.DisableCollector();
repo.EnableCollector();
You can also use the IsCollectorEnabled
property to interrogate the repository about the state of its internal collector. The EnableCollector()
method has also an override that accept two arguments: the number of milliseconds after which the collector is fired, useful if you want to tweak this interval for performance reasons, and a Boolean value that specified if a CLR garbage collection is forced before firing it – but this second one is seldom used except for debug or very specialized scenarios.
Proxy Types
Even if our applications will only deal with its own domain-level POCO instances it may happen that the entities Kerosene ORM
will return from the database are not of these types, but rather of a proxy type that inherits from the original POCO one.
This is the situation when lazy dependencies are used: Kerosene ORM
will create a proxy type where the setters and getters of those lazy virtual properties are overridden, if possible, in order to inject the logic to load their contents in a deferred way. A number of additional fields and properties are also included in the proxy type (whose names end with either “_Completed” or with “_Source”) but, otherwise, their instances behave as the original ones.
The NewEntity()
method will return either an instance of the original POCO class or, if a proxy type has been created by Kerosene ORM
for the associated map, an instance of the proxy type:
var obj = repo.NewEntity<region>();
</region>
Remember that if the original POCO class has a parameterless constructor it will be used. Otherwise, a new un-initialized object will be created in-memory and returned without invoking any constructor.
The way Kerosene ORM
generates these proxy types involve emitting some IL code to add the additional properties and fields mentioned, and to override the virtual getters and setters of the lazy properties. Please refer to the accompanying articles for more details.
Controlling Unit Of Work exceptions
Kerosene ORM
follows the Unit Of Work pattern. It prescribes that we have to submit (annotate) into the repository all the change operations we are interested at and, when we are done, execute them all as a single unit against the underlying database:
var region = new Region() { ... }; repo.Insert(region);
var ctry = new Country() { ... }; repo.Insert(ctry);
repo.ExecuteChanges();
Internally Kerosene ORM
will cascade the dependencies associated with the entities for which we have submitted change operations, will reorder them all to meet their logical constrains, and then execute them one by one under a transaction.
If the execution of any of those operations fail the transaction is aborted so leaving the database at its original state, and then, by default, an exception is thrown with the description of the failure (this typically will be the exception returned from the database). Our application can execute the ExecuteChanges()
method inside a try-catch block or rather it can set the OnExecuteChangesError
property of the repository with the delegate to invoke if an exception happens with, precisely, that exception as its argument. In this case the exception is not thrown but rather used as that argument. This is handy for many scenarios, and for logging and tracing purposes.
Forced Columns
When there is no corresponding method for a given column in the database but, for whatever reasons we want that column to participate into the mapping mechanism, we can achieve so by adding an entry into the map’s Columns
collection:
Columns.Add(x => x.MyColumnName)
.OnWriteRecord(entity => { ... })
.OnLoadEntity((value, entity) => { ... })
.OnMember(x => x.MyMemberName);
The OnWriteRecord()
method takes a delegate that shall return the value of that column when the time comes to persist it back to the database. It takes the hosting entity as its argument and can do whatever operations it may need.
Similarly the OnLoadEntity()
method takes a delegate that will be invoked when the associated record is read from the database. It takes the value of the column and a reference to the hosting entity. Again it can do whatever operations needed.
The last one, OnMember()
, is used for simplification purposes when instead of invoking convoluted operations we just want to map the column in the database with a given member in the type whose name may not match. In this case its argument is a dynamic lambda expression that resolves to the name of that member. Remember that it can be either a property or a field, and they can be public, protected or private ones.
Once a map is validated its Columns
collection will contain all the columns taken into consideration. In many circumstances Kerosene ORM
have discovered automatically a number of columns to map. If this is the case their AutoDiscovered
property is set to true.
What else?
This article is the last generic tutorial on the Entity Maps operational mode of Kerosene ORM
. Next articles will be shorter ones and focused on the specific details of the techniques used in the internals of Kerosene ORM
.