Kirk Evans Blog

.NET From a Markup Perspective

Building a SharePoint App as a Timer Job

Building a SharePoint App as a Timer Job

Rate This
  • Comments 23

This post will show how to create an app as a timer job.

Background

One of the complicated parts of the app model today is trying to figure out how to do things that I used to do in full trust code using the app model.  Honestly, things look a little different, and this pattern will be useful to understand. 

As usual, if you aren’t interested in the narrative, skip down to the section, “Show Me the Code!”

I worked with a few customers who were concerned about some of their end users who kept using the new Change the Look feature of SharePoint 2013 to change the branding of their site.  They turned their site into something hideous like this.

image

This doesn’t conform to their corporate branding, so they wanted a way to go back and change this in an automated fashion.  Further, they want to do this on a daily basis to make sure the site is always changed back.  They didn’t want to remove permissions for the user to do this.  I’ll admit, there are other ways to do this, but it helps me to illustrate using a timer job to achieve the same thing.

Create the Console Application

In Visual Studio 2013, create a new Console Application.  Here’s one of my favorite parts… go to the NuGet Package Manager and search for “sharepoint app”.  You will see the App for SharePoint Web Toolkit. 

image

Add this NuGet package to your Console app and you will get the TokenHelper and SharePointContext code to work with SharePoint apps.

Creating the AppPrincipal

The first step to understand is how to create the app principal.  The app principal is an actual principal in SharePoint 2013 for the app that can be granted permissions.  When you create an app in Visual Studio 2013 and press F5, Visual Studio is nice enough to take care of registering the app principal for you behind the scenes.  It does this when using the Visual Studio template, but we’re not using their template here… we are using a Console Application.  We need to register the app principal first for our Console Application to be able to call SharePoint.

To register the app principal, we can use a page in SharePoint 2013 called “_layouts/AppRegNew.aspx”.

image

This page is used to create the client ID and client secret for the app.  Give it a name and click Generate, then click Create.

image

Note that I removed the actual string for the client secret for security purposes (hey, it’s a secret!)

The result is an app principal.

image

No, that is not the real client secret… I changed it for security purposes.   Just use the string that the page generates and don’t change it.

Giving the App Principal Permissions

Now we need to grant permissions to the app principal.  The easiest way to do this is to create a new provider-hosted app in SharePoint, give it the permissions that your app needs, then go to the appmanifest.xml and copy the AppPermissionRequests element. 

<AppPermissionRequests AllowAppOnlyPolicy="true">
    <AppPermissionRequest Scope="http://sharepoint/content/tenant" Right="Manage" />
</AppPermissionRequests>

The permissions that we will grant will be Tenant/Manage permission because our Console Application will go to multiple webs that are located in multiple site collections and change the branding.  To have permission to access multiple site collections, I need to request Tenant permission.  To change the branding for a site, I need Manage… hence Tenant/Manage.

You then go to a second page in SharePoint called “_layouts/AppInv.aspx”. 

image

Look up the app based on the Client ID that you just generated and click Lookup, it will find the app principal.  Then paste the AppPermissionRequests XML into the Permissions text box and click Create.

image

Once you click Create, the result is the Trust It dialog.

image

Click Trust It (of course you trust it). 

App Only Permission

I previously wrote a blog post, SharePoint 2013 App Only Policy Made Easy, that talks about the app only policy. If you aren’t familiar with app only, you need to go read that post.  Our timer job will not have an interactive user, so we need to use the app only policy.  The relevant code for this is the TokenHelper.GetAppOnlyAccessToken.

//Get the realm for the URL
string realm = TokenHelper.GetRealmFromTargetUrl(siteUri);

//Get the access token for the URL.  
//   Requires this app to be registered with the tenant
string accessToken = TokenHelper.GetAppOnlyAccessToken(
    TokenHelper.SharePointPrincipal, 
    siteUri.Authority, realm).AccessToken;

Once we have the access token, we can now create a ClientContext using that access token.

using(var clientContext = 
    TokenHelper.GetClientContextWithAccessToken(
        siteUri.ToString(),accessToken))

We now have a client context to use with the rest of our CSOM operation calls.

Update the App.config

The app.config will be used to store the URLs for various webs that need to have their branding updated via a timer job.  The app.config also stores the Client ID and Client Secret for our app.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="Sites"
             type="System.Configuration.NameValueSectionHandler"/>
  </configSections>
  <appSettings>
    <add key="ClientId" value="0c5579cd-c3c7-458c-91e4-8a557c33fc50"/>
    <add key="ClientSecret" value="925gRemovedForSecurityReasons="/>
  </appSettings>
  <startup>
    <supportedRuntime version="v4.0"
                      sku=".NETFramework,Version=v4.5" />
  </startup>
  <Sites>
    
    <add key="site2"
         value="https://kirke.sharepoint.com/sites/dev"/>
    
    <add key="site1"
         value="https://kirke.sharepoint.com/sites/developer"/>
    <add key="site3"
         value="https://kirke.sharepoint.com/sites/dev2"/>
         
  </Sites>
</configuration>

Our Console Application will read the Sites section, pull the URL for each site, and call CSOM on it to update the branding.

Show Me the Code!

using Microsoft.SharePoint.Client;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Configuration;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TimerJobAsAnApp
{
    class Program
    {
        /// <summary>
        /// To register the app:
        /// 1) Go to appregnew.aspx to create the client ID and client secret
        /// 2) Copy the client ID and client secret to app.config
        /// 3) Go to appinv.aspx to lookup by client ID and add permission XML below
        /// </summary>
        /// <param name="args"></param>         

        /*          
         <AppPermissionRequests AllowAppOnlyPolicy="true">
            <AppPermissionRequest Scope="http://sharepoint/content/tenant" Right="Manage" />
         </AppPermissionRequests>
        */
        static void Main(string[] args)
        {
            
            var config = (NameValueCollection)ConfigurationManager.GetSection("Sites");
            foreach (var key in config.Keys)
            {
                Uri siteUri = new Uri(config.GetValues(key as string)[0]);
                    
                //Get the realm for the URL
                string realm = TokenHelper.GetRealmFromTargetUrl(siteUri);

                //Get the access token for the URL.  
                //   Requires this app to be registered with the tenant
                string accessToken = TokenHelper.GetAppOnlyAccessToken(
                    TokenHelper.SharePointPrincipal, 
                    siteUri.Authority, realm).AccessToken;

                //Get client context with access token
                using(var clientContext = 
                    TokenHelper.GetClientContextWithAccessToken(
                        siteUri.ToString(),accessToken))
                {
                    //Poor man's timer
                    do
                    {
                        ApplyTheme(clientContext);
                        System.Threading.Thread.Sleep(10000);
                    }
                    while (true);
                }
            }            
        }

        /// <summary>
        /// Applies a red and black theme with a Georgia font to the Web
        /// </summary>
        /// <param name="clientContext"></param>
        private static void ApplyTheme(ClientContext clientContext)
        {
            Web currentWeb = clientContext.Web;
            clientContext.Load(currentWeb);
            clientContext.ExecuteQuery();

            //Apply RED theme with Georgia font
            currentWeb.ApplyTheme(
                URLCombine(
                    currentWeb.ServerRelativeUrl, 
                    "/_catalogs/theme/15/palette022.spcolor"),
                URLCombine(
                    currentWeb.ServerRelativeUrl, 
                    "/_catalogs/theme/15/fontscheme002.spfont"),
                null, false);
            clientContext.ExecuteQuery();
        }
        private static string URLCombine(string baseUrl, string relativeUrl)
        {
            if (baseUrl.Length == 0)
                return relativeUrl;
            if (relativeUrl.Length == 0)
                return baseUrl;
            return string.Format("{0}/{1}", 
                baseUrl.TrimEnd(new char[] { '/', '\\' }), 
                relativeUrl.TrimStart(new char[] { '/', '\\' }));
        }
    }
}

The Result

The sites in the app.config used to have that hideous theme.  Once the code runs, the sites in the app.config will have the colors of my favorite college football team (Go Georgia Bulldogs!), even using the “Georgia” font Smile

image

Of course, you can use whatever logic you want, the logic we use here is setting branding based on pre-configured URLs for the sites.  You can use whatever you want to schedule the timer job.  I used a poor man’s timer job, using a While loop with Thread.Sleep, but you could use the Windows Scheduler, a Cron job, or event Azure Web Jobs.

For More Information

SharePoint 2013 App Only Policy Made Easy

  • Thank you! Very interesting for me technical approach to combine the app model and console application.

  • Thanks for the info. I have a query, Can I develop this scenario with Visual studio 2012?

  • @Vardhini - absolutely.  Use the NuGet package manager to add the App for SharePoint Web Toolkit for the Console application and that's all you need.

  • Thank you very much for reply. I tried the sample code with Visual Studio 2012. I am using my Dev cloud server for this. while running the project, I get the following error: is this something related to my authentication? Can you pls guide me in this ?

    System.Net.WebException was unhandled

     HResult=-2146233079

     Message=The remote server returned an error: (404) Not Found. - The requested namespace does not exist.

     Source=MyFirstApp

     StackTrace:

          at MyFirstApp.TokenHelper.GetAppOnlyAccessToken(String targetPrincipalName, String targetHost, String targetRealm) in c:\vardhini\MyFirstApp\MyFirstApp\TokenHelper.cs:line 314

          at MyFirstApp.Program.Main(String[] args) in c:\vardhini\MyFirstApp\MyFirstApp\Program.cs:line 27

          at System.AppDomain._nExecuteAssembly(RuntimeAssembly assembly, String[] args)

          at System.AppDomain.nExecuteAssembly(RuntimeAssembly assembly, String[] args)

          at System.Runtime.Hosting.ManifestRunner.Run(Boolean checkAptModel)

          at System.Runtime.Hosting.ManifestRunner.ExecuteAsAssembly()

          at System.Runtime.Hosting.ApplicationActivator.CreateInstance(ActivationContext activationContext, String[] activationCustomData)

          at System.Runtime.Hosting.ApplicationActivator.CreateInstance(ActivationContext activationContext)

          at System.Activator.CreateInstance(ActivationContext activationContext)

          at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssemblyDebugInZone()

          at System.Threading.ThreadHelper.ThreadStart_Context(Object state)

          at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)

          at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)

          at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)

          at System.Threading.ThreadHelper.ThreadStart()

     InnerException: System.Net.WebException

          HResult=-2146233079

          Message=The remote server returned an error: (404) Not Found.

          Source=System

          StackTrace:

               at System.Net.WebClient.DownloadDataInternal(Uri address, WebRequest& request)

               at System.Net.WebClient.DownloadData(Uri address)

               at System.Net.WebClient.DownloadData(String address)

               at MyFirstApp.TokenHelper.AcsMetadataParser.GetMetadataDocument(String realm) in c:\vardhini\MyFirstApp\MyFirstApp\TokenHelper.cs:line 892

               at MyFirstApp.TokenHelper.AcsMetadataParser.GetStsUrl(String realm) in c:\vardhini\MyFirstApp\MyFirstApp\TokenHelper.cs:line 909

               at MyFirstApp.TokenHelper.GetAppOnlyAccessToken(String targetPrincipalName, String targetHost, String targetRealm) in c:\vardhini\MyFirstApp\MyFirstApp\TokenHelper.cs:line 306

          InnerException:

  • Vardhini - If I had to guess, it sounds like you did not go to AppRegNew.aspx to create the client ID and client secret yet in your SharePoint site and then replace the values in the sample code with your own client ID and client secret.  Also double-check that you are using the URL for your SharePoint Online site and not "kirke.sharepoint.com".  

  • Thanks for the guidance .. I verified all my steps that you did listed. But still I am getting the same error. I am not able to execute the code

  • Varhhini - what values are you using for the app.config?    Make sure to replace these values with the path to your web:

    <Sites>    

       <add key="site2"

            value="kirke.sharepoint.com/.../>    

       <add key="site1"

            value="kirke.sharepoint.com/.../>

       <add key="site3"

            value="kirke.sharepoint.com/.../>        

     </Sites>

  • I replaced this at the very first time I created the application. I ensured that on first hand. Again I verified when you asked me to. Still the problem persist. Sorry for bugging you  many times :(

  • Varhhini - I wish that I could provide better guidance, but the blog post above works and I've used it with many customers.  There's something we've not been able to discover about your environment, but I am not able to determine it based on our conversation.  Perhaps post to stackoverflow.com/.../sharepoint as the product team, MVPs, and many community members monitor that regularly... you have a much higher chance of getting further assistance there from a broader set of people.  I apologize that I cannot solve your issue, I'm not sure what your problem might be.  

  • Thanks for your suggestion. I will post my query as you said

  • Kirk, for creating a clientcontext we normally need the sitecollection url, right? however one scenario I have is that I have 20,000 site collections in one web application, with server side you can normally iterate over all sites in a web application, can this be done with a console app/csom?

  • Luis - There is an example in our OfficeAMS project that shows an approach to iterating site collections.  http://officeams.codeplex.com/

  • Thanks Kirk, I wanted to build your example but based on a different scenario, I want to run a timer job that searches for new files and then sends an email, I have followed all your steps but I get an unauthorized exception,

    I have asked the question here:

    stackoverflow.com/.../rest-search-api-the-remote-server-returned-an-error-401-unauthorized

    I hope you or somebody from your team might take a look at it.

    thank you

  • What a breeze setting up my first app! Thanks Boys! Kirk & Richard

  • Hi Kirk,

    It was good to meet you are Techfest! I was using an Azure webjobs to run an "unattended" sharepoint app like this... but I was using SharePoinOnlineCredentials with ClientContext.

    Obviously I do not want to deal with raw passwords, so I'll try to implement this.

    I'd like to understand more about what exactly TokenHelper is doing in this scenario. For example: to get the Realm I notice that TokenHelper sends a request to /_vti_bin/client.svc with a header "Authorization: Bearer"

    Is there a good article where I can read more about client.svc and what it's doing exactly?

Page 1 of 2 (23 items) 12
Leave a Comment
  • Please add 3 and 6 and type the answer here:
  • Post
Translate This Page
Search
Archive
Archives