Cofoundry 0.5 includes a significant number of smaller features and bug fixes mostly centered around image assets, document assets and feature configuration. The main breaking changes you may encounter are with asset URLs and with page block type display model mappers, read on for more details.

Permanently cacheable asset files

For static resources it is best practice to set cache headers with a long expiry date. To do this requires resource URLs to be immutable; if the resource file changes, the URL should change.

Until now this has not been the case for Cofoundry asset URLs and this has made setting long cache expiry dates problematic. One of the big changes in this release is that asset URLs are now immutable; if the file against the image or document asset changes, then so does the URL.

Furthermore, if the asset file is updated or the file name changes, then any requests to the old URL will be redirected to the the updated asset URL. This ensures you can update your image assets without breaking any hard-coded or external URLs or harming your SEO.

Compatibility URLs

To implement this feature we've had to make a change to image asset URL path. If you're rendering assets using the Cofoundry routing helper then your URLs will automatically be updated:

@inject ICofoundryHelper Cofoundry

<img src="@Cofoundry.Routing.ImageAsset(Model.HeaderImageAsset, 400, 300)">

But if you have hard-coded URLs or want to maintain external references to your existing images you can enable the old asset path using a configuration flag:

    "Cofoundry": {
        "ImageAssets:EnableCompatibilityRoutesFor0_4": true,
        "DocumentAssets:EnableCompatibilityRoutesFor0_4": true

The only downside to keeping these compatibility routes enabled is that they are vulnerable to enumeration i.e. a user could tamper with the asset ids in the URLs to discover assets that may not yet available on your live site. This has been fixed in the new URL structure.

Improving block type mapping

The IPageBlockTypeDisplayModelMapper interface used for custom mapping of page block types has been refined to make it a little more straight forward to use. The main change is the introduction of a mapping context that groups together several parameters and makes it easier for us to extend in the future without making a breaking change.

We also pass in the result collection so you don't need to create this yourself or deal with wrapping the mapped output correctly, which will be done for you when you add it to the collection.

Lastly, we've also included the IExecutionContext of the calling query into the mapping context so you can pass it down to any child queries. This is really only for advanced scenarios where your mapper might be called under an elevated user account e.g. in a startup or background process.

The new example from the docs looks like this:

using Cofoundry.Domain;
using Cofoundry.Core;

/// <summary>
/// The mapper supports DI which gives you flexibility in what data
/// you want to include in the display model and how you want to 
/// map it. Mapping is done in batch to improve performance when 
/// the same module type is used multiple times on a page.
/// </summary>
public class MyContentDisplayModelMapper : IPageBlockTypeDisplayModelMapper<MyContentDataModel>
    private IImageAssetRepository _imageAssetRepository;

    public MyContentDisplayModelMapper(
        IImageAssetRepository imageAssetRepository
        _imageAssetRepository = imageAssetRepository;

    public async Task MapAsync(
            PageBlockTypeDisplayModelMapperContext<MyContentDataModel> context,
            PageBlockTypeDisplayModelMapperResult<MyContentDataModel> result
        var imageAssetIds = context.Items.SelectDistinctModelValuesWithoutEmpty(i => i.ThumbnailImageAssetId);
        var imageAssets = await _imageAssetRepository.GetImageAssetRenderDetailsByIdRangeAsync(imageAssetIds, context.ExecutionContext);

        foreach (var item in context.Items)
            var displayModel = new MyContentDisplayModel();
            displayModel.Title = item.DataModel.Title;
            displayModel.Description = new HtmlString(item.DataModel.Description);
            displayModel.ThumbnailImageAsset = imageAssets.GetOrDefault(item.DataModel.ThumbnailImageAssetId);

            result.Add(item, displayModel);

Disabling features in config

Cofoundry includes serveral website-orientated features by default in order to provide a familiar out-of-the-box experience for website development, but these features can now be turned off if you're not using them. This is useful if you're just building an API or running "headless":

    "Cofoundry": {
        "Pages:Disabled": true,
        "ImageAssets:Disabled": true,
        "DocumentAssets:Disabled": true,

We've also added a config setting to disable the admin panel completely. This is useful if you want to use the same codebase to deploy the site to both a public facing server and a back-office server behind a restrictive firewall.

    "Cofoundry": {
        "Admin:Disabled": true

Changing the admin panel path

You can now change the admin panel path by simply changing a config setting:

    "Cofoundry": {
        "Admin:DirectoryName": "my-administration-path"

This is useful if you have an existing application with a path conflict on '/admin' or if you want to obscure the admin panel path to make it less discoverable.

Asset file cleanup & soft-deletes

There were a handful of entities in the Cofoundry database that marked deleted records using a soft-delete technique, this has now been removed and deleted data has been purged.

Assets were one of the entities that used soft-deletes and so prior to this release the asset files were never removed. We've now implemented a background task to periodically clean up deleted files. Using a queue and a background task here allows asset file deletion to be transactional and resilient to failures due to file locking.

Cofoundry does not yet have a default background task runner, so for this to run you'll need to install the Hangfire background task plugin. We will be working on a simple default background task runner in a later update, this is tracked in issue 148.

Querying custom entities by UrlSlug

We've added a new query for fetching custom entities using the UrlSlug property. Here's an example taken from the menu sample project

private async Task<CustomEntityRenderSummary> GetMenuByIdAsync(string menuId)
    var customEntityQuery = new GetCustomEntityRenderSummariesByUrlSlugQuery(NestedMenuDefinition.DefinitionCode, menuId);
    var menus = await _customEntityRepository.GetCustomEntityRenderSummariesByUrlSlugAsync(customEntityQuery);

    // Forcing UrlSlug uniqueness is a setting on the custom entity definition and therefpre
    // the query has to account for multiple return items. Here we only expect one item.
    return menus.FirstOrDefault();

Configurable file type validation

Cofoundry has always validated uploaded files against a blacklist of potentially dangerous file extensions and mime types. By default this used the list in DangerousFileConstants.cs, but we've now made this fully configurable.

Read more about this in the documentation.

Automatic database updates

Please note that this release contains automated database updates.

These will apply when you run your application for the first time so please ensure you are not connecting to production databases from your development environment and have tested the update before applying it to production systems.

Bug fixes and breaking changes

The main breaking changes you're likely to encounter are listed here, but for a complete list of bug fixes, features and breaking changes check out the full version 0.5 release notes on GitHub.