Experiments with Umbraco's IContentService

I haven't had very much experience with the Umbraco ContentService. I've only just started playing around with it so if I've missed something obvious please tell me in the comments.

A Confusing API

Reading through the documentation, it seems a shade basic considering the importance of the subject matter and really doesn't go out of it's way to explain what to do and what will happen.

When saving an item using the content service, the methods signature for the first overload available for setting a property value is as follows.

IContentBase.SetValue(string propertyAlias, object value);

According to the documentation you should be able to set different types as the second argument. At time of writing they give the following example:

// Given a `ContentService` object get Content by its Id, set a few values
// and saves the Content through the `ContentService`
var content = contentService.GetById(1234);
content.SetValue("bodyText", "This text will be added to by RTE field");
content.SetValue("date", DateTime.Now);
contentService.Save(content);

Note the DateTime.Now instance there. Unfortunately support for the different types seems extremely limited. Passing a float causes the following exception.

The best overloaded method match for 'Umbraco.Core.Models.ContentBase.SetPropertyValue(string, string)' has some invalid arguments.

Yowza! That really doesn't give a great developer experience.

Magic Strings

Not much in Umbraco when dealing with content is strong-typed. If I were to hazard a guess as to why, I would imagine it was because the code tooling to do some of the conversion functionality we can now was not available when the API's were designed. (Update Turns out it was just a design decision). I have, however, felt the distinct impression at times through conversation that strong-typing is not wanted in certain areas.

I'm doing my best to provide it to developers when dealing with the cached IPublishedContent methods by contributing to Ditto. Content services, however, are not part of the scope for that project so having spent so much time developing with Umbraco in a strong-typed manner, coming across them here was a bit of a shock.

Let's look at that method again.

IContentBase.SetValue(string propertyAlias, object value);

The first parameter accepts a string. Now I don't know about you but the most common use case I would imagine for the method would be uploading a form or importing content through another service.

That form or service would contain classes with properties and using the API in this format would require the developer to insert magic strings for every property or use reflection to loop through with the property info name manipulating them for each instance.

Not code I want to write...

Expression Trees to the Rescue!

I wanted a way that I could call the method in the following format and not have to write those pesky strings:

IContentBase.SetValue(() => Class.Property);

In order to do that I will need to create an extension method that uses an Expression Tree. This is done by creating a Lambda Expression to represent the property.

Once inside our method we check to see whether, a property is being passed, and if so, coerce the various property values to call the old method with sanitized values.

/// <summary>
/// Sets the value of an <see cref="IContentBase"/> property to the given value.
/// </summary>
/// <param name="contentBase">
/// The <see cref="IContentBase"/>.
/// </param>
/// <param name="propertyLambda">
/// The <see cref="Expression"/> designating the value to save.
/// </param>
/// <typeparam name="T">
/// The <see cref="Type"/> of the property value.
/// </typeparam>
/// <exception cref="ArgumentException">
/// Thrown if the given <see cref="Expression"/> is not in the correct form.
/// </exception>
public static void SetValue<T>(this IContentBase contentBase, Expression<Func<T>> propertyLambda)
{
    MemberExpression expression = propertyLambda.Body as MemberExpression;

    if (expression == null)
    {
        throw new ArgumentException("You must pass a lambda of the form: '() => Class.Property' or '() => object.Property'");
    }

    PropertyInfo property = expression.Member as PropertyInfo;
    if (property != null)
    {
        MemberExpression member = (MemberExpression)expression.Expression;
        ConstantExpression constant = (ConstantExpression)member.Expression;
        object fieldInfoValue = ((FieldInfo)member.Member).GetValue(constant.Value);
        object value = property.GetValue(fieldInfoValue, null);
        string name = property.Name.ToSafeAlias(true);
        contentBase.SetValue(name, value.ToString());
    }
}

It's basic and doesn't take into account type conversion to and from strings, nor existing supported types but it's certainly a step in the right direction and better than what was there before.

Obviously, also, there is a an element of assumption in that technique in that we cannot predict what the alias will be and can only go on the most likely name based on the property. Personally though I don't believe the alias should be human editable and should be abstracted away with greater convention. That allows greater predictability and better quality code.

I would like to see more API signatures in Umbraco like the example I have created and would be more than happy to help try to tighten up the various APIs for the next major version.

Let me know what you think.

comments powered by Disqus