Consolidated Data Access

The biggest feature of this release is the introduction of a new consolidated data access repository that simplifies working with Cofoundry data programmatically.

IContentRepository

IContentRepository is the primary interface for our new data access API. It has an easily discoverable fluent API and is enriched with inline documentation to help you choose the most suitable query or command for you needs.

This example uses the fluent builder to describe the query we want and select how much detail is returned by selecting the projection type:

using Cofoundry.Domain;

public class ExampleController : Controller
{
    private readonly IContentRepository _contentRepository;

    public ExampleController(
        IContentRepository contentRepository
        )
    {
        _contentRepository = contentRepository;
    }

    [Route("/api/categories-example")]
    public async Task<IActionResult> Categories()
    {
        var categories = await _contentRepository
            .CustomEntities() // select entity type
            .GetByDefinition<CategoryCustomEntityDefinition>() // select query type
            .AsRenderSummary() // select projection
            .ExecuteAsync(); // execute

        return Json(categories);
    }
}

As you build the query, you can use the intellisense hints provided by our XML comments to understand the difference between different queries or projection types:

XML comments in the content repository

This example uses an MVC controller, but you can use this interface anywhere that supports dependency injection just by requesting an instance of IContentRepository.

IAdvancedContentRepository

We designed IContentRepository to be noise-free and only include the most common queries you might use in a website, however, we have also created IAdvancedContentRepository, which builds upon IContentRepository and includes the full range of queries and commands available in Cofoundry.

Other Features

Both repositories can be extended via extension methods, so that plugins can light-up additional data access paths.

They also share the following features:

Here's an example combining a few of these features, which prior to this release would have required dependencies on 7 different services to be injected:

public async Task RegisterUser(string email, string password)
{
    var exampleRoleId = await _advancedContentRepository
        .Roles()
        .GetByCode(ExampleRole.ExampleRoleCode)
        .AsDetails()
        .Map(r => r.RoleId)
        .ExecuteAsync();

    using (var scope = _advancedContentRepository
        .Transactions()
        .CreateScope())
    {
        await _advancedContentRepository
            .WithElevatedPermissions()
            .Users()
            .AddAsync(new AddUserCommand()
        {
            Email = email,
            Password = password,
            UserAreaCode = MemberUserArea.AreaCode,
            RoleId = exampleRoleId
        });

        // ...do some other things

        await scope.CompleteAsync();
    }
}

You can find out more in the data access docs.

Deprecations

Deprecated code will be removed in an upcoming release.

IAsyncCommandHandler, IAsyncQueryHandler

Handler names have been changed to remove the reference to "Async". When we used to target .NET Framework we needed sync handlers, but they are no longer necessary in .NET Core.

  • IAsyncQueryHandler is now IQueryHandler
  • IAsyncCommandHandler is now ICommandHandler

Repositories

The individual entity repositories have now been replaced by IContentRepository and are now marked as deprecated.

Breaking changes

Internal Namespace

We've started moving some code to an Internal namespace to better indicate that these classes are not for general use. This includes classes such as handler and service implementations, which should not be referenced directly.

Although Microsoft are in the process of removing all their "Pubternal" namespaces, for us I think it's a good way to keep all our code accessible while clearly defining what is and is not part of the public API surface.

AssetFilesSettings

We've updated some of the terms used in asset file type validation to put them inline with current standards:

  • AssetFileTypeValidation.Whitelist is now AssetFileTypeValidation.Allowlist
  • AssetFileTypeValidation.Blacklist is now AssetFileTypeValidation.Blocklist

If you've customized AssetFilesSettings.FileExtensionValidation or AssetFilesSettings.MimeTypeValidation, you may need to update your settings.

PropertyValidationErrorException is now ValidationErrorException

We've improved some of our validation error classes in include a new ErrorCode string property. This is useful in web APIs where the error is serialized, making it easier for JavaScript front-ends to handle specific types of validation errors.

To enable this feature we have restructured this area a little and PropertyValidationErrorException has changed to be ValidationErrorException

TransactionScopeManager: default isolation changed from Serializable to ReadCommitted

The default transaction scope isolation of Serializable is not ideal and not aligned with the SQLServer default. We have therefore decided to change it to ReadCommited.

We've also added ways to override this behaviour by default and on a per-use basis. See the Transactions section of the docs for more information on how to do this.

Notice on .NET 5 and .NET 6

Having been burnt a little with the support cycle around .NET Core 2.2 we are now only going to target LTS .NET releases. This means that we will not move to .NET 5 and instead wait for the .NET 6 LTS release.

Bug fixes

  • #383 Error loading page template detail page

Links