Umbraco: Programmatically Add/Update A Content Page

I decided to write this post to primarily act as a reminder to myself when dealing with programmatically creating content pages in Umbraco and expanding upon my previous post on setting a dropdownlist in code. I have been working on a piece of functionality where I needed to develop an import task to pull in content from a different CMS platform to Umbraco that encompassed the use of different field-types, such as:

  • Textbox
  • Dropdownlist
  • Media Picker
  • Content Picker

It might just be me, but I find it difficult to find solutions to Umbraco related problems I sometimes face. This could be due to results returned in search engines reference forum posts for older versions of Umbraco that are no longer compatible in the version I'm working in (version 8).

When storing data in the field types listed (above), I encountered issues when trying to store values in all field types except “Textbox”. The other fields either required some form of JSON structure or Udi to be parsed.

Code

My code contains three methods:

  1. SetPost - to create a new blog post, or update an existing blog post if one already exists.
  2. GetAuthorIdByName - uses Umbraco Examine Search Index to get back an Author document and return the Udi.
  3. GetUmbracoMedia - uses the internal Examine Search Index to return details of a file in a form that will be acceptable to store within a Media Picker content field.

The SetPost method consists of a combination of fields required by my Blog Post document, the primary ones being:

  • Blog Post Type (blogPostType) - Dropdownlist
  • Blog Post Author (blogPostAuthor) - Content Picker
  • Image (image) - Media Picker
  • Categories (blogPostCategories) - Tags
/// <summary>
/// Creates or updates an existing blog post.
/// </summary>
/// <param name="title"></param>
/// <param name="summary"></param>
/// <param name="postDate"></param>
/// <param name="type"></param>
/// <param name="imageUrl"></param>
/// <param name="body"></param>
/// <param name="categories"></param>
/// <param name="authorId"></param>
/// <returns></returns>
private static PublishResult SetPost(string title, 
                                    string summary, 
                                    DateTime postDate, 
                                    string type, 
                                    string imageUrl, 
                                    string body, 
                                    List<string> categories = null, 
                                    string authorId = "")
{
    PublishResult publishResult = null;
    IContentService contentService = Current.Services.ContentService;
    ISearcher searchIndex = ExamineUtility.GetIndex().GetSearcher();

    // Get blog post by it's page title.
    ISearchResult blogPostSearchItem = searchIndex.CreateQuery()
                                    .Field("pageTitle", title.TrimEnd())
                                    .And()
                                    .NodeTypeAlias("blogPost")
                                    .Execute(1)
                                    .FirstOrDefault();

    bool existingBlogPost = blogPostSearchItem != null;

    // Get the parent section where the new blog post will reside, in this case Blog Index.
    IContent blogIndex = contentService.GetPagedChildren(1099, 0, 1, out _).FirstOrDefault();

    if (blogIndex != null)
    {
        IContent blogPostContent;

        // If blog post doesn't already exist, then create a new node, otherwise retrieve existing node by ID to update.
        if (!existingBlogPost)
            blogPostContent = contentService.CreateAndSave(title.TrimEnd(), blogIndex.Id, "blogPost");
        else
            blogPostContent = contentService.GetById(int.Parse(blogPostSearchItem.Id));

        if (!string.IsNullOrEmpty(title))
            blogPostContent.SetValue("pageTitle", title.TrimEnd());

        if (!string.IsNullOrEmpty(summary))
            blogPostContent.SetValue("pageSummary", summary);

        if (!string.IsNullOrEmpty(body))
            blogPostContent.SetValue("body", body);
                
        if (postDate != DateTime.MinValue)
            blogPostContent.SetValue("blogPostDate", postDate);

        // Set Dropdownlist field.
        if (!string.IsNullOrEmpty(type))
            blogPostContent.SetValue("blogPostType", JsonConvert.SerializeObject(new[] { type }));

        // Set Content-picker field by parsing a "Udi". Reference to an Author page. 
        if (authorId != string.Empty)
            blogPostContent.SetValue("blogPostAuthor", authorId);

        // Set Media-picker field.
        if (imageUrl != string.Empty)
        {
            string umbracoMedia = GetUmbracoMedia(imageUrl);

            // A stringified JSON object is required to set a Media-picker field.
            if (umbracoMedia != string.Empty)
                blogPostContent.SetValue("image",  umbracoMedia);
        }    

        // Set tags.
        if (categories?.Count > 0)
            blogPostContent.AssignTags("blogPostCategories", categories);

        publishResult = contentService.SaveAndPublish(blogPostContent);
    }

    return publishResult;
}

/// <summary>
/// Gets UDI of an author by fullname.
/// </summary>
/// <param name="fullName"></param>
/// <returns></returns>
private static string GetAuthorIdByName(string fullName)
{
    if (!string.IsNullOrEmpty(fullName))
    {
        ISearcher searchIndex = ExamineUtility.GetIndex().GetSearcher();

        ISearchResult authorSearchItem = searchIndex.CreateQuery()
                                        .Field("nodeName", fullName)
                                        .And()
                                        .NodeTypeAlias("author")
                                        .Execute(1)
                                        .FirstOrDefault();

        if (authorSearchItem != null)
        {
            UmbracoHelper umbracoHelper = Umbraco.Web.Composing.Current.UmbracoHelper;
            return Udi.Create(Constants.UdiEntityType.Document, umbracoHelper.Content(authorSearchItem.Id).Key).ToString();
        }
    }

    return string.Empty;
}

/// <summary>
/// Gets the umbracoFile of a media item by filename.
/// </summary>
/// <param name="fileName"></param>
/// <returns></returns>
private static string GetUmbracoMedia(string fileName)
{
    if (!string.IsNullOrEmpty(fileName))
    {
        ISearcher searchIndex = ExamineUtility.GetIndex("InternalIndex").GetSearcher();

        ISearchResult imageSearchItem = searchIndex.CreateQuery()
                                        .Field("umbracoFileSrc", fileName)
                                        .Execute(1)
                                        .FirstOrDefault();

        if (imageSearchItem != null)
        {
            List<Dictionary<string, string>> imageData = new List<Dictionary<string, string>> {
                    new Dictionary<string, string>() {
                        { "key", Guid.NewGuid().ToString() },
                        { "mediaKey", imageSearchItem.AllValues["__Key"].FirstOrDefault().ToString() },
                        { "crops", null },
                        { "focalPoint", null }
                }
            };

            return JsonConvert.SerializeObject(imageData);
        }
    }

    return string.Empty;
}

Usage Example - Iterating Through A Dataset

In this example, I'm iterating through a dataset of posts and parsing the field value to each parameter of the SetPost method.

...
...
...
SqlDataReader reader = sqlCmd.ExecuteReader();

if (reader.HasRows)
{
    while (reader.Read())
    {
        SetPost(reader["BlogPostTitle"].ToString(),
                reader["BlogPostSummary"].ToString(),
                DateTime.Parse(reader["BlogPostDate"].ToString()),
                reader["BlogPostType"].ToString(),
                reader["BlogPostImage"].ToString(),
                reader["BlogPostBody"].ToString(),
                new List<string>
                {
                        "Category 1",
                        "Category 2",
                        "Category 3"
                },
                GetAuthorIdByName(reader["BlogAuthorName"].ToString()));
    }
}
...
...
...

Use of Umbraco Examine Search

One thing to notice is that when I’m retrieving the parent page to where the new page will reside or checking for a page or media file, Umbraco Examine Search Index is used. I find querying the search index is the most efficient way to return data without consistently hitting the database - ideal for when carrying out a repetitive task like an import.

In my code samples, I'm using a custom ExamineUtility class to retrieve the search index in a more condensed and tidy manner:

public class ExamineUtility
{
    /// <summary>
    /// Get Examine search index.
    /// </summary>
    /// <param name="defaultIndexName"></param>
    /// <returns></returns>
    public static IIndex GetIndex(string defaultIndexName = "ExternalIndex")
    {
        if (!ExamineManager.Instance.TryGetIndex(defaultIndexName, out IIndex index) || !(index is IUmbracoIndex))
            throw new Exception("Examine Search Index could not be found.");

        return index;
    }
}

Conclusion

Hopefully, the code I have demonstrated in this post will give a clearer idea on how to programmatically work with content pages using a combination of different field types. For further code samples on working with different field types, take a look at the "Built-in Umbraco Property Editors" documentation.

Umbraco: Setting A DropDownList Value Programmatically

After working on all things Hubspot over the last year whether that involved building and configuring site instances to developing API integrations, I have seemed to have missed out on CMS development-related projects. Most recently, I have been involved in an Umbraco site build where pages needed to be dynamically created via an external API.

Programmatically creating CMS pages is quite a straight-forward job as all one needs to do is:

  • Select the parent node your dynamically created page needs to reside
  • Check the parent exists
  • Create page and set field values
  • Publish

From past experience when passing values to page fields, it's been simple as passing a single value based on the field type. For example:

myNewPage.SetValue("pageTitle", "Hello World");
myNewPage.SetValue("bodyContent", "<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>");
myNewPage.SetValue("hasExpired", true);
myNewPage.SetValue("price", 9.99M);

Umbraco has something completely different in mind if you plan on setting the value of type "Dropdown". Simply sending a single value will not work even though it is accepted during runtime. You will need to send a value as a Json array:

string type = "Permanent";
myNewPage.SetValue("jobType", JsonConvert.SerializeObject(new[] { type }));

This is approach is required regardless of whether you've set the "Dropdown" field type in Umbraco as single or multiple choice.

Umbraco: Excessive Logging Causing Socket Timeout

Ever since a Ucommerce site built in Umbraco went live, the uptime monitoring system would send a notification every day or so to report the site has gone down.

There isn't any reason for this to happen as the site in question wasn't getting enough visitors a day to warrant such a service disruption. It was a new online presence. In addition, the hosting architecture can handle such an influx of visitors with ease should such a scenario arise.

Something random is happening at application level causing the site to timeout. Naturally, the first place to check when there are any application problems is the event log. No errors were being flagged. Just pages and pages of "Information" level entries, which are of little use. This didn't seem right and decided to check where the log files are stored within the '/App_Data/Logs' directory and just as well I did. I was greeted by many log files totalling over 3GB in size, with the current day log measuring at 587MB and increasing.

Appending new lines of text to an already large text file is bound to have an impact on site performance, especially when it's happening at such regular intervals. I needed to streamline what gets logged. I have no interest in "Information" log entries. To do this, the following setting in the serilog.config file found in the "/config" director needed to be updated:

<add key="serilog:write-to:File.restrictedToMinimumLevel" value="Warning" />

Previously, this setting is set to "Debug" which logs all site activity. Once this was updated, the socket timeout issue was resolved.