JSON

6 posts

JOS.Epi.ContentApi updated to version 3.0.0

Thanks to the discussion in the comments on the blogpost written by Johan Björnfot I could simplify the code a lot and remove the HttpModule.

Version 3.0.0 is now available on the nuget feed.

It's a major version bump since I removed the HttpModule, you will need to remove the ContentApiModule entry in your web.config to make it work.

Remove the following entry

<system.webServer>  
  <modules>
    .....
    <add name="ContentApiModule" type="JOS.Epi.ContentApi.ContentApiModule, JOS.Epi.ContentApi" />
    ....
  </modules>    
</system.webServer>  
Headless Episerver? Meet JOS.Epi.ContentApi

Headless Episerver? Meet JOS.Epi.ContentApi

I read this blog post by Mathias Kunto where he says that he will leave my beloved library behind and start using the new Episerver Headless API instead.

I felt like the picture above :(

I couldn't stand it. So, I decided to create a new library; meet JOS.Epi.ContentApi..
JOS.Epi.ContentApi uses JOS.ContentSerializer by default(this can be changed by swapping out the IContentApiSerializer) so you don't need to have Episerver Find to use it.

I haven't had the time to try out the new Episerver version so I don't know how well it works, but I noticed this quote from Mathias:

I had a quick look at the API functionality, and it seems like you get pretty JSON requesting URLs like /api/episerver/v1.0/content/3 and so on. However, what I really wanted was friendly URLs delivering JSON for the page in question. For instance, requesting /en/alloy-plan/download-alloy-plan/start-downloading/ would give me JSON for the Start Downloading page.

I don't like that you must know the ContentReference to fetch the JSON data, it would be better if it worked like Mathias wants it to work.

I've built that.

How to use it

  1. Install-Package Jos.Epi.ContentApi(Normal nuget, not episerver feed)
  2. Set your accept header to "application/json" and make a GET request to your desired page.
  3. Profit.

Note, by default(this can be changed) the library will only serialize the response if the Accept header contains ONE value, not two, not empty, one.
If you want to change this behaviour, just swap out the IShouldSerializeResponseStrategy interface.

You can also customize when/if the serialization should take place in the same method.

Demo

This is version 1, I've already started working on filtering and stuff like that, stay tuned!

As always, the code can be found on Github.

Customizing PropertyHandlers in JOS.ContentSerializer

The problem

Imagine the following ContentType:

public class PostPage : PageData  
{
    ....
    [Display(Order = 400)]
    [CultureSpecific]
    [AllowedTypes(typeof(AuthorBlock))]
    public virtual ContentReference Author { get; set; }
    ...
}

As you can see, it's only possible to select AuthorBlocks in the Author-property.
The default implementation of ContentReferencePropertyHandler looks like this:

public class ContentReferencePropertyHandler : IPropertyHandler<ContentReference>  
{
    private readonly IUrlHelper _urlHelper;
    private readonly IContentSerializerSettings _contentSerializerSettings;

    public ContentReferencePropertyHandler(IUrlHelper urlHelper, IContentSerializerSettings contentSerializerSettings)
    {
        _urlHelper = urlHelper;
        _contentSerializerSettings = contentSerializerSettings ?? throw new ArgumentNullException(nameof(contentSerializerSettings));
    }

    public object Handle(ContentReference contentReference, PropertyInfo propertyInfo, IContentData contentData)
    {
        if (contentReference == null || contentReference == ContentReference.EmptyReference)
        {
            return null;
        }

        var url = new Uri(this._urlHelper.ContentUrl(contentReference, this._contentSerializerSettings.UrlSettings));

        if (this._contentSerializerSettings.UrlSettings.UseAbsoluteUrls && url.IsAbsoluteUri)
        {
            return url.AbsoluteUri;
        }

        return url.PathAndQuery;
    }
}

What will happen when we run .ToJson?
Well, since it's impossible to link directly to a block, the output will look like this:

{
    ...
    "author": "http://localhost:54321/"
    ...
}

That's not nice, it would be better if we could display the actual block instead.

The solution

Simply create a new class implementing the IPropertyHandler<ContentReference> interface and then register it in the DI container.

Improved ContentReferenceHandler

public class CustomContentReferencePropertyHandler : IPropertyHandler<ContentReference>  
{
    private readonly IUrlHelper _urlHelper;
    private readonly IContentSerializerSettings _contentSerializerSettings;
    private readonly IContentLoader _contentLoader;
    private readonly IPropertyHandler<BlockData> _blockDataPropertyHandler;

    public CustomContentReferencePropertyHandler(
        IUrlHelper urlHelper,
        IContentSerializerSettings contentSerializerSettings,
        IContentLoader contentLoader,
        IPropertyHandler<BlockData> blockDataPropertyHandler)
    {
        _urlHelper = urlHelper ?? throw new ArgumentNullException(nameof(urlHelper));
        _contentSerializerSettings = contentSerializerSettings ?? throw new ArgumentNullException(nameof(contentSerializerSettings));
        _contentLoader = contentLoader ?? throw new ArgumentNullException(nameof(contentLoader));
        _blockDataPropertyHandler = blockDataPropertyHandler ?? throw new ArgumentNullException(nameof(blockDataPropertyHandler));
    }

    public object Handle(ContentReference contentReference, PropertyInfo property, IContentData contentData)
    {
        if (contentReference == null || contentReference == ContentReference.EmptyReference)
        {
            return null;
        }

        if (IsReferenceToBlock(property))
        {
            if (this._contentLoader.TryGet<BlockData>(contentReference, out var blockData))
            {
                return this._blockDataPropertyHandler.Handle(blockData, property, contentData);
            }
        }

        var url = new Uri(this._urlHelper.ContentUrl(contentReference, this._contentSerializerSettings.UrlSettings));

        if (this._contentSerializerSettings.UrlSettings.UseAbsoluteUrls && url.IsAbsoluteUri)
        {
            return url.AbsoluteUri;
        }

        return url.PathAndQuery;
    }

    private static bool IsReferenceToBlock(MemberInfo property)
    {
        var allowedTypesAttribute = property.GetCustomAttribute<AllowedTypesAttribute>();

        if (allowedTypesAttribute?.AllowedTypes == null || !allowedTypesAttribute.AllowedTypes.Any())
        {
            return false;
        }

        return allowedTypesAttribute.AllowedTypes.All(x => typeof(BlockData).IsAssignableFrom(x));
    }
}

Replace the default implementation

Replace for all properties of type ContentReference

[InitializableModule]
[ModuleDependency(typeof(JOS.ContentSerializer.Internal.ContentSerializerInitalizationModule))]
public class ContentSerializerInitializationModule : IConfigurableModule  
{
    public void Initialize(InitializationEngine context) {}

    public void Uninitialize(InitializationEngine context) {}

    public void ConfigureContainer(ServiceConfigurationContext context)
    {
        context.Services.RemoveAll<IPropertyHandler<ContentReference>>();
        context.Services.AddSingleton<IPropertyHandler<ContentReference>, CustomContentReferencePropertyHandler>();
    }
}

Replace for specific property

[Display(Order = 400)]
[CultureSpecific]
[AllowedTypes(typeof(AuthorBlock))]
[ContentSerializerPropertyHandler(typeof(CustomContentReferencePropertyHandler))]
public virtual ContentReference Author { get; set; }