PnP 计时器作业框架

PnP 计时器作业框架是一套旨在简化针对 SharePoint 网站运行的后台进程创建的类。 计时器作业框架类似于本地完全信任代码计时器作业 (SPJobDefinition)。 计时器作业框架和完全信任代码计时器作业之间的主要差异是计时器作业框架仅使用客户端 API,因此可以(并且应该)在 SharePoint 外部运行。 计时器作业框架可以构建针对 SharePoint Online 运行的计时器作业。

计时器作业创建后,需要对其进行计划和执行。 两种最常用的选项包括:

  • 当 Microsoft Azure 在托管平台时,计时器作业可以作为 Azure WebJob 部署和运行。
  • 当 Windows 服务器在托管平台(例如本地 SharePoint)时,计时器作业可以在 Windows 计划程序中部署并运行。

有关介绍计时器作业的视频,请查看视频 PnP 计时器作业框架简介,其中介绍了计时器作业框架并演示简单的计时器作业示例。

计时器作业的简单示例

在此部分中,你将了解如何创建非常简单的计时器作业。 此示例的目的是为读者提供一个快速视图,稍后我们将提供计时器作业框架的更详细说明。

注意

有关包含十个计时器作业示例(从“Hello world”示例到实际内容到期作业)的更广泛 PnP 解决方案,请访问 Core.TimerJobs.Samples

以下步骤介绍如何创建一个简单的计时器作业。

步骤 1:创建一个控制台项目并引用 PnP 核心

通过执行以下任一项创建“控制台”类型的新项目并引用 PnP 核心库:

  • 向项目中添加 Office 365 开发人员模式和做法核心 NuGet 程序包。 有 v15 NuGet(本地)和 v16 (Office 365) 的 NuGet 程序包。 此为首选项。

  • 向项目添加现有 PnP 核心源项目。 这样一来,可以在调试时单步执行 PnP 核心代码。

    注意

    你需要负责根据添加到 PnP 的最新更改,不断更新此代码。

步骤 2:创建计时器作业类并添加计时器作业逻辑

  1. 向计时器作业添加一个名为 SimpleJob 的类。

  2. 具有继承 TimerJob 抽象基类的类。

  3. 在构造函数中为计时器作业指定名称 (base("SimpleJob")) 并连接 TimerJobRun 事件处理程序。

  4. 将计时器作业逻辑添加到 TimerJobRun 事件处理程序。

结果将如下所示:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.SharePoint.Client;
using OfficeDevPnP.Core.Framework.TimerJobs;

namespace Core.TimerJobs.Samples.SimpleJob
{
    public class SimpleJob: TimerJob
    {
        public SimpleJob() : base("SimpleJob")
        {
            TimerJobRun += SimpleJob_TimerJobRun;
        }

        void SimpleJob_TimerJobRun(object sender, TimerJobRunEventArgs e)
        {
            e.WebClientContext.Load(e.WebClientContext.Web, p => p.Title);
            e.WebClientContext.ExecuteQueryRetry();
            Console.WriteLine("Site {0} has title {1}", e.Url, e.WebClientContext.Web.Title);
        }
    }
}

步骤 3:更新 Program.cs 以使用计时器作业

上一步中创建的计时器作业仍需执行。 为此,请通过执行以下步骤更新 Program.cs:

  1. 实例化计时器作业类。

  2. 为计时器作业提供身份验证详细信息。 此示例使用用户名和密码对 SharePoint Online 进行身份验证。

  3. 为要访问的计时器作业程序添加一个或多个站点。 本示例在 URL 中使用通配符。 计时器作业将在与此通配符 URL 匹配的所有站点上运行。

  4. 通过调用 Run 方法来启动计时器作业。

static void Main(string[] args)
{
    // Instantiate the timer job class
    SimpleJob simpleJob = new SimpleJob();

    // The provided credentials need access to the site collections you want to use
    simpleJob.UseOffice365Authentication("user@tenant.onmicrosoft.com", "pwd");

    // Add one or more sites to operate on
    simpleJob.AddSite("https://<tenant>.sharepoint.com/sites/d*");

    // Run the job
    simpleJob.Run();
}

计时器作业部署选项

上一步演示一个简单的计时器作业。 下一步将部署计时器作业。

计时器作业是必须安排在托管平台上的 .exe 文件。 根据选定的托管平台,部署有所不同。 以下各节介绍两种最常用的托管平台选项:

  • 使用 Azure 作为托管平台。
  • 使用 Windows Server 作为托管平台。

使用 Azure WebJob 将计时器作业部署到 Azure

在部署计时器作业之前,确保此作业可以在无需用户交互的情况下运行。 本文中的示例将提示用户提供密码或客户端密码(有关详细信息,请参阅身份验证部分),它在测试时起作用,但在部署时不起作用。 所有现有示例均允许用户通过使用 app.config 文件来提供密码或 客户端密码:

  <appSettings>
    <add key="user" value="user@tenant.onmicrosoft.com"/>
    <add key="password" value="your password goes here!"/>
    <add key="domain" value="Contoso"/>
    <add key="clientid" value="a4cdf20c-3385-4664-8302-5eab57ee6f14"/>
    <add key="clientsecret" value="your clientsecret goes here!"/>
  </appSettings>

将这些更改添加到 app.config 文件之后,从 Visual Studio 运行计时器作业以确认它可在无需用户交互的情况下运行。

到 Azure 的实际部署基于 Azure WebJob。 若要部署此计时器作业示例,请执行以下步骤:

  1. 右键单击 Visual Studio 中的项目,并选择“发布为 Azure WebJob”

  2. 为计时器作业提供日程安排,然后选择“确定”

  3. 选择“Microsoft Azure 网站”作为发布目标。 将要求你登录到 Azure 并选择将托管计时器作业的 Azure 网站(如果需要,还可以创建一个新的网站)。

  4. 选择“发布”以将 WebJob 推送到 Azure。

  5. 计时器作业发布后,可以触发作业并从 Visual Studio 或 Azure 门户检查作业执行。

    Azure 门户 (旧版)

  6. 此外,通过选择该作业并选择“运行”,可以从新 Azure 门户运行计时器作业。 在文章在 Azure 应用服务中通过 WebJob 运行后台任务中,可以找到有关如何从新门户使用 WebJob 的更多详细信息。

    Azure 门户 (当前)

注意

有关如何部署 Azure WebJob 的详细指导,请参阅开始对 Office 365 网站使用 Azure WebJob(计时器作业)

使用 Windows 计划程序将计时器作业部署到 Windows Server

在部署到 Windows Server 时,该计时器作业必须在无需用户交互的情况下运行。

  1. 按照上一节使用 Azure WebJob 将计时器作业部署到 Azure中所述的步骤修改 app.config 文件。

  2. 将作业的发行版本复制到你希望在其上运行的服务器。

    重要

    复制相关的所有程序集、.exe 文件和 .config 文件以确保作业可以在未安装任何其他文件或程序的服务器上运行。

  3. 计划执行计时器作业。 建议使用内置的 Windows 任务计划程序。 若要使用 Windows 任务计划程序:

    1. 打开任务计划程序(“控制面板”>“任务计划程序”)。
    2. 选择“创建任务”并指定将执行该任务的名称和帐户。
    3. 选择“触发器”并添加一个新触发器。 指定计时器作业所需的计划。
    4. 选择“操作”并选择“启动程序”操作,选择计时器作业 .exe 文件,然后在文件夹中设置启动。
    5. 选择“确定”保存任务。

Windows 任务计划程序

深入了解计时器作业框架

此部分详细介绍了计时器作业框架功能及其工作原理。

结构

TimerJob 类是一个抽象基类,包含下列公共属性、方法和事件:

TimerJob 类结构

大多数属性和方法将在接下来的部分中详细介绍。 此处描述属性和方法的其余部分:

  • IsRunning 属性:获取一个值,该值指示是否执行计时器作业。 如果执行,值为 true;如果不执行,值为 false
  • Name 属性:获取计时器作业的名称。 在计时器作业构造函数中最初设置的名称。
  • SharePointVersion 属性:获取或设置 SharePoint 版本。 此属性基于加载的 Microsoft.SharePoint.Client.dll 版本自动设置,一般情况下不应更改。 但是,如果要在 v15(本地)部署中使用 v16 CSOM 库,可以更改此属性。
  • Version 属性:获取此计时器作业的版本。 版本是在计时器作业构造函数中最初设置的版本,或在未通过构造函数设置时默认为 1.0。

要准备运行计时器作业,必须首先对其进行配置:

  1. 提供身份验证设置。
  2. 提供范围,即站点列表。
  3. (可选)设置计时器作业属性

从执行的角度,运行计时器作业开始时会采取以下全部步骤:

  1. 解析站点:通配符站点 url(例如,https://tenant.sharepoint.com/sites/d*)解析为现有站点的实际列表。 如果需要扩展子站点,则解析的站点列表将展开并显示所有子站点。
  2. 创建工作批处理基于当前线程设置并为每个批处理创建一个线程。
  3. 线程执行工作批处理,然后为列表中的每个站点调用 TimerJobRun 事件。

有关每个步骤的更多详细信息,可在以下各部分中找到。

身份验证

可以使用计时器作业之前,计时器作业需要知道如何身份验证回 SharePoint。 该框架目前支持 AuthenticationType 枚举中的方法:Office365NetworkCredentialsAppOnly。 使用下面的方法还可将 AuthenticationType 属性自动设置为 Office365NetworkCredentialsAppOnly 的相应值。

下面的流程图显示要执行的步骤,然后对每种方法进行了详细说明。

身份验证步骤的流程图

用户凭据

若要指定针对 Office 365 运行的用户凭据,可以使用以下两个方法:

public void UseOffice365Authentication(string userUPN, string password)
public void UseOffice365Authentication(string credentialName)

第一种方法接受用户名和密码。 第二个方法允许你指定在 Windows 凭据管理器中存储的泛型凭据。 下面的屏幕截图展示了 bertonline 泛型凭据。 若要使用此凭据对计时器作业进行身份验证,请提供 bertonline 作为第二个方法的参数。

Windows 凭据管理器


有一些类似的方法可针对本地 SharePoint 运行:

public void UseNetworkCredentialsAuthentication(string samAccountName, string password, string domain)
public void UseNetworkCredentialsAuthentication(string credentialName)

仅限应用

仅限应用身份验证是首选方法,因为你可以授予租户范围内的权限。 对于用户凭据,用户帐户必须具有所需的权限。

注意

某些网站解析逻辑对仅限应用的身份验证不起作用。 下一节中可以找到详细信息。

要配置作业以进行仅限应用的身份验证,请使用下列方法之一:

public void UseAppOnlyAuthentication(string clientId, string clientSecret)
public void UseAzureADAppOnlyAuthentication(string clientId, string clientSecret)

同样的方法可以用于 Office 365 或 SharePoint 本地,这样计时器作业可以使用仅限应用的身份验证在环境之间轻松移动。

注意

如果使用仅限应用的身份验证,计时器作业逻辑就会在使用不可与 AuthenticationType.AppOnly 结合使用的 API 时失败。 典型的示例是搜索 API,写入分类存储,并使用用户配置文件 API。

要对其执行操作的网站

计时器作业运行时,它需要一个或多个站点对其进行运行。

向计时器作业添加网站

若要将站点添加到计时器作业,请使用下列方法集:

public void AddSite(string site)
public void ClearAddedSites()

若要添加网站,请指定完全限定的 URL(例如,https://tenant.sharepoint.com/sites/dev)或通配符 URL。

通配符 URL 是指以星号 (*) 结尾的 URL。 只允许有一个 *,且必须是 URL 的最后一个字符。 示例通配符 URL 为 https://tenant.sharepoint.com/sites/*,它将返回包含此网站的托管路径的所有网站集。 再例如,https://tenant.sharepoint.com/sites/dev* 将返回 URL 包含 dev 的所有网站集。

通常,由实例化该计时器作业对象的程序添加站点,但如果需要,计时器作业可以控制传递的站点列表。 通过为 UpdateAddedSites 虚拟方法添加方法重写可实现,如下面的示例所示:

public override List<string> UpdateAddedSites(List<string> addedSites)
{
    // Let's assume we're not happy with the provided list of sites, so first clear it
    addedSites.Clear();

    // Manually adding a new wildcard Url, without an added URL the timer job will do...nothing
    addedSites.Add("https://bertonline.sharepoint.com/sites/d*");

    // Return the updated list of sites
    return addedSites;
}

指定枚举凭据

添加通配符 URL 并为仅限应用设置身份验证之后,指定枚举凭据。 枚举凭据用于获取网站集列表,此列表用于站点匹配算法以返回真正的站点列表。

要获取网站集列表,Office 365 (v16) 和本地 (v15) 之间的计时器框架行为将不同:

  • Office 365Tenant.GetSiteProperties 方法用于读取“常规”网站集;搜索 API 用于读取 OneDrive for Business 网站集。
  • SharePoint 本地:搜索 API 用于读取所有网站集。

如果搜索 API 不可与用户上下文结合使用,计时器作业会回到指定的枚举凭据。

若要指定针对 Office 365 运行的用户凭据,可以使用以下两个方法:

public void SetEnumerationCredentials(string userUPN, string password)
public void SetEnumerationCredentials(string credentialName)

有一些类似的方法可针对本地 SharePoint 运行:

public void SetEnumerationCredentials(string samAccountName, string password, string domain)
public void SetEnumerationCredentials(string credentialName)

第一种方法只需接受用户名、密码和(可选)域(位于本地时)。 第二个方法可指定在 Windows 凭据管理器中存储的泛型凭据。 请参阅身份验证部分以了解有关凭据管理器的详细信息。

展开子网站

通常,你希望针对网站集的根网站和网站集的所有子网站执行计时器作业代码。 若要执行此操作,将 ExpandSubSites 属性设置为 true。 在站点解析步骤中,这将导致计时器作业展开子网站。

重写解析和/或扩展站点

在计时器框架解析通配符站点并选择性地展开子站点后,下一步是处理站点列表。 处理站点列表之前,可能需要修改站点列表。 例如,可能需要移除特定站点或向列表中添加更多站点。 这可以通过重写 ResolveAddedSites 虚拟方法来实现。 下面的示例演示如何重写 ResolveAddedSites 方法以从列表中删除一个站点。

public override List<string> ResolveAddedSites(List<string> addedSites)
{
    // Use default TimerJob base class site resolving
    addedSites = base.ResolveAddedSites(addedSites);

    //Delete the first one from the list...simple change. A real life case could be reading the site scope
    //from a SQL (Azure) DB to prevent the whole site resolving.
    addedSites.RemoveAt(0);

    //Return the updated list of resolved sites...this list will be processed by the timer job
    return addedSites;
}

TimerJobRun 事件

计时器作业框架将站点列表拆分为工作批处理。 每批站点将在其自己的线程上运行。 默认情况下,框架将创建五个批次,并创建五个线程来运行这五个批次。 请参阅线程部分以了解有关计时器作业线程选项的更多信息。

当某个线程处理一个批次时,计时器框架会触发 TimerJobRun 事件,并提供运行计时器作业所有必要的信息。 计时器作业作为事件运行,这样代码必须将事件处理程序连接到 TimerJobRun 事件:

public SimpleJob() : base("SimpleJob")
{
    TimerJobRun += SimpleJob_TimerJobRun;
}

void SimpleJob_TimerJobRun(object sender, TimerJobRunEventArgs e)
{
    // your timer job logic goes here
}

另一种方法是使用内联代理,如下所示:

public SimpleJob() : base("SimpleJob")
{
    // Inline delegate
    TimerJobRun += delegate(object sender, TimerJobRunEventArgs e)
    {
        // your timer job logic goes here
    };
}

TimerJobRun 事件触发时,会收到 TimerJobRunEventArgs 对象,此对象提供必要的信息来编写计时器作业逻辑。 以下属性和方法在此类中可用:

TimerJobRunEventArgs 类结构

几个属性和所有方法用于可选状态管理功能,这将在下一节中讨论。 但是以下属性将始终在每个事件中可用,无论使用的配置如何:

  • Url 属性:针对计时器作业获取或设置要运行的站点的 URL。 这可以是网站集的根网站,但在站点展开的情况下也可以是子网站。
  • ConfigurationData 属性:获取或设置其他计时器作业配置数据(可选)。 此配置数据将作为 TimerJobRunEventArgs 对象的一部分进行传递。
  • WebClientContext 属性:获取或设置当前 URL 的 ClientContext 对象。 对于 Url 属性中定义的站点,此属性是 ClientContext 对象。 这通常是你会在计时器作业代码中使用的 ClientContext 对象。
  • SiteClientContext 属性:获取或设置网站集根网站的 ClientContext 对象。 如果计时器作业需要相应访问权限,此属性会提供根网站的访问权限。 例如,该计时器作业可以使用 SiteClientContext 属性将页面布局添加到母版页样式库。
  • TenantClientContext 属性:获取或设置 ClientContext 对象以使用租户 API。 此属性提供通过租户管理网站 URL 构造的 ClientContext 对象。 若要在计时器作业 TimerJobRun 事件处理程序中使用租户 API,请使用此 TenantClientContext 属性创建一个新的 Tenant 对象。

所有 ClientContext 对象使用身份验证部分所述的身份验证信息。 如果你已经选择用户凭据,请确保所使用的帐户具有对指定站点操作所需的权限。 当使用“仅限应用”时,最好将租户范围权限设置为仅限应用主体。

状态管理

在编写定时器作业逻辑时,通常需要保持状态;例如,记录上次处理站点的时间或存储数据以支持你的计时器作业业务逻辑。 因此,计时器作业框架具有状态管理功能。

状态管理存储并作为处理站点的 Web 属性包中的 JSON 序列化字符串检索一组标准和自定义属性(名称 = 计时器作业名称 +“_Properties”)。 以下是 TimerJobRunEventArgs 对象的默认属性:

  • PreviousRun 属性:获取或设置上次运行的日期和时间。
  • PreviousRunSuccessful 属性:获取或设置一个值,该值指示上次运行是否成功。 请注意,计时器作业作者负责通过在计时器作业实施过程中设置 CurrentRunSuccessful 属性将作业记为运行成功。
  • PreviousRunVersion 属性:获取或设置上次运行的计时器作业版本。

在这些标准属性旁边,还可以选择通过将关键字 (keyword) 值对添加到 TimerJobRunEventArgs 对象的 Properties 集合来指定自己的属性。 有三种方法可帮助你简化此过程:

  • SetProperty 添加或更新属性。
  • GetProperty 返回属性的值。
  • DeleteProperty 从属性集合中移除属性。

下面的代码演示如何使用状态管理:

void SiteGovernanceJob_TimerJobRun(object o, TimerJobRunEventArgs e)
{
    try
    {
        string library = "";

        // Get the number of admins
        var admins = e.WebClientContext.Web.GetAdministrators();

        Log.Info("SiteGovernanceJob", "ThreadID = {2} | Site {0} has {1} administrators.", e.Url, admins.Count, Thread.CurrentThread.ManagedThreadId);

        // grab reference to list
        library = "SiteAssets";
        List list = e.WebClientContext.Web.GetListByUrl(library);

        if (!e.GetProperty("ScriptFileVersion").Equals("1.0", StringComparison.InvariantCultureIgnoreCase))
        {
            if (list == null)
            {
                // grab reference to list
                library = "Style%20Library";
                list = e.WebClientContext.Web.GetListByUrl(library);
            }

            if (list != null)
            {
                // upload js file to list
                list.RootFolder.UploadFile("sitegovernance.js", "sitegovernance.js", true);

                e.SetProperty("ScriptFileVersion", "1.0");
            }
        }

        if (admins.Count < 2)
        {
            // Oops, we need at least 2 site collection administrators
            e.WebClientContext.Site.AddJsLink(SiteGovernanceJobKey, BuildJavaScriptUrl(e.Url, library));
            Console.WriteLine("Site {0} marked as incompliant!", e.Url);
            e.SetProperty("SiteCompliant", "false");
        }
        else
        {
            // We're all good...let's remove the notification
            e.WebClientContext.Site.DeleteJsLink(SiteGovernanceJobKey);
            Console.WriteLine("Site {0} is compliant", e.Url);
            e.SetProperty("SiteCompliant", "true");
        }

        e.CurrentRunSuccessful = true;
        e.DeleteProperty("LastError");
    }
    catch(Exception ex)
    {
        e.CurrentRunSuccessful = false;
        e.SetProperty("LastError", ex.Message);
    }
}

状态存储为单个 JSON 序列化属性,即也可以由其他自定义使用。 例如,如果计时器作业编写状态项“SiteCompliant = false”,JavaScript 例程可能提示用户进行操作,因为计时器作业已确定该网站是违规的。

线程

默认情况下,该计时器作业框架使用线程来并行化工作。 线程用于子站点扩展(请求时),也可用于为每个站点运行实际计时器作业逻辑(TimerJobRun 事件)。 可以使用以下属性来控制线程实施:

  • UseThreading 属性:获取或设置一个值,该值指示是否将使用线程。 默认值为 true。 设置为 false 以通过使用主应用程序线程来执行所有操作。
  • MaximumThreads 属性:获取或设置要用于此计时器作业的线程数。 有效值为 2 至 100。 默认值为 5。 有大量线程并不一定比只有几个线程速度快。 应使用不同的线程数进行测试以获取最佳的数目。 已发现在大多数场景下使用默认值 5 个线程可显著提高性能。

限制

因为计时器作业使用线程且计时器作业操作通常是资源密集型操作,所以无法阻止运行计时器作业。 为了正确处理限制计时器作业框架且整个 PnP 核心使用 ExecuteQueryRetry 方法而不是默认的 ExecuteQuery 方法。

注意

请务必在计时器作业实施代码中使用 ExecuteQueryRetry

并发问题 - 处理同一个线程中的网站集的所有子网站

使用多个线程来处理子站点时,计时器作业可能具有并发问题。

例如:线程 A 从网站集 1 处理第一组子网站,线程 B 从网站集 1 处理其余子站点。 如果计时器作业处理子网站和根网站(使用 SiteClientContext 对象),可能会出现并发问题,因为线程 A 和线程 B 均处理根网站。

若要避免并发问题(不在单个线程中运行计时器作业),请在计时器作业中使用 GetAllSubSites 方法。

下面的代码演示如何在计时器作业中使用 GetAllSubSites 方法:

public class SiteCollectionScopedJob: TimerJob
{
    public SiteCollectionScopedJob() : base("SiteCollectionScopedJob")
    {
        // ExpandSites *must* be false as we'll deal with that at TimerJobEvent level
        ExpandSubSites = false;
        TimerJobRun += SiteCollectionScopedJob_TimerJobRun;
    }

    void SiteCollectionScopedJob_TimerJobRun(object sender, TimerJobRunEventArgs e)
    {
        // Get all the subsites in the site we're processing
        IEnumerable<string> expandedSites = GetAllSubSites(e.SiteClientContext.Site);

        // Manually iterate over the content
        foreach (string site in expandedSites)
        {
            // Clone the existing ClientContext for the sub web
            using (ClientContext ccWeb = e.SiteClientContext.Clone(site))
            {
                // Here's the timer job logic, but now a single site collection is handled in a single thread which
                // allows for further optimization or prevents race conditions
                ccWeb.Load(ccWeb.Web, s => s.Title);
                ccWeb.ExecuteQueryRetry();
                Console.WriteLine("Here: {0} - {1}", site, ccWeb.Web.Title);
            }
        }
    }
}

日志记录

计时器作业框架使用 PnP 核心日志记录组件,因为它是 PnP 核心库的组成部分。 若要激活内置的 PnP 核心记录,请使用适当的配置文件(app.config 或 web.config)对其进行配置。 以下示例显示了所需语法:

  <system.diagnostics>
    <trace autoflush="true" indentsize="4">
      <listeners>
        <add name="DebugListenter" type="System.Diagnostics.TextWriterTraceListener" initializeData="trace.log" />
        <!--<add name="consoleListener" type="System.Diagnostics.ConsoleTraceListener" />-->
      </listeners>
    </trace>
  </system.diagnostics>

使用上述配置文件时,计时器作业框架将使用 System.Diagnostics.TextWriterTraceListener 将日志写入与计时器作业 .exe 相同的文件夹中名为 trace.log 的文件中。 其他可用的跟踪侦听器,如:

强烈建议将与计时器作业框架相同的日志记录方法用于自定义计时器作业代码。 在计时器作业代码中,可以使用 PnP 核心 Log 类:

void SiteGovernanceJob_TimerJobRun(object o, TimerJobRunEventArgs e)
{
    try
    {
        string library = "";

        // Get the number of admins
        var admins = e.WebClientContext.Web.GetAdministrators();

        Log.Info("SiteGovernanceJob", "ThreadID = {2} | Site {0} has {1} administrators.", e.Url, admins.Count, Thread.CurrentThread.ManagedThreadId);

        // Additional timer job logic...

        e.CurrentRunSuccessful = true;
        e.DeleteProperty("LastError");
    }
    catch(Exception ex)
    {
        Log.Error("SiteGovernanceJob", "Error while processing site {0}. Error = {1}", e.Url, ex.Message);
        e.CurrentRunSuccessful = false;
        e.SetProperty("LastError", ex.Message);
    }
}

另请参阅