Skip to content

Orchard Core 2.0.0

Release date: September 9, 2024.

🎉 Orchard Core 2.0 Is Here – Elevate Your Projects to the Next Level! 🎉

We are thrilled to announce the second major release of Orchard Core! This release is packed with performance improvements, bug fixes, and a variety of enhancements that make it our best version yet. Whether you're building a new project or maintaining an existing one, Orchard Core 2.0 brings stability and power to your fingertips.

🚀 Why Upgrade to Orchard Core 2.0?

  • Enhanced Performance: Your sites and applications will run faster, smoother, and more efficiently.
  • Rock-Solid Stability: We've squashed numerous bugs, ensuring a more reliable and enjoyable development experience.
  • Seamless Development: By sticking to version control best practices, we've ensured no breaking changes in any patch or minor release, allowing developers to confidently create modules and libraries that will work across the entire 2.x series.

🎯 Module and Library Developers, Take Note!

Want to create a module or library for Orchard Core 2.0? We've made it easier than ever to build with confidence. Simply set your dependencies to [2.0, 3.0) to ensure full compatibility with future updates, without worrying about breaking changes.

Important Upgrade Instructions

Prior to making the leap to Orchard Core 2.0, it's crucial to follow a step-by-step upgrade process. Begin by upgrading your current version to 1.8, resolving any obsolete warnings encountered along the way, and guaranteeing that every site successfully operates on version 1.8.3. This approach ensures that all sites are seamlessly transitioned to 1.8. Subsequently, proceed with confidence to upgrade to 2.0. Prior to initiating this upgrade, thoroughly review the documented list of breaking changes provided below to facilitate a smooth and hassle-free transition.

Breaking Changes

Drop Newtonsoft.Json Support

The utilization of Newtonsoft.Json has been discontinued in both YesSql and OrchardCore. Instead, we have transitioned to utilize System.Text.Json due to its enhanced performance capabilities. To ensure compatibility with System.Text.Json during the serialization or deserialization of objects, the following steps need to be undertaken:

  • If you are using a custom deployment steps, change how you register it by using the new AddDeployment<> extension. This extension adds a new service that is required for proper serialization. For instance, instead of registering your deployment step like this:
services.AddTransient<IDeploymentSource, AdminMenuDeploymentSource>();
services.AddSingleton<IDeploymentStepFactory>(new DeploymentStepFactory<AdminMenuDeploymentStep>());
services.AddScoped<IDisplayDriver<DeploymentStep>, AdminMenuDeploymentStepDriver>();

change it to the following:

services.AddDeployment<AdminMenuDeploymentSource, AdminMenuDeploymentStep, AdminMenuDeploymentStepDriver>();
  • If you are using a custom AdminMenu node, change how you register it by using the new AddAdminNode<> extension. This extension adds a new service that is required for proper serialization. For instance, instead of registering your custom admin menu nodep like this:
services.AddSingleton<IAdminNodeProviderFactory>(new AdminNodeProviderFactory<PlaceholderAdminNode>());
services.AddScoped<IAdminNodeNavigationBuilder, PlaceholderAdminNodeNavigationBuilder>();
services.AddScoped<IDisplayDriver<MenuItem>, PlaceholderAdminNodeDriver>();

change it to the following:

services.AddAdminNode<PlaceholderAdminNode, PlaceholderAdminNodeNavigationBuilder, PlaceholderAdminNodeDriver>();
  • Any serializable object that contains a polymorphic property (a base type that can contain sub-classes instances) needs to register all possible sub-classes this way:
services.AddJsonDerivedTypeInfo<UrlCondition, Condition>();

Alternatively, you can simplify your code by using the newly added extensions to register custom conditions. For example,

services.AddRule<HomepageCondition, HomepageConditionEvaluator, HomepageConditionDisplayDriver>();
  • Any type introduced in custom modules inheriting from MenuItem, AdminNode, Condition, ConditionOperator, Query, SitemapType will have to register the class using the services.AddJsonDerivedTypeInfo<> method. For example,
services.AddJsonDerivedTypeInfo<SqlQuery, Query>();
  • The extension PopulateSettings<T>(model) was removed from PartFieldDefinition. If you are using this method in your code, you'll have to get the settings using the Settings object directly. For instance, if you have this code,
public override IDisplayResult Edit(ContentPartFieldDefinition partFieldDefinition)
{
   return Initialize<NumericFieldSettings>("NumericFieldSettings_Edit", model => partFieldDefinition.PopulateSettings(model));
}

You'll change it to the following:

 public override IDisplayResult Edit(ContentPartFieldDefinition partFieldDefinition)
{
   return Initialize<NumericFieldSettings>("NumericFieldSettings_Edit", model => 
   {
       var settings = partFieldDefinition.GetSettings<NumericFieldSettings>();
       model.Hint = settings.Hint;
       // ...
   });
}

Queries

Previously, any query type had to inherit from Query and required its own distinct type (e.g., SqlQuery, LuceneQuery, ElasticQuery). However, with pull request #16402, creating a custom type for each query type is no longer necessary. This update involved changes to the IQueryManager and IQuerySource interfaces, as well as the Query class. Additionally, a new project, OrchardCore.Queries.Core, was introduced.

A migration process has been implemented to transition existing queries into the new structure, ensuring no impact on existing tenants.

Key Changes

  • Modification of Interfaces and Classes: Updates were made to the IQueryManager, IQuerySource interface, and Query class to accommodate the new structure.
  • Addition of OrchardCore.Queries.Core: This new project supports the updated query handling mechanism.

Implementing IQuerySource

We now request implementations of IQuerySource using keyed services. Below is an example of how to register new implementations:

services.AddQuerySource<SqlQuerySource>(SqlQuerySource.SourceName);

This approach allows for more flexible and modular query handling in OrchardCore.

Handlers

We now have handlers for dealing with queries. To manipulate a query using a handler, implement the IQueryHandler interface. This provides a structured way to extend and customize the behavior of queries within the framework.

Twitter Module

The Twitter module has been rebranded to X.

If you were using the OrchardCore_Twitter configuration key to configure the module, please update the configuration key to OrchardCore_X. The OrchardCore_Twitter key continues to work but will be deprecated in a future release.

Users Module

  • The Login.cshtml has undergone a significant revamp. The previous AfterLogin zone, which allowed filters for injecting shapes, has been replaced. Now, you can inject shapes using drivers by implementing IDisplayDriver<LoginForm>. For example, the 'Forgot Password?' link is injected using the following driver:
public class ForgotPasswordLoginFormDisplayDriver : DisplayDriver<LoginForm>
{
    private readonly ISiteService _siteService;

    public ForgotPasswordLoginFormDisplayDriver(ISiteService siteService)
    {
        _siteService = siteService;
    }

    public override async Task<IDisplayResult> EditAsync(LoginForm model, BuildEditorContext context)
    {
        var settings = await _siteService.GetSettingsAsync<ResetPasswordSettings>();

        if (!settings.AllowResetPassword)
        {
            return null;
        }

        return View("LoginFormForgotPassword_Edit", model).Location("Links:5");
    }
}
  • The ForgotPassword.cshtml has undergone a significant revamp. The previous AfterForgotPassword zone, which allowed filters for injecting shapes, has been replaced. Now, you can inject shapes using drivers by implementing IDisplayDriver<ForgotPasswordForm>. For example, the ReCaptcha shape is injected using the following driver:
public class ReCaptchaForgotPasswordFormDisplayDriver : DisplayDriver<ForgotPasswordForm>
{
    private readonly ISiteService _siteService;

    public ReCaptchaForgotPasswordFormDisplayDriver(ISiteService siteService)
    {
        _siteService = siteService;
    }

    public override async Task<IDisplayResult> EditAsync(ForgotPasswordForm model, BuildEditorContext context)
    {
        var settings = await _siteService.GetSettingsAsync<ReCaptchaSettings>();

        if (!settings.IsValid())
        {
            return null;
        }

        return View("FormReCaptcha_Edit", model).Location("Content:after");
    }
}
  • The ResetPassword.cshtml has undergone a significant revamp. The previous AfterResetPassword zone, which allowed filters for injecting shapes, has been replaced. Now, you can inject shapes using drivers by implementing IDisplayDriver<ResetPasswordForm>. For example, the ReCaptcha shape is injected using the following driver:
public class ReCaptchaResetPasswordFormDisplayDriver : DisplayDriver<ResetPasswordForm>
{
    private readonly ISiteService _siteService;

    public ReCaptchaResetPasswordFormDisplayDriver(ISiteService siteService)
    {
        _siteService = siteService;
    }

    public override async Task<IDisplayResult> EditAsync(ResetPasswordForm model, BuildEditorContext context)
    {
        var settings = await _siteService.GetSettingsAsync<ReCaptchaSettings>();

        if (!settings.IsValid())
        {
            return null;
        }

        return View("FormReCaptcha_Edit", model).Location("Content:after");
    }
}

Previously, users were only able to reset their password through email when the "Reset Password" feature was enabled. However, we've enhanced this functionality to offer users the flexibility of resetting their password using either their email or username. Consequently, the Email property on both the ForgotPasswordViewModel and ResetPasswordViewModel have been deprecated and should be replaced with the new UsernameOrEmail property for password resets.

  • The Register.cshtml has undergone a significant revamp. The previous AfterRegister zone, which allowed filters for injecting shapes, has been replaced. Now, you can inject shapes using drivers by implementing IDisplayDriver<RegisterUserForm>. For example, the ReCaptcha shape is injected using the following driver:
public class RegisterUserFormDisplayDriver : DisplayDriver<RegisterUserForm>
{
    private readonly ISiteService _siteService;

    public RegisterUserFormDisplayDriver(ISiteService siteService)
    {
        _siteService = siteService;
    }

    public override async Task<IDisplayResult> EditAsync(RegisterUserForm model, BuildEditorContext context)
    {
        var settings = await _siteService.GetSettingsAsync<ReCaptchaSettings>();

        if (!settings.IsValid())
        {
            return null;
        }

        return View("FormReCaptcha_Edit", model).Location("Content:after");
    }
}
  • Two-factor authentication (2FA) is now available for all users. Previously, only users with permission to access the admin area could set up their own 2FA methods, although all users could still use 2FA if it was set up by an admin. To enable this change, the method IsRequiredAsync() in both the ITwoFactorAuthenticationHandlerCoordinator and ITwoFactorAuthenticationHandler interfaces has been updated to IsRequiredAsync(IUser user).

  • A new UserConfirmedEvent workflow event is now available. This event is triggered when a user successfully confirms their email address using the link sent during user registration.

  • We are introducing a new interface, IUserTimeZoneService, to replace the existing UserTimeZoneService. If your project directly injects UserTimeZoneService, you will need to switch to using IUserTimeZoneService instead.

Notifications Module

The endpoint for marking notifications as read has been updated. Previously the route was /api/notifications/read, and now it is /Notifications/MarkAsRead. This change will only affect you if you have overridden the UserNotificationNavbar view.

Contents

The IContentManager interface was modified. The method Task<IEnumerable<ContentItem>> GetAsync(IEnumerable<string> contentItemIds, bool latest = false) was removed. Instead use the method that accepts VersionOptions by providing either VersionOptions.Latest or VersionOptions.Published will be used by default.

Additionally, the GetContentItemByHandleAsync(string handle, bool latest = false) and GetContentItemByIdAsync(string contentItemId, bool latest = false) were removed from IOrchardHelper. Instead use the method that accepts VersionOptions by providing either VersionOptions.Latest or VersionOptions.Published will be used by default.

Lastly, we dropped support for AllVersions option in VersionOptions. Instead, you can use the new GetAllVersionsAsync(string contentItemId) method on IContentManager.

Media Indexing

Previously, .pdf files were automatically indexed in the search providers (Elasticsearch, Lucene or Azure AI Search). Now, if you want to continue to index .PDF file you'll need to enable the OrchardCore.Media.Indexing.Pdf feature.

Additionally, if you need to enable indexing for text file with .txt, .md extensions, you will need the OrchardCore.Media.Indexing.Text feature.

If you need to enable indexing for other extensions like (.docx, or .pptx), you will need the OrchardCore.Media.Indexing.OpenXML feature.

SMS Module

In the past, we utilized the injection of ISmsProvider for sending SMS messages. However, in this release, it is now necessary to inject ISmsService instead.

Additionally, Twilio provider is no longer enabled by default. If you want to use Twilio SMS provider, you must enable the provider by visiting the email settings Configuration > Settings > Email and see the Twilio tab.

Display Management

In this release, the signatures of the UpdateAsync() method within the SectionDisplayDriver base class have undergone modifications. Previously, these signatures accepted the BuildEditorContext parameter. However, with this update, all signatures now require the UpdateEditorContext instead. This alteration necessitates that every driver inheriting from this class adjusts their contexts accordingly.

Here are the updated signatures:

  1. From: Task<IDisplayResult> UpdateAsync(TModel model, BuildEditorContext context) To: Task<IDisplayResult> UpdateAsync(TModel model, UpdateEditorContext context)

  2. From: Task<IDisplayResult> UpdateAsync(TModel model, TSection section, BuildEditorContext context) To: Task<IDisplayResult> UpdateAsync(TModel model, TSection section, UpdateEditorContext context)

  3. From: Task<IDisplayResult> UpdateAsync(TSection section, BuildEditorContext context) To: Task<IDisplayResult> UpdateAsync(TSection section, UpdateEditorContext context). This method is now obsolete.

These adjustments ensure compatibility and adherence to the latest conventions within the DisplayDriver class.

Additionally, we've enhanced display performance by optimizing the display drivers. The following methods have been marked as obsolete:

  • IDisplayResult Display(TModel model)
  • IDisplayResult Edit(TModel model)

the following methods have been removed, with updated alternatives provided:

  • Task<IDisplayResult> DisplayAsync(TModel model, IUpdateModel updater) → Use DisplayAsync(TModel model, BuildDisplayContext context) where the Updater property is available via context.
  • IDisplayResult Display(TModel model, IUpdateModel updater) → Use Display(TModel model, BuildDisplayContext context) where the Updater property is available via context.
  • Task<IDisplayResult> UpdateAsync(TModel model, IUpdateModel updater) → Use UpdateAsync(TModel model, UpdateEditorContext context) where the Updater property is available via context.
  • Task<IDisplayResult> EditAsync(TModel model, IUpdateModel updater) → Use EditAsync(TModel model, BuildEditorContext context) where the Updater property is available via context.
  • IDisplayResult Edit(TModel model, IUpdateModel updater) → Use Edit(TModel model, BuildEditorContext context) where the Updater property is available via context.

In addition, we've introduced new static methods in the DisplayDriverBase class:

  • Task<IDisplayResult> CombineAsync(params IDisplayResult[] results)
  • Task<IDisplayResult> CombineAsync(IEnumerable<IDisplayResult> results)

The same refactoring applies to the SectionDisplayDriver class. The following methods are now obsolete:

  • Display(TSection section, BuildDisplayContext context)
  • Display(TSection section)
  • Edit(TSection section, BuildEditorContext context)
  • UpdateAsync(TSection section, UpdateEditorContext context)
  • UpdateAsync(TSection section, IUpdateModel updater, string groupId)

And the following methods have been removed, with updated alternatives:

  • UpdateAsync(TModel model, TSection section, IUpdateModel updater, UpdateEditorContext context) → Use UpdateAsync(TModel model, TSection section, UpdateEditorContext context) where the Updater is available via context.
  • UpdateAsync(TSection section, IUpdateModel updater, UpdateEditorContext context) → Use UpdateAsync(TModel model, TSection section, UpdateEditorContext context) where the Updater is available via context.
  • UpdateAsync(TSection section, IUpdateModel updater, string groupId) → Use UpdateAsync(TModel model, TSection section, UpdateEditorContext context) where the Updater and GroupId properties are available via context.
  • UpdateAsync(TSection section, UpdateEditorContext context) → Use UpdateAsync(TModel model, TSection section, UpdateEditorContext context) where the Updater is available via context.
  • EditAsync(TSection section, BuildEditorContext context) → Use EditAsync(TModel model, TSection section, BuildEditorContext context) where the Updater is available via context.
  • DisplayAsync(TSection section, BuildDisplayContext context) → Use DisplayAsync(TModel model, TSection section, BuildDisplayContext context) where the Updater is available via context.

Note

To enhance your project's performance, address any warnings generated by the use of these obsolete methods. Please note that these methods will be removed in the next major release.

A new SiteDisplayDriver base class was introduced to simplify the process of adding a UI settings.

Note

With the new SiteDisplayDriver base class for custom settings, you no longer need explicit checks like context.GroupId.Equals(GroupId, StringComparison.OrdinalIgnoreCase) as the base class handles this check for you.

A new extension has been introduced to simplify the registration of site setting drivers. Instead of using service.AddScoped<IDisplayDriver<ISite>, CustomSettingsDisplayDriver>(), you can now register it with the more streamlined service.AddSiteDisplayDriver<CustomSettingsDisplayDriver>().

Similarly, the ContentPartDisplayDriver base class have undergone modifications. The following methods were marked obsolete:

  • IDisplayResult Display(TPart part)
  • IDisplayResult Edit(TPart part)
  • Task<IDisplayResult> UpdateAsync(TPart part, IUpdateModel updater)

At the same time, the following method was removed:

  • Task<IDisplayResult> UpdateAsync(TPart part, IUpdateModel updater)

Moreover, the ContentFieldDisplayDriver base class have undergone modifications. The following methods were marked obsolete:

  • UpdateAsync(TField field, IUpdateModel updater, UpdateFieldEditorContext context)
  • Update(TField field, IUpdateModel updater, UpdateFieldEditorContext context)

At the same time, the following method was added:

  • UpdateAsync(TField field, UpdateFieldEditorContext context)

GraphQL Module

The GraphQL schema may change because fields are now always added to the correct part. Previously, additional fields may have been added to the parent content item type directly.

You may have to adjust your GraphQL queries in that case.

Additionally, the GetAliases method in the IIndexAliasProvider interface is now asynchronous and has been renamed to GetAliasesAsync. Implementations of this interface should be modified by updating the method signature and ensure they handle asynchronous operations correctly.

Media fields

The schema for media fields has been updated. Instead of separate lists for fileNames, urls, paths, and mediatexts, these fields are now grouped under a single files type to simplify data binding on the front end.

Please update your existing queries as shown in the following example:

Before:

{
  article {
    contentItemId
    image {
      mediatexts(first: 10)
      urls(first: 10)
    }
  }
}

After:

{
  article {
    contentItemId
    image {
      files(first: 10) {
        mediaText
        url
      }
    }
  }
}

A compatibility switch has been introduced to allow continued use of the old fileNames, urls, paths, and mediatexts fields. To restore the legacy fields, add the following AppContext switch to your startup class:

AppContext.SetSwitch("OrchardCore.Media.EnableLegacyMediaFieldGraphQLFields", true);

Resource Management

Previously the <resources type="..." /> Razor tag helper and the {% resources type: "..." %} Liquid tag were only capable of handling a hard-coded set of resource definition types (script, stylesheet, etc). Now both can be extended with IResourcesTagHelperProcessor to run custom rendering logic. To make this possible, the OrchardCore.ResourceManagement.TagHelpers.ResourceType and OrchardCore.Resources.Liquid.ResourcesTag.ResourceType enums have been replaced with a common OrchardCore.ResourceManagement.ResourceTagType. It was renamed to avoid confusion with ResourceDefinition.Type. This change does not affect the uses of the Razor tag helper or the Liquid tag in templates of user projects, but it affects published releases such as NuGet packages. Any themes and modules that contain <resources type="..." /> in a Razor file (e.g. Layout.cshtml) must be re-released to generate the updated .cshtml.cs files, because the old pre-compiled templates would throw MissingMethodException.

Workflows Module

Previously, the CreateContentTask, RetrieveContentTask, and UpdateContentTask in the workflow's CorrelationId was updated each time with the ContentItemId of the current content item. Now, the CorrelationId value will only be updated if the current workflow's CorrelationId is empty.

Additionally, a new workflow-scoped script function setCorrelationId(id:string): void was added, that you can use to update the workflow's CorrelationId.

The BuildNavigationAsync method in the INavigationProvider interface was optimized by changing its return type from Task to ValueTask.

Change Logs

Azure AI Search Module

Introducing a new "Azure AI Search" module, designed to empower you in the administration of Azure AI Search indices. When enabled with the "Search" module, it facilitates frontend full-text search capabilities through Azure AI Search. For more info read the Azure AI Search docs.

New ImageSharp Image Caches for Azure Blob and AWS S3 Storage

The Microsoft Azure Media and Amazon S3 Media modules have new features for replacing the default PhysicalFileSystemCache of ImageSharp that stores resized images in the local App_Data folder. Instead, you can now use Azure Blob Storage with the Azure Media ImageSharp Image Cache feature (that utilizes AzureBlobStorageImageCache), and AWS S3 with the Amazon Media ImageSharp Image Cache feature (that utilizes AWSS3StorageCache). Depending on your use case, this can provide various advantages. Check out the Azure Media and the Amazon S3 Media docs for details.

Deployment Module

New Extensions

Added new extensions to make registering custom deployment step easier:

  • services.AddDeployment<TSource, TStep>().
  • services.AddDeployment<TSource, TStep, TDisplayDriver>().
  • services.AddDeploymentWithoutSource<TStep, TDisplayDriver>().

Recipe Executions

Before this release, implementations of IRecipeStepHandler or IRecipeEventHandler would throw exceptions to report errors if something failed to import. However, this is no longer the recommended approach for error reporting.

Now, to handle errors, we have introduced a new property named Errors in the RecipeExecutionContext. This property allows you to log errors instead of throwing exceptions. These errors should be localized and must not contain any sensitive data, as they are visible to the end user. Exceptions are still used for logging additional information, but these are not shown to the end user.

Additionally, if an error occurs, a new custom exception, RecipeExecutionException, is thrown. For more info visit the related pull-request.

Workflows Module

The method Task TriggerEventAsync(string name, IDictionary<string, object> input = null, string correlationId = null, bool isExclusive = false, bool isAlwaysCorrelated = false) was changed to return Task<IEnumerable<WorkflowExecutionContext>> instead.

New Events

The following events were added:

  • UserConfirmedEvent: this event triggers when a user successfully confirms their email address after registration.

GraphQL Module

When identifying content types for GraphQL exposure, we identify those without a stereotype to provide you with control over the behavior of stereotyped content types. A new option, DiscoverableSterotypes, has been introduced in GraphQLContentOptions. This allows you to specify stereotypes that should be discoverable by default.

For instance, if you have several content types stereotyped as ExampleStereotype, you can make them discoverable by incorporating the following code into the startup class:

services.Configure<GraphQLContentOptions>(options =>
{
    options.DiscoverableSterotypes.Add("ExampleStereotype");
});

The GraphQL module now allows for filtering content fields, making it easier to query specific content fields using GraphQL. This enhancement relies on having the OrchardCore.ContentFields.Indexing.SQL module enabled.

To filter content items based on a specific content field, you can use a query like this:

product(where: {price: {amount_gt: 10}}) {
    contentItemId
    displayText
    price {
        amount
    }
}

Email Module

The OrchardCore.Email module has undergone a refactoring process with no breaking changes. However, there are compile-time warnings that are recommended to be addressed. Here is a summary of the changes:

  • Previously, we used the injection of ISmtpService for sending email messages. In this release, it is now necessary to inject IEmailService instead.
  • The SMTP related services are now part of a new module named OrchardCore.Email.Smtp. To use the SMTP provider for sending emails, enable the OrchardCore.Email.Smtp feature.
  • If you were using the OrchardCore_Email configuration key to set up the SMTP provider for all tenants, please update the configuration key to OrchardCore_Email_Smtp. The OrchardCore_Email key continues to work but will be deprecated in a future release.
  • A new email provider was added to allow you to send email using Azure Communication Services Email. Click here to read more about the ACS module.

Menus and AdminMenus now support specifying the target property.

Admin Menu

The admin menu has been optimized with significant performance improvements and code enhancements. When adding a navigation provider to the admin menu, it is now recommended to use the new AdminNavigationProvider class rather than directly implementing the INavigationProvider interface in your project.

Furthermore, when passing route values to actions, it's a best practice to store these values in constant variables for maintainability and clarity. Below is an example demonstrating this approach:

public sealed class AdminMenu : AdminNavigationProvider
{
    private static readonly RouteValueDictionary _routeValues = new()
    {
        { "area", "OrchardCore.Settings" },
        { "groupId", AdminSiteSettingsDisplayDriver.GroupId },
    };

    internal readonly IStringLocalizer S;

    public AdminMenu(IStringLocalizer<AdminMenu> stringLocalizer)
    {
        S = stringLocalizer;
    }

    protected override ValueTask BuildAsync(NavigationBuilder builder)
    {
        builder
            .Add(S["Configuration"], configuration => configuration
                .Add(S["Settings"], settings => settings
                    .Add(S["Admin"], S["Admin"].PrefixPosition(), admin => admin
                        .AddClass("admin")
                        .Id("admin")
                        .Action("Index", "Admin", _routeValues)
                        .Permission(PermissionsAdminSettings.ManageAdminSettings)
                        .LocalNav()
                    )
                )
            );

        return ValueTask.CompletedTask;
    }
}

A new extension has been introduced to simplify the registration of navigation providers. Instead of using services.AddNavigationProvider<AdminMenu>(), you can now register it with the more streamlined services.AddNavigationProvider<AdminMenu>();.

Admin Routes

The [Admin] attribute now has optional parameters for a custom route template and route name. It works just like the [Route(template, name)] attribute, except it prepends the configured admin prefix. You can apply it to the controller or the action; if both are specified then the action's template takes precedence. The route name can contain {area}, {controller}, and {action}, which are substituted during mapping so the names can be unique for each action. This means you don't have to define these admin routes in your module's Startup class anymore, but that option is still available and supported. Take a look at this example:

[Admin("Person/{action}/{id?}", "Person{action}")]
public sealed class PersonController : Controller
{
    [Admin("Person", "Person")]
    public IActionResult Index() { ... }

    public IActionResult Create() { ... }

    public IActionResult Edit(string id) { ... }
}

In this example, (if the admin prefix remains the default "Admin") you can reach the Index action at ~/Admin/Person (or by the route name Person), because its own action-level attribute took precedence. You can reach Create at ~/Admin/Person/Create (route name PersonCreate) and Edit for the person whose identifier string is "john-doe" at ~/Admin/Person/john-doe (route name PersonEdit).

Users Module

Added a new User Localization feature that allows to be able to configure the culture per user from the admin UI.

Added a new Navbar() function to Liquid to allow building the Navbar shape using Liquid. Here are the steps needed to add the Navbar shape into your custom Liquid shape:

  1. Construct the shape at the beginning of the layout.liquid file to enable navbar items to potentially contribute to the resources output as necessary.

{% assign navbar = Navbar() | shape_render %}
2. Subsequently in the layout.liquid file, invoke the shape at the location where you want to display it.

{{ navbar }}

Notifications Module

TheINotificationMessage interface was updated to includes the addition of a Subject field, which facilitates the rendering of notification titles. Moreover, the existing Summary field has been transitioned to HTML format. This adjustment enables the rendering of HTML notifications in both the navigation bar and the notification center. Consequently, HTML notifications can now be created, affording functionalities such as clickable notifications.

Furthermore, the introduction of the NotificationOptions provides configuration capabilities for the notifications module. This structure comprises the following attributes:

  • TotalUnreadNotifications: This property specifies the maximum number of unread notifications that are displayed in the navigation bar. By default, it is set to 10.
  • DisableNotificationHtmlBodySanitizer: Notifications generated from workflows have their HtmlBody sanitized by default. This property allows you to disable the sanitization process if needed.
  • AbsoluteCacheExpirationSeconds: Specifies the absolute maximum duration, in seconds, for which the top unread user notifications are cached when caching is enabled. A value of 0 does not disable caching but indicates that there is no fixed expiration time for the cache. You can set this value to define a maximum lifespan for the cached data before it is invalidated.
  • SlidingCacheExpirationSeconds: Defines the sliding cache duration, in seconds, for unread user notifications when caching is enabled. By default, the cache is refreshed and extended for up to 1800 seconds (30 minutes) after each user activity. To disable sliding expiration, you can set this value to 0.

Secure media files

Introduces a new "Secure Media" feature for additional control over who can access media files. For more info read the Secure Media docs.

Users Module

Enhanced functionality has been implemented, giving developers the ability to control the expiration time of different tokens, such as those for password reset, email confirmation, and email change, which are sent through the email service. Below, you'll find a comprehensive list of configurable options along with their default values:

Class Name Default Expiration Value
ChangeEmailTokenProviderOptions The token is valid by default for 15 minutes.
EmailConfirmationTokenProviderOptions The token is valid by default for 48 hours.
PasswordResetTokenProviderOptions The token is valid by default for 15 minutes.

You may change the default values of these options by using the services.Configure<> method. For instance, to change the EmailConfirmationTokenProviderOptions you can add the following code to your project

services.Configure<EmailConfirmationTokenProviderOptions>(options => options.TokenLifespan = TimeSpan.FromDays(7));

Reloading Tenants

The recent addition in the Pull Request introduces the IShellReleaseManager, enabling you to initiate a shell release at the request's conclusion via the RequestRelease() method. This action is accessible from any service within the application. However, there's no assurance of immediate fulfillment since another service may utilize the same service and suspend the release request later. Should you need to suspend the request to release the shell, you can utilize the SuspendReleaseRequest() method to suspend the tenant release request and the tenant will not be released.

Reloading Tenants Display Drivers

When implementing a site settings display driver, you have the option to reload the shell (restart the tenant). If you choose to do so, manually releasing the shell context using await _shellHost.ReleaseShellContextAsync(_shellSettings) is unnecessary. Instead, you should use the new IShellReleaseManager.RequestRelease(). This ensures that the shell stays intact until all settings are validated. For instance, in ReverseProxySettingsDisplayDriver, the code was modified from this:

public class ReverseProxySettingsDisplayDriver : SiteDisplayDriver<ReverseProxySettings>
{
    //
    // For  example simplicity, other methods are not visible.
    //

    public override async Task<IDisplayResult> UpdateAsync(ISite site, ReverseProxySettings settings, UpdateEditorContext context)
    {
        var user = _httpContextAccessor.HttpContext?.User;

        if (!await _authorizationService.AuthorizeAsync(user, Permissions.ManageReverseProxySettings))
        {
            return null;
        }

        //
        // For  example simplicity, other logic are not visible.
        //

        // If the settings are valid, release the current tenant.
        if (context.Updater.ModelState.IsValid)
        {
            await _shellHost.ReleaseShellContextAsync(_shellSettings);
        }

        return await EditAsync(site, settings, context);
    }
}

To this:

public class ReverseProxySettingsDisplayDriver : SiteDisplayDriver<ReverseProxySettings>
{
    private readonly IShellReleaseManager _shellReleaseManager;
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly IAuthorizationService _authorizationService;

    public ReverseProxySettingsDisplayDriver(
        // (1) Inject the new service.
        IShellReleaseManager shellReleaseManager
        IHttpContextAccessor httpContextAccessor,
        IAuthorizationService authorizationService)
    {
        _shellReleaseManager = shellReleaseManager;
        _httpContextAccessor = httpContextAccessor;
        _authorizationService = authorizationService;
    }

    //
    // For  example simplicity, other methods are not visible.
    //

    public override async Task<IDisplayResult> UpdateAsync(ISite site, ReverseProxySettings settings, UpdateEditorContext context)
    {
        var user = _httpContextAccessor.HttpContext?.User;

        if (!await _authorizationService.AuthorizeAsync(user, Permissions.ManageReverseProxySettings))
        {
            return null;
        }

        //
        // For  example simplicity, other logic are not visible.
        //

        // (2) Request a release at the end of the request.
        _shellReleaseManager.RequestRelease();

        return await EditAsync(site, settings, context);
    }
}

Infrastructure

Prior to this release, if you had a module with multiple features, you had to use the [Feature("your feature id")] attribute to assign IShapeTableProvider, IDataMigration, and IPermissionProvider to a specific feature of the module that had a feature ID other than the module ID. With pull-request 15793, this is no longer needed. However, [Feature("...")] is still required for the Startup class and controllers when you want the controller action to be available only when a specific feature is enabled.

Site Settings

  • New extension methods named GetSettingsAsync<T>() and GetSettingsAsync<T>("") were added to the ISiteService interface. These methods allow you to retrieve specific settings with a single line of code. For example, to get the LoginSettings, you can now use:
await _siteService.GetSettingsAsync<LoginSettings>();

Previously, achieving the same result required more code:

(await _siteService.GetSiteSettingsAsync()).As<LoginSettings>();
  • A new extension method named GetCustomSettingsAsync() was added to the ISiteService interface. This method allows you to retrieve custom settings. For example, to get custom settings of type 'BlogSettings', you can now use:
ContentItem blogSettings = await _siteService.GetCustomSettingsAsync("BlogSettings");

Previously, achieving the same result required more code:

var siteSettings = await _siteService.GetSiteSettingsAsync();
var blogSettings = siteSettings.As<ContentItem>("BlogSettings");

Content Fields

Before this release, the MarkdownField used a single-line input field by default (named the Standard editor) and offered two different editors: Multi-line with a simple textarea and WYSIWYG with a rich editor. Now, by default, it uses a textarea as the Standard editor, and the separate Multi-line option has been removed.

You have nothing to do, fields configured with the Standard or Multi-line editors previously will both continue to work. Note though, that their editors will now be a textarea.

Liquid

The Culture context variable got the new properties for a fuller Liquid culture support: DisplayName, NativeName, and TwoLetterISOLanguageName. These are the same as their .NET counterparts.

A new filter named supported_cultures was added to allow you to get a list of supported cultures. Here is an example of how to print the names of supported cultures using a list:

{% assign cultures = Culture | supported_cultures %}

<ul>
{% for culture in cultures %}
    <li>{{ culture.Name }}</li>
{% endfor %}
</ul>

Adding properties with additional tag helpers

The new <add-property> tag helper can be placed inside the <shape> tag helpers to add properties to the shape. This is similar to prop-* attributes, but you can also include Razor code as the IHtmlContent property value, which was impossible before. See more details here.

Sealing Types

Many type commonly used by modules can be sealed, which improves runtime performance. While it's not mandatory, we recommend that you consider applying this improvement to your own extensions as well. We've implemented this enhancement in the following pull-requests:

Note

Do not seal classes that are used to create shapes like view-models. Sealing these classes can break your code at runtime, as these classes need to be unsealed to allow for proxy creation.

Workflow Trimming

The Workflows module now has a Trimming feature to automatically clean up old workflow instances. See the corresponding documentation for details.

Obsoleting TrimEnd

The methods public static string TrimEnd(this string value, string trim = "") from OrchardCore.Mvc.Utilities and OrchardCore.ContentManagement.Utilities are being obsoleted and replaced by OrchardCore.ContentManagement.Utilities.TrimEndString(this string value, string suffix). This was done to prepare the code base for the next .NET 9.0 release which has a conflicting method with a different behavior.

Miscellaneous

A new extension has been introduced to simplify the registration of permission providers. Instead of using service.AddScoped<IPermissionProvider, CustomPermissionProvider>(), you can now register it with the more streamlined service.AddPermissionProvider<CustomPermissionProvider>().

What's Changed

New Contributors

Full Changelog: https://github.com/OrchardCMS/OrchardCore/compare/v1.8.4...v2.0.0

Important Upgrade Instructions

Prior to making the leap to Orchard Core 2.0, it's crucial to follow a step-by-step upgrade process. Begin by upgrading your current version to 1.8, resolving any obsolete warnings encountered along the way, and guaranteeing that every site successfully operates on version 1.8.3. This approach ensures that all sites are seamlessly transitioned to 1.8. Subsequently, proceed with confidence to upgrade to 2.0. Prior to initiating this upgrade, thoroughly review the documented list of breaking changes provided below to facilitate a smooth and hassle-free transition.

Breaking Changes

Drop Newtonsoft.Json Support

The utilization of Newtonsoft.Json has been discontinued in both YesSql and OrchardCore. Instead, we have transitioned to utilize System.Text.Json due to its enhanced performance capabilities. To ensure compatibility with System.Text.Json during the serialization or deserialization of objects, the following steps need to be undertaken:

  • If you are using a custom deployment steps, change how you register it by using the new AddDeployment<> extension. This extension adds a new service that is required for proper serialization. For instance, instead of registering your deployment step like this:
services.AddTransient<IDeploymentSource, AdminMenuDeploymentSource>();
services.AddSingleton<IDeploymentStepFactory>(new DeploymentStepFactory<AdminMenuDeploymentStep>());
services.AddScoped<IDisplayDriver<DeploymentStep>, AdminMenuDeploymentStepDriver>();

change it to the following:

services.AddDeployment<AdminMenuDeploymentSource, AdminMenuDeploymentStep, AdminMenuDeploymentStepDriver>();
  • If you are using a custom AdminMenu node, change how you register it by using the new AddAdminNode<> extension. This extension adds a new service that is required for proper serialization. For instance, instead of registering your custom admin menu nodep like this:
services.AddSingleton<IAdminNodeProviderFactory>(new AdminNodeProviderFactory<PlaceholderAdminNode>());
services.AddScoped<IAdminNodeNavigationBuilder, PlaceholderAdminNodeNavigationBuilder>();
services.AddScoped<IDisplayDriver<MenuItem>, PlaceholderAdminNodeDriver>();

change it to the following:

services.AddAdminNode<PlaceholderAdminNode, PlaceholderAdminNodeNavigationBuilder, PlaceholderAdminNodeDriver>();
  • Any serializable object that contains a polymorphic property (a base type that can contain sub-classes instances) needs to register all possible sub-classes this way:
services.AddJsonDerivedTypeInfo<UrlCondition, Condition>();

Alternatively, you can simplify your code by using the newly added extensions to register custom conditions. For example,

services.AddRule<HomepageCondition, HomepageConditionEvaluator, HomepageConditionDisplayDriver>();
  • Any type introduced in custom modules inheriting from MenuItem, AdminNode, Condition, ConditionOperator, Query, SitemapType will have to register the class using the services.AddJsonDerivedTypeInfo<> method. For example,
services.AddJsonDerivedTypeInfo<SqlQuery, Query>();
  • The extension PopulateSettings<T>(model) was removed from PartFieldDefinition. If you are using this method in your code, you'll have to get the settings using the Settings object directly. For instance, if you have this code,
public override IDisplayResult Edit(ContentPartFieldDefinition partFieldDefinition)
{
   return Initialize<NumericFieldSettings>("NumericFieldSettings_Edit", model => partFieldDefinition.PopulateSettings(model));
}

You'll change it to the following:

 public override IDisplayResult Edit(ContentPartFieldDefinition partFieldDefinition)
{
   return Initialize<NumericFieldSettings>("NumericFieldSettings_Edit", model => 
   {
       var settings = partFieldDefinition.GetSettings<NumericFieldSettings>();
       model.Hint = settings.Hint;
       // ...
   });
}

Queries

Previously, any query type had to inherit from Query and required its own distinct type (e.g., SqlQuery, LuceneQuery, ElasticQuery). However, with pull request #16402, creating a custom type for each query type is no longer necessary. This update involved changes to the IQueryManager and IQuerySource interfaces, as well as the Query class. Additionally, a new project, OrchardCore.Queries.Core, was introduced.

A migration process has been implemented to transition existing queries into the new structure, ensuring no impact on existing tenants.

Key Changes

  • Modification of Interfaces and Classes: Updates were made to the IQueryManager, IQuerySource interface, and Query class to accommodate the new structure.
  • Addition of OrchardCore.Queries.Core: This new project supports the updated query handling mechanism.

Implementing IQuerySource

We now request implementations of IQuerySource using keyed services. Below is an example of how to register new implementations:

services.AddQuerySource<SqlQuerySource>(SqlQuerySource.SourceName);

This approach allows for more flexible and modular query handling in OrchardCore.

Handlers

We now have handlers for dealing with queries. To manipulate a query using a handler, implement the IQueryHandler interface. This provides a structured way to extend and customize the behavior of queries within the framework.

Twitter Module

The Twitter module has been rebranded to X.

If you were using the OrchardCore_Twitter configuration key to configure the module, please update the configuration key to OrchardCore_X. The OrchardCore_Twitter key continues to work but will be deprecated in a future release.

Users Module

  • The Login.cshtml has undergone a significant revamp. The previous AfterLogin zone, which allowed filters for injecting shapes, has been replaced. Now, you can inject shapes using drivers by implementing IDisplayDriver<LoginForm>. For example, the 'Forgot Password?' link is injected using the following driver:
public class ForgotPasswordLoginFormDisplayDriver : DisplayDriver<LoginForm>
{
    private readonly ISiteService _siteService;

    public ForgotPasswordLoginFormDisplayDriver(ISiteService siteService)
    {
        _siteService = siteService;
    }

    public override async Task<IDisplayResult> EditAsync(LoginForm model, BuildEditorContext context)
    {
        var settings = await _siteService.GetSettingsAsync<ResetPasswordSettings>();

        if (!settings.AllowResetPassword)
        {
            return null;
        }

        return View("LoginFormForgotPassword_Edit", model).Location("Links:5");
    }
}
  • The ForgotPassword.cshtml has undergone a significant revamp. The previous AfterForgotPassword zone, which allowed filters for injecting shapes, has been replaced. Now, you can inject shapes using drivers by implementing IDisplayDriver<ForgotPasswordForm>. For example, the ReCaptcha shape is injected using the following driver:
public class ReCaptchaForgotPasswordFormDisplayDriver : DisplayDriver<ForgotPasswordForm>
{
    private readonly ISiteService _siteService;

    public ReCaptchaForgotPasswordFormDisplayDriver(ISiteService siteService)
    {
        _siteService = siteService;
    }

    public override async Task<IDisplayResult> EditAsync(ForgotPasswordForm model, BuildEditorContext context)
    {
        var settings = await _siteService.GetSettingsAsync<ReCaptchaSettings>();

        if (!settings.IsValid())
        {
            return null;
        }

        return View("FormReCaptcha_Edit", model).Location("Content:after");
    }
}
  • The ResetPassword.cshtml has undergone a significant revamp. The previous AfterResetPassword zone, which allowed filters for injecting shapes, has been replaced. Now, you can inject shapes using drivers by implementing IDisplayDriver<ResetPasswordForm>. For example, the ReCaptcha shape is injected using the following driver:
public class ReCaptchaResetPasswordFormDisplayDriver : DisplayDriver<ResetPasswordForm>
{
    private readonly ISiteService _siteService;

    public ReCaptchaResetPasswordFormDisplayDriver(ISiteService siteService)
    {
        _siteService = siteService;
    }

    public override async Task<IDisplayResult> EditAsync(ResetPasswordForm model, BuildEditorContext context)
    {
        var settings = await _siteService.GetSettingsAsync<ReCaptchaSettings>();

        if (!settings.IsValid())
        {
            return null;
        }

        return View("FormReCaptcha_Edit", model).Location("Content:after");
    }
}

Previously, users were only able to reset their password through email when the "Reset Password" feature was enabled. However, we've enhanced this functionality to offer users the flexibility of resetting their password using either their email or username. Consequently, the Email property on both the ForgotPasswordViewModel and ResetPasswordViewModel have been deprecated and should be replaced with the new UsernameOrEmail property for password resets.

  • The Register.cshtml has undergone a significant revamp. The previous AfterRegister zone, which allowed filters for injecting shapes, has been replaced. Now, you can inject shapes using drivers by implementing IDisplayDriver<RegisterUserForm>. For example, the ReCaptcha shape is injected using the following driver:
public class RegisterUserFormDisplayDriver : DisplayDriver<RegisterUserForm>
{
    private readonly ISiteService _siteService;

    public RegisterUserFormDisplayDriver(ISiteService siteService)
    {
        _siteService = siteService;
    }

    public override async Task<IDisplayResult> EditAsync(RegisterUserForm model, BuildEditorContext context)
    {
        var settings = await _siteService.GetSettingsAsync<ReCaptchaSettings>();

        if (!settings.IsValid())
        {
            return null;
        }

        return View("FormReCaptcha_Edit", model).Location("Content:after");
    }
}
  • Two-factor authentication (2FA) is now available for all users. Previously, only users with permission to access the admin area could set up their own 2FA methods, although all users could still use 2FA if it was set up by an admin. To enable this change, the method IsRequiredAsync() in both the ITwoFactorAuthenticationHandlerCoordinator and ITwoFactorAuthenticationHandler interfaces has been updated to IsRequiredAsync(IUser user).

  • A new UserConfirmedEvent workflow event is now available. This event is triggered when a user successfully confirms their email address using the link sent during user registration.

  • We are introducing a new interface, IUserTimeZoneService, to replace the existing UserTimeZoneService. If your project directly injects UserTimeZoneService, you will need to switch to using IUserTimeZoneService instead.

Notifications Module

The endpoint for marking notifications as read has been updated. Previously the route was /api/notifications/read, and now it is /Notifications/MarkAsRead. This change will only affect you if you have overridden the UserNotificationNavbar view.

Contents

The IContentManager interface was modified. The method Task<IEnumerable<ContentItem>> GetAsync(IEnumerable<string> contentItemIds, bool latest = false) was removed. Instead use the method that accepts VersionOptions by providing either VersionOptions.Latest or VersionOptions.Published will be used by default.

Additionally, the GetContentItemByHandleAsync(string handle, bool latest = false) and GetContentItemByIdAsync(string contentItemId, bool latest = false) were removed from IOrchardHelper. Instead use the method that accepts VersionOptions by providing either VersionOptions.Latest or VersionOptions.Published will be used by default.

Lastly, we dropped support for AllVersions option in VersionOptions. Instead, you can use the new GetAllVersionsAsync(string contentItemId) method on IContentManager.

Media Indexing

Previously, .pdf files were automatically indexed in the search providers (Elasticsearch, Lucene or Azure AI Search). Now, if you want to continue to index .PDF file you'll need to enable the OrchardCore.Media.Indexing.Pdf feature.

Additionally, if you need to enable indexing for text file with .txt, .md extensions, you will need the OrchardCore.Media.Indexing.Text feature.

If you need to enable indexing for other extensions like (.docx, or .pptx), you will need the OrchardCore.Media.Indexing.OpenXML feature.

SMS Module

In the past, we utilized the injection of ISmsProvider for sending SMS messages. However, in this release, it is now necessary to inject ISmsService instead.

Additionally, Twilio provider is no longer enabled by default. If you want to use Twilio SMS provider, you must enable the provider by visiting the email settings Configuration > Settings > Email and see the Twilio tab.

Display Management

In this release, the signatures of the UpdateAsync() method within the SectionDisplayDriver base class have undergone modifications. Previously, these signatures accepted the BuildEditorContext parameter. However, with this update, all signatures now require the UpdateEditorContext instead. This alteration necessitates that every driver inheriting from this class adjusts their contexts accordingly.

Here are the updated signatures:

  1. From: Task<IDisplayResult> UpdateAsync(TModel model, BuildEditorContext context) To: Task<IDisplayResult> UpdateAsync(TModel model, UpdateEditorContext context)

  2. From: Task<IDisplayResult> UpdateAsync(TModel model, TSection section, BuildEditorContext context) To: Task<IDisplayResult> UpdateAsync(TModel model, TSection section, UpdateEditorContext context)

  3. From: Task<IDisplayResult> UpdateAsync(TSection section, BuildEditorContext context) To: Task<IDisplayResult> UpdateAsync(TSection section, UpdateEditorContext context). This method is now obsolete.

These adjustments ensure compatibility and adherence to the latest conventions within the DisplayDriver class.

Additionally, we've enhanced display performance by streamlining the display drivers. To achieve this, the following methods have been marked as obsolete:

  • IDisplayResult Display(TModel model)
  • IDisplayResult Edit(TModel model)

and removed the following methods:

  • Task<IDisplayResult> DisplayAsync(TModel model, IUpdateModel updater)
  • IDisplayResult Display(TModel model, IUpdateModel updater)
  • Task<IDisplayResult> UpdateAsync(TModel model, IUpdateModel updater)
  • Task<IDisplayResult> EditAsync(TModel model, IUpdateModel updater)
  • IDisplayResult Edit(TModel model, IUpdateModel updater)

At the same time, we've introduced the following static methods in the DisplayDriverBase:

  • Task<IDisplayResult> CombineAsync(params IDisplayResult[] results)
  • Task<IDisplayResult> CombineAsync(IEnumerable<IDisplayResult> results)

Similar refactoring was done to SectionDisplayDriver class. The following methods have been marked as obsolete:

  • Display(TSection section, BuildDisplayContext context)
  • Display(TSection section)
  • Edit(TSection section, BuildEditorContext context)
  • UpdateAsync(TSection section, UpdateEditorContext context)
  • UpdateAsync(TSection section, IUpdateModel updater, string groupId)

and removed the following methods:

  • UpdateAsync(TModel model, TSection section, IUpdateModel updater, UpdateEditorContext context)
  • UpdateAsync(TSection section, IUpdateModel updater, UpdateEditorContext context)
  • UpdateAsync(TSection section, IUpdateModel updater, string groupId)
  • UpdateAsync(TSection section, UpdateEditorContext context)
  • EditAsync(TSection section, BuildEditorContext context)
  • DisplayAsync(TSection section, BuildDisplayContext context)

Note

To enhance your project's performance, address any warnings generated by the use of these obsolete methods. Please note that these methods will be removed in the next major release.

A new SiteDisplayDriver base class was introduced to simplify the process of adding a UI settings.

Note

With the new SiteDisplayDriver base class for custom settings, you no longer need explicit checks like context.GroupId.Equals(GroupId, StringComparison.OrdinalIgnoreCase) as the base class handles this check for you.

A new extension has been introduced to simplify the registration of site setting drivers. Instead of using service.AddScoped<IDisplayDriver<ISite>, CustomSettingsDisplayDriver>(), you can now register it with the more streamlined service.AddSiteDisplayDriver<CustomSettingsDisplayDriver>().

Similarly, the ContentPartDisplayDriver base class have undergone modifications. The following methods were marked obsolete:

  • IDisplayResult Display(TPart part)
  • IDisplayResult Edit(TPart part)
  • Task<IDisplayResult> UpdateAsync(TPart part, IUpdateModel updater)

At the same time, the following method was removed:

  • Task<IDisplayResult> UpdateAsync(TPart part, IUpdateModel updater)

Moreover, the ContentFieldDisplayDriver base class have undergone modifications. The following methods were marked obsolete:

  • UpdateAsync(TField field, IUpdateModel updater, UpdateFieldEditorContext context)
  • Update(TField field, IUpdateModel updater, UpdateFieldEditorContext context)

At the same time, the following method was added:

  • UpdateAsync(TField field, UpdateFieldEditorContext context)

GraphQL Module

The GraphQL schema may change because fields are now always added to the correct part. Previously, additional fields may have been added to the parent content item type directly.

You may have to adjust your GraphQL queries in that case.

Additionally, the GetAliases method in the IIndexAliasProvider interface is now asynchronous and has been renamed to GetAliasesAsync. Implementations of this interface should be modified by updating the method signature and ensure they handle asynchronous operations correctly.

Media fields

The schema for media fields has been updated. Instead of separate lists for fileNames, urls, paths, and mediatexts, these fields are now grouped under a single files type to simplify data binding on the front end.

Please update your existing queries as shown in the following example:

Before:

{
  article {
    contentItemId
    image {
      mediatexts(first: 10)
      urls(first: 10)
    }
  }
}

After:

{
  article {
    contentItemId
    image {
      files(first: 10) {
        mediaText
        url
      }
    }
  }
}

A compatibility switch has been introduced to allow continued use of the old fileNames, urls, paths, and mediatexts fields. To restore the legacy fields, add the following AppContext switch to your startup class:

AppContext.SetSwitch("OrchardCore.Media.EnableLegacyMediaFieldGraphQLFields", true);

Resource Management

Previously the <resources type="..." /> Razor tag helper and the {% resources type: "..." %} Liquid tag were only capable of handling a hard-coded set of resource definition types (script, stylesheet, etc). Now both can be extended with IResourcesTagHelperProcessor to run custom rendering logic. To make this possible, the OrchardCore.ResourceManagement.TagHelpers.ResourceType and OrchardCore.Resources.Liquid.ResourcesTag.ResourceType enums have been replaced with a common OrchardCore.ResourceManagement.ResourceTagType. It was renamed to avoid confusion with ResourceDefinition.Type. This change does not affect the uses of the Razor tag helper or the Liquid tag in templates of user projects, but it affects published releases such as NuGet packages. Any themes and modules that contain <resources type="..." /> in a Razor file (e.g. Layout.cshtml) must be re-released to generate the updated .cshtml.cs files, because the old pre-compiled templates would throw MissingMethodException.

Workflows Module

Previously, the CreateContentTask, RetrieveContentTask, and UpdateContentTask in the workflow's CorrelationId was updated each time with the ContentItemId of the current content item. Now, the CorrelationId value will only be updated if the current workflow's CorrelationId is empty.

Additionally, a new workflow-scoped script function setCorrelationId(id:string): void was added, that you can use to update the workflow's CorrelationId.

The BuildNavigationAsync method in the INavigationProvider interface was optimized by changing its return type from Task to ValueTask.

Change Logs

Azure AI Search Module

Introducing a new "Azure AI Search" module, designed to empower you in the administration of Azure AI Search indices. When enabled with the "Search" module, it facilitates frontend full-text search capabilities through Azure AI Search. For more info read the Azure AI Search docs.

New ImageSharp Image Caches for Azure Blob and AWS S3 Storage

The Microsoft Azure Media and Amazon S3 Media modules have new features for replacing the default PhysicalFileSystemCache of ImageSharp that stores resized images in the local App_Data folder. Instead, you can now use Azure Blob Storage with the Azure Media ImageSharp Image Cache feature (that utilizes AzureBlobStorageImageCache), and AWS S3 with the Amazon Media ImageSharp Image Cache feature (that utilizes AWSS3StorageCache). Depending on your use case, this can provide various advantages. Check out the Azure Media and the Amazon S3 Media docs for details.

Deployment Module

New Extensions

Added new extensions to make registering custom deployment step easier:

  • services.AddDeployment<TSource, TStep>().
  • services.AddDeployment<TSource, TStep, TDisplayDriver>().
  • services.AddDeploymentWithoutSource<TStep, TDisplayDriver>().

Recipe Executions

Before this release, implementations of IRecipeStepHandler or IRecipeEventHandler would throw exceptions to report errors if something failed to import. However, this is no longer the recommended approach for error reporting.

Now, to handle errors, we have introduced a new property named Errors in the RecipeExecutionContext. This property allows you to log errors instead of throwing exceptions. These errors should be localized and must not contain any sensitive data, as they are visible to the end user. Exceptions are still used for logging additional information, but these are not shown to the end user.

Additionally, if an error occurs, a new custom exception, RecipeExecutionException, is thrown. For more info visit the related pull-request.

Workflows Module

The method Task TriggerEventAsync(string name, IDictionary<string, object> input = null, string correlationId = null, bool isExclusive = false, bool isAlwaysCorrelated = false) was changed to return Task<IEnumerable<WorkflowExecutionContext>> instead.

New Events

The following events were added:

  • UserConfirmedEvent: this event triggers when a user successfully confirms their email address after registration.

GraphQL Module

When identifying content types for GraphQL exposure, we identify those without a stereotype to provide you with control over the behavior of stereotyped content types. A new option, DiscoverableSterotypes, has been introduced in GraphQLContentOptions. This allows you to specify stereotypes that should be discoverable by default.

For instance, if you have several content types stereotyped as ExampleStereotype, you can make them discoverable by incorporating the following code into the startup class:

services.Configure<GraphQLContentOptions>(options =>
{
    options.DiscoverableSterotypes.Add("ExampleStereotype");
});

The GraphQL module now allows for filtering content fields, making it easier to query specific content fields using GraphQL. This enhancement relies on having the OrchardCore.ContentFields.Indexing.SQL module enabled.

To filter content items based on a specific content field, you can use a query like this:

product(where: {price: {amount_gt: 10}}) {
    contentItemId
    displayText
    price {
        amount
    }
}

Email Module

The OrchardCore.Email module has undergone a refactoring process with no breaking changes. However, there are compile-time warnings that are recommended to be addressed. Here is a summary of the changes:

  • Previously, we used the injection of ISmtpService for sending email messages. In this release, it is now necessary to inject IEmailService instead.
  • The SMTP related services are now part of a new module named OrchardCore.Email.Smtp. To use the SMTP provider for sending emails, enable the OrchardCore.Email.Smtp feature.
  • If you were using the OrchardCore_Email configuration key to set up the SMTP provider for all tenants, please update the configuration key to OrchardCore_Email_Smtp. The OrchardCore_Email key continues to work but will be deprecated in a future release.
  • A new email provider was added to allow you to send email using Azure Communication Services Email. Click here to read more about the ACS module.

Menus and AdminMenus now support specifying the target property.

Admin Menu

The admin menu has been optimized with significant performance improvements and code enhancements. When adding a navigation provider to the admin menu, it is now recommended to use the new AdminNavigationProvider class rather than directly implementing the INavigationProvider interface in your project.

Furthermore, when passing route values to actions, it's a best practice to store these values in constant variables for maintainability and clarity. Below is an example demonstrating this approach:

public sealed class AdminMenu : AdminNavigationProvider
{
    private static readonly RouteValueDictionary _routeValues = new()
    {
        { "area", "OrchardCore.Settings" },
        { "groupId", AdminSiteSettingsDisplayDriver.GroupId },
    };

    internal readonly IStringLocalizer S;

    public AdminMenu(IStringLocalizer<AdminMenu> stringLocalizer)
    {
        S = stringLocalizer;
    }

    protected override ValueTask BuildAsync(NavigationBuilder builder)
    {
        builder
            .Add(S["Configuration"], configuration => configuration
                .Add(S["Settings"], settings => settings
                    .Add(S["Admin"], S["Admin"].PrefixPosition(), admin => admin
                        .AddClass("admin")
                        .Id("admin")
                        .Action("Index", "Admin", _routeValues)
                        .Permission(PermissionsAdminSettings.ManageAdminSettings)
                        .LocalNav()
                    )
                )
            );

        return ValueTask.CompletedTask;
    }
}

A new extension has been introduced to simplify the registration of navigation providers. Instead of using services.AddNavigationProvider<AdminMenu>(), you can now register it with the more streamlined services.AddNavigationProvider<AdminMenu>();.

Admin Routes

The [Admin] attribute now has optional parameters for a custom route template and route name. It works just like the [Route(template, name)] attribute, except it prepends the configured admin prefix. You can apply it to the controller or the action; if both are specified then the action's template takes precedence. The route name can contain {area}, {controller}, and {action}, which are substituted during mapping so the names can be unique for each action. This means you don't have to define these admin routes in your module's Startup class anymore, but that option is still available and supported. Take a look at this example:

[Admin("Person/{action}/{id?}", "Person{action}")]
public sealed class PersonController : Controller
{
    [Admin("Person", "Person")]
    public IActionResult Index() { ... }

    public IActionResult Create() { ... }

    public IActionResult Edit(string id) { ... }
}

In this example, (if the admin prefix remains the default "Admin") you can reach the Index action at ~/Admin/Person (or by the route name Person), because its own action-level attribute took precedence. You can reach Create at ~/Admin/Person/Create (route name PersonCreate) and Edit for the person whose identifier string is "john-doe" at ~/Admin/Person/john-doe (route name PersonEdit).

Users Module

Added a new User Localization feature that allows to be able to configure the culture per user from the admin UI.

Added a new Navbar() function to Liquid to allow building the Navbar shape using Liquid. Here are the steps needed to add the Navbar shape into your custom Liquid shape:

  1. Construct the shape at the beginning of the layout.liquid file to enable navbar items to potentially contribute to the resources output as necessary.

{% assign navbar = Navbar() | shape_render %}
2. Subsequently in the layout.liquid file, invoke the shape at the location where you want to display it.

{{ navbar }}

Notifications Module

TheINotificationMessage interface was updated to includes the addition of a Subject field, which facilitates the rendering of notification titles. Moreover, the existing Summary field has been transitioned to HTML format. This adjustment enables the rendering of HTML notifications in both the navigation bar and the notification center. Consequently, HTML notifications can now be created, affording functionalities such as clickable notifications.

Furthermore, the introduction of the NotificationOptions provides configuration capabilities for the notifications module. This structure comprises the following attributes:

  • TotalUnreadNotifications: This property specifies the maximum number of unread notifications that are displayed in the navigation bar. By default, it is set to 10.
  • DisableNotificationHtmlBodySanitizer: Notifications generated from workflows have their HtmlBody sanitized by default. This property allows you to disable the sanitization process if needed.
  • AbsoluteCacheExpirationSeconds: Specifies the absolute maximum duration, in seconds, for which the top unread user notifications are cached when caching is enabled. A value of 0 does not disable caching but indicates that there is no fixed expiration time for the cache. You can set this value to define a maximum lifespan for the cached data before it is invalidated.
  • SlidingCacheExpirationSeconds: Defines the sliding cache duration, in seconds, for unread user notifications when caching is enabled. By default, the cache is refreshed and extended for up to 1800 seconds (30 minutes) after each user activity. To disable sliding expiration, you can set this value to 0.

Secure media files

Introduces a new "Secure Media" feature for additional control over who can access media files. For more info read the Secure Media docs.

Users Module

Enhanced functionality has been implemented, giving developers the ability to control the expiration time of different tokens, such as those for password reset, email confirmation, and email change, which are sent through the email service. Below, you'll find a comprehensive list of configurable options along with their default values:

Class Name Default Expiration Value
ChangeEmailTokenProviderOptions The token is valid by default for 15 minutes.
EmailConfirmationTokenProviderOptions The token is valid by default for 48 hours.
PasswordResetTokenProviderOptions The token is valid by default for 15 minutes.

You may change the default values of these options by using the services.Configure<> method. For instance, to change the EmailConfirmationTokenProviderOptions you can add the following code to your project

services.Configure<EmailConfirmationTokenProviderOptions>(options => options.TokenLifespan = TimeSpan.FromDays(7));

Reloading Tenants

The recent addition in the Pull Request introduces the IShellReleaseManager, enabling you to initiate a shell release at the request's conclusion via the RequestRelease() method. This action is accessible from any service within the application. However, there's no assurance of immediate fulfillment since another service may utilize the same service and suspend the release request later. Should you need to suspend the request to release the shell, you can utilize the SuspendReleaseRequest() method to suspend the tenant release request and the tenant will not be released.

Reloading Tenants Display Drivers

When implementing a site settings display driver, you have the option to reload the shell (restart the tenant). If you choose to do so, manually releasing the shell context using await _shellHost.ReleaseShellContextAsync(_shellSettings) is unnecessary. Instead, you should use the new IShellReleaseManager.RequestRelease(). This ensures that the shell stays intact until all settings are validated. For instance, in ReverseProxySettingsDisplayDriver, the code was modified from this:

public class ReverseProxySettingsDisplayDriver : SiteDisplayDriver<ReverseProxySettings>
{
    //
    // For  example simplicity, other methods are not visible.
    //

    public override async Task<IDisplayResult> UpdateAsync(ISite site, ReverseProxySettings settings, UpdateEditorContext context)
    {
        var user = _httpContextAccessor.HttpContext?.User;

        if (!await _authorizationService.AuthorizeAsync(user, Permissions.ManageReverseProxySettings))
        {
            return null;
        }

        //
        // For  example simplicity, other logic are not visible.
        //

        // If the settings are valid, release the current tenant.
        if (context.Updater.ModelState.IsValid)
        {
            await _shellHost.ReleaseShellContextAsync(_shellSettings);
        }

        return await EditAsync(site, settings, context);
    }
}

To this:

public class ReverseProxySettingsDisplayDriver : SiteDisplayDriver<ReverseProxySettings>
{
    private readonly IShellReleaseManager _shellReleaseManager;
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly IAuthorizationService _authorizationService;

    public ReverseProxySettingsDisplayDriver(
        // (1) Inject the new service.
        IShellReleaseManager shellReleaseManager
        IHttpContextAccessor httpContextAccessor,
        IAuthorizationService authorizationService)
    {
        _shellReleaseManager = shellReleaseManager;
        _httpContextAccessor = httpContextAccessor;
        _authorizationService = authorizationService;
    }

    //
    // For  example simplicity, other methods are not visible.
    //

    public override async Task<IDisplayResult> UpdateAsync(ISite site, ReverseProxySettings settings, UpdateEditorContext context)
    {
        var user = _httpContextAccessor.HttpContext?.User;

        if (!await _authorizationService.AuthorizeAsync(user, Permissions.ManageReverseProxySettings))
        {
            return null;
        }

        //
        // For  example simplicity, other logic are not visible.
        //

        // (2) Request a release at the end of the request.
        _shellReleaseManager.RequestRelease();

        return await EditAsync(site, settings, context);
    }
}

Infrastructure

Prior to this release, if you had a module with multiple features, you had to use the [Feature("your feature id")] attribute to assign IShapeTableProvider, IDataMigration, and IPermissionProvider to a specific feature of the module that had a feature ID other than the module ID. With pull-request 15793, this is no longer needed. However, [Feature("...")] is still required for the Startup class and controllers when you want the controller action to be available only when a specific feature is enabled.

Site Settings

  • New extension methods named GetSettingsAsync<T>() and GetSettingsAsync<T>("") were added to the ISiteService interface. These methods allow you to retrieve specific settings with a single line of code. For example, to get the LoginSettings, you can now use:
await _siteService.GetSettingsAsync<LoginSettings>();

Previously, achieving the same result required more code:

(await _siteService.GetSiteSettingsAsync()).As<LoginSettings>();
  • A new extension method named GetCustomSettingsAsync() was added to the ISiteService interface. This method allows you to retrieve custom settings. For example, to get custom settings of type 'BlogSettings', you can now use:
ContentItem blogSettings = await _siteService.GetCustomSettingsAsync("BlogSettings");

Previously, achieving the same result required more code:

var siteSettings = await _siteService.GetSiteSettingsAsync();
var blogSettings = siteSettings.As<ContentItem>("BlogSettings");

Content Fields

Before this release, the MarkdownField used a single-line input field by default (named the Standard editor) and offered two different editors: Multi-line with a simple textarea and WYSIWYG with a rich editor. Now, by default, it uses a textarea as the Standard editor, and the separate Multi-line option has been removed.

You have nothing to do, fields configured with the Standard or Multi-line editors previously will both continue to work. Note though, that their editors will now be a textarea.

Liquid

The Culture context variable got the new properties for a fuller Liquid culture support: DisplayName, NativeName, and TwoLetterISOLanguageName. These are the same as their .NET counterparts.

A new filter named supported_cultures was added to allow you to get a list of supported cultures. Here is an example of how to print the names of supported cultures using a list:

{% assign cultures = Culture | supported_cultures %}

<ul>
{% for culture in cultures %}
    <li>{{ culture.Name }}</li>
{% endfor %}
</ul>

Adding properties with additional tag helpers

The new <add-property> tag helper can be placed inside the <shape> tag helpers to add properties to the shape. This is similar to prop-* attributes, but you can also include Razor code as the IHtmlContent property value, which was impossible before. See more details here.

Sealing Types

Many type commonly used by modules can be sealed, which improves runtime performance. While it's not mandatory, we recommend that you consider applying this improvement to your own extensions as well. We've implemented this enhancement in the following pull-requests:

Note

Do not seal classes that are used to create shapes like view-models. Sealing these classes can break your code at runtime, as these classes need to be unsealed to allow for proxy creation.

Workflow Trimming

The Workflows module now has a Trimming feature to automatically clean up old workflow instances. See the corresponding documentation for details.

Obsoleting TrimEnd

The methods public static string TrimEnd(this string value, string trim = "") from OrchardCore.Mvc.Utilities and OrchardCore.ContentManagement.Utilities are being obsoleted and replaced by OrchardCore.ContentManagement.Utilities.TrimEndString(this string value, string suffix). This was done to prepare the code base for the next .NET 9.0 release which has a conflicting method with a different behavior.

Miscellaneous

A new extension has been introduced to simplify the registration of permission providers. Instead of using service.AddScoped<IPermissionProvider, CustomPermissionProvider>(), you can now register it with the more streamlined service.AddPermissionProvider<CustomPermissionProvider>().