EPiServer, EPiServer Add-On

Swapcode.Episerver.AuditLog is now available in Episerver NuGet feed

What is it and what it does? Keep reading to find out.

In my previous blog post I wrote about how to write the content security changes to Episerver ‘Change Log’ (activity log) and then administrators can view and filter those changes in the out-of-the-box view. The actual code required for this is really simple and not hard to copy paste to your project – but why You should need to do that when the code can be packaged to a NuGet package and shared from Episerver NuGet Feed.

So here you go, the code is now available as a NuGet package named Swapcode.Episerver.AuditLog in Episerver NuGet feed. All you need to do is add the Episerver NuGet feed configuration to your Visual Studio configuration or add it to your solutions NuGet.config file. Then you can easily install it to your project from Visual Studio package manager console or from solution/project context menu.

What it does?

When a user has changed access rights in Episerver for content then an event is triggered in the system which this add-on is listening and then writes the change to Episerver ‘Change Log’ (activity log :D). Then these changes can be viewed in the ‘Change Log’ – sometimes there are these questions “Who changed the access rights to this content?” but out-of-the-box that information is nowhere to be seen.

New category ‘Content security’ added and Action filters.

When the package is installed a new category ‘Content security’ is added to the ‘Category’ filter and when selected then the ‘Action’ dropdown will show the possible additional action filters.

Sample listing of audit log entries.

So now when an administrator gets a request about who changed the access rights they can easily view that from the change log (activity log).

Is it really an audit log?

No.

Why? Because activities are not stored forever (neither the archived ones). Please read the Episerver documentation about ‘Activity Log’ and how you can configure it. So for example if you would like to keep the activities archived for 3 years then you would need to add or modify existig attribute named ‘activityArchiveRetentionPeriod’ in web.config, applicationSettings section below the episerver section and set the value to 36 (meaning 36 months => 3 years ;P). At the moment of writing the configuration information is missing from the documentation on the applicationSettings configuration reference documentation but maybe that gets added soon (pinged the authors responsible for the documentation).

Is it free? Is it open source?

Yes and Yes.

You can find the sources from my GitHub repository.

Closing comments

I initially wanted to support Episerver version 10.10.4 as it has the IActivityRepository required by this implementation (note: that was released in 30 Aug, 2017) BUT then later on there were also changes to the class EPiServer.Web.InitializationModule – it used to be in assembly EPiServer but was later moved to EPiServer.Cms.AspNet assembly in version 11.1.0 (released Nov 21, 2017) and this created an issue for me to support 10.10.4 version as I would need to have two NuGet packages with different dependencies.

So my decision was easy, the 10.10.4 was released 3 years ago and You should not be running that old version anymore! If you really need the functionality on such old version then copy paste code is your solution 😉

So the minimum version supported by the package is 11.1.0 (dependecy to EPiServer.CMS.AspNet package version 11.1.0) and .NET Framework 4.6.1.

Activity log, CMS, EPiServer, Episerver 11

Episerver activity log with custom content security activity

In this post I will show you how you can create a custom content security activity and store that to Episerver activity log (change log) when access rights are changed in Episerver.

This blog post was inspired by the fact that there is no out of the box audit log about content access rights changes and often it is a requirement in customer projects that the changes are logged. Episerver IContentSecurityRepository exposes events of changes and we are interested of the ContentSecuritySaved event where we get the information what content the change is about and what changes were done. So in projects we can have an audit logger (log4net) that we use to log these changes or have custom DB table where we store this information but then we need to implement a custom view to see the entries or the audit log is only available to developers or if Application Insights is used then those entries can be seen there (but there is a drawback with Application Insights, it is not preserved enough long).

But what we could do is have a custom Episerver activity and then store the access rights changes to Episerver activity log (well it is called ‘Change log’ in admin view). And the people with admin view access can see the changes in the log using the built-in ‘Change log’ view. NOTE! read the activity loggin developer instructions and configuration – activity log entries are kept for one month and then archived for 12 months by default. In the admin view you can view the archived entries but if you need longer than 12 months retention then you must change the configuration.

Custom activity code

The code needed is quite simple – we need a custom activity class to present the activity in the log (or category), enum to hold different actions related to the activity and an initialization module to register the activity and hook to the IContentSecurityRepository ContentSecuritySaved event.

Action type enum

Here is the enum used as the different actions related to our custom activity. The enum values map to the same values used on EPiServer.Security.SecuritySaveType, as this was fast way not to try to invent my own actions – we are logging the access rights chnages after all.


namespace EpiAuditLogging.Web.Features.AuditLogging
{
/// <summary>
/// Content security action type (maps directly to <see cref="EPiServer.Security.SecuritySaveType"/>).
/// </summary>
public enum ContentSecurityActionType
{
// same as EPiServer.Security.SecuritySaveType
None = 0,
RecursiveReplace = 1,
RecursiveModify = 2,
Modify = 3,
Replace = 4,
ReplaceChildPermissions = 5,
MergeChildPermissions = 6
}
}

Custom ContentSecurityActivity

This is the custom activity class we use. The dictionary passed in the constructor is the data that is going to be logged to the activity log.


using System.Collections.Generic;
using EPiServer.DataAbstraction.Activities;
namespace EpiAuditLogging.Web.Features.AuditLogging
{
public class ContentSecurityActivity : Activity
{
/// <summary>
/// Name of this activity (used for example to register this activity).
/// </summary>
public const string ActivityTypeName = "ContentSecurity";
/// <summary>
/// Creates a new instance using the given action type.
/// </summary>
/// <param name="action">What kind of action was done, <see cref="ContentSecurityActionType"/>.</param>
public ContentSecurityActivity(ContentSecurityActionType action) : base(ActivityTypeName, (int)action, new Dictionary<string, string>(0)) { }
/// <summary>
/// Creates a new instance using the action type and dictionary containing the data that will be logged.
/// </summary>
/// <param name="action">What kind of action was done, <see cref="ContentSecurityActionType"/>.</param>
/// <param name="extendedData">The data in dictionary that will be logged to the activity log.</param>
public ContentSecurityActivity(ContentSecurityActionType action, IDictionary<string, string> extendedData) : base(ActivityTypeName, (int)action, extendedData) { }
}
}

Custom initialization module

And finally we need an initialization module to register our custom activity and hook to the access rights changed event. NOTE! the code that registers the activity uses Episerver internal API from EPiServer.DataAbstraction.Activities.Internal.IActivityTypeRegistry but I hope this will be moved to public API so that we don’t loose our sleep if they suddenly would decide to make it internal or make other changes to the signatures. You have been warned 😀


using System;
using System.Collections.Generic;
using System.Linq;
using EPiServer.DataAbstraction;
using EPiServer.DataAbstraction.Activities;
using EPiServer.DataAbstraction.Activities.Internal;
using EPiServer.Framework;
using EPiServer.Framework.Initialization;
using EPiServer.Logging;
using EPiServer.Security;
namespace EpiAuditLogging.Web.Features.AuditLogging
{
[InitializableModule]
[ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
public class AuditLogInitializationModule : IInitializableModule
{
private static readonly ILogger Logger = LogManager.GetLogger(typeof(AuditLogInitializationModule));
private IActivityRepository _activityRepository;
public void Initialize(InitializationEngine context)
{
// just DEMO CODE – separate catches just to log the details what really happened
// and return used in each catch just to be able to switch the order or if something is later added
// if there is an Exception we should not continue any execution because the dependencies are not working
try
{
// register the custom ContentSecurityActivity and the action types
RegisterContentSecurityActivity(context.Locate.Advanced.GetInstance<IActivityTypeRegistry>());
}
catch (Exception ex)
{
Logger.Error("Failed to register ContentSecurityActivity and action types.", ex);
return;
}
try
{
_activityRepository = context.Locate.Advanced.GetInstance<IActivityRepository>();
}
catch (Exception ex)
{
Logger.Error("Failed to get IActivityRepository service.", ex);
return;
}
try
{
var repo = context.Locate.Advanced.GetInstance<IContentSecurityRepository>();
repo.ContentSecuritySaved += ContentSecuritySaved;
}
catch (Exception ex)
{
Logger.Error("Failed to register content security saved handler.", ex);
return;
}
}
public void Uninitialize(InitializationEngine context)
{
try
{
var repo = context.Locate.Advanced.GetInstance<IContentSecurityRepository>();
repo.ContentSecuritySaved -= ContentSecuritySaved;
}
catch (Exception ex)
{
Logger.Error("Failed to uninitialize the content security saved handler.", ex);
}
}
private void ContentSecuritySaved(object sender, ContentSecurityEventArg e)
{
try
{
// what access rights changes were made, target can be user or group (including visitor groups if those are set to be usable to protect content)
var permissions = e.ContentSecurityDescriptor?.Entries?.Select(entry => $"{entry.EntityType}: {entry.Name} access level set to: {entry.Access}.");
string creator = e.ContentSecurityDescriptor.Creator; // this is always null/empty, why?
string userFromContext = PrincipalInfo.CurrentPrincipal.Identity.Name; // this is guranteed to return a valid principal
string msg = $"Access rights changed by '{userFromContext}' (creator value: '{creator}') to content id {e.ContentLink}, save type: {e.SecuritySaveType}. Following changes were made: {string.Join(" ", permissions)}";
// just log for reference in the demo
Logger.Information(msg);
// the logged data to activity log
// you could have multiple keys for example to format the data in the 'change log' view
Dictionary<string, string> activityData = new Dictionary<string, string>
{
{ "message", msg }
};
// ContentSecurityActionType uses same values as SecuritySaveType so just casting from e.SecuritySaveType
var activity = new ContentSecurityActivity((ContentSecurityActionType)e.SecuritySaveType, activityData);
var result = _activityRepository.SaveAsync(activity).GetAwaiter().GetResult();
Logger.Information($"New activity saved with id: {result}.");
}
catch (Exception ex)
{
Logger.Error("Failed to handle content security saved event.", ex);
}
}
private void RegisterContentSecurityActivity(IActivityTypeRegistry activityTypeRegistry)
{
if (activityTypeRegistry == null)
{
throw new ArgumentNullException(nameof(activityTypeRegistry));
}
// this is similiar code that the Episerver implementations use to register the activities
// NOTE! The enum value None, value zero is excluded from the list as the UI will never show that
// in the dropdown filter in 'change log' view, assumed to be filter that shows all
// so remember not to use the value zero in your type for anything that 'means' something
ActivityType activityType = new ActivityType(ContentSecurityActivity.ActivityTypeName,
from ContentSecurityActionType x in Enum.GetValues(typeof(ContentSecurityActionType))
where x != ContentSecurityActionType.None
select new ActionType((int)x, x.ToString()));
// the implementation calls AddOrUpdate so it is safe to always call it
// the backing type currently is ConcurrentDictionary not database
activityTypeRegistry.Register(activityType);
}
}
}

Localizing the activity ‘category’ and action types

The activity ‘category’ and ‘action’ filters in the ‘Change log’ view can be localized using the default XML based localization.


<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<languages>
<language name="English" id="en">
<changelog>
<activitytypes>
<contentsecurity>
<description>Content security</description>
<!– ContentSecurityActionType –>
<recursivereplace>Recursive replace</recursivereplace>
<recursivemodify>Recursive modify</recursivemodify>
<modify>Modify</modify>
<replace>Replace</replace>
<replacechildpermissions>Replace child permission</replacechildpermissions>
<mergechildpermissions>Merge child permission</mergechildpermissions>
</contentsecurity>
</activitytypes>
</changelog>
</language>
</languages>

Wrapping up

Now we can view the access rights changes and filter by action in Episerver admin view under the ‘Change log’.

activity-log-entries

Content Delivery API, EPiServer, Episerver 11, InitializationModule

Episerver Content Delivery API new feature and options

If you missed the Episerver update 296 release notes or the Episerver Features February 2020 announcement – then this post is for you, because you have missed some important information about Episerver Content Delivery API. So keep on reading..

Episerver Content Delivery API new feature: support for local blocks

Before version 2.9.0 of Content Delivery API if you had used Episerver block (local block) as a property on a content type, then that property was not serialized to the Content Delivery API response – but starting from Content Delivery API version 2.9.0 local blocks are now supported (here is the feature ticket).

A simple sample can be made with the Episerver Alloy MVC site. The sites start page has used a local block as the logo type property. After installing Content Delivery API version 2.9.0 or newer, when you request the start page from Content Delivery API you also get the information about the logo type. Here is a screenshot from WinMerge comparision for version 2.6.1 VS 2.10.0 response where we can see that the local block is now included in the response (and also you can see that some properties now include “expanded” property).

cdapi-new-feature-local-block-output
Local block included in response

Episerver Content Delievery API new options

Four new options where introduced in update 296 (copied from release notes):

  • ValidateTemplateForContentUrl: Enable/Disable the validation of content URL. For example, this option should be set to false if you want to get the URL of a content folder
  • FlattenPropertyModel: If this option is set to true, the response JSON will be flattened
  • IncludeNullValues: If this option is set to false, the JSON serializer will ignore all null values in the response
  • IncludeMasterLanguage: If this option is set to false, the response model of content will not contain the MasterLanguage property

All these new options are used in the Content Delivery API configuration module but at the time of this post writing that information is missing from the official Content Delivery API configuration documentation.

See the configuration used at the end of the page.

SetIncludeNullValues(false)

If we call in configuration to SetIncludeNullValues(false) and then compare the Content Delivery API output we can see that it has done exactly what is described, it has removed the null values:

cdapi-new-option-includenullvalues-output
SetIncludeNullValues(false) to remove null values from output

SetIncludeMasterLanguage(false)

Normally the Content Delivery API would have MasterLanguage property but when we set this to false that property is excluded from the output.

cdapi-new-option-includmasterlanguage-output
SetIncludeMasterLanguage(false) removes the masterLanguage property from the output

SetFlattenPropertyModel(true)

This option when set to true (default is false) has a big impact to the Content Delivery API output and I would say this is the option that the front-end developers have asked for, simple and “less verbose” model out-of-the-box (there is still information that most likely is not needed in front-end, but at least the model is now a bit simpler).

cdapi-new-option-flattenpropertymodel-output
SetFlattenPropertyModel(true), flattened model in response

SetValidateTemplateForContentUrl(false)

By default the Content Delivery API validates that there is a template for the content requested. So for example in the Alloy MVC sample site there are container page types which don’t have a template (ContainerPage tagged with IContainer) and this means that there is no public url to browse to with a browser. Alloy MVC for example has page “How to buy” which is a container and you cannot browse to that page with a browser. Now if we configure Content Delivery API with SetValidateTemplateForContentUrl(false) the template validation is skipped and url for the container page is generated to the response JSON.

cdapi-new-option-validatetemplateforcontenturl-output
SetValidateTemplateForContentUrl(false), on the right we see we get url for a container page

Summary

So here was a quick look at the new Content Delivery API feature and new options. I believe these were things that front-end developers have been waiting for (and backend developers too :D). These additions take the Episerver Content Delivery API to the next level in the out-of-the-box category 🙂

Here is the configuration which was used in the samples:


using System;
using EPiServer.Framework;
using EPiServer.Framework.Initialization;
using EPiServer.ServiceLocation;
namespace ContentDelivery.NewFeatures.Web.Business.Initialization
{
[InitializableModule]
[ModuleDependency(typeof(EPiServer.ContentApi.Cms.ContentApiCmsInitialization))]
public class ContentDeliveryApiInitialization : IConfigurableModule
{
public void ConfigureContainer(ServiceConfigurationContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
context.Services.Configure<EPiServer.ContentApi.Core.Configuration.ContentApiConfiguration>(config =>
{
// contentapiread is the default role for content delivery API (and it is by default set to the RequiredRole
// and it is also by default mapped to: WebAdmins, WebEditors and Administrators
// setting MinimumRoles to string.Empty allows anonymous calls
config.Default()
.SetMinimumRoles(string.Empty)
.SetMultiSiteFilteringEnabled(false)
.SetRequiredRole("contentapiread")
.SetSiteDefinitionApiEnabled(true)
.SetIncludeNullValues(false) // new option
.SetIncludeMasterLanguage(false) // new option
.SetFlattenPropertyModel(true) // new option
.SetValidateTemplateForContentUrl(false); // new option
});
}
public void Initialize(InitializationEngine context)
{
//Add initialization logic, this method is called once after CMS has been initialized
}
public void Uninitialize(InitializationEngine context)
{
//Add uninitialization logic
}
}
}

EPiServer

Episerver developer meetup Helsinki March 2020

We had an awesome Episerver developer meetup in Helsinki. This time the years first meetup was kindly hosted by Solita Oy. Topics were this time: Episerver ASP.NET Core, customizing the Episerver Content Delivery API and a customer case utilizing Episerver CMS and real time public transport tracking. Of course there was also developer friendly menu: pizza & cold beverages!

This was most likely one of the biggest if not the biggest Episerver developer meetup in Finland so far. There were 30+ developers from different Episerver partner companies.

dev-meetup-helsinki-march-2020
Picture by Petri Isola (@epipetri)

Customizing Episerver Content Delivery API

dmhm2020-vladimir-anaimanovich

Vladimir Anaimanovich from Solita had a presentation how they had customized in a client project the Episerver Content Delivery API because they felt it was too verbose for the use case. His presentation included information on how they utilized the Content Delivery API services in a custom Web API controller, which is a great idea how to use existing services instead of writing everything from scratch. Have a look at the “Customizing Episerver content delivery API” blog post by Vladimir.

Episerver ASP.NET Core

dmhm2020-johan-bjornfot

Johan Björnfot from Episerver presented us the ASP.NET Core support for Episerver and how it will split the architecture to use delivery and management sites. Management site is the place where content is managed and the goal in the future is that it could be totally managed by Episerver and they could updated it for customers (no need from partners to keep some NuGet packages updated). Content delivery “site” then is responsible to deliver the content and partners can develop the front-end with React, Vue, Angular or whatever they feel is hot and pop 😉 Also there are plans to make part of the platform open sourced and open for contributions.

There is a closed beta and you can apply for it on the Episerver ASP.NET Core beta signup page.

Customer case: Episerver CMS & real time public transport tracking with Azure services

dmhm2020-valdis-iljuconoks

Valdis Iljuconoks (EMVP and Microsoft MVP) from Geta gave us a really nice presentation of a customer case where Episerver CMS platform with Azure services is used to build a real time tracking system of public transport. It was really interesting to hear how the system started and how it has evolved over the years and how different problems (latency, milliseconds) and data garthering were solved.

See the blog posts by Valdis: part 1 and part 2 on the subject.

Be active in Episerver developer community

dmhm2020-antti-alasvuo
Picture by Petri Isola (@epipetri)

I said a few words on how every developer can easily contribute to the Episerver community

Participate on Episerver World developer community:

  • answer questions on the forums
  • upvote answers you feel are good ones and should get credit
  • read and rate blogs
  • write blogs
    • you can use the Episerver World platform or aggregate your posts to World from a blogging platform of your choice
    • share your experinces, good and bad
  • tag Episerver in your some posts 😉

Contribute to Episerver GitHub projects:

 

.Net, ASP.NET MVC, Episerver 11, EPiServer Add-On, InitializationModule

Broken Web API attribute routing

Have you encountered the situation with your Episerver solution that your attribute based Web API starts throwing “The object has not yet been initialized. Ensure that HttpConfiguration.EnsureInitialized() is called in the application’s startup code after all other initialization code.”? If Yes, then this post is for you.

The issue

You have an Episerver solution and you have Web API endpoints created with attribute routing. Then suddenly the Web API endpoints start throwing exception stating “The object has not yet been initialized. Ensure that HttpConfiguration.EnsureInitialized() is called in the application’s startup code after all other initialization code.”. Stop and think, you didn’t change your code but you have added an Episerver Add-On maybe or some other NuGet package?

Simple sample to demonstrate this is to install Episerver Alloy MCV sample site with Episerver Find or Episerver Search. Then add some sample Web API endpoint and test it that it works. Next install the EPiServer.Marketing.Testing package (version 2.5.0) from Episerver NuGet feed. Rebuild your solution, run it and then try to call your Web API endpoint – it will now throw exception with the previously mentioned message.

Please note that the EPiServer.Marketing.Testing version 2.5.0 is old version used just to demo the issue. Currently at least the latest version 2.5.11 doesn’t cause this issue anymore.

So if you Google for Web API, Episerver and the exception message you will most likely end-up to one of the Episerver World forum threads like this or this. Which workaround the issue by using an Episerver initialization module that has dependency to FrameworkInitialization module and this “magically” fixes the issue. But wouldn’t it be important to understand why it fixes the issue after all the configuration is only moved to an earlier stage in Episerver initialization.

What causes the issue?

So as per the ASP.NET documentation the Web API 2 should be configured in the Application_Start method calling the GlobalConfiguration.Configure method and do the configuration. Now if you have an Add-On for example that wants to make sure its Web API attribute routing is registered it calls that same method or uses that method to register some other ‘classic’ routes using the MapHttRoute method. Now if you have correctly your code in Application_Start to register attribute routing it is the second time the GlobalConfiguration.Configure method is called and this second call will cause the issue.

GlobalConfiguration.Configure can be called multiple times BUT there is logic to run some code only once which leads to the fact that MapHttpAttributeRoutes() must be called only once before EnsureInitialized() is called. See the StackOverflow post that has good explanation what the method is about.

So back in 2.5.0 version of EPiServer.Marketing.Testing there is code that calls GlobalConfiguration.Configure with a delegate which actually only calls MapHttpRoute on the HttpConfiguration object and doesn’t call MapHttpAttributeRoutes() and in your code when you call the GlobalConfiguration.Configure and try to call MapHttpAttributeRoutes (this doesn’t throw) the EnsureInitialized is already called and leads to the situation that your attribute based routings will not be registered and exception is thrown when you try to call you Web API endpoints.

As a side note, if you call MapHttpRoute inside your Configure implementation also those routes are broken (unless you comment out the call to MapHttpAttributeRoutes()).

How to fix the issue?

First you could try to comment the call to MapHttpAttributeRoutes() in you GlobalConfiguration.Configure implementation – this is a long shot, if the ‘offending’ code is also calling MapHttpAttributeRoutes() everything should start to work again.

If that didn’t work then you need to implement an Episerver initialization module that runs before the code that ‘breakes’ the Web API configuration. Most modules take dependecy to EPiServer.Web.InitializationModule so your module most likely need to run before that to make the Web API configuration work. I have blogged and listed about the public Episerver initialization modules so have a look at that list (also if you are interested about the initialization framework look at the post by Waldis). So based on my listing your module should take dependency to EPiServer.Framework.FrameworkInitialization or EPiServer.Framework.FrameworkAspNetInitialization or anything else that is executed before the EPiServer.Web.InitializationModule. The Episerver World forum thread has the accepted answer which shows the initialization module you need to create and you can modify the ModuleDependency as needed.

Sample initialization module to fix the Web API attribute routing issue:


using System.Web.Http;
using EPiServer.Framework;
using EPiServer.Framework.Initialization;
namespace AlloyFindWithWebApi.Business.Initialization
{
[InitializableModule]
[ModuleDependency(typeof(FrameworkAspNetInitialization))]
public class FixWebApiInitializationModule : IInitializableModule
{
public void Initialize(InitializationEngine context)
{
// register web api attributeroutes here
// this module is executed before EPiServer.Marketing.Testing initialization module because that module
// has dependency to EPiServer.Web.InitializationModule and this module uses FrameworkAspNetInitialization
// which is executed before the InitializationModule
// this is only needed in old marketing testing versions, issue has been fixed in version 2.5.11 at least
GlobalConfiguration.Configure(config =>
{
config.MapHttpAttributeRoutes();
});
}
public void Uninitialize(InitializationEngine context)
{
}
}
}

How to find the dependency in the offending module causing the issue you would need a .NET decompiler like ILSpy (or your some other favorite tool) and then browse the code to see what initialization module it has dependency to and to be able to precisly make your initialization module execute before the offending one. If you donät look at the offending code then you might need to try different initialization modules until you find an initizalition module that is executed before the offending modules initialization module dependency.

Closing words

If you are building an Add-On that uses Web API attribute routing and therefore need to call the MapHttpAttributeRoutes() – then I think there are two options:

  • call MapHttpAttributeRoutes() in you Add-ons initialization module but have an option to disable that with an AppSettings ‘key – value’ and document that information for your users
  • don’t call MapHttpAttributeRoutes() but document to installation instructions that MapHttpAttributeRoutes() must be called in Application_Start like described in .NET Web API 2 configuration for your Add-On to work properly

If you are using the ‘old’ convention based routing then you can in your Add-Ons initialization module get a reference to the HttpConfiguration using the GlobalConfiguration.Configuration property and use that instance to add your convention based routes like: httpConfig.Routes.MapHttpRoute(your-code-here).

Sample initialization module to register routes in an Add-On:


using System.Web.Http;
using EPiServer.Framework;
using EPiServer.Framework.Initialization;
namespace AlloyFindWithWebApi.Business.Initialization
{
[InitializableModule]
[ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
public class ApiHttpRoutesInitializationModule : IInitializableModule
{
public void Initialize(InitializationEngine context)
{
// get the HttpConfiguration and then add our "custom" routes to the Routes collection
var httpConfig = GlobalConfiguration.Configuration;
httpConfig.Routes.MapHttpRoute(
"CustomWebApiRouteTemplate",
routeTemplate: "custom-api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
// use the httpConfig instance to register other routes as you need
}
public void Uninitialize(InitializationEngine context)
{
}
}
}

Hopefully this post clarifys what is causing the issue and how it can be fixed. As a closing note,  I would say that never call GlobalConfiguration.Configure method in your Add-ons, leave that to the application as it is its responsibility.

EPiServer, TinyMCE

TinyMCE entity encoding

Just a quick post about few TinyMCE settings that you might need when you use Episerver content created with “XhtmlString” property in some other context than web browser or you might be doing some manipulation to the content using XML structure.

Default entity encoding is ‘named’

The default setting in TinyMCE entity_encoding is ‘named’ which means that characters like ‘ä’ gets encoded as ‘&auml;’ and the copyright ‘©’ gets encoded as ‘&copy;’. Below is a screenshot how these characters look like by default in the HTML source.

tinymce-named-entity-encoding

All is fine and works in browsers but what about if you need that content in an XML document? If we now wrap that inside some ‘root-node’ and try load it as XmlDocument we get “System.Xml.XmlException: Reference to undeclared entity ‘Auml’.”. This is because the named entities are not valid XML entities, there is only five valid named entities in XML.

Use entity encoding ‘numeric’

Change the TinyMCE entity encoding setting to ‘numeric’. This is done in Episerver using the ‘AddSetting(string, object)’ method, see the Episerver documentation how TinyMCE is configured. So we use AddSetting(“entity_encoding”, “numeric”) like the following sample code:


using EpiWithSearch.Models.Blocks;
using EpiWithSearch.Models.Pages;
using EPiServer.Cms.TinyMce.Core;
using EPiServer.Framework;
using EPiServer.Framework.Initialization;
using EPiServer.ServiceLocation;
namespace EpiWithSearch.Business.Initialization
{
[ModuleDependency(typeof(TinyMceInitialization))]
public class ExtendedTinyMceInitialization : IConfigurableModule
{
public void Initialize(InitializationEngine context)
{
}
public void Uninitialize(InitializationEngine context)
{
}
public void ConfigureContainer(ServiceConfigurationContext context)
{
context.Services.Configure<TinyMceConfiguration>(config =>
{
// Add content CSS to the default settings.
config.Default()
.ContentCss("/static/css/editor.css")
// global config to use numeric encoding, see last line if you only need
// it for one property on a type
.AddSetting("entity_encoding", "numeric");
// add settings to tinymce
config.Default()
.AddPlugin("code charmap")
.AppendToolbar("code charmap");
// OR use numeric encoding only for a specific types field
config.For<StandardPage>(t => t.SpecialField).AddSetting("entity_encoding", "numeric");
});
}
}
}

Now when we look at the editor source code we get the special characters using the numeric entities which are then valid in XML but also valid in HTML.

tinymce-numeric-entity-encoding

Now if we wrap that HTML inside a root node in XML we can load it as valid XML document and can process it and use it somewhere else.

Note, there is also the ‘encoding’ setting in TinyMCE and setting this to value ‘xml’ should produce XML encoded content, but couldn’t get this to show expected results – looking at the POSTS the content was always using HTML named entities even when checking from tinymce JS object that the setting existed and was set correctly.

 

Dependency injection, Episerver 11, Web API

Dependency injection with Episerver CMS and Web API

This post is about clarifying the configuration you need to do if you wish to use dependency injection in your Episerver project together with Web API.

Episerver CMS uses dependecy injection and it is documented in the CMS developer guide. The documentation covers how you setup the dependency injection for your ASP.NET MVC controllers in the section “Integration with MVC 5 dependency resolver” and it includes the code how to do it using the “IServiceLocator” implementation (Alloy MVC sample site uses also this “default” IServiceLocator implementation which you can always refer to too). IServiceLocator is the abstraction that Episerver uses to de-couple “our” code from the underlying StuctureMap that it currenly uses as the IoC/DI container implementaiton.

All id fine with the “default” implementation but what about if we need Web API and want to use dependency injection with it? Henrik Fransas has already blogged about this at the end of 2016 but back then the documentation was still using the reference implementation that had direct dependecy to StructureMap. That old code is still available if you look at the CMS 9 documentation about dependecy injection. So lets call my blog post a “2019 refresh to what Henrik Fransas already has blogged“.

Episerver CMS 11 and Web API dependecy injection

So if you are using Episerver CMS 11 with the currently suggested “ServiceLocatorDependencyResolver” or you are testing/demoing with Episerver MVC Alloy sample site and add Web API then what are your options to get DI to work with your Web API controllers?

  • Add a new dependency resolver implementation for Web API
  • Remove the exising one and replace it with a StuctureMapDependecyResolver implementation that works with both MVC and Web API

The default ServiceLocatorDependencyResolver can not be used with Web API because it doesn’t expose the underlying container so that we could request a “scope” from it, which we would need in the Web API.

Option: new dependency resolver just for the Web API

For this option we need a new dependency resolver and an implementation of IDependencyScope.

The sample implementation of IDependencyScope below requires NuGet packages:

  • StructureMap (used version 4.7.1)
  • Microsoft.AspNet.WebApi.Core (used version 5.2.7)


using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Http.Dependencies;
using StructureMap;
namespace EpiWithWebAPI.Web.Business.Dependencies.WebApi
{
public class StructureMapDependencyScope : IDependencyScope
{
// this is a StructureMap specific implementation
private readonly IContainer _container;
public StructureMapDependencyScope(IContainer container)
{
_container = container ?? throw new ArgumentNullException(nameof(container));
}
public object GetService(Type serviceType)
{
ThrowIfDisposed();
if (serviceType == null)
{
return null;
}
if (serviceType.IsAbstract || serviceType.IsInterface)
{
return _container.TryGetInstance(serviceType);
}
return _container.GetInstance(serviceType);
}
public IEnumerable<object> GetServices(Type serviceType)
{
ThrowIfDisposed();
return _container.GetAllInstances(serviceType).Cast<object>();
}
private void ThrowIfDisposed()
{
if (disposedValue)
{
throw new ObjectDisposedException(nameof(StructureMapDependencyScope));
}
}
#region IDisposable Support
private bool disposedValue = false; // To detect redundant calls
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
// TODO: dispose managed state (managed objects).
_container.Dispose();
}
// TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
// TODO: set large fields to null.
disposedValue = true;
}
}
// TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources.
// ~StructureMapScope()
// {
// // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
// Dispose(false);
// }
// This code added to correctly implement the disposable pattern.
public void Dispose()
{
// Do not change this code. Put cleanup code in Dispose(bool disposing) above.
Dispose(true);
// TODO: uncomment the following line if the finalizer is overridden above.
// see: https://docs.microsoft.com/en-us/dotnet/standard/garbage-collection/implementing-dispose
// "If the type has no finalizer, the call to GC.SuppressFinalize has no effect."
// GC.SuppressFinalize(this);
}
#endregion
}
}

Implementation sample for our Web API dependency resolver:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Http.Dependencies;
using StructureMap;
namespace EpiWithWebAPI.Web.Business.Dependencies.WebApi
{
public class StructureMapDependencyResolver : IDependencyResolver
{
private readonly IContainer _container;
public StructureMapDependencyResolver(IContainer container)
{
_container = container ?? throw new ArgumentNullException(nameof(container));
}
public IDependencyScope BeginScope()
{
ThrowIfDisposed();
return new StructureMapDependencyScope(_container.GetNestedContainer());
}
public object GetService(Type serviceType)
{
ThrowIfDisposed();
return _container.TryGetInstance(serviceType);
}
public IEnumerable<object> GetServices(Type serviceType)
{
ThrowIfDisposed();
return _container.GetAllInstances(serviceType).Cast<object>();
}
private void ThrowIfDisposed()
{
if(disposedValue)
{
throw new ObjectDisposedException(nameof(StructureMapDependencyResolver));
}
}
#region IDisposable Support
private bool disposedValue = false; // To detect redundant calls
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
// TODO: dispose managed state (managed objects).
_container.Dispose();
}
// TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
// TODO: set large fields to null.
disposedValue = true;
}
}
// TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources.
// ~StructureMapDependencyResolver()
// {
// // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
// Dispose(false);
// }
// This code added to correctly implement the disposable pattern.
public void Dispose()
{
// Do not change this code. Put cleanup code in Dispose(bool disposing) above.
Dispose(true);
// TODO: uncomment the following line if the finalizer is overridden above.
// see: https://docs.microsoft.com/en-us/dotnet/standard/garbage-collection/implementing-dispose
// "If the type has no finalizer, the call to GC.SuppressFinalize has no effect."
// GC.SuppressFinalize(this);
}
#endregion
}
}

And then in your Episerver initialization module you would have it registered like the following:


using System.Web.Mvc;
using EPiServer.Framework;
using EPiServer.Framework.Initialization;
using EPiServer.ServiceLocation;
namespace EpiWithWebAPI.Web.Business.Initialization
{
[InitializableModule]
public class DependencyResolverInitialization : IConfigurableModule
{
public void ConfigureContainer(ServiceConfigurationContext context)
{
// some additional custom registration can be done here
// web api dependency resolver
// full namepsaces used here on purpose to make it clear the dependecy resolver
// code is for Web API and not for MVC
// context.StructureMap() is an extension method in namespace: EPiServer.ServiceLocation
// where from we get the StructureMap IContainer
// The extension method is NuGet package: EPiServer.ServiceLocation.StructureMap version 2.x
// the used dependency resolver is in gist: https://gist.github.com/alasvant/f7c8bc121c7f886a02ab38d2dd45ee5f
// and the IDependencyScope is in gist: https://gist.github.com/alasvant/5f3e3e6b385a7e0236d066316155948c
System.Web.Http.GlobalConfiguration.Configuration.DependencyResolver = new Dependencies.WebApi.StructureMapDependencyResolver(context.StructureMap());
}
public void Initialize(InitializationEngine context)
{
DependencyResolver.SetResolver(new ServiceLocatorDependencyResolver(context.Locate.Advanced));
}
public void Uninitialize(InitializationEngine context)
{
}
public void Preload(string[] parameters)
{
}
}
}

Option: combined dependency resolver

Note! The following samples will use the same StructureMapDependencyScope.cs as used in the above code sample.

So following dependency resolver works both MVC controller and Web API controllers:


using System;
using System.Collections.Generic;
using System.Linq;
using StructureMap;
namespace EpiWithWebAPI.Web.Business.Dependencies.Combined
{
public class StructureMapDependencyResolver : System.Web.Mvc.IDependencyResolver, System.Web.Http.Dependencies.IDependencyResolver
{
private readonly IContainer _container;
public StructureMapDependencyResolver(IContainer container)
{
_container = container ?? throw new ArgumentNullException(nameof(container));
}
public object GetService(Type serviceType)
{
ThrowIfDisposed();
if (serviceType == null)
{
throw new ArgumentNullException(nameof(serviceType));
}
if (serviceType.IsInterface || serviceType.IsAbstract)
{
return GetInterfaceService(serviceType);
}
return GetConcreteService(serviceType);
}
private object GetConcreteService(Type serviceType)
{
try
{
// Can't use TryGetInstance here because it won’t create concrete types
return _container.GetInstance(serviceType);
}
catch (StructureMapException)
{
return null;
}
}
private object GetInterfaceService(Type serviceType)
{
return _container.TryGetInstance(serviceType);
}
public IEnumerable<object> GetServices(Type serviceType)
{
ThrowIfDisposed();
return _container.GetAllInstances(serviceType).Cast<object>();
}
#region System.Web.Http.Dependencies.IDependencyResolver
public System.Web.Http.Dependencies.IDependencyScope BeginScope()
{
ThrowIfDisposed();
return new WebApi.StructureMapDependencyScope(_container.GetNestedContainer());
}
#endregion
private void ThrowIfDisposed()
{
if (disposedValue)
{
throw new ObjectDisposedException(nameof(StructureMapDependencyResolver));
}
}
#region IDisposable Support
private bool disposedValue = false; // To detect redundant calls
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
// TODO: dispose managed state (managed objects).
_container.Dispose();
}
// TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
// TODO: set large fields to null.
disposedValue = true;
}
}
// TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources.
// ~StructureMapDependencyResolver()
// {
// // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
// Dispose(false);
// }
// This code added to correctly implement the disposable pattern.
public void Dispose()
{
// Do not change this code. Put cleanup code in Dispose(bool disposing) above.
Dispose(true);
// TODO: uncomment the following line if the finalizer is overridden above.
// see: https://docs.microsoft.com/en-us/dotnet/standard/garbage-collection/implementing-dispose
// "If the type has no finalizer, the call to GC.SuppressFinalize has no effect."
// GC.SuppressFinalize(this);
}
#endregion
}
}

And then our initialization module would look like the following:


using System.Web.Mvc;
using EPiServer.Framework;
using EPiServer.Framework.Initialization;
using EPiServer.ServiceLocation;
namespace EpiWithWebAPI.Web.Business.Initialization
{
[InitializableModule]
public class DependencyResolverInitialization : IConfigurableModule
{
public void ConfigureContainer(ServiceConfigurationContext context)
{
//Implementations for custom interfaces can be registered here.
// dependecy resolver impl in gist: https://gist.github.com/alasvant/c56c2886c6b85494db98cd2c4ce9379c
// context.StructureMap() is an extension method in namespace: EPiServer.ServiceLocation
// where from we get the StructureMap IContainer
// The extension method is in NuGet package: EPiServer.ServiceLocation.StructureMap version 2.x
var resolver = new Dependencies.Combined.StructureMapDependencyResolver(context.StructureMap());
DependencyResolver.SetResolver(resolver); // MVC
System.Web.Http.GlobalConfiguration.Configuration.DependencyResolver = resolver; // Web API
}
public void Initialize(InitializationEngine context) {}
public void Uninitialize(InitializationEngine context) {}
public void Preload(string[] parameters) {}
}
}

Using DI in Web API controller in Episerver project

After implementing one of the above options we can use DI in our Web API controller(s) like the following code sample shows (naturally first configure Web API):


using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;
using EPiServer;
using EPiServer.Core;
namespace EpiWithWebAPI.Web.WebApi
{
[RoutePrefix("routed-api/pagename")]
public class AttributeBasedController : ApiController
{
// NOTE!!!
// These are just simple demos and don't do any checks that real production code should do!
// NOTE!!!
private readonly IContentLoader _contentLoader;
public AttributeBasedController(IContentLoader contentLoader)
{
_contentLoader = contentLoader ?? throw new ArgumentNullException(nameof(contentLoader));
}
[HttpGet, Route("all")]
public IEnumerable<string> GetPageNames()
{
// simple sample to get pages under start page
var pages = _contentLoader.GetChildren<PageData>(ContentReference.StartPage);
return pages.Select(x => x.Name);
}
[HttpGet, Route("byid/{id:int}")]
public string GetPageById(int id)
{
if (id < ContentReference.StartPage.ID)
{
throw new ArgumentOutOfRangeException($"Page id cannot be less than {ContentReference.StartPage}.");
}
return _contentLoader.Get<PageData>(new ContentReference(id))?.Name;
}
}
}

p.s. I have a follow up post coming about the Web API configuration – sometimes the Web API configuration just doesn’t seem to work. For now if you run to issues with Web API configuration then it is most likely about the Episerver add-ons and their initialization modules and order of what module does what. Look at the forum post “Cant get web api to work in EPiServer” in Episerver world and Scotts answer.

EPiServer, Episerver 11, Episerver Find

Episerver Find, Track(), oops 404 when search result is clicked

This is a quick informative post about Episerver Find and tracking the search results link clicks. Your site users might get 404 not found error instead of the page or content they were trying to reach from the search results. So keep on reading to understand the background and solution for the issue.

Background for this post

It all begun a few months ago when we updated customers Episerver instance. At that time the latest EPiServer.Find.CMS package was 13.2.1. Smoke tests looked good and live we went. It wasn’t long that first reports came in that the site search was not working for anonymous users (we don’t use ASP.NET session on the site). Next week there was a fix to this issue, EPiServer.Find.CMS 13.2.2 was released (Aug 9, 2019). We quickly installed that version and all was looking great again.

But unfortunately we later discovered there were new issues with that release and this post is about the issue we unfortunately just now discovered.

EPiServer.Find.CMS 13.2.2 package issue

It fixed the issue FIND-5883 – Tracking does not work if sessions are disabled which we were affected by but it also included a new feature that was not mentioned anywhere.

When you use Track() in your searches and show the results to your users the Find tracking script in browser changes all the links to point ‘/find_v2/_click?xxxx’ where xxxx is a bunch of query string parameters. And when user clicks a link the tracking is done here and the users browser is then redirected to the actual content from this action.

So in the 13.2.2 package they implemented two additional checks: Ensure the redirect url is absolute url and VerifyCorrectRedirectLocation.

When user clicks the search result link there is query string parameter ‘_t_redirect’ which contains the url where the user is redirected to. So the first check checks that this parameter contains absolute url and if it doesn’t the controller will return 404 response. So as long your search results ‘hit.Document.Url’ is absolute url, everything just works.

The more problematic check that caused 404 responses to us is the VerifyCorrectRedirectLocation check. Basically what it does, is run the search again. It uses the ‘_t_q’ parameter which contains the query done by user and ‘_t_hit.pos’ parameter which contains the ‘search hit’ position in the search results done by user on your site. And here is the problem – how will it know what kind of query has been done, what has been filtered out and so on. The implementation uses the ‘hit position’ as the Take(hitpos+10) call and it also uses the redirect url in ‘.Include(pseudo-code: include items that match the redirect url)’. But this check can lead to a situation where the returned results don’t have the content where the redirect should happen – and then the click action will again return 404 not found.

How to reproduce with Alloy MVC site?

With the current Episerver Visual Studio extension version 11.6.0.421 when you create a new Alloy MVC site using Episerver Find you actually get this problematic version of EPiServer.Find.CMS 13.2.2. So I’ll proceed with this. Add your Episerver Find demo index or developer index configuration to web.config. Run the site and setup your admin account as you would normally do.

In edit mode create a new ‘Standard page’ under ‘About us’ and name the page ‘Blog’. On the SEO tab enter title ‘Blog’, enter keywords ‘blog’ and enter description ‘This is a fake blog page.’. Switch to Content tab and enter some text to the ‘Main body’ and in teaser enter ‘This is an awesome blog page.’. Publish the page.

Next step is to upload images on the site where the image name starts with ‘blog’. You will need more than 10 of these images. I uploaded images blog_kuva_A.png, blog_kuva_B.png, … blog_kuva_Q.png 😀 These are needed to throw off the VerifyCorrectRedirectLocation check.

find-package-issue-images

Next step is to navigate to Find -> Configure -> Boosting. Here you can change how the search results are boosted. Boost ‘Title’ to ‘Very high’ and all other options to ‘Very low’. Like in the following picture (don’t mind the ‘Best bets’ entry ‘Alloy Blog’ you can see in the image as the first result in the preview).

find-package-issue-boosting-settings

Next step is to browse the Alloy demo site and make a search for ‘blog’. You will get this kind of result page:

find-package-issue-link

And now when you click that link you will get 404 not found like the image below:

find-package-issue-404page

So why do we get the above 404 not found page? The VerifyCorrectRedirectLocation uses the results ‘hit position’ which in this case is 2 and adds 10 to it => 12 and then uses that in the search it makes to take 12 hits from the search and also uses the ‘redirect url’ to include all content matching that url. And then takes the first hit which url matches the redirect url value. So in theory that should get the “page” and some extra result and happily do the redirect. The problem is in the fact that we don’t include images in our search result and we have boosted search to use the ‘Title’ property (the underlying index item SearchTitle$$string like “SearchTitle$$string”: “blog_kuva_A.PNG”). So when the check does its search it included also all the images and as we have more than 10 images that will match to ‘blog’ search the check gets all those images and the include result for the content url is not in the results. That is why it returns 404 not found because it can’t check the redirect url (because the result it got contains mainly images).

This same “issue” is also in the EPiServer.Find.CMS version 13.2.3.

How to fix this issue?

Update to EPiServer.Find.CMS 13.2.4 or newer where this extra checking is removed from the code 🙂 I looked at the 13.2.4 release notes but none of the entries ring a bell to fix exactly this issue.

So most likely if you are still using 13.2.2 or 13.2.3 version of EPiServer.Find.UI you should update it (as would be the answer from support if you report issues and are not on the latest version).

EPiServer, Episerver Ascend

Summary from Episerver Ascend 2019 Miami

Episerver Ascend 2019 Miami was held in 27th-29th of October, in a “luxury” hotel Fontainebleau in Miami Beach Florida, USA. This post is more images than textual content and definitely doesn’t contain any code samples (other than the famous ‘PropertyLotteryAttribute’ from CodeMania presentation).

Ascend Miami 2019

ascend-screen

I arrived to the location in the evening of 26th of October and basically just hit the bed immediately upon arriving to the hotel as my time was over 2am versus the local time 8pm.

26th October, Partner day

Sunday was mainly about event registration, education services (Episerver CMS and Commerce certification exams) and Partner day.

ascend-registration
Episerver Ascend registration

 

ascend-miami-schedule
Ascend schedule

partner-day
Partner day

ascend-hall
Asecend hall – way to all the session locations

ascend-mojito
Mojito while on break

ascend-lizard
After Mojito I really saw a couple of lizards by the hotel pools 😀

27th October, Ascend 2019 really starts

Day started with breakfast and partner Expo. Episerver Chief Product Officer Justin Anovick opened the Ascend with first keynote and also invited all EMVPs on the stage.

Next on stage was Deane Barker and also little Oprah joke was introduced, look whats under your chair.. well everyone participating to the Ascend got a free copy of his latest book: Real World Content Modeling.

During the day there were a lot of interesting sessions and I had difficulties to pick the ones to attend to, all were so interesting ones. I attended for example to Tracking events in Insight by Episerver Jonas Bergqvist, Extending Episerver’s content delivery API by Alf Nilsson, The Future Might Be Distributed by Deane Barker, Code Mania session by Allan Thraen and Fredrik Haglund and few other sessions.

codemania-property-lottery-attr
Code Mania – PropertyLotteryAttribute 😀

In the evening there was the cocktail reception in the partner village sponsored by Valtech.

ascend-miami-2019-lights

28th October, Ascend day 2

Day started with breakfast & Expo. Followed by Welcome session by Episerver CMO Jessica Dannemann.

Day #2 keynote was by Amy Purdy. Her speech was a non-technical keynote, nothing to do with IT, BUT oh-boy what a survival story. After her keynote the crowd stood up and kept clapping their hands. Good that the room was dark, because otherwise you might have seen a teardrop in the corner of my eye 😉 I really encourage you to Google her story up and see the impossible she has done in snowboarding and even in the show called ‘dancing with the stars’ (see youtube for those episodes).

Some sessions that I took part were the Episerver Driven by AI: Autonomous content creation and auditing by Patrick Van Kleef and Brian Weeteling – proof of concept on creating content and auditing it just by using AI, Content considerations for developers by Jeroen Stemerdink – simple things every developer should think of when creating Episerver sites, Episerver Commerce Roadmap, deep dive to Episerver Personalization Algorithms, Developing on the Episerver cloud platform in 2020 and beyond.

And then there were the Episerver Web, Partner & EIC Awards 2019 – congratulations to all the winners!

Oh and there was this Final Night Party at LIV nightclub sponsored by Niteco.

ascend-end-party2
Butterfly?

ascend-end-party
Shiny dancer

EMVP Summit

Following the Episerver Ascend Miami there was the EMVP Summit in a top-secret location and top-secret agenda. Cannot tell You about the content but it was 2½ days intense set of important things about current and future Episerver plans. Personally I was most interested about the insights about the .Net core plans and it just might be that EMVPs got a pre-preview of the .Net core implementation (and we are expected to give feedback).

emvp-mansion
EMVP mansion

emvp-mansion-pool
EMVP mansion pool

emvp-mansion-boat
EMVP boat 😀

emvp-sessions
EMVP session(s)

emvp-dunkin-coffee
Re-fueling in between sessions

emvp-soccer
Different kind of soccer

emvp-swag-and-badge
EMVP swag 😀

emvp-reserved
EMVP VIP seating during Ascend Miami

 

*** updated 2019-12-07, thanks J-M, Amy Purdy Twitter link corrected (was pointing to Jessica Dannemann account).

Azure Websites, EPiServer, Episerver 11

Why You should not use SiteDefinition.Current.SiteUrl in Episerver site?

In this blog post I’ll show You why you shouldn’t use the SiteDefinition.Current.SiteUrl property to create absolute urls to your site content.

I’ve recently worked with two projects that both used SiteDefinition.Current.SiteUrl property to get the “host” part to create absolute urls to content/resources. Everything has worked just fine so far but moving to cloud and updating the Episerver licenses caused weird issue in the environments mainly in the customer test environment. All absolute urls pointed to production environment and not to test environment.

Episerver has documented the cloud licenses and how those are configured. Basically your “site definition” has the production environment url in both test and production environments and then you configure the primary host differently per environment. Key point is that the SiteDefinition.SiteUrl has the same value in all of your environments – it is used as “site identifier” for the license.

So lets say we have an Episerver (cloud) license (single license file) that is for one site and three instances. Two instances are consumed by production environment and one instance is left for the test environment. If you would now just configure production site with http://www.myawesome-site.com for the site url and then your test environment with site url test.myawesome-site.com when activating the license you would notice that you have TWO sites and would get a license warning about trying to run two sites with the license which is valid for one site.

Now we have the background for “Why You shouldn’t use SiteDefinition.Current.SiteUrl” to create absolute urls for your site content.

Use HostDefinition for absolute urls

So here is my sample Episerver website configuration:

sitedef-website-settings

 

The “site definition url” is www.awesomeepi.test which is my sample site and that url is for production environment. When the site is created the site definition url is added to the host names (I’ve also added the * wildcard host for this site, needed by scheduled jobs). The above screenshot is taken from my test environment where I have added test.awesomeepi.test host and added it as the primary host.

Please note that when you look at the SiteDefinition.SiteUrl documentation it says “Returns primary url” but this is not true, kind of, it returns the SiteDefinition site url value, NOT HostDefinition which is marked as primary.

Now I can create a simple extension method that will return me the primary host when I use it together with the SiteDefinition.Current.Hosts.


using System;
using System.Collections.Generic;
using System.Linq;
using EPiServer.Web;
namespace EpiSiteDef.Web.Business
{
public static class HostDefinitionExtensions
{
/// <summary>
/// Gets the (first) host definition marked as the primary host which is not wildcard host.
/// </summary>
/// <param name="hosts"></param>
/// <returns>first primary host or null if there is no primary host</returns>
/// <exception cref="System.ArgumentNullException"><paramref name="hosts"/> is null</exception>
public static HostDefinition GetPrimaryHostDefinition(this IList<HostDefinition> hosts)
{
if (hosts == null)
{
throw new ArgumentNullException(nameof(hosts));
}
return hosts.FirstOrDefault(h => h.Type == HostDefinitionType.Primary && !h.IsWildcardHost());
}
}
}

I created a sample page to print out the differences.

hostdef-primary-host

Now when the primary host is switched to ‘www.awesomeepi.test’ host in “production” we get correct absolute url host for that environment.

And here is the sample code for the view printing out the above values.


@using EPiServer.Web
@using EpiSiteDef.Web.Business
@model IPageViewModel<SiteDefinitionSample>
@{ Layout = "~/Views/Shared/Layouts/_LeftNavigation.cshtml"; }
@{
var currentSiteDef = SiteDefinition.Current;
string siteDefUrl = currentSiteDef.SiteUrl.ToString();
var hostDef = currentSiteDef.Hosts.GetPrimaryHostDefinition();
string hostUrl = hostDef != null ? hostDef.Url.ToString() : "no primary host";
string hostName = hostDef != null ? hostDef.Name : "no primary host";
string hostAuthority = hostDef != null ? hostDef.Authority.ToString() : "no primary host";
}
<p>@HttpContext.Current.Request.Url.ToString()<br /><br /></p>
<p><b>SiteDefinition.Current.SiteUrl:</b><br />@siteDefUrl</p>
<p><b>Host definition url:</b><br />@hostUrl <br />(Notice! The HostDefinition.Url is marked as Episerver internal API which means it can change without any notice)</p>
<p><b>Host definition name:</b><br />@hostName</p>
<p><b>Host definition authority:</b><br />@hostAuthority</p>

view raw

index.cshtml

hosted with ❤ by GitHub

So by using the HostDefinition marked as primary to create absolute urls gets you always the correct host for your environment (well with the assumtion that you have actually configured it :D). I myself would use the ‘HostDefinition.Url’ property even it is documented as ‘Unsupported INTERNAL API’ (Episervers way of saying not following semantic versioning) otherwise you need to build the “host url” from combination of the HostDefinition instance properties: Authority and UseSecureConnection.

Naturally this was just a simple sample but shows why you should use the HostDefinition instead of SiteDefinition.SiteUrl when creating absolute urls. Next it is up to you which kind of extension or helper methods you create for your Episerver solutions.