Peter Löfman
Sep 25, 2016
  5956
(5 votes)

Property Validations

Inspiration for this blog post comes from the blog post on page validations and I wanted to spin a little further on this subject by creating a flexible validation atttribute you can place on the property to validate.

I wanted to use lamba expression but didn't realize that you cannot do that, so I then started trying other means of getting the flexibility I wanted to achieve befaore finding a wonderful little blog post on how to evaluate c# code. This makes it possible to write a string lambda expression and then compile this into to valid code you can use.

Anyway, enough history on the origins of this blog post let's dive into how to make property validation flexible and versatile.

Examples of the ValidateAttribute

My thinking was that you should be able to create multiple validation rules for each property with different levels of severity. And, you should also be able to use the current PageData instance in order to compare with other properties on the same PageData instance.

[Validate(
    "(str, pageData) => str != null || pageData.MetaDescription != null", 
    ValidationErrorSeverity.Error, 
    "MyTitle is empty.")]
[Validate(
    "str => str == null || str.Length > 10",
    ValidationErrorSeverity.Info, 
    "MyTitle is a bit short - should be at least 10 characters.")]
[Display(GroupName = SystemTabNames.Content, Order = 1000)]
public virtual string MyTitle { get; set; }

[Validate(
    "dateTime => dateTime >= new DateTime(2016, 10, 01)", 
    ValidationErrorSeverity.Error,
    "MyDateTime must be greater than or equal to 20016-10-01")]
[Display(GroupName = SystemTabNames.Content, Order = 2000)]
public virtual DateTime MyDateTime { get; set; }

There are three properties of the ValidateAttribute: Expression, Severity, and Message. 

The expression can be of two types:

Func<dynamic, bool>

or 

Func<dynamic, dynamic, bool>

The first simple takes the value of the property and validates against it and the latter also takes the PageData instance into account.

Next we need a class that implements IValidate<PageData> in order to look through all the properties on the current PageData instance and validate against any possible ValidateAttributes and that's really it. 

The two classes needed are the ones that follow.

ValidateAttribute

using System;
using EPiServer.Validation;

/// <summary>
/// Validates a string expression against a PageData instance.
/// <example>propertyValue => propertyValue != null</example>
/// <example>(propertyValue, pageData) => propertyValue != null || pageData.AnotherProperty != null</example>
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public class ValidateAttribute : Attribute
{
    /// <summary>
    /// The validation expression, of <see cref="Func{dynamic,Boolean}"/>.
    /// </summary>
    public string ValidationExpression { get; set; }

    /// <summary>
    /// The Severity if failing the validation.
    /// </summary>
    public ValidationErrorSeverity Severity { get; set; }

    /// <summary>
    /// The message to be displayed if the validation fails.
    /// </summary>
    public string Message { get; set; }

    /// <summary>
    /// The constructor for using the ValidateAttribute
    /// </summary>
    /// <param name="validationExpression">The validation expression, of <see cref="Func{dynamic,Boolean}"/>.</param>
    /// <param name="severity">The Severity if failing the validation.</param>
    /// <param name="message">The message to be displayed if the validation fails.</param>
    public ValidateAttribute(string validationExpression, ValidationErrorSeverity severity, string message)
    {
        this.ValidationExpression = validationExpression;
        this.Severity = severity;
        this.Message = message;
    }
}

ValidateContent

using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;

using EPiServer;
using EPiServer.Core;
using EPiServer.Validation;

using Microsoft.CSharp;

public class ValidateContent : IValidate<PageData>
{
    public IEnumerable<ValidationError> Validate(PageData instance)
    {
        // A dictionary with property name as key with multiple validate attributes as value.
        var propertyValidateAttributes =
            instance.GetOriginalType()
                .GetProperties()
                .Select(
                    propertyInfo =>
                    new
                        {
                            propertyInfo.Name,
                            ValidateAttributes = propertyInfo.GetCustomAttributes<ValidateAttribute>()
                        })
                .Where(i => i.ValidateAttributes != null)
                .ToDictionary(i => i.Name, i => i.ValidateAttributes);

        var validationErrors = new List<ValidationError>();

        foreach (var propertyValidationAttribute in propertyValidateAttributes)
        {
            foreach (var validateAttribute in propertyValidationAttribute.Value)
            {
                var isValid = IsValid(
                    validateAttribute.ValidationExpression,
                    instance.Property[propertyValidationAttribute.Key].Value,
                    validateAttribute.ValidationExpression.StartsWith("(") ? instance : null);

                if (isValid) continue;

                validationErrors.Add(
                    new ValidationError
                        {
                            ErrorMessage = validateAttribute.Message,
                            PropertyName = propertyValidationAttribute.Key,
                            Severity = validateAttribute.Severity,
                            ValidationType = ValidationErrorType.AttributeMatched
                        });
            }
        }

        return validationErrors;
    }

    //http://www.codeproject.com/Articles/11939/Evaluate-C-Code-Eval-Function
    public static bool IsValid(string expression, object propertyValue, PageData pageData = null)
    {
        var codeProvider = new CSharpCodeProvider();
        var compilerParameters = new CompilerParameters();

        compilerParameters.ReferencedAssemblies.Add("System.Core.dll");
        compilerParameters.ReferencedAssemblies.Add("Microsoft.CSharp.dll");
        compilerParameters.CompilerOptions = "/t:library";
        compilerParameters.GenerateInMemory = true;

        var sb = new StringBuilder();

        sb.Append("using System;\n");
        sb.Append("using System.Linq.Expressions;\n");
        sb.Append("namespace ExpressionEvaluate {\n");
        sb.Append("public class ExpressionEvaluate {\n");
        sb.Append("public static Func<dynamic" + (pageData != null ? ", dynamic" : "") + ", bool> Expression { get { return " + expression + "; } }\n");
        sb.Append("}\n");
        sb.Append("}\n");

        var compilerResults = codeProvider.CompileAssemblyFromSource(compilerParameters, sb.ToString());
        if (compilerResults.Errors.Count > 0)
        {
            throw new Exception(string.Join("; ", compilerResults.Errors));
        }

        var compiledAssembly = compilerResults.CompiledAssembly;
        var instance = compiledAssembly.CreateInstance("ExpressionEvaluate.ExpressionEvaluate");

        if (instance == null)
            throw new Exception("Could not create an instance of the expression evaluator.");

        var instanceType = instance.GetType();

        var propertyInfo = instanceType.GetProperty("Expression", BindingFlags.Static | BindingFlags.Public);

        var func = propertyInfo.GetGetMethod().Invoke(instance, null);

        return pageData != null
                    ? ((Func<dynamic, dynamic, bool>)func)(propertyValue, pageData)
                    : ((Func<dynamic, bool>)func)(propertyValue);
    }
}

User interaction

And if using the validation rules as depicted in the Examples of the ValidationAttribue section, the following outcome could appear.

Image validate-attribute-example.png

Sep 25, 2016

Comments

Sep 25, 2016 06:53 PM

Fun solution but I'm not sure I would dare to use it in production.

Implementing the interface seems safer to me :)

K Khan
K Khan Sep 25, 2016 10:15 PM

Appriciate effort, Please keep sharing.

Sep 26, 2016 07:26 AM

Code generation ftw:)

Well done to get it working all the way :) Keep em coming.

valdis
valdis Sep 27, 2016 01:32 PM

however, I do see some small issues here: no intellisense, no find usages, no compilation time error check, no refactoring or rename tooling support

Sep 27, 2016 03:33 PM

Memory consumption might be good to keep an eye on if you dynamically generate assemblies per request as well.

Sep 27, 2016 03:34 PM

I DO love code generation though! It's the next best thing after sliced bread...so really fun example.

Arve Systad
Arve Systad Sep 29, 2016 08:43 AM

Cool solution, but I do agree with Valdis here: The reason why many frameworks and utilities rely on lambda expressions is the delayed execution plus real static typing. Having them stored as a string leaves you very vulnerable to change, and will very likely cause you some nasty headaches in the future.

So instead of relying on properly formatted strings with no tool assistance, I think regular IValidate-implementations would help you get a bit farther. Or possibly implement a generic solution adding support for the built in validation attributes in System.ComponentModel.DataAnnotations.

Sep 29, 2016 03:03 PM

Even works way in the future: 200016.

Please login to comment.
Latest blogs
Opti ID overview

Opti ID allows you to log in once and switch between Optimizely products using Okta, Entra ID, or a local account. You can also manage all your use...

K Khan | Jul 26, 2024

Getting Started with Optimizely SaaS using Next.js Starter App - Extend a component - Part 3

This is the final part of our Optimizely SaaS CMS proof-of-concept (POC) blog series. In this post, we'll dive into extending a component within th...

Raghavendra Murthy | Jul 23, 2024 | Syndicated blog

Optimizely Graph – Faceting with Geta Categories

Overview As Optimizely Graph (and Content Cloud SaaS) makes its global debut, it is known that there are going to be some bugs and quirks. One of t...

Eric Markson | Jul 22, 2024 | Syndicated blog

Integration Bynder (DAM) with Optimizely

Bynder is a comprehensive digital asset management (DAM) platform that enables businesses to efficiently manage, store, organize, and share their...

Sanjay Kumar | Jul 22, 2024

Frontend Hosting for SaaS CMS Solutions

Introduction Now that CMS SaaS Core has gone into general availability, it is a good time to start discussing where to host the head. SaaS Core is...

Minesh Shah (Netcel) | Jul 20, 2024

Optimizely London Dev Meetup 11th July 2024

On 11th July 2024 in London Niteco and Netcel along with Optimizely ran the London Developer meetup. There was an great agenda of talks that we put...

Scott Reed | Jul 19, 2024