Sharepoint 2013 On-Premises C# CSOM Cross Site Collection List Access

2.9k Views Asked by At

I see this has sorta been asked a bunch times before, but all the examples I am running across will not work or are in JavaScript , I NEED HELP WITH C# please.

I have a farm with server site collections on it, I successfully created a provider hosted addin/App, When it trys to access lists on the Web that launched it everything is fine! I need to try to access lists on other webs in the same farm and on the same domain does anyone have an example of C# code that can do this

2

There are 2 best solutions below

0
On BEST ANSWER

I figured this out, so I am posting it just in case someone else needs it, also please understand that I am total noob to SharePoint and this may not be the best way or even the SharePoint Accepted way of doing things.

First you need to give you app Tenant Permission (Full Control or Manage) ! - Very Important

Second I created this function that make a n SharePoint Context to a site other then the one the app is running on

 public ClientContext CreateRemoteSharePointContext(string TargetWebURL, SharePointContext CurrentSharePointContext)
    {
        //In order for us to create a share point client context that points to 
        //site other then the site that this app is running we need to copy some url parameters from the current 
        //context. These parameters are found on the current share-point context 
        NameValueCollection QueryString = Request.QueryString;
        //Since, The Query string is a read only collection, a use of reflection is required to update the 
        //values on the request object,  we must use the current request object because it contains 
        //security and other headers/cookies that we need for the context to be created, Grab the url params that we need 
        //other then TargetWebUrl, that will be the url of the site we want to manipulate
        Utility.AddToReadonlyQueryString(QueryString, "SPHostUrl", CurrentSharePointContext.SPHostUrl.ToString(), System.Web.HttpContext.Current.Request);
        Utility.AddToReadonlyQueryString(QueryString, "SPAppWebUrl", TargetWebURL, System.Web.HttpContext.Current.Request);
        Utility.AddToReadonlyQueryString(QueryString, "SPLanguage", CurrentSharePointContext.SPLanguage, System.Web.HttpContext.Current.Request);
        Utility.AddToReadonlyQueryString(QueryString, "SPClientTag", CurrentSharePointContext.SPClientTag, System.Web.HttpContext.Current.Request);
        Utility.AddToReadonlyQueryString(QueryString, "SPProductNumber", CurrentSharePointContext.SPProductNumber, System.Web.HttpContext.Current.Request);
        //This is a special line, we need to get the AppOnly access token and pass it along to the target site, its is a little counter intuitive
        //Because we are using TokenHelper.GetS2SAccessToeknWithWindowsIdentity - but we pass NULL as the User identity, this will 
        //check the app manifest and if the app has a CERT and AppOnly Permission it will return a valid app only token to use
        Utility.AddToReadonlyQueryString(QueryString, "AppContextToken", TokenHelper.GetS2SAccessTokenWithWindowsIdentity(new Uri(TargetWebURL), null), System.Web.HttpContext.Current.Request);
        //Return the newly created context
        return SharePointContextProvider.Current.CreateSharePointContext(HttpContext.Request, TargetWebURL).CreateAppOnlyClientContextForSPAppWeb();
    }

As you can see the I had to kinda hack up the Querystring and grab some values so here is the Utility class that does that :

public class Utility
{
    public static void UpdateReadonlyQueryString(NameValueCollection collectionToUpdate, string paramName, string paramValue, HttpRequest Request)
    {

        collectionToUpdate = (NameValueCollection)Request.GetType().GetField("_queryString", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(Request);
        PropertyInfo readOnlyInfo = collectionToUpdate.GetType().GetProperty("IsReadOnly", BindingFlags.NonPublic | BindingFlags.Instance);
        readOnlyInfo.SetValue(collectionToUpdate, false, null);
        collectionToUpdate[paramName] = paramValue;
        readOnlyInfo.SetValue(collectionToUpdate, true, null);
    }
    public static void AddToReadonlyQueryString(NameValueCollection collectionToUpdate, string paramName, string paramValue, HttpRequest Request)
    {
        collectionToUpdate = Request.QueryString;
        collectionToUpdate = (NameValueCollection)Request.GetType().GetField("_queryString", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(Request);
        PropertyInfo readOnlyInfo = collectionToUpdate.GetType().GetProperty("IsReadOnly", BindingFlags.NonPublic | BindingFlags.Instance);
        readOnlyInfo.SetValue(collectionToUpdate, false, null);
        collectionToUpdate.Add( paramName, paramValue);
        readOnlyInfo.SetValue(collectionToUpdate, true, null);
    }
}

and Finally the SharePoint access code the looks like much of the same SharePoint code out there on the web, I had to remove some stuff from it that would identify the project or who its for, but it should be easy to pick out what you need from in side

 try
{

    //Get the name of the sharepoint list that needs to be updated from settings 
    var ListName = ConfigurationManager.AppSettings[Constants.SettingsConstants.SPLaunchQueList];
    var TargetSiteToUpdate =  "URL TO THE SITE YOUR TRYING TO UPDATE"; 
    //Get the sharepoint context from session 
    var spContext = <SOME HOW CREATE YOUR CONTEXT> 
    //Lets create a client context from the current sharepoint context to the target site 
    //NOTE this requires the application to HAVE Tenant level permission, it must be trusted by
    //the farm admin 
    using (var spClientContext = CreateRemoteSharePointContext(TargetSiteToUpdate, spContext))
    {

        //Get the current Web (Sharepoint Web) from the client context 
        var web = spClientContext.Web;
        //Load all the webs properties including title , url all the lists and get the subwebs if any as well 
        spClientContext.Load(web, x => x.Title, x => x.Url, x => x.Lists, x => x.Webs.Include(w => w.Title, w => w.Url));
        spClientContext.ExecuteQuery();
        //Lets grab the list that needs to be updated 
        SP.List OrgList = web.Lists.GetByTitle(ListName);
        //Construct a caml query Where the groupID of the SQL Server record is the same 
        //as the list GroupID 
        var caml = new Caml<DetailParts>().Where(o => o.GroupID == updateRecord.GroupID);
        CamlQuery camlQuery = new CamlQuery();
        camlQuery.ViewXml = caml.ToString();
        //Load the CAML query 
        ListItemCollection Rows = OrgList.GetItems(camlQuery);
        spClientContext.Load(Rows);
        spClientContext.ExecuteQuery();
        //The CAML Query should only return one row because GroupID should be UNQIUE 
        //however due to how sharepoint returns list data we are forcing the first value 
        //here 
        ListItem RowToUpdate = Rows[0];
        //Get a list of sharepoint columns that match the local detail parts 
        var ColumnsToUpdate = GetSharePointColumns(typeof(DetailParts));

         RowToUpDate["SomeColumn"] = "YOUR NEW VALUE"; 

         RowToUpdate.Update();
         //Commit the changes 
         spClientContext.ExecuteQuery();
        }
    }

}
catch (Exception ex)
{
    //Log any exception and then throw to the caller 
    logger.Error("Sharepoint exception", ex);
}

That last section of code should be in a function or method of some sort I just pull out the relevant parts. As I Stated this is the only way I found that works and if someone has a better way please share it as I am not a SharePoint expert.

4
On

You can create a repository method like this:

public class SharepointRepository
    {
        public ListItemCollection ListTopN(string urlSite, string listName, bool ascending, string column, int rowLimit)
        {
            using (var context = new ClientContext(urlSite))
            {
                context.Credentials = CredentialCache.DefaultCredentials;
                List list = context.Web.Lists.GetByTitle(listName);

                string myQuery = string.Format("<View><Query><OrderBy><FieldRef Name='{0}' Ascending='{1}' /></OrderBy></Query><RowLimit>{2}</RowLimit></View>", column, ascending.ToString(), rowLimit);

                CamlQuery query = new CamlQuery();
                query.ViewXml = myQuery;

                ListItemCollection collection = list.GetItems(query);

                context.Load(list);
                context.Load(collection);
                context.ExecuteQuery();
                return collection;
            }
        }
}

this approach uses the managed csom.

and if you are facing problems with ADFS, try adding after this line

context.Credentials = CredentialCache.DefaultCredentials;

this

context.ExecutingWebRequest += new EventHandler<WebRequestEventArgs>(MixedAuthRequestMethod);

and this function

void MixedAuthRequestMethod(object sender, WebRequestEventArgs e) { e.WebRequestExecutor.RequestHeaders.Add("X-FORMS_BASED_AUTH_ACCEPTED", "f"); }

This is a basic referente: https://msdn.microsoft.com/en-us/library/office/fp179912.aspx

You should also look at the Sharepoint App Model and the Rest OData API.