Serenity 6.4.2 Release Notes (2022-12-09)

Set Default for TOptions Generic Argument for Widget as Any (6.3.3)

The Widget class which is the base class for all UI components was defined as:

class Widget<TOptions> {
    // ...
}

Now we have a default for the TOptions generic argument:

class Widget<TOptions = any> {
}

So it is possible to use it like "Widget" instead of "Widget<any>".

If you are getting an error like:

Generic type `Widget<TOptions>` requires 1 type argument(s)

You should update "@serenity-is/corelib" package in your package.json file to the latest version (6.3.3 or higher), execute "npm install", then restart Visual Studio.

Added Option to Disable Module Re-Exports in sergen.json

While generating ServerTypes for your modules, Sergen also generates module re-exports or module indexes like the following:

This allows importing multiple types from a module with one import statement:

import { RoleRow, UserColumns, UserRow, UserService } from "@/ServerTypes/Administration";

But this also means your Grid.ts file will have an indirect dependency on all the types re-exported from the Administration.ts file. In this sample LanguageColumns and RoleColumns, etc. will also be imported even if we don't use them.

Even though tree-shaking should reduce its effects, you may not want it in some cases. We added an option to disable these re-export files in sergen.json if desired:

{
    "ServerTypings": {
        "ModuleReExports": false
    }
}

Better Detection for Formatter Types for Client Types Transform (6.3.5)

In some cases, especially when your custom formatter type is defined in an ESM module, attributes corresponding to the formatter types would not be generated.

We now check for more interface matches including:

  • @serenity-is/corelib:Formatter
  • @serenity-is/corelib/slick:Formatter

and more attribute types:

  • @serenity-is/corelib:Decorators.registerFormatter
  • Decorators.registerFormatter
  • registerFormatter

Fixed SleekGrid Auto Column Hints Plugin

SleekGrid is our ES6 rewrite of the SlickGrid. We tried to drop jQuery dependency and make it optional. This means if jQuery is available in the global scope, SleekGrid can make use of it.

As we used the addEventHandler calls to bind events, as it works both with and without jQuery, there were some unexpected side effects.

We rewrote the core and grid classes in ES6 but not all plugins are converted to ES modules yet. Some of them, including the AutoTooltips plugin, are still used from legacy SlickGrid code, and they are dependent on jQuery and its synthetic event system.

Because of this, tooltips were not shown when hovering over cells with overflowed text content.

We are now using jQuery.on to bind events when jQuery is available instead of the addEventHandler calls to fix such issues.

Fixed RTL Mode Scroll Column Rendering (6.3.6, SleekGrid 1.4.4)

SlickGrid did not originally support RTL mode. We modified the original SlickGrid code years ago to add experimental RTL support.

As reported in this issue:

https://github.com/serenity-is/Serenity/issues/6595

SleekGrid was not rendering properly when doing horizontal scroll in RTL and mobile mode, e.g. narrow screens.

We thought it was a bug introduced by our SleekGrid rewrite, but that was not the case.

Seems like this bug was there for years, also with our old SlickGrid code and it was reported just now.

The reason was due to the browser returning scrollLeft as a negative number in RTL mode. We now fixed that issue by taking the absolute value of the scrollLeft.

Added XML Docs for Serenity.Net.Services / Serenity.Net.Web

We had XML doc comments for Serenity.Net.Core, Serenity.Net.Data, Serenity.Net.Entity libraries, but not for Serenity.Net.Services and Serenity.Net.Web.

All these libraries now have XML comments, and the API reference docs (https://serenity.is/docs/api/dotnet/Serenity.Net.Web/README) have descriptions for all classes/methods/properties.

Updated Serenity Docs Topics for .NET 6 and ES Modules

Some of the docs in the documentation site were still for old Serenity versions or were written for Namespace style instead of ES Modules style like our Movie Tutorial.

Serene is now also using ES modules by default and the Movie Tutorial is updated to reflect this.

We updated many topics including Introduction, DI, Configuration, Authentication, Localization, Caching, and Connections.

There are still some that need to be updated but we are hoping to complete them as soon as possible.

jQuery Path in the Serenity.Assets Package is Changed

Normally jQuery was included in the following path:

// appsettings.bundles.json
"~/Serenity.Assets/Scripts/jquery-{version}.js",

The Scripts folder and {version} suffix come from the jQuery NuGet packages and their default installation locations. We previously used third-party NuGet packages to obtain static script libraries, but we have since switched to using the Libman tool (Microsoft Library Manager) and our Serenity.Assets package to install and update static scripts such as Bootstrap and jQuery as web assets. We have kept the original file structure for compatibility purposes.

jQuery` is now located at:

// appsettings.bundles.json
"~/Serenity.Assets/jquery/jquery.js",

You should update your includes in the appsettings.bundles.json file accordingly.

Bootstrap and jQuery Are Updated

We are now using Bootstrap 5.2.3 and jQuery 3.6.1. Bootstrap is in the same location as before in the Serenity.Assets package but you should change the jQuery path as mentioned above.

We had the request handler and service behavior systems for Retrieve, List, Save and Delete operations but we didn't have similar interfaces and service behaviors for the Undelete operation.

Starting from 6.4.0, there is now a full set of dependency injectable request handlers and relevant service behaviors for undelete operations.

namespace Serenity.Services
{
    public interface IUndeleteBehavior
    {
        void OnPrepareQuery(IUndeleteRequestHandler handler, SqlQuery query);
        void OnValidateRequest(IUndeleteRequestHandler handler);
        //...
    }

    public interface IUndeleteHandler<TRow, TUndeleteRequest, 
        TUndeleteResponse>
        : IRequestHandler<TRow, TUndeleteRequest, TUndeleteResponse>
        where TRow : class, IRow, new()
        where TUndeleteRequest : UndeleteRequest
        where TUndeleteResponse : UndeleteResponse, new()
    {
        TUndeleteResponse Undelete(IUnitOfWork uow, 
            TUndeleteRequest request);
    }

    //....

Breaking Change - IUploadStorage.CopyFrom and IUploadStorage.WriteFile Method Arguments Changed

Our upload storage abstraction, which is named IUploadStorage had the following two methods in addition to others:

public interface IUploadStorage
{
    string CopyFrom(IUploadStorage sourceStorage, 
        string sourcePath, string targetPath, bool? autoRename);

    string WriteFile(string path, Stream source,
        bool? autoRename);

The last parameter, which was named autoRename was not very clear about its purpose.

If it was passed false, the upload storage was expected to raise an exception if a file at the same path exists.

If it was passed true, the upload storage would try to find a suitable name like File (2).jpg if a file at the same path exists.

If it was passed null, the upload storage would overwrite the existing file.

We now have an OverwriteOption enumeration:

public enum OverwriteOption
{
    /// <summary>Raise an error</summary>
    Disallowed = 0,
    /// <summary>Overwrite the target file</summary>
    Overwrite = 1,
    /// <summary>Try to find a suitable name</summary>
    AutoRename = 2
}

and use that instead of the autoRename flag:

public interface IUploadStorage
{
    string CopyFrom(IUploadStorage sourceStorage, 
        string sourcePath, string targetPath, OverwriteOption overwrite);

    string WriteFile(string path, Stream source, 
        OverwriteOption overwrite);

This is a breaking change only if you implemented a custom IUploadStorage object before. Otherwise, it should not cause any issues as we also added extension methods (though obsolete) with the autoRename flag for compatibility.

Switched to PNPM from NPM for Serenity Projects

This should not affect you if you are not using Serenity as a submodule or copying common-features, pro-features, etc. projects into your solution.

PNPM is a better, faster, more efficient, and no-nonsense alternative to NPM. After having some issues with NPM versions we decided to give PNPM a go:

StartSharp and Serene still use npm as we wanted to test PNPM a bit more before asking our customers to switch.

You may install PNPM via NPM:

npm install -g pnpm

or follow the installation instructions at:

Interface Checks Via Q.isAssignableFrom Use Type Name Instead of Relying Only on Object References

We had some issues while writing JEST tests for Serenity using esbuild. Some of the errors were about some types like formatters not found.

Upon further inspection, we determined that @serenity-is/corelib/q, e.g. the Q helper had multiple copies in memory and some interface classes had multiple copies as well.

This was caused by Q and some other packages getting bundled into multiple intermediate output files by esbuild.

It was mostly a configuration issue, but we thought this might happen for our users in the future even if we fixed the configuration for ourselves.

So in addition to fixing the esbuild configuration, we also added a workaround that allows discovering formatter types and marker interfaces, etc. even if they have multiple copies in memory, e.g. they are included in multiple bundles.

To do this, we are checking their registration names, e.g. two separate ISlickFormatter interface copies that both have Serenity.ISlickFormatter as their registration name is now considered a match. So, FormatterTypeRegistry.get() can now locate formatters defined in another bundle even if the marker interfaces for them are different.

SixLabors.ImageSharp Dependency is Abstracted (6.4.0)

We are using SixLabors.ImageSharp instead of System.Drawing which does not work properly in Linux etc.

The library is used mainly for upload image scaling and thumb generation.

Unfortunately, SixLabors.ImageSharp changed the licensing terms recently, and we may have to switch to another library if it will become incompatible with our MIT (Serenity/Serene) / commercial (StartSharp) licenses.

So, instead of having a hard-coded dependency on SixLabors or another library, we abstracted the dependency via the new IImageProcessor interface:

(int width, int height) GetImageSize(object image);

object Load(Stream source, out ImageFormatInfo formatInfo);

object Scale(object image, int width, int height,
    ImageScaleMode mode, string backgroundColor, bool inplace);

void Save(object image, Stream target, string mimeType, 
    ImageEncoderParams  encoderParams);

This interface only contains the most basic methods we need for upload image processing.

It will allow us to switch to another library in the future if required.

You may also use a completely different library if desired via dependency injection, or customize the default one.

UploadProcessor is Obsolete and Replaced with IUploadProcessor Abstraction

The UploadProcessor class that we use in FilePage.cs temporary upload method is now obsolete.

You should inject the new IUploadProcessor interface and use it in the HandleUploadRequest method:

public class FileController : Controller
{
    private readonly IUploadStorage uploadStorage;
    private readonly IUploadProcessor uploadProcessor;

    public FileController(IUploadStorage uploadStorage, 
        IUploadProcessor uploadProcessor)
    {
        //...
        this.uploadProcessor = uploadProcessor ?? 
            throw new ArgumentNullException(nameof(uploadProcessor));
    }

    [NonAction]
    private ServiceResponse HandleUploadRequest(HttpContext context)
    {
        // ...
        IUploadOptions uploadOptions = new UploadOptions
        {
            ThumbWidth = 128,
            ThumbHeight = 96,
            ThumbMode = ImageScaleMode.PreserveRatioNoFill
        };

        var uploadIntent = Request.Query["uploadIntent"];
        if (!string.IsNullOrEmpty(uploadIntent))
        {
            // if desired modify uploadOptions here based on uploadIntent
        }

        var uploadInfo = uploadProcessor.Process(file.OpenReadStream(),
            file.FileName, uploadOptions);
    }

Added Optional UploadIntent Property to Upload Editor Attributes

The options you specify on an upload editor like ImageUploadEditor(...) have no effect during temporary uploads as there was no connection between the temporary upload processing and the editor that triggered it.

To explain better, let's say we have such a property defined in one of our rows:

[FileUploadEditor(MinSize = 1_000, MaxSize = 10_000_000)]
public string SomeFile { get; set; }

This property is not supposed to allow files smaller than 1KB and bigger than 10MB.

If you opened the form containing this property and uploaded a file, the first upload will be a temporary one, handled by FilePage.TemporaryUpload method.

As the FilePage.TemporaryUpload method has no information about where this temporary file will be used, e.g. which upload editor attribute was used to initiate this temporary upload, it has no way to check the constraints and uses default options:

var processor = new UploadProcessor(UploadStorage, Logger)
{
    ThumbWidth = 128,
    ThumbHeight = 96
};

There are no file size constraints here other than the thumb width/height to generate. And even if you specify the thumb size in the upload editor attribute it won't be used during temporary upload as well.

When you click the Save button, the upload behavior kicks in, locates the editor attribute, and checks constraints.

But until clicking the Save button, the user does not know that the file she is uploading is not conforming to the constraints.

This may not be very critical, but showing the error early to the end user might be helpful, especially when the form is big and has multiple upload editors.

We now have a free-form string UploadIntent property that you may pass to upload editors, and which they pass back to the temporary upload method via the query string:

// SomeRow.cs
// ...
[FileUploadEditor(MinSize = 1_000, MaxSize = 10_000_000, UploadIntent = "ProfileImage")]
public string SomeFile { get; set; }
// ...

// FilePage.cs
IUploadOptions uploadOptions = new UploadOptions
{
    ThumbWidth = 128,
    ThumbHeight = 96,
    ThumbMode = ImageScaleMode.PreserveRatioNoFill
};

var uploadIntent = Request.Query["uploadIntent"];
if (!string.IsNullOrEmpty(uploadIntent))
{
    // if desired modify uploadOptions here based on uploadIntent
    switch (uploadIntent) {
        case "ProfileImage":
            var attr = typeof(SomeRow).GetProperty("SomeFile")
                .GetAttribute<FileUploadEditor>();
            uploadOptions.MinSize = attr.MinSize;
            uploadOptions.MaxSize = attr.MaxSize;
            // ...
            break;
    }
}

Restructured Upload Editor Interfaces and Introduced BaseUploadEditorAttribute (6.4.0)

We started abstracting and restructuring upload editor attributes in 6.3.0 and the interfaces got another overhaul in 6.4.0:

We now have the following interfaces implemented by upload editor attributes and the new UploadOptions class:

  • IUploadEditor

    • Marker interface for all upload editor attributes. It contains IsMultiple, DisableDefaultBehavior, and UploadIntent properties.
  • IUploadFileConstraints

    • Contains AllowNonImage, AllowedExtensions (default empty, e.g. allow all extensions), ExtensionBlacklist (file extensions considered dangerous like .exe, .aspx, etc.), ImageExtensions (set of extensions that are considered images, default is ".jpg, .jpeg, .gif, .png". Image validation is only performed for these extensions.), MaxSize, MinSize properties.
  • IUploadFileOptions

    • Additional file options like CopyToHistory, DisplayFileName, FilenameFormat, etc.
  • IUploadImageConstraints

    • These constraints are checked when a file is considered an image based on its extension. Contains MinWidth, MaxWidth, MinHeight, MaxHeight, IgnoreExtensionMismatch (e.g. when an uploaded .jpg file contains a PNG image), IgnoreEmptyBitmap (allow 0x0 images), IgnoreInvalidImage (if an uploaded image is invalid, assume it as a regular file instead of raising an error)
  • IUploadImageOptions

    • Additional image options like ThumbWidth, ThumbQuality, ScaleWidth, etc.

All upload editors now implement these interfaces and they derive from a base abstract class named BaseUploadEditorAttribute which you may also subclass to create a custom upload editor attribute.

There is also a UploadOptions class that implements all upload interfaces except the IUploadEditor as it does not apply to temporary uploads.

It also provides the default values for all upload editor attribute properties like ImageExtensions.

The UploadOptions class is used in FilePage to initialize the defaults for temporary uploads and customize it via the UploadIntent if desired.

A new IFilenameFormatSanitizer interface that you can inject via DI allows customizing the sanitizing behavior for file name format strings with placeholders like |SomeField|/|AnotherField|.