Manual

Revision: 3.9.5

 

 


 

Table of contents

Introduction. 6

Product Editions. 12

System Requirements. 13

Installation. 13

Redistribution. 13

Documentation. 13

Key concepts. 14

Persistent type, persistent instance, persistent properties 14

Instance identification. 15

Version indentifiers 16

Database schema maintenance. 16

Domain. 16

DataObjects.NET Configuration File. 18

Session. 19

Transparent persistence. 19

Creating, updating and removing persistent instances 20

Persistent properties. 23

Primitive fields 23

Serializable fields 23

Reference fields 24

Reference cleanup process 25

Struct fields 25

Collection fields 27

Native (or serializable) collections 27

DataObjectCollection. 27

ValueTypeCollection. 30

Delegate fields 32

Relationships. 34

One-way relationships 34

One-to-many relationship example. 34

Many-to-many relationship example. 34

Traversing relationships example. 34

ValueTypeCollection describing many-to-many relationship example. 35

ValueTypeCollection describing n-ary (ternary) relationship example. 35

Two-way (mutual, or paired) relationships 36

One-to-one mutual relationships 36

One-to-many mutual relationships 37

Many-to-many mutual relationships 38

Symmetric relationships 39

ValueTypeCollections describing mutual relationships (many-to-many, ternary, n-ary) 39

Queries. 43

Introduction. 43

General query syntax 43

Field reference substitution. 43

Substitutions in the 'order by' clause. 44

Additional conditions in square brackets 44

Root, this, parent 44

Count, item, expression. 44

Query options 45

Parameters 45

Type casts 45

Distinct, joins, aliases 45

Querying for collection\reference property content 45

Subqueries 46

Full-text queries 46

Querying the database directly. 46

Appendix: Comprehensive query format description. 47

Transactions. 48

Manual transactions 48

Automatic transactions 48

Transparent deadlock handling. 50

Distributed transactions 50

Data services. 52

Runtime services 53

IXxxEventWatcher interfaces 54

Events. 55

DataObject events 55

DataObjectCollection \ ValueTypeCollection events 56

Session events 56

Domain events 56

Attributes (reference). 58

Type-level attributes 58

[Abstract] 58

[Sealed] 58

[DeclarationMode] 58

[TypeReference] 58

[ServiceType] 59

Mapping attributes 59

[Persistent] \ [NotPersistent] 59

[ShareAncestorTable] 60

[ShareDescendantTable] 60

[NotVersionized] 61

[DbName] 61

[Nullable] 61

[SqlType] 62

[Length] 62

Indexing attributes 63

[Indexed] 63

[Index] 63

Multilingual database support 64

[Translatable] 64

[Collatable] 64

Relationships and collection attributes 65

[Contained] 65

[SelfReferenceAllowed] 66

[ItemType] 66

[PairTo] 67

[AutoFixup] 67

Access \ update strategy definition. 67

[LoadOnDemand] 67

[ChangesTracking] 68

Property validators, correctors, modifiers 69

[Validator] 69

[Corrector] 69

[StorageValueModifier] 70

[TypeModifier] 70

[PropertyType] 70

Security attributes 71

[Demand] 71

Method attributes 71

[NotOverridable] 71

[Transactional] 71

[BusinessMethod] 74

[OfflineBusinessMethod] 74

Miscellaneous attributes 74

[Alias] 74

[NotSerializable] 74

[ToOffline] 75

[ProxyAttribute] 76

Advanced features. 77

Persistent interfaces 77

Full-text indexing and search. 79

Implementation steps 79

FtObject descendant example. 79

Full-text indexer usage example. 80

Full-text filters usage example. 80

Full-text search query example. 80

Serialization. 81

Implementation steps 81

Security System. 82

Basic security concepts 82

Domain security policy. 83

Session-level security. 84

DataObject-level security. 84

System security objects 86

Implementation steps 86

Security-related classes 87

Security and deserialization. 89

Partitioning. 90

Partitioning types 90

Using partitioning. 92

DataObjectCollection fields partitioning. 92

ValueTypeCollection fields partitioning. 93

Limitations and pitfalls 94

Transparent caching. 95

Overview. 95

TransactionContext 95

SessionCache. 97

GlobalCache. 98

Dependency tracking. 99

When transparent caching doesn't work or is less efficient 101

Manual caching. 102

Performance optimization. 103

Using performance counters 103

Performance optimization tips 103

Adapter component 104

Adapter configuration. 104

Fill operation. 105

Update operation. 105

Implementation steps 106

BindingManager component 107

Implementation steps 107

BindingSource and DataSource. 107

Offline layer (DataObjects.NET.Offline namespace) 108

Why it's necessary to support DTO pattern? 108

Weaknesses of original DTO pattern. 109

Advanced DTO pattern implementation in DataObjects.NET. 109

Why web applications doesn't require use of DTO pattern? 112

Implementation steps 112

Fill descriptors 112

Short Offline layer FAQ. 114

Detecting changes made by arbitrary BLL code with TrackingSet 116

Implementation steps 116

Detecting concurrent changes 117

Interaction with environment 118

Modifying ObjectModel at runtime. 118

Versionizing. 119

Remote access (via .NET Remoting) 120

Building ASP.NET applications: DataContext and Global.asax overview. 121

DataObjects.NET Tools. 123

Query Profiler 123

DataObjects.NET Samples. 124

Running Samples 124

Step 1: Configure IIS. 124

Step 2: Configure databases 124

Step 3 (optional): Running a debug version of samples 126

Sample overview. 126

Demo_FirstStep. 126

Demo_Animals 126

Demo_Books 126

Demo_Transfers 126

Demo_RemotingTransfers 127

Demo_WebTransfers 127

Demo_Collections 128

Demo_Atricles 128

Demo_FullTextFilters 128

Demo_Adapter 128

Demo_BindingManager 128

Demo_FormsBinding. 128

Demo_WebBinding. 128

Demo_ExtendedObjectModel 128

Demo_Versionizing. 128

Demo_Partitioning. 128

Demo_DisconnectedSets 129

DataObjects.NET PetShop (DoPetShop) 129

Driver\RDBMS-specific Issues. 131

Using DataObjects.NET with Microsoft SQL Server 2005 on Snapshot Isolation. 131

Differences between Oracle and Microsoft SQL Server drivers 131

2 driver versions 131

Collations 131

Identity. 131

Identifiers 131

Data Types 131

Indexing. 132

Constraints 132

Transactions 132

Versionizing. 132

SQL syntax 132

Additional issues 132

Differences between Firebird and Microsoft SQL Server drivers 133

Firebird data provider 133

Unicode support 133

Collations 133

Identity. 133

Identifiers 133

Data Types 133

Indexing. 133

Transactions 133

Versionizing. 134

SQL syntax 134

Additional issues 134

Differences between Microsoft Access and Microsoft SQL Server drivers 135

OLEDB Provider 135

Connection String. 135

Data Types and Indexes 135

Versionizing. 135

Additional issues 135

Transactions 135

SQL syntax 135

Indexing. 135

Troubleshooting. 136

General troubleshooting. 136

Query troubleshooting. 136

Reporting bugs 136


 

Introduction

DataObjects.NET is .NET library that dramatically simplifies development of data and business tiers of the database application. It provides:

Methodology that standardizes and simplifies development of persistent classes and services operating with them providing very clear separation of your business and data tiers from other parts of an application. Essentially DataObjects.NET requires you to build a hierarchy of your persistent and business objects over two base classes: DataObject and DataService. It provides more then 20 attributes controlling almost any persistence or behavior-related aspect. Don't worry about this number - you can know nearly 5 of them to start using DataObjects.NET.

Persistence framework handling all object persistence related tasks transparently. Moreover, this framework allows to almost forget that the underlying database exists - it handles even database schema updates. This framework fully supports inheritance, persistent interfaces, relations and collections, object queries, full-text indexing and search, multilingual properties and a lot of other features. Use of this framework makes most part of your data tier automatically compatible with Microsoft SQL Server 2005 \ 2000, MDSE 2000, Microsoft Access, Oracle and Firebird (free, but one of the most featured database servers) without any additional code.

Transactional services allowing to almost forget that your business and data objects operate in the concurrent transactional environment. Transactional services intercept calls of your business tier methods and wraps them into transactions (outermost or nested) providing that if exception or deadlock occurs, no data will be changed. These services are capable of re-processing method call on deadlock exceptions (and similar). This behavior is provided completely transparently for developers, but nevertheless it's highly configurable.

Security system supporting per-instance access control lists (allow-deny lists), permissions (custom permissions), security principals (users and roles) and permission inheritance. Its primary goal is to make usage of business objects completely safe, even when these objects are publicly available - via .NET Remoting, for example.

All is initially remotable - any persistent object or business service can be marshaled to another application domain via .NET Remoting (as well as all other DataObjects.NET-related objects, e.g. Query). This means that you can access your data and business tier from a completely different network or across the Internet with almost no additional code. DataObjects.NET supports two marshalling scenarios: access-by-reference for regular DataObjects and by-value marshalling for so-called offline entities (data transfer objects).

 

DataObjects.NET allows you to focus on code of business tier and application data model - it completely solves a set of problems that could take up to 80% of development time. Just imagine, what does it means - to find and fix an error (e.g. thread deadlock or "the latest update wins"­-like problem) that appears only under high concurrency conditions. Sometimes it's not so easy to even imagine that such a problem exists!

 

DataObjects.NET can be used in virtually any application that accesses a relational database. You simply add a reference to DataObjects.NET.dll to your project to start using it.

 

DataObjects.NET is shipped with DataObjects.NET PetShop (DoPetShop) sample. This is a DataObjects.NET-based clone of the famous Microsoft .NET Pet Shop. Here are the most interesting comparison facts:

DoPetShop contains ~ 50 Kb of data and business tier code while .NET Pet Shop - 140 Kb (including Business Logic Layer, Model, DAL and two its implementations - SQL Server DAL and Oracle DAL, but without BLL\OrderInsert.cs - read further about this). This means that use of DataObjects.NET reduced business and data tier code size by 3 times! Note that DoPetShop shipped with latest versions of DataObjects.NET includes a lot of additional features - for example, it utilizes security system, full-text search and serialization features.

Moreover, it includes administration module. First version of DoPetShop that was very close to the original .NET Pet Shop by the feature set was much smaller - its data and business tier code size was less then 20 Kb, so it was nearly 7 times smaller then its original!

DoPetShop utilizes DataObjects.NET access control system - this means that DataObjects.NET takes complete care about the authentication and authorizes access to application's business objects

DataObjects.NET brings true full-text search to DoPetShop, while .NET Pet Shop always uses like to locate necessary products

DataObjects.NET, and consequently, DoPetShop, fully supports 6 database server platforms (Microsoft SQL Server 2005 \ 2000, MSDE 2000, Microsoft Access, Oracle and Firebird) while .NET Pet Shop - only SQL Server 2005 \ 2000 \ MSDE and Oracle

DoPetShop provides full, but safe access to its data and business tier via .NET Remoting (while .NET Pet Shop allows to perform only one simple operation via its web service), so generally you can perform any activity remotely, e.g. such manipulations with persistent objects as creation, changing, deletion. Allowed activities certainly depend on your security permissions. See DoPetShop Remoting Client - this WindowsForms application really does this (it shows a lot of other features also, see the screenshot).

There is only one Query in DoPetShop sensitive to database server version! I.e. different values are assigned to Text property of this Query instance depending on current database server.

There are some other benefits of DoPetShop, e.g. it provides more information on cart and order-related pages, but may be the most exciting one is that it's extendable with much less amount of efforts. You can find more information about this sample further.

 

We believe DataObjects.NET currently is one of the best tools on the market. It's a complete DAL and RAD tool for your business tier. It offers the richest feature set. We hope you'll enjoy using it!

 

Major features of DataObjects.NET:

Transparent persistence: you don't need to write data access code for insert\update\delete operations - DataObjects.NET persists instances transparently for you. This means that you should never think about invoking a Save\Load-like methods also - DataObjects.NET handles such tasks completely transparently making you fell you're working with ordinary object instances. Transparent persistence has similar benefits as automatic garbage collection - you shouldn't worry about persisting your changes. Note that this doesn't mean all changes are persisted immediately - DataObjects.NET optimizes the update sequence (see delayed updates feature description)

Automatic database schema building\upgrading (database schema includes tables, views, columns, indexes, etc.): on each startup of Domain its database can be upgraded to support new persistent model (persistent model changes e.g. when you modify, add or delete some persistent classes). Upgrade process doesn't destroy existing data.

Instance identification: DataObjects.NET uses unique 64-bit integers in the database scope to identify instances; upcoming versions will support GUID and 32-bit integers also.

Querying: use DataObjects.NET query language (see Query description) or perform a direct SQL query (see SqlQuery description) to select the instances you're interested in. An example of DataObjects.NET query: "Select Animal instances where {LegCount}=4 order by {Name}" - this query fetches all four-legged Animal instances, as well as instances of Animal descendants - e.g. Cats and Dogs). More difficult example selecting grandparents instances: "Select Animal instances where {Children[{Children.count}>0].count}>0". DataObjects.NET query language supports sub-queries, joins, distinct and full-text search part in criteria.

Inheritance support: as it was mentioned, DataObjects.NET fully supports inheritance for persistent classes. But we went much further here: DataObjects.NET supports so-called persistent interfaces. This unique feature allows you to query for objects implementing some interface and refer to its persistent properties inside query criteria.

Highly configurable persistence for instance properties:

Multilingual properties (unique feature): you can mark any property with [Translatable] attribute to specify that it should store independent versions of its value for each Culture registered in you Domain. This and a set of other features dramatically simplifies development of multilingual database applications

References: don't worry about loading referenced objects, as well as persisting them properly - just write a code like cat.Parent = grandCat. Note that if you'll try to delete the grandCat instance after executing this code, cat will be notified (because it holds a reference to grandCat). Also after grandCat deletion cat.Parent will be automatically set to null

Collections: cat.Children.Add(kittyCat). Paired collections and reference properties (unique feature) are supported (so kittyCat.Parent can be automatically set to cat for the code above). "Contained" collections and references are supported as well (if cat.Children is marked by [Contained] attribute, all instances referenced by Children collection will be deleted on cat's deletion)

Structs: any struct type can be used as type of persistent property or item of collection. DataObjects.NET persists such properties into multiple database columns according to the structure of struct type.

[Serializable] properties: properties which type is marked by [Serializable] attribute are persisted without any additional coding - they're stored as byte streams (BLOBs). DataObjects.NET handles references to other persistent entities inside such serialized graphs in a special way making them to point to appropriate instances on each deserialization (note that DataObjects.NET is transactional, so you can find different versions of the same persistent instance in different sessions)

SQL-related attributes are available for almost any type of persistent properties ([SqlType], [Length] and similar ones)

Indexing: you can apply [Indexed] attribute to a persistent property to tell DataObjects.NET that an index should be created on it, or use [Index] attribute to describe a compound index

Full-text indexing & search (unique feature): DataObjects.NET supports full-text indexing and search. Microsoft Search (available for SQL Server 2005\2000 only) and DotLucene (free, RDBMS-independent) full-text indexing and search drivers are shipped with DataObjects.NET. Built-in managed wrapper for Microsoft Index Service filters helps to index almost any document\file type stored in database or externally. In particular, you can index the following document types: Microsoft Office files (.doc, .dot, .rtf, .xls, .ppt, etc...), HTML files (.htm, .html), Adobe PDF files and so on.

Built-in NTFS-like security system  (unique feature): DataObjects.NET has extremely powerful security system allowing to define the permission for any action (e.g. method execution or property access), grant or deny it for a set of security principals (users and roles) on some persistent instances (like on folders in NTFS) and enforce its presence by demanding it inside methods or property assessors of persistent types or DataServices. It brings NTFS-like security into your business tier with almost zero additional coding!
We want to underline two very important features of this subsystem:

Performance: security system is extremely fast - all passed permission demands are cached, effective permission sets for any cached DataObject instance are cached too, internal security notification layer allows any cached DataObject instance to notify all dependent cached instances on changes in its effective permission set... Normally a subsequent permission demand on the same instance is quite fast. This allows DataObjects.NET to execute up to 4000000 permission demands per second on 2,8GHz P4! It's almost impossible to implement a security system having the similar features and performance without implementing all other caching features DataObjects.NET has. Just imagine the nightmare of implementing the similar part in your DAL!

Immediate effect: all security restrictions take effect immediately on any security-related changes in Session - so it's not necessary to reopen Session \ invoke some method to apply new security restrictions. When you adding a User to some Role, granting permission for him or for some role it belongs to, this immediately affects on its security restrictions in the current Session - so all is transparent even in this case. Even rollback of nested transaction (or rollback to savepoint) immediately affects on security restrictions.

Rich data import and export capabilities:

Serialization: DataObjects.NET completely supports .NET Serialization, so you can serialize or deserialize a graph with persistent instances using binary or SOAP formatters. Custom formatters are supported as well.

Adapter component exports persistent instances to DataSet and imports back the changes

Offline layer provides advanced implementation of well-known DTO (Data Transfer Object) pattern. It allows to export a set of DataObject instances to serializable, marshal-by-value ObjectSets, pass them to the client to operate with them locally and propagate the changes made to them back to the application server

BindingManager component (unique feature) establishes two-way bindings between ASP.NET\WindowsForms controls and object properties. It brings Property-PropertyEditor bindings in contrast to common Property-ControlProperty bindings - we think this approach is also interesting for developers

All import\export tools support VersionID\VersionCode validation (optimistic updates)

Versionizing provides an ability to "see" the database at any previous state (point in time). This feature works only if it's turned on for the whole Domain. Currently Versionizing is supported by SQL Server 2005 \ 2000 driver only.

Automatic transactions (unique feature - it quite differs from standard COM+ automatic transactions, read more about it further): DataObjects.NET is completely transactional. It supports automatic transactions (started with a method call or property access and committed or rolled back based on result; transactional behavior is controlled by attributes) as well as manual transactions (you can also start/commit/rollback a transaction manually); nested transactions and savepoints are also supported.

Transparent deadlock handling (unique feature): in a highly concurrent environment deadlocks are regular. In case of a deadlock one of the deadlocked transactions (deadlock victim) is aborted by the database server. Normally it should be re-executed later by the application - this situation is called "deadlock handling". DataObjects.NET is capable of handling deadlocks transparently - if a deadlock occurs during an automatic transaction, it can be transparently handled by re-processing of the outermost transaction (re-calling of corresponding method).

Data services (unique feature) allows to use all DataObjects.NET transactional capabilities (automatic transactions, transparent deadlock handling) with non-persistent classes (DataService descendants). This feature greatly simplifies development of services operating with persistent instances. It's very convenient to use this conception to develop common services of an application, such as Logging Service. Data services are very close to ServicedComponents of .NET, the difference is that they are much faster (method calls are intercepted by the runtime proxies rather then .NET transparent proxies, no COM+ services are used) and easier to use.

Runtime services (unique feature) are data services of special type (RuntimeService descendants) that can be periodically executed in a special Thread and Session inside Domain. The purpose of runtime services is to perform various maintenance tasks periodically. For example, all IFtIndexer implementors are runtime services that periodically update full-text indexes.

 

Transparent persistence, built-in security system, automatic transactions, data services, .NET Remoting and DTO pattern support allows you to build not only the data access tier with DataObjects.NET, but the whole business tier of your application.

 

Supported technologies\platforms:

6 RDBMS platforms: DataObjects.NET makes most part of your business tier automatically compatible with Microsoft SQL Server 2005\2000, MDSE 2000, Microsoft Access, Oracle and Firebird without any additional code
Note: DataObjects.NET v3.9.X currently supports only SQL Server 2000\2005; upcoming v4.0.X will initially support only this RDBMS as well.

2 full-text search engines: Microsoft Search (supports SQL Server 2005\2000 only) and DotLucene (free, RDBMS-independent)

.NET Framework 2.0: DataObjects.NET supports all essential features of .NET 2.0 (generics, nullable types)

Mono: core part of DataObjects.NET runs on Mono, you can find some screenshots here: http://www.x-tensive.com/Forum/viewtopic.php?p=3837#3837

.NET Framework 1.1/2.0 languages: C# and VisualBasic.NET are supported. DataObjects.NET also should support almost any programming language available for .NET - JScript.NET, J#, Managed C++ (actually this is a feature of almost any .NET application)

.NET Remoting: any persistent instance or DataService can be marshaled to another Application Domain via .NET Remoting (as well as all other DataObjects.NET-related objects, e.g. Query). This means that you can access these objects from a completely different network or across the Internet with almost no additional code; moreover, you can use offline layer - it's an implementation of DTO (data transfer object) pattern for DataObjects.NET.

Distributed transactions: you can enlist any Session instance in the MS DTC\COM+ distributed transaction.

 

Performance-related features:

Caching speeds up queries and access to referenced instances by reusing already fetched data. There are two-level caching in DataObjects.NET:

Global cache is shared between all Sessions operating in the same Domain (usually there is one Domain object per application except NLB clustering case). Global cache has fixed size, and contains instantiation data for a set of most frequently accessed objects. The information fetched from this cache is always validated - i.e. DataObjects.NET uses it only when it really actual. Usually validation requires much less time then fetching, and moreover, DataObjects.NET gathers validation data on any query (since it's also quite cheap) - e.g. running a query with LoadOnDemand option (such queries are quite fast, since they fetch only two numeric columns from the storage) may lead to zero subsequent fetches (queries), because all necessary data could be taken from global cache, as well as validated.

Session cache is a WeakReference-based storage caching already instantiated objects, as well as information necessary to instantiate them or validate the instantiation data cached in the global cache. For example, if you execute two subsequent queries and process two object sets returned by these queries, none of processed objects with the same ID will be instantiated twice, moreover, instantiation data necessary to create it won't be even fetched twice from the underlying IDataReaders (certainly if these operations are executed in the same transaction).

Two-level caching layer is only a part of caching techniques used in DataObjects.NET. Lot of data \ intermediates are cached internally - for example, all evaluated effective permission sets and effective user's role sets are cached by the security system.

Lazy instantiation: DataObjects.NET instantiates (creates in-memory object and fetches its state from the database) referenced objects on the first access attempt. E.g. evaluation of cat.Children[0].Friends[1].Parent can lead to instantiation of up to 3 new objects (cat.Children[0]cat.Children[0].Friends[1] and cat.Children[0].Friends[1].Parent). The actual number of fetched objects depends on if these instances were accessed before and the state of caches.

Lazy loading (or load-on-demand): [LoadOnDemand] attribute applied on the persistent property notifies DataObjects.NET that value of this property should be fetched from the database on the first attempt to access it. This feature is highly required while working with instances containing large amount of rarely accessed data (e.g. BLOBs). The similar behavior is available for queries - it's possible to specify that QueryResult should internally contain only IDs of selected objects rather then complete instantiation data, although will transparently transform these IDs into the DataObject instances for you (by performing additional queries). This feature helps to keep very large result sets in memory.

Preloading: DataObjects.NET provides a set of ways to preload a group of load-on-demand properties or collections for the specified set of objects, or specified set of objects itself - by minimal amount of queries.

Delayed updates: almost all types of updates are delayed by default and flushed to the database as late as it's possible. Late update sequence is normally performed by much less number of queries. This feature is almost equivalent to using BeginUpdate()\EndUpdate() blocks in the early versions of DataObjects.NET, but always whenever it's possible.

DataObjects.NET is optimized for Microsoft SQL Server 2005\2000. Our performance tests & profiling are performed mainly on SQL Server 2005. Moreover, we provide support for a set of special features of this database server family (for example, full-text search and *(max) data types).

Only low-level ADO.NET classes are used. We don't use DataSet, DataTable and other high-level ADO.NET classes to increase the performance.

 

DataObjects.NET is NOT an ordinary object-relation mapping tool or API. The main differences in comparison with the most commonly used tools are:

DataObjects.NET is more than a persistence layer - its feature set (transparent persistence, built-in security system, automatic transactions, data services and .NET Remoting support) allows you to build not only the data access tier, but the whole business tier on it.

DataObjects.NET uses classes, their properties and special attributes as the only information source. No additional files or a database with existing structures is required. For example, if you want some field to be indexed, you can apply [Indexed] attribute on it and DataObjects.NET will automatically create a new index on corresponding database column.

DataObjects.NET can automatically upgrade existing database schema to a new version without any data loss.

 

Note that you can still use the database built by DataObjects.NET in another application or even in the DataObjects.NET-based application by any traditional way. So if your problem would be better solved by direct SQL queries you can use them.

 

The benefits of using existing relational database to store objects are obvious: the evolution of the relational database is a process that we have been watching for the past 30 years; the most successful relational databases are quite reliable and fast, so to build a new level on the top of them is one of the most risk-free ways to implement a new object-oriented database system.  This way of storing objects data is also known as Object-Relational Mapping (ORM).


 

 

Product Editions

DataObjects.NET is available in 6 editions. All differences between them are shown in the table below.

 

Editions Comparison Matrix

Feature \ Edition

Express

Standard

Advanced

Professional

Enterprise

Persistence engine

 

   Persistent types count

50 at max

No limit

   Tables count

100 at max

No limit

   Persistent interfaces*

Not Supported

Supported

   Versionizing

Not Supported

Supported

Global Cache

 

   Maximal size

100 Kb

10 Mb

No limit

   Dependencies

Not Supported

Supported

Diagnostics

 

   Performance counters

Not Supported

Supported

Upgrades \ updates

 

   Release period*

~ 3 months

~ 2 weeks ... 1 month

   Free update period*

-

1 year

Support

 

   E-mail

No

Yes

   Forum

No

Yes

   Response time*

-

24 hours

12 hours

Source code

No

Yes

Nightly Builds

Not available

Available

   Update period

-

Weekly

Daily

Developers*

-

1

4

Online order

Price & purchase options

 

   Price (EUR)

Free

199

299

695

1995

   Online order page

-

Buy now

Buy now

Buy now

Buy now

Order at ComponentSource.com

   Pay by wire transfer

Click here to request the invoice by e-mail

Discounts

 

   1st Training session

-

None

300 EUR

   Volume discounts

Check volume discounts

   "Pay by wire" discount

5%

 

Remarks:

All unspecified features are absolutely the same for all Editions

"-" means "Not applicable"

Persistent interfaces in Express Edition are supported, but only built-in (declared inside DataObjects.NET assembly, e.g. IFtObject), as well as with few pre-defined names only (to make all samples that utilize them working)

"Release period" is average period between subsequent releases of the product

"Free update period" is the period of free product updates after the moment of purchase. All upcoming versions (releases) of product, including major version updates, are electronically delivered to the customers without any additional charges during this period

"Support response time" is maximal amount of time that can be spent on the first response for regular support requests; bug reports, as well as other cases requiring testing may require more time then 24 hours

"Developers" row contains the number of developers allowed to use matching DataObjects.NET edition as it's described in License Agreement. The number greater then 1 states that corresponding Edition is License Pack also.

All online order pages are secure.

 


 

System Requirements

To use DataObjects.NET you should have the following components installed:

.NET Framework 1.1 / 2.0 or Mono (Mono support is in alpha stage currently)

Microsoft SQL Server 2005 with SP1, or Microsoft SQL Server 2000 with SP3, or MSDE 2000 with SP3, or Microsoft Access, or Oracle (9i or higher) or Firebird (1.5.1 or higher).
Note: DataObjects.NET v3.9.X currently supports only SQL Server 2000\2005; upcoming v4.0.X will initially support only this RDBMS as well.

Microsoft Search service should be installed and running in order to use full-text search functionality with Microsoft SQL Server 2005\2000

DotLucene (Lucene.Net.dll) should be available in assembly search path list in order to use its full-text search functionality (it's fully RDBMS-independent)

Oracle Client should be installed in order to use DataObjects.NET with Oracle. If you're planning to use NativeOracle driver, ODP.NET should also be installed

Firebird Client and Firebird .NET Data Provider (1.6.3 or higher) should be installed in order to use DataObjects.NET with Firebird.

 

Additionally you should have Visual Studio .NET 2003 installed to recompile the samples shipped with DataObjects.NET in the most convenient way. Otherwise you can perform this task using command line compiler supplied with .NET Framework.

 

Installation

To install DataObjects.NET you should simply execute the installer. There are several possible names of the installer (it depends on the download location): DataObjects.NET_X.Y.Z_EditionName.exe (e.g. DataObjects.NET_3.6_Express.exe), DataObjects.NET_EditionName.exe.

 

Redistribution

DataObjects.NET assemblies (DataObjects.NET.dll, its .NET 2.0 and Mono versions) may be distributed along with your products. This is the only redistributable file. Note that you may not distribute the DataObjects.NET.dll with any sort of software that attempts to replace or duplicate in part, or in whole, the functionality of DataObjects.NET. Essentially, if your product makes use of the DataObjects.NET.dll and your application's functionality does not reproduces the functionality of the DataObjects.NET, you are free to distribute the DataObjects.NET assembly with your application.

 

Please note that this description is not a binding license agreement, you should refer to the License Agreement bundled with the product for details.

 

Documentation

DataObjects.NET includes the following documentation:

DataObjects.NET Manual
Covers most of DataObjects.NET features and concepts

DataObjects.NET HTML Help
Contains HTML versions of all available documents (Manual, Readme, FAQ, License Agreement, Benefits) as well as reference covering all DataObjects.NET classes and their members

DataObjects.NET FAQ (Frequently Asked Questions)
Contains answers to most frequently asked questions.

DataObjects.NET Revision History

 


 

Key concepts

Persistent type, persistent instance, persistent properties

Persistent type is a type whose instances are stored (exists) in the database, rather then RAM. In-memory proxy objects provide access to instances of these types.

 

Persistent instance is an instance of persistent type.

 

Persistent property is a property of persistent type whose value is kept in the database. A persistent type may have both persistent, as well as not persistent properties. Any not persistent property "keeps" its value only while a proxy object for the persistent type resides in memory.

 

Let's see how DataObjects.NET distinguishes between persistent and regular types:

 

public abstract class Person: DataObject

{

  public abstract string Name {get; set;}

  public abstract string SecondName {get; set;}

  public abstract string Surname {get; set;}

  public abstract int    Age {get; set;}

  public abstract string Info {get; set;}

}

 

Persistent type named Person is declared here. This type has the following persistent properties: Name, SecondName, Surname, Age and Info.

 

Any persistent type should be inherited from DataObject or any of its descendants; any persistent type should be declared as abstract. Persistent properties can be abstract\virtual or not - this depends only on your wish. If persistent property is an abstract one, DataObjects.NET will automatically implement its setter and getter in proxy type (proxy type is always a non-abstract descendant of some abstract persistent type, DataObjects.NET builds proxy types at runtime); otherwise it should be implemented by your own. You can find an example of implementing non-abstract persistent property in FAQ.

 

By default any public or protected property of persistent type are also persistent. But it is possible to explicitly specify whether a property is persistent or not - you can use [DeclarationMode], [Persistent] and [NotPersistent] attributes for this purpose. To specify whether all properties are persistent by default or not [DeclarationMode] attribute is used. To manually specify whether a certain property is persistent or not [Persistent] and [NotPersistent] attributes are used.

 

Let's add a not persistent property to Person type and apply above-mentioned attributes:

 

[DeclarationMode(DeclarationMode.PersistentByDefault)]

public abstract class Person: DataObject

{

  public abstract string Name {get; set;}

  public abstract string SecondName {get; set;}

  public abstract string Surname {get; set;}    

  public abstract int    Age {get; set;}

  public abstract string Info {get; set;}     

  [NotPersistent] public abstract string SomeNotPersistentProperty {get; set;}     

}

 

SomeNotPersistentProperty is marked by [NotPersistent] attribute. So this property is not persistent.

 

Let's see on alternative way to achieve the same result:

 

[DeclarationMode(DeclarationMode.NotPersistentByDefault)]

public abstract class Person: DataObject

{

  [Persistent] public abstract string Name {get; set;}

  [Persistent] public abstract string SecondName {get; set;}

  [Persistent] public abstract string Surname {get; set;}    

  [Persistent] public abstract int Age {get; set;}

  [Persistent] public abstract string Info {get; set;}

  public abstract int SomeNotPersistentProperty {get; set;}

}

 

Let's add FullName property to Person type (instead of SomeNotPersistentProperty).

 

public abstract class Person: DataObject

{

  public abstract string Name {get; set;}

  public abstract string SecondName {get; set;}

  public abstract string Surname {get; set;}    

  public abstract int Age {get; set;}

  public abstract string Info {get; set;}     

 

  [NotPersistent]

  public virtual string FullName {

    get { return Name+" "+SecondName+" "+Surname; }

  }   

}

 

Now let's declare a descendant of Person named Author having its own persistent property:

 

public abstract class Author: Person

{

  public abstract string HomePage {get; set;}

}

 

As you may find, you can easily inherit one persistent type from another.

Instance identification

DataObjects.NET uses a 64-bit integer to uniquely identify each persistent instance in the database. The instance identifier (ID) is available through the ID property of DataObject.

 

Two persistent objects are the same if the references to them are equal, and contrary, instances are different, if references to them aren't equal.

Note: this rule is valid only for instances that belong to (accessed\created within) the same Session (in practice each thread usually uses a single Session object to access the database). To test two instances of different sessions for equality you should compare their ID property value.

 

This identification technique allows us to provide full inheritance support. Imagine that two descendants (for example, Bear and Mouse) of some base type (for example, Animal) have different SQL types of primary key (for example, VarChar and Int). If some other persistent type declares a persistent property of Animal type (for example, a Cage type can declare ContainedAnimal property) - what SQL type should be used to store this reference? The problem is that any Animal object can be assigned to this property - this means that actual type of object can be Animal, Bear or Mouse. If all these types use different primary key types, it's problematic to decide what should be done to store the foreign key to Animal type.

 

For example, we can use a set of columns (one per each PK type) to store the reference, but this is expensive, plus makes lots of problems with queries in addition (imagine the joins in this case). So the first conclusion: we should use a single type of primary key in any inheritance branch.

 

Moreover, primary key values should be unique in the scope of this branch - otherwise it will be impossible to use a single column to store the foreign keys. For example, imagine that two Bear and Mouse instances have the same primary key value (this is possible, because they may keep their data in completely different tables) - but in this case we can't use a single column of their PK type to store a ContainedAnimal reference in the Cage class. This is the second conclusion.

 

And finally, if we want to have a single root type in our inheritance branch (like Object in the .NET Framework - for example, to have an ability to use persistent properties or collections of this type), we should use a single type of primary key in the whole database scope, and its values should be unique in this scope too. That's why DataObjects.NET has this feature.

Version indentifiers

Every persistent instance has VersionID property that is checked (for equality) and increased on every persist operation on instance. This property prevents updates of already modified instances on Read Committed isolation level and helps to maintain instance cache higher isolation levels.

 

In the web environment you can pass value of this property through web page to ensure that instance, which you're going to update on postback, wasn't updated by a concurrently working user.

 

An example:

 

public abstract class Person: DataObject

{

  public abstract string Name {get; set;}

}

 

...

 

Person p = (Person)session.CreateObject(typeof(Person));

Console.WriteLine("Initial VersionID: ", p.VersionID);

p.Name = "Alex";

p.Persist();

Console.WriteLine("Current VersionID: ", p.VersionID);

Database schema maintenance

Database schema is maintained completely automatically by DataObjects.NET.

 

This is one of the nicest DataObjects.NET features. You can almost forget that you are actually working with relational database - DataObjects.NET handles not just instance persistence, but maintenance of the database schema as well.

 

Earlier we mentioned that assemblies with persistent types are the only information source for DataObjects.NET - they contain all necessary information to be "translated" into the database schema.

 

Note: wide set of attributes allows to define desirable persistence behavior for persistent types and their properties.

 

On every startup of your application (exactly - during Domain.Build(...) execution) DataObjects.NET checks if current database schema is valid for the currently used persistent model (a set of persistent types registered in the Domain) and upgrades it, if necessary - adds new tables, table columns, views or deletes some of them, etc... It can completely recreate it, or just check if current database schema requires an update - all is upon your wish.

 

Please read the next chapter and see Domain.Build(), DomainUpdateMode description in .HxS\.Chm Help for additional information.

Domain

Each Domain instance provides access to a single database. You can think of Domain as of instance of object-oriented database managed by DataObjects.NET. Note that more then one Domain object may provide access to a single database (for example, each of them may exist on its own cluster node) - DataObjects.NET fully supports this operation scenario.

 

Domain lifetime consists of 2 stages:

Initialization (setup) stage

Operational stage

 

During Domain setup time you should register the following sets of objects\types:

Cultures (Culture objects)

Persistent types (DataObject descendants)

Data service types (DataService descendants)

as well as specify some other parameters, such as ConnectionUrl (a string describing how to connect to the database server), security settings, etc...

 

Setup stage completes when Domain.Build method is called. DataObjects.NET studies your persistent types, builds proxies for each of them and validates\updates the underlying database. It's under your responsibility to provide an exclusive access to the database for this period.

 

Domain becomes operational on completion of setup stage - i.e. it can be used for creating Session objects, which provide access to persistent instances.

 

Let's look on this example:

 

class SampleA

{

  private static Domain BuildDomain()

  {

    string connectionUrl = "mssql://localhost/DataObjectsDotNetDemos";

 

    string productKeyFile = @"C:\ProductKey.txt";

    string productKey = "";

    if (File.Exists(productKeyFile))     

      using (StreamReader sr = new StreamReader(productKeyFile))

      {

        productKey = sr.ReadToEnd().Trim();

      }     

 

    Domain domain = new Domain(connectionUrl, productKey);

    // Domain setup process starts here

    domain.Configuration.RegisterCulture(

      new Culture("En","U.S. English", new CultureInfo("en-us",false)));

    // It's always necessary to specify default culture

    domain.Cultures["En"].Default = true;

    // Here we register all types from "SampleA.Model" namespace

    domain.RegisterTypes("SampleA.Model");

 

    // Now we can build the domain. An exclusive access

    // to domain database should be provided for this period.

    domain.Build(DomainUpdateMode.Recreate);

 

    // Domain setup process is finished here

    return domain;

  }

 

  static void Main(string[] args)

  {

    Console.WriteLine("Building domain...");

    Domain domain = BuildDomain();

    Console.WriteLine("Domain is built.");

  }

}

 

Note: it is supposed that product key is placed to C:\ProductKey.txt file, underlying database is located on the local Microsoft SQL Server instance and its name is "DataObjectsDotNetDemos". Windows authentication should.


 

DataObjects.NET Configuration File

Note: This section describes new feature of DataObjects.NET v3.9.

 

It is possible to use a configuration file to configure the Domain (i.e. register cultures, persistent types, specify ConnectionUrl, product key and so on). Here is an example of configuration file shipped with DoPetShop (a part of its Web.config file):

 

<?xml version="1.0" encoding="utf-8"?>

<configuration>

  <configSections>

    <section name="DataObjects.NET"

             type="DataObjects.NET.ConfigurationSectionHandler,Dataobjects.NET" />

  </configSections>

 

  ...

 

  <DataObjects.NET>

    <!-- connectionUrl="oracle://admin:admin@localhost/DoPetShp" -->

    <!-- connectionUrl="sapdb://admin:admin@localhost/DoPetShp" -->

    <domains>

      <domain>

  <domainConfig

    connectionUrl="mssql://localhost/DoPetShop"

    productKeyPath="..\..\..\ProductKey.txt"

    debugInfoOutputFolder="C:\Debug"     

    databaseOptions="CreateForeignKeys, CreateNullRows, EnableFullTextSearch"

    securityMode="Modern"

    securityOptions="Standard, AllowCreateUnauthenticatedSessions"

    sessionSecurityOptions="Standard"     

    updateMode="Perform"

    >

    <cultures defaultCulture="En">

      <culture name="En" title="U.S. English"

               cultureName="en-US"

               compareOptions="IgnoreWidth,IgnoreKanaType"

      />

    </cultures>

    <references>

      <assembly  name="DoPetShop.Model" />

    </references>

  </domainConfig>

      </domain>

    </domains>

  </DataObjects.NET>

 

  ...

 

</configuration>

 

DoPetShop utilizes this code configure the domain from Web.config:

 

DomainConfiguration configuration = DataObjects.NET.Configuration.Default;

domain = new Domain(configuration);

#if !DEBUG

  domain.DebugInfoOutputFolder = null;

#endif

domain.InitializeSystemObjects += new SessionEventHandler(

  Application_InitializeSystemObjects);

domain.FtsDriver.StartBackgroundIndexing();

domain.RuntimeServices.AddRuntimeService("CleanupService",typeof(CleanupService));

domain.Build();

 

As you may see, first line of this code utilizes static member of Configuration class to get default DomainConfiguration object; this object is later used to create a new Domain instance based on it.

 

Please see Configuration class description for example of all possible configuration options (attributes and tags).

 

Why we decided to move lots of formerly Domain properties to DomainConfiguration class? This new approach provides many benefits:

It is possible to easily create the clone of Domain: just create one more Domain with the same DomainConfiguration object

It's easy to clone DomainConfiguration object (e.g. to modify the configuration) - just call its Clone method

You can serialize \ deserialize DomainConfiguration objects, as well as pass them across AppDomain boundaries in serialized form using .Net Remoting (they aren't MBR objects)

.Net 2.0 specific feature: DomainConfiguration can be imported from and saved to ExeName.config \ Web.config files (any configuration file). See FromElement \ ToElement methods of Configuration class for details.

 

Summary: above features open the opportunity to develop tools that are fully independent from the actual Domain configuration, such as upcoming Oql.exe (query profiling tool) and Upgrade.exe (sequential upgrade tool + advanced upgrade API). Any of such tools will be able to build fully functional Domain object by using new configuration API -  e.g. it may simply load the configuration file from the current folder, or use command line parameters to locate it. Formerly it was impossible to do the same without additional coding.

Session

Session is IDbConnection analogue in DataObjects.NET.  Each Session instance is associated with single IDbConnection object that is used to access the underlying database.

 

Session instances aren't safe for the multithreaded operations. To work with the database using multiple threads you should use separate Session instances in each thread, or manually synchronize access to these objects.

 

Key facts you should know about sessions and transactions:

Each Session object is created by Domain.CreateSession method (or by Session constructor, which takes Domain as argument). Thus any Session always belongs to the Domain it was created in, and consequently allows to operate with (and only with) types\cultures, which are registered in this Domain.

DataObject instances can exist only within the Session (it's a SessionBoundObject descendant). I.e. any DataObject instance belongs to some Session, moreover, two different DataObject instances with the same ID may exist in your application (but in this case they definitely belong to different Session objects). Thus different Session instances that belongs to the same Domain contains different DataObject instances, even while their IDs are the same.

Each Session maintains its own cache of fetched DataObject instances, but there is also global instance cache, that is shared between all sessions. Both caches are used and updated completely transparently for you. There is no need to synchronize access to global instance cache.

You can access the database (work with the instances of DataObject) only during a Transaction. Or it's better to say: "any attempt to access the database will lead to creation of automatic transaction - moreover, DataObjects.NET will create it independently of your wish". We'll explain this behavior further, for now you should just know that any DataObjects.NET method invoked from your code that can potentially access the database will be "wrapped" into a transaction.

Transparent persistence

Transparent persistence is the main and the simplest to understand feature - DataObjects.NET makes you feel that you are working with ordinary object instances (almost as if they are instances of non-persistent classes). When you try to read or change the properties of instances, setup the relations between them and so on, DataObjects.NET fetches all the required data or persists changes to the underlying relational database transparently for you.

 

Let's look at an example:

Cat sonya = ... ; // some code that fetches the Cat instance
sonya.Name = "Sonya"; // DataObjects.NET transparently persist a new
                      // property value
Cat kitty = sonya.Children[0]; // DataObjects.NET queries a collection and fetches
                               // the collection item
Console.WriteLine(kitty.Parent.Name); // output: "Sonya"

Creating, updating and removing persistent instances

Let's create, update and remove some persistent instances in the following sample:

 

class SampleA

{

  private static Domain BuildDomain()

  {

    ...

  }

 

  static void Main(string[] args)

  {

    Console.WriteLine("Building domain...");

    Domain domain = BuildDomain();

    Console.WriteLine("Domain is built");

 

    long personID = 0;

    long authorID = 0;

 

    // You should work with Session objects by nearly the same way

    using (Session session = new Session(domain))

    {

      // We can access the storage only during transaction, so...

      session.BeginTransaction();

 

      Console.WriteLine("\nFirst session");

 

      // That's how we create our first persistent instance of Person

      Console.WriteLine("Creating an instance of Person ");

      Person p = (Person)session.CreateObject(typeof(Person));

      // Instance is already persisted to the storage now

 

      // Let's set persistent properties

      p.Name = "John";

      p.Surname = "Smith";

      p.SecondName = "V.";

      p.Age = 32;

      p.Info = "John V. Smith, 32 years old";

 

      Console.WriteLine("Person's properties:");

      Console.WriteLine("\tPerson's name: {0}", p.Name);

      Console.WriteLine("\tPerson's surname: {0}", p.Surname);

      Console.WriteLine("\tPerson's second name: {0}", p.SecondName);

      Console.WriteLine("\tPerson's info: {0}", p.Info);

     

      // And read the instance ID       

      personID = p.ID;

      Console.WriteLine("\tPerson's ID: {0}", p.ID);

      Console.WriteLine();

 

      // Let's create an instance of Author

      Console.WriteLine("Creating an instance of Author ");

      Author a = (Author)session.CreateObject(typeof(Author));

      a.Name = "Alex";

      a.Surname = "Box";

      a.SecondName = "T.";

      a.Age = 47;

      a.Info = "Famous author";

      a.HomePage = "www.Alex-Box.com";

 

      Console.WriteLine("Author's properties:");

      Console.WriteLine("\tAuthor's name: {0}", a.Name);

      Console.WriteLine("\tAuthor's surname: {0}", a.Surname);

      Console.WriteLine("\tAuthor's second name: {0}", a.SecondName);

      Console.WriteLine("\tAuthor's info: {0}", a.Info);

      Console.WriteLine("\tAuthor's homepage: {0}", a.HomePage);

 

      authorID = a.ID;

      Console.WriteLine("\tAuthor's ID: {0}", a.ID);

 

      // And finally commit our work

      session.Commit();

    }     

 

    // Let's create another one Session

    using (Session session = new Session(domain))

    {

      session.BeginTransaction();

      Console.WriteLine("\nSecond session");

      // And fetch the instance by its ID

      Person p = (Person)session[personID];

 

      Console.WriteLine("Person's age: {0}", p.Age); // Output: 32

 

      // Let's try Savepoints

      Console.WriteLine("Creating savepoint...");

      Savepoint sp = new Savepoint(session);

      Console.WriteLine("Savepoint created.");

 

      p.Age = 33;

      Console.WriteLine("Person's age: {0}", p.Age); // Output: 33

 

      Console.WriteLine("Rolling back to the savepoint...");

      sp.Rollback();

      Console.WriteLine("Rollback completed.");

 

      // During execution of the next line DataObjects.NET will

      // notice that transactional context is changed (by

      // rollback to the savepoint operation) and instance

      // will be reloaded.

      Console.WriteLine("Person's age: {0}", p.Age); // Output: 32

 

      Console.WriteLine("Person's name: {0}", p.Name); // Output: "John"

 

      // Changing the Name property.       

      Console.WriteLine("Changing person's name");

      p.Name = "James";

      Console.WriteLine("Changed person's name: {0}", p.Name);

 

      // Let's fetch instance of Author type by instance ID.

      Author a = (Author)session[authorID];

 

      // Removing instance.

      Console.WriteLine("Removing instance of Author type");

      a.Remove();

     

      session.Commit();

    }

 

    using (Session session = new Session(domain))

    {

      session.BeginTransaction();       

      Console.WriteLine("\nThird session");

 

      Person p = (Person)session[personID];

      Console.WriteLine("Person's name: {0}", p.Name); // Output: "James"

 

      // Let's try to fetch an instance of Author type by instance ID.

      Console.WriteLine("Trying to fetch an instance of Author type");

      Author a = (Author)session[authorID];

      if (a == null)

        Console.WriteLine(string.Format("There is no instance with ID = {0}",

          authorID))

      else

        Console.WriteLine("Author's name: {0}", a.Name);

      session.Commit();

    }

 

    Console.Write("\nPress Enter to close... ");

    Console.ReadLine();

  }

}


 

Persistent properties

Note: Further we'll frequently refer to persistent properties as persistent fields (really, they're actually much closer to fields rather then to properties) - don't be confused by such terminology.

Primitive fields

We have seen that persistent types can contain persistent properties of int and string types. It is certainly possible to declare persistent properties of other primitive types such as double, bool, long, char, DateTime, Guid and so on - let's call all properties of these types as primitive further.

 

As you may notice, DataObjects.NET provides wide set of attributes allowing to describe the behavior of persistent properties and types. Let's look on the list of attributes that can be applied to primitive properties:

[SqlType] attribute specifies the type of underlying database column. E.g. Guid property can be stored in SqlType.Char, SqlType.VarChar, SqlType.AnsiChar, SqlType.AnsiVarChar and SqlType.GUID columns.

[Nullable] attribute indicates that underlying database column should be marked as "can contain null values"; in this case null property value will be represented by NULL column value

[LoadOnDemand] attribute indicates that value of the property should be loaded on the first access, but not on the instance loading

[Translatable] attribute indicates that property is translatable. This means that set of values will be maintained for this property, one for each Culture registered in the Domain. Each of such values will be stored in its own column having a culture suffix (e.g. if property name is "Title", the name of the column can be "Title-En" for a culture having "En" Name). Further you'll find an examples of translatable properties.

[Indexed] attribute specifies that index should be built on this field. See [Index] attribute also.

[ChangesTracking] indicates which strategy should be used to track the changes of property. It will be described in "Persistent Property Attributes" section.

 

There is a set of additional attributes, which can be applied to persistent properties of specific types:

[Length] attribute indicates length of the underlying database column. Can be applied to properties of string, byte[] and similar types.

[Collatable] attribute indicates that property is collatable. This means that set of columns with different collations (one per each Culture registered in the Domain) will be created in the database to keep the value of single string to allow use different sorting rules for this property. This attribute can be applied to string properties only.
Note: usually each
Culture has its own sorting rules, e.g. "A" goes before "B" in "en-us" culture, but some other culture may specify different order for these letters. [Collatable] allows to order a set of objects by the same property values by several different ways.

 

Note: this list of attributes doesn't contain "common" attributes, which can be applied to all types of properties, such as [Persistent], [NotPersistent], [Alias], etc. These attributes are described in "Persistent Property Attributes" section. Please keep this in mind while studying other types of persistent properties.

Serializable fields

Any [Serializable] type can be used as type of persistent property. Such property is internally handled nearly by the same way as byte[] property - i.e. value of any property of serializable type is internally stored as byte stream in the single column of property owner's table.

 

DataObjects.NET utilizes binary formatter with a bit overridden serialization behavior to serialize\deserialize the values of such properties. Overridden part allows to transparently serialize and deserialize references to DataObject instances that can be contained internally in such properties.

 

Note: it was mentioned that DataObjects.NET automatically detects the changes made to persistent properties. But it's certainly can't automatically detect the changes made to internal contents of value of serializable property. Let's think Author.Image is property of System.Drawing.Image type:

 

Author a = (Author)session[aid];

a.Image.RotateFlip(RotateFlipType.Rotate90FlipNone);

a.Image = a.Image;

 

Last line of code is absolutely necessary in this case, since it allows DataObjects.NET to mark a.Image property as modified, and further persist it.

 

Note: It was mentioned that delegate properties are internally handled nearly as byte[] properties - thus all attributes supported for byte[] type can be applied to them.

Reference fields

Properties that refer to other persistent objects are handled in a special way. Every reference is internally stored as the ID of the referred instance and transparently "transformed" to the real instance. This allows the .NET Framework to collect any persistent instance that isn't directly referred to from your code by its garbage collector.

 

Each reference property can be marked by the [Contained] attribute to notify DataObjects.NET that the referred instance should be automatically removed on removal of the container.

 

Well, let's declare new persistent type named Article which has a reference property.

 

public abstract class Article: DataObject

{   

  public abstract string Title {get; set;}  

  public abstract string Annotation {get; set;}

  public abstract string Body {get; set;}

  public abstract int PublicationDate {get; set;}   

  public abstract Author Author {get; set;}

}

 

As it is seen Author property is a reference to an instance of Author type. This property isn't marked by

[Contained] attribute so when an instance of Article is removed corresponded instance of Author is not removed. But if we want to remove that instance of Author we should declare Article type in the following way:

 

public abstract class Article: DataObject

{   

  ...

  [Contained]

  public abstract Author Author {get; set;}

}

 

Let's see, which attributes can be applied to reference properties:

[Nullable] - to indicate that underlying database column should be marked as "can contain null values"; in this case null property value will be represented by NULL column value (by default it's represented by 0)

[LoadOnDemand] indicates that property value (not referenced object, but its ID) isn't fetched from the database while its owner (an object to which this value belongs) is instantiated in memory, but on fetched the first attempt to use the property

[SelfReferenceAllowed] indicates that reference property may contain reference to its owner (by default this is not allowed)

[Contained] attribute was already described earlier. It indicates that referenced object should be removed together with property owner.

[Translatable] has the same meaning as for primitive properties

[AutoFixup(false)] indicates that reference property shouldn't participate in the reference cleanup process (this process is described right below)

 

There are several other and a bit more complex attributes, but we'll return to them further:

[PairTo], [Symmetric] - these attributes allows to define a pair of reference\collection properties, that should be automatically synchronized. Both of them are described further.

[ChangesTracking] indicates which strategy should be used to track the changes of property.

 

Note: it isn't necessary to apply [Index] attribute on reference fields - they're always indexed automatically (except when [AutoFixup(false)] is applied - next chapter explains this).

Reference cleanup process

Most likely you already noticed, that you should explicitly remove instances by invoking their Remove method. But why? E.g. you never do the same in C#, because .NET offers garbage collection, and certainly it's better alternative, but not for database applications:

There is a set of problems with implementing GC in RDMBS:

It is almost impossible to perform garbage collection without stopping any activity on the database;

It is always possible to fetch (and obtain the reference to) an instance that isn't referred from the primary object graph (that should be collected on the next garbage collection) by querying the database, that is impossible in such frameworks as .NET, and moreover, rather dangerous.

And moreover, it's technically difficult to implement fast GC for object graph stored in RDBMS, since the number of instances in it can be quite large (e.g. billions of objects) - but it's necessary to temporarily build a graph of relationships in RAM (to identify primary object graph)... This is impossible because of its size.

 

That's we have DataObject.Remove method.

 

Let's think we removed some Author a, but there was a Book b having b.Author==a. This book instance still resides in the database after author removal, and a column related to its Author property still contains identifier of removed author. It's certainly bad - e.g. query fetching all books without author will skip this object, since it looks like this book has an author. So it's desirable to have some mechanism allowing to handle such situations.

 

DataObjects.NET has this mechanism, and moreover, it operates completely automatically and transparently - you don't need to do anything to activate it. On removal of any instance DataObjects.NET performs the following actions:

Removes all contained instances (instances referred by a property marked by the [Contained] attribute) recursively;

Identifies all instances that refer (has references to) to the removing instance by querying the database;

Fetches each of these instances and sets the value of the property that refers to the removed instance to null (if the property is a simple reference) or removes the instance from the collection (if the property is a collection);

 

Additional notes:

Because of necessity to query for objects having specified reference field values an index is automatically created on any reference field. Each DataObjectCollection-related table also has 2 indexes

[AutoFixup(false)] indicates that reference property shouldn't participate in the reference cleanup process (and consequently index shouldn't be built automatically on such property).

Reference cleanup process is seriously optimized for removal of instance groups. You can find additional information about this in Revision History ("Version 2.5 - What's New"). See Session.RemoveObjects method also

All reference properties are processed during cleanup process:

Regular reference properties

Reference fields in struct properties

DataObjectCollection properties (since they can't contain null references, references to removed objects are simply removed from such collections)

References stored in ValueTypeCollection properties

Struct fields

You can use any struct type as type of persistent property. For example you want to declare a persistent type which describes a rectangle. So it is necessary to specify left-top and right-bottom vertexes. Of course it is possible to declare four separate properties: X1, Y1 (coordinates of left-top vertex) and X2, Y2 (coordinates of right-bottom vertex):

 

public abstract class Rectangle: DataObject

{   

  public abstract double X1 {get; set;}  

  public abstract double Y1 {get; set;}

  public abstract double X2 {get; set;}

  public abstract double Y2 {get; set;}   

}

 

But it is more convenient to declare a struct named Point which describes one point and to declare only two struct properties of Rectangle:

 

[Serializable]

public struct Point

{

  public double X;

  public double Y;

 

  public Point(double x, double y)

  {

    X = x;

    Y = y;

  }

}

 

public abstract class Rectangle: DataObject

{   

  public abstract Point LeftTop {get; set;}  

  public abstract Point RightBottom {get; set;}  

}

 

So now we can create an instance of Rectangle and read its properties in the following way:

 

Rectangle rect = (Rectangle)session.CreateObject(typeof(Rectangle));

Point leftTop = new Point(2, 4.5);

rect.LeftTop = leftTop;

rect.RightBottom = new Point(10.85, 8);

Console.WriteLine("Left: {0}", rect.LeftTop.X);

Console.WriteLine("Bottom: {0}", rect.RightBottom.Y);

 

Note: struct types should be marked by [Serializable] attribute.

 

It isn't necessary to register struct types you're going to use - such types can be stored only as part of its owners (DataObject instances), so DataObjects.NET is capable to find all such types by studying registered persistent types.

 

Now let's change our Article type by adding modified HomePage property which is now not string but struct:

 

[Serializable]

public struct WebReference

{

  public string Url;

  public string Description;

 

  public WebReference(string url, string description)

  {

    Url = url;

    Description = description;

  }

 

  public WebReference(string url)

  {

    Url = url;

    Description = null;

  }

}

 

public abstract class Author: Person

{

  public abstract WebReference HomePage {get; set;}

}

 

Additional notes:

It's impossible to mark some of struct fields as [NotPersistent]. DataObjects.NET consider all struct fields as persistent.

Struct fields can be private or internal - DataObjects.NET is capable to persist them anyway

You can use [Alias] and [DbName] attributes to "rename" such private fields (usually private fields are named in "camel" style)

Structs may contain nested struct fields

DateTime, TimeSpan and Guid are actually structs, but DataObjects.NET doesn't split their content into column set - they're handled by corresponding DataObjects.NET.ObjectModel.PrimitiveField descendants (so DataObjects.NET doesn't consider them as structs).

Structs may contain reference fields. Referenced objects are automatically instantiated in-memory when struct fields value is populated first time (usually this occurs on the first attempt to access it). OnReference\OnDereference events are properly fired.

Structs can't contain DataObjectCollection\ValueTypeCollection fields (such fields require an additional table for their persistence, but not column or a set of columns in owner's table).

All supported attributes may be applied to struct fields also with just one exception:

[Translatable], [LoadOnDemand] and [ChangesTracking] attributes can be applied to the whole struct property, but not to its parts (struct fields, or fields of nested structs)

DataObject.GetProperty(...)\SetProperty(...)\Item(...) methods allow to get\set nested fields of struct properties by using "X.Y.Z" notation. E.g. you can use SetProperty("VisitCard.Address.City", value) notation to do this. Nevertheless you woudn't feel this in your DataObject.OnSetProperty-like event handlers (method overrides), since DataObjects.NET always passes "fully combined" value and root property name to them. E.g. in this case you'll get usual OnSetProperty("VisitCard", culture, fullNewVisitCardValue) event.

Struct fields can be indexed - you should apply one or more [Index] attributes to the owner of struct property (class), and specify all struct parts you want to include into the index in "X.Y.Z, A.B" notation (e.g. [Index("TopLeft.X, BottomRight.Y")])

DataObjects.NET doesn't store a value of struct passed to any of its method - internally it's stored in  "exploded" (but enough compact) form. This allows it to track changes in its parts, and thus persist just modified part of struct. Moreover, it's anyway necessary to hold a copy of struct rather then passed struct - e.g. to allow GC to collect DataObject instances referenced from struct fields. So you can consider that DataObjects.NET always holds its own copy of passed value.

Collection fields

Native (or serializable) collections

DataObjects.NET can persist any collection that marked as [Serializable], such as ArrayList or Hashtable. But it persists such property as any other serializable type - i.e. by serializing it and storing its value in a single column of owner's table.

 

This is certainly not the best approach, but you can use it for storing relatively small collections (it can be even efficient in some cases). Let's list most important disadvantages of this approach:

The whole collection should be fetched\persisted on any change of its content

It's impossible to query against contents of such collection

DataObjectCollection

DataObjectCollection is a collection type that persisted into additional (collection) table. You can declare properties of this type, or of its customized (e.g. typed) descendants.

 

Notes:

Custom DataObjectCollection descendants must be abstract (proxies are generated for them also)

Collections must have only the get accessor (getters)

Use the [ItemType] attribute to specify the of objects that is stored in the collection

Collections are unordered - it's not guaranteed that order of collection items is always the same (collections aren't ordered), but you can use DataObjectCollection.CreateQuery or DataObjectCollection.CreateSqlQuery methods to get an ordered list of collection items

[PairTo] attribute allows to define a "virtual collection" - such collection "reflects" some other reference or collection property, thus "master" and "paired" properties form two-way relationship. This attribute it described further. It isn't necessary to use it, but it's usually used quite frequently.

 

Each collection property can be marked by [Contained] attribute, as well as reference property. In this case all objects contained in it are removed on removal of collection owner.

 

Let's modify Author class:

 

public abstract class Article: DataObject

{   

  ...

 

  public abstract Author Author {get; set;}

}

 

public abstract class Author: Person

{

  ...

 

  [Contained]

  [ItemType(typeof(Article))]

  [PairTo(typeof(Article), "Author")]

  public abstract DataObjectCollection Articles {get;}

}

 

Notes:

Articles property is marked by [Contained] attribute. So when e.g. Author a is removed, all instances which are contained in a.Artiles colelction are also removed

Atricles property is marked by [PairTo] attribute - it "reflects" Article.Author property

 

Let's see what can be done with Author.Aricles collection:

 

Article article1 = (Article)session.CreateObject(typeof(Article));

Article article2 = (Article)session.CreateObject(typeof(Article));

article1.Title = "The Spots on the Sun Phenomena";               

article2.Title = "Friends";

 

Author author = (Author)session.CreateObject(typeof(Author));

author.Name = "John Smith";

 

article1.Author = author; // First way to add an author to paired collection

author.Articles.Add(article2); // Second way to do the same

 

Console.WriteLine("\nArticle1 name: {0}", ((Article)article1).Title); Console.WriteLine("Author's name of Article1: {0}", article1.Author.Name);

 

Console.WriteLine("\nArticle2 name: {0}", ((Article)article2).Title); Console.WriteLine("Author's name of Article2: {0}", article2.Author.Name);

 

Console.WriteLine("\n{0}'s articles:", author.Name);

foreach (Article article in author.Articles)

  Console.WriteLine("{0}", article.Title);                

 

long article1ID = article1.ID;

long article2ID = article2.ID;

 

Console.WriteLine("\nRemoving {0}", author.Name);       

author.Remove();

 

article1 = (Article)session[article1ID];

if (article1==null)

  Console.WriteLine("Article1 is removed automatically");

 

article2 = (Article)session[article2ID];

if (article2==null)

  Console.WriteLine("Article2 is removed automatically");

 

Here two instances of Article and one instance of Author are created. Every Article has a reference to an author (Article.Author property) and Author has a collection of references to articles (Author.Articles property, which is automatically synchronized with Article.Author property). At last we remove the instance of Author and corresponding instances of Article are removed automatically.

 

It is possible to put references to any instances of type specified in [ItemType] attribute, including instances of descendants of this type. For example let's modify our Article class by adding a collection property which can contain references to any persistent instances (i.e. DataObject instances):

 

public abstract class Article: DataObject

{   

  public abstract string Title {get; set;}  

  public abstract string Annotation {get; set;}

  public abstract string Body {get; set;}

  public abstract int PublicationDate {get; set;}   

  public abstract Author Author {get; set;}

 

  [ItemType(typeof(DataObject))]

  public abstract DataObjectCollection SeeAlso { get; }

}

 

Now let's add some persistent instances to SeeAlso collection:

 

Article article = (Article)session.CreateObject(typeof(Article));

article.Title = "FFT Calculation Optimized For SSE2";

 

Person p = (Person)session.CreateObject(typeof(Person));

p.Name = "Ivan Petrov";

 

Author a = (Author)session.CreateObject(typeof(Author));

a.Name = "MyAuthor";

a.HomePage = new Reference("www.x-tensive.com", "X-tensive.com");

 

Article ar = (Article)session.CreateObject(typeof(Article));

ar.Title = "Rumors from the Point of Mathematician";

 

article.SeeAlso.Add(p);

article.SeeAlso.Add(a);

article.SeeAlso.Add(ar);

 

Console.WriteLine("\nSee also of {0}", article.Title);

foreach (DataObject obj in article.SeeAlso)

{

  if (obj is Article)

    Console.WriteLine("Article title: {0}", ((Article)obj).Title);

  else if (obj is Author)

  {

    Console.WriteLine("Author's name: {0}", ((Author)obj).Name);

    Console.WriteLine("Author's homepage URL: {0}",

      ((Author)obj).HomePage.Url);

    Console.WriteLine("Author's homepage URL: {0}",

      ((Author)obj).HomePage.Description);

  }

  else if (obj is Person)         

    Console.WriteLine("Person's name: {0}", ((Person)obj).Name);         

}

 

Let's see, which attributes can be applied to collection properties:

[ItemType] specifies the type of collection items

[Contained] attribute was already described earlier. It indicates that contained objects should be removed together with property owner.

[SelfReferenceAllowed] indicates that collection may contain references to its owner (by default this is not allowed)

[Translatable] has the same meaning as for primitive properties

[LoadOnDemand] attribute - can be used only to specify Threshold value (it's 0 by default). Valid only when changes tracking mode (see [ChangesTracking]) is ChangesTrackingMode.ThroughOwner. [LoadOnDemand] attribute will be fully described further.

 

There are several other and a bit more complex attributes, but we'll return to them further:

[PairTo], [Symmetric] - these attributes allows to define a pair of reference\collection properties, that should be automatically synchronized. Both of them are described in details further.

[ChangesTracking] indicates which strategy should be used to track the changes of property.

ValueTypeCollection

DataObjects.NET also supports persistent collections of value types - ValueTypeCollection properties or properties of custom descendants of this type.

 

Note: currently only struct types can be stored in such collections, but this doesn't decreases the genericity - any primitive value type, such as string or int can be "wrapped" into struct having just one field of this type.

 

The main advantage of ValueTypeCollection is that it allows to store a set of tiny objects more efficiently then DataObjectCollection. It's inefficient to use DataObject instances to store small amounts of information, since any DataObject contains significant amount of system-managed information (nevertheless this information is necessary - without it it's simply impossible to achieve other important goals).

 

ValueTypeCollections are very similar to DataObjectCollections, but holds struct values rather then references to DataObjects.

 

Let's imagine we should store a set of lists (may be rather big lists) of RGB values or vectors (X,Y,Z coordinates). It's inefficient to use a DataObject instance to store each element of such list. ValueTypeCollection is designed to handle such tasks with nearly the performance of regular database tables.

 

So DataObject instances may act as containers where smaller objects (structs) can reside (in ValueTypeCollection properties), and you may choose between these two approaches of storing different types of entities in your application.

 

Any ValueTypeCollection resides in the separate table (as well as DataObjectCollection). This table contains at least two columns: ObjectID and ItemID (both columns stores Int64 values, ItemID is autoincrement and primary key column), and two indexes (one on each column, the second index is unique). Other columns correspond to fields of struct designated by [ItemType] attribute that was applied on ValueTypeCollection property.

 

So as you see, it's possible to add multiple equal values to such collection - they'll anyway be distinguished by their ItemIDs.

 

But what do you loose with structs? You loose all inherence-related features (structs can't be inherited even in .NET, you won't be able to select different types of structs by a single query, query them by a supported interface, etc...) and some BLL features. For example, struct can't react on its modification itself, but DataObject instance that stores it - can. You can't apply security restrictions on a particular struct, but you can apply them on DataObject that stores it. You can't locate a particular struct by some ID unique in the storage scope, etc...

 

You gain performance and database space in exchange.

 

Now let's look at some interesting features of our implementation:

Custom ValueTypeCollection descendants must be abstract (proxies are generated for them also)

All struct-related notes are intact for ValueTypeCollection items

DataObjects.NET has built-in access control system. So we can't allow anything that may act as "backdoor" in it. And you still can't override security restrictions by e.g. executing a query fetching ValueTypeCollection items - new Query allows to execute such queries, but any access to elements of ValueTypeQueryResult is conducted through DataObject instances that hold selected items (so you'll anyway receive a set of DataObject.OnGetProperty-like calls - this allows to check all security restrictions).

References to DataObject instances are still correctly tracked - e.g. you'll anyway get OnReference\OnDereference events if you'll add, change or remove an item holding the reference

ValueTypeCollection items are ordered by their ItemID values

 

Example:

 

public abstract class Author: DataObject

{

  public abstract string Name {get; set;}

 

  ...

 

  [ItemType(typeof(Quote))]

  public abstract ValueTypeCollection Quotes { get; }

}

 

[Serializable]

public struct Quote

{

  [Length(250)]

  public string Body;

 

  public Quote(string body)

  {

    Body = body;

  }

}

 

...

 

Author Tyler = (Author)session.CreateObject(typeof(Author));

Tyler.Name   = "Tyler Durden";

Tyler.Quotes.Add(new Quote("You're not your job."));

Tyler.Quotes.Add(new Quote("You're not how much money you have in the bank."));

Tyler.Quotes.Add(new Quote("You're not the car you drive."));

Tyler.Quotes.Add(new Quote("You're not the contents of your wallet."));

Tyler.Quotes.Add(new Quote("You are not a beautiful or unique snowflake."));

 

Console.WriteLine("Quotes with length > 30:");

q  = new Query(session, "Select Author.Quotes values where len({Body}) > 30");

ValueTypeQueryResult vr = q.ExecuteValueTypeQueryResult();

Console.WriteLine("  Result: {0} values.", vr.Count);

foreach (ValueTypeQueryResultEntry entry in vr)

  Console.WriteLine("    {0}", ((Quote)entry.Value).Body);

 

A bit more complex example:

 

public abstract class Article: DataObject

{

  ...

 

  [ItemType(typeof(Comment))]

  public abstract ValueTypeCollection Comments { get; }

}

 

[Serializable]

public struct Comment

{

  public Author Author;

 

  public DateTime Date;

 

  [SqlType(SqlType.Text)]

  public string Body;

 

  public Comment(Author author, string body)

  {

    Author = author;

    Date = DateTime.Now;

    Body = body;

  }

}

 

In this example each comment implicitely defines many-to-many relationship between Article and Author types: each comment resides in Article.Comments collection, and has Author property.

 

Let's see, which attributes can be applied to collection properties:

[ItemType] specifies the type of collection items. You can set OwnerField property of this attribute in its definition, that is impossible for DataObjectCollection properties. You'll see examples of OwnerField usage further ("ValueTypeCollections describing mutual relationships (many-to-many, ternary, n-ary)" section)

[Translatable] has the same meaning as for primitive properties

[LoadOnDemand] attribute - can be used only to specify Threshold value (it's 0 by default). Valid only when changes tracking mode (see [ChangesTracking]) is ChangesTrackingMode.ThroughOwner. [LoadOnDemand] attribute will be fully described further.

 

There are two other and a bit more complex attributes, but we'll return to them further:

[PairTo] - these attributes allows to describe mutually related ValueTypeCollection properties (n-ary relationships). It's described in details further  ("ValueTypeCollections describing mutual relationships (many-to-many, ternary, n-ary)" section)

[ChangesTracking] indicates which strategy should be used to track the changes of property.

Delegate fields

Delegate properties\fields are fully supported. Single column is used to store the value of such property (thus it's stored in the serialized form).

 

Example:

 

public delegate void CallbackDelegate(DataObject sender);

 

public abstract class DelegateOwner: DataObject

{

  [LoadOnDemand(Threshold=5)]

  public abstract CallbackDelegate Callback {get; set;}

 

  public virtual void OnCallback()

  {

    if (Callback!=null)

      Callback(this);

  }

}

 

...

 

CallbackDelegate staticDelegate = new

  CallbackDelegate(SomeClass.SomeStaticMethodThatConformsToCallbackDelegateSpec);

someDelegateOwner.Change = staticDelegate;

 

CallbackDelegate regularDelegate = new

  CallbackDelegate(SomeClass.SomeMethodThatConformsToCallbackDelegateSpec);

someDelegateOwner.Change += regularDelegate;

someDelegateOwner.Change = someDelegateOwner.Change;

 

Last line "informs" DO that internal contents of delegate was updated; to do this automatically, you should provide a protected persistent delegate, and give an access to it via public event (having add\remove rather then get\set accessors).

 

Note: It was mentioned that delegate properties are internally handled nearly as byte[] properties - thus all attributes supported for byte[] type can be applied to them.

 

 


 

Relationships

One-way relationships

As you could notice, DataObjects.NET offers two persistent property types allowing to define one-way relationships:

Reference properties (DataObject or its descendant-type properties). It's obvious that reference properties allows to describe many-to-one, one-to-one (a particular case of many-toone, it's desirable to implement one-to-one condition check in this case) and many-to-many (via intermediate type) relationships.

DataObjectCollection (or its descendant-type) properties. Allows to define many-to-many relationships. Further you'll see that they can also be used in one-to-many relationships ([PairTo] attribute allows this).

 

It's not obvious, but ValueTypeCollection also allows to describe many-to-many relationships - it can contain structs with reference fields inside, thus its even more generic, then DataObjectCollection.

 

And moreover, ValueTypeCollection allows to define n-ary (e.g. ternary) relationships explicitely (without necessity to add intermediate type).

 

Let's see examples of each type of relationships...

One-to-many relationship example

public abstract class Person: DataObject

{

  // reference to an Address instance

  public abstract Address Address {get; set;} 

 

  ...

}

 

public abstract class Address: DataObject {

  public abstract string City {get; set;}

  ...

}

Many-to-many relationship example

Let's extend previously declared type:

 

public abstract class Person: DataObject

{

  // reference to an Address instance

  public abstract Address Address {get; set;} 

 

  // collection of Persons

  [ItemType(typeof(Person))]

  public abstract DataObjectCollection Friends {get;}

  ...

}

 

Note: collections must have only the get accessor!

Traversing relationships example

Just use reference properties as any other properties:

 

Person Michael;

Address InBrazil;

 

...

 

InBrazil.City = "Rio de Janeiro";

Michael.Address = InBrazil;

 

...

 

foreach (Person p in Michael.Friends) {

  if (p.Address.City=="Rio de Janeiro") {

    ...

  }

}

 

Note: it's not guaranteed that order of collection items is always the same (collections aren't ordered). But you can use DataObjectCollection.CreateQuery or DataObjectCollection.CreateSqlQuery methods to get an ordered list of collection items.

ValueTypeCollection describing many-to-many relationship example

public abstract class Article: DataObject

{

  ...

 

  [ItemType(typeof(Comment))]

  public abstract ValueTypeCollection Comments { get; }

}

 

[Serializable]

public struct Comment

{

  public Author Author;

 

  public DateTime Date;

 

  [SqlType(SqlType.Text)]

  public string Body;

 

  public Comment(Author author, string body)

  {

    Author = author;

    Date = DateTime.Now;

    Body = body;

  }

}

 

In this example each comment implicitely defines many-to-many relationship between Article and Author types: each comment resides in Article.Comments collection, and has Author property.

 

ValueTypeCollection describing n-ary (ternary) relationship example

public abstract class Article: DataObject

{

  ...

 

  [ItemType(typeof(Comment))]

  public abstract ValueTypeCollection Comments { get; }

}

 

[Serializable]

public struct Comment

{

  public Author Author;

 

  public Author Reviewer;

 

  public DateTime Date;

 

  [SqlType(SqlType.Text)]

  public string Body;

 

  public Comment(Author author, string body)

  {

    Author = author;

    Date = DateTime.Now;

    Body = body;

  }

}

 

In this example each comment implicitely defines ternary relationship between Article, Author (comment author) and Author (comment reviewer) types.

Two-way (mutual, or paired) relationships

Mutual relationships (associations) are very common in our world. For example, if a person has a friend, then he is a friend of his friend too (friend-friend). Or if A is a manager for B then B is an employee for A (manager-employee). Maintaining of such relationships is useful but always tiresome. DataObjects.NET introduces the [PairTo] attribute to maintain mutual associations transparently for a developer.

 

Let's look on different kinds of relationships.

One-to-one mutual relationships

It's nice when one-to-one relationship can be traversed from both of objects participating in it. One-to-one mutual relationship is a kind of relationship where reference property is paired to another reference property.

 

Let's see on "old way" example first. Let's declare a type named Passport with SerialNumber and Person properties. Also let's add Passport property to Person type.

 

public abstract class Passport: DataObject

{

  public abstract string SerialNumber {get; set;}

  public abstract Person Person {get; set;}

}

 

public abstract class Person: DataObject

{

  ...

 

  public abstract Passport Passport {get; set;}       

}

 

Now we can create Person and Passport instances and try to establish a relationship between them:

 

Person   person   = (Person)session.CreateObject(typeof(Person));

Passport passport = (Passport)session.CreateObject(typeof(Passport));

 

person.Name = "Alex";

passport.SerialNumber = "123-456";

passport.Person = person;

 

Console.WriteLine("Passport serial number: {0}", passport.SerialNumber);

Console.WriteLine("Passport owner's name: {0}", passport.Person.Name);

 

// Let's verify whether person.Passport is null or not

if (person.Passport == null) {

  Console.Write("{0} has no passport. ", person.Name);

  person.Passport = passport;

  Console.WriteLine("{0} has already a passport with the serial number {1}",

    person.Name, passport.SerialNumber);

}

else

  Console.WriteLine("{0} has a passport with the serial number {1}",

    person.Name, passport.SerialNumber);

 

This code produces the following output: "Alex has no passport. Alex has already a passport with the serial number 123-456". So as we've seen, to "assign" passport and person to each other it is necessary to do these operations manually:

 

passport.Person = passport;

person.Passport = person;

 

But there is more simple and effective way toachieve the same: you can apply [PairTo] attribute to one of related properties - either to Person.Passport or to Passport.Person. Let's apply it to Person.Passport property:

 

public abstract class Person: DataObject

{

  ...

 

  [PairTo(typeof(Passport),"Person")]           

  public abstract Passport Passport {get; set;}       

}

 

The previous code produces the following output: "Alex has a passport with the serial number 123-456". So as we've seen it is enough to "assign" a person to passport and get passport "assigned" to person, and vice versa, so upper two lines of code procude the same result:

 

passport.Person = passport; // the same as person.Passport = person;

 

Notes:

Use the [PairTo] attribute on only a one side of an association!

DataObjects.NET automatically ensures "one-to-one" rule for this type of pairing, thus if you bind two objects, and one (or both of them) had a pair with some other object, this (these) pair becomes destroyed.

One-to-many mutual relationships

One-to-many mutual relationship is a kind of relationship where a collection property is paired to a reference property.

 

We have Author type with Articles property which describes a collection of articles written by this author. Also we have Article type with Author property describing an author who wrote that article. So each instance of Author corresponds to zero or more instances of Article - it's a one-to-many relationship. Let's see how to make it mutual:

 

public abstract class Article: DataObject

{   

  ...

 

  public abstract Author Author {get; set;}

}

 

public abstract class Author: Person

{

  ...

 

  [ItemType(typeof(Article))]

  [PairTo(typeof(Article), "Author")]

  public abstract DataObjectCollection Articles {get;}

}

Many-to-many mutual relationships

Many-to-many mutual relationship is a kind of relationship where a collection property is paired to a collection property.

 

Example:

 

public abstract class Principal: FtObject

{

  [ItemType(typeof(Role))]

  public abstract RoleCollection Roles {get;}

  // RoleCollection is "typed" DataObjectCollection descendant, see below

 

  ...

}

 

public abstract class Role: Principal

{

  [ItemType(typeof(Principal))]

  [PairTo(typeof(Principal),"Roles")] // !!!

  public abstract PrincipalCollection Principals {get;}

  // PrincipalCollection is "typed" DataObjectCollection descendant, see below

 

  ...

}

 

// This is an example of typed collection

public abstract class PrincipalCollection: DataObjectCollection

{

  public int Add(Principal item)

  {

    return List.Add(item);

  }

 

  public new Principal this[int n] {

    get {

      return (Principal)List[n];

    }

    set {

      List[n] = value;

    }

  }

 

  public PrincipalCollection(System.Type itemType): base(itemType)

  {

    if (!(itemType==typeof(Principal) ||

          itemType.IsSubclassOf(typeof(Principal))))

      throw new ObjectModelBuilderException(

        String.Format("Illegal ItemType value: \"{0}\".",itemType)

        );

  }

}

 

public abstract class RoleCollection: DataObjectCollection

{

  public int Add(Role item)

  {

    return List.Add(item);

  }

 

  public new Role this[int n] {

    get {

      return (Role)List[n];

    }

    set {

      List[n] = value;

    }

  }

 

  public RoleCollection(System.Type itemType): base(itemType)

  {

    if (!(itemType==typeof(Role) || itemType.IsSubclassOf(typeof(Role))))

      throw new ObjectModelBuilderException(

        String.Format("Illegal ItemType value: \"{0}\".",itemType)

        );

  }

}

 

Symmetric relationships

Symmetric relationship is a specific type of many-to-many mutual relationship where collection property is paired to itself.

 

Symmetric collection always satisfies the following condition: if instance A contains instance B in its symmetric collection Coll (so A.Coll.Contains(B)==true) then B contains instance A in corresponding collection (so B.Coll.Contains(A)==true too).

 

It may look strange when a property is paired to itself. [Symmetric] attribute was introduced as a synonym for this case.

 

Let's see an example of symmetric collection ("if Bob is a friend of my, then I'm a friend of Bob"):

 

public abstract class Person: DataObject

{

  public abstract string Name {get; set;} 

 

  [ItemType(typeof(Person))]

  [Symmetric] // == [PairTo(typeof(Person),"Friends")]

  public abstract DataObjectCollection Friends {get;}

}

 

...

 

Person me  = (Person)session.CreateObject(typeof(Person));

me.Name    = "Alex";

Person bob = (Person)session.CreateObject(typeof(Person));

bob.Name   = "Bob";

me.Friend.Add(bob);

if (me.Friends.Contains(bob))

      Console.WriteLine("Bob is a friend of my.");

if (bob.Friends.Contains(me))

      Console.WriteLine("I'm a friend of Bob.");

 

ValueTypeCollections describing mutual relationships (many-to-many, ternary, n-ary)

N-ary mutual relationships can be described with use of ValueTypeCollections. Let's start directly from example (it's based on Articles sample):

 

public abstract class Article: DataObject

{

  ...

 

  [ItemType(typeof(Comment), OwnerField = "Article")]

  public abstract ValueTypeCollection Comments { get; }

}

 

public abstract class Author: DataObject

{

  ...

 

  // New code:

  [ItemType(typeof(Comment), OwnerField = "Author")]

  [PairTo(typeof(Article), "Comments")]

  public abstract ValueTypeCollection Comments { get; }

}

 

...

 

[Serializable]

public struct Comment

{

  public Article Article;

 

  public Author Author;

 

  public DateTime Date;

 

  [SqlType(SqlType.Text)]

  public string Body;

 

  public Comment(Author author, string body)

  {

    Article = null;

    Author = author;

    Date = DateTime.Now;

    Body = body;

  }

 

  public Comment(Article article, string body)

  {

    Article = article;

    Author = null;

    Date = DateTime.Now;

    Body = body;

  }

}

 

It's a many-to-many mutual relationship. Brief description:

Article type still contains Comments (ValueTypeCollection), but adds OwnerField specification to [ItemType] attribute. OwnerField denotes that no additional column should be allocated in the collection table to store an owner reference for each item, but specified field of struct (OwnerField) should be used for this purpose.

A similar collection definition is added to Author type, but with two differences:
a) it specifies that it's a pair to
Article.Comments property
b) its
OwnerField specifies another field - Author

Comment (struct) is also modified - it has two reference fields. Date field is added only for further sorting purposes.

 

Now let's look how these paired collections can be used:

 

Author Tyler = (Author)s.CreateObject(typeof(Author));

Tyler.Name   = "Tyler Durden";

Tyler.Homepage = new Reference(

  "http://www.fightclub.com/",

  "Fight Club");

 

Author Marla = (Author)s.CreateObject(typeof(Author));

Marla.Name   = "Marla Singer";

 

Author Anonymous = (Author)s.CreateObject(typeof(Author));

Anonymous.Name = "Anonymous";

 

// Articles

 

Article a1 = (Article)s.CreateObject(typeof(Article));

a1.Title = "Car accidents";

a1.Date = new DateTime(1998, 3, 5);

a1.Author = Anonymous;

a1.References.Add(new Reference("http://www.crash.com/"));

a1.References.Add(new Reference("ftp://www.crash.com/"));

a1.Comments.Add(new Comment(Tyler, "No comments..."));

// An alternative way to add the same comment:

// Tyler.Comments.Add(new Comment(a1,"No comments..."));

 

// Let's check if last modification is reflected

// by all paired collections:

Debug.Assert(

  ((Comment)Tyler.Comments[0].Value).Body==

  ((Comment)a1.Comments[0].Value).Body);

 

Article a2 = (Article)s.CreateObject(typeof(Article));

a2.Title = "Corporations";

a2.Date = new DateTime(1998, 3, 1);

a2.Author = Anonymous;

a2.Comments.Add(new Comment(Tyler,"You'd look deeper."));

// An alternative way to add the same comment:

// Tyler.Comments.Add(new Comment(a2,"You'd look deeper."));

 

You can query this relationship - just use any allowed path in query expression:

 

Console.WriteLine("> Comments by Tyler Durden ordered by Article date:");

text = "Select Article.Comments values where 'Tyler Durden' = any{Author.Name} order by {parent.Date}";

Console.WriteLine("  Query: \"{0}\"", text);

 

q = new Query(s, text);

vr = q.ExecuteValueTypeQueryResult();

Console.WriteLine("  Result: {0} values.", vr.Count);

foreach (ValueTypeQueryResultEntry entry in vr)

  Console.WriteLine("    {0}", ((Comment)entry.Value).Body);

Console.WriteLine();

 

 

Console.WriteLine("> Comments by Tyler Durden ordered by Comment date:");

text = "Select Author.Comments values where {parent.Name} = 'Tyler Durden'

        order by {Date}";

Console.WriteLine("  Query: \"{0}\"", text);

 

q = new Query(s, text);

vr = q.ExecuteValueTypeQueryResult();

Console.WriteLine("  Result: {0} values.", vr.Count);

foreach (ValueTypeQueryResultEntry entry in vr)

  Console.WriteLine("    {0}", ((Comment)entry.Value).Body);

Console.WriteLine();

 

As you may see, different paths to the same relationship (Article.Comments, Author.Comments) are used in this query.

 

And finally let's show an example of ternary relationship:

 

[Serializable]

public struct OrderPosition

{

  public int       Index; // In Order's collection

  public Order     Order;

  public Product   Product;

  public Vendor    Vendor;

  public double    Quantity;

 

  public OrderPosition(int index, Product product, Vendor vendor, double quantity)

  {

    Index    = index;

    Order    = null;

    Product  = product;

    Vendor   = vendor;

    Quantity = quantity;

  }

}

 

public abstract class Order: DataObject

{

  ...

 

  [ItemType(typeof(OrderPosition), OwnerField = "Order")]

  public abstract ValueTypeCollection Positions {get;}

}

 

public abstract class Product: DataObject

{

  ...

 

  [ItemType(typeof(OrderPosition), OwnerField = "Product")]

  [PairTo(typeof(Order), "Positions")]

  public abstract ValueTypeCollection OrderPositions {get;}

}

 

public abstract class Vendor: DataObject

{

  ...

 

  [ItemType(typeof(OrderPosition), OwnerField = "Vendor")]

  [PairTo(typeof(Order), "Positions")]

  public abstract ValueTypeCollection OrderPositions {get;}

}

 

Note: as you know, DataObject class declares OnReference \ OnDereference events, that can be used eg. in reference counting schemas. These events are raised correctly for new ValueTypeCollections. For example, (n-1)*n events are raised if we'll add one new item to a ValueTypeCollection having n OwnerFields (or n-1 pairs). And if this relationship includes other reference properites, each of referenced objects will also be notified n times.

 


 

Queries

Introduction

DataObjects.NET creates an evident database structure for storing objects. By default (if [ShareXxxTable] attributes and in short future partitioning attributes are unused) a separate table corresponds to each persistent class, a column - to each property (a set of columns is created for [Translatable] and [Collatable] properties). An additional table is created for each collection also (a set of tables - for a [Translatable] collection). Such a schema allows writing of virtually any SQL queries for objects selection and reports generation.

 

You should remember that fields of a single DataObject instance can be distributed through several tables because of inheritance support. Only properties defined directly in a particular class (i.e. properties that were declared in this class) are stored in the table related to this class; properties inherited from parent classes are stored in their own tables. For example, since every persistent class is finally inherited from the DataObject class, some data is stored for all objects in the database in its table (such data as objects' IDs, TypeIDs, VersionIDs and security permissions).

 

To simplify querying the database, DataObjects.NET maintains a view for each persistent class, where all properties of this class are mapped (including properties of its ancestors). Normally these views are used in queries generated by Query \ SqlQuery classes, rather then tables.

 

There are some drawbacks of using SqlQuery class for object queries exist. The first consists in that names of tables, columns, and views are different from classes' names. There are several factors affecting naming of tables, views, and columns in the database.

 

Domain.IdentifierPrefix. This prefix serves for distinction of DataObjects.NET tables and view from others in the same database. Besides, several domains with different prefixes can exist in one database.

Domain.NamingMode. Provides several modes for including namespace name or its hash into table names.

[Translatable] and [Collatable] properties. A language suffix is added for their columns.

 

The second drawback lies in the difficulty to write SQL queries for referenced objects and properties (such as Category.Product.Item.Name) while it is a common task. Joins or complex "where" clause can be used for that, but such queries tend to grow fast.

 

The primary goal of the OQL-like query engine is to solve these drawbacks. DataObjects.NET provides Query class that uses this engine. The following tutorial introduces its syntax elements and its usage.

General query syntax

Select [count] [distinct] [top N] (TypeName | FieldName)

       [(instances | objects | values)]

  [with [options] (OptionsExpression)]

  [joins]

  [where WhereExpression]

  [textsearch SearchExpression]

  [order by OrderByExpression]

Field reference substitution

Query engine translates class names into table names and embraced property names into column names or subqueries (e.g. for collection properties).

Query:  Select Category instances
where {ID}=15

 

N-level referenced properties can be put in braces:

Query:  Select Product instances
where {Category.Name} like 'Dog%'

Note: Such construction is translated to SQL using a subquery.

 

The item keyword is used to reference collection items:

Query:  Select Product instances
where 'Adult Male' = any {Items.item.Name}

 

Note: The language suffix can be used to distinguish translatable properties, e.g. {Name-En} or {Name-Ru}.

Substitutions in the 'order by' clause

Properties can be referenced from the "order by" clause as well.

Query:  Select Category instances
order by {Name}

Query:  Select Category instances
order by {Products.Count} desc

Additional conditions in square brackets

XPath-like conditions in square brackets can be additionally applied to collection references. The following example queries categories containing poodles among products.

Query:  Select Category instances
where exists {Products[{Name}='Poodle']}

Root, this, parent

Another XPath-like feature is the ability to reference parent objects in "nested" queries.

 

This query selects all categories which have at least one product referencing to another category. Thus {root} refers to a Category object which is selected, and {Category} is a reference property of a product in the Products collection.

Query:  Select Category instances
where exists {Products[{Category} <> {root}]}

Note: ID property can be omitted: you can write {Category} <> {root} instead of {Category.ID} <> {root.ID}.

 

Another way to make the same query:

Query:  Select Category instances
where exists {Products[{Category} <> {this.parent}] }

Count, item, expression

Count is the number of collection items. The following query selects all categories having more than three products in it.

Query:  Select Category instances
where {Products.Count} > 3

 

Item is used to reference collection items.

Query:  Select Product instances
where 'Adult Male' = any {Items.item.Name}

 

Expression or shortly Expr allows to impose aggregate condition for collection elements. The following query selects all categories having the average length of product names in it greater than 7. Avg and len (length in Oracle) are standard SQL functions; expr only tells that an aggregate expression (in brackets) must be calculated for collection items.

Query:  Select Category instances
where {Products.Expr[avg(len({Name}))]} > 7

Query options

It is possible to put options for the query after with (options...) keyword. The full list of options can be found under the "QueryOptions Enumeration" section of DataObjects.NET Help.

Query:  Select top 5 Item instances with (fastFirst, forUpdate)
where {Name} <> 'Rattleless'

 

Note: Some query options can be omitted\rejected by active DataObjects.NET database driver.

Parameters

Let's start from this example:

Query:  Select Category instances where {Products.Count} > @Min

 

@Min is the query parameter. Parameters notation is the same for all queries (i.e. for Query and SqlQuery; it doesn't depend from the database driver). Parameters are stored in the Parameters collection of a query object. For example:

Query q = new Query(session,
  "Select Category instances where {Products.Count} > @Min");
q.Parameters.Add("@Min", 3);

Type casts

Example:

Query:  Select Cat instances
where exists{(Dog)Friends.(Mouse)Friends}

 

Description: selects all Cats having such Dogs in their Friends collection which have at least 1 Mouse in their own Friends collection (just a way to find a mouse for a cat :) )

Distinct, joins, aliases

Author.Articles is DataObjectCollection property; "require" is the same as "inner join", "let" is the same as "left join":

 

Query:  Select distinct Author instances
require $root.Articles as $a order by {$a.Date}

Querying for collection\reference property content

Querying for DataObjectCollection property contents:

Query:  Select (YellowBook)Author.Books instances
// Books is DataObjectCollection

 

Querying for reference property contents:

Query:  Select Book.Author instances
// Author is a reference field

 

Querying for reference property contents (nested to struct):

Query:  Select Building.Address.City instances
// Address is a struct field

 

Querying for reference property contents (nested to ValueTypeCollection item):

Query: Select Building.Addresses.Item.City instances
// Addresses is a ValueTypeCollection field

 

Querying for ValueTypeCollection property contents:

Query:  Select Author.Quotes values where len{Body} > 100
// Quotes is ValueTypeCollectionfield

Subqueries

Use {Select ...} syntax to specify a subquery:

Query:  Select top 100 IFtObject instances as $fto with (SkipLocked)
where not exists {Select FtRecord objects where {FtObject}={$fto} }

 

Notes:

"}}"  and "{{" (double figure brackets) are subject to substitution everywhere except in string constants - they're always translated to single ones (such as \" in C# strings). Use empty spaces to set them up correctly (e.g. space is used in the tail of example query: "} }")

Use "as $alias"  to alias the subquery

We recommend you to always profile queries containing subqueries (e.g. see the execution that SQL Server generates for them)  - they can be very inefficient (e.g. lead to table scans, and consequently - to table-level locks) - in such cases it's better to replace them with distinct\join queries, if possible.

Full-text queries

Query language supports full-text search syntax elements, so full-text queries can be written in very similar fashion.

Query:  Select Author instances where {Name}>='D'
textsearch top 5 freetext 'Jungle'
order by {FullTextRank} desc

Querying the database directly

As it was said earlier, query engine translates queries to SQL commands. It is possible to get a real SQL command running against a database. It can be useful for complex queries as for reports generation.

 

string sqlText = query.CreateRealCommand().CommandText;

 

Note: This operation is allowed only if AllowAccessRealObjects option is included to SecurityOptions of the current Session.

 

Let's see an example of direct command execution (aggregate query example):

 

[ServiceType(DataServiceType.Shared)]

public abstract class AggregateInfo : DataService

{

  [Transactional(TransactionMode.TransactionRequired)]

  public virtual double MinimalAnimalAge()

  {

    Demand(...);

    DisableSecurity(); // Otherwise Session.CreateRealCommand()

                       // call will fail.

    try {

      IDbCommand cmd = Session.CreateRealCommand();

      cmd.CommandText =

        "Select min([Age]) from " +

        Session.Types[typeof(Animal)].RelatedView.Name;

      return cmd.ExecuteScalar();

    }

    finally {

      EnableSecurity();

    }

  }

}


Appendix: Comprehensive query format description

 

QUERY = 'Select' ['distinct'] [top Integer] (INSTANCES | VALUES) ['as' Alias]

        ['with' ['options'] '(' QUERYOPTIONS ')']

        {JOINEXPRESSION}

        ['where' EXPRESSION]

        ['textsearch' [top N]

          ['freetext' | 'condition' | 'likeexpression']

          TEXTSEARCHEXPRESSION

          ['within' ['any' | CultureName] 'culture'] ]

        ['order by' '(' EXPRESSION ')' [('asc' | 'desc')]

                 {, '(' EXPRESSION ')' [('asc' | 'desc')]}]

 

INSTANCES = [CASTEXPRESSION] (TYPEREFERENCE | FIELDREFERENCE) ['instances' | 'objects']

VALUES    = FIELDREFERENCE ['values']

TYPEREFERENCE  = {NamespaceName '.'}TypeName // You can use quoted identifiers here

FIELDREFERENCE = {NamespaceName '.'}TypeName '.' FieldName['-' CultureName]{'.' FieldName}

QUERYOPTIONS   = [OptionName {',' OptionName}]

 

JOINEXPRESSION = (TYPEJOINEXPRESSION | FIELDJOINEXPRESSION)

TYPEJOINEXPRESSION  = (('inner join' | 'require') | ('left' ['outer'] 'join' | 'let'))

                      [CASTEXPRESSION] TYPEREFERENCE [as] Alias on '(' EXPRESSION ')'

FIELDJOINEXPRESSION = (('inner join' | 'require') | ('left' ['outer'] 'join' | 'let'))

                      [Alias '.'] JOINPATH [[as] Alias] on '(' EXPRESSION ')'

FIELDJOINPATH = [CASTEXPRESSION] (

                  (REFERENCEFIELD  ['[' EXPRESSION ']'] {'.' FIELDJOINPATH } ) |

                  (COLLECTIONFIELD ['[' EXPRESSION ']'] {'.' FIELDJOINPATH } )

                )

CASTEXPRESSION = '(' TYPEREFERENCE ')'

EXPRESSION = {(AnySqlCode | REFERENCE | SUBQUERY)}  // Any SQL code is any character

                                                    // content that isn't classified

                                                    // as REFERENCE or SUBQUERY

 

SUBQUERY  = '{' QUERY '}' // Remember, double open\closing figure brackets

                          // is an escape sequence that is substituted to

                          // single figure bracket of the same type

 

REFERENCE = '{' [('this.' | 'root.' | 'parent.' | Alias '.')] PATH '}'

PATH = (FIELD) |

       ([CASTEXPRESSION] REFERENCEFIELD  ['[' EXPRESSION ']'] {'.' PATH } ) |

       ([CASTEXPRESSION] COLLECTIONFIELD ['[' EXPRESSION ']'] (

         ('.item') {'.' PATH } |

         '.count' |

         '.' ('expression' | 'expr') '[' EXPRESSION ']'

       )))

FIELD            = ROOTFIELD | CONTAINEDFIELD       // You can use quoted identifiers here,

ROOTFIELD        = FieldName['-' CultureName]       // e.g. Name, Name-En

CONTAINEDFIELD   = ROOTFIELD {'.' FieldName}        // "Name", "Name"-"En"

                                                    // are allowed.

REFERENCEFIELD   = FIELD     // But of DataObject type

COLLECTIONFIELD  = ROOTFIELD // But of DataObjectCollection or

                             // ValueTypeCollection type

 

TextSearchExpression = QuotedString     // e.g. 'cats', '"Elvis"', '''Elvis''', 'computer*'

NamespaceName    = QuotedIdentifier     // e.g. MyModel, "MyModel"

TypeName         = QuotedIdentifier     // e.g. Animal, "Animal"

FieldName        = QuotedIdentifier     // e.g. Age, "Age"

CultureName      = QuotedIdentifier     // e.g. En, "De"

OptionName       = Identifier           // e.g. LoadOnDemand

Alias            = '$' QuotedIdentifier // e.g. $Parent, $"Parent"

 

AnySqlCode       = Regex: ([^\]|[\{]|[\}]|[\\])* // Approximately, e.g. query syntax constructs

                                                 // aren't classified as AnySqlCode

Identifier       = Regex: [_A-Za-z0-9]+

QuotedIdentifier = Regex: [_A-Za-z0-9]+  // Regular Identifier

                   Regex: "([^"]|[""])+" // "" means "

QuotedString     = Regex: '([^']|[''])+' // '' means '

Integer          = Regex: [-]?[0-9]+

 


 

Transactions

DataObjects.NET is completely transactional. This means that you can't do anything without using transactions, but in most of cases DataObjects.NET begins and commits them automatically and transparently for you.

 

Nested transactions, savepoints, and all isolation levels higher then Read Uncommitted are supported (we decided to not support Read Uncommitted - it's risky enough to operate on this level for any complex application; this isn't solely ours opinion - e.g. Oracle doesn't support this isolation level at all).

Manual transactions

DataObjects.NET supports automatic transactions, as well as manual ones. First let's take a look at manual transaction sample code:

 

using (Session session = new Session(domain)) {
  session.BeginTransaction();


  Animal a = (Animal)session.CreateObject(typeof(Animal));
  a.Age = 2;
  Console.WriteLine(a.Age); // Output: 2
  Savepoint sp = new Savepoint(session);
  a.Age = 3;
  Console.WriteLine(a.Age); // Output: 3
  sp.Rollback();
  Console.WriteLine(a.Age); // Output: 2


  session.Commit();
}

 

One more sample:

 

using (Session s = domain.CreateSession(LoginInfo.None)) {

  s.BeginTransaction();

 

  Query q = new Query(s,"Select Account instances with (FastFirst) " +

                        "where {Title}=@Title");

  QueryParameter p = new QueryParameter("@Title",DbType.String);

  q.Parameters.Add(p);

  p.Value  = "Account_"+lFrom;

  Account aFrom = (Account)(q.ExecuteArray()[0]);

  p.Value  = "Account_"+lTo;

  Account aTo   = (Account)(q.ExecuteArray()[0]);

  aFrom.TransferTo(aTo, amount);

 

  Transaction t = s.BeginTransaction();

    aFrom.TransferTo(aTo, amount*2);

  t.Rollback();

 

  Savepoint sp = new Savepoint(s);

    aFrom.TransferTo(aTo, amount*2);

  sp.Rollback();

 

  s.Commit();

}

 

Any BeginTransaction-like call starts the outermost or nested transaction. Nested transactions are executed on the same connection, and internally implemented using savepoints or real nested transactions - this depends on features of currently used database server (or database driver).

Automatic transactions

Let's take a look at automatic transactions. DataObjects.NET allows you to make your data model accessible via .NET Remoting, so generally anyone can be connected to it. Of course, you may use some authentication\authorization schema, but even when this is done, the problem with secure access to the application server isn't solved completely.

 

Let's consider the following situation: some malicious user connected to your Domain remotely, began a transaction, executed Transfer method of Account class; this method reached the point where some amount is added to AccountA, but not yet withdrawn from AccountB, and at this point some exception occurs (e.g. DeadlockException - it can occur on generally any SQL query, so a malicious user can use the code making this situation highly probable). Let's assume this happened, and this user catches the exception (because it finally reaches his own code) and commits the transaction! The result is: some additional amount is added to AccountA, but not withdrawn from AccountB - in violation of the application's business rules.

 

In short, malicious user made application server to commit inconsistent data produced as result of partially finished server-side operation, because he is able to control the transaction boundaries, but not the application server.

 

Or we may say this may happen because code of Transfer method is defective - it gives an opportunity to break the transfer in the middle of the operation. It would be great if this method should:

Be successfully finished;

Throw an exception; in this case no data should be changed.

 

As you see, we have a problem with the second option. To fix it, we can, for example, place a Savepoint at the beginning of the method body, and put a try-catch block that performs rollback to the Savepoint on any exception. Or even better: this method should begin a transaction (outermost or nested) on its start, and commit\rollback it on its completion, depending on if an exception is thrown or not. This would allow us to call this method even without beginning a transaction - it starts it automatically in any case.

 

This is the complete solution, but it is actually not quite good because:

It seems almost any method of data model will contain the code that potentially has the same problem. So it would become a rather routine task for developer - to put the same code everywhere.

The code actually should be more sophisticated (for performance reasons) For example, let's consider that we have a SetOfTransfers method, that performs a set of transfers by calling the Transfer method several times. In this case the Transfer method should detect this situation and omit beginning of a nested transaction (because SetOfTransfers itself begins it and catches inner exceptions).

 

A good solution would be:

All methods of the data model should behave as described above, but automatically: our data model contains only abstract DataObject descendants (DataObjects), for each of them one more descendant (proxy class) is built during runtime (by the DataObjects.NET proxy builder) providing automatic persistence, so we can make proxy builder to additionally override every virtual method or property with a new method that behaves as described above.

Proxy builder should look at a special attribute (TransactionalAttribute, e.g.  [Transactional(TransactionMode.TransactionRequired)] to implement this behavior.

 

This solution is called automatic transactions, and it is fully implemented in DataObjects.NET. Let's look at examples:

 

public abstract class Account: DataObject

{

  [Indexed(Unique=true)]

  public abstract string Title {get; set;}

 

  [Indexed]

  public abstract double Balance {get; set;}

 

  [Transactional(TransactionMode.TransactionRequired)]

  public virtual void TransferTo(Account accountTo, double amount)

  {

    if (accountTo==this)

      throw new InvalidOperationException("Accounts are the same.");

    if (amount<0.1)

      throw new InvalidOperationException("Illegal amount.");

    if (Balance<amount)

      throw new InvalidOperationException("Illegal amount.");

   

    Balance -= amount;

    accountTo.Balance += amount;

}

...

}

 

Now the code is much safer, also we can call the Transfer method even without a transaction:

 

using (Session s = domain.CreateSession(LoginInfo.None)) {

  Query q = new Query(s);

  q.Text = "Select Account instances where {Title}='John Smith';

  Account aFrom = (Account)(q1.ExecuteArray()[0]);

  q.Text = "Select Account instances where {Title}='Ann Smith';

  Account aTo   = (Account)(q1.ExecuteArray()[0]);

  aFrom.TransferTo(aTo, amount);

}

 

Note: you can call any method or change property of the Account class without starting a transaction - all necessary methods and properties of DataObject class (and set of other related classes like Session, Query and DataObjectCollection) are already marked as methods requiring automatic transactions. Currently you can find more information on this subject in the DataObjects.NET FAQ.

Transparent deadlock handling

As you may have noticed, DataObjects.NET generates proxy class for each DataObject descendant. This class primarily handles persistence and automatic transactions, so we can give one more task to such classes - to re-process method calls if a DeadlockException occurs.

 

DataObjects transparently handles deadlock occurrences in the following manner:

It handles deadlocks by catching a TransactionCanBeReprocessedException in any overridden methods \ properties code of the proxy classes

The method will then be re-processed automatically on deadlock, if:

It requires a new or existing transaction

A TransactionCanBeReprocessedException was thrown during its execution

It is the method for which the outermost automatic transaction was started

 

Note: the last condition is required because most database servers don't release any locks acquired by a nested transaction on its rollback. All locks are released only on commit or rollback of the outermost transaction.

Distributed transactions

You can enlist any Session instance in the MS DTC\COM+ distributed transaction.

 

Let's look at the example:

 

[Transaction(TransactionOption.Required)]

public class SomeServer: ServicedComponent

{

  public void DoSomething(Domain domain)

  {

    try {

      Session s = domain.CreateSession(new LoginInfo("",""))) {

      s.EnlistDistributedTransaction(
        (ITransaction)ContextUtil.Transaction);

 

      Author a   = (Author)s.CreateObject(typeof(Author));

      af.Name    = "Leo";

      af.Surname = "Tolstoy";

 

      ContextUtil.SetComplete();

    }

    catch (Exception) {

      ContextUtil.SetAbort();

      throw;

    }

  }

}

 

Note:

Currently distributed transactions are supported by the Microsoft SQL Server and Oracle drivers

You can't commit or rollback Session.OutermostTransaction if the Session is enlisted in the distributed transaction. You should use the ContextUtil.SetComplete() or ContextUtil.SetAbort() instead (in the case of the ServicedComponent usage)

Session.OutermostTransaction.RealTransaction is null if the Session is enlisted in the distributed transaction

Nested transactions and savepoints are related to the Session only, not to the whole distributed transaction. So if you perform a rollback to the Savepoint, this affects only on the instances from corresponding Session, but the other resources of the distributed transaction (e.g. other Session instances enlisted in the same distributed transaction) will not be affected by this operation.


 

Data services

Data services allows to use all DataObjects.NET transactional capabilities (automatic transactions, transparent deadlock handling) with non-persistent classes. This feature greatly simplifies the development of services operating with persistent instances. It's very convenient to use this conception to develop common services of the application, e.g. Search Service or Logging Service.

 

Data services are very close to ServicedComponents of the .NET, the difference is that they are much faster (method calls are intercepted by the runtime proxies rather then .NET TransparentProxy, no COM+ services are used) and easier to use.

 

Data services are descendants of the DataService class. These classes (like DataObjects) should be registered in the Domain by RegisterServices\RegisterService invocation.

 

The main difference of data services from persistent objects (DataObject descendants) is that DataService instances can't store any persistent data themselves. Session.GetService(...) or Session.CreateService(...) should be called to get\create a DataService instance - each DataService instance is bound to some Session (like DataObjects).

 

For each registered DataService class a proxy class is generated by DataObjects.NET. These proxy classes handle automatic transactions during DataService method invocation (again like with DataObjects).

 

Example (code from DoPetShop sample):

 

[ServiceType(DataServiceType.Shared)]

public abstract class SearchService: DataService

{

  [Transactional(TransactionMode.TransactionRequired | TransactionMode.Unsafe)]

  public virtual QueryResult SearchProductsByKeyword(string[] keywords)

  {

    if (!Domain.ExtractedDatabaseModel.ExtractedInfo.FullTextIndexingRunning)

      // Full-text search isn't available, let's go by another (old) way

      return SearchProductsByKeyword_Simple(keywords);

     

    try {

      string freeTextString = "";

      foreach (string kw in keywords)

        freeTextString += " "+kw;

 

      Query q = Session.CreateQuery(

        "Select Product instances with (LoadOnDemand) "+

        "textsearch freetext "+Session.Utils.QuoteString(freeTextString)+" "+

        "order by {FullTextRank} desc");

 

      return q.Execute();

    }

    catch {

      // Full-text search isn't available, let's go by another (old) way

      return SearchProductsByKeyword_Simple(keywords);

    }

  }

 

  ...

}

 

...

 

SearchService searchSrv =
  (SearchService)session.GetService(typeof(SearchService));

QueryResult products = searchSrv.SearchProductsByKeyword("Black Cat".Split());

Runtime services

Runtime services are data services of special type (RuntimeService descendants) that can be periodically executed in the separate Thread and Session maintained by the Domain. The purpose of runtime services is to perform any maintenance tasks periodically. An example of such service is FtIndexer that periodically updates full-text index data.

 

Example (code from DoPetShop sample):

 

[ServiceType(DataServiceType.Shared)]

public abstract class CleanupService: RuntimeService

{

  private Random random = new Random();

  private int    maxObjectsPerIteration = 100;

 

  // Cleans up dead Carts

  public override void Execute()

  {

    // We need maximal permissions during this operation

    DisableSecurity();

    try {

      string queryText;

      if (Session.DriverInfo.Type=="Oracle" ||
          Session.DriverInfo.Type=="NativeOracle")

        queryText =

          "Select top "+maxObjectsPerIteration.ToString()+" Order objects " +

          "With (SkipLocked) " +

          "Where {OrderStatus.Status}=0 and " +

                "(@Now-{LastChangedTime}) > 1";

      else        

        queryText =

          "Select top "+maxObjectsPerIteration.ToString()+" Order objects " +

          "With (SkipLocked) " +

          "Where {OrderStatus.Status}=0 and " +

                "datediff(day, {LastChangedTime}, @Now) > 1 ";

      Query q = Session.CreateQuery(queryText);

      q.Parameters.Add("@Now",DateTime.Now);

 

      foreach (Order o in q.Execute())

        o.Remove();

    }

    finally {

      EnableSecurity();

    }

  }

 

  public override TimeSpan GetDelay(Exception e)

  {

    if (e!=null)

      return TimeSpan.FromSeconds(10+random.Next(5));

    else

      return TimeSpan.FromSeconds(600+random.Next(300));

  }

}

 

...

 

// Let's add full-text indexing service to the runtime services pool

domain.RuntimeServices.AddRuntimeService("FtIndexer",typeof(FtIndexer));

// Let's add cleanup service to the runtime services pool

domain.RuntimeServices.AddRuntimeService("CleanupService",typeof(CleanupService));

IXxxEventWatcher interfaces

DataObjects.NET provides 3 interfaces that are specially supported by DataServices and Session\DataObject\Transaction\QueryBase objects. In case if some of these interfaces is implemented by one of your shared DataServices, it's being notified on the whole set of events declared in the corresponding interface.

 

The following interfaces are provided:

IDataObjectEventWatcher makes DataService being notified on DataObject-related events, such as instance creation, modification and deletion. All DataObject.OnXxx events are also "forwarded" to IDataObjectEventWatcher implementers

ITransactionEventWatcher makes DataService being notified on Transaction-related events, such as transaction creation, commit or rollback

IQueryEventWatcher makes DataService being notified on Query\SqlQuery-related events, such as query execution. This interface allows to transparently modify underlying IDbCommand - e.g. to apply an additional restriction to its "where" clause.

 

Please refer to .HxS\.Chm Help for additional information on these interfaces.


 

Events

DataObject events

Such types as DataObject and DataObjectCollection provide a set of virtual methods rather then regular delegates for events. You can override the following methods of DataObject to implement event handlers:

 

Method name

Description

OnCreate

A constructor analogue. Called on instance initialization. Declare OnCreate(parameters ...) to use parameterized instance initialization (in this case appropriate OnCreate method will be chosen based on arguments passed to the Session.CreateObject method). Override this method (or declare a new virtual method with OnCreate name) to perform custom actions after instance creation.

Notes:

- Security system is disabled during this method execution. Use OnCreated method for enforcing different security restrictions

- IsCreating is true during this method execution.

OnCreateDeserializable

This method is similar to OnCreate, but is invoked on newly created instance before its deserialization. Override this method to perform custom actions before instance deserialization.

OnCreated

Override this method to perform custom actions after instance creation. Usually security permission Demands should be executed in it.

OnLoad

Called after instance is loaded or reloaded. Override this method to perform custom actions after the instance is loaded\reloaded (e.g. calculation of some computable properties).

OnPersist

Called before changes are persisted (see DataObject.Persist method) to the database. Override this method to perform custom actions before persisting the instance.

OnPersisted

Called after changes are persisted to the database. Override this method to perform custom actions after persisting the instance.

OnGetProperty

Called during DataObject.GetProperty execution. Override this method to perform custom actions before property reading.

OnSetProperty

Called during SetProperty method execution. Override this method to perform custom actions before property changing.

OnPropertyContentChanging

Called before inner content of some non-ValueType property is changed (e.g. and item is added into DataObjectCollection\ValueTypeCollection property). Override this method to perform custom actions before contents of collection or non-ValueType property is changed.

OnPropertyContentChanged

Called when inner content of some non-ValueType property was changed (e.g. and item is added into DataObjectCollection\ValueTypeCollection property). Override this method to perform custom actions after contents of collection or non-ValueType property was changed.

OnRemove

Called before instance is removed (see DataObject.Remove method). Override this method to perform custom actions before the removal of instance.

OnRemoved

Called after instance is removed (see DataObject.Remove method). Override this method to perform custom actions after the removal of instance.

OnSecurityParentChanged

Called when DataObject.SecurityParent was changed. Override this method to perform custom actions on this event.

OnReference

Called before establishing a reference to the instance. Override this method to perform custom actions before reference to the current instance is established.

OnDereference

Called before removing a reference to the instance. Override this method to perform custom actions before reference to the current instance is removed.

OnSerializing

Called before instance is serialized (see Serializer class). Override this method to perform custom instance serialization.

OnSerializeIdentity

Serializes object identity, when object is serialized as reference. Override this method to add custom serializable identity to your persistent type.

OnDeserializing

Called before instance is deserialized. Override this method to perform custom actions before instance deserialization.

OnPropertyDeserializationError

Called on any error during deserialization of instance field. Override this method to handle deserialization errors. This can be very necessary if it's required to deserialize another version of object - e.g. if this version was containing a field that doesn't exists in the current version.

OnDeserialized

Called after instance is deserialized. Override this method to perform custom actions after instance deserialization.

OnGraphDeserialized

Called when the whole object graph is completely deserialized. Override this method to perform custom actions after complete graph deserialization (e.g. permission demands).

 

Note: There is one more way to intercept most of these events: you should create a shared (see DataServiceType.Shared) DataService and implement IDataObjectEventWatcher interface in it. In this case it will be notified on all DataObject-related events. See IDataObjectEventWatcher interface description for additional information.

DataObjectCollection \ ValueTypeCollection events

Please see .HxS\.Chm Help: all events of these types are very similar to DataObject events:

They're represented by protected virtual methods

All such methods are named OnXXX

Session events

There are no regular event delegates in Session, but you can use DataServices to implement event handling. You'd just implement one of following interfaces in some of your DataService types to get notified on necessary category of events:

IDataObjectEventWatcher should be implemented by shared (see DataServiceType.Shared) DataService, if it needs to be notified on DataObject-related events, such as creation, modification and deletion

ITransactionEventWatcher should be implemented by shared (see DataServiceType.Shared) DataService, if it needs to be notified on Transaction-related events, such as transaction beginning and committing

Domain events

Domain provides a set of event delegates:

Event name

Description

ObjectModelBuilt

Occurs during execution of the Build methodin the moment when ObjectModel is already built, but its DatabaseModel is not built yet. Allows you to extend ObjectModel at runtime.

InitializeSystemObjects

Occurs before completion of the Build method. This event is raised before Initialize event.

Initialize

Occurs before completion of the Build method. This event is raised after InitializeSystemObjects event.

SessionCreated

Occurs on Session instance creation.

UserAuthenticate

Occurs before user authentication in the Session.

UserAuthenticated

Occurs when successful user authentication in the Session takes place.

UserAuthenticationFailed

Occurs when unsuccessful user authentication in the Session takes place.

UserChange

Occurs before changing active User in the Session.

UserChanged

Occurs on successful change of Session.User property value.

 


 

Attributes (reference)

Please note: .HxS\.Chm Help also contains comprehensive documentation for each attribute. Please refer to it for additional details\examples\additional references (see DataObjects.NET.Attributes namespace).

Type-level attributes

[Abstract]

Specifies that persistent type or service is abstract.

 

This attribute allows to mark a persistent class as abstract. Note that all DataObjects.NET persistent classes are abstract in CLR, but nevertheless DataObjects.NET generates a non-abstract proxy for each persistent type. This attribute allows to mark this proxy as abstract CLR class, so it will be impossible to create an instance of this proxy.

 

Example:

 

[Abstract] // !!!

public abstract class Principal: FtObject

{

  ...

}

[Sealed]

Specifies that class can't be inherited.

 

This attribute allows to mark a persistent class as sealed. You can't simply declare that class is sealed because all DataObjects.NET persistent classes are abstract (so they can't be normally sealed).

 

Example:

 

[Sealed] // !!!

public abstract class SealedUser: StdUser

{

  ...

}

[DeclarationMode]

Specifies DeclarationMode for the class.

 

An example:

 

[DeclarationMode(DeclarationMode.NonPersistentByDefault)] // !!!

public abstract class Animal: DataObject

{

  ...

 

  [Persistent] // !!!

  [Indexed]

  public abstract int Age {get; set;}

}

[TypeReference]

Specifies DataObjects.NET to add a reference to assembly with specified Type into assembly with proxy classes.

 

It's difficult to detect all assemblies that should be referenced by proxy assembly during its code generation. E.g. it's not enough to analyze all types used in proxy classes.

 

If DataObjects.NET can't determine that some assembly is required to build an assembly with proxies, you should use this attribute to manually specify a type from this assembly (this specification will allow DataObjects.NET to locate required assembly).

 

This type is mentioned in error message if such situation takes place (it can occur only during Domain.Build method execution.

 

Example:

 

[TypeReference(typeof(ATypeThatAppearsInProxyCodeButUndetectedByDO))]

public abstract class SomePersistentType: DataObject

{

  ...

}

 

Another example from support forum:

 

[TypeReference(typeof(IListSource))]

[TypeReference(typeof(IXmlSerializable))]

[TypeReference(typeof(ISupportInitialize))]

[TypeReference(typeof(MarshalByValueComponent))]

public abstract class MyPersistentTypeOrService: ...

[ServiceType]

Specifies the type of the DataService.

 

Possible service types:

Shared: This type of service is a "sessional singleton". This means that you should use Session.GetService to obtain a new or existing instance of the service.

NonShared: On contarary, multiple instances of services of this type can be created in the single Session - you should call Session.CreateService to obtain a new instance.

 

Example:

 

[ServiceType(DataServiceType.Shared)] // !!

public abstract class MySharedService : RuntimeService

{

  ...

}

Mapping attributes

[Persistent] \ [NotPersistent]

Indicates that property is (not) persistent (see [DeclarationMode] also). These attribute override default declaration mode for a particular property.

 

Example:

 

public abstract class HomeAnimal: Animal

{

  // Persistent property!

  [Indexed]

  [Length(128)]

  public abstract string Name {get; set;}

 

  ...

 

  [NotPersistent] // Not persistent property!

  [Transactional(TransactionMode.Disabled)]

  // Necessary!

  // Otherwise DataObjects.NET will consider

  // this method (property) as requiring a

  // transaction, but it couldn't implement a

  // wrapper for it, because it isn't a virtual

  // method (property).

  public string SomeNotPersistentProperty {

    get {

      return someNotPersistentData;

    }

  }

 

  // This is a transactional, but not persistent property.

  [NotPersistent] // !!!

  public virtual Animal[] AnimalsWithTheSameName  {

    get {

      Query q = Session.CreateQuery(

        "Select Animal objects where {Name}=@Name");

      q.Parameters.Add("@Name", Name);

      return (Animal[])q.ExecuteArray();

    }

  }

 

  ...

}

[ShareAncestorTable]

Indicates that class should share a table that stores properties of parent class instances. Parent's table will additionally contain a set of columns representing persistent properties declared in descendant having this attribute.

 

Note: this attribute can be applied only on persistent classes.

 

Example:

 

[ShareAncestorTable] // !!! Shares "doDataObject" table

public abstract class SomeClassA: DataObject

{

  ...

}

 

[ShareAncestorTable] // !!! Shares "doDataObject" table also

public abstract class SomeClassB: SomeClassA

{

  ...

}

[ShareDescendantTable]

Indicates that class should share tables that store properties of descendant instances. Descendant's tables will additionally contain a set of columns representing persistent properties declared in ancestor having this attribute.

 

Note: this attribute can be applied only on persistent classes.

 

Example:

 

[ShareDescendantTable] // !!! Shares "SomeClassB" & "SomeClassC" tables

public abstract class SomeClassA: DataObject

{

  ...

}

 

public abstract class SomeClassB: SomeClassA

{

  ...

}

 

public abstract class SomeClassC: SomeClassA

{

  ...

}

[NotVersionized]

Indicates that class shouldn't be versionized. This attribute should be applied to all classes that doesn't require versionizing (maintaining their lifetime history in the storage). Attribute is inherited.  Has no effect, if Domain.Versionizing==false.

 

Note: this attribute can be applied only on persistent classes.

 

Example:

 

[NotVersionized] // !!!

public abstract class SomeNotVersionizedClass: DataObject

{

  ...

}

 

[DbName]

Specifies the base part of the field's related column name or the base part of the class' related table name.

 

Note: this attribute can be applied on types (classes and structs), persistent properties and fields.

 

You can use the following characters in DbNames: [_A-Za-z0-9-]. DbName can't be an empty string or null reference (Nothing in Visual Basic).

 

Example:

 

public abstract class Person: DataObject

{

  ...

 

  [DbName("FN")] // !!!

  public abstract string FullName {get; set;}

 

  ...

}

[Nullable]

Indicates that underlying database column should be marked as "can contain null values"; in this case null property value will be represented by NULL column value.

 

An example:

 

public abstract class Article: DataObject

{

  [Nullable] // !!!

  public abstract DateTime PublicationDate {get; set;}

 

  // This property can be not transactional, since it executes

  // a single method that is transactional itself.

  [NotPersistent]

  [Transactional(TransactionMode.Disabled)]

  // Anyway necessary, since this isn't

  // a virtual property.

  // Otherwise DataObjects.NET will consider

  // this method (property) as requiring a

  // transaction, but it couldn't implement a

  // wrapper for it, because it isn't a virtual

  // method (property).

  public bool IsPublicationDateNull  {

    get {

      return this["PublicationDate"]==null;

      // Or GetProperty("PublicationDate")==null;

    }

  }

 

  ...

}

[SqlType]

Specifies column SqlType that should be used to hold the value of the property (available for properties that can be mapped to multiple SqlTypes).

 

Example:

 

public abstract class HomeAnimal: DataObject

{

  [Indexed]

  [SqlType(SqlType.AnsiVarChar)] // !!!

  [Length(128)]

  public abstract string Name {get; set;}

 

  [SqlType(SqlType.AnsiChar)] // !!!

  [Length(32)]

  public abstract AnimalType Type {get; set;}

 

  ...

}

 

public enum AnimalType {

  ...

}

[Length]

Specifies column length for the property with variable length (e.g. string or blob).

 

Example:

 

public abstract class HomeAnimal: DataObject

{

  [Indexed]

  [Length(128)] // !!!

  public abstract string Name {get; set;}

 

  ...

}

Indexing attributes

[Indexed]

Indicates that property should be indexed.

 

Note: this attribute can be applied only on persistent properties. Use [Index] attribute to index struct fields.

 

public abstract class HomeAnimal: Animal

{

  [Indexed(Unique=true)] // !!!

  [Length(128)]

  public abstract string Name {get; set;}

 

  [Indexed] // Not necessary - reference properties are always [Indexed]

  public abstract Person Owner {get; set;}

 

  ...

}

 

Indexes can be:

Unique (see Unique property)

Clustered (one per type \ collection). Clustered index is created on primary key of each table, if it doesn't have explicitly specified clustered index

[Index]

Describes one multicolumn index.

 

Note: this attribute can be applied only on persistent classes.

 

Example:

 

[Index("IX_SFNSN", new string[] {"Surname","FirstName","SecondName"})] // !!!

[Index("IX_FNSN",  new string[] {"FirstName","SecondName"})] // !!!

[Index("IX_SSFNSN", "Surname,SecondName")] // !!!

public abstract class Person: DataObject

{

  [Length(128)]

  public abstract string FirstName {get; set;}

 

  [Length(128)]

  public abstract string SecondName {get; set;}

 

  [Length(128)]

  public abstract string Surname {get; set;}

 

  ...

}

 

Indexes can be:

Unique (see Unique property)

Clustered (one per type \ collection). Clustered index is created on primary key of each table, if it doesn't have explicitly specified clustered index

 

Notes:

Use "StructField.NestedStructField.NestedField" notation to include nested struct fields into the index

Use "ValueTypeCollectionField.NestedField" notation to include nested ValueTypeCollection fields into the index

Multilingual database support

Comprehensive support for multilingual databases is one of unique features of DataObjects.NET. Each persistent property can be an:

Regular property

[Translatable] property - a set of values will be maintained for this property, one for each Culture registered in the Domain.

[Collatable] property (valid only for string properties) - a set of columns with different collations (one per each Culture) will be created in the database to keep the value of single string to allow the use of different sorting rules for this property.

[Translatable]

Indicates that property is translatable. A set of values will be maintained for this property, one for each Culture registered in the Domain. Each of such values will be stored in its own column having culture suffix (e.g. if property name is "Title", the name of the column will be "Title-En" for a culture having "En" Name).

 

Note: this attribute can be applied on any persistent property, but not on struct fields (but the whole struct property can be translatable).

 

Example goes further.

[Collatable]

Indicates that property is collatable. A set of columns with different collations (one per each Culture registered in the Domain) will be created in the database to keep the value of single string to allow use different sorting rules for this property.

 

Note: this attribute can be applied on string properties only.

 

Example (for both [Translatable] and [Collatable] attributes):

 

public abstract class Person: DataObject

{

  [Collatable]

  [Indexed]

  public abstract string Name {get; set;}

 

  ...

}

 

public abstract class Book: DataObject
{
  [Indexed]
  public abstract string Title {get; set;}

  [Translatable]
  public abstract string Description {get; set;}

  [Translatable]

  [ItemType(typeof(Comment))]

  public abstract ValueTypeCollection Comments { get; }

 

  ...

}

...

Culture cEn = domain.Cultures["En"];
Culture cRu = domain.Cultures["Ru"];

Book b  = (Book)session.CreateObject(typeof(Book));
b.Title = "DataObjects.NET Internals";
session.Culture  = cEn; // Switches current culture in the session

b.Description    = "DataObjects.NET is...";
session.Culture  = cRu; // Switches current culture in the session
b.Description    = "DataObjects.NET есть...";

 

In this example Session.Culture property was used to switch the session to another culture. Later we assigned a value to the another (Russian) version of the translatable Description property. Another way to do the same thing is:

 

Book b  = (Book)session.CreateObject(typeof(Book));
b.Title = "DataObjects.NET Internals";
bk1["Description",cEn] = "DataObjects.NET is... ";
bk1["Description",cRu] = "DataObjects.NET есть... ";

 

Now let's look on [Collatable] properties:

 

Culture cEn = domain.Cultures["En"];

Culture cRu = domain.Cultures["Ru"];

session.Culture  = cEn; // Switches current culture in the session

QueryResult qr1 = session.CreateQuery(

  "Select Person object where {Name}>'Й' order by {Name}").Execute();

session.Culture  = cRu; // Switches current culture in the session

QueryResult qr2 = session.CreateQuery(

  "Select Person object where {Name}>'Й' order by {Name}").Execute();

 

Both queries are absolutely the same, nevertheless the number and order of objects in qr1 and qr2 can be different: English and Russian languages has different sorting rules, thus comparison of two equal Unicode characters may lead to different results.

Relationships and collection attributes

[Contained]

Indicates that related DataObject instance (or instances - when property is collection) is contained, so it should be removed on property owner's removal.

 

This attribute indicates aggregation relationship between refered object (reference property) and its container. Aggregation implies that any aggregated object can exist only in some contained object, so its lifetime is usually shorter then the lifetime of its container (exception is when you move an aggregated object to another container). Any aggregated object is removed on removal of its container.

 

Example:

 

public abstract class Printer: DataObject

{

  ...

 

  [Contained] // !!!

  public abstract PrinterInfo Info {get; set;}

 

  [Contained] // !!!

  [ItemType(typeof(InkLevelInfo))]

  public abstract DataObjectCollection InkLevelInfo {get;}

 

  ...

}

[SelfReferenceAllowed]

Indicates that property is allowed to store a reference to its owner (persistent instance to which the property belongs).

 

Example:

 

public abstract class Person: DataObject

{

  ...

 

  [SelfReferenceAllowed] // A person can be a best friend of itself :)

  [ItemType(typeof(Person))]

  public abstract Person BestFriend {get;}

 

  [SelfReferenceAllowed] // A person can be a friend of itself :)

  [Symmetric]

  [ItemType(typeof(Person))]

  public abstract DataObjectCollection Friends {get;}

 

  ...

}

[ItemType]

Specifies the type of items in the DataObjectCollection or ValueTypeCollection. Allows to specify OwnerField for ValueTypeCollections additionally.

 

Example (DataObjectCollection):

 

public abstract class Person: DataObject

{

  ...

   

  [ItemType(typeof(Person))] // !!!

  public abstract DataObjectCollection Children {get;}

 

  [Symmetric]

  [ItemType(typeof(Person))] // !!!

  public abstract DataObjectCollection Friends {get;}

}

 

Example (ValueTypeCollection):

 

public struct Order

{

  public Seller Seller;

  public Customer Customer;

  public Person Advisor;

  public int Quantity;

  ....

}

public abstract class Person: DataObject

{

  [ItemType(typeof(Order), OwnerField = "Advisor", AllowNullOwner = true)] // !!!

  [PairTo(typeof(Seller), "Orders")]

  public abstract ValueTypeCollection Orders {get;}

 

  ...

}

 

public abstract class Seller: Person

{

  [ItemType(typeof(Order), OwnerField = "Seller")] // !!!

  public abstract ValueTypeCollection Orders {get;}

 

  ...

}

 

public abstract class Customer: Person

{

  [ItemType(typeof(Order), OwnerField = "Customer")] // !!!

  [PairTo(typeof(Seller), "Orders")]

  public abstract ValueTypeCollection Orders {get;}

 

  ...

}

 

See "Relationships" chapter for additional information.

[PairTo]

Defines two-way (mutual, or paired) relationship. Indicates that persistent collection (DataObjectCollection or ValueTypeCollection) or reference property is a "reflection" of another ("master") collection or reference property.

 

 "Two-way (mutual, or paired) relationships" chapter is devoted to solely this attribute, please refer to it for additional information and examples.

[AutoFixup]

Specifies the action type that should be performed with the reference\collection property on removal of its target. By default Clear action is performed for any reference\collection property.

 

AutoFixupAction types:

Clear (default): a reference property should be automatically set to null on removal of its target

Block: prevents removal of reference target. An exception will be thrown on attempt to do this except the case when reference holder object is also removing.

None: reference fixup is turned off for a particular reference property - useful e.g. when there is a service similar to XxxFtIndexer that actually performs the same, but more efficiently

 

Example:

 

public abstract class Article: DataObject

{

  [ItemType(Author)]

  [PairTo(typeof(Author),"Articles")]

  [AutoFixup(AutoFixupAction.Block)] // !!!

  public abstract DataObjectCollection Authors {get; set;}

  ...

 

  [AutoFixup(AutoFixupAction.None)] // !!!

  public abstract Publisher Publisher {get; set;}

  ...

}

 

Note: reference properties are always indexed automatically, except when [AutoFixup(false)] is applied on them.

Access \ update strategy definition

[LoadOnDemand]

Indicates that value of the property should be loaded on the first access, but not on the instance loading.

 

Note: this attribute can be applied to persistent properties of any type. All collection properties (DataObjectCollection \ ValueTypeCollection) are always loaded on demand.

 

Threshold property of this attribute allows to specify the minimal size of property value, for which [LoadOnDemand] attribute becomes intact. This allows to load relatively small values without additional queries needed to fetch a value of such property.

 

To be exact, it specifies how large the property value should be while it's possible to keep its copy in FastLoadData column (if it's larger then Threshold, nothing is kept).

 

Notes:

New behavior is fully supported by collection\delegate properties - you can specify that a collection may reside in FastLoadData also, while it's relatively small (say, less then 16 elements)

Each threshold unit is corresponds to one unit of property size, e.g. for collection\delegate properties it's the count of their items, for string properties - length of the string, for serializable\byte[] properties - their length in bytes

[LoadOnDemand] with Threshold can be used only for properties marked as [ChangesTracking(ChangesTrackingMode.ThroughOwner)] properties (properties, these changes are tracked by owner's VersionID value). This is obviously necessary, since any change of such property affects on FastLoadData value, and consequently - on VersionID value. This ChangesTrackingMode is default for all properties, but it can be overridden by [ChangesTracking] attribute.

 

Example:

 

public abstract class Article: DataObject

{

  [LoadOnDemand(Threshold=32)] // !!!

  public abstract string Content {get; set;}

 

  ...

}

[ChangesTracking]

Specifies ChangesTrackingMode to use for the field.

 

There are two changes tracking modes:

ThroughOwner: Allows owner (DataObject instance containing the field) to track changes through its VersionID field. This means that owner's VersionID will be increased on any change made to field.

Independently: Changes are tracked independently of owner, that means its VersionID won't be increased on changes in field. You can use this mode to decrease the level of concurrency (may be efficient for frequently updating collections), or prevent VersionID changes (that can be made by such services as FtIndexer) but in this case cached value of such property will be invalidated on the completion of each new outermost transaction (rather then only on changes of owner's VersionID).

 

Example:

 

public abstract class Person: DataObject

{

  ...

 

  [ChangesTracking(ChangesTrackingMode.Independently)] // !!!

  // Changes are tracked independently

  [Symmetric]

  [ItemType(typeof(Person))]

  public abstract DataObjectCollection Friends {get;}

 

  ...

}

Property validators, correctors, modifiers

[Validator]

Attaches IPropertyValueValidator object to DataObject property.

 

IPropertyValueValidator validates DataObject property value before it is actually set by DataObject.SetProperty method.

 

Example:

 

using DataObjects.NET;

using DataObjects.NET.Attributes;

using DataObjects.NET.Helpers;

 

public abstract class Person: DataObject

{

  ...

 

  [Validator(typeof(DisallowLessThan), "Abraham")]  // !!!

  [Validator(typeof(DisallowGreaterThan), "Zorro")] // !!!

  [Validator(typeof(DisallowEqualTo), "Guest")]     // !!!

  [Validator(typeof(DisallowLongerThan), 32)]       // !!!

  public abstract string FullName {get; set;}

 

  [Validator(typeof(DisallowLessThan),    0)]   // !!!

  [Validator(typeof(DisallowGreaterThan), 150)] // !!!

  public abstract int Age {get; set;}

 

  ...

}

 

Notes:

See IPropertyValueValidator and DataObjects.NET.Helpers namespace for list of all built-in validators

All exceptions thrown by associated IPropertyValueValidator are "wrapped" into ValidationException

Remember that associated IPropertyValueCorrector (see [Corrector] attribute) is executed before execution of any validators

[Corrector]

Attaches IPropertyValueCorrector object to DataObject property.

 

IPropertyValueCorrector corrects DataObject property value before it is actually set by DataObject.SetProperty method.

 

Example:

 

using DataObjects.NET;

using DataObjects.NET.Attributes;

using DataObjects.NET.Helpers;

 

public abstract class Person: DataObject

{

  ...

 

  [Corrector(typeof(Truncator), 64)] // !!! Truncates FullName value to 64 chars

  public abstract string FullName {get; set;}

 

  ...

}

 

Notes:

See IPropertyValueCorrector and DataObjects.NET.Helpers namespace for list of all built-in validators

Remember that associated IPropertyValueCorrector (see [Corrector] attribute) is executed before execution of any validators

[StorageValueModifier]

Attaches IPropertyStorageValueModifier object to DataObject property.

 

IPropertyStorageValueModifier implementros allows to pre-process internal property value before persisting it to the database (e.g. compress it) and do the same on fetching it (e.g. decompress it).

 

Example:

 

using DataObjects.NET;

using DataObjects.NET.Attributes;

using DataObjects.NET.Helpers;

 

public abstract class DbImage: DataObject

{

  ...

 

  [LoadOnDemand]

  [StorageValueModifier(typeof(Compressor),

    CompressionMethod.Zip, CompressionLevel.Best)]

  public abstract System.Drawing.Image Image {get; set;}

 

  [StorageValueModifier(typeof(Compressor))]

  public abstract byte[] AdditionalData {get; set;}

 

  ...

}

[TypeModifier]

Attaches IPropertyTypeModifier object to DataObject property.

 

IPropertyTypeModifier allows to associate different internal type (in comparison with actual property type) that should be used to internally store a DataObject property. For example, it allows to expose some property as object, but internally store it as string.

 

See IPropertyTypeModifier interface description for additional information.

[PropertyType]

Specifies the type of the Field descendant that should be used to handle (manage persistence of) the property. Normally you shouldn't use this attribute.

 

Example:

 

public abstract class StrangeContainer: DataObject

{

  [PropertyType(typeof(ObjectField))]

  public abstract byte[] Content {get; set;} // !!!

  // By default DataObjects.NET.ObjectModel.BlobField

  // should handle a persistence of such field, but

  // [PropertyType(...)] overrides this - so content

  // will be persisted as serializable object field.

}

Security attributes

[Demand]

Provides declarative support for permission demands.

 

public abstract class Book: DataObject

{

  [Demand(typeof(BookReadPermission),   AccessorType = AccessorType.Get)]

  [Demand(typeof(BookModifyPermission), AccessorType = AccessorType.Set)]

  public abstract string Title { get; set; }

 

  [Demand(typeof(AdministrationPermission))]

  // AccessorType.All is used by default

  public abstract Author Author { get; set; }

 

  ...

}

 

Note: this attribute can be applied on persistent and non-persistent properties, as well as on methods.

Method attributes

[NotOverridable]

Indicates property or method can't be overriden.

 

Note: this attribute can be applied to methods of persistent types (DataObject descendants) or services (DataService descendants).

 

You should use this attribute when it's necessary to disable possible override of some virtual method\property in descendants.

 

Some methods\properties of your classes should be virtual because they require automatic transactions provided by DataObjects.NET. DataObjects.NET overrides these methods\properties in proxy classes to provide desired services.

 

So if you should declare a method as virtual (to provide automatic transaction support for it), but don't want to give an opportunity to override it in descendants (for any descendant except proxy class), you should use this attribute. In this case DataObjects.NET will throw an exception during Build execution if it discovers that method was overriden.

 

Example:

 

public abstract class Article: DataObject

{

  ...

 

  [NotOverridable] // !!!

  [Transactional(TransactionMode.TransactionRequired)]

  public virtual Book ConvertToBook()

  {

    ...

  }

 

  ...

}

[Transactional]

Controls automatic transactions behavoir for a method or a property and specifies automatic transaction mode (see TransactionMode) for it.

 

Automatic transactions are discussed in corresponding section of this manual.

 

Notes:

This attribute can be applied to methods of persistent types (DataObject descendants) or services (DataService descendants)

If you decided to apply this attribute to some method or property, you should ensure that this method changes only transaction-dependent data. E.g. imagine that one of your methods can modify an external (e.g. ArrayList, passed as argument) instance of non-DataObject type - it will be impossible to make a complete rollback if exception occurs and modifications are already made. But you can handle this situation by writting a custom exception handler.

See TransactionController type also to understand the behavior of this attribute better.

 

TransactionMode members (it's a [Flags] enumeration):

Member Name

Description

Disabled

Automatic transactions are disabled for specified method or property. Note that you can use Unsafe flag with this transaction mode - this means that method doesn't require a transaction itself, but contains unsafe exception handlers (so any transactional method called by it should be executed in the new transaction). Value is 0x0.

TransactionRequired

Method\property requires a transaction during its execution. This means that if a transaction wasn't started and locked by the upper caller, or upper caller is marked as Unsafe, or upper caller is a non-transactional method, a new transaction should be started on the method invocation. In this case this transaction should be committed on successful execution, or rolled back if any exception is thrown. Note that if a transaction wasn't started on the method invocation (so existing transaction was running and all other mentioned conditions were satisfied), it will be rolled back on any thrown exception also, but it won't be comitted on successful invocation. Value is 0x1.

NewTransactionRequired

Method\property requires a new transaction during its execution. Note that normally you shouldn't use this mode - its behavior can be completely reproduced by use of TransactionRequired option and Unsafe flag - in this case the number of inner transactions tends to be significantly lower and consequently the performance will be higher (each inner transaction requires delayed updates to be flushed before its beginning and completion). Value is 0x3.

ExistingTransactionRequired

Method\property requires an existing transaction during its execution. This means that if a transaction wasn't started by the upper caller, TransactionRequiredException will be thrown. Value is 0x5.

Unsafe

Method is unsafe - this means that it contains unsafe exception handlers catching exceptions from transactional methods. This exactly means that any of transactional methods invoked by it and marked with TransactionRequired option should be executed in the new transaction (to make it possible to rollback any of such methods). Note that you shouldn't use this flag with methods that contain no exception handlers or use only safe exception handlers. See the TransactionController type description for examples of transaction-safe exception handling code. Value is 0x100.

SupportsOfflineMode

Method supports offline mode. This flag means that a new transaction shouldn't be created automatically for in the OfflineMode, if there is no already running transaction. Practically this means that this method is capable of returning cached property values in the OfflineMode. Value is 0x200.

 

Example:

 

public abstract class HomeAnimal: Animal

{

  ...

 

  [Transactional(TransactionMode.Disabled)] // Not transactional method!

  public string GetTypeName()

  {

    return this.GetType().BaseType.FullName; // BaseType is required, since

                                            // we are work with proxies!

  }

 

  [Transactional(TransactionMode.TransactionRequired)] // Transactional method!

  public virtual Animal[] GetAnimalsWithTheSameName1()

  {

    Query q = Session.CreateQuery(

      "Select Animal objects where {Name}=@Name");

    q.Parameters.Add("@Name", Name);

    return (Animal[])q.ExecuteArray();

  }

 

  // Any method requires a transaction by default, so this declaration

  // is equivalent to previous one.

  public virtual Animal[] GetAnimalsWithTheSameName2()

  {

    Query q = Session.CreateQuery(

      "Select Animal objects where {Name}=@Name");

    q.Parameters.Add("@Name", Name);

    return (Animal[])q.ExecuteArray();

  }

 

  [Transactional(TransactionMode.NewTransactionRequired)]

  // Transactional method, that always

  // executes in the new (possible nested)

  // transaction.

  public virtual Animal[] GetAnimalsWithTheSameName3()

  {

    Query q = Session.CreateQuery(

      "Select Animal objects where {Name}=@Name");

    q.Parameters.Add("@Name", Name);

    return (Animal[])q.ExecuteArray();

  }

 

  [Transactional(TransactionMode.Unsafe)] // A method containing try-catch blocks

                                          // that can perform some transactional

                                          // methods on exception.

  public virtual void TryCatchMethod()

  {

    try {

      SomeTransactionalMethod();

    }

    catch {

      // Since this method is marked as unsafe, this transactional

      // method will have a chance to execute (otherwise it couldn't

      // be executed, because DataObjects.NET usually rolls back

      // a transaction on exception, but in this case it will perform

      // a rollback of a method that thrown an exception, i.e.

      // of SomeTransactionalMethod() in our case).

      OtherTransactionalMethod();

    }

  }

 

  ...

}

[BusinessMethod]

Indicates that internal or protected online method is safe for calling from the offline layer (i.e. it can be invoked via IMethodCallTarget.Invoke method).

 

Note: any public method is always considered as [BusinessMethod].

[OfflineBusinessMethod]

Indicates that offline method has its online analogue. All invocations of such offline method will be registered (see ObjectSet.DisableBusinessMethodCallRegistration and ObjectSet.EnableBusinessMethodCallRegistration),  and corresponding online methods will be invoked on the corresponding server-side objects during ObjectSet.ApplyChanges execution - to reproduce the actions performed by the offline methods, but on the server side.

Miscellaneous attributes

[Alias]

Specifies an alias (synonym) for the field\class name.

 

Note: this attribute can be applied on types (classes and structs), persistent properties and fields.

 

Aliases are useful when:

It's necessary to give a short synonym to the type or property name - to use this synonym in queries

It's desirable to shorten full type names in case when two or more types have equal names, but located in different namespaces

Give "pascal"-style synonym to internal\private struct field ("camel" naming style is usually used for such identifiers)

 

Multiple aliases are allowed. You can use the following characters in aliases: [_A-Za-z0-9]. Alias can't be an empty string or null.

 

Example:

 

public abstract class Person: DataObject

{

  ...

 

  [Alias("FN")] // !!!

  public abstract string FullName {get; set;}

 

  ...

}

[NotSerializable]

Indicates that value of the property shouldn't be serialized/deserialized by Serializer.

Example:

 

public abstract class Person: DataObject

{

  [Length(128)]

  public abstract string FirstName {get; set;}

 

  [Length(128)]

  public abstract string SecondName {get; set;}

 

  [Length(128)]

  public abstract string Surname {get; set;}

 

  // Look below, this is a calculated persistent property,

  // so it isn't necessary to serialize it, since it can be

  // re-calculated on deserialization.

  [Length(768)]

  [NotSerializable] // !!!

  public abstract string FullName {get; set;}

 

  ...

 

  // This method is be virtual, because it should require a transaction

  // to run. Actually all methods require a transaction to run by default,

  // but an exception will be thrown if you'll declare a not virtual method

  // without explicitely marking it as not transactional.

  protected virtual void UpdateFullName()

  {

    FullName = (FirstName.Trim() + " " +

      SecondName.Trim() + " " +

      Surname.Trim()).Trim();

  }

 

  protected override void OnPropertyChanged(string name, Culture culture,

    object value)

  {

    base.OnPropertyChanged (name, culture, value);

    if (name=="FirstName" || name=="SecondName" || name=="Surname")

      UpdateFullName();

  }

 

  protected override void OnDeserialized(

    DataObjects.NET.Serialization.Serializer serializer)

  {

    base.OnDeserialized (serializer);

    UpdateFullName();

  }

 

  ...

}

[ToOffline]

Indicates if persistent property can be used in offline entities.

 

By default all persistent properties are available in offline entities (unless this attribute indicates the opposite).

 

Example:

 

public abstract class Folder: DataObject

{

  [ToOffline(false)] // !!!

  protected abstract string SomeSystemProperty {get; set;}

 

  ...

}

[ProxyAttribute]

Allows to define set of attributes that should be applied to DataObjects.NET-generated proxy class, its member, method parameter, etc. If these attribute is used, DataObjects.NET doesn't tries to copy other attributes - except of that it applies only attributes specified using this attribute. Normally you shouldn't use this attribute.

 

Please see this attribute constructors.


 

Advanced features

Persistent interfaces

Persistent interfaces allow to execute queries against objects that supports them.


Let's think we have
IHasCreateModifyDates persistent interface. It declares two persistent properties: CreateDate and ModifyDate. These properties can be automatically maintained by your persistent objects (e.g. you can even put a code that automatically updates these properties in some of your base types - such code should check if current object supports IHasCreateModifyDates and perform appropriate updates of CreateDate\ModofyDate properties in OnCreate\OnPropertyChanged\OnPropertyContentChanged events).

 

So you should simply declare that some of your types supports IHasCreateModifyDates (+ add 2 corresponding persistent properties of this interface) to get a complete support of desired behavior!

 

Moreover, you can execute a Query like: "Select IHasCreateModifyDate objects where {CreateDate}>@MinCreateDate" - it will return all objects that supports this interface and satisfy specified criteria.

 

Another useful example: you can implement IHasOwner interface + provide its automatic support in some base class to allow some of your objects to have an Owner.

 

Let's think how we can use this feature to build an application extendable by third parties (say, clients). Since third-party (client-made) libraries should interact with our owns, we should ensure this is possible. Let's look what can they do with our own types:

 

They can reference them - simply by declaring the following types of persistent properties:

DataObject\IDataObject-type property

YourOwnType (DataObject descendant) property

IYourOwnType (DataObject descendant) property (property of interface type)

DataObjectCollection property with item type of DataObject\IDataObject\YourOwnType\IYourOwnType

Struct property containing DataObject\IDataObject\YourOwnType\IYourOwnType property

ValueTypeCollection property with item type of struct containing DataObject\IDataObject\YourOwnType\IYourOwnType property.


Moreover, you can declare some base persistent types (e.g.
DocumentBase\IDocumentBase), and your clients will be able to extend these types (e.g. add new properties and override their virtual methods). The main benefit of doing this is that your application (framework) will be able to interact with client-made types through your base types.


E.g. some of your clients may implement
XXXXDocument (DocumentBase descendant or IDocumentBase implementor), and you'll be able to interact with objects of this type, since they are descendants of type you know. You will be able to:

Reference XXXXDocument objects via DocumentBase\IDocumentBase-typed properties (since XXXXDocument is descendant of DocumentBase or implementor of IDocumentBase)

Maintain collections of DocumentBase\IDocumentBase objects

Call methods of client-made objects (known to you through your base types, or via reflection)

Query client-made objects objects by using DocumentBase\IDocumentBase properties in query criteria

And of course play with client-made objects as with any other DataObject instance

 

Example (from DoPetShop.Model):

 

public interface IPerson: IDataObject

{

  [SqlType(SqlType.VarChar), Length(80)]

  string FirstName {get; set;}

 

  [SqlType(SqlType.VarChar), Length(80)]

  string LastName {get; set;}

 

  [Contained, Nullable]

  Address Address {get; set;}

 

  [ItemType(typeof(Order))]

  OrderCollection ShipToMeOrders {get;} // "Reflects" Order.ShipToPerson property

 

  [ItemType(typeof(Order))]

  OrderCollection BillToMeOrders {get;} // "Reflects" Order.BillToPerson property

 

  void CopyFrom(IPerson fromPerson);

}

 

public abstract class Person: DataObject, IPerson

{

  [Nullable]

  public abstract DataObject Holder {get; set;}

 

  [SqlType(SqlType.VarChar), Length(80)]

  public abstract string FirstName {get; set;}

 

  [SqlType(SqlType.VarChar), Length(80)]

  public abstract string LastName {get; set;}

 

  [Contained, Nullable]

  public abstract Address Address {get; set;}

 

  // Collection of "ShipToPerson" orders

  [ItemType(typeof(Order))]

  [PairTo(typeof(Order), "ShipToPerson")]

  public abstract OrderCollection ShipToMeOrders {get;}

  // "Reflects" Order.ShipToPerson property

 

  // Collection of "BillToPerson" orders

  [ItemType(typeof(Order))]

  [PairTo(typeof(Order), "BillToPerson")]

  public abstract OrderCollection BillToMeOrders {get;}

  // "Reflects" Order.BillToPerson property

 

  // ----- End of persistent properties definitions -----

 

  public virtual void CopyFrom(IPerson fromPerson)

  {

    this.FirstName = fromPerson.FirstName;

    this.LastName = fromPerson.LastName;

    this.Address.CopyFrom(fromPerson.Address);

  }

 

  ...

 

}


 

Full-text indexing and search

DataObjects.NET supports full-text indexing and search with following features:

Unified, but absolutely customizable full-text data population

Two fill-text indexing & search driver types:

Native: uses RDBMS-provided full-text indexing and search features. Requires support of these features by underlying RDBMS driver. Currently only MS SQL driver supports native full-text indexing and search.

External: uses external full-text search engine. These driver types are RDBMS-independent, i.e. you can use such driver even while RDBMS doesn't support full-text indexing and search at all (e.g. Firebird). DotLucene full-text indexing and search driver is built-in into DataObjects.NET.

Unified full-text search queries:

Query-based: Select Author instances where {Name}>='D' textsearch top 5 freetext 'Jungle' order by {FullTextRank} desc

SqlQuery-based (see SqlQuery.FtsCondition)

Three full-text search modes:

FreeText - well-known free-text search mode, supported by all FtsDrivers

Condition - allows to specify exact full-text search condition on the language of underlying full-text search engine

LikeExpression - doesn't require full-text search engine at all, uses SQL "like" predicate to execute the query (not recommended mode, since it's actually can be very slow on >1000000-object database)

Built-in managed wrapper for Microsoft Index Service Filters allows to index content of almost any imaginable file type (such as HTML, Adobe PDF and Microsoft Office files).

Implementation steps

Derive your type from FtObject (or implement IFtObject interface, see its description for details) and implement\override one of ProduceFtData methods to make it ready for full-text indexing.

Select FtsDriver you're going to use in the Domain, and configure it. FtsDriver type and its configuration is determined by Domain.FtsConnectionUrl property. FtsConnectionUrl examples:

native://localhost/

lucene://localhost/
?IndexPath=C:\Debug\LuceneIndex
&DefaultAnalyzer=Lucene.Net.Analysis.Standard.StandardAnalyzer,%20Lucene.Net
&Analyzer-Ru=Lucene.Net.Analysis.RU.RussianAnalyzer,%20Lucene.Net
&CreateSummaryFieldsForCultures=true

Ensure that XxxFtIndexer (FtsDriver-provided RuntimeService that gathers full-text content produced by your IFtObject implementers and sends it to the underlying full-text indexing and search service) is periodically executed - it ensures that populated full-text search data is consistent with current object states. You can add it to Domain.RuntimeServices to achieve this, or manually execute it periodically.

DataObjects.NET provides Filter type operating as managed wrapper over unmanaged Index Service Filters. So it's possible to index almost any document\file type stored on the database server (or externally), in particular - Microsoft Office, HTML, PDF and almost any other commonly used file type

And finally you use full-text search clause in queries - it's supported by query language

FtObject descendant example

Let's see an example of FtObject descendant (a code from DoPetShop\Model\Category.cs)

 

public abstract class Category: FtObject, ITopLevelObject

{

  [SqlType(SqlType.VarChar), Length(80), Nullable]

  [Indexed]

  public abstract string Name {get; set;}

 

  [SqlType(SqlType.VarChar), Length(255), Nullable]

  [LoadOnDemand]

  public abstract string Descn {get; set;}

 

  ...

 

  public override string ProduceFtData(Culture culture)

  {

    return

      base.ProduceFtData(culture) + " " +

      (""+Name).ToUpper() + ": " +

      (""+Descn).ToUpper();

  }

}

 

Note: you can override or implement other (more general) ProduceFtData methods - they allow to populate several full-text search fields (such as Content, Title, Keywords, etc.). Full FtData class support is implemented in the DataObjects.NET Lucene Full-text Search Driver. Native full-text search driver actually uses only "Content-CultureName" fields of FtData.

Full-text indexer usage example

Domain initialization code of DoPetShop (see Global.asax.cs) includes this line of code:

 

domain.RuntimeServices.AddRuntimeService("FtIndexer",

  domain.FtsDriver.GetFtIndexerType());

 

This code puts XxxFtIndexer service into runtime service pool to ensure its periodic execution. See Domain.RuntimeServices property and RuntimeServicePool class for additional information.

 

Alternatively you can execute XxxFtIndexer manually at any desirable moment:

 

FtIndexer ftIndexer = domain.FtsDriver.CreateFtIndexer(s, 1000000);

ftIndexer.Execute();

Full-text filters usage example

DataObjects.NET provides Filter type operating as managed wrapper over unmanaged Index Service Filters. So it's possible to index almost any document\file type stored on the database server (or externally).

 

In particular you can index the following document types: Microsoft Office files (.doc, .dot, .rtf, .xls, .ppt, etc...), HTML files (.htm, .html), plain text files (.txt, including unicode files).

 

Notes:

You can install additional third-party filters to index other file types, e.g. Adobe PDF IFilter: http://www.adobe.com/support/salesdocs/1043a.htm.

Filters are available even while Microsoft Index Service isn't running.

See new Demo_FullTextFilters for quick start.

 

Full-text filtering example:

 

Filter docFilter = Filter.Create("doc");

FileStream docFile = new FileStream(@"C:\AnyMicrosoftWordFile.doc",FileMode.Open);

Debug.WriteLine(docFilter.GetFilteredContent(docFile));

 

Filter txtFilter = Filter.Create("txt");

FileStream txtFile = new FileStream(@"C:\AnyPlainTextFile.txt",FileMode.Open);

Debug.WriteLine(txtFilter.GetFilteredContent(txtFile));

Full-text search query example

As it was mentioned, it's possible to use full-text search clause in queries:

Query:  Select Canegory instances where {Name}>='D'
textsearch top 10 freetext 'home animal'
order by {FullTextRank} desc


 

Serialization

DataObjects.NET completely supports .NET Serialization. It allows to serialize or deserialize a graph of objects containing persistent instances using binary or SOAP formatters, user-specified formatters are also supported.

 

DataObject descendants aren't serializable by their nature - any such instance belongs to some Session and has relationships with set of other non-serializable instances. So it's impossible to serialize a DataObject instance by the usual way. To perform serialization or deserialization, you should use special class - Serializer. This class is bound to Session (see Session.CreateSerializer) and capable to serialize\deserialize a graph of objects that contains some DataObject instances using BinaryFormatter\SoapFormatter or user-specified formatter.

 

Serializer internally uses a set of helper objects to handle its job. Basically its task is to configure a formatter (add special SurrogateSelector and SerializationBinder) in the way allowing to serialize DataObject instances without difficulties.

 

Example:

 

Serializer serializer = session.CreateSerializer();

serializer.FormatterType = FormatterType.Soap;

serializer.SerializationOptions = SerializationOptions.IncludeContainedInstances;

using (StreamWriter sw = new StreamWriter("Serialized.xml")) {

  serializer.Serialize(sw.BaseStream, myObjectGraph);

  Console.WriteLine("Serialized:   {0} instance(s), {1} external(s).",

    serializer.LastOperationInstanceCount,

    serializer.LastOperationExternalInstanceCount);

}

using (StreamReader sr = new StreamReader("Serialized.xml")) {

  myObjectGraph = serializer.Deserialize(sr.BaseStream);

  Console.WriteLine("Deserialized: {0} instance(s), {1} external(s).",

    serializer.LastOperationInstanceCount,

    serializer.LastOperationExternalInstanceCount);

}

Implementation steps

Properly override serialization-related event-like methods in your custom DataObject descendants, if necessary (for example, OnCreateDeserializable, OnSerializing, OnSerializeIdentityOnDeserializing, OnDeserialize, OnGraphDeserialized, OnDeserializeIdentity)

Use Session.CreateSerializer() method to create a Serializer

Tune up the Serializer by setting desirable values to some of its properties (e.g. FormatterType & SerializationOptions)

Use Serializer.Serialize method to serialize the graph of persistent (and may be not persistent) objects

Use Serializer.Deserialize method to deserialize the stream that was previously produced by Serializer.Serialize method


 

Security System

DataObjects.NET offers built-in security system supporting per-instance access control lists (allow-deny lists), permissions (including custom permissions), security principals (users and roles) and permission inheritance. Its primary goal is to make the usage of business objects completely safe, even when these objects are publicly available - for example, via .NET Remoting.

Basic security concepts

The key feature of any security system is protection of some resources from unauthorized access. DataObjects.NET allows to make any operation with business objects protected by the security system. This means that it also allows to protect any business object - it's simply necessary to protect all operations with this business object in such case.

 

So DataObjects.NET security system protects from unauthorized access:

DataObject instances;

Instance methods and properties;

DataService instance methods an properties.

 

Now let's look on more difficult question: from whom to protect? DataObjects.NET is simply the code that executes in your applications, as well as its clients. This means that DataObjects.NET should provide some mechanism allowing to distinguish what code is attempting to perform a particular protected action.

 

DataObjects.NET provides this mechanism - it is based on Session objects. Any Session instance can be associated with a security user (User instance, User is the persistent type). So you can look on Session objects as on "security keys" also - any code that can access a particular Session object (by any possible way) is generally allowed to do anything, that is allowed for User associated with this Session.

 

This means that secure code should protect its Session objects from external access - a malicious code can, for example, "steal" the Session object from a code with higher permissions to increase its own permissions. There are several possible ways to "steal" the Session, but all of them can be prevented:

If some code has a ReflectionPermission (see MSDN Library: System.Security and System.Security.Permissions namespaces), it can access any private\protected\internal members in its application domain. This means that this code can steal the Session instance, if it gets a reference to any object that holds a reference to the Session in its non-public members. Moreover, such a code can even modify internal fields of any accessible objects - this allows it to break the operation of the whole system (at least). So if your application domain loads an assembly with unsafe code (a code that can try to steal the Session), a ReflectionPermission shouldn't be granted for it. Actually this requirement should be enforced in any .NET application that has some protected resources (if this application uses DataObjects.NET or not);

Also a Session can be stolen by malicious code if it gets a reference to some SessionBoundObject instance (e.g. DataObject or DataService instance) from this Session. This means that your code shouldn't pass these instances directly to such code - e.g. it can pass IDs of objects or name\type of service. Notice, that this doesn't mean it should be impossible for a malicious code to access any SessionBoundObject instances - it can access these instances, but only through its own Session with corresponding permissions;

 

Generally we can recommend two possible ways to prevent execution of potentially malicious code:

The safest way is to execute this code in another application domain. You can pass a pre-created Session instance to this application domain via .NET Remoting (an authentication should be performed in this Session before this operation), or pass a reference to the Domain instance to it - again via .NET Remoting. In this case you should disable AllowCreateUnauthenticatedSessions in SecurityOptions of this Domain to make it impossible to create the unauthenticated Session (such Session allows to do anything, so any permission Demand will complete successfully). Also this way allows to unload the whole application domain with unsafe code - this can be useful too, e.g. if unsafe code is actually a compiled user's script.

And the second way is to load an assembly with unsafe code to your application domain, but with as low permissions, as possible (e.g. ReflectionPermission should be definitely removed, also it's better to remove all file access-related permissions, and so on). Further actions should be almost the same as in previous case, the difference is that in this case it's not necessary to use .NET Remoting. Notice that in this case it will be impossible to unload an assembly with unsafe code - .NET doesn't allow to unload a particular assembly from the application domain, but it's possible to unload the whole application domain.

 

Now one more fact should be said: DataObjects.NET considers all code of persistent objects and data services registered in the Session's Domain as completely safe code. This means that this code potentially can do anything. Moreover, DataObjects.NET specially provides this opportunity to such a code. For example, any DataObject\DataService descendant can:

Invoke DisableSecurity\EnableSecurity methods. These methods are protected, so only SessionBoundObject descendants can access them. When security system is disabled, anything is allowed (e.g. any permission Demand completes successfully and Session.SecurityOptions aren't checked).

Change Session.User property to any desired value without invoking Session.Authenticate method. This means that these objects can perform impersonation to any user without authentication.

 

So in few words, business objects' code can omit any security restrictions and even perform impersonation to another user without authentication. This is allowed because:

Sometimes it's necessary to execute some business code on a lower security level, than the current user has. For example, let's look on FtIndexer service (a service that performs full-text indexing). It can be executed by some user manually to perform immediate full-text index data update, but imagine that this service makes an attempt to update FtRecord data for some object that isn't accessible for the current user (e.g. this user has no ReadPermission on it). A SecurityException will be thrown, that will lead to rollback of the whole re-indexing operation. The solution is to allow the code of this service to operate in insecure mode - this is completely safe in this case (because this service doesn't call any possibly malicious code).

It's not even necessary to make a "firewall" protection of some business objects from other business objects, it's enough if they follow all security restrictions by default, but can omit them. From a common point of view it's not a normal situation if some business object is coded to harm the whole system!

 

But: it's necessary to make really a "firewall" protection for all other code - it should be completely impossible to omit these restrictions for any "external" code.

 

Note: partially this depends on implementation of your business objects (DataObject and DataService descendants) - e.g. if one of such objects disables the security system and doesn't properly enables it, or provides Session.RealConnection value without any permission checks, the "real security" of the whole system will be nearly zero.

 

Note: SessionBoundObject has no public or protected constructors - this means that it's impossible for a malicious code to create a custom descendant of this type and use it to access mentioned protected members. Also all DataObjects.NET-provided descendants of this type uses the same principal, or

They are sealed (e.g. Transaction);

They provide public or protected constructors, but none of them accepts a Session instance as one of its arguments (e.g. DataObjectCollection). This means that it's possible to create such an instance, but only DataObjects.NET can bind it so some Session. E.g. DataObjectCollectionBase.Attach method checks if it was called by the DataObjects.NET assembly to ensure this (DataObjects.NET calls this method only on instances that were created by it, so it's impossible to make DataObjects.NET to invoke this method on a DataObjectCollection descendant implemented and instantiated by a malicious code).

Domain security policy

Domain security policy is determined by:

Domain.SecurityOptions property value;

Domain.SessionSecurityOptions determines default security options for any new Session;

Domain event handlers determine run-time security options for all Session instances during their lifetime.

 

Domain security policy becomes "locked" (unchangeable) after the Domain.Build(...) method invocation. This means that after this moment it's impossible to change mentioned properties and event handlers. So this part of Domain security is defined by a code that creates and builds the domain.

 

Note: pay attention to all security options: it's strongly recommended to set Domain.SecurityOptions to Maximal, if an external access to the Domain (i.e. an access from unsafe code) is possible. The same option is recommended for any Session that can be used by such a code (you can set the security options for a particular Session in Domain's event handlers, e.g. different security options can be set for different authenticated users).

Session-level security

Anything that is allowed for a particular Session instance (or in the particular Session) is determined by:

Its SecurityOptions;

Its current User (see Session.User). Note that if current user is undefined (i.e. Session.User is null), any permission demand will complete successfully;

 

As it was mentioned earlier, the security system can be temporarily turned off in the particular Session (see SessionBoundObject.DisableSecurity and SessionBoundObject.EnableSecurity methods).

 

Note: DisableSecurity method disables the security system only in the Thread from which it was invoked. All other threads working with this Session won't fell this change.

 

Note: we recommend to operate at least on RepeatableRead isolation level in any Session requiring a strong security.

DataObject-level security

DataObjects.NET security system grants, denies and checks security permissions on DataObject instances. Any DataObject instance contains Access Control List (see AccessControlList class), where allow-deny permission sets for the security principals are stored. So a particular permission is always demanded on a particular DataObject instance (and for the current User in the Session, to which this instance belongs).

 

One more feature of DataObjects.NET security system is that it supports permissions inheritance (acquisition). This means that effective permission set for any persistent instance is determined by:

Permissions applied directly on it;

All inherited permissions (i.e. permissions applied on its SecurityParent and so further - till the SecurityRoot).

 

As you've noticed, any DataObject instance has SecurityParent - a DataObject instance, from which its permissions are inherited. So from the point of the security system all persistent objects are organized into the tree, where parent not for any other node is its SecurityParent property value. But any tree should have a root object - an object with no (i.e. where SecurityParent is null). DataObjects.NET doesn't allows to have a set of such objects (so the storage should be a tree, but not a forest - anyway this doesn't lowers the genericity). Such object is called SecurityRoot object. This is again DataObject instance, that supports special interface (ISecurityRoot). ISecurityRoot is a tagging persistent interface that simply allows to locate this object in the storage. You can view on permissions applied to the SecurityRoot as on global permissions - e.g. your data services (DataService descendants) can demand some custom permissions on the SecurityRoot object (or on the Session object - when some permission is demanded on the Session object, it is actually demanded on the SecurityRoot object).

 

"Permissions acquisition" exactly means that if we want to determine effective permission set for some User and some DataObject instance, we should:

Locate all Role instances (i.e. locate all roles) to which this user belongs directly or indirectly (see Principal.AllRoles property description). Let's call all these roles + current user as effective security principal set.

Locate all SecurityParent instances of the current DataObject instance. Let's call all these instances + current DataObject instance as inheritance branch.

Union (see IPermissionSet.Union) all permissions, that are explicitly allowed (see AccessControlList.Allow method) on instances from the inheritance branch for Principals from effective security principal set;

Subtract (from the permission set we produced on the previous step) all permissions, that are explicitly denied (see AccessControlList.Deny method) on instances from the inheritance branch for Principals from effective security principal set;

The resulting permission set will be an effective permission set (for the specified User and DataObject instance).

 

Any permission that exists in the effective permission set is allowed for the current User in the Session (on the specified DataObject instance) - this means that Demand (see DataObject.Demand) of this permission won't throw a SecurityException. Also permission can be allowed, if there is another permission from its GrantedIfGrantedAnyOf permission list exists in the effective permission set. All other permissions are not allowed (SecurityException will be thrown on attempt to Demand any of such permissions).

 

Actually this is almost the same security model as used in the NTFS - any DataObject instance behaves like NTFS folder, the primary difference is that DataObjects.NET allows to use not only the predefined permissions, but a custom permissions too.

 

Note: Some permissions are supported internally by DataObjects.NET - e.g. presence of AdministrationPermission in the effective permission set actually means that any permission is allowed and any Demand will be completed successfully.

 

There are 4 cases when DataObjects.NET doesn't checks permissions in a particular Session (so the security system is turned off in this session):

If Session.User is null;

If Session.User is System User (Session.User equals to Session.SystemObjects.SystemUser);

If Session.User is Administrator User (Session.User equals to Session.SystemObjects.AdministratorUser);

If Session.IsSecurityEnabled is false. This means that SessionBoundObject.DisableSecurity method was called more times than SessionBoundObject.EnableSecurity in the current Thread and Session.

 

You can extend DataObjects.NET security system to satisfy your application requirements - all you have to do is to:

Implement your own Permission descendants (custom permission), or implement a new IPermission\IParametrizedPermission descendant;

Properly apply Demands of these permissions.

 

Note: Moreover, you can prevent most of permission demands that DataObjects.NET executes - most of internal permission demands are executed by the DataObject.OnXXX methods (event handlers). You can simply override some of these methods in your persistent classes to override default permission demands. You can find examples of such overrides in the FtRecord, Principal, User and Role implementation (the code of these classes is shipped with DataObjects.NET 2.0).

 

Note: there are set of operations that are always available on any DataObject instance. Moreover, it's impossible to block these operations. It's always possible to:

Get a reference to any DataObject instance;

Read its ID, VersionID and TypeID properties;

Read most of non-persistent properties declared in the DataObject class, e.g. Type and Session.

We think these operations don't provide any sensitive information, but their availability can significantly simplify the code in several cases (e.g. ID and VersionID properties can be used in custom caching schemas).

 

And finally two nice features of this part should be mentioned:

Performance: DataObjects.NET security system is extremely fast - passed permission demands are cached, effective permission sets for any cached DataObject instance are cached too, additionally a security notification schema is used - any cached DataObject instance notifies all dependent cached instances on changes in its effective permission set (so normally a subsequent permission demand on the same instance is executed extremely fast). This allows DataObjects.NET to execute up to 2000000 permission demands per second on 2,8GHz P4! Note that it's almost impossible to implement a security system having the similar features and performance without implementing all other caching features DataObjects.NET has. Just imagine the nightmare of implementing the similar part in your DAL!

Immediate effect: all security restrictions take effect immediately on any security-related changes in the Session - for example, it's not necessary to reopen the Session or to invoke some method to apply new security restrictions. When you adding a User to some Role or denying some permission for him or for some role it belongs to, this immediately affects on its security restrictions in the current Session. So all is transparent even in this case. Even a rollback of the inner transaction (or a rollback to the savepoint) immediately affects on security restrictions.

System security objects

As you may notice, there are some objects, that should always exist in any Domain. The minimal set of these objects is:

System User,

Administrator User,

Administrators Role,

Everyone Role,

Security root object.

 

All system users and roles are distinguished by their names - see Principal.Name and Domain.SystemObjectNames (you can change default system object names). The last object is SecurityRoot object - it's distinguished by the implementation of a tagging interface (ISecurityRoot).

 

Existence of these objects is strongly required - DataObjects.NET simply can't properly operate without them. So Domain object performs one additional step during its building - system objects initialization. It is one of the last stages of the building process. Domain checks if each of mentioned objects exists and creates it, if this is necessary. Also it applies default permissions on the SecurityRoot object during this process.

 

You can find system objects initialization code - it's shipped with the DataObjects.NET 2.0 (see DoInitializeSystemObjects method in the Domain.cs file).

 

Implement your own Domain.InitializeSystemObjects event handler to perform custom initialization of system objects (e.g. if you want to use system objects of your own types).

Implementation steps

Add custom permissions: add a set of your own permissions (Permission type descendants). DoPetShop.Model project contains examples of such permissions.

Demand new permissions to secure some execution paths. Exactly, put a set of DataObject.Demand or Session.Demand calls into all places when presence of some permission is required. Use [Demand] attribute, if you prefer declarative demand syntax. See DataObject.IsAllowed and Session.IsAllowed methods also. Search for "Demand" word in DoPetShop.Model project to find examples.

Establish permission inheritance hierarchy: properly override DataObject.SecurityParent property in all of your types, and add SecurityParentChanged calls to all places that may affect on result of this property invocation. SecurityParent property of an object should return an object, which permissions should be inherited by the current instance. Search for "SecurityParent" and "SecurityParentChanged" words in DoPetShop.Model project to find examples.

Possibly: implement custom Role\User (or StdUser) descendants. You may do this, if built-in types don't suit you by some reason, e.g. if they don't allow you to store some additional information. If this isn't necessary, use instances of Role and StdUser types.

Possibly: implement Domain.InitializeSystemObjects event handler - it allows to override default system initialization behavior. See previous chapter - it explains this step deeper.

Create instances of security principals (Users and Roles): this is nothing more then creation of User and Role instances (or instances of their descendants) and assignment of a set of Roles to each User (i.e. adding them to User.Roles collection).

Grant or deny some permissions for some security principals on some objects in the storage. E.g. usually it's necessary to grant a set of permissions for such group as "Guests" - granting ReadPermission for "Guests" on Session.SecurityRoot object will ensure that any member of this group will be able to read properties of every object. Or one more example: usually it's necessary to grant ReadPermission on each User object for itself - to allow it to read its own properties. Use AccessControlList (see DataObject.Permissions) class methods to do this.

Possibly: implement UI allowing to manage Users, Roles and permissions in your application. This is usually necessary, if you want to allow end-users to tune up security settings in your application. DataObjects.NET can't seriously help you here, but it provides a set of methods and types allowing to simplify even this part - see AccessControlList.GetFlatPermissions method.

Security-related classes

DataObjects.NET 2.0 introduces wide set of security-related classes. The most important of them are listed further, complete list of security-related classes you can find in the DataObjects.NET CHM Help (see DataObjects.NET.Security and DataObjects.NET.Security.Permissions namespaces).

Principal is the base class for any security principal. Any principal can belong to the set of Roles and has unique Name. It's possible to get AllRoles collection of any principal (a set of Roles to which it belongs directly or indirectly) and test, if a particular Principal belongs to the specific Role (see IsInRole method);

Role (Principal descendant, to which some other principals can belong). Any Role maintains its Principals collection (actually this collection is simply paired to Principal.Roles collection). As you may noticed, DataObjects.NET makes such paired collections automatically synchronized - so you can add a Principal to some Role by two equivalent (but different in the notation) ways: principal.Roles.Add(role), or role.Principals.Add(principal);

User (it's a Principal descendant too) is base class for any security User. This type introduces Authenticate method (an abstract method that should be implemented in its descendants - this allows you to implement your custom authentication schemas) and two events: OnAttachToSession() (called when a particular User becomes the current User in the Session) and OnDetachFromSession. These events allow to perform custom actions when the User becomes current in the Session (current user is a user, for which DataObjects.NET performs permission checks) - e.g. it's possible to additionally impersonate the current Thread to the specific Windows account in such case.

StdUser (User descendant) - it's the ready-to-use User implementation (so it implements Authenticate method). This type of user supports authentication by the string of characters (i.e. by the password). This type also introduces SetPassword(...) method (see its description and code to understand its behavior better - the code of almost all mentioned classes is shipped with DataObjects.NET 2.0). This type supports the following formats of storing a password: Plain, MD5 hash, SHA1 hash and SHA256 hash. All default system users (System and Administrator) created by the Domain are instances of this type (nevertheless you can override even this - you should implement you own Domain.InitializeSystemObjects event handler in this case).

IPermission is a non-persistent interface that should be implemented by any security permission type. Any instance supporting this interface should be serializable to allow DataObjects.NET to store it. It defines the following primary methods: Copy, IsAllowed, Demand.
Note:
Demand and IsAllowed methods should simply call the same methods on the object that was passed to them (ISecureObject). Also any permission should be immutable (except IPermissionSets - they are IPermission descendants too). And finally, DataObjects.NET considers two permissions as equal, if these types are equal, and they aren't IParameterizedPermission or IPermissionSet descendants (so particular IPermission instance actually has no meaning - any IPermission should be a singleton, but we decided to not use this pattern here primarily to simplify the coding of custom permissions).

Permission is default IPermission implementation. There is a set of pre-defined permissions supported internally by DataObjects.NET:

AdministrationPermission - this permission is supported internally by DataObjects.NET.

It allows to perform anything - any permission Demand will be successful if this permission is allowed.

ChangeFullTextDataPermission is required to change any full-text indexing data (see IFtObject, FtRecord);

ChangePasswordPermission is required to use SetPassword method for the particular StdUser;

ChangePermission is required to change any property of the DataObject instance;

ChangePermissionsPermission is required to change the Permissions property of the DataObject instance;

ChildrenDeserializationPermission is required to deserialize child objects of the DataObject instance;

ChildrenPermissionsDeserializationPermission is required to deserialize child objects' permissions;

OwnerPermission - the presence of this permission for a specific user (or group) tells that he is an owner of the DataObject instance (actually it implicitly grants ChangePermission, ChangePermissionsPermission, ReadPermission, ReadPermissionsPermission, RemovePermission, ChildrenDeserializationPermission, ChildrenPermissionsDeserializationPermission, SerializationPermission).

SerializationPermission is required to serialize the DataObject instance;

ReadPermission is required to read any property of the DataObject instance;

ReadPermissionsPermission is required to read the Permissions property of the DataObject instance;

RemovePermission is required to remove the DataObject instance.

Note: because any permission is immutable, it's possible to introduce the static Value method in each permission type returning the pre-created (default) instance of this permission (this is just a performance consideration).

Note: most of these permissions are checked in the DataObject.OnXXX virtual methods, so you can override the enforcement of pre-defined permissions in your business objects!

IParameterizedPermission is an interface for any "parameterized" permission. Any parameterized permission should actually represent a permission set where the number of elements isn't determinable, but nevertheless it's possible to perform Union, Subtract and IsSubsetOf operations with such a set. Instances of this type should be immutable too. There are no built-in parameterized permissions in the DataObjects.NET.

IPermissionSet represents the set of permissions. It introduces Clear, Union, Subtract, IsSubsetOf and IsSupersetOf methods.
Note:
IPermissionSet instances can be used anywhere where the IPermission instance can be passed (e.g. you can pass an IPermissionSet instance to the Demand method).
Note:
IPermissionSet isn't immutable - we decided to do this because of performance considerations - it's not a good idea to create a new permission set every time when it's necessary to modify some permission set.

PermissionSet is its ready-to-use implementation. Note, that this type properly overrides ToString() method.

ISecureObject is an object on which an IPermission can be demanded. There are two ISecureObject implementers in the DataObjects.NET: DataObject and Session. Actually Session "forwards" any permission demands to Session.SecurityRoot object (it is the DataObject and ISecureRoot descendant, the root node in the security inheritance hierarchy).

ISecityRoot is an interface that should be supported by the DataObject instance that can be used as the root object in the permission inheritance hierarchy. This is a tagging interface - DataObject.NET uses it to find the root object.
Note: only one instance supporting this interface can exist in the storage.

AccessControlList class - it's the class that actually stores permissions and performs permission checks. It is accessible through Permissions property of the particular DataObject instance. It provides the following methods: Allow, Deny, RemoveAllowed, RemoveDenied, ResetPermissions, ResetChildPermissions, IsAllowed, Demand, GetAllowedPermissionSet, GetDeniedPermissionSet, GetEffectivePermissionSet methods. This type also properly overrides ToString() method.

DataObject class introduces set of new methods (IsAllowed, Demand) and properties (SecurityPerent, SecurityChildren and SecurityRoot).

Session provides Authenticate method, User and SystemObjects properties (see its SecurityOptions also).

Domain introduces set of security-related events and SystemObjectNames property (see its SecurityOptions and SessionSecurityOptions also).

Finally there is a SecurityException class - instances of this type are used to inform about security-related exceptions.

 

Refer to the DataObjects.NET CHM Help for the further information. We recommend you to study the source code of XXXPermission, Principal, User, Role and StdUser classes, and move to the DoPetShop sample further.

Security and deserialization

The main problem of deserialization is that actually anything can be "feed" to the Serializer during this process - e.g. a malicious user (or code) can serialize some objects, change serialized data (e.g. to increase its own permissions) and make an attempt to deserialize them.

 

Because of that DataObjects.NET uses rather complicated deserialization process. This process can be divided into two steps: actual deserialization (when OnCreateDeserializable, OnDeserializing, OnPropertyDeserializationError and OnDeserialized methods of deserialized DataObject instances are executed) and post-deserialization (when OnGraphDeserialized is executed on each deserialized DataObject instance).

 

During the first step IsDeserializing flag is set to true on each deserializing DataObject instance. When this flag is set, all permission demands completes successfully on this instance. Nevertheless security system isn't turned off for the whole Session - all other instances do the same normally. This is required because sometimes it's impossible to construct permissions inheritance chain during the deserialization.

 

During the second step IsDeserializing flag is set to false - this allows to perform all necessary permission checks. Default OnGraphDeserialized implementation executes the following code (you can find it in the DataObject.cs file):

 

[Transactional(TransactionMode.Disabled)]

protected virtual void OnGraphDeserialized(Serializer serializer)

{

  SecurityParent.Demand(ChildrenDeserializationPermission.Value);

  if ((serializer.DeserializationOptions &
       DeserializationOptions.DeserializePermissions)!=0)

    SecurityParent.Demand(ChildrenPermissionsDeserializationPermission.Value);

}

 

Notice, that both permissions are demanded not on the current instance, but on its SecurityParent - a malicious user could and add these permissions to e.g. all serialized objects. Checking these permissions on SecurityParent ensures that they will be checked at least on one non-deserialized object (SecurityRoot object) - it's impossible to serialize the SecurityRoot object (it can be serialized only as DataObject reference that is completely different from the normal serialization and completely safe in this case).

 

Please refer to the .HxS\.Chm Help for the further information (DataObjects.NET.Serialization namespace).

 


 

Partitioning

Notes:

This section describes new feature of DataObjects.NET v3.9.

See Demo_Partitioning - it shows how partitioning may affect on performance.

 

Partitioning allows distributing the data of a single table across several partitions according with your own rules. Partitions can be located on different paths inside file system, probably - on different volumes. Partitioning is normally used to improve the performance and availability. There is horizontal and vertical partitioning. Horizontal partitioning implies placing different table rows into different physical locations; vertical partitioning implies the same for different columns.

 

DataObjects.NET supports only horizontal partitioning - it is normally used much more frequently, and provides more benefits. Further we'll imply we're talking about horizontal partitioning.

 

There are two partitioning modes - emulated and native:

Emulated partitioning is RDBMS-independent; it implies that DataObjects.NET creates table for each partition, and a view uniting the partitioned data across several tables.

Native partitioning is based on native partitioning features of RDBMS. Database schema stays almost the same in this case, but DDL statements generated for partitioned table creation \ updates include partitioning information.

 

Note: Microsoft SQL Server 2000 supports emulated partitioning mode only; SQL Server 2005 supports both native and emulated partitioning modes.

 

An appropriate partition for each row is selected based on calculated value of partitioning expression. Partitioning expression is a function operating on column values in rows that are inserted into the table. This function is nothing more then a regular SQL expression.

Partitioning types

DataObjects.NET currently supports 4 types of partitioning:

Range partitioning - selects partition by determining if the partitioning expression evaluation result is falling into a given range. For example, let's define the table that stores information about persons and has two partitions: the first for persons with age less than or equal to 21 years, and the second for all the others:

 

[PartitionedByRange("Age", RangeBoundaryType.Left)]

[RangePartitionDescriptor(21)]

public abstract class Person: DataObject

{

  public abstract string   Name {get; set;}

  public abstract string   SecondName {get; set;}

  public abstract string   Surname {get; set;}

  public abstract int      Age {get; set;}

  public abstract string   Info {get; set;}

}

 

Let's make our example a little bit more complex. Assume that the first section will store the information in PRIMARY file group, and the second - in SECONDARY. Then we should declare partitioning as follows:

 

[PartitionedByRange("Age", RangeBoundaryType.Left, DataFilegroup = "PRIMARY")]

[RangePartitionDescriptor(21, DataFilegroup = "SECONDARY")]

 

Note: You should define all necessary file groups for the database.

 

If we would change the type of borders in out example to RangeBoundaryType.Right, DataObjects.NET would create two partitions: the first one for persons with age less than 21 years, storing the information in the SECONDARY file group, and the second one for others, storing the information in the PRIMARY file group.

 

[PartitionedByRange("Age", RangeBoundaryType.Right, DataFilegroup = "PRIMARY")]

[RangePartitionDescriptor(21, DataFilegroup = "SECONDARY")]

 

List partitioning - the partition is selected based on partitioning expression result matching one of a set of discrete values. Let's create the table having two partitions: the first will store all the rows where birthday month is an odd number, the second one will store the rows where the birthday month is an even one:

 

[PartitionedByList("BirthDay", "Month(@)", SqlType.Int32)]

[ListPartitionDescriptor("1,3,5,7,9,11")]

[ListPartitionDescriptor("2,4,6,8,10,12")]

public abstract class Person: DataObject

{

  public abstract string Name {get; set;}

  public abstract string SecondName {get; set;}

  public abstract string Surname {get; set;}

  public abstract DateTime BirthDay {get; set;}

  public abstract string Info {get; set;}

}

 

Let's note, that we specify both partitioning expression ("Month(@)"), as well as its SQL type (SqlType.Int32). Any appearance of '@' symbol will be replaced by the partitioning column name.

Hash partitioning - partition is selected based on the value of hash function applied to the partitioning expression evaluation result. The result of hash function is a residue of division of partitioning expression result by the count of partitions. So assuming there are four partitions, the hash function will return an integer value from 0 to 3 inclusively. The next example demonstrates hash partitioning usage:

 

[PartitionedByzHash("Age", 4)]

public abstract class Person: DataObject

{

  public abstract string Name {get; set;}

  public abstract string SecondName {get; set;}

  public abstract string Surname {get; set;}

  public abstract int    Age {get; set;}

  public abstract string Info {get; set;}

}

 

Object type partitioning - selects partition by determining if the object type matches one of a set of object types. The following example demonstrates using of object type partitioning:

 

public struct VerySimpleStruct

{

  public int IntValue;

 

  public VerySimpleStruct(int i)

  {

    IntValue = i;

  }

}

 

public abstract class VerySimpleStructCollection : ValueTypeCollection

{

  public VerySimpleStructCollection (System.Type itemType) :

    base(itemType)

  {

  }

}

 

[Abstract]

public abstract class VerySimpleStructCollectionOwner: DataObject

{

  [ItemType(typeof(VerySimpleStruct))]

  [PartitionedByObjectType("Owner", true, DataFilegroup = "PRIMARY")]

  [ObjectTypePartitionDescriptor(null, "SECONDARY", "OwnerA,OwnerB")]

  [ObjectTypePartitionDescriptor("OwnerD")]

  public abstract VerySimpleStructCollection VerySimpleStructs {get;}

}

 

public abstract class OwnerA : VerySimpleStructCollectionOwner

{

}

 

public abstract class OwnerB : VerySimpleStructCollectionOwner

{

}

 

public abstract class OwnerC : VerySimpleStructCollectionOwner

{

}

 

public abstract class OwnerD : VerySimpleStructCollectionOwner

{

}

 

In this example three partitions will be created for the VerySimpleStructs collection table based on value of its actual owner's type (VerySimpleStructCollectionOwner or one of its descendants): one for OwnerA and OwnerB types storing the information in the SECONDARY file group, one for OwnerD type storing the information in default file group (in this case - in the PRIMARY file group), and the default partition for other types storing the information in default file group also.

Using partitioning

Partitioning usage is actually pretty simple:

Domain.Configuration.DatabaseOptions property should include:

DomainDatabaseOptions.EnableEmulatedPartioning option to enable emulated partitioning mode

DomainDatabaseOptions.EnableNativePartioning option to enable native partitioning mode.

Note: Attempting to enable both native and emulated partitioning mode simultaneously will lead to an exception at Domain.Build() execution.

Domain.Configuration.DatabaseOptions property should include DomainDatabaseOptions.StoreReferenceTypes option to allow using reference type partitioning and collection item\owner type partitioning

Define the partitioning for some types or collections (DataObjectCollection or ValueTypeCollection fields) as shown above.

No data loss occurs on changing partitioning options for some class or collection - DataObjects.NET converts old partitions into new ones on the Domain.Build(...) stage.

 

Additional notes:

Translatable/collatable fields cannot be used in partitioning criteria.

Partitioned attribute can't be applied to a paired field, because it has no its own column or table. You should apply the partitioning rule to its pair instead.

Partitioning expression type cannot be SqlType.Unknown, SqlType.GUID, SqlType.Image, SqlType.Binary, SqlType.VarBinary, SqlType.VarBinaryMax or SqlType.Variant.

DataObjectCollection fields partitioning

DataObjectCollection fields supports only object type partitioning. An example:

 

public abstract class Article: DataObject

{

  ...

}

 

public abstract class Magazine: Article

{

  ...

}

 

public abstract class Paper: Article

{

  ...

}

 

public abstract class Book: Article

{

  ...

}

 

public abstract class Author: Person

{

  ...

 

  [Contained]

  [ItemType(typeof(Article))]

  [PartitionedByObjectType("$Item", true, DataFilegroup = "PRIMARY")]

  [ObjectTypePartitionDescriptor(null, "SECONDARY", "Magazine,Paper")]

  [ObjectTypePartitionDescriptor("Book")]

  public abstract DataObjectCollection Articles {get;}

}

 

Notes:

[PartitionedByObjectType] attribute allows to use only two pre-defined constants as parameter:

"$Item" denotes that collection should be partitioned by the collection item type (applicable only for DataObjectCollection fields)

"$Owner" denotes that collection should be partitioned by the collection owner type (applicable for both DataObjectCollection and ValueTypeCollection fields).

It is necessary to set Domain.Configuration.DatabaseOptions property to DomainDatabaseOptions.StoreReferenceTypes value.

ValueTypeCollection fields partitioning

ValueTypeCollection field partitioning doesn't differ much from regular type partitioning - the only exception is that you can use "$Owner" column in partitioning expression. An example:

 

public struct DateTimeStruct

{

  [SqlType(SqlType.VarChar)]

  public DateTime DateTimeValue;

 

  public DateTimeStruct(DateTime value)

  {

    DateTimeValue = value;

  }

}

 

public abstract class DateTimeStructCollection : ValueTypeCollection

{

  public DateTimeStructCollection (System.Type itemType) :

    base(itemType)

  {

  }

}

 

public abstract class DateTimeStructCollectionOwner: DataObject

{

  [ItemType(typeof(DateTimeStruct))]

  [PartitionedByRange("DateTimeValue", "Year(@)", SqlType.Int32,

    RangeBoundaryType.Right)]

  [RangePartitionDescriptor(2000)]

  [RangePartitionDescriptor(2005)]

  public abstract VerySimpleStructCollection VerySimpleStructs {get;}

}

Limitations and pitfalls

Emulated partitioning mode limitations and pitfalls:

No more than 256 partitions can be created for a single class or collection field

Unique indexes would not work as expected - underlying data would be split across several tables, so they'll ensure the uniqueness just in each of these tables. You should use UniqueValidator to ensure the true uniqueness.


 

 

Transparent caching

Overview

DataObjects.NET has 2-level transparent caching architecture:

Each Session object has its own SessionCache

Each Domain object (usually there is just one Domain object) has GlobalCache object.

 

"Transparent caching" term means that presence of caching layers isn't noticeable by effects other then performance. If you turn all of them off, you'll get absolutely the same results, the only effect of this will be performance decrease.

 

The following types are used in all caching layers:

TransactionContext is may be the key type in our caching implementation. TransactionContext instance indicates if some data fetched in the moment when it was created or was active (when Session.TransactionContext property was returning this instance) is still valid (couldn't be potentially changed by other concurrent transactions). You can find more information about this type further.

DataObjectIndentificationInfo (see corresponding type in the DataObjects.NET.Database namespace, if you're interested) is nothing more then ID of DataObject instance. This is a base class for other "holders" of caching information

DataObjectValidationInfo is bucket of ID, VersionID and TransactionContext values (VersionID, TransactionContext + DataObjectIndentificationInfo). Objects of this type are used to validate the DataObjectInstantiationInfo objects - don't focus on it now, we'll explain how it's used further.

DataObjectInstantiationInfo is a bucket of ID, VersionID, TypeID, FastLoadData, Permissions and TransactionContext values (TypeID, FastLoadData, Permissions + DataObjectValidationInfo). Valid instantiation info allows to create the DataObject instance it describes in memory without additional queries.

TransactionContext

As it was mentioned, this is a key type in our caching implementation, so let's study it better. Each TransactionContext instance describes the state of arbitrary data that was fetched in a continuous period of time in the transactional application.

 

You can't create TransactionContext instances manually, as well as modify any already existing instance of it. Active transaction context instance (an instance describing the state of data that can be fetched right now) is exposed by Session.TransactionContext property.

 

Active transaction context of Session changes (i.e. Session.TransactionContext property starts to return another object) when:

New outermost or inner transaction begins

Rollback of any transaction (inner or outer) occurs

Rollback to savepoint occurs

 

Let's imagine we have:

someData - a value of some DataObject property \ a set of such values \ some aggregated info fetched from the database (e.g. count of Animal instances) - an arbitrary data fetched from the database

someDataTransactionContext instance - value of Session.TransactionContext, that was stored before or after the moment when someData was fetched, assuming that it was the same before beginning and in the end of this operation (so Session.TransactionContext change conditions aren't violated in it)

 

What additional information about someData can someDataTransactionContext provide to us? If we'll look on its State property at any arbitrary moment of time, we can find that:

someData is still valid - this mean that it couldn't be changed by a concurrent transaction yet. This means that if we'll try to retrieve someData once more right now (buy the same way), and there were no modifications affecting on someData in our own transaction between these moments of time, we'll get absolutely the same value.

someData belongs to a another outermost transaction (or requires a version check). So it's possible that some other transaction has made a modification affecting on someData, and consequently, an attempt to re-fetch is right now may lead to absolutely different result. But if we remember IDs and VersionIDs of all DataObject instances, which data was "used" in someData "production", we can check if someData is still the same by another way (possibly it's a cheaper way in comparison to re-fetching of someData): we should check if all these (ID, VersionID) pairs are still the same. As you know, DataObjects.NET automatically increases VersionID value on any change made to DataObject instance, so unchanged set of  (ID, VersionID) pairs means that instances with these IDs were unchanged at all, and consequently, any result of combination of their property values is also unchanged. Such version check process is called validation of cached data in DataObjects.NET

someData is dirty - i.e. it was fetched in a transaction (or in a part of transaction) that was rolled back. So if we'll try to re-fetch someData right now, the result can be different, and moreover, it's impossible to use a set of previously stored (ID, VersionID) pairs to validate this data.

 

But what is the relationship between TransactionContext and caching? In fact, TransactionContext classifies any "old" (i.e. simply fetched some time ago) data into 3 classes:

The data that can be considered as "valid" - i.e. if you didn't affect on it in your own transaction, you'll get the same data on an attempt to re-fetch it.

Possibly changed data - this data was fetched in some old, but committed transaction. So other transactions could change it, but you can check this by comparing VersionIDs (validate the old data).

Dirty data. In fact, this is absolutely unusable data.

Since any cached data (a data that is stored in caches) is "old", TransactionContext objects associated with it simply define 3 types of rules of its validation ("no validation", "requires validation", "can't be used at all").

 

Additional comments:

 

Nested TransactionContext TCN (relatively to TransactionContext TC) is any TransactionContext object that was exposed via TransactionContext property of a Transaction TN that was nested into the Transaction T of TransactionContext TC. I.e. TCN is nested into TC when TCN.Transaction.OuterTransaction{.OuterTransaction}==TC.Transaction.

 

TransactionContext TC is dirty (i.e. TC.State==TransactionContextState.Dirty, or the same: TC.IsDirty==true), if it was explicitly marked as dirty, or its outer context is dirty (i.e. TC.OuterContext.IsDirty==true). So if some TransactionContext is dirty, any of its nested contexts is also dirty.

 

Active transaction context of Session S (S.TransactionContext) always exposes S.Transaction.TransactionContext property value except when S.Transaction==null. In this case it exposes a special "always dirty" TransactionContext object.

 

TransactionContext.State change rules (TC is some TransactionContext instance):

TC becomes dirty (TC.IsDirty==true and TC.State==TransactionContextState.Dirty), if its transaction (TC.Transaction) was rolled back, or a rollback to the Savepoint S created in it (S.Transaction==TC.Transaction) occurs (this case was described above).

TC is valid, if it isn't dirty (by rule 1) and TC.Transaction.OutermostTransaction==TC.Session.OutermostTransaction (i.e. an outermost transaction it belongs to is still running).

Otherwise TC.State==TransactionContextState.RequiresVersionCheck (the same as TransactionContextState.BelongsToAnotherOutermostTransaction).

 

So it's easy to check if some cached data is still valid. We should always memorize (i.e. remember a reference to) Session.TransactionContext value while retrieving any data we're going to cache. Let's call our cached data as A, and a TransactionContext is was retrieved in as ATC.

 

Then:

A is valid (i.e. it couldn't be changed by another transaction) if ATC.State== TransactionContextState.Valid. In this case we can use value of A without any additional queries to the database.

A requires a version check (i.e. it's necessary to check versions of all objects this data depends on), if ATC.State==TransactionContextState.RequiresVersionCheck. If A is an aggregate value, usually it's simpler to re-fetch A in this case (and of course memorize a new ATC value: ATC = Session.TransactionContext), rather then compare versions of all objects with previously stored values.

And A should be re-fetched, if ATC.State==TransactionContextState.Dirty.

 

Now let's look closer on the caching layers.

SessionCache

Session cache is a Hashtable-like object (internally it really contains a Hastable) keeping week references to:

All DataObject instances that were accessed in the Session it belongs to

Instantiation info (DataObjectInstantiationInfo objects)

Validation info (DataObjectValidationInfo objects).

 

It's almost obvious why it's necessary to keep week references to all already accessed DataObject instances in SessionCache: a subsequent attempt to get the same object (e.g. by its ID) should return a reference to the same object, otherwise reference equality principle can be violated. Weak references allows to perfectly solve this problem.

 

But how other two types of stored objects are used?

 

Instantiation info is stored simply to delay DataObject instantiation as far as it's possible. Instantiation info objects are "pushed" to the Session cache on RDBMS drivers level - exactly, when driver extracts any instantiation info, it immediately pushes it into the Session cache (it uses IsObjectCached method of Persister class, which in turn the method with the same name, but of SessionCache class).

 

Since TransactionContext object is always associated with instantiation info, it will be possible to use this info further to instantiate the object without any queries when this object will be accessed (by e.g. Session[id] indexer, or by any other possible way).

 

Zero-query instantiation is possible, if:

TransactionContext of Instantiation info is valid

Or there is a Validation info with same ID and VersionID, as well as with valid TransactionContext (this situation is called zero-query validation).

 

Otherwise the only possible way to use this Instantiation info is to validate it by a fast version check query.

 

Remarks:

If DataObjectInstantiationInfo.TransactionContext is dirty, it's impossible to use it at all (no validation is possible)

If validation fails (i.e. either a Validation info with the same ID but different VersionID, and valid TransactionContext is found, or if fast version check query "says" that VersionID of checked instance is changed), it's also impossible to use this Instantiation info.

 

So the only left question now is: how Validation data is used?

 

As it was mentioned, Validation data doesn't allow to instantiate the DataObject instance itself simply because it doesn't contain enough information (there is no even TypeID). But it allows to validate some other Instantiation info - i.e. it allows to "convert" an Instantiation info which TransactionContext is marked as "requiring version check" to an Instantiation info which TransactionContext is marked as "valid" (so it's possible to use this Instantiation info).

 

Let's imagine we have a Validation info with ID=idN - what can we really validate with it?

Most likely it will be an Instantiation info fetched from the Global cache - as you'll understand further, any Instantiation info fetched from the Global cache has a TransactionContext marked as "requiring version check" (this means that any info fetched from the Global cache should be validated before usage).

It can be also an existing DataObject instance itself - as you know, each DataObject instance also belongs to some TransactionContext (see DataObject.TransactionContext property). Session cache may store both a weak reference to the DataObject instance, as well as reference to some Validation info that can be used to validate this instance in case if its TransactionContext is not valid. Actually such validation info is stored in DataObject.useableValidationInfo field.

 

So as you see, Validation info becomes quite useful if Global cache or Session cache contains an Instantiation info for the same object (or DataObject itself), but fetched in some previous outermost transaction. In this case it's possible to validate this data without any additional queries - valid Validation info with same ID and Version ID ensures that this object is still the same in active transaction!

 

Finally, let's explain how Validation info objects are "pushed" to the Session cache - in fact, they're pushed by the same methods of Persister class (Persister.IsObjectCached and Persister.TryCacheIdentificationInfo methods - if you're interested, please refer to e.g. MSSQLPersister.ExecuteAndFetchDataObjects method code to see how they actually invoked) when it processed the result of any query (SqlQuery or Query).

 

May be you know there are two different ways of performing any query in DataObjects.NET:

Executing a query without  "with (LoadOnDemand)" option. In this case full rows from DataObject-related table (usually - from "doDataObject" table) are fetched by the underlying RDBMS driver, and consequently - lots of Instantiation info objects (one per each selected DataObject instance) are "pushed" into the Session cache. Any caching plays zero role in this case, since all necessary data is fetched from RDBMS. The only optimization that works in this case is elimination of FastLoadData extraction from the IDataReader - this part is omitted for a fetched row, if Session cache already has valid Instantiation info or DataObject instance (Persister.IsObjectCached method says if this is true or not).

Executing a query with  "with (LoadOnDemand)" option. In this case only IDs and VersionIDs of selected objects are fetched by the underlying RDBMS driver, and consequently - lots of Validation info objects (one per each selected DataObject instance) are "pushed" into the Session cache. Since it's quite cheap - to fetch just ID and VersionID for each object, this type of query can be executed much faster. But imagine, that Instantiation info for most of these objects are contained in the Global cache - in this case fetched Validation info objects allows Session cache to replace them by validated  Instantiation info objects from the Global cache! That's how Validation info and Global cache help to reduce the client-server throughput and the number of queries.

 

Let's look on the Global cache a bit closer now.

GlobalCache

In contrast to SessionCache, GlobalCache instance is shared between all Sessions operating in the same Domain (usually there is one Domain object per application - except e.g. NLB clustering case).

 

Global cache also differs from the Session cache by other its properties:

It doesn't use weak references, so it doesn't allow garbage collector to decide when to release some cached data - instead decides itself when to do this.

Its Size is limited (see corresponding Domain property). When its CurrentSize reaches this limit, it releases a set of most lately used entries in its table.

Internally it's based on TopDequeue class (internal type in DataObjects.NET) - it's a hybrid between Hashtable and queue. When GlobalCache entry is accessed, it's moved to the top of queue. When cache size limit is reached, a set of tail entries are removed from internal queue to keep it in the specified size limit.

Global cache caches IGlobalCacheItems: in fact, they're DataObjectInstantiationInfo and PropertyInstantiationInfo. So as you see, Global cache can cache not only DataObject instances, but values of [LoadOnDemand] fields and content of collections (DataObjectCollection and ValueTypeCollection fields). Note that Session cache caches these types of data "implicitly" - i.e. it caches references to DataObject instances, which in turn cache all these values (DataObject instances keep every field value they fetch, or a part of it).

Since it's necessary to independently cache not only DataObject instances, but values of their properties, keys other then Int64 (currently represented by PropertyKey objects) are also used in the Global cache to identify such values.

 

DataObjects.NET (actually - Session, SessionCache, DataObject, XxxCollectionImplementation classes) queries the Global cache for data in the following cases:

When any  Instantiation info is needed. In particular, this happens when:

Instance is accessed by any  possible way, e.g. by indexer of Session (Session[id]), getter of persistence property of DataObject type, indexer of DataObjectCollection, etc. - and neither its instance, nor its instantiation data was found for it in SessionCache. It isn't necessarily that SessionCache should contain a Validation info for this instance - Instantiation info (if any) is anyway fetched from the Global cache and put into Session cache (it's quite cheap operation - i.e. it's mainly a reference copying). Further it will be either validated by found and valid Validation info, or by a version check query.

Session.Preload method builds a list of objects having no any Instantiation info at all. This simply means that Session.Preload method is aware of Global cache (is optimized for its presence).

When any [LoadOnDemand] \ DataObjectCollection \ ValueTypeCollection property content is needed. In this case this information is immediately validated (since instance with valid VersionID is already exists at this moment, so it's just necessary to compare its VersionID with the same values stored for property in the Global cache) and used.

 

The data (Instantiation info or property values) is sent to the Global cache immediately when it's fetched, but only when all the following conditions are satisfied:

There were no write operations in the current outermost transaction yet. This condition ensures that Global cache contains only the data that was really valid at some previous moment of time, so it's possible to validate it by VersionID comparison. We can certainly forward any Instantiation info to it, but in this case there is a chance to get dirty Instantiation info (which is modified a transaction, that was rolled back) from the Global cache - and consequently, violate cache transparency condition.

Session.BrowsePast mode isn't turned on (in BrowsePast mode Global cache is in fact unused at all - at least currently it can't cache several versions of the same instance, so we decided to not use it when Session browses some old snapshot of the database).

Dependency tracking

Dependency tracing is a way of increasing the performance of DataObjects.NET caching system. This feature isn't provided for free - when you establish a dependency tracking hierarchy, you always increase the level of concurrency on updates in the storage. But if you'll follow our further recommendations, you'll get only the advantages of doing this.

 

Dependency tracking is in fact a way of informing DataObjects.NET caching system that a whole group of instances (and their [LoadOnDemand] \ collecton properties) should be cached "as whole", but not as a set of independent objects. In this case just one version check query is needed to validate the whole group, rather then one per each instance of such group.

 

To establish your dependency tracking hierarchy, you should just properly override DataObject.DependencyParent property. For each instance it should either return null (by default it does exactly this), or its "dependency parent" - an object which acts as "aggregate" for the group of instances like current one. One "dependency parent" may have its own "dependency parent" as well (i.e. this.DependencyParent.DependencyParent), and so on - but there should be no "rings" in such hierarchies.

 

In may be 95% of cases DependencyParent property, if implemented, should return either null, or SecurityParent property value - since SecurityParent is usually exactly the object we need as DependencyParent (usually it's something like container of the current instance).

 

So how Dependency tracking works?

 

First of all, VersionID of this.dependencyParent is always changed with each change of this.VersionID. Moreover, if there is a chain of DependencyParent objects, all VersionIDs in such chain are changed when the topmost object is updated. Note that the whole set of VersionID changes in such hierarchies are usually flushed only when all delayed updates are flushed, so actual number of queries is much smaller then you may expect.

 

The negative effect of this is that the whole set of such dependent objects starts to act as a single "concurrency endpoint" in this case, since finally any update "reaches" the lowest DependencyParent object in chain, thus it becomes impossible to concurrently (i.e. in two simultaneously running transactions) update two formally independent objects in such hierarchy without getting a concurrency conflict, or delaying of execution of one of such transactions (this depends on actual implementation of transaction isolation in RDBMS - e.g. SQL Server 2000 will most likely delay one of transactions - x-lock on lowest DependencyParent can be given to only one of them).

 

Now let's say about the positive effect: if data for object X is contained in the Global cache, but isn't validated in the current Session yet, and there is an object Y, which is in DependencyParent chain of X, and validation is passed for Y, then we may consider that validation is also passed for X (so this isn't necessary to run a version check query for it).

 

So in fact, it becomes possible to validate any cached data related to all "upper" levels of DependencyParent hierarchy by comparing VersionIDs for any DependencyParent in its "lower" levels. In particular, it's possible to validate the cached data for the whole set of objects which DependencyParents finally lead to some topmostDependencyParent object by comparing just its cached and current VersionIDs!

 

Imagine, you need to perform just one version-check query to validate e.g. 10, 100 or even 1000 objects (i.e. make a decision to use their cached versions without any additional queries)! What a performance increase you may get!

 

This optimization works exactly as follows:

Global cache always remembers the DependencyParent for each cached Instantiation info.

When Session cache wants to validate the Instantiation info for some object (let's say O - and let's imagine we've fetched it from the Global cache), first of all it asks Global cache if it has a DependencyParent chain (since Global cache remembers each DependencyParent, it can reconstruct the whole chain). If there is no chain, all goes as it was described before (i.e. Session cache tries to find necessary Validation info, or validates the Instantiation info by a version check query).

But if there is a DependencyParent chain, Session cache tries to validate the lowest object in this chain by Validation info (i.e. it tries to find a valid Validation info for it and perform VersionID comparison). If succeed, our initial object (O) is considered as valid too. If ValidationInfo isn't found, version check query is performed. If validation fails, it continues to do the same for other DependencyParents in chain (because change of this.DependencyParent.VersionID doesn't necessarily states that this.VersionID was also updated). Finally we'll anyway either validate our initial object, or not - as well as all its dependency parents.

 

Just one this optimization may reduce the number of automatic version check queries by 2..5 times (5...20 version check \ preload queries per transaction), if DependencyParents are defined properly.

 

Note: you should make current object to update its VersionID (e.g. by invoking IncreaseVersion method on it), when DependencyParent changes - but usually this isn't necessary, since DependencyParent reflects some persistent property (so when it's changed, VersionID is increased automatically). May be you remember that SecurityParent property requires nearly the same - you should call SecurityParentChange() method in such cases.

 

But when exactly it's efficient to apply this optimization (override DependencyParent)?

 

General recommendation: for each "dependency tracking set" (a set of objects having the same DependencyParent object in the lowest position of their DependencyParent chains) the following condition should be satisfied: [the number of transactions updating a part of this set] / [the number of transactions reading a part of this set] -> 0 (is close to zero).

 

Obviously this is rather common case, so we decided to explicitly support it. Although it slightly decreases the performance on updates (but only when DependencyParent is overridden), but it also seriously improves the Global cache efficiency for such sets.

 

Practical examples of object sets where dependency tracking can be very effecient:

The whole set of objects related to user or global configuration can be "joined" to such a set, where User (or some GlobalConfiguration instance) they belong to may act as common DependencyParent. All of these objects are quite rarely changed, but some of them are usually should be accessed in each session (e.g. to read the user's GMT offset, etc.).

Product \ ProductProperty objects in some virtual shop: usually product properties aren't changed much more frequently when the product itself, but there can be 20-30 properties per each product - so it will be nice to validate all of them by a single version check query.

Any mostly read-only hierarchies (e.g. all secondary data dictionaries)

When transparent caching doesn't work or is less efficient

As you might notice, transparent caching is fully based on VersionID property. So it doesn't work for all collection or [LoadOnDemand] properties marked by [ChangesTracking(ChangesTrackingMode.Independently)] attribute. Their cached values are unused in the next outermost transaction - it's nearly the same as if version check validation would always fail for them.

 

Global cache becomes read-only after the first write (persist) operation in the active outermost transaction - i.e. no any new data is propagated to it starting from the first write operation and until the end of such transaction (otherwise a dirty data can be potentially propagated into it). So we recommend you to try keeping all data modification operations to the end of outermost transactions.

Buy the way, this is relatively easy to almost fully eliminate this inefficiency by implementing a runtime service that will simply preload N most recently [accessed, but not cached] or [modified] objects when CPU is unused.
IDataObjectEventWatcher and ITransactionEventWatcher interfaces allow you to implement it by your own (if needed), or you may wait for our own implementation J

 

Global cache is unused, when Session works in Browse Past mode.

 


 

Manual caching

DataObjects.NET offers CacheableValue, CacheableCollection and CacheableQuery classes - useful helpers for persistent data caching. These objects "wrap" a value that can be cached, and ensures validity of it on any attempt to read it. Cache validity checks are based on TransactionContext in which cached value was calculated, CalculationTime and ExpirationPeriod.

 

An example:

 

CacheableQuery qr = new CacheableQuery(

    session.CreateQuery("Select Country instances with (Count)"),

    new TimeSpan(1000));

Console.WriteLine("Number of countries: {0}", qr.Count);

Thread.Sleep(500);

Console.WriteLine("Number of countries: {0}", qr.Count); // Is the same anyway here

Thread.Sleep(500);

Console.WriteLine("Number of countries: {0}", qr.Count); // Can be different here      

 

More complex example:

 

public abstract class CIsAMultipliedByB: DataObject

{

  public abstract double A {get; set;}

  public abstract double B {get; set;}

 

  [NotPersistent] // But transactional!

  public virtual double C {

    get {

      UpdateCachedData();

      return cachedC; // C = A*B

    }

  }

 

  private TransactionContext cachedCTransactionContext = null;

  private double cachedC = 0;

 

  [Transactional(TransactionMode.ExistingTransactionRequired)]

  // It's better to use this mode for such methods.

  protected virtual void UpdateCachedData()

  {

    if (cachedCTransactionContext==null ||

        cachedCTransactionContext.State!=TransactionContextState.Valid) {

      cachedC = A*B;

      cachedCTransactionContext = Session.TransactionContext;

    }

  }

 

  protected override void OnPropertyChanged(string name, Culture culture, object value)

  {

    base.OnPropertyChanged(name, culture, value);

    if (name=="A" || name=="B")

      cachedCTransactionContext = null;

  }

}

 


 

Performance optimization

Using performance counters

DataObjects.NET provides a set of performance counters exposing performance-related statistics.

To install DataObjects.NET performance counters, type "InstallUtil.exe <path_To_DataObjects.NET.dll>" in console

Set Domain.EnablePerformanceCounters = true in your application to make DataObjects.NET to maintain their values

Use Perfmon.msc ("Start" -> "Control Panel" -> "Administrative tools" -> "Performance") to study their current values

To uninstall DataObjects.NET performance counters, type "InstallUtil.exe /u <path_To_DataObjects.NET.dll>" in console.

Performance optimization tips

We recommend you to pay attention to the following classes and methods:

Session.Preload(...) allows to significantly reduce the number of version check queries (described in Revision History and Support Forum)

Session.RemoveObjects(...) performs bulk instance removal (described in Revision History)

CacheableValue, CacheableQuery, CacheableCollection allows to implement time span-based data caching (described above)

Pager, QueryPager allows to browse large result sets page-by-page (described in Revision History and Support Forum)

SessionCache type (see Session.Cache property) provides several methods allowing to check if specified object is cached or not

 

Pay attention on the Global cache efficiency:

Install DataObjects.NET performance counters and use Perfmon.msc ("Start" -> "Control Panel" -> "Administrative tools" -> "Performance") to study global cache efficiency statistics. DataObjects.NET offers more then 10 performance counters related to the Global cache.

Domain.GlobalCacheSize controls the size of global cache.

Please study the above chapters describing Global cache itself, especially - "When transparent caching doesn't work or is less efficient"

 

And certainly you should pay attention to all regular common ways of optimizing the performance - properly index your fields, profile and optimize queries, try to access as less data as it's possible (to avoid large number of locks), etc.

 

We recommend you to read these forum threads also:

http://www.x-tensive.com/Forum/viewtopic.php?t=347

http://www.x-tensive.com/Forum/viewtopic.php?t=344

 


 

Adapter component

DataObjects.NET Adapter is a universal tool for exporting DataObject instances into DataSets and importing them back. Adapter is implemented as a .NET component - you can add it to your Visual Studio.NET toolbox. Before executing any fill/update operations Adapter has to be configured.

Adapter configuration

Almost all Adapter configurations may be done in design time mode without writing any code. The main configuration part is mapping creation. On this stage persistent types (classes and interfaces) and collections (DataObjectCollections and ValueTypeCollections) should be mapped to DataTables; properties (fields - in case of ValueTypeCollection) should be mapped to DataColumns. There are two ways of mappings creation:

Manually. First you should choose DataSet which will be used by the Adapter. Then using editors for class mapping items and interface mapping items you can create all necessary mappings. Adapter will help you to do it: it utilizes Visual Studio.NET code model to provide lists of available persistent classes and properties at design time. Of course, it is possible to create all mappings from code without using any designers.

 Automatically. Select Adapter component's context menu "Create mappings" and Adapter will try to create mappings himself. You should stick to some rules while giving names to tables and columns so Adapter can guess correct mappings:

To automatically map a persistent type to a table the name of the table should be equal to the name of the type. For example, to map DataObject type to a table you should name this table "DataObjects.NET.DataObject" or just "DataObject". Another example: giving "IDataObject" name to a table will force Adapter to map IDataObject interface to it.

To automatically map a collection to a table the name of the table should be equal to the name of the collection. For example, to map Role.Principals collection to a table you should name this table "DataObjects.NET.Security.Role.Principals" or just "Role.Principals". The full syntax of the table name is "<collection>[-<culture>]". If culture suffix is specified Adapter will use this culture to access the collection. Otherwise Adapter will use current Session culture to access the collection.

To automatically map a property to a column the name of the column should be equal to the name of the property. Culture suffix is also available here. If a property is of structure type it's possible to specify a field of a structure, a subfield of a field and so on. The full syntax of the column name is "<property>[-<culture>][.<field1>[.<field2>[...]]]". For example, if you'll name a column "TypeID-En" and the table is mapped to a DataObject type Adapter will map "TypeID-En" column to the DataObject.TypeID property and "En" culture will be used by Adapter to get/set TypeID property while Fill/Update operation.

To automatically map a field of the ValueTypeCollection item to a column the name of the column must satisfy the following syntax: "<field1>[.<field2>[.<field3>[...]]]".

 

Note: Automatic mappings generation is also available at runtime - just call Adapter.GenerateMappings() method.

 

You can combine these two methods while creating mappings. For example, first run automatic mappings creation process then manually correct created mappings and add or remove some if necessary.

 

It's possible to map a column to a "virtual" property that actually isn't exists. Adapter generates GetProperty event while getting any property value and SetProperty event while setting any property value. Creating handlers for these events you can specify the behavior of any "virtual" property; also you can make any "corrections" for "non-virtual" property values.

 

Another interesting feature is a mapping of descendant type properties. For example, you maps DataObject type to a table and you maps "Name" column to the "Name" property. As you know DataObject class doesn't contain a definition for the "Name" property but DataObjects.NET.Security.User class (Security.User class is of course a descendant of the DataObject class) contains a property "Name". So Security.User instances will successfully fill "Name" column.

 

It's impossible to create mappings for "virtual" properties and descendant type properties using automatic mappings generator. Moreover automatic mappings generator will try to remove such mappings.

 

For Fill/Update actions Adapter requires some indexes and constraints on a DataSet to be created. The best way to create them is to use context menu item "Prepare DataSource" or call Adapter.PrepareDataSource() method from code. Adapter will make some changes to the DataSet:

In tables mapped to a class or an interface Adapter creates "ID" column (if it doesn't exist), makes it of type Int64 and makes this column a primary key column.

In tables mapped to DataObjectCollection Adapter creates "ID-1" and "ID-2" columns (if they don't exist), makes them of type Int64 and makes these columns a primary key columns.

In tables mapped to ValueTypeCollection Adapter creates "ID" and "Owner" columns (if they don't exist), makes them of type Int64 and makes these columns a primary key columns.

 

Note: you should not process these columns manually or create any mappings to them.

Fill operation

Adapter uses FillQueue that contains DataObject instances which are to be exported to a DataSet, and continues fill operation while FillQueue is not empty. Extracting next item from the FillQueue it processes the following steps:

It determines the type of the extracted DataObject instance.

Analyzing mappings it determines the DataTable which corresponds to the instance type; also it determines all DataTables that are mapped to the types which are ancestors of the instance type or mapped to the interfaces implemented by the instance type. As a result Adapter obtains a set of DataTables which are to be filled using current DataObject instance.

For the extracted DataObject instance and for each DataTable corresponded to this DataObject instance Adapter starts fill operation:

It creates a new row and fills "ID" column.

It fills all columns that are mapped to the properties.

If a column is mapped to a reference property Adapter fills this column with the ID of the referred DataObject instance and queues it to the FillQueue (in case when this object was not processed by the Adapter before). Before new DataObject instance is placed into the FillQueue Adapter generates EnqueueObject event. EnqueueObject event handler can prevent Adapter to queue the object.

Adapter determines DataTables mapped to any DataObjectCollections of the current instance. Then Adapter fills these tables using corresponding collections. All tables mapped to DataObjectCollections have similar structure - "ID-1" column represents the owner of the collection, "ID-2" column represents the contained object. If the contained object was not processed before Adapter will queue it to the FillQueue.

Adapter determines DataTables mapped to any ValueTypeCollections of the current instance. This DataTables are filled in much the same way.

 

When the FillQueue is empty Adapter stops filling process and commits all changes made to the DataSet (it calls DataSet.AcceptChanges() method). Fill process is fully transactional - Adapter ensures that even DataSet won't be changed in case if rollback occurs.

Update operation

Update operation is much more complex than Fill operation. Adapter performs Update in several stages:

Creation of new DataObjects instances. On this stage Adapter analyzes all tables mapped to classes and interfaces and collects rows that correspond to DataObjects which are to be created. It is consider that a row corresponds to the DataObject that is to be created if the "ID" column contains negative value. This stage is divided into the following steps:

Adapter finds negative ID that has not been processed before.

Adapter obtains all tables (mapped to classes and interfaces) that have rows with this negative ID. The set of these rows contains property values for object that will be created.

Analyzing rows Adapter determines the type of the object (it uses TypeID columns in case if they exist and information about types that are mapped to the corresponding tables).

Next Adapter generates CreateObject event suggesting a type for new object. CreateObject event handler can do the following: create new object himself, correct a type and make Adapter to automatically create an object, delay object creation (in this case Adapter will attempt to create this object after all other objects creation), cancel this object creation. If CreateObject event handler is not set Adapter will do all decisions about new object type and creation of the object himself.

It is allowed to have negative IDs not only in "ID" columns but all columns mapped to reference properties, columns in tables mapped to DataObjectCollections and columns in tables mapped to ValueTypeCollections may contain negative IDs. To rule such situation Adapter builds internal map between negative IDs and real IDs of newly created DataObject instances.

Updating. Adapter builds the list of IDs of DataObject instances which are to be updated. Then Adapter starts to update objects one by one. Updating of a single object is processed using the following algorithm:

Using mappings Adapter determines a set of tables that could contain rows for current object update.

Adapter builds a priority schema for tables from this set. Changes in table with lower priority could be overridden by changes in table with higher priority. Priorities are built according to the type hierarchy. If class T1 is a descendant of class T2 then T1 has bigger priority than T2.  Classes have bigger priorities than interfaces. All interfaces have the same priority (their order i