Confinity Documentation
  • Latest Version
  • Latest Version
  • Getting Started

    • Introduction
    • Core Concepts
    • Create an Application
    • Glossary
  • Essentials

    • Authentication & SSO
    • Breaking Changes
    • Roslyn Source Analyzers
    • Changelog
    • ConfinityContent
    • ConfinitySelectable
    • Confinity Schedules
    • Data Seeding
    • Development guidelines [WIP]
    • Entity App
    • Entity Form
    • Entity Permissions
    • Frontend Configuration
    • Images
    • Known Issues
    • Localization
    • Migrations
    • Modules [WIP]
    • On-Site Editing
    • Settings
    • Cascade Delete
    • Replication
    • Infrastructure
  • Modules

    • Analytics Module
    • Assets Module
    • Blog Module
    • Cookie Consent Module
    • Forms Module
    • Friendly Captcha (Forms Module )
    • GeoIP Module
    • Htmx
    • Mail Module
    • Mailing Module
    • MediaPlayer Module
    • GoogleMyBusiness Module
    • OpenTelemetry Module
    • Pages Module [WIP]
    • Pattern Library Module
    • SIX Saferpay (worldline) Module
    • Products Module
    • Search Module
    • Wizard Module
  • Guides

    • Create a Custom Entity App Form Element
    • Date and Time
    • Entity Change Listener
    • File Upload / Temp File
    • HTTP security headers
    • conventions [WIP]
    • How to use Confinity with nginx
    • Notifications
    • Nullability
    • Rename Entity
    • Schedules
    • Useful snippets
    • Content Localization
  • Design Guidelines

    • Introduction
    • Page Components
    • Forms Module

Breaking Changes

On this site you'll find all breaking changes introduced with a new Confinity release.

For not breaking changes and diagnostics see Diagnostics

2025.1.9.0

Component Rendering

  • Confinity page and view components don't receive a div wrapper anymore. To reenable this feature, set the option ConfinityPages.EnableLegacyComponentWrapper to true

2025.1.8.0

Products

The property ProductAttribute.Type is now required and must therefore always be set.

2025.1.7.0 [migration]

Pattern library

  • ComponentElement does not have a property Items anymore. Use the method Items() instead.

Migrations

  • Only relevant when using the products module: This migration is implemented as an extension method, just create a new migration and replace the Up method with the following code (Sql Server & Postgres only):
protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.Confinity_2025_1_7_Up(); // set bool arguments accordingly
}

2025.1.6.0

  • ICspBuilder.GetNonce was removed use ICspBuilder.GetNonceAttribute() instead.
  • ICspBuilder moved from namespace Confinity to Confinity.Web

Example update:

// old
@{string nonce = csp.GetNonce(CspDirectives.ScriptSrc);}
<script nonce="{{nonce}}">

// new 
@{string nonce = csp.GetNonceAttribute(CspDirectives.ScriptSrc);}
<script {{nonce}}>

2025.1.5.0

IImageAssetEditor was removed.

2025.1.4.0

Excel Export

The methods in Confinity.EntityApp.Exporter.Exporter now always sanitize the values to prevent XSLX-injection with through formulas. If your export should contain formulas as before, use the newly introduced overloads with 'Unsafe' in the name.

  • DictionaryToXlsxAsync -> DictionaryToXlsxUnsafeAsync
  • ToXlsxAsync -> ToXlsxUnsafeAsync

2025.1.3.0 [migration]

Migrations

  • This migration is implemented as an extension method, just create a new migration and replace the Up method with the following code (Sql Server & Postgres only):
protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.Confinity_2025_1_3_Up();
}

This will apply the following migrations:

  • A new flag IsSuperadmin will be added to ConfinityRole
  • IsSuperadmin will be set to true on the existing role with the key confinity.admin

2025.1.2.0 [migration]

Products

SelectStringAttributeDefinition got replaced by SelectSingleOptionAttributeDefinition.

The following classes are now internal:

  • StringListValue
  • IntegerListValue
  • DoubleListValue

When evaluating a AttributeValueModel.Value, expect the following types instead:

  • ICollection<double>
  • ICollection<int>
  • ICollection<string>

See Products for all possible types.

Migration

  • Added index IX_ConfinityReplicationSchedule_TransactionId.

2025.1.1.0 [migration]

Miscellaneous

  • Added a new source analyzer that prevents default values for nullable properties in ConfinityContents.

Migration

  • The index IX_ConfinityAsset_Filename_FolderId has been removed.

Forms

  • Replaced the property FormFieldModel.NotSetValue with the method FormFieldModel.GetNotSetValue();

2025.1.0.0 [migration]

Miscellaneous

  • Confinity is now updated to use .Net 9. The recommended way to update is the .NET Upgrade Assistant.
  • The Confinity.Application assembly now references the Confinity.SourceAnalyzers so that this must no longer be included explicitly.

Blog

  • IQueryOptions, used in IBlogService methods, has a new optional parameter IgnoreEntityPermissions.

Pages

  • IPageModel.ChildPageModels moved to IPageService.GetChildPages
  • PageModelExtensions.PreorderTraversal expects mandatory parameter IPageService
  • Implementations of IDynamicPageResolver<T> should always use IgnoreQueryFilters() to read data. Check Reading ConfinityEntities
  • LinkModel requires a Title to be initialized (this helps the developer to not forget to set the Title). The old value was an empty string.
  • ExternalLinkModel requires a Url to be initialized (this helps the developer to not forget to set the Url). The old value was an empty string.

Assets

  • The Confinity entity Asset has a new property PublicName that should be used outside of entity apps, e.g. in a galleries.
  • The interface IAsset has a new method PublicName() that should be used outside of entity apps, e.g. in a galleries.

Products

  • When working with AttributeValueModel (e.g. by calling GetAttributeById, GetAttributes or GetGroupedAttributes) the Value property no longer returns entities or lists of entities but EntityRef<T> or ICollection<EntityRef<T> (where T is the entity).

Entity App

Exporter

  • The following methods are now async and thereby got renamed with the Async suffix:
    • Exporter.DictionaryToXlsxAsync (in addition, the first parameter changed to IEnumerable<IDictionary<string, object?>>)
    • Exporter.ToXlsxAsync

ISelectOptionsProvider

  • The return value and parameters of ISelectOptionsProvider<T>.GetOptions changed
Task<IReadOnlyCollection<ISelectOption<T>>> GetOptions(GetOptionsParameters getOptionsParameters);

AddSelectSingle/AddSelectMultiple/AddSelectAsset/AddSelectAssets

The entity app form configuration changes slightly. For AddSelectSingle, AddSelectMultiple, AddSelectAsset and AddSelectAssets the generic types must be given explicitly, e.g.

tab.AddSelectSingle<EmployeeViewModel, Employee>(e => e.Picture!, a => a.Title);
tab.AddSelectMultiple<EmployeeViewModel, Employee>(e => e.TeamMembers!, a => a.FirstName + " " + a.LstName);
tab.AddSelectAsset<EmployeeViewModel>(e => e.Picture!);

AddComputed

  • When defining an entity form with AddComputed(...) (the non-generic call), make sure that all used entity properties are either reference by other form elements or call DependentProperty for those properties, e.g.
tab.AddComputed(p => a.News.Count(), "News count").DependentProperty(a => a.News);

Confinity Contents

  • ConfinityContents got new restrictions enforced by source analyzers. The provided code fixes and error messages should be enough to migrate. For details see (Confinity Content)[./confinity-content/confinity-content.md].
  • It is no longer possible to have a reference to a ConfinityEntity in your ConfinityContent. Instead, the ConfinityEntity must be wrapped in a EntityRef<T>, e.g.
public class EmployeeViewModel : ConfinityContent
{
    public string FullName {get; set; } = "";
    public EntityRef<Employee>? Boss { get; set; }
    public ICollection<EntityRef<Employee>>? TeamMembers { get; set; }
    public EntityRef<Asset>? Picture { get; set; }
}

AddWysiwyg

  • The property type must be migrated from string to IHtmlContent.
    • to get a new IHtmlContent you can use new HtmlString(X)
    • HtmlContentUtil provides utils to create a nullable type (FromNullable), to check if it is empty (IsNullOrWhiteSpace) or to convert to string. Conversion to string should be avoided where possible or to HTML encode a string (GetEncoded).

Migration

  • Create a migration with EF.Core and insert the following call in the beginning (either for Postgres or SQL Server)

Postgres

migrationBuilder.Sql(
    """
    UPDATE "ConfinityAsset"
    SET "Filename" = subquery."WithoutExtension"
                        || '_'
                        || subquery.row_number - 1
                        || subquery."Extension"
    FROM (SELECT "Id",
                 (regexp_matches("ConfinityAsset"."Filename", '(.+?)(\.[^.]*$|$)'))[1] "WithoutExtension",
                 (regexp_matches("ConfinityAsset"."Filename", '(.+?)(\.[^.]*$|$)'))[2] "Extension",
                 ROW_NUMBER() OVER (
                     PARTITION BY "Filename", "FolderId" ORDER BY "Id"
                     ) AS row_number
          FROM "ConfinityAsset") AS subquery
    WHERE "ConfinityAsset"."Id" = subquery."Id"
      AND subquery.row_number > 1;
        
    UPDATE "__ConfinityHistory_ConfinityAsset" history
    SET "Entity_Filename" = asset."Filename"
    FROM "ConfinityAsset" asset
    where history."Entity_Id" = asset."Id"
      AND NOT ISFINITE(history."To")
      AND history."Entity_Filename" <> asset."Filename";
    """);

Sql Server

migrationBuilder.Sql(
    """
    ;
    WITH CTE AS
             (SELECT Filename,
                     REVERSE(SUBSTRING(REVERSE(Filename), CHARINDEX('.', REVERSE(Filename)) + 1,
                                       LEN(Filename)))                                   AS WithoutExtension,
                     REVERSE(LEFT(reverse(Filename), CHARINDEX('.', REVERSE(Filename)))) AS Extension,
                     ROW_NUMBER() OVER (
                         PARTITION BY Filename, Filename ORDER BY Id
                         )                                                               AS row_number
              FROM ConfinityAsset)
    UPDATE CTE
    SET Filename = WithoutExtension + '_' + CONVERT(varchar(10), row_number - 1) + "Extension"
    WHERE row_number > 1;
    
    UPDATE history
    SET history.Entity_Filename = asset."Filename" 
    FROM ConfinityAsset asset
    INNER JOIN __ConfinityHistory_ConfinityAsset history
        ON history.Entity_Id = asset.Id
    WHERE history.[To] = '9999-12-31 23:59:59.9999999 +00:00'
      AND history.Entity_Filename <> asset.Filename;
    """);
  • If you are using the Confinity.Forms module, insert the following call in the end (either for Postgres or SQL Server)

Postgres

migrationBuilder.Sql(
    """
    INSERT INTO "__ConfinityHistory_ConfinityFormSubmission"
    (
    "Id",
    "Entity_ClientIpAddress",
    "Entity_ConfinityFormId",
    "Entity_ConfinityMetadata_ModifiedAt",
    "Entity_ConfinityMetadata_ModifiedById",
    "Entity_FormData",
    "Entity_Id",
    "Entity_PageUrl",
    "From",
    "To"
    )
    SELECT gen_random_uuid(),
    "ClientIpAddress",
    "ConfinityFormId",
    "ConfinityMetadata_ModifiedAt",
    "ConfinityMetadata_ModifiedById",
    "FormData",
    "Id",
    "PageUrl",
    "ConfinityMetadata_ModifiedAt",
    'infinity'
    FROM "ConfinityFormSubmission";
    """);

Sql Server

migrationBuilder.Sql(
    """
    INSERT INTO __ConfinityHistory_ConfinityFormSubmission
    (Id, 
     Entity_ClientIpAddress,
     Entity_ConfinityFormId,
     Entity_ConfinityMetadata_ModifiedAt,
     Entity_ConfinityMetadata_ModifiedById,
     Entity_FormData,
     Entity_Id,
     Entity_PageUrl,
     "From",
     "To")
    SELECT NEWID(),
           ClientIpAddress,
           ConfinityFormId,
           ConfinityMetadata_ModifiedAt,
           ConfinityMetadata_ModifiedById,
           FormData,
           Id,
           PageUrl,
           ConfinityMetadata_ModifiedAt,
           '9999-12-31 23:59:59.9999999 +00:00'
    FROM ConfinityFormSubmission
    """);

Optional migration

Optionally, you might append the following script to set the new asset file PublicName to the same value as the already existing Name.

migrationBuilder.Sql(
    """
    UPDATE "ConfinityAsset" SET "PublicName" = "Name";
    
    UPDATE "__ConfinityHistory_ConfinityAsset" SET "Entity_PublicName" = "Entity_Name";
    """);

2024.2.2.0

Entity App

  • The following methods are now async and thereby got renamed with the Async suffix:
    • Exporter.DictionaryToXlsxAsync (in addition, the first parameter changed to IEnumerable<IDictionary<string, object?>>)
    • Exporter.ToXlsxAsync

2024.2.0.0 [migration]

General

  • Confinity is replacing the term 'Archive' with 'Rename'. Therefor the following methods got renamed:
    • Renamed ActionEventType.Archive to ActionEventType.Delete.
    • Renamed IReplicationService.ArchiveAsync to IReplicationService.DeleteAsync.
    • Renamed IUserService.ArchiveAsync to IUserService.DeleteAsync.
    • Renamed IUserService.ArchiveRangeAsync to IUserService.DeleteRangeAsync.
    • Renamed IAppBuilder.AddArchiveAction to IAppBuilder.AddDeleteAction.
    • Renamed ITableLayoutBuilder.AddArchiveAction to ITableLayoutBuilder.AddDeleteAction.
  • The following entities don't have a history-table anymore (maybe update your db cleanup script?):
    • Notification
    • DataProtectionKey
    • FormSubmission
    • UserTaskEntityPayload
    • UserTaskLog
  • Removed IDocumentAsset.

Authentication & JWT

  • UserService is no longer public
  • Removed the following properties from ConfinityIdentity: Groups, Roles, Email
  • Removed the following properties from ConfinityClaims: Username, GivenName, FamilyName, Email, Roles, Groups

AI Search & Chat-Bot

  • Removed property Type from IContentSearchDocumentResult. Use Attributes instead.
  • Removed ElasticsearchAiServiceKeys
  • When querying the ElasticSearch-AI index, use IElasticsearchAiQueryService instead of the keyed ISearchQueryService'.

Migration

  • This migration is implemented as an extension method, just create a new migration and replace the Up method with the following code (Sql Server & Postgres only):
protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.Confinity_2024_2_0_Up();
}

In the Confinity_2024_2_0_Up method, you have to provide a set of Boolean, depending on whether you're using the corresponding module.

WYSIWYG Configuration

  • TinyMCE has been upgraded to v6.8.5 and therefore the following configuration properties has been removed from IWysiwygEditorConfiguration:
    • PasteWordValidElements (replaced with PasteFromWordValidElements)
    • TableResponsiveWidth (replaced with TableSizingMode)
  • In WysiwygEditorToolbarButton the following enum members have been renamed:
    • Styleselect to Styles
    • Formatselect to Blocks

Pages

  • HttpContext.ConfinityPage().Resources.AddJavaScript and HttpContext.ConfinityPage().Resources.AddStylesheet removed the property Version. A version string is automatically created when the new property FilePath is set (preferred). If this is not wanted, add the parameter to your uri. To give attention to this change the property FileUri was renamed to Uri.

Examples preferred:

// old
httpContext.ConfinityPage().Resources.AddJavaScript(
    new JavaScriptResource(new Uri(url, UriKind.Relative)) { Version = s_version }
);

// new 
httpContext.ConfinityPage().Resources.AddJavaScript(
    new JavaScriptResource() { FilePath = filePath } // filePath should be the relative path to the file. No call to Url.Content or something is required. The filePath must not start with a "~".
);
// old
string url = urlHelper.Content($"~/assets/bundled/{bundleName}.js");
var fileVersionProvider = httpContext.RequestServices.GetService<IFileVersionProvider>();
var versionUrl = fileVersionProvider?.AddFileVersionToPath(httpContext.Request.PathBase, url);
var js = versionUrl != null ? new JavaScriptResource(new Uri(versionUrl, UriKind.Relative)) { } : new JavaScriptResource(new Uri(url, UriKind.Relative)) { Version = s_version, };
httpContext.ConfinityPage().Resources.AddJavaScript(js);

// new 
var js = new JavaScriptResource() { FilePath = $"/assets/bundled/{bundleName}.js" }
httpContext.ConfinityPage().Resources.AddJavaScript(js);

Examples not preferred:

// old
var jsUrl = _staticResourceHelper.CreateUrlForStaticResource(CookieConsentModule.ModuleKey, "/my.js");
return new JavaScriptResource(jsUrl) { Version = "3" };

// new v1 
var jsUrl = _staticResourceHelper.CreateUrlForStaticResource(CookieConsentModule.ModuleKey, "/my.js");
return new JavaScriptResource(jsUrl.AddParameter("v","3"));

// new v2
var jsUrl = _staticResourceHelper.CreateUrlForStaticResource(CookieConsentModule.ModuleKey, "/my.js", new QueryString("?v=3"));
return new JavaScriptResource(jsUrl);

2024.1.1.0 Assets Seeding

The folder "Assets" with the id 85E56776-DAEA-461C-A308-CB177E4526F0 is no longer seeded on start up if there are other folders.

2024.1.0.0 [migration]

Web

The content security policy (csp) directive frame-ancestors is now set to 'self' by default.

Custom SSO

  • IProvideConfinityUser has been refactored and renamed to IConfinityUserProvider. The class has now two methods:
    • ProvideUsernameAsync() which should return the current username or null, if no user is logged in.
    • With UpdateUserAsync() you can update/sync the user stored in confinity.
  • Removed IssuerJsonWebKey, AllowedIssuers and AllowedAudiences from AuthJwk options, since custom SSO implementation has to be made in host application.

User / Roles / Groups Entities

  • Roles are no longer assigned via groups to a user. Instead, roles are now assigned directly to the users.

Entity App

  • Implementations of IActionEventListener should handle the new ActionEventType.Restore, invoked by the new 'Restore' action.

Persistence

  • on IPersistensConfiguration the PropertyBuilderExtensions method HasJsonConversion has been removed.
  • Confinity Contents no longer work on Entities not inheriting ConfinityEntity

Migration

  • This migration is implemented as an extension method, just create a new migration and replace the Up method with the following code (Sql Server & Postgres only):
protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.Confinity_2024_1_0_Up();
}

Configuration

  • Paid editions of Confinity must now be explicitly activated via appsettings or environment variables. Enable paid editions by setting Confinity__Edition to Enterprise or Professional.

Wizard module

  • Added separate handling of relocation results in the view. Reloaction result is now rendered with "a" - Tag instead of "button".
  • Styling needs to be created for link results and adjusted for button results separately. Added class="option" to all results for better styling.
  • If you have overriden the view, you have to change your view accordingly and you have to with a relocation result.

Feedback module

  • New members on FeedbackPayload : ConfinityContent need to be implemented:
    public abstract class FeedbackPayload : ConfinityContent
    {
        public abstract IReadOnlyCollection<ExportColumnDefinition> GetExportColumns();
        public abstract IReadOnlyCollection<ExportValue> GetExportValues();
    }

Unit-Testing

  • The base class WithEfCoreTestBase for unit testing with a IConfinityDbContext was removed. Use ConfinityTestDatabase instead.

References

After installing 2024.1, you must run the scheduled task Confinity.ReferenceChecker.RefreshScheduledTask to repair broken references.

v1.6.X [migration]

Migration

  • history of AssetAttribute was removed.
protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.Confinity_1_6_Up();
}

Asset

  • History image assets throw an error when they are requested in a specific exact resized dimension. Before this gave a silent wrong result.
  • The filename of images for a history version gets ignored. An authenticated user can request a history file with any name.
  • Static asset delivery by a different webserver got removed (this feature may come back if the interest is there).

v1.5.X [migration]

Assets module

  • Moved IAssetService.GetContent to IAssetContentLoader.GetContent. Removed the param ContentKey from and made the method async due to an additional loading which was previously hidden in the stream. There are several other overloads available.
  • We've changed the image manipulation library. The new one uses less memory, is faster, does correctly handle color profile conversion, makes smaller files (especially for jpg), supports more formats and more.
  • If you had issues with color profiles you should delete your asset cache directory.
  • If you used the IImageAssetEditor.Resize(without focal point) or IImageAssetEditor.ResizeByMax and the image must be cropped, then the new library keeps interesting things centered.

Forms module

  • IListOfOptionModels defines the method GetOptions for the options instead of a readonly property.

Confinity

  • Update to .net 8 including all referenced Nuget libraries
  • ILinkHelper.CreateUrlFromLinkModel was removed (use CreateUrlFromLinkModelAsync instead).
  • IUrlHelper.ConfinityLink was removed (use ConfinityLinkAsync instead).
  • IDatabaseProviderConfiguration.ConfigureModel was removed (use OnModelCreating).
  • AssetStream is no longer public.
  • IEntityAppBuilder.Entity was removed (use AddEntity instead).
  • IFormContainerBuilder.AddConditionalGroup overload with label property was removed.
  • ISettingsStore was removed (use ISettings<TSettingsEntry> instead).
  • HeadViewContext.AddMetaTitle was removed (use HeadViewContext.SetMetaTitle instead).
  • HeadViewContext.AddMetaDescription was removed (use HeadViewContext.SetMetaDescription instead).
  • HeadViewContext.AddMetaImage was removed (use HeadViewContext.SetMetaImage instead).
  • HeadViewContext.SetMetaImage(string) was removed (use HeadViewContext.SetMetaImage(IImageAsset) instead).
  • ILinkModelUrlResolver.ResolveUrl was removed (use ILinkModelUrlResolver.ResolveUrlAsync instead).
  • HtmlHelperExtensions.FromWysiwyg was removed (use HtmlHelperExtensions.FromWysiwyglAsync instead).
  • until this release general Exception where thrown. With this release the exceptions got more explicit.
  • Validation
    • Confinity.EntityApp.IValidationFailure required a field ErrorCode and PropertyName both were unused / used wrong and were therefore removed.
    • Confinity.Validation.ConfinityValidationResult moved to an internal namespace. When needed use ValidationResult (which is now in the namespace Confinity.Validation)
    • Confinity.Validation.ConfinityValidationFailure moved to an internal namespace. When needed use ValidationFailure (which is now in the namespace Confinity.Validation)
  • IRelocationStore uses RelocationDto instead of Relocation.
  • Changed IEnumerable<> to ICollection<> for the following properties:
    • User.Attributes
    • UserModel.Attributes
    • IUserModel.Attributes
    • UserNewEditViewModel.Attributes
    • Page.Domains
    • Tag.Synonyms
    • BlogArticle.Categories
    • BlogArticle.Content
    • Form.Fields
    • Form.ConfirmationComponents
    • Form.ListenerModels
    • MediaPlayerCategory.RssFeedConfiguration
    • MediaPlayerElement.Categories
    • ProductAttributes.Attributes
    • ProductType.Attributes
    • WizardConfiguration.Questions
    • WizardConfiguration.Results

Migration

  • This migration is implemented as an extension method, just create a new migration an replace the Up method with the following code (Sql Server & Postgres only):
protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.Confinity_1_5_Up();
}

v1.4.X [migration]

Forms module

  • The abstract base class FormItemModel has been replaced with the class attribute [ConfinityFormItemAttribute]. Existing types should inherit from ViewComponentModel instead of FormItemModel.

Confinity

  • Instance key has to be unique across all stages.
  • AddConfinity without WebApplicationBuilder as argument was removed. This method was marked as obsolete since .net 5.
  • The connection to all instances waits only 10 seconds for a response for an authentication. Before this, it was 110 seconds.
  • The replication comes to a halt if any instance goes offline. Before this, this check was only made after startup.
  • Renamed ISelectManyAssetsOnConfinityContentFieldFormElementBuilder to ISelectAssetsOnConfinityContentFieldFormElementBuilder.

Blog module

  • In ConfinityBlogOverview/Default.cshtml razor view:
    • Added "most used tags" filter
  • In ConfinityBlogArticle/Default.cshtml razor view:
    • Added "last update" information
    • Replaced class badge-primary with bg-primary for all tags
  • Check if both views are still correct in your host application!
  • DB-Migration: Added new column LastUpdate in table BlogArticle

v1.3.X [migration]

Migration

  • This migration is implemented as an extension method, just create a new migration an replace the Up method with the following code (Sql Server & Postgres only):
protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.Confinity_1_3_Up();
}

v1.2.1

Core Module

  • Moved ConfinityNoHistoryAttribute & ConfinityAllowInheritanceAttribute to namespace Confinity.Attributes

v1.2.X [migration]

Migration

  • Added new column PublishedStage in table ConfinityEntityReference

v1.1.x

Core Module

  • IAsset has a new method called GetHash(). Custom Implementations of IAsset need to implement this.

v1.0.X [migration]

Razor helper

  • Introduced the new IRazorToStringRenderer (namespace Confinity.WebUI.Util) that replaces IRazorToStringRenderer (namepsace Confinity.Mail) and IRazorToStringRenderer (namespace Confinity.Pages).

Forms Module

  • The property ConfirmationText was changed to ConfirmationComponents
  • The property ConfirmationComponents is now a IEnumerable<ViewComponentModel>
  • You can now add ViewComponentModel which will be rendered after the form was submitted successfully which also means the previous Success-Alert after submitting is removed
  • Models marked with the attribute ConfinityFormsConfirmation will be able to used as elements for the ConfirmationComponents property
  • Confirmations are no longer wrapped in this container: <div data-cfy-form-confirmation>

Migration

  • This migration is implemented as an extension method, just create a new migration an replace the Up method with the following code (Sql Server & Postgres only):
//template = JSON string for a confinity content. This will overwrite all existing confirmationtext
protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.Confinity_1_0_Up(template);
}

  • The variable template must be a json with an array. In the array must be a component model, with the to parameters and
    • will be replaced with the text of the previous confirmationtext

Example

"[{\"$type\":\"RocketLaunches.ConfinityPages.PageComponents.TextComponentModel\",\"title\":null,\"lead\":null,\"text\":\"{{text}}\",\"id\":\"{{id}}\"}]"

⚠️ Important: Replace "RocketLaunches.ConfinityPages.PageComponents.TextComponentModel" with an existing ConfinityContent of the Application

v0.68.x [migration]

Replication

  • The way how the replication automatically deletes entities on all stages changed. Please check replication for further information.

Entity App

  • The property parentPropertySelector in IFormContainerBuilder.AddChildList(...) is no longer nullable.
  • The IComputedHandler in IFormContainerBuilder.AddComputed(...) has a second type parameter TProperty.
  • The property jsFilename in IFormContainerBuilder.AddCustom(...) is no longer the path to a file but the filename.
  • IFormContainerBuilder.AddCustom(...) expects one more property byte[] jsFileContent
  • The fields PublishedAt and PublishedById were removed from ConfinityMetadata and are now on the corresponding History-table
    • Entities marked with the ConfinityNoHistoryAttribute do not have those fields anymore.

Frontend Configuration

When the window wasn't scrollable components from modules (like the Forms) which had to scroll, did not scroll to the correct location. The default logic changed so that the first scrollable element (from the target HTMLElement) will scroll. This probably won't break anything, but if there are any issues, you can copy the following javascript to get back to the old behaviour.

window.__confinityScrollTo = (targetElement) => {
    // custom scroll which only scroll on the window.

    let positionFromTop = element.offsetTop;
    const scrollOffset = window.__confinityScrollOffsetTop;

    if (scrollOffset && !isNaN(scrollOffset)) {
        positionFromTop -= scrollOffset;
    }

    window.scrollTo({top: positionFromTop, behavior: 'smooth'});
}

OpenTelemetry Update

Due to a dependency update the api to add OpenTelemetry changed:

services.AddOpenTelemetry()
// services.AddOpenTelemetryMetrics(b => b old
    .WithMetrics(b => // new
    {
        b.AddConfinityInstrumentation()
            .AddAspNetCoreInstrumentation()
            .AddHttpClientInstrumentation();
    })
// services.AddOpenTelemetryTracing(b => b old
    .WithTracing(b => b // new
        .AddConfinityInstrumentation()
        .AddHttpClientInstrumentation()
        .AddAspNetCoreInstrumentation()
        .AddSqlClientInstrumentation()
    );

Asset App

  • Several permissions are now obsolete or renamed
  • AssetStream is obsolete. Use IAssetService.GetContent() instead.

Analytics

Since Google Analytics Universal Tracking is now stop processing data (https://support.google.com/analytics/answer/11583528?sjid=13203110707687459332-EU) we have removed it. Make sure to update your Analytics configuration by configuring Google Analytics 4, Google Tag manager, or Microsoft Clarity (with forward to Google Analytics 4).

Migration

  • No more history table for AssetContents
  • Cleanup asset permissions
  • Sets To in History to infinity where date is greater then 9999-01-01 (Postgres only)
  • Moves PublishedAt and PublishedById from ConfinityMetadata to the corresponding History-table
  • This migration is implemented as an extension method, just create a new migration an replace the Up method with the following code (Sql Server & Postgres only):
protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.Confinity_0_68_Up();
}

v0.67.16

Frontend Configuration (backport)

When the window wasn't scrollable components from modules (like the Forms) which had to scroll, did not scroll to the correct location. The default logic changed so that the first scrollable element (from the target HTMLElement) will scroll. This probably won't break anything, but if there are any issues, you can copy the following javascript to get back to the old behaviour.

window.__confinityScrollTo = (targetElement) => {
    // custom scroll which only scroll on the window.

    let positionFromTop = element.offsetTop;
    const scrollOffset = window.__confinityScrollOffsetTop;

    if (scrollOffset && !isNaN(scrollOffset)) {
        positionFromTop -= scrollOffset;
    }

    window.scrollTo({top: positionFromTop, behavior: 'smooth'});
}

v0.67.x [migration] (Confinity MediaPlayer module only)

Confinity Media Player

Database Migration

Warning

This migration will DELETE ALL data from the media player module!

If you have to migrate existing data, create the migration and replace the Up method with the following code. This migration might only work with SQL Server. If you use another DB provider, you have to adapt the migration script accordingly.

protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropForeignKey(
                name: "FK_ConfinityMediaPlayer_ConfinityAsset_PreviewImageId",
                table: "ConfinityMediaPlayer");

            migrationBuilder.DropForeignKey(
                name: "FK_ConfinityMediaPlayer_ConfinityAsset_VideoFileId",
                table: "ConfinityMediaPlayer");
            
            migrationBuilder.DropIndex(
                name: "IX_ConfinityMediaPlayer_PreviewImageId",
                table: "ConfinityMediaPlayer");

            migrationBuilder.DropIndex(
                name: "IX_ConfinityMediaPlayer_VideoFileId",
                table: "ConfinityMediaPlayer");
            
            migrationBuilder.AddColumn<string>(
                name: "Content",
                table: "ConfinityMediaPlayer",
                type: "nvarchar(max)",
                nullable: true);
            
            migrationBuilder.AddColumn<Guid>(
                name: "AuthorId",
                table: "ConfinityMediaPlayer",
                type: "uniqueidentifier",
                nullable: true);

            migrationBuilder.AddColumn<DateTimeOffset>(
                name: "Date",
                table: "ConfinityMediaPlayer",
                type: "datetimeoffset",
                nullable: true);
            
            migrationBuilder.AddColumn<string>(
                name: "Entity_Content",
                table: "__ConfinityHistory_ConfinityMediaPlayer",
                type: "nvarchar(max)",
                nullable: true);

            migrationBuilder.AddColumn<Guid>(
                name: "Entity_AuthorId",
                table: "__ConfinityHistory_ConfinityMediaPlayer",
                type: "uniqueidentifier",
                nullable: true);

            migrationBuilder.AddColumn<DateTimeOffset>(
                name: "Entity_Date",
                table: "__ConfinityHistory_ConfinityMediaPlayer",
                type: "datetimeoffset",
                nullable: true);

            
            // DATA MIGRATION
            migrationBuilder.Sql("""
DECLARE @id UNIQUEIDENTIFIER;
DECLARE @videoTemplate NVARCHAR(MAX);
DECLARE @audioTemplate NVARCHAR(MAX);

SET @videoTemplate = '{
                      "$type": "Confinity.MediaPlayer.MediaElementVideoContent",
                      "videoAssets": [
                        {
                          "$type": "Confinity.MediaPlayer.VideoAsset",
                          "asset": {
                            "id": "%s"
                          },
                          "resolution": null,
                          "id": "%s"
                        }
                      ],
                      "aspectRatio": "%s",
                      "previewImage": {
                        "id": "%s"
                      },
                      "chapter": null,
                      "id": "%s"
                    }'
SET @audioTemplate = '{
                      "$type": "Confinity.MediaPlayer.MediaElementAudioContent",
                        "asset": {
                        "id": "%s"
                      },
                      "previewImage": {
                        "id": "%s"
                      },
                      "chapter": null,
                      "aspectRatio": "%s",
                      "id": "%s"
                    }'

WHILE EXISTS(SELECT NULL
             FROM ConfinityMediaPlayer p
             INNER JOIN ConfinityAsset a ON a.Id = p.VideoFileId
             WHERE p.Content IS NULL AND a.Filename LIKE '%.mp4')
    BEGIN
        
        DECLARE @videoAssetId UNIQUEIDENTIFIER;
        DECLARE @videoContentId UNIQUEIDENTIFIER;

        SET @id = (SELECT TOP 1 p.Id FROM ConfinityMediaPlayer p
                                            INNER JOIN ConfinityAsset a ON a.Id = p.VideoFileId
                   WHERE p.Content IS NULL AND a.Filename LIKE '%.mp4');
        SET @videoAssetId = NEWID();
        SET @videoContentId = NEWID();

        UPDATE ConfinityMediaPlayer
        SET Content = REPLACE(REPLACE(REPLACE(FORMATMESSAGE(@videoTemplate
                                                  , LOWER(CONVERT(nvarchar(36), ConfinityMediaPlayer.VideoFileId))
                                                  , LOWER(CONVERT(nvarchar(36), @videoAssetId))
                                                  , ConfinityMediaPlayer.AspectRatio
                                                  , LOWER(CONVERT(nvarchar(36), ConfinityMediaPlayer.PreviewImageId))
                                                  , LOWER(CONVERT(nvarchar(36), @videoContentId)))
                                          , CHAR(13), ''), CHAR(10), ''), CHAR(32), ''), --remove line breaks and spaces
            Date = ConfinityMetadata_ModifiedAt
        WHERE ConfinityMediaPlayer.Id = @id

        UPDATE __ConfinityHistory_ConfinityMediaPlayer
        SET Entity_Content = REPLACE(REPLACE(REPLACE(FORMATMESSAGE(@videoTemplate
                                                  , LOWER(CONVERT(nvarchar(36), __ConfinityHistory_ConfinityMediaPlayer.Entity_VideoFileId))
                                                  , LOWER(CONVERT(nvarchar(36), @videoAssetId))
                                                  , __ConfinityHistory_ConfinityMediaPlayer.Entity_AspectRatio
                                                  , LOWER(CONVERT(nvarchar(36), __ConfinityHistory_ConfinityMediaPlayer.Entity_PreviewImageId))
                                                  , LOWER(CONVERT(nvarchar(36), @videoContentId)))
                                          , CHAR(13), ''), CHAR(10), ''), CHAR(32), ''), --remove line breaks and spaces
            Entity_Date = Entity_ConfinityMetadata_ModifiedAt
        WHERE __ConfinityHistory_ConfinityMediaPlayer.Entity_Id = @id
    END

WHILE EXISTS(SELECT NULL
             FROM ConfinityMediaPlayer p
                      INNER JOIN ConfinityAsset a ON a.Id = p.VideoFileId
             WHERE p.Content IS NULL AND a.Filename LIKE '%.mp3')
    BEGIN
        DECLARE @audioContentId UNIQUEIDENTIFIER;

        SET @id = (SELECT TOP 1 p.Id FROM ConfinityMediaPlayer p
                                              INNER JOIN ConfinityAsset a ON a.Id = p.VideoFileId
                   WHERE p.Content IS NULL AND a.Filename LIKE '%.mp3');
        SET @audioContentId = NEWID();

        UPDATE ConfinityMediaPlayer
        SET Content = REPLACE(REPLACE(REPLACE(FORMATMESSAGE(@audioTemplate
                                                  , LOWER(CONVERT(nvarchar(36), ConfinityMediaPlayer.VideoFileId))
                                                  , LOWER(CONVERT(nvarchar(36), ConfinityMediaPlayer.PreviewImageId))
                                                  , ConfinityMediaPlayer.AspectRatio
                                                  , LOWER(CONVERT(nvarchar(36), @audioContentId)))
                                          , CHAR(13), ''), CHAR(10), ''), CHAR(32), ''), --remove line breaks and spaces
            Date = ConfinityMetadata_ModifiedAt
        WHERE ConfinityMediaPlayer.Id = @id

        UPDATE __ConfinityHistory_ConfinityMediaPlayer
        SET Entity_Content = REPLACE(REPLACE(REPLACE(FORMATMESSAGE(@audioTemplate
                                                         , LOWER(CONVERT(nvarchar(36), __ConfinityHistory_ConfinityMediaPlayer.Entity_VideoFileId))
                                                         , LOWER(CONVERT(nvarchar(36), __ConfinityHistory_ConfinityMediaPlayer.Entity_PreviewImageId))
                                                         , __ConfinityHistory_ConfinityMediaPlayer.Entity_AspectRatio
                                                         , LOWER(CONVERT(nvarchar(36), @audioContentId)))
                                                 , CHAR(13), ''), CHAR(10), ''), CHAR(32), ''), --remove line breaks and spaces
            Entity_Date = Entity_ConfinityMetadata_ModifiedAt
        WHERE __ConfinityHistory_ConfinityMediaPlayer.Entity_Id = @id
    END

GO
""");

            migrationBuilder.Sql("""
UPDATE [ConfinityEntityReference] 
SET [DependentEntityName] = 'Confinity.MediaPlayer.MediaPlayerElement'
WHERE [DependentEntityName] = 'Confinity.MediaPlayer.MediaPlayer'
""");
            
            
            migrationBuilder.DropColumn(
                name: "PreviewImageId",
                table: "ConfinityMediaPlayer");

            migrationBuilder.DropColumn(
                name: "VideoFileId",
                table: "ConfinityMediaPlayer");

            migrationBuilder.DropColumn(
                name: "Entity_PreviewImageId",
                table: "__ConfinityHistory_ConfinityMediaPlayer");

            migrationBuilder.DropColumn(
                name: "Entity_VideoFileId",
                table: "__ConfinityHistory_ConfinityMediaPlayer");
            
            migrationBuilder.DropColumn(
                name: "AspectRatio",
                table: "ConfinityMediaPlayer");
            
            migrationBuilder.DropColumn(
                name: "Entity_AspectRatio",
                table: "__ConfinityHistory_ConfinityMediaPlayer");
            
            migrationBuilder.DropPrimaryKey(
                name: "PK_ConfinityMediaPlayer",
                table: "ConfinityMediaPlayer");

            migrationBuilder.DropPrimaryKey(
                name: "PK___ConfinityHistory_ConfinityMediaPlayer",
                table: "__ConfinityHistory_ConfinityMediaPlayer");
            
            migrationBuilder.DropIndex(
                name: "IX___ConfinityHistory_ConfinityMediaPlayer_Entity_Id",
                table: "__ConfinityHistory_ConfinityMediaPlayer");
            
            migrationBuilder.AlterColumn<DateTimeOffset>(
                name: "Date",
                table: "ConfinityMediaPlayer",
                oldType: "datetimeoffset",
                type: "datetimeoffset",
                oldNullable: true,
                nullable: false);

            //RENAME TABLE HERE
            migrationBuilder.RenameTable(name: "ConfinityMediaPlayer", 
                newName: "ConfinityMediaPlayerElement");
            
            migrationBuilder.RenameTable(name: "__ConfinityHistory_ConfinityMediaPlayer",
                newName: "__ConfinityHistory_ConfinityMediaPlayerElement");
            
            migrationBuilder.AddPrimaryKey(
                name: "PK_ConfinityMediaPlayerElement",
                table: "ConfinityMediaPlayerElement",
                column: "Id");

            migrationBuilder.AddPrimaryKey(
                name: "PK___ConfinityHistory_ConfinityMediaPlayerElement",
                table: "__ConfinityHistory_ConfinityMediaPlayerElement",
                column: "Id");
            
            migrationBuilder.CreateTable(
                name: "__ConfinityHistory_ConfinityMediaPlayerAuthor",
                columns: table => new
                {
                    Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
                    Entity_ConfinityMetadata_ModifiedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false),
                    Entity_ConfinityMetadata_ModifiedById = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
                    Entity_ConfinityMetadata_PublishedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true),
                    Entity_ConfinityMetadata_PublishedById = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
                    Entity_ConfinityMetadata_PublishedStage = table.Column<string>(type: "nvarchar(max)", nullable: true),
                    Entity_Description = table.Column<string>(type: "nvarchar(max)", nullable: true),
                    Entity_FirstName = table.Column<string>(type: "nvarchar(max)", nullable: true),
                    Entity_Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
                    Entity_ImageId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
                    Entity_LastName = table.Column<string>(type: "nvarchar(max)", nullable: true),
                    From = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false),
                    To = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK___ConfinityHistory_ConfinityMediaPlayerAuthor", x => x.Id);
                });

            migrationBuilder.CreateTable(
                name: "__ConfinityHistory_ConfinityMediaPlayerCategory",
                columns: table => new
                {
                    Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
                    Entity_ConfinityMetadata_ModifiedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false),
                    Entity_ConfinityMetadata_ModifiedById = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
                    Entity_ConfinityMetadata_PublishedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true),
                    Entity_ConfinityMetadata_PublishedById = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
                    Entity_ConfinityMetadata_PublishedStage = table.Column<string>(type: "nvarchar(max)", nullable: true),
                    Entity_Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
                    Entity_Name = table.Column<string>(type: "nvarchar(max)", nullable: true),
                    Entity_RssFeedConfiguration = table.Column<string>(type: "nvarchar(max)", nullable: true),
                    Entity_Slug = table.Column<string>(type: "nvarchar(max)", nullable: true),
                    From = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false),
                    To = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK___ConfinityHistory_ConfinityMediaPlayerCategory", x => x.Id);
                });

            migrationBuilder.CreateTable(
                name: "__ConfinityHistory_ConfinityMediaPlayerElementCategory",
                columns: table => new
                {
                    Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
                    Entity_ConfinityMetadata_ModifiedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false),
                    Entity_ConfinityMetadata_ModifiedById = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
                    Entity_ConfinityMetadata_PublishedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true),
                    Entity_ConfinityMetadata_PublishedById = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
                    Entity_ConfinityMetadata_PublishedStage = table.Column<string>(type: "nvarchar(max)", nullable: true),
                    Entity_Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
                    Entity_MediaPlayerCategoryId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
                    Entity_MediaPlayerElementId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
                    Entity_SortIndex = table.Column<int>(type: "int", nullable: false),
                    From = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false),
                    To = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK___ConfinityHistory_ConfinityMediaPlayerElementCategory", x => x.Id);
                });

            migrationBuilder.CreateTable(
                name: "ConfinityMediaPlayerAuthor",
                columns: table => new
                {
                    Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
                    FirstName = table.Column<string>(type: "nvarchar(max)", nullable: false),
                    LastName = table.Column<string>(type: "nvarchar(max)", nullable: false),
                    ImageId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
                    Description = table.Column<string>(type: "nvarchar(max)", nullable: true),
                    ConfinityMetadata_ModifiedById = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
                    ConfinityMetadata_ModifiedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false),
                    ConfinityMetadata_PublishedStage = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
                    ConfinityMetadata_PublishedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true),
                    ConfinityMetadata_PublishedById = table.Column<Guid>(type: "uniqueidentifier", nullable: true)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_ConfinityMediaPlayerAuthor", x => x.Id);
                    table.ForeignKey(
                        name: "FK_ConfinityMediaPlayerAuthor_ConfinityAsset_ImageId",
                        column: x => x.ImageId,
                        principalTable: "ConfinityAsset",
                        principalColumn: "Id");
                });

            migrationBuilder.CreateTable(
                name: "ConfinityMediaPlayerCategory",
                columns: table => new
                {
                    Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
                    Name = table.Column<string>(type: "nvarchar(max)", nullable: false),
                    Slug = table.Column<string>(type: "nvarchar(250)", maxLength: 250, nullable: false),
                    RssFeedConfiguration = table.Column<string>(type: "nvarchar(max)", nullable: true),
                    ConfinityMetadata_ModifiedById = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
                    ConfinityMetadata_ModifiedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false),
                    ConfinityMetadata_PublishedStage = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
                    ConfinityMetadata_PublishedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true),
                    ConfinityMetadata_PublishedById = table.Column<Guid>(type: "uniqueidentifier", nullable: true)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_ConfinityMediaPlayerCategory", x => x.Id);
                });

            migrationBuilder.CreateTable(
                name: "ConfinityMediaPlayerElementCategory",
                columns: table => new
                {
                    Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
                    MediaPlayerElementId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
                    MediaPlayerCategoryId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
                    ConfinityMetadata_ModifiedById = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
                    ConfinityMetadata_ModifiedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false),
                    ConfinityMetadata_PublishedStage = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
                    ConfinityMetadata_PublishedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true),
                    ConfinityMetadata_PublishedById = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
                    SortIndex = table.Column<int>(type: "int", nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_ConfinityMediaPlayerElementCategory", x => x.Id);
                    table.ForeignKey(
                        name: "FK_ConfinityMediaPlayerElementCategory_ConfinityMediaPlayerCategory_MediaPlayerCategoryId",
                        column: x => x.MediaPlayerCategoryId,
                        principalTable: "ConfinityMediaPlayerCategory",
                        principalColumn: "Id");
                    table.ForeignKey(
                        name: "FK_ConfinityMediaPlayerElementCategory_ConfinityMediaPlayerElement_MediaPlayerElementId",
                        column: x => x.MediaPlayerElementId,
                        principalTable: "ConfinityMediaPlayerElement",
                        principalColumn: "Id");
                });

            migrationBuilder.CreateIndex(
                name: "IX___ConfinityHistory_ConfinityMediaPlayerAuthor_Entity_Id",
                table: "__ConfinityHistory_ConfinityMediaPlayerAuthor",
                column: "Entity_Id");

            migrationBuilder.CreateIndex(
                name: "IX___ConfinityHistory_ConfinityMediaPlayerCategory_Entity_Id",
                table: "__ConfinityHistory_ConfinityMediaPlayerCategory",
                column: "Entity_Id");

            migrationBuilder.CreateIndex(
                name: "IX___ConfinityHistory_ConfinityMediaPlayerElement_Entity_Id",
                table: "__ConfinityHistory_ConfinityMediaPlayerElement",
                column: "Entity_Id");

            migrationBuilder.CreateIndex(
                name: "IX___ConfinityHistory_ConfinityMediaPlayerElementCategory_Entity_Id",
                table: "__ConfinityHistory_ConfinityMediaPlayerElementCategory",
                column: "Entity_Id");

            migrationBuilder.CreateIndex(
                name: "IX_ConfinityMediaPlayerAuthor_ImageId",
                table: "ConfinityMediaPlayerAuthor",
                column: "ImageId");

            migrationBuilder.CreateIndex(
                name: "IX_ConfinityMediaPlayerCategory_Slug",
                table: "ConfinityMediaPlayerCategory",
                column: "Slug",
                unique: true);

            migrationBuilder.CreateIndex(
                name: "IX_ConfinityMediaPlayerElement_AuthorId",
                table: "ConfinityMediaPlayerElement",
                column: "AuthorId");

            migrationBuilder.CreateIndex(
                name: "IX_ConfinityMediaPlayerElementCategory_MediaPlayerCategoryId",
                table: "ConfinityMediaPlayerElementCategory",
                column: "MediaPlayerCategoryId");

            migrationBuilder.CreateIndex(
                name: "IX_ConfinityMediaPlayerElementCategory_MediaPlayerElementId",
                table: "ConfinityMediaPlayerElementCategory",
                column: "MediaPlayerElementId");
        }

Page Component / Frontend

The page component for the media player has been reworked. Please verify if the component still works and check all your CSS & JavaScripts.

v0.66.x only Confinity Forms Module

Csp Headers

AllowPrefetchSource is removed from HttpContext.ConfinityPage().Csp(). This is no longer supported by the browser.

Confinity Forms

A new component for the buttons was created so that they can be overwritten. If you have overwritten the ConfinityForm/Default.cshtml you have to call the corresponding button:

// replace the <button type="submit" with:
@await Component.InvokeAsync(typeof(ConfinityFormButtonViewComponent),
                                   new FormButtonModel { Usage = FormButtonUsage.Submit, Label = Model.SubmitButtonLabel })

If you have overwritten the ConfinityFormWizard/Default.cshtml you have to call the corresponding buttons:

// replace the back button (<button type="button" ...) with:
@await Component.InvokeAsync(typeof(ConfinityFormButtonViewComponent),
        new FormButtonModel { Label = "", Usage = FormButtonUsage.WizardBack })
// replace the forward button (<button type="submit" ...) with:
@await Component.InvokeAsync(typeof(ConfinityFormButtonViewComponent),
        new FormButtonModel { Label = Model.WizardInfo?.NextButtonLabel ?? "", Usage = FormButtonUsage.WizardForward })

v0.65.x [migration]

Confinity Media Player

Nullability fixed for the MediaPLayer entity.

VideoFileId    from Guid? to Guid
PreviewImageId from Guid? to Guid

HasJsonConversion deprecated

PropertyBuilderExtensions.HasJsonConversion is deprecated. ConfinityContents don't need this anyway.

v0.64.x [migration]

ConfinityMetaData

ConfinityMetaData.PublishedStage gets a max. length of 50 to reduce space- and memory consumption. This will effect every ConfinityEntity.

v0.63.x [migration]

Settings can no longer be used with the options pattern!

See Settings for further information.

User Tasks

New input 'planned for' added to UserTask, needs a migration.

v0.62.x

Entity App: No more custom keys for RegisterSettings

The support for named settings was dropped, therefor the Key method on the ISettingsBuilder as well.

WARNING

If you were using the Key method to configure a specific key, your existing Settings will no longer be found!

ISettingsStore deprecated

The ISettingsStore is deprecated, to access your settings, use ISettings<T>. See Settings for further information.

v0.61.x

ConfinitySelectable

Classes inheriting from ConfinitySelectable must have a constructor with the following parameters (type & name). Additional constructor are allowed.

private MySelectable(string id) {}

//or

private MySelectable(string id, string label) {}

//or

private MySelectable(string id, string label, int sortIndex) {}

v0.60.x [mgiration]

Services now uses it's own DbContext. The returned values are therefore no longer tracked. This was an architectural issue and had to be fixed. To help you, the method is renamed and has a different return type.

The migration is for additional navigation properties.

IAssetService

// old
var file = await _assetService.SaveFile(/*...*/);
myEmployee.Image = file; // wrong!

// new 
var file = await _assetService.SaveFileAsync(/*...*/);
myEmployee.ImageId = file.Id // correct

For the asset methods, the return type changed from Asset to IAsset. For the folder methods, the return type changed form AssetFolder to IFolder

ITagService

The return / parameter changed to ITag instead of Tag.

OnStartup removed

The base class OnStartup was removed. Use your implementation of IModuleConfiguration.UseModule for startup logic instead.

v0.59.x [migration]

Confinity Pages

  • PageDomain got new a new property StageKey to link it to a stage.
  • ConfinityPageOptions.StrictDomainBindingForWebsites got removed. Strict domain binding is activated on every public (= non author) instance.

Entity app preview action

  • The IPreviewResolver.ResolveUrl got a second, optional parameter baseUrl. This is will be the host (e.g. 'conx.ch') in multi stage environments and should be incorporated in the constructed URL.

v0.58.x

Saferpay Notify API changes

ask Roman

v0.57.x

Saferpay

Implementations of ISaferpayPaymentListener must capture or cancel when the transaction state is authorized.

v0.56.x [migration]

Removal of Confinity.Messages

All NuGet packages named 'Confinity.Messages' were removed.

ConfinityDbContext constructor

The constructor for ConfinityDbContext no longer expects a IMessageBroker<DeltaTopic>.

ISelectOptionsProvider<T> is now async

Implementations of this interface must return a Task:

// from
IList<ISelectOption<T>> GetOptions(string? searchQuery = null, IEnumerable<string>? ids = null);
// to
Task<IList<ISelectOption<T>>> GetOptions(string? searchQuery = null, IEnumerable<string>? ids = null);

v0.55.x [migration]

Update to .Net 7

  • All Confinity projects now target .NET 7 and require you to have it installed.
  • All Nuget references where updated to versions compatible with .NET 7. Check breaking changes if necessary:
    • [.NET 7]https://learn.microsoft.com/en-us/dotnet/core/compatibility/7.0
    • [EF7]https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-7.0/breaking-changes

SQL Server connection string

Since the update to EF 7, the SQL Server connection string is supposed to use encryption for the credentials. If you are not using encryption, set Encrypt=false in your connection string.

ConfinityConfiguration

Removed property LogSql. EF.Core always logs to the logger factory defined in DI. Set

{
  "Logging": {
    "LogLevel": {
      "Microsoft.EntityFrameworkCore.Database.Command": "Warning"
    }
  }
}

in you appsettings to reduce noise from EF.Core logging.

ConfinityOptions

  • Renamed DefaultLanguage -> DefaultUiLanguage
  • Renamed SupportedLanguages -> SupportedUiLanguages

NotEmpty validation

The NotEmpty validation is not accepting default values for value types anymore, e.g. the first value of an enum is not valid.

v0.54.x [migration]

The index on SettingsContainer.Key is now unique.

Warning

Check your settings app and remove the settings entry before the unique key is created.

More specific database configurations causes minimal definition changes.

EfPoly.Random

For random within sql queries you should now use Ef.Functions.Random.

v0.53.x [migration]

The replication now handles longer class names.

DAM / Asset endpoints changed

See Assets for details.

v0.52.x

Search Facets

Search facets (e.g. Type) need to be configured explicitly now. To get the same behaviour as before, use the following snippet:

public void AddModule(IModuleContext module)
{
    module.Configure.RegisterSearchFacet(SearchFacetDefault.Type);
    module.Configure.RegisterSearchFacet(SearchFacetDefault.DocumentFileExtension);
}

Or specify your own facets configurations:

public void AddModule(IModuleContext module)
{
    module.Configure.RegisterSearchFacet("coolness-level", "Coolness level", 5);
}

v0.51.x

Entity App Wysiwyg editor configuration

IWysiwygEditorConfiguration and DefaultWysiwygEditorConfiguration got refactored:

  • property names use pascal casing
  • properties in DefaultWysiwygEditorConfiguration are virtual and have setters

v0.50.x [migration]

Mailing

  • DB Migration: Removed length limit for MailingSchedule.Message.

v0.49.x

Assets

The file system cache for image assets changed. It is advised to delete the current cache directory (configured in AssetOptions.CacheDirectory) to save disk space.

v0.48.x

Forms

Changed the property Fields of the interface IFormItemCollectionModel to a method.

v0.47.3 CSP Changes

Analytics

The google tag manager now allows to set the 'strict-dynamic' script-src directive. When this is enabled, all scripts must have a nonce (confinity-nonce).

v0.47.x [migration]

Mailing

Removed length limit for MailingSchedule.OutgoingData & MailingSchedule.IncomingReply.

0.46.x

XCampaign Module refactoring

-IXCampaingService now only has the methods UpdateProfilesAsync & GetProfilesAsync and returns proper response objects. -Renamed Attribute to XCampaignAttribute

0.45.x

ConfinityPictureModel

The default value for the property FocalPoint in the ConfinityPictureModel has been changed to true.

ILinkHelper

  • CreateUrlFromLinkModel is now obsolete, use the async counterpart CreateUrlFromLinkModelAsync
  • CreateUrlFromLinkModelAsync has a new parameter forceAbsolute.

ILinkModelUrlResolver

  • ResolveUrl is now obsolete, use the async counterpart ResolveUrlAsync
  • ResolveUrlAsync has a new parameter forceAbsolute.

UrlHelperExtensions

  • ConfinityLink is now obsolete, use the async counterpart ConfinityLinkAsync

0.44.x

TempFile

  • The interface ITempFileController requires a new parameter: [FromForm] string? contextPayload = null
  • For security reasons, the returned error messages from the JavaScript TempFileClient has been reduced to these for errors:
    • FileCapacityExceeded
    • ExtensionNotAllowed
    • ContentNotAllowed
    • ProcessingError
  • The strings for the error messages are now translated on the backend

Forms Module: FileUpload

The view for the ConfinityFormFileUpload view component has been slightly changed.

0.43.x

Forms Module

The interface IFormItemCollectionModel changed slightly:

public interface IFormItemCollectionModel
{
    public IEnumerable<ConfinityContent>? Fields { get; }
}

0.42.1

Optional migration forms module

The forms from the form module have now the default spam protection "honeypot". To set this for all forms, use the following sql:

Microsoft SQL Server

migrationBuilder.Sql(@"update ConfinityForm set SpamProtection = 1 where 1=1;");

PostgreSQL

migrationBuilder.Sql(@"update ""ConfinityForm"" set ""SpamProtection"" = 1 where 1=1;");

0.42.x [Migration]

TempFile

  • The registration for a TempFileSpace changed, see File Upload / Temp File for an example.
  • ISpaceDefinitionStore and SpaceDefinition were removed. The configuration of your space is stored as an Named Option in DI. The name is the name of your space.
  • Instead of configuring your space with code, you can configure it in your appsettings, e.g.
{
  "ConfinityTempFile": {
    "NAME-OF-YOUR-SPACE": {
      "SpaceCapacityInBytes": 209715200,
      "SessionCapacityInBytes": 20971520,
      "FileCapacityInBytes": 4194304,
      "MaxAgeInSeconds": 20,
      "PermittedFileSignatures": {
        "pdf": [
          [
            37,
            80,
            68,
            70,
            45
          ]
        ]
      }
    }
  }
}

Forms Module

If you had configured FileUploadOptions before in your appsettings, you need to transform them according to the example below. Notice that the property name for PermittedFileSignatures changed.

{
  "ConfinityTempFile": {
    "FileUploadOptions": {
      "SpaceCapacityInBytes": 209715200,
      "SessionCapacityInBytes": 20971520,
      "FileCapacityInBytes": 4194304,
      "MaxAgeInSeconds": 20,
      "PermittedFileSignatures": {
        "pdf": [
          [
            37,
            80,
            68,
            70,
            45
          ]
        ]
      }
    }
  }
}

Asset

  • History image assets throw an error when they are requested in a specific exact resized dimension. Before this gave a silent wrong result.
  • The filename of images for a history version gets ignored. An authenticated user can request a history file with any name.
  • Static asset delivery by a different webserver got removed (this feature may come back if the interest is there).

Metadata

  • New tables ConfinityAssignedUser and ConfinityEntityNote will be created.

Configuration

  • Paid editions of Confinity must now be explicitly activated via appsettings or environment variables. Enable paid editions by setting Confinity__Edition to Enterprise or Professional.

Unit-Testing

  • The base class WithEfCoreTestBase for unit testing with a IConfinityDbContext was removed. Use ConfinityTestDatabase instead.

0.41.x

If you reference static files in your module and publish a module separately, you have to use Microsoft.Extensions.FileProviders.Embedded.

For details see [Modules](./modules.md#Add static files)

0.40.x

Addition to 0.36.x:

Use the following migration to ensure there are no null values in the history table of Forms:

migrationBuilder.Sql(@"update ""__ConfinityHistory_ConfinityForm"" set ""Entity_ListenerModels"" = '[]'  where ""Entity_ListenerModels"" is null;")

v0.39.x

Entity app

  • Most values were removed from enum InputType. The remaining are all valid and supported.
  • For configuring the DateOnly-, TimeOnly- and DateAndTime-inputs use the new enum DateAndTimeType
  • For hidden fields use the AddHidden method

v0.38.x [migration]

Scheduled Task Index

Improved index for schedules requires a migration.

MailService throws exceptions

IMailService.SendMail now throws exceptions instead of just logging them.

v0.37.x [migration]

linq2db

Dependency to linq2db was removed. If used in the host application add linq2db there or replace with methods from Microsoft.EfCore.

DataProtection

Confinity stores now comes with a key storage provider that stores data protection keys in the database and ensures they are automatically replicated to all stages.

Notifications

Changed first parameter type from User to Guid for all overloads of SendAsync.

v0.36.x [migration]

From Module IFormSubmitEventListener<EmailFormSubmitEventListenerModel>

  • The methode signature of the method BeforeSubmitAsync (IFormSubmitEventListener) changed.

From:

Task<SubmitListenerResult> BeforeSubmitAsync(TModel model, IFormSubmissionRequest formSubmissionRequest);

To:

Task<SubmitListenerResult> BeforeSubmitAsync(FormSubmit<TConfig> submit);

The submit object holds the earlier properties model and formSubmissionRequest.

  • The property Enumerable<FormSubmitEventListenerModel> ListenerModels on Form is not longer nullable.

Use the following migration to ensure there are no null values:

migrationBuilder.Sql(@"update ""ConfinityForm"" set ""ListenerModels"" = '[]'  where ""ListenerModels"" is null;");

v0.35.x

IHtmlHelper

Replaced ConfinityHead with ConfinityHeadAsync. Call it in your razor view as follows:

@await Html.ConfinityHeadAsync()

v0.34.x

Settings [migration]

Increased length for keys.

v0.33.x

Pages [migration]

  • Replaced MetaNoIndex flag with SearchIndexing selectable on the IDynamicPage interface
  • Replaced MetaNoIndex flag with SearchIndexing selectable on the Page entity. For production databases on Postgres please use the following migration:
protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.AddColumn<string>(
        name: "SearchIndexing",
        table: "ConfinityPage",
        type: "character varying(50)",
        maxLength: 50,
        nullable: false,
        defaultValue: "index");

    migrationBuilder.AddColumn<string>(
        name: "Entity_SearchIndexing",
        table: "__ConfinityHistory_ConfinityPage",
        type: "text",
        nullable: true);
    
    migrationBuilder.Sql(@"
        UPDATE ""ConfinityPage"" SET ""SearchIndexing"" = 'none'
        WHERE ""MetaNoIndex"" = True;
    ");
    migrationBuilder.Sql(@"
        UPDATE ""ConfinityPage"" SET ""SearchIndexing"" = 'index'
        WHERE ""MetaNoIndex"" = False;
    ");
    
    migrationBuilder.Sql(@"
        UPDATE ""__ConfinityHistory_ConfinityPage"" SET ""Entity_SearchIndexing"" = 'none'
        WHERE ""Entity_MetaNoIndex"" = True;
    ");
    
    migrationBuilder.Sql(@"
        UPDATE ""__ConfinityHistory_ConfinityPage"" SET ""Entity_SearchIndexing"" = 'index'
        WHERE ""Entity_MetaNoIndex"" = False;
    ");

    migrationBuilder.DropColumn(
        name: "MetaNoIndex",
        table: "ConfinityPage");

    migrationBuilder.DropColumn(
        name: "Entity_MetaNoIndex",
        table: "__ConfinityHistory_ConfinityPage");
}

v0.32.x

Replication [migration]

Dropped history table on entity ReplicationSchedule.

Entity App [migration]

Added index to column 'Entity_Id' for all history tables.

v0.31.x

Replication [migration]

New field FailureCount on entity ReplicationSchedule.

v0.30.x

UrlHelper

The extension method IUrlHelper.ConfinityAssetImageAsync returns now a Task<Uri?> in compliance with the other helper methods.

v0.29.x

Scheduled tasks

Scheduled task get a cancellation token passed to the Process Method.

public Task Process() -> public Task Process(CancellationToken cancellationToken)

v0.28.x

Replication

The password for the replication moved from Confinity.ReplicationHubSecret to ConfinityReplication.ReplicationHubSecret. The SignalR login for the replications will now use the ReplicationHubSecret as a symmetric key for the token issue / validation.

Error Pages

Confinity did accidentally handle all error pages (status code 399-599) and return the configured _error404 page. Starting with this release Confinity will only handle 404 errors.

v0.27.x

Pages [migration]

The pages app has a new option for case insensitive relocations.

Blog [migration]

The blog has new fields for meta description.

v0.26.x

Confinity.Blog [migration]

BlogArticle.AuthorId is not-nullable anymore. For production databases on Postgres please use the following migration:

protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.DropForeignKey(
        name: "FK_ConfinityBlogArticle_ConfinityBlogAuthor_AuthorId",
        table: "ConfinityBlogArticle");

    migrationBuilder.AlterColumn<Guid>(
        name: "AuthorId",
        table: "ConfinityBlogArticle",
        type: "uuid",
        nullable: false,
        defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
        oldClrType: typeof(Guid),
        oldType: "uuid",
        oldNullable: true);

    migrationBuilder.AlterColumn<Guid>(
        name: "Entity_AuthorId",
        table: "__ConfinityHistory_ConfinityBlogArticle",
        type: "uuid",
        nullable: false,
        defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
        oldClrType: typeof(Guid),
        oldType: "uuid",
        oldNullable: true);

    migrationBuilder.AddForeignKey(
        name: "FK_ConfinityBlogArticle_ConfinityBlogAuthor_AuthorId",
        table: "ConfinityBlogArticle",
        column: "AuthorId",
        principalTable: "ConfinityBlogAuthor",
        principalColumn: "Id",
        onDelete: ReferentialAction.Restrict);
    
    
    // Add this at the end of the migration.
    migrationBuilder.Sql(@"
        UPDATE ""ConfinityEntityReference"" SET ""IsOptional"" = false
        WHERE ""DependentEntityName"" = 'Confinity.Blog.BlogArticle'
        AND ""PrincipalId"" IN (SELECT ""Id"" FROM ""ConfinityBlogAuthor"");
    ");
}

v0.25.x

Confinity.Blog [migration]

The blog name was longer than what the database allowed. The blog authors now have a max length for the name and description.

v0.24.x

Change to async api

The following methods are now asynchronous, therefor their return value changed and the the method name got the suffix Async.

  • IAssetUrlHelper.CreateUrlForAssetAsync
  • IImageUrlHelper.CreateUrlForAssetImageAsync
  • IImageUrlHelper.CreateAbsoluteUrlForAssetImageAsync
  • UrlHelperExtension.ConfinityAssetImageAsync
  • UrlHelperExtension.ConfinityAssetAsync

v0.23.x

Refactoring of Confinity.Forms [migration]

Warning

There will be data loss when not paying attention to this change! The following instructions only apply for Postgres databases. For any other database, create a migration as usually and accept that the email configuration of forms will be lost.

Before upgrading to this version, you need to make sure there are no hanging migrations. To do so, create a migration and ensure that the Up-method is empty. You can delete the migration afterwards (don't forget to reset the Snapshot). Then follow these steps:

  1. Upgrade your dependencies and
  2. Rebuild the solution.
  3. Create a new migration
  4. Replace the generated Up-method with the following method
protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.AddColumn<string>(
        name: "ListenerModels",
        table: "ConfinityForm",
        type: "TEXT",
        nullable: true);
    
    migrationBuilder.AddColumn<string>(
        name: "Entity_ListenerModels",
        table: "__ConfinityHistory_ConfinityForm",
        type: "TEXT",
        nullable: true);
    
    migrationBuilder.Sql(@"
        UPDATE ""ConfinityForm""
        SET ""ListenerModels"" =
                format('[{' ||
                       '""$type"": ""Confinity.Forms.EmailFormSubmitEventListenerModel"",' ||
                       '""AllowDynamicRecipient"": %s,' ||
                       '""SendEmailTo"": %s,' ||
                       '""EmailSubject"": %s,' ||
                       '""EmailLead"": %s' ||
                       '}]',
                       COALESCE(to_json(""AllowDynamicRecipient""), 'null'),
                       COALESCE(to_json(""SendEmailTo""), 'null'),
                       COALESCE(to_json(""EmailSubject""), 'null'),
                       COALESCE(to_json(""EmailLead""), 'null'))
        WHERE ""SendEmail"" = true;
    ");

    migrationBuilder.Sql(@"
        UPDATE ""__ConfinityHistory_ConfinityForm""
        SET ""Entity_ListenerModels"" =
                format('[{' ||
                       '""$type"": ""Confinity.Forms.EmailFormSubmitEventListenerModel"",' ||
                       '""AllowDynamicRecipient"": %s,' ||
                       '""SendEmailTo"": %s,' ||
                       '""EmailSubject"": %s,' ||
                       '""EmailLead"": %s' ||
                       '}]',
                        COALESCE(to_json(""Entity_AllowDynamicRecipient""),'null'),
                        COALESCE(to_json(""Entity_SendEmailTo""),'null'),
                        COALESCE(to_json(""Entity_EmailSubject""),'null'),
                        COALESCE(to_json(""Entity_EmailLead""),'null'))
        WHERE ""Entity_SendEmail"" = true;
    ");
    
    migrationBuilder.DropColumn(
        name: "AllowDynamicRecipient",
        table: "ConfinityForm");

    migrationBuilder.DropColumn(
        name: "EmailLead",
        table: "ConfinityForm");

    migrationBuilder.DropColumn(
        name: "EmailSubject",
        table: "ConfinityForm");

    migrationBuilder.DropColumn(
        name: "SendEmail",
        table: "ConfinityForm");
    
    migrationBuilder.DropColumn(
        name: "SendEmailTo",
        table: "ConfinityForm");

    migrationBuilder.DropColumn(
        name: "Entity_AllowDynamicRecipient",
        table: "__ConfinityHistory_ConfinityForm");

    migrationBuilder.DropColumn(
        name: "Entity_EmailLead",
        table: "__ConfinityHistory_ConfinityForm");

    migrationBuilder.DropColumn(
        name: "Entity_EmailSubject",
        table: "__ConfinityHistory_ConfinityForm");

    migrationBuilder.DropColumn(
        name: "Entity_SendEmail",
        table: "__ConfinityHistory_ConfinityForm");
    
    migrationBuilder.DropColumn(
        name: "Entity_SendEmailTo",
        table: "__ConfinityHistory_ConfinityForm");
}

Moved UrlHelperExtensions to namespace Confinity.Forms

This is only relevant for the extension method ConfinityFormRecipient.

v0.22.x

Moved entity UserTask-entities to Confinity.UserTasks module [migration]

This might result in a migration that removes the tables Confinity_UserTask & Confinity_UserTaskLog if you are not using the Confinity.UserTasks module.

v0.21.x

Added notifications [migration]

For the new notifications feature, the entity Notification was added.

IStaticResourceHelper.CreateUrlForStaticResource signature change

To prevent coding errors the path parameter was changed to a PathString. The path now needs a leading slash and existing query parameters will get escaped and must be passed as a second parameter.

Migration:

// from: 
staticResourceHelper.CreateUrlForStaticResource(AnalyticsModule.ModuleKey, "dist/analytics.js?v2");

// to: 
staticResourceHelper.CreateUrlForStaticResource(AnalyticsModule.ModuleKey, "/dist/analytics.js", new QueryString("v2"));

v0.20.x

Move Icons to Confinity.Domain assembly

Because the namespace is still the same, this should not affect you.

v0.19.x

Extact TableLayoutBuilder's Add...Action methods to extension methods

AddDefaultAction, AddNewAction etc. are now extension methos for the ITableLayoutBuilder<T> and are available in the Confinity.EntityApp namespace.

Some overloads of the following methods expect a new generic parameter TEntity.

  • AddDefaultActions
  • AddNewAction
  • AddEditAction
  • AddArchiveAction
  • AddExportAction
  • AddPreviewAction

Therefor the invocation changes from

    tableLayoutBuilder.AddDefaultActions<PageActionEventListener>(PageFormConfiguration);

to

    tableLayoutBuilder.AddDefaultActions<Page, PageActionEventListener>(PageFormConfiguration);

v0.18.x

DateTimeOffset converter for SQLite [migration]

Instead of using the build in DateTimeOffsetToBinaryConverter for storing DateTimeOffset as long in the database, a custom converter is used that stores DateTimeOffset as DateTime. The DateTime is always in UTC.

Warning

There is no migration possible for this change. Your SQLite databases need to be recreated.

Maintenance

There is no maintenance job so far that cleans up the replication tables. You can add the following script to your PostgreSQL migration to clean up records older than a month.

migrationBuilder.Sql(@"
    DELETE FROM ""ConfinityReplicationSchedule""
    WHERE ""ConfinityMetadata_ModifiedAt"" < CURRENT_DATE - INTERVAL '1 month';

    DELETE FROM ""__ConfinityHistory_ConfinityReplicationSchedule""
    WHERE ""Entity_ConfinityMetadata_ModifiedAt"" < CURRENT_DATE - INTERVAL '1 month';
");

v0.17.x

TempFile added stream api and unified data structure [migration]

v0.16.x

TempFile add column for size [migration]

The TempFile service is updated to support chunked uploads. The documentation is updated: (https://confinity.conx.ch/latest/guides/fileupload.html)

The TempFile configuration has new, relatively low limits. Please check the documentation.

v0.15.x

Renamed Analytics Helper

@Html.AddAnalyticsScript() has become @Html.ConfinityAnalyticsHead()

v0.14.x

EntityApp: Column with ConfinitySelectable shows label instead of id

Tables and trees with columns that inherit from ConfinitySelectable will show the label instead of the id in your entity app.

decimal precision on history [migration]

History entities with decimal properties now use the same precision and scale as the main table instead of the default value of the database provider.

on-site editing store added [migration]

v0.13.x

AssetPath is no longer a database entity [migration]

v0.12.x

DeleteBehavior for non ConfinityEntities [migration]

Instead of changing the DeleteBehavior for all entities, only the entities inheriting form ConfinityEntity get their DeleteBehavior set to a default. See Cascade Delete for details.

For postgres: Add the following script to your migration (at the end):

migrationBuilder.Sql(@"
CREATE OR REPLACE FUNCTION confinityHistoryUpdate12() RETURNS void AS
$$
DECLARE
    rec           record;
    nbrow         bigint;
    v_RowCountInt Int;
BEGIN
    FOR rec IN
        SELECT *
        FROM information_schema.tables T1
        WHERE table_schema = 'public'
          AND table_name NOT LIKE '__ConfinityHistory_%'
          AND EXISTS(SELECT NULL
                     FROM information_schema.tables T2
                     WHERE T2.""table_name"" = '__ConfinityHistory_' || T1.""table_name"")
          AND table_name NOT IN ('ConfinityReplicationSchedule')
        ORDER BY table_name
        LOOP
            execute '
UPDATE ' || quote_ident('__ConfinityHistory_' || rec.table_name) || '
SET ""To"" = ''2021-12-31 23:59:59.999999+00''
WHERE ""To"" > ''2099-01-01 00:00:00''
and ""Entity_Id"" NOT IN (SELECT ""Id"" FROM ' || quote_ident(rec.table_name) || ' )';
            GET DIAGNOSTICS v_RowCountInt = ROW_COUNT;
            RAISE NOTICE 'Fixed % rows in table %', v_RowCountInt, quote_ident('__ConfinityHistory_' || rec.table_name);
        END LOOP;
END
$$ LANGUAGE plpgsql;

select confinityHistoryUpdate12();

DROP FUNCTION confinityHistoryUpdate12;
");

Tips

The migration is resource intensive and can be run separately after the installation.

v0.11.x

User Group [migration]

The UserGroup entity got the new property Key. Please include the following script in your migration.

migrationBuilder.Sql(@"
UPDATE ""ConfinityUserGroup"" SET ""Key"" = ""Name"";
UPDATE ""__ConfinityHistory_ConfinityUserGroup"" SET ""Entity_Key"" = ""Entity_Name"";
");

Dropped support for DeleteBehavior.CascadeDelete [migration]

Confinity does not support DeleteBehavior.CascadeDelete for entities inheriting ConfinityEntitiy. Use DeleteBehavior.ClientCascade instead. This is needed to ensure correct updates of the entity history.

v0.10.x

Confinity Search

Constants moved to a single class.

ConfinitySearchAttributes => ConfinitySearchConstants.AttributesConfinitySearchAttributeTypes => ConfinitySearchConstants.AttributeTypesConfinitySearchFacets => ConfinitySearchConstants.Facets

v0.9.x

Tips

Update to 0.8.x first (with migration) to make the update to .net6 easier.

.Net 6.0 / ef core 6.0 [migration]

  • the project was updated to use .net 6.0 see breaking changes
  • the project was update to use ef core 6.0 see breaking changes

Warning

Create a migration to verify your schema has not changed. If changes show up, update your model to remove the listed schema changes. The only change from Confinity is a recreation of the foreign key FK_ConfinityTag_ConfinityTag_SynonymForId.

IAsset, IImageAsset, IDocumentAsset changes

  • IAsset, IImageAsset, IDocumentAsset properties migrated to methods. Ex. Name => GetName() etc.
  • ConfinityPictureViewModel property Model renamed to Picture

v0.8.x

Page [migration]

  • removed column length constraint ConfinityPage.MetaCanonicalLink.
  • A type migration for Page.MetaCanonicalLink and IPageModel.MetaCanonicalLink from string? to LinkModel? is required.
migrationBuilder.Sql(@"
Update ""__ConfinityHistory_ConfinityPage"" set ""Entity_MetaCanonicalLink"" = null;
Update ""ConfinityPage"" set ""MetaCanonicalLink""= null;
");

v0.7.x

Forms [migration]

  • Added column ConfinityForm.AllowDynamicRecipient.

v0.6.x

Entity App: Replace ITableLayoutBuilder SortableBy with Sortable with

  • Support for configurable sort fields was removed. If entities should be sortable by authors, they must implement IConfinitySortable.

Pages [migration]

  • Rename Page.SortPosition to Page.SortIndex.
  • Rename PageModelExtensions.GetSortPosition to PageModelExtensions.GetSortIndex.

v0.5.11

Core [migration] [admin-configuration]

  • ScheduledTask got split into ScheduledTask and ScheduledTaskExecution. Before this split, every execution of a ScheduledTask would create a row in the corresponding history table. You can integrate the following script in your migration to tidy up the history table. It will delete all history entries but the newest per `ScheduledTask``.
migrationBuilder.Sql(
    @"WITH newest_entry AS
    (
        SELECT ""Id"" FROM ""__ConfinityHistory_ConfinityScheduledTask"" T1
        JOIN (SELECT MAX(""From"") ""MaxFrom"", ""Entity_Id""  from ""__ConfinityHistory_ConfinityScheduledTask"" GROUP BY ""Entity_Id"") T2
        ON T1.""From"" = T2.""MaxFrom"" AND T1.""Entity_Id"" = T2.""Entity_Id""
    )
    DELETE FROM ""__ConfinityHistory_ConfinityScheduledTask""
    WHERE ""Id"" NOT IN (SELECT ""Id"" from newest_entry);");

*** Important: All schedules must be reconfigured. Foreach scheduled task select on which instance they should run! ***

v0.5.10

Core [migration]

  • TempFile changes

v0.5.7

Core [migration]

  • Added TempFile & TempFileContent

v0.5.x

Removed Component.InvokeConfinityViewComponentAsync(object model)

Component.InvokeConfinityViewComponentAsync(object model) got removed. Use Component.InvokeConfinityViewComponentAsync(ViewComponentModel model)

v0.4.x

Moved DisableSecurityHeaders

Moved DisableSecurityHeaders from ConfinityOptions to new ConfinityHttpHeaderOptions.

v0.3.x

Core [migration]

  • Removed ChangeLogHistory

v0.2.x

Entity App

  • Renamed EntityEvents to IActionEventListener
  • Renamed EntityEventContext to ActionEventContext
  • Renamed EntityOperation to ActionEventType

v0.1.121

Forms Module

  • The base type for form fields has changed from FormField to FormItem

v0.1.111

Add index to ReplicationSchedule-Table [migration]

v0.1.98

Rendering of meta tags, stylesheets and javascripts

  • The html helper @Html.PageMetaData() is now @Html.ConfinityHead().
  • Removed the Scripts section and replaced it with the HTML helper @Html.ConfinityJavaScripts()
  • Removed the Styles section and replaced it with the HTML helper @Html.ConfinityStylesheets()
  • Removed IStaticResourceManager and replaced it with an extension for ViewData which can be used like this:
    • in a view component: HttpContext.ConfinityPage().Resources.AddJavaScript() (or .AddStyleSheet())
    • in a view: @Context.ConfinityPage().Resources.AddStylesheet() (or .AddJavaScript())

Changes in IModuleConfigurationBuilder

  • New type parameter TEntity for method RegisterPlaceholderPage
  • Rename AddMenuEntry -> AddMenuGroup

v0.1.95

Harmonization of dynamic links and page links

The Property IDynamicPage.Identifier was replaced by IDynamicPage.Id in order to harmonize the interaction with dynamic pages. This brought the following changes:

  • Removed IPageUrlHelper.CreateUrlForDynamicPage (use IPageUrlHelper.CreateUrlForPage instead)
  • Removed IPageService.GetDynamicPage (use IPageService.GetPage instead)
  • RemovedIPageService.CreateUrlForPage<TIdentifier> (use IPageService.CreateUrlForPage instead)

v0.1.94

Introduce property IsPrimary on PageDomain [migration]

By moving the PageDomain in its on entity app (instead of being a child list on Page) the property SortIndex got removed and a new IsPrimary property was added instead.

IMPORTANT

Before this release, the first (lowest index) PageDomain was considered to be primary. Use the following snippet in the beginning of your generated migration to update the data accordingly.

migrationBuilder.Sql("UPDATE \"ConfinityPageDomain\" SET \"SortIndex\"=0 WHERE \"SortIndex\" <> 1;");
migrationBuilder.Sql("UPDATE \"__ConfinityHistory_ConfinityPageDomain\" SET \"Entity_SortIndex\"=0 WHERE \"Entity_SortIndex\" <> 1;");

v0.1.90

Reading time and authors in blog module [migration]

  • Replaced User with BlogAuthor entity
  • Added reading time for articles

v0.1.88

Rework replication error handling [migration]

  • Rename ReplicationSchedule.ErrorMessage -> ReplicationSchedule.Error
    • Store Exception.ToString() instead of Exception.Message
  • Remove ReplicationSchedule.StackTrace

v0.1.87

Rename EntityReference.IsNullable -> EntityReference.IsOptional [migration]

v0.1.67

Reworked IModuleConfigurationBuilder

Many configuration methods signature was changed. They now return a Builder (e.g. IAppBuilder) instead of void. Thereby the number of parameters and overloads could be reduced by moving optional parameters in those builders.

  • AddApp returns a IAppBuilder (was a parameter before)
  • AddEntityApp returns a IEntityAppBuilder (was a parameter before)
  • AddEntityApp returns a IEntityAppBuilder (was a parameter before)
  • TryAddScheduledTask was renamed to RegisterScheduledTask and returns a newly introduced IScheduledTaskBuilder
  • RegisterPageLayout returns a newly introduced IPageLayoutBuilder
  • RegisterConfinityContent returns a newly introduced IConfinityContentBuilder
  • RegisterPlaceholderPage returns a newly introduced IPlaceholderPageBuilder
  • RegisterSettings returns a newly introduced ISettingsBuilder

v0.1.65

Changed signature for IHistoryDbContext.HistorySet

Removed the second parameter (DbContext historyContext) from IHistoryDbContext.HistorySet.

Replaced ReplicationSchedule.IsDeletion [migration]

ReplicationSchedule.IsDeletion got replaced by ReplicationSchedule.ReplicationType.

IMPORTANT

In order to not corrupt your existing replication schedules a Postgres DB, overwrite the generated migration content with the following code:

protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.AddColumn<string>(
        name: "ReplicationScheduleOptions",
        table: "ConfinityReplicationSchedule",
        type: "text",
        nullable: false,
        defaultValue: "");

    migrationBuilder.AddColumn<int>(
        name: "ReplicationType",
        table: "ConfinityReplicationSchedule",
        type: "integer",
        nullable: false,
        defaultValue: 0);

    migrationBuilder.AddColumn<string>(
        name: "Entity_ReplicationScheduleOptions",
        table: "__ConfinityHistory_ConfinityReplicationSchedule",
        type: "text",
        nullable: true);

    migrationBuilder.AddColumn<int>(
        name: "Entity_ReplicationType",
        table: "__ConfinityHistory_ConfinityReplicationSchedule",
        type: "integer",
        nullable: false,
        defaultValue: 0);
    
    migrationBuilder.Sql("UPDATE \"ConfinityReplicationSchedule\" SET \"ReplicationType\" = 1 WHERE \"IsDeletion\" = TRUE;");
    migrationBuilder.Sql("UPDATE \"__ConfinityHistory_ConfinityReplicationSchedule\" SET \"Entity_ReplicationType\" = 1 WHERE \"Entity_IsDeletion\" = TRUE;");
    
    migrationBuilder.DropColumn(
        name: "IsDeletion",
        table: "ConfinityReplicationSchedule");

    migrationBuilder.DropColumn(
        name: "Entity_IsDeletion",
        table: "__ConfinityHistory_ConfinityReplicationSchedule");
}

protected override void Down(MigrationBuilder migrationBuilder)
{
    migrationBuilder.AddColumn<bool>(
        name: "IsDeletion",
        table: "ConfinityReplicationSchedule",
        type: "boolean",
        nullable: false,
        defaultValue: false);

    migrationBuilder.AddColumn<bool>(
        name: "Entity_IsDeletion",
        table: "__ConfinityHistory_ConfinityReplicationSchedule",
        type: "boolean",
        nullable: false,
        defaultValue: false);
    
    migrationBuilder.Sql("UPDATE \"ConfinityReplicationSchedule\" SET \"IsDeletion\" = TRUE WHERE \"ReplicationType\" = 1;");
    migrationBuilder.Sql("UPDATE \"__ConfinityHistory_ConfinityReplicationSchedule\" SET \"Entity_IsDeletion\" = TRUE WHERE \"Entity_ReplicationType\" = 1;");
    
    migrationBuilder.DropColumn(
        name: "ReplicationScheduleOptions",
        table: "ConfinityReplicationSchedule");

    migrationBuilder.DropColumn(
        name: "ReplicationType",
        table: "ConfinityReplicationSchedule");

    migrationBuilder.DropColumn(
        name: "Entity_ReplicationScheduleOptions",
        table: "__ConfinityHistory_ConfinityReplicationSchedule");

    migrationBuilder.DropColumn(
        name: "Entity_ReplicationType",
        table: "__ConfinityHistory_ConfinityReplicationSchedule");
}

v0.1.55

Renamed appsettings keys

In order to have appsettings that can be overwritten by environment variables, the following keys got renamed:

  • Confinity.Assets -> ConfinityAssets
  • Confinity.Pages -> ConfinityPages
  • Messaging -> ConfinityMessaging
  • Confinity.Elasticsearch -> ConfinityElasticsearch
  • Confinity.MediaPlayer -> ConfinityMediaPlayer
  • Confinity.Replication -> ConfinityReplication

Changed ConfinityReplication appsettings to dictionary structure

Example:

{
  "ConfinityReplication": {
    "Stages": {
      "previewStage": {
        "Key": "previewStage",
        "Order": 1,
        "Label": "Preview",
        "Instances": {
          "preview1": {
            "Key": "preview1",
            "ServerUrl": "http://preview1",
            "Secret": "confinityconfinity"
          }
        }
      },
      "publicStage": {
        "Key": "publicStage",
        "Order": 2,
        "Label": "Public",
        "Instances": {
          "public1": {
            "Key": "public1",
            "ServerUrl": "http://public1",
            "Secret": "confinityconfinity"
          },
          "public2": {
            "Key": "public2",
            "ServerUrl": "http://public2",
            "Secret": "confinityconfinity"
          }
        }
      }
    }
  }
}

v0.1.53

Moved replication settings into appsettings

The replication configuration (e.g. stages, instances) is no longer a setting but moved to the appsettings. Example:

{
  "Confinity.Replication": {
    "Stages": [
      {
        "Key": "previewStage",
        "Order": 1,
        "Label": "Preview",
        "Instances": [
          {
            "Key": "preview1",
            "ServerUrl": "http://preview1",
            "Secret": "confinity"
          }
        ]
      },
      {
        "Key": "publicStage",
        "Order": 2,
        "Label": "Public",
        "Instances": [
          {
            "Key": "public",
            "ServerUrl": "http://public1",
            "Secret": "confinity"
          }
        ]
      }
    ]
  }
}

v0.1.49

Removed IFolderHelper

Removed IFolderHelper and added its methods to IAssetService instead.

v0.1.48

Reworked Unique validator

Calling Unique now has an optional parameter Action<IUniqueValidatorBuilder> with the methods WithSameValue and ErrorMessage and returns a IElementValidatorBuilder.

v0.1.47

Reworked Unique validator

Calling Unique now returns a IUniqueValidatorBuilder with the method WithSameValue. This replaces the overload of Unique with a params of same value selectors.

v0.1.39

User Management [migration]

Added field User Provider to store the system from where the user comes from.

v0.1.37

Moved IAsset

Moved IAsset from namespace Confinity.Interfaces to Confinity.Assets.

Renamed IAssetHelper

Renamed IAssetHelper to IAssetService.

ImageTagHelper and ConfinityPictureComponent requiring IImageAsset

The ImageTagHelper and the ConfinityPictureComponent now require an IImageAsset instead of an IAsset.

You can use IAssetService.GetImageAsset() to get an IImageAsset.

v0.1.29

Renamed ICmsSortable to IConfinitySortable

Renamed the ICmsSortable to IConfinitySortable.

v0.1.28

Renamed AddSelectAsset for multiple assets to AddSelectAssets

Renamed the AddSelectAsset on IFormContainerBuilder to AddSelectAsset for multiple assets.

v0.1.20

Renamed CreateUrlForAssetImage and CreateUrlForAsset

Renamed the IUrlHelper extensions methods CreateUrlForAssetImage and CreateUrlForAsset to ConfinityAssetImage and ConfinityAsset respectively.

v0.1.19

IDynamicPageResolver expects async implementation

Changed signature of GetPagesForPlaceholderPageAsync to

Task<IEnumerable<IDynamicPage>> GetPagesForPlaceholderPageAsync(
            TConfiguration configuration, IPageModel placeholderPageModel);

0.1.21

AssetApp

FolderHelper moved to internal package use IFolderHelper

0.1.16

ModuleConfigurationBuilder

Renamed IModuleConfigurationBuilder.RegisterChangeHandler to IModuleConfigurationBuilder.RegisterChangeListener.

IEntityChangeListener: Abstraction for parameter

IEntityChangeListener.Handle parameter changed. IEnumerable<EntityChange> -> IEnumerable<EntityChange>.

Bugfix IEntityChangeListener: Called on all replication instances

Fixed a bug where the implementations of IEntityChangeListener were not invoked on all instances when there where multiple instances configured for the same stage.

0.1.9

Configuration

Email configuration Key changed from Confinity.Mail to ConfinityMail. This way, one can overwrite the variable with environments variables.

0.1.6

IPageService: Simplify methods

All methods in IPageService that return a IPageModel and required a HttpContext now obtain the HttpContext themself and are renamed.

  • IPageModel? GetPage(HttpContext httpContext) -> IPageModel? GetCurrentPage()
  • IPageModel? Get404Page(HttpContext httpContext) -> IPageModel? GetCurrent404Page()
  • IPageModel? GetWebsite(HttpContext httpContext) -> IPageModel? GetCurrentWebsite()
  • Uri? CreateUrlForPage<TIdentifier>(TIdentifier pageIdentifier, HttpRequest httpRequest, bool forceAbsolute = false) -> Uri? CreateUrlForPage<TIdentifier> (TIdentifier pageIdentifier, bool forceAbsolute = false)

0.1.5

Pages: Renamed enum values of PageTyes

Renamed enum PageType.Page to PageType.ContentPage & PageType.DynamicPage to PageType.PlaceholderPage.

Pages: Renamed class DynamicPageConfiguration

Renamed class DynamicPageConfiguration to PlaceholderPageConfiguration.

Removed IPageStore

The IPageStore is now an internal interface. Developers can use IPageService (to retrieve pages) & IPageCacheService to rebuild the page cache.

Introduced IPageModel

IPageService & IPageUrlHelper no longer work with the entity Page but with the abstraction IPageModel.

Reworked IDynamicPageResolver

IDynamicPageResolver has only one method to implement now:

IEnumerable<IDynamicPage> GetPagesForPlaceholderPage(TConfiguration configuration,
    IPageModel placeholderPageModel);

0.1.x

Changes of Confinity entities require a data migration.

PostgreSQL

 // will be recreated on launch
migrationBuilder.Sql(@"DELETE FROM ""ConfinityRoleRolePermission"";");
migrationBuilder.Sql(@"DELETE FROM ""__ConfinityHistory_ConfinityRoleRolePermission"";");
migrationBuilder.Sql(@"DELETE FROM ""ConfinityRolePermission"";");
migrationBuilder.Sql(@"DELETE FROM ""__ConfinityHistory_ConfinityRolePermission"";");

// default value for new not null
migrationBuilder.Sql(@"UPDATE ""ConfinityUser"" set ""PasswordHash"" = ''::bytea where ""PasswordHash"" is null;");

sqlite

 // will be recreated on launch
migrationBuilder.Sql(@"DELETE FROM ConfinityRoleRolePermission;");
migrationBuilder.Sql(@"DELETE FROM __ConfinityHistory_ConfinityRoleRolePermission;");
migrationBuilder.Sql(@"DELETE FROM ConfinityRolePermission;");
migrationBuilder.Sql(@"DELETE FROM __ConfinityHistory_ConfinityRolePermission;");

0.0.99

The method ContextAction has been removed from IEntityAppActionConfigurationBuilder. Every action that has not set an ' ContextAction' is defining a permission.

0.0.90

PageUrlHelper: Moved method CreateUrlForStaticResource

The method CreateUrlForStaticResource has been moved from IPageUrlHelper to IStaticResourceHelper.

Prev
Authentication & SSO
Next
Roslyn Source Analyzers