Serenity 6.2.6 Release Notes (2022-10-18)

Descending Columns Sorted First When Multiple Sort Orders Are Defined (fixed in 6.2.5)

Previously, when multiple sorted columns were defined in Column.cs, the sorting order behaved unexpectedly. For instance:

public class SomeColumns
{
    [SortOrder(1)]
    public string Column1 { get; set; }
    [SortOrder(2, descending: true)]
    public string Column2 { get; set; }
}

The grid was expected to be sorted by Column1 followed by Column2 DESC, but it was the opposite: Column2 DESC followed by Column1.

This issue was caused by the way sort orders were passed to the client side, where descending orders were represented as negative values (e.g., Column2 had a sort order of -2).

This has been fixed by introducing a simple Math.abs() call during the initial sort order calculation (thanks to @RaportTrap, e.g. A. Schlögelhofer).

Published tsbuild Helpers as @serenity-is/tsbuild Npm Package

The node script file tsbuild.js, responsible for calling esbuild to compile modular-style TypeScript code, has now been distributed as an npm package. tsbuild.js now imports that package, allowing for easier updates and fixes when necessary.

Two-Level Cache Group Expiration Applied To More Groups Than Required

In the TwoLevelCache system in Serenity, a cache group key-based cache invalidation feature is present, enabling the expiration of items belonging to a common group in the distributed cache, without needing to know the keys for individual cached items.

For example, if you have cached data derived from UserRow records, like individual user information cached by id/username, it's possible to expire all such items when something in the User row changes.

The ExpireGroupItems method changes the generation number of the group, automatically expiring all cached items that use the old generation number. This is called by the InvalidateOnCommit extension method within the Save/Delete handlers after the transaction is committed.

Previously, during a batch update where multiple records were updated in a transaction, group generation changes could occur multiple times for the same entity type. This caused the distributed cache to be updated unnecessarily. An optimization was introduced to bundle those generation changes into a single change per group key.

This optimization worked correctly until the transition to .NET dependency injection. Due to the porting to .NET 5 DI, it was observed that this optimization was causing unnecessary group key generation changes originating from unrelated transactions.

For example, if a change was made in the Users table, this helper would reset only the Users group generation, as expected. However, if a subsequent call modified the Language table, it would also reset the cache for Users in addition to the Language table.

This issue has been resolved, but it might have some side effects. As the cache is no longer reset for unrelated entities, you may occasionally notice that it used to randomly reset the cache before the fix but no longer does so.

Reverted Microsoft.Data.SqlClient from 4.1.0 back to 3.1.1 in Sergen

Recently, we received error reports from our users regarding sergen being unable to connect to the SQL database due to a certificate issue. The solution to this problem was adding TrustServerCertificate=true to the connection string.

Interestingly, the portal could connect to the SQL server without needing that setting. In response to this, we conducted an investigation to identify the cause. The only notable difference we found was related to the version of the Microsoft.Data.SqlClient package. While sergen had a direct reference to Microsoft.Data.SqlClient 4.1.0, StartSharp/Serene indirectly used version 2.0.1 via FluentMigrator and Serenity.Data packages.

We discovered the following document on the internet:

Released: General Availability of Microsoft.Data.SqlClient 4.0

This document highlighted a crucial change; in versions 4.x+, Encrypt=true, meaning wire encryption for the connection between the client and SQL server, became the default setting for Microsoft.Data.SqlClient. Encryption necessitates the presence of a certificate, and most SQL server installations have local untrusted or development certificates. Some older versions do not even support encryption.

As the document advises, it's recommended to install a trusted certificate on your SQL server. If you cannot do this, you may use Encrypt=false in your connection string or TrustServerCertificate=true. However, these settings are not usually recommended for security reasons.

We acknowledge the reasons behind this change, but it represents a significant breaking change, which uniquely affected sergen as it was using 4.1.0. Consequently, most users assumed that the issue was with sergen itself.

In the future, we plan to use the latest versions of Microsoft.Data.SqlClient, which is currently 5.x. However, we understand that most users do not read the change logs. If we were to update the package, it could potentially cause problems for them, particularly if these issues surfaced in a production environment. As a result, for the time being, we are reverting to the latest 3.x release, specifically 3.1.1, for both sergen and Serenity.Data.

You have the option to update the Microsoft.Data.SqlClient version in your application or set Encrypt=true in your connection strings to use encrypted connections (which is recommended). Alternatively, you can wait for our package update to see if it resolves the issue. Certainly! Here are your release notes with improvements and grammar fixes:

Added a NodeScriptRunner Class to Serenity.Web for Running Node Scripts on Application Startup

As you may already know, we have been working on ES module support since version 6.1. Previously, users had to manually run node tsbuild --watch in their project directory during development to enable compile-on-save support, similar to TypeScript's compileOnSave option.

To streamline this process, we've introduced the NodeScriptRunner and a relevant IApplicationBuilder extension method to handle this task automatically. Now, during application startup, the watching process is initiated automatically, and you will receive output from esbuild in your console log. When the application stops, the background watch process should also terminate.

The blog post detailing the transition to ES modules has been updated to reflect this new feature: Read more

This feature comes pre-enabled in projects created from StartSharp 6.2.6+. For others, you can enable it by following the instructions provided in the blog post.

In-Place Add Dialog Types are Automatically Imported into the *Form.ts Files Generated by Sergen / Pro.Coder

We offer an InplaceEdit option along with the corresponding DialogType property in LookupEditor attributes. When enabled, it displays a button next to the editor, allowing you to add or edit the records listed in the dropdown editor.

When the DialogType is not explicitly specified, we use the lookup key, as long as they match. For example, if the lookup key is Administration.Role and the dialog class is MyProject.Administration.RoleDialog, the system handles it seamlessly.

In the context of namespaces, finding a dialog class by its full or partial name was possible by searching under the global scope (e.g., window). However, with the shift to ES module-style dialogs, they are not loaded into the global scope. They may not even be loaded at all unless explicitly imported somewhere.

This transition to ES modules posed some challenges for the in-place add feature, particularly when clicking the in-place button. What worked before may have seemed to stop working.

To address this, we've implemented a workaround. As long as you use the corresponding Form.ts files in your calling dialogs (which sergen automatically does when generating the code for the first time), the system will handle the dialog types seamlessly.

// UserDialog.ts
import { UserForm, UserRow, UserService } from "../";
//...

@Decorators.registerClass()
export class UserDialog extends EntityDialog<UserRow, any> {
    // ...

    protected form = new UserForm(this.idPrefix);

In the example above, we import UserForm and use it. Keep in mind that importing is not enough; you also need to reference or use it somewhere, as esbuild may remove the import from the output if it's unused. Suppose you've added InplaceAdd = true to the UserForm.cs roles property:

    [FormScript("Administration.User")]
    public class UserForm
    {
        //...
        [LookupEditor(typeof(RoleRow), Multiple = true, InplaceAdd = true)]
        public List<int> Roles { get; set; }
    }

For this to work, there must be a way for the Roles editor in UserDialog to locate the RoleDialog class.

This form definition in C# is serialized to JSON and passed to the client side. Therefore, the only thing the Roles editor knows about the dialog type is a simple string: Administration.Role.

When you click the in-place add button in UserDialog, it attempts to find YourProject.Administration.RoleDialog, but it can't. This is because the role dialog is defined within a module, not in the global scope:

In-place add dialog not found

A manual workaround involves manually importing RoleDialog from the UserDialog:

// UserDialog.ts
import { UserForm, UserRow, UserService } from "../";
import { RoleDialog } from "../Role/RoleDialog";

RoleDialog.name.toString();

//...

@Decorators.registerClass()
export class UserDialog extends EntityDialog<UserRow, any> {
    // ...

    protected form is new UserForm(this.idPrefix);

This workaround enables the RoleDialog to be located by the Roles editor in the UserDialog.

This simply imports the RoleDialog and pretends to be using it so that it is not erased by esbuild/TypeScript.

However, this may not work if the @registerClass decorator on RoleDialog did not include the full name:

@Decorators.registerClass()
export class RoleDialog extends EntityDialog<RoleRow, any> {

In the sample above, Serenity type registration knows nothing more than the class name is RoleDialog. Therefore, it is not possible to match this class with an Administration.RoleDialog since there could be multiple Role dialog classes in different modules.

But in namespaces mode, the class was placed in the StartSharp.Administration namespace. So Serenity could find its full name by scanning the window (this is normally done on jQuery's ready event). This is not possible with ES modules.

To address this, let's add the full name to the registerClass call:

@Decorators.registerClass('StartSharp.Administration.RoleDialog')
export class RoleDialog extends EntityDialog<RoleRow, any> {

Now, we have our RoleDialog displayed properly:

Role Dialog After Registering

Starting from Sergen version 6.2.6, it will automatically add the full name to the registerClass decorators it generates. However, for the existing dialogs that you convert manually, you'll need to set the full name yourself as well.

Nonetheless, even after doing this, we still need to "fake import" the RoleDialog class into the UserDialog.ts file.

To simplify this process for our users who are accustomed to having the in-place add feature work as it did before, we are now generating these "fake imports" in the UserForm.ts file:

import { RoleDialog } from "@/Administration/Role/RoleDialog";

export interface UserForm {
    //..
}

export class UserForm extends PrefixedContext {
    //..
}

[RoleDialog]; // In-place add dialog types

This should resolve most such cases, but you still need to add the full name to the registerClass decorators for the ES module dialog classes you use for the in-place adding feature.

Sergen Adds Full Name to @registerClass Decorators in Generated Code

As mentioned in the preceding topic, sergen now automatically appends the full name to @registerClass for newly generated modules, as it is a necessary step for proper registration of ES module classes:

@Decorators.registerClass('StartSharp.Administration.MyNewDialog')
export class MyNewDialog extends EntityDialog<RoleRow, any> {

For your existing classes that you convert to ES modules, please ensure to add the full name during the conversion process, taking into account your project name, module name, and class name.

Sending an Empty Source Map with a 200 Status, Instead of 403

In StartSharp 6.1.8, we introduced a SourceMapSecurityMiddleware that allows you to optionally disable source maps in production.

Previously, we returned a 403, indicating a "not allowed" response, which led to some warnings being displayed in the browser console. To address this, we have now updated it to return an empty source map with an HTTP 200 status code, instead of a 403 response.