How to create a custom step for SnAdmin

From Sense/Net Wiki
Jump to: navigation, search
  • 100%
  • 6.3.1
  • Enterprise
  • Community
  • Planned


Sense/Net ECMS has a powerful package installer infrastructure that can be used by portal builders, developers and operators to patch and upgrade the core product or custom solution installations. In this article developers can learn how to create custom install steps for the packaging infrastructure, if none of the Built-in steps are sufficient for a particular goal.


The packaging infrastructure in Sense/Net allows developers to customize the package behavior by writing a custom Step class, built on the packaging API.

When you want to use a custom step class in a package you need to provide the codebehind for the SnAdmin tool. You can do that by placing your custom library in one of the following places:

  • the web bin folder of the website (for custom steps that will be executed multiple times in subsequent packages)
  • into the package itself: SnAdmin will look for custom steps in the PackageCustomization folder inside the package.

Custom step

We will create a custom step to execute a content query for discovering the count of a content set and terminate package execution or write to log if it is out of a specified range.

Create a step class

  • Create a new .Net project and add a new class that inherits the SenseNet.Packaging.Steps.Step base class.
  • Implements the abstract Execute method.
public class ContentQueryPrerequisit : Step
    public override void Execute(ExecutionContext context)

Before you choose your base class, please take a look at the Built-in steps, there are many useful basic steps there that you can inherit from (e.g. text editing or xml-related stuff, loops and conditional statements).

Optional: override the ElementName property if the class name is not appropriate. You will need to provide this name in the package manifest XML when using your custom step.

public override string ElementName { get { return "ContentCountCondition"; } }

Implement the properties that you want to expose to the package builder.

public string Query { get; set; }
public int CountMin { get; set; }
public int CountMax { get; set; }
public bool Terminate { get; set; }

We would like to provide the content query as an XML element text in the manifest so mark the Query property with the DefaultPropertyattribute:

public string Query { get; set; }

Implement the operation

public override void Execute(ExecutionContext context)
    // Execute the provided query and get the count.
    var count = ContentQuery.Query(this.Query).Count;
    // Echo the query.
    Logger.LogMessage("Query: " + Query);
    // Terminate if the step specifies and the count is out of the expected range
    if (Terminate && (count < CountMin || count > CountMax))
        throw new PackagePreconditionException(String.Format("Count is {0} but the expected range: {1} - {2}", count, CountMin, CountMax));
    // Log the count and expectations.
    Logger.LogMessage(String.Format("Count is {0}, expected range: {1} - {2}.", count, CountMin, CountMax));

Terminate package execution

Take a look at the package termination: if you throw a common exception in a custom step, the package will be terminated and the full exception (inner exceptions and stack trace) will be written to the console and to the log. But if the exception is PackagePreconditionException, only the message will be printed. For example:

================================================== #2/2 ContentCountCondition
Query: TypeIs:ContentType
Count is 0 but the expected range: 100 - 200
SnAdmin stopped with error.

Start the Content Repository

Executing a content query needs a running repository, so you need to make sure that you place a StartRepository step before your custom step into the manifest.

It is possible to check if the repository is running: the RepositoryStarted property of the execution context (see below) indicates that the StartRepository step has already been executed. The current execution context is passed to every step so insert this at the start of the Execute method:

// Check the running state of the repository.
if (!context.RepositoryStarted)
    // Terminate if it is not started.
    throw new PackagePreconditionException("REPOSITORY MUST BE STARTED. Use StartRepository before this step.");

Check Sense/Net version

It is possible to check for the current version and edition of the Sense/Net core pruduct. Here is an example for that:

var version = RepositoryVersionInfo.Instance.OfficialSenseNetVersion.Version;
var edition = RepositoryVersionInfo.Instance.OfficialSenseNetVersion.Edition;

You can use the values above (or a few other helper properties on the objects above) to determine if your custom step is able to execute in a certain environment.

Create and execute the package

Create a TestPackage1 directory under the your <WebSite>_admin directory. Create the PackageCustomization directory under TestPackage1 and copy your assembly here. Create a text file under the TestPackage1 with th name Manifest.xml and the following content:

<?xml version="1.0" encoding="utf-8"?>
<Package type="Application" level="Tool">
    <StartRepository />  
    <ContentCountCondition CountMin="100" CountMax="200" terminate="true">Type:ContentType .AUTOFILTERS:OFF .COUNTONLY</ContentCountCondition>

Start SnAdmin with this command line:

C:\......\<WebSite>_admin\bin\SnAdmin TestPackage1

After a few seconds you will see the results:

                              Sense/Net Admin v1.0
Start at 2014-04-29 08:21:58
Target:  C:\Dev10\SenseNet\Development\Budapest\Source\SenseNet\WebSite
Package: C:\Dev10\SenseNet\Development\Budapest\Source\SenseNet\WebSite_admin\TestPackage1
Loading package customizations:
Name:    TestTool
Type:    Product
Level:   Tool
Current version: 6.3.1
                              Executing phase 1/1
Executing steps
================================================== #1/2 StartRepository
Starting ... Ok.
  References: 29.
  Loaded before start: 54.
  Plugins: 0.
Index: C:\Dev10\SenseNet\Development\Budapest\Source\SenseNet\WebSite\App_Data\LuceneIndex\20140428102242.
Index was read only: false
Time: 00:00:05.3780843
================================================== #2/2 ContentCountCondition
Count is 157, expected range: 100 - 200.
Time: 00:00:00.0532635
All steps are executed.
Aggregated time: 00:00:05.4361648
Errors: 0
Stopping repository ... Ok.
SnAdmin has been successfully finished.
See log file: C:\Dev10\SenseNet\Development\Budapest\Source\SenseNet\WebSite_admin\log\TestPackage1_20140429-082158.log

Packaging API

There are two elements to aid you when writing custom steps:

  • Protected helper methods of the abstract Step class.
  • ExecutionContext object passed to step execution.

Step base

/// <summary>Represents one activity in the package execution sequence</summary>
public abstract class Step
    /*=========================================================== Instance part ===========================================================*/
    /// <summary>Returns the XML name of the step element in the manifest. Default: simple or fully qualified name of the class.</summary>
    public virtual string ElementName { get { return this.GetType().Name; } }
    /// <summary>Order number in the phase.</summary>
    public int StepId { get; private set; }
    /// <summary>The method that executes the activity. Called by packaging framework.</summary>
    public abstract void Execute(ExecutionContext context);
    /*=========================================================== Common tools ===========================================================*/
    /// <summary>Returns with a full path under the package if the path is relative.</summary>
    protected static string ResolvePackagePath(string path, ExecutionContext context);
    /// <summary>Returns with a full path under the target directory on the local server if the path is relative.</summary>
    protected static string ResolveTargetPath(string path, ExecutionContext context);
    /// <summary>Returns with a full paths under the target directories on the network servers if the path is relative.</summary>
    protected static string[] ResolveNetworkTargets(string path, ExecutionContext context);
    /// <summary>Returns with a full paths under the target directories on all servers if the path is relative.</summary>
    protected static string[] ResolveAllTargets(string path, ExecutionContext context);

Execution context

Every step runs in an execution context. It is an object that is passed to every step's Execute method. This object contains all information of the manifest and the current executing phase.

/// <summary>Contains package information for executing a step.</summary>
public class ExecutionContext
    /// <summary>Returns a named value that was memorized in the current phase.</summary>
    public object GetVariable(string name);
    /// <summary>Memorize a named value at the end of the current phase.</summary>
    public void SetVariable(string name, object value);
    /// <summary>Fully qualified path of the executing extracted package.</summary>
    public string PackagePath { get; }
    /// <summary>Fully qualified path of the executing extracted package.</summary>
    public string TargetPath { get; }
    /// <summary>UNC paths of the related network server web directories.</summary>
    public string[] NetworkTargets { get;}
    /// <summary>Parsed manifest.</summary>
    public Manifest Manifest { get; 
    /// <summary>Zero based index of the executing phase.</summary>
    public int CurrentPhase { get; }
    /// <summary>Phase count of the currently executed package.</summary>
    public int CountOfPhases { get; }
    /// <summary>Console out of the executing SnAdmin. Write here any information that you do not want to log.</summary>
    public TextWriter Console { get; }
    /// <summary>True if the StartRepository step has already executed.</summary>
    public bool RepositoryStarted { get; }

Variable support

It is possible to use variables to pass information from one step to another. In the Execute method you can store and recall any object associated with a name:

// setting
context.SetVariable("MeaningOfLife", 42);
// getting (in another step)
var ml = (int)context.GetVariable("MeaningOfLife");

The execution context stores these variables for the lifetime of the whole phase so these are available in all steps after the one you set them in. Starting a new phase resets variables.

Related links


There are no related external articles for this article.