This is a follow up on yesterdays post regarding autoinstalling NuGet packages into Sitecore.
On request, here is my prototype code on how I use WebActivator to automatically insert/update embedded items into a Sitecore installation. Please note that this is very much prototype code, and it’s really ugly too. But I hope you get the idea of how it works.
First, I’ve started with a (to be extended) class library with some helper methods that’ll handle operations regarding loading items into Sitecore. This class is packed into a separate NuGet package, so that I can reuse it in other Sitecore modules:
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Resources;
using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.Serialization;
using Sitecore.Diagnostics;
using Sitecore.SecurityModel;
namespace Stendahls.SitecoreModuleInstaller
{
    public abstract class SitecoreModuleAutoinstaller
    {
        protected readonly HashSet<string> ForceItems = new HashSet<string>();
        protected readonly HashSet<string> UpdateItems = new HashSet<string>();
        public virtual SortedDictionary<string, byte[]> LoadResources (Assembly assembly, string databaseName)
        {
            var itemResources = new SortedDictionary<string, byte[]>(StringComparer.CurrentCultureIgnoreCase);
            var resourceName = string.Format("{0}.{1}.resources", assembly.GetName().Name, databaseName);
            var stream = assembly.GetManifestResourceStream(resourceName);
            if (stream == null)
            {
                Log.Warn(string.Format("Could not load resource {0}", resourceName), this);
                return null;
            }
            using (var reader = new ResourceReader(stream))
            {
                var enumerator = reader.GetEnumerator();
                while (enumerator.MoveNext())
                {
                    var key = enumerator.Key as string;
                    var data = enumerator.Value as byte[];
                    if (key != null && data != null)
                        itemResources.Add(key, data);
                }
            }
            return itemResources;
        }
        public virtual bool StoreItemToDisk (Database db, string itemPath, byte[] data, bool overwrite)
        {
            var itemReference = new ItemReference(db.Name, itemPath);
            string path = PathUtils.GetFilePath(itemReference.ToString());
            if (!overwrite && File.Exists(path))
                return false;
            string directory = Path.GetDirectoryName(path);
            if (directory != null && !Directory.Exists(directory))
                Directory.CreateDirectory(directory);
            Log.Info(string.Format("Storing {0} to disk at {1} ({2} bytes)", itemPath, path, data.Length), this);
            File.WriteAllBytes(path, data);
            return true;
        }
        public virtual void RestoreItem (Database db, string itemPath, bool force)
        {
            var itemReference = new ItemReference(db.Name, itemPath);
            string path = PathUtils.GetFilePath(itemReference.ToString());
            var loadOptions = new LoadOptions(db) { ForceUpdate = force };
            Log.Info(string.Format("Restoring {0} to {1}", itemPath, db.Name), this);
            using (new SecurityDisabler())
            {
                Manager.LoadItem(path, loadOptions);
            }
        }
        public virtual void ProcessItem(Database db, string itemPath, byte[] data)
        {
            string key = string.Format("/{0}/{1}", db.Name, itemPath.TrimStart('/'));
            if (ForceItems.Contains(key))
            {
                StoreItemToDisk(db, itemPath, data, true);
                RestoreItem(db, itemPath, true);
            }
            if (UpdateItems.Contains(key))
            {
                StoreItemToDisk(db, itemPath, data, true);
                RestoreItem(db, itemPath, false);
            }
        }
        public virtual IEnumerable<string> DatabaseNames
        {
            get { 
                yield return "core";
                yield return "master";
            }
        }
        public abstract Assembly ResourceAssembly { get; }
        public virtual void ProcessInstall ()
        {
            Log.Info("Autoinstalling Sitecore modules", this);
            foreach (string database in DatabaseNames)
            {
                var db = Factory.GetDatabase(database);
                Assert.IsNotNull(db, "Cannot load database {0}", database);
                var resources = LoadResources(ResourceAssembly, db.Name);
                if (resources == null)
                    continue;
                foreach (var resource in resources)
                {
                    string path = resource.Key;
                    if (path.EndsWith(".item"))
                        path = path.Substring(0, path.Length - 5);
                    ProcessItem(db, path, resource.Value);
                }
            }
        }
    }
}
Now, having imported the class above into my module, I create a setup class that’ll do the install work. I use WebActivator to trigger the setup on PostApplicationStart. Below is a sample of such class that I have in one of my modules:
using System.Reflection;
using Stendahls.SitecoreModuleInstaller;
[assembly: WebActivator.PostApplicationStartMethod(typeof(Stendahls.Modules.Tools.Setup.ConfigureSitecore), "Configure")]
namespace Stendahls.Modules.Tools.Setup
{
    public class ConfigureSitecore : SitecoreModuleAutoinstaller
    {
        public static void Configure()
        {
            var configurator = new ConfigureSitecore();
            configurator.ProcessInstall();
        }
        public ConfigureSitecore()
        {
            ForceItems.Add("/core/sitecore/content/Applications/Content Editor/Gutters/Media In Use");
            ForceItems.Add("/core/sitecore/content/Applications/Content Editor/Gutters/Missing Local Version");
            ForceItems.Add("/core/sitecore/content/Applications/Content Editor/Gutters/Related Items Published");
            ForceItems.Add("/master/sitecore/system/Settings/Subitems Sorting/Pure Logical");
            ForceItems.Add("/master/sitecore/system/Settings/Subitems Sorting/Pure Display Name");
        }
        public override Assembly ResourceAssembly
        {
            get { return GetType().Assembly; }
        }
    }
}
In this example I treat those items as master, and any changes in a local Sitecore installation will be overwritten. I can of course add missing items, do updates or run any kind of custom code to update my installation as needed.
The above asumes that I have core.resources and master.resources embedded in my solution. I’ve created those with a Pre-build event like this:
Stendahls.SitecoreItemResourcePacker.exe -d master -i $(SolutionDir)\Tds.Master -r $(ProjectDir) Stendahls.SitecoreItemResourcePacker.exe -d core -i $(SolutionDir)\Tds.Core -r $(ProjectDir)
And here’s the prototype code for packer application:
using System;
using System.IO;
using System.Resources;
using CommandLine;
using CommandLine.Text;
namespace Stendahls.SitecoreItemResourcePackager
{
    internal class Program
    {
        private sealed class CommandLineArgs : CommandLineOptionsBase
        {
            private string _resourcePath;
            [Option("d", "database", Required = true, HelpText = "Database name")]
            public string Database { get; set; }
            [Option("i", "itempath", Required = true, HelpText = "Item base path")]
            public string ItemPath { get; set; }
            [Option("r", "resourcepath", Required = true, HelpText = "Resource file base path")]
            public string ResourcePath
            {
                get { return _resourcePath; }
                set
                {
                    if (value == null)
                    {
                        _resourcePath = null;
                        return;
                    }
                    string path = value.TrimEnd('\\');
                    if (path.EndsWith("\\sitecore", StringComparison.InvariantCultureIgnoreCase))
                        path = path.Substring(0, path.Length - "\\sitecore".Length);
                    _resourcePath = path;
                }
            }
            [Option("v", "verbose", HelpText = "Verbose output")]
            public bool Verbose { get; set; }
            [HelpOption]
            public string GetUsage ()
            {
                return HelpText.AutoBuild(this, current => HelpText.DefaultParsingErrorsHandler(this, current));
            }
        }
        static void Main(string[] args)
        {
            try
            {
                var options = new CommandLineArgs();
                var parser = new CommandLineParser(new CommandLineParserSettings(Console.Error));
                if (!parser.ParseArguments(args, options))
                {
                    Console.Error.WriteLine(options.GetUsage());
                    Environment.Exit(1);
                }
                string resourceFile = Path.Combine(options.ResourcePath, string.Format("{0}.resources", options.Database));
                string filesBasePath = Path.Combine(options.ItemPath, "sitecore");
                if (options.Verbose)
                {
                    Console.WriteLine("Creating resource file {0}", resourceFile);
                    Console.WriteLine("Scanning {0} for .item files...", filesBasePath);
                }
                int count = 0;
                using (var writer = new ResourceWriter(resourceFile))
                {
                    var files = Directory.GetFiles(filesBasePath, "*.item", SearchOption.AllDirectories);
                    foreach (string file in files)
                    {
                        string key = file.Substring(options.ItemPath.Length).Replace('\\', '/');
                        key = key.Substring(0, key.Length - 5); // Strip out .item
                        var data = File.ReadAllBytes(file);
                        writer.AddResource(key, data);
                        if (options.Verbose)
                        {
                            Console.WriteLine("Item {0} ({1} bytes) added to resource package with key {2}", file, data.Length, key);
                        }
                        count++;
                    }
                }
                Console.WriteLine("Successfully packed {0} items into {1}.resources", count, options.Database);
                Environment.Exit(0);
            }
            catch (Exception ex)
            {
                Console.Error.WriteLine(ex);
                Environment.Exit(1);
            }
        }
    }
}
			 
			
One of the most inspiring posts about Sitecore, NuGet and TDS. I’d been wondering how to achieve something like this for some time and you’ve totally nailed it. Kudos. Simon