1 2 3 4 5 6 7 8 9 10 11
Quino: partially-mapped queries

In Quino: an overview of query-mapping in the data driver we took a look at some of the basics of querying data with Quino while maintaining acceptable performance and memory usage.

Now we'll take a look at what happens with partially-mapped queries. Before explaining what those are, we need a more concrete example to work with. Here's the most-optimized query we ended up with in the previous article:

var query = Session.GetQuery<Person>();
query.Join(Person.Relations.Company).WhereEqual(Company.Fields.Name, "IBM");

Assert.That(Session.GetCount(query), Is.GreaterThanEqual(140000));

With so many entries, we'll want to trim down the list a bit more before we actually create objects. Let's choose only people whose last names start with the letter "M".

var query = Session.GetQuery<Person>();
query
  .Where(Person.Fields.LastName, ExpressionOperator.StartsWith[^1], "M")
  .Join(Person.Relations.Company).WhereEqual(Company.Fields.Name, "IBM");

Assert.That(Session.GetCount(query), Is.Between(100, 120));

This is the kind of stuff that works just fine in other ORMs, like Entity Framework. Where Quino goes just a little farther is in being more forgiving when a query can be only partially mapped to the server. If you've used EF for anything beyond trivial queries, you've surely run into an exception that tells you that some portion of your query could not be mapped.1

Instead of throwing an exception, Quino sends what it can to the database and uses LINQ to post-process the data sent back by the database to complete the query.

Introducing unmappable expressions

Unmappable code can easily sneak in through aspects in the metadata that define filters or sorts using local methods or delegates that do not exist on the server. Instead of building a complex case, we're going to knowingly include an unmappable expression in the query.

var query = Session.GetQuery<Person>();
query
  .Where(new DelegateExpression[^3](c => c.GetObject<Person>().LastName.StartsWith("M")[^4])
  .Join(Person.Relations.Company).WhereEqual(Company.Fields.Name, "IBM");

Assert.That(Session.GetCount(query), Is.Between(100, 120));

The new expression performs the same check as the previous example, but in a way that cannot be mapped to SQL.2 With our new example, we've provoked a situation where any of the following could happen:

  • The ORM could throw up its hands and declare the query unmappable, pushing the responsibility for separating mappable from unmappable onto the shoulders of the developers. As noted above, this is what EF does.
  • The ORM could determine that the query is unmappable and evaluate everything locally, retrieving only the initial set of Person objects from the server (all several million of them, if you'll recall from the previous post).
  • The ORM could map part of the query to the database, retrieving the minimal set of objects necessary in order to guarantee the correct result. This is what Quino does. The strategy works in many cases, but is not without its pitfalls.

What happens when we evaluate the query above? With partial mapping, we know that the restriction to "IBM" will be applied on the database. But we still have an additional restriction that must be applied locally. Instead of being able to get the count from the server without creating any objects, we're now forced to create objects in memory so that we can apply the local restrictions and only count the objects that match them all.

But as you'll recall from the previous article, the number of matches for "IBM" is 140,000 objects. The garbage collector just gave you a dirty look again.

Memory bubbles

There is no way to further optimized this query because of the local evaluation, but there is a way to avoid another particularly nasty issue: memory bubbles.

What is a memory bubble you might ask? It describes what happens when your application is using nMB and then is suddenly using n + 100MB because you created 140,000 objects all at once. Milliseconds later, the garbage collector is thrashing furiously to clean up all of these objects -- and all because you created them only in order to filter and count them. A few milliseconds after that, your application is back at nMB but the garbage collector's fur is ruffled and it's still trembling slightly from the shock.

The way to avoid this is to stream the objects through your analyzer one at a time rather than to create them all at once. Quino uses lazily-evaluated IEnumerable<T> sequences throughout the data driver specifically to prevent memory bubbles.

Streaming with IEnumerable<T> sequences

Before tackling how the Quino ORM handles the Count(), let's look at how it would return the actual objects from this query.

  • Map the query to create a SELECT statement
  • At this point, it doesn't matter whether the entire query could be mapped
  • Create an IEnumerable<T> sequence that represents the result of the mapped query
  • At this point, nothing has been executed and no objects have been returned
  • Wrap the sequence in another sequence that applies all of the "unhandled" parts of the query
  • Return that sequence as the result of executing the query
  • At this point, we still haven't actually executed anything on the database or created any objects

Right, now we have an IEnumerable<T> that represents the result set, but we haven't lit the fuse on it yet.

How do we light the fuse? Well, the most common way to do so is to call ToList() on it. What happens then?

  • The IEnumerator<T> requests an element
  • The query is executed against the database and returns an IDataReader
  • The reader requests a row and creates a Person object from that row's data
  • The wrapper that performs the local evaluation applies its filter(s) to this Person and yields it if it matches
  • If it matched the local filters, the Person is added to the list
  • Control returns to the IDataReader, which requests another row
  • Repeat until no more rows are returned from the database

Since the decision to add all objects to a list occurs all the way at the very outer caller, it's the caller that's at fault for the memory bubble not the driver.3 We'll see in the section how to avoid creating a list when none is needed.

Using cursors to control evaluation

If we wanted to process data but perhaps offer the user a chance to abort processing at any time, we could even get an IDataCursor<T> from the Quino ORM so control iteration ourselves.

**using** (**var** cursor = Session.CreateCursor(query))
{
  **foreach** (**var** obj **in** cursor)
  {
    // Do something with obj

    **if** (userAbortedOperation) { **break**; }
  }
}

And finally, the count query

But back to evaluating the query above. The Quino ORM handles it like this:

  • Try to map the query to create a COUNT statement
  • Notice that at least one restriction could not be mapped
  • Create a cursor to SELECT all of the objects for the same query (shown above) and count all the objects returned by that instead

So, if a count-query cannot be fully mapped to the database, the most efficient possible alternative is to execute a query that retrieves as few objects as possible (i.e. maps as much to the server as it can) and streams those objects to count them locally.

Tune in next time for a look at how to exert more control with limit and offset and how those work together with partial mapping.



  1. If we were worried that the last names in our database might not necessarily be capitalized, we would use the ExpressionOperator.StartsWithCI to perform the check in a case-insensitive manner instead.

  2. If Quino had a LINQ-to-SQL provider, there's a chance that more of these delegates could be mapped, but we don't have one...and they can't.

  3. Did we still create 140,000 objects? Yes we did, but not all at once. Now, there are probably situations where it is better to create several objects rather than streaming them individually, but I'm confident that keeping this as the default is the right choice. If you find that your particular situation warrants different behavior, feel free to use Session.CreateCursor() to control evaluation yourself and create the right-sized batches of objects to count. The ChangeAndSave() extension method does exactly that to load objects in batches (size adjustable by an optional parameter) rather than one by one.

An introduction to query-mapping in the ORM

One of the most-used components of Quino is the ORM. An ORM is an Object-Relational Mapper, which accepts queries and returns data.

  • Applications formulate queries in Quino using application metadata
  • The ORM maps this query to the query language of the target database
  • The ORM transforms the results returned by the database to objects (the classes for which were also generated from application metadata).

This all sounds a bit abstract, so let's start with a concrete example. Let's say that we have millions of records in an employee database. We'd like to get some information about that data using our ORM. With millions of records, we have to be a bit careful about how that data is retrieved, but let's continue with concrete examples.

Attempt #1: Get your data and refine it locally

The following example returns the correct information, but does not satisfy performance or scalability requirements.1

var people = Session.GetList<Person[^2]>().Where(p => p.Company.Name == "IBM");

Assert.That(people.Count(), Is.GreaterThanEqual(140000));

What's wrong with the statement above? Since the call to Where occurs after the call to GetList<Person>(), the restriction cannot possibly have been passed on to the ORM.

The first line of code doesn't actually execute anything. It's in the call to Count() that the ORM and LINQ are called into action. Here's what happens, though:

  • For each row in the Person table, create a Person object
  • For each person object, create a corresponding Company object
  • Count all people where the Name of the person's company is equal to "IBM".

The code above benefits from almost no optimization, instantiating a tremendous number of objects in order to yield a scalar result. The only side-effect that can be considered an optimization is that most of the related Company objects will be retrieved from cache rather than from the database. So that's a plus.

Still, the garbage collector is going to be running pretty hot and the database is going to see far more queries than necessary.2

Attempt #2: Refine results on the database

Let's try again, using Quino's fluent querying API.3 The Quino ORM can map much of this API to SQL. Anything that is mapped to the database is not performed locally and is, by definition, more efficient.4

var people = Session.GetList<Person>();
people.Query.Join(Person.Relations.Company).WhereEqual(Company.Fields.Name, "IBM");[^6]

Assert.That(people.Count, Is.GreaterThanEqual(140000));

First, we get a list of people from the Session. As of the first line, we haven't actually gotten any data into memory yet -- we've only created a container for results of a certain type (Person in this case).

The default query for the list we created is to retrieve everything without restriction, as we saw in the first example. In this example, though, we restrict the Query to only the people that work for a company called "IBM". At this point, we still haven't called the database.

The final line is the first point at which data is requested, so that's where the database is called. We ask the list for the number of entries that match it and it returns an impressive number of employees.

At this point, things look pretty good. In older versions of Quino, this code would already have been sufficiently optimized. It results in a single call to the database that returns a single scalar value with everything calculated on the database. Perfect.

Attempt #3: Avoid creating objects at all

However, since v1.6.0 of Quino5, the call to the property IDataList.Count has automatically populated the list with all matching objects as well. We made this change because the following code pattern was pretty common:

var list = Session.GetList<Person>();
// Adjust query here
if (list.Count > 0)
{
  // do something with all of the objects here
}

That kind of code resulted in not one, but two calls to the database, which was killing performance, especially in high-latency environments.

That means, however, that the previous example is still going to pull 14,000 objects into memory, all just to count them and add them to a list that we're going to ignore. The garbage collector isn't a white-hot glowing mess anymore, but it's still throwing you a look of disapproval.

Since we know that we don't want the objects in this case, we can get the old behavior back by making the following adjustment.

var people = Session.GetList<Person>();
people.Query.Join(Person.Relations.Company).WhereEqual(Company.Fields.Name, "IBM");

Assert.That(Session.GetCount(people.Query), Is.GreaterThanEqual(140000));

It would be even clearer to just forget about creating a list at all and work only with the query instead.

var query = Session.GetQuery<Person>();
query.Join(Person.Relations.Company).WhereEqual(Company.Fields.Name, "IBM");

Assert.That(Session.GetCount(query), Is.GreaterThanEqual(140000));

Now that's a maximally efficient request for a number of people in Quino 1.10 as well.

Tune in next time for a look at what happens when a query can only be partially mapped to the database.


There are different strategies for retrieving associated data. Quino does not yet support retrieving anything other than root objects. That is, the associated Company object is not retrieved in the same query as the Person object.

In the example in question, the first indication that the ORM has that a Company is required is when the lambda retrieves them individually. Even if the original query had somehow indicated that the Company objects were also desired (e.g. using something like Include(Person.Relations.Company) as you would in EF), the most optimal mapping strategy is still not clear.

Should the mapper join the company table and retrieve that highly redundant data with each person? Or should it execute a single query for all companies and prime a cache with those? The right answer depends on the latency and bandwidth between the ORM and the database as well as myriad other conditions. When dealing with a lot of data, it's not hard to find examples where the default behavior of even a clever ORM isn't maximally efficient -- or even very efficient at all.

As we already noted, though, the example in question does everything in memory. If we reasonably assume that the people belong to a relatively small number of companies -- say qc -- then the millions of calls to retrieve companies associated with people will result in a lot of cache hits and generate "only" qc + 1 queries.


  1. I suppose it depends on what those requirements are, but if you think your application's performance requirements are so loose that it's OK to create millions of objects in memory just in order to count them, then you're probably not in the target audience for this article.

  2. Quino does not have LINQ to SQL support. I'm not even going to write "yet" at the end of that sentence because it's not at all clear that we're ever going to have it. Popular demand might convince us otherwise, but for now we're quite happy with our API (and soon-to-be-revealed query language QQL).

  3. That's an assumption I'm going to make for which counterexamples certainly exist, but none of which apply to the simple examples we'll address in this article.

  4. That was in almost three years ago, in June of 2011.

v1.10.0: .NET 4.5, MVC5, JSON remoting; improved NuGet and Windows services support

The summary below describes major new features, items of note and breaking changes. The full list of issues is also available for those with access to the Encodo issue tracker.

Highlights

Breaking changes

Utilities

  • PathTools.Canonicalize() has been replaced with FileTools.Canonicalize().
  • ICredentials.Password no longer exists; instead, use ICredentials.PasswordHash.
  • The ReversibleEncryptionProvider no longer exists; instead, use the RijndaelEncryptionProvider.
  • UrlParts.Protocol no longer exists; instead, use UrlParts.Scheme.
  • DirectoryServicesSettings.ServerUri no longer exists; instead, use DirectoryServicesSettings.HostName.

Configuration

  • The extension method CoreConfigurationTools.Integrate() no longer exists; instead, use ConfigurationTools.Add(). If you still need to clear existing packages, call StartupActions.Clear() and ShutdownActions.Clear() before adding the package.
  • MetaConfigurationTools.IntegrateMongoLoopbackDatabase(), MetaConfigurationTools.IntegrateLocalDatabase and MetaConfigurationTools.IntegrateLoopbackDatabase() no longer exist. Instead, the MongoLoopbackDatabase is the default provider registered for the ILoopbackDatabase interface. If the configuration of an application requires a loopback database, the version provided by the ServiceLocator will provide it. Override that registration to change the type of database to use.
  • The class Encodo.Quino.Tools.DataGenerator has been moved to the Encodo.Quino.Models.Tools.DataGenerators namespace.

Metadata/Builders/Modules

  • MetaBuilderBase.ApplyGenerators() no longer exists; instead, use MetaBuilderBase.Commit().
  • The attribute Encodo.Quino.Methods.AspectsBaseAttribute has been replaced with Encodo.Quino.Meta.Aspects.AspectProviderAttributeBase.
  • The interface Encodo.Quino.View.Aspects.IViewColorAspect and class ViewColorAspect have been replaced with the Encodo.Quino.View.Aspects.IViewAppearanceAspect and ViewAppearanceAspect, respectively.

Reporting

  • The class Encodo.Quino.Models.Reports.ReportsModuleGenerator has been moved to the Encodo.Quino.Models.Reports.Generators namespace.
  • The aspect Encodo.Quino.Models.Reports.ReportContextVisibleAspect has been moved to the Encodo.Quino.Models.Reports.Aspects namespace.

Security

  • The aspect Encodo.Quino.Models.Security.SecurityContextVisibleAspect has been moved to the Encodo.Quino.Models.Security.Aspects namespace.
  • The aspect Encodo.Quino.Models.Security.SecurityModuleSecurityClassAspect has been moved to the Encodo.Quino.Models.Security.Aspects namespace.
  • The class Encodo.Quino.Models.Security.StandardSecurityModuleAccessControl has been moved to the Encodo.Quino.Models.Security.Logic namespace.
  • The class Encodo.Quino.Security.LoginTokenBasedLoginAuthenticator has been replaced with the Encodo.Quino.Models.Security.Login.SecurityModuleTokenBasedAuthenticator.
  • The class Encodo.Quino.Security.UniqueIdentifierBasedAuthenticator has been replaced with the Encodo.Quino.Models.Security.Login.SecurityModuleUniqueIdentifierBasedAuthenticator.

Data providers

  • PostgreSqlMetaDatabase no longer has a constructor that accepts a string to name the database. The name is always taken from the ConnectionSettings.Name.
  • Database.RefreshState() and Database.DefaultConnectionSettings no longer exist; instead, use Database.RefreshDetails() and Database.ConnectionSettings, respectively.
  • Extension method MetaApplicationTools.GetQueryableDatabases(this IMetaApplication) has been replaced with extension method DataProviderTools.GetDatabaseHandlers(this IDataProvider).
  • Extension method MetaConfigurationTools.GetQueryableDatabases(this IMetaApplication) has been replaced with extension method DataProviderTools.GetDatabaseHandlers(this IDataProvider).
  • The IDataConnection.SyncRoot object no longer exists. Connections should never be shared between threads. Instead of explicit locking, applications should use an IPooledDataConnectionManager to maximize sharing of connections between threads.
  • The method IPersistable.SetStored() no longer exists; instead, use SetState().
  • IPersistentObjectContext no longer exists; instead, use IPersistentObjectContext.InitialObjectState.
  • The methods IMetaObjectHandler.GetObjectStored(), IMetaObjectHandler.GetObjectDelete(), IMetaObjectHandler.GetObjectChangedOrNew() and IMetaObjectHandler.SetObjectStored() no longer exist; instead, use some combination of IMetaObjectHandler.GetObjectState(), IMetaObjectHandler.GetObjectChanged() and IMetaObjectHandler.SetObjectState().
  • The NullCache no longer exists; instead, register an IDataCacheFactory with the ServiceLocator that returns a DataCache without any providers defined or has the property Enabled set to false.
  • The constant MetaActionNames.DetermineDatabaseState has been replaced with MetaActionNames.DetermineDataProviderConnectivity.

Remoting

  • The attribute Encodo.Quino.Methods.MetaMethodParameterInterceptorAspect has been replaced with Encodo.Quino.Methods.Aspects.MethodParameterInterceptorAspectBase.
  • The extension method MetaConfigurationTools.IntegrateRemoting() no longer exists. Remoting is now automatically configured to use the HttpRemoteClientFactory. To override this default behavior, use the ServiceLocator to register a different implementation for the IRemoteClientFactory.
  • MetaConfigurationTools.SetupForRemotingServer() no longer exists; instead, use MetaConfigurationTools.ConfigureAsRemotingServer().
  • The SetUpRemotingAction and MetaActionNames.SetUpRemoting constant no longer exist. Instead, the IRemoteClientFactory is configured in the ServiceLocator; override that registration to use a different implementation.
  • The constant RemoteServerDataHandler.DefaultName and method RemoteServerDataHandler.GetName() no longer exist; instead, use the name of the driver, which is derived from the name in the connection settings for that driver.
  • The interface Encodo.Quino.Remoting.Client.IRemoteMethodCaller has been moved to the Encodo.Quino.Remoting.Methods namespace.
  • The class RemoteClientFactoryAuto no longer exists. Remoting is now automatically configured to use the HttpRemoteClientFactory. To override this default behavior, use the ServiceLocator to register a different implementation for the IRemoteClientFactory.
  • HttpRemoteHost and RemoteHostHttp no longer exist; instead, use Encodo.Quino.Remoting.Http.MetaHttpRemoteHost.

Winform

  • WinformDxMetaConfigurationTools.IncludeSettings() has been replaced with the extension method WinformDxMetaConfigurationTools.IncludeWinformDxSettings().
  • WinformDxMetaConfigurationTools.ConfigureDefaults() has been replaced with the extension method WinformDxMetaConfigurationTools.IntegrateWinformDxPackages().
  • The method ControlToolsDX.FindProperty() no longer exists; instead, use the method ControlsToolsDX.TryGetProperty().
v1.9.4: Bug fixes for 1.9.3 -- Reporting fixes and improvements

The summary below describes major new features, items of note and breaking changes. The full list of issues is also available for those with access to the Encodo issue tracker.

Highlights

  • QNO-4436: Generated objects can now hook events directly, instead of being forced to use aspects
  • QNO-4435: MetaBindingList no longer implicitly saves deleted objects
  • QNO-4434: Reporting: import for multiple reports works again (only the first report was imported)

Breaking changes

  • None
v1.9.3: Bug fixes for 1.9.2 -- Reporting fixes and improvements

The summary below describes major new features, items of note and breaking changes. The full list of issues is also available for those with access to the Encodo issue tracker.

Highlights

Breaking changes

  • None
Frans Bouma (founder/developer of LLBLGen) "discovers" Quino

Encodo Systems AG started work on its metadata framework Quino in late 2007. We've used it successfully in many projects, from Windows desktop applications to standalone servers, Windows services and web sites. It has grown considerably since its inception and the core concept of keeping the focus of an application on its metadata has stood the test of time quite well.

The recent article Code-first O/R mapping is actually rather silly by Frans Bouma recounts how the lead developer and architect of another popular ORM LLBLGen Pro, has also recently "discovered" the benefits of the metadata-first approach.

He writes,

Starting with code in the form of entity classes is equally odd as starting with a table: they both require reverse engineering to the abstract entity definition to create the element 'on the other side': reverse engineer[ing] the class to the abstract entity definition to create a table and the mappings is equal to reverse engineering a table to a class and create the mappings. [T]he core issue is that if you start with a class or a table, you start with the end result of a projection of an abstract entity definition [...]

What if that abstract entity definition which was used to create the class or table was in a model which contained all of the domain types for the domain used in the software to build? [...] it would give a couple of benefits: you can create overviews of the model and more importantly, changes in the domain can be applied directly into the model which then ripple through to classes and tables in the right form [...] (Emphasis added.)

He describes the core tenets of Quino rather well: starting with the metadata avoids diluting the domain model with the limitations of a projection domain (classes, tables, etc.). This has been borne out by our experience working in exactly this manner for the last several years.

The ORM in Quino is only a satellite component that leverages the centrally defined metadata just as many other components do. The programmer defines the metadata of the domain model and Quino provides tools to do many tasks automatically:

  1. Create or update a database schema
  2. Generate business-logic classes (primarily C# but it could be JavaScript or indeed any target language where code would benefit from strongly typed domain classes)
  3. Select/create/update/delete data in one or more databases
  4. Generate standardized user interfaces for multiple platforms
  5. Integrate with reporting engines and designers
  6. Generate UML diagrams
  7. ...and much more...

I heartily encourage Frans to continue thinking in this direction. He will be rewarded greatly for it.

v1.9.2: Bug fixes for 1.9.1 -- Reporting fixes and improvements

The summary below describes major new features, items of note and breaking changes. The full list of issues is also available for those with access to the Encodo issue tracker.

Highlights

Breaking changes

  • None
v1.9.1: Bug fixes for 1.9 -- WAN speed, deadlocks and connection pooling

The summary below describes major new features, items of note and breaking changes. The full list of issues is also available for those with access to the Encodo issue tracker.

Highlights

  • QNO-4201: Improved WAN speed by adding connection-pooling to avoid high connection setup costs
  • QNO-4233, QNO-4239, QNO-4241: Fixed some deadlock and connection-exhaustion issues in more complex models/data structures
  • QNO-4260: Upgraded DevExpress component library to v13.1
  • QNO-4246, QNO-4247: Made several improvements to object-graph management to improve identity/reference handling
  • QNO-4270, QNO-4278: Made some schema-migration improvements to support more conversions and constraint/path types

Breaking changes

  • Removed RemoteMethodCallException and added CreatePayloadException
v1.9: Plugins, model overlays and more!

The summary below describes major new features, items of note and breaking changes. The full list of issues is also available for those with access to the Encodo issue tracker.

Highlights

  • Plugins: You can now extend your Quino applications with plugins. The application can determine the locations from which plugins are loaded but the default behavior is to load plugin assemblies from a folder next to the executable as well as from a folder in the user's local configuration folder. Also by default, if the application is running in debug mode, C# files in those directories are compiled and loaded as plugins as well. This allows developers or demonstrators in the field to update the metadata without using Visual Studio to recompile the application.
  • Software Update: The software updater component now supports HTTP as well as file-shares and downloads the file locally before executing it, showing download progress for slower connections or larger updates.
  • Remoting Routes: QNO-4060: Routes/URLs on the remote server are now resolved using a registry of route handlers. An application is free to replace or enhance the standard route handlers or add new ones. Testing support remoting has been improved (i.e. configuration of a loopback remoting server has been vastly simplified and encapsulated in a RemoteTextMixinBase).
  • Data sources: QNO-4048, QNO-3628, QNO-4034, QNO-4037, QNO-4051, QNO-4139, QNO-4140, QNO-4071: Multiple remote data sources are now supported throughout the configuration, startup, login and application.
  • Optimistic locking: QNO-4049: Added aspects to include optimitic-locking support for meta-classes
  • Change tracking: QNO-4050: Added aspects to include change-tracking support for meta-classes
  • Model names: QNO-4152, QNO-4156: Improved support for models with long(er) names (in particular, schema migration now works as expected)
  • VS Project template: QNO-4149, QNO-4150, QNO-4151: Improved the Quino project/solution Visual Studio 2012 template
  • Error messages: QNO-4081, QNO-4166: Improved error messages for data-source errors and improved the API for formatting error user-facing messages
  • Remoting Driver: QNO-4075, QNO-4080, QNO-4068, QNO-4060, QNO-4059, QNO-4056, QNO-4037: Made many improvements and optimizations to the remoting data driver
  • Caching: QNO-140, QNO-3809, QNO-3808: Made several improvements to caching API, including new default providers for the default DataCache.
  • Many-to-many Relations: QNO-4219: Added a QueryAspect to show read-only views of many-to-many relations
  • Object comparison: QNO-4027: GenericObject should be comparable to other GenericObjects or PK values

Breaking changes

  • QNO-4026: IMetaReadable.GetValue() behavior is inconsistent with relations and methods. The behavior or reading a related object or list using GetValue() has changed. Previously, the value returned non-null only if it had been caused to load by using GetObject() or GetList()) or if it had a ValueGenerator. If it was generated with a ValueGenerator, it was always regenerated, ignoring the value of ValueGenerationFrequency.
  • QNO-4028: GenericObject should behave correctly when adding to a HashSet. Previously, the hash-code generated for GenericObjects was not well-matched to the result of the Equals() method, which resulted in unpredictable behavior with hash tables, sets or dictionaries. This has been fixed, but applications that relied on the formerly unpredictable behavior will now function differently.
  • The enum DatabaseState no longer exists. It has been replaced with ExternalResourceState.
  • The IAuthenticator<TApplication>, IAuthorizer<TApplication> and ILoginValidator<TApplication> interfaces have all acquired a method with interface void LoadSettings(TConfiguration configuration, IMessageRecorder recorder).
  • Encodo.Data.SessionFlags have been moved to Encodo.Quino.Data.Persistence.SessionFlags
  • The signature of ICoreConfiguration.GetConnectionDetails()no longer returns a simple string, but a sequence of strings. To restore , use string.Join("; "_configuration.GetConnectionDetails())
  • EnryptionType was moved from Encodo.Security to Encodo.Encryption
  • DirectoryServiceSettings.ServerUri is deprecated; use HostName instead.
  • MetaBuilderModuleTools has been renamed to MetaBuilderTools.
  • MetaBuilderBasedMetadataGeneratorBase.ConfigureBuilder() no longer exists; instead, override MetaBuilderBasedMetadataGeneratorBase.RegisterDependencies().
  • MetaBuilder.AddMultiLanguageProperty() now requires a base Guid parameter.
  • MetaBuilder.AddWrapperClass() no longer requires a Guid parameter
v1.8.7: Bug fix release for v1.8.6 (no new features)

The summary below describes major new features, items of note and breaking changes. The full list of issues is also available for those with access to the Encodo issue tracker.

Highlights

  • QNO-4196: Reporting does not preview and print the correct font (for some fonts and only under Windows 8)
  • QNO-3779: Add support for basic connection-pooling; remoting servers share a server-side connection pool; clients use a connection pool per session by default
  • QNO-4204, QNO-4203: Data statistics show wrong timing information and got rid of ShortCircuit event type (replaced with primary data handler)
  • QNO-4179: One to one relations do not show the correct object on the primary key class

Breaking changes

No known breaking changes