Tag : code

Creating Custom Server Roles in Sitecore 9

Sitecore 9 brings about a new feature: Server Roles. This is actually one of the features I was most excited about.

What is a Server Role?

Sitecore 9 ships with 5 server roles: ContentManagement, ContentDelivery, Processing, Reporting, Standalone. You can read more about what each role is used for from their documentation page. Using these server roles, you can conditionally load sections of configuration based off the server role. Rob Ahnemann wrote a more in-depth blog post that you should check out if you want more information on what you can do with server roles.

What Role is Right for Me?

Standalone is typically used for local development. Emphasis on the word “typically.” Many dev servers are typically Standalone environments too. I may have a configuration that I want to be set on local environments, but not the dev server. What role would I use?

None of them!

You’re not limited to using just the 5 roles. You can also set more than one role at a time. If you take a peek at web.config:

<!-- SUPPORTED SERVER ROLES Specify the roles that you want this server to perform. A server can perform one or more roles. Enter the roles in a comma separated list. The supported roles are: ContentDelivery ContentManagement Processing Reporting Standalone Default value: Standalone -->
    <add key="role:define" value="Standalone" />

you’ll see that the role value is currently “Standalone”. This is actually a comma-delimited list of roles.

Proof of Concept Time

I tossed a random config file into the App_Config/Include folder:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:role="http://www.sitecore.net/xmlconfig/role/">
    <sitecore>
        <foo role:require="LocalDev">foobar</foo>
    </sitecore>
</configuration>

Notice I wrote “LocalDev” as the required role? I went to /sitecore/admin/showconfig.aspx and did a CTRL+F for “foobar”. 0 Results! What happens if I change the defined roles now?

<add key="role:define" value="Standalone,LocalDev" />

Save. Refresh. Search.
We have a winner!

<foo patch:source="dev.config">foobar</foo>

is now present in the ShowConfig.aspx

Hope this helps anybody looking to work some config magic on their solutions.

Still want to learn more? Check Kamruz Jaman’s blog post showing more advanced features and tricks for server roles.

Autowiring XConnect Models

I’ve been playing around with XConnect for the last few weeks and so far I like what I see.

While playing around with custom models, I noticed that model schemas need to be manually registered. While not actually a big deal, this is an additional step that can be forgotten. I prefer to eliminate these “forgettable” tasks. I sought to determine a way of automatically wiring in these models. I’m pretty handy with reflection and figured if I could tag these models with an attribute, I could figure something out.

The Standard Configuration

For starters, let’s take a look at the configuration to see how you would normally add a model:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
<sitecore>
    <xconnect>
    <runtime type="Sitecore.XConnect.Client.Configuration.RuntimeModelConfiguration,Sitecore.XConnect.Client.Configuration">
        <schemas hint="list:AddModelConfiguration">
            <!-- value of 'name' property must be unique -->
            <schema name="documentationmodel" type="Sitecore.XConnect.Client.Configuration.StaticModelConfiguration,Sitecore.XConnect.Client.Configuration" patch:after="schema[@name='collectionmodel']">
                <param desc="modeltype">Documentation.Model.CollectionModel, Documentation.Model</param>
            </schema>
        </schemas>
    </runtime>
    </xconnect>
</sitecore>
</configuration>

Let’s break this down a bit. RuntimeModelConfiguration is the runtime model the XConnect client uses. It has a public method AddModelConfiguration This method is called in the XML at runtime. This entire config block essentially translates to:

var runtime = new RuntimeModelConfiguration();
var documentationmodel = new StaticModelConfiguration();
runtime.AddModelConfiguration(modeltype: documentationmodel);

The Model

First, I need an attribute:

public class XModelAttribute : Attribute
{
}

That was easy.
Now I need a model:

[XModel]
public class TestModel
{
    public static XdbModel Model => BuildModel();
    public static string ModelName => typeof(TestModel).FullName;
    protected static XdbModelVersion ModelVersion => new XdbModelVersion(0, 1);

    private static XdbModel BuildModel()
    {
        var builder = new XdbModelBuilder(ModelName, ModelVersion);
        builder.ReferenceModel(CollectionModel.Model);
        return builder.BuildModel();
    }
}

The above is a sample model. I used the dev docs as an example, but trimmed it down a bit.

The Registration

I wanted to customize as little as possible to make upgrades a bit easier. I also wanted to ensure compatibility in case someone wanted to manually register models. I decided the cleanest way to do this would be to extend the RuntimeModelConfiguration

public class CustomRuntimeModelConfiguration : RuntimeModelConfiguration
{
    public CustomRuntimeModelConfiguration()
    {
        var assemblies = AppDomain.CurrentDomain.GetAssemblies();
        foreach (var assembly in assemblies)
        {
            var types = assembly.GetTypes().Where(t => t.IsDefined(typeof(XModelAttribute), true));
            foreach (var type in types)
            {
                AddModelConfiguration(new StaticModelConfiguration($"{type.FullName}, {type.Assembly}"));
            }
        }
    }
}

When this configuration is initialized, it scans the current assemblies for any classes decorated with the XModel attribute, and creates a new StaticModelConfiguration for it. It’s then added to the runtime model.

Remember this from before?

var runtime = new RuntimeModelConfiguration();
var documentationmodel = new StaticModelConfiguration();
runtime.AddModelConfiguration(modeltype: documentationmodel);

We’re doing the same thing now, but from the constructor.

All that’s left is to patch the config so that we’re using the Custom runtime config.

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
    <sitecore>
        <xconnect>
            <runtime type="Sitecore.XConnect.Client.Configuration.RuntimeModelConfiguration,Sitecore.XConnect.Client.Configuration">
                <patch:attribute name="type" value="Foundation.XConnect.CustomRuntimeModelConfiguration, Foundation" />
            </runtime>
        </xconnect>
    </sitecore>
</configuration>

The Conclusion

Our custom XConnect models will now autowire themselves! The actual customization is fairly light. Manual registration is still possible, as well.

Now you’re probably wondering, “isn’t tagging these classes with an attribute is almost as forgettable as adding an entry to a config file?”

Well… yeah, technically. At least it’s in the same file now, though!

Link Description Length Validator

Today’s adventure is about custom validators.

A feature I’m working on has a requirement that a link’s description must be no longer than 60 characters. I decided to look at the field validators that Sitecore comes with. The closest one I could find was the “Max Length 40” validator located at:

/sitecore/system/Settings/Validation Rules/Field Rules/Sample/Max Length 40

I setup a template and slapped that validator onto the general link field and, as I expected, it didn’t work right:

Inserting the link
Insert External Links

Displays a nice red validation error
Red Bar

To determine why less than 40 characters were throwing a validation errors, we need to look at the raw values. Behind the scenes, Sitecore is storing this in the field:

<link text="Some Link Text" linktype="external" url="http://google.com" anchor="" target="" />;

The 40 character validator is looking at the entire <link> tag and determining that it’s over 40 characters. This clearly won’t do.

Let’s take a peek under the hood. The “Max Length 40” validator indicates that its type is “Sitecore.Data.Validators.FieldValidators.MaxLengthFieldValidator,Sitecore.Kernel”

Max Length Type

Using ReSharper/dotPeek, I reflected into the MaxLengthFieldValidator and see that the evaluation logic is pretty simple

protected override ValidatorResult Evaluate()
{
    int num = MainUtil.GetInt(this.Parameters["maxlength"], 0);
    if (num <= 0)
        return ValidatorResult.Valid;
    string controlValidationValue = this.ControlValidationValue;
    if (string.IsNullOrEmpty(controlValidationValue) || controlValidationValue.Length <= num)
        return ValidatorResult.Valid;
    this.Text = this.GetText("The maximum length of the field \"{0}\" is {1} characters.", this.GetFieldDisplayName(), num.ToString());
    return this.GetFailedResult(ValidatorResult.Error);
}

It doesn’t care about what the field type is. It’s just looking at the raw value. I decided to extend this class and create my own custom validator.

The first thing I needed to do was determine whether or not the field being passed to Evaluate was a link field. Validators have a method called GetField() which will return the Field the validation is being called on. Using that, I’m able to add the following to the top of my Evaluate method:

var field = GetField();
if (field.TypeKey != "general link") return base.Evaluate();

Right now, I only care about general link fields. In the future, this can be extended/rewritten to incorporate other types of fields, but right now general link is the focus. Right now, to get the value of this field, I have to call the property ControlValidationValue. This returns the raw string value of the field. To save you the scroll, this is what that looks like:

<link text="Some Link Text" linktype="external" url="http://google.com" anchor="" target="" />

From here, I considered using some form of regex in order to parse out the text field. That could get messy. I thought to myself, “well, Sitecore’s rendering engine has to be able to translate this into an <a> tag, so it must have a way of parsing it!”. This lead me to finding the LinkField class. Using the LinkField class, I’m able to wrap the Field returned from GetField() and access all of the link properties:

var link = new LinkField(field);

There’s only one problem: that constructor will grab the link from the database. At the time this validator is called, the value has not been saved to the field. This means the link I get back doesn’t have the text populated. Thankfully, there’s another constructor that allows you to pass in the runtime value.

var link = new LinkField(field, ControlValidationValue);

From here, I’m able to read the parsed text from the link and run the validation on that. Unfortunately, I don’t have a way of passing this text to the base MaxLengthFieldValidator, so I resorted to a copy/paste of the base evaluator.

Putting it all together, we end up with:

using System;
using System.Runtime.Serialization;
using Sitecore;
using Sitecore.Data.Fields;
using Sitecore.Data.Validators;
using Sitecore.Data.Validators.FieldValidators;

namespace Website.Validators
{
    [Serializable]
    public class TextOrLinkMaxLengthFieldValidator : MaxLengthFieldValidator
    {
        public TextOrLinkMaxLengthFieldValidator()
        {
        }

        public TextOrLinkMaxLengthFieldValidator(SerializationInfo info, StreamingContext context)
            : base(info, context)
        {
        }

        protected override ValidatorResult Evaluate()
        {
            var field = GetField();
            if (field.TypeKey != "general link") return base.Evaluate();

            var link = new LinkField(field, ControlValidationValue);
            int num = MainUtil.GetInt(Parameters["maxlength"], 0);
            if (num <= 0) return ValidatorResult.Valid;

            string linkText = link.Text;
            if (string.IsNullOrEmpty(linkText) || linkText.Length <= num)
            {
                return ValidatorResult.Valid;
            }

            Text = GetText("The maximum length of the description text for field \"{0}\" is {1} characters.",
                GetFieldDisplayName(), num.ToString());

                
            return GetFailedResult(ValidatorResult.Error);
        }

        public override string Name
        {
            get {return "Max Length Text or Link"; }
        }
    }
}