<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>PnP Framework and PnP Core on Jeppe Spanggaard - Software Developer | .NET, Azure &amp; Microsoft 365</title><link>https://jeppe-spanggaard.dk/tags/pnp/</link><description>Recent content in PnP Framework and PnP Core on Jeppe Spanggaard - Software Developer | .NET, Azure &amp; Microsoft 365</description><generator>Hugo</generator><language>en-US</language><lastBuildDate>Mon, 27 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://jeppe-spanggaard.dk/tags/pnp/index.xml" rel="self" type="application/rss+xml"/><item><title>PnP Core vs PnP.Framework: Why I Haven't Switched Yet</title><link>https://jeppe-spanggaard.dk/blogs/pnp-core-vs-pnp-framework-migration-blockers/</link><pubDate>Mon, 27 Jul 2026 00:00:00 +0000</pubDate><guid>https://jeppe-spanggaard.dk/blogs/pnp-core-vs-pnp-framework-migration-blockers/</guid><description>Learn why I'm still on PnP.Framework instead of PnP Core, the two blockers holding the migration back, and what has to land before I make the jump.</description><content:encoded><![CDATA[<p>I&rsquo;ve been building an internal shared library. The idea is boring and useful: collect the code I write over and over into one place, keep it as up to date as I can, and make it battle tested rather than &ldquo;worked on my tenant last Tuesday&rdquo;. Testing with <a href="https://learn.microsoft.com/en-us/microsoft-cloud/dev/dev-proxy/overview">Dev Proxy</a> instead of hope - I&rsquo;ve <a href="https://jeppe-spanggaard.dk/blogs/devproxy-throttling-testing/">simulated throttling with it</a> enough now to trust what it tells me.</p>
<p>That kind of library is exactly the moment to pick your foundation on purpose. So I sat down and looked hard at PnP Core.</p>
<p>And then I stayed on PnP.Framework.</p>
<h2 id="what-makes-pnp-core-tempting">What Makes PnP Core Tempting</h2>
<p>The thing I actually want from PnP Core isn&rsquo;t a feature. It&rsquo;s that somebody already made a decision I keep having to make myself.</p>
<p>With CSOM and raw calls, every time I read something out of SharePoint I have to think about <em>how</em>: SharePoint REST, or Microsoft Graph? Which one has this property? Which one is faster here? PnP Core removes that question. Per the docs, the SDK &ldquo;by default is configured to favor the Microsoft Graph API when you&rsquo;re reading SharePoint data assuming the requested properties are available via Graph&rdquo;, and it falls back to SharePoint REST when they aren&rsquo;t. If you disagree, you flip <code>GraphFirst</code> to false on the <code>PnPContext</code> and it prefers REST instead.</p>
<p>Someone with more context than me picked the better call per operation, and I get to write the intent instead of the plumbing. Add async-first, a real domain model, and modern .NET hosting, and it&rsquo;s not a close comparison on paper.</p>
<p>The problem is that my code doesn&rsquo;t run on paper.</p>
<h2 id="blocker-1-the-provisioning-engine-isnt-there">Blocker 1: The Provisioning Engine Isn&rsquo;t There</h2>
<p>Almost everything I build for SharePoint eventually provisions something. Lists, content types, pages, navigation. That&rsquo;s <code>ApplyProvisioningTemplate</code>, and it lives in PnP.Framework. PnP Core has no provisioning engine.</p>
<p>So a migration wasn&rsquo;t &ldquo;rewrite my queries&rdquo;. It was &ldquo;rewrite my queries and keep PnP.Framework around anyway for the part that does the heavy lifting&rdquo;. Two SDKs, two mental models, no win.</p>
<p>That&rsquo;s the part I&rsquo;d already made peace with, until <a href="https://github.com/pnp/pnpframework/issues/1237">pnpframework issue #1237</a> showed up in June: a roadmap for moving both the Provisioning Engine and the Modernization Engine into PnP Core as dedicated projects, with the PnP.Framework versions deprecated afterwards. Target is Q4 2026.</p>
<p>That single issue turns my biggest objection into a waiting game. I don&rsquo;t need PnP Core to grow a provisioning engine as a favor to me. It&rsquo;s on the roadmap, in the open, with a phase 2 already referenced for the wider PnP.Framework deprecation. Everything I&rsquo;ve written about <a href="https://jeppe-spanggaard.dk/blogs/pnp-template-sizing-resilience/">splitting templates into modular files for retry resilience</a> survives that move, because it&rsquo;s a property of how you structure templates, not of which SDK applies them.</p>
<h2 id="blocker-2-caml-joins-come-back-empty">Blocker 2: CAML Joins Come Back Empty</h2>
<p>The second one I couldn&rsquo;t wait out, because it&rsquo;s the query pattern I lean on hardest.</p>
<p>SharePoint&rsquo;s CAML supports joining lists and pulling fields across the join with <code>&lt;Joins&gt;</code> and <code>&lt;ProjectedFields&gt;</code>. It&rsquo;s the difference between one query and four, and I&rsquo;ve written a <a href="https://jeppe-spanggaard.dk/blogs/joining-multiple-lists-csom-caml/">whole post on doing it with CAMLEX in CSOM</a> because it saves resource units and spares you from merging rows in C#.</p>
<p>Run that same view XML through PnP Core&rsquo;s <code>LoadItemsByCamlQueryAsync</code> and nothing breaks. No exception, no error, no warning. You get items back, the join is honored for filtering, and the projected fields simply aren&rsquo;t in the result. The REST <code>GetItems</code> endpoint doesn&rsquo;t serialize them, so they never make it into the response for the SDK to map.</p>
<p>A silent hole in the data is worse than a thrown exception, and it&rsquo;s a hole in the one query shape I most wanted to keep.</p>
<p>So I filed <a href="https://github.com/pnp/pnpcore/issues/1799">issue #1799</a> and then <a href="https://github.com/pnp/pnpcore/pull/1802">PR #1802</a> with an implementation: two new methods on <code>IList</code> that run the query through CSOM&rsquo;s <code>List.GetItems(CamlQuery)</code> instead of REST, because CSOM does return projected fields.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#75715e">/// &lt;summary&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">/// Loads list items based up on a CAML query executed via CSOM, which also returns</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">/// fields projected from a joined list (CAML Joins/ProjectedFields)</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">/// &lt;/summary&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> Task&lt;ICamlQueryCsomResult&gt; LoadItemsByCamlQueryViaCsomAsync(CamlQueryOptions queryOptions);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> ICamlQueryCsomResult LoadItemsByCamlQueryViaCsom(CamlQueryOptions queryOptions);
</span></span></code></pre></div><p>Using it looks like any other PnP Core call. This is the shape from the integration test in the PR:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> page1 = <span style="color:#66d9ef">await</span> list.LoadItemsByCamlQueryViaCsomAsync(<span style="color:#66d9ef">new</span> CamlQueryOptions()
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    ViewXml = viewXml,
</span></span><span style="display:flex;"><span>    DatesInUtc = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> firstItem = page1.Items[<span style="color:#ae81ff">0</span>];
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> projected = firstItem[<span style="color:#e6db74">&#34;ProjectedText&#34;</span>] <span style="color:#66d9ef">as</span> IFieldLookupValue;
</span></span><span style="display:flex;"><span>Console.WriteLine(projected.LookupValue);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Next page, without hand-building a paging string</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> page2 = <span style="color:#66d9ef">await</span> list.LoadItemsByCamlQueryViaCsomAsync(<span style="color:#66d9ef">new</span> CamlQueryOptions()
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    ViewXml = viewXml,
</span></span><span style="display:flex;"><span>    DatesInUtc = <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>    PagingInfo = page1.PagingInfo
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><p><strong>What&rsquo;s happening here?</strong></p>
<ol>
<li><code>LoadItemsByCamlQueryViaCsomAsync</code> takes the exact same <code>CamlQueryOptions</code> as the REST-based method, so the view XML with <code>&lt;Joins&gt;</code> and <code>&lt;ProjectedFields&gt;</code> is unchanged. Only the transport differs.</li>
<li>The projected field comes back typed as an <code>IFieldLookupValue</code>, not as a raw string. That&rsquo;s how CSOM represents it and it&rsquo;s what you&rsquo;d expect from a lookup coming across a join, so <code>LookupId</code> and <code>LookupValue</code> are both there.</li>
<li>The returned <code>ICamlQueryCsomResult</code> carries <code>Items</code> plus <code>PagingInfo</code>, taken from CSOM&rsquo;s <code>ListItemCollectionPosition</code>. Feed <code>PagingInfo</code> back into the next call and you get the next page. It&rsquo;s null when there are no more pages.</li>
<li>The loaded items are also merged into the list&rsquo;s <code>Items</code> collection, the same as the existing CAML methods, so nothing about the surrounding model changes.</li>
</ol>
<h2 id="gotchas">Gotchas</h2>
<ul>
<li><strong>The failure mode is silence, not an error.</strong> No exception on a join query with projected fields means your code happily maps a null and moves on. If you&rsquo;re evaluating PnP Core for join-heavy work, assert on the projected value in a test, don&rsquo;t eyeball the item count.</li>
<li><strong>A roadmap is a plan, not a shipped release.</strong> Issue #1237 has a Q4 2026 target and it&rsquo;s still open. Same for my PR: open, no reviewer assigned yet. I&rsquo;m building on the current state of both SDKs, not on the version I hope exists in six months.</li>
<li><strong>No batch variant for the CSOM path, on purpose.</strong> The items are materialized immediately, so it doesn&rsquo;t fit PnP Core&rsquo;s batching model. If you&rsquo;re used to queueing everything into a batch, this one call stands apart.</li>
<li><strong>Deprecation warnings are part of the plan.</strong> Once the engines land in PnP Core, the PnP.Framework equivalents get marked deprecated and stop taking new features. Staying put is a decision with an expiry date, which is exactly why I&rsquo;d rather have the blockers resolved than keep postponing.</li>
</ul>
<h2 id="wrapping-up">Wrapping Up</h2>
<p>I&rsquo;m not avoiding PnP Core because I prefer PnP.Framework. I&rsquo;m on PnP.Framework because two specific things weren&rsquo;t there: the provisioning engine, and CAML joins that actually return the joined data. Both now have an issue number attached, and one of them has my name on the pull request.</p>
<p>Rule of thumb: pick the SDK by what your workload actually needs today, then go make the gap smaller instead of waiting for someone else to. Filing #1799 took an evening. It moved my migration date more than another month of reading release notes would have.</p>
<p>When the provisioning engine lands in PnP Core and projected fields come back from a join, I&rsquo;m switching and I&rsquo;m not looking back.</p>
]]></content:encoded></item><item><title>One Big PnP Template or Many Small Ones?</title><link>https://jeppe-spanggaard.dk/blogs/pnp-template-sizing-resilience/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><author>Jeppe</author><guid>https://jeppe-spanggaard.dk/blogs/pnp-template-sizing-resilience/</guid><description>After testing with DevProxy, I found that monolithic PnP templates silently destroy your retry strategy. Here's what I learned.</description><content:encoded><![CDATA[<p>Every time I sit down to build a SharePoint provisioning engine, I hit the same dilemma: do I put everything into one big PnP template, or do I split it into many smaller ones — one per feature?</p>
<p>Both approaches work. Both have real trade-offs. And for a long time I did not have a strong opinion either way. Then I started testing with <a href="https://learn.microsoft.com/en-us/microsoft-cloud/dev/dev-proxy/overview">Dev Proxy</a> and suddenly the answer became very clear to me.</p>
<h2 id="what-is-a-pnp-provisioning-template">What Is a PnP Provisioning Template?</h2>
<p>If you have not worked with PnP provisioning before: a PnP Provisioning Template is an XML (or JSON) file that describes the structure of a SharePoint site. It can contain:</p>
<ul>
<li><strong>Lists and libraries</strong> — custom columns, views, and settings</li>
<li><strong>Content types</strong> — site or list content types with their fields</li>
<li><strong>Pages and web parts</strong> — modern pages, hero sections, text blocks</li>
<li><strong>Navigation and branding</strong> — site navigation structure, themes, logos</li>
</ul>
<p>You apply a template to a site using the <a href="https://github.com/pnp/pnpframework">PnP Framework</a> library for .NET, which reads the file and provisions everything described in it. The library handles the order of operations, dependencies between objects, and a lot of edge cases you would rather not think about.</p>
<p>The typical entry point looks like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span>web.ApplyProvisioningTemplate(template, applyInfo);
</span></span></code></pre></div><p>One call. One template. PnP handles the rest — until throttling enters the picture.</p>
<h2 id="the-monolith-approach">The Monolith Approach</h2>
<p>The simplest way to structure your provisioning is one template that contains everything. All lists, all content types, all pages, all navigation — in a single file.</p>
<p><strong>Pros:</strong></p>
<ul>
<li>✅ Simple to manage — one file, one version, one deployment artifact</li>
<li>✅ Easy to apply — a single <code>ApplyProvisioningTemplate</code> call</li>
<li>✅ PnP handles dependencies between objects internally</li>
</ul>
<p><strong>Cons:</strong></p>
<ul>
<li>❌ All-or-nothing retry — if anything fails, you restart from the beginning</li>
<li>❌ Long templates take a long time to apply, which means long retries too</li>
<li>❌ Hard to reason about what failed and where</li>
</ul>
<p>The all-or-nothing behavior is the killer. SharePoint throttles aggressively under load, and <code>ApplyProvisioningTemplate</code> does not pick up where it left off. Under a sustained throttle — the kind where even 15 retries at the CSOM call level are not enough to get through — the template application eventually throws a <code>MaximumRetryAttemptedException</code> — a PnP-specific exception signalling that all retry attempts were used up. At that point your outer retry logic has no choice but to start the whole template over from the very first list.</p>
<h2 id="the-modular-approach">The Modular Approach</h2>
<p>The alternative is splitting your template into multiple smaller files, each representing a logical feature or concern. For example:</p>
<ul>
<li><code>01-lists-and-libraries.xml</code></li>
<li><code>02-content-types.xml</code></li>
<li><code>03-pages.xml</code></li>
<li><code>04-navigation.xml</code></li>
</ul>
<p>You apply them in sequence, and after each one completes successfully you record that fact in memory. If a template fails, you retry only from the failed template — not from the beginning.</p>
<p><strong>Pros:</strong></p>
<ul>
<li>✅ Granular retry — only the failed feature is re-applied</li>
<li>✅ Faster recovery from throttling</li>
<li>✅ Easier to reason about failures (&ldquo;pages failed, lists are fine&rdquo;)</li>
<li>✅ Easier to test individual features in isolation</li>
</ul>
<p><strong>Cons:</strong></p>
<ul>
<li>❌ More files to manage and version</li>
<li>❌ Ordering matters — you need to think about dependencies</li>
<li>❌ Slightly more orchestration code on your end</li>
</ul>
<p>The trade-off is real. Modular means more moving parts. But for resilience, the payoff is significant.</p>
<h2 id="testing-with-devproxy--where-it-got-interesting">Testing With DevProxy — Where It Got Interesting</h2>
<p>I wanted to put both approaches through their paces under realistic throttling conditions. For that I used Dev Proxy with two plugins:</p>
<ul>
<li><strong>GenericRandomErrorPlugin</strong> — injects random 429 responses to simulate SharePoint throttling unpredictably</li>
<li><strong>RateLimitingPlugin</strong> — caps requests per second to trigger organic throttling at volume</li>
</ul>
<p>With these configured, I ran my provisioning engine against both template structures.</p>
<p>The result with the <strong>monolithic</strong> template: <code>PnPClientContext</code> did its job — it absorbed a lot of 429s silently and kept going. But under sustained throttling, the retries ran out, PnP threw a <code>MaximumRetryAttemptedException</code>, and my outer retry had to restart it from scratch. From list number one. Even though we were deep into page provisioning when it failed.</p>
<p>The result with <strong>modular</strong> templates: the same CSOM-level retry absorbed the same throttling. But when a sustained burst finally exhausted PnP&rsquo;s retries on the pages template, my retry loop skipped straight to <code>03-pages.xml</code> — lists and content types had already been marked done in memory and were left alone.</p>
<blockquote>
<p>This is exactly the kind of thing that looks fine in a happy-path test, but silently destroys your provisioning SLA in production.</p>
</blockquote>
<h2 id="pnpclientcontext-bump-the-retry-count-for-provisioning">PnPClientContext: Bump the Retry Count for Provisioning</h2>
<p>The PnP Framework&rsquo;s internal CSOM calls already use <code>ExecuteQueryRetry()</code> — an extension method that handles 429 throttling with exponential backoff, respecting the <code>Retry-After</code> header SharePoint returns. You get this for free with a plain <code>ClientContext</code>. The default is 10 retries.</p>
<p>If you expect heavier throttling during provisioning — and a large site with lots of lists, content types, and pages is a reasonable place to expect it — you can easily raise that limit by converting to a <code>PnPClientContext</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">using</span> var siteCtx = CSOMClientFactory.Create(provCtx.NewSiteUrl, credential);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> pnpCtx = PnPClientContext.ConvertFrom(siteCtx, retryCount: <span style="color:#ae81ff">15</span>);
</span></span></code></pre></div><p>That is the entire change. The provisioning code picks up the higher retry count automatically. Your outer retry logic only comes into play if throttling is so sustained that even 15 attempts on a single CSOM call are not enough — at which point PnP throws a <code>MaximumRetryAttemptedException</code>.</p>
<h2 id="idempotency-strategy">Idempotency Strategy</h2>
<p>The key property that makes modular templates safe to retry is idempotency — applying a template that has already been applied should not cause errors or duplicate data. PnP handles most of this for you: before creating a list, content type, or page, it checks whether it already exists.</p>
<p>A very simple example of how this looks in practice:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">foreach</span> (<span style="color:#66d9ef">var</span> source <span style="color:#66d9ef">in</span> config.Templates)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> template = LoadTemplate(source);
</span></span><span style="display:flex;"><span>    pnpCtx.Web.ApplyProvisioningTemplate(template, applyInfo);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>If this loop is re-invoked after a failure (for example by an Azure Function retry policy), templates that already completed will be re-applied — but because each template is idempotent, that is safe. The already-provisioned lists and pages are left untouched. The only cost is the time it takes to re-apply them.</p>
<p>That wasted time is the whole point. With a monolithic template, a retry means re-applying everything from scratch. With modular templates, it means re-applying a small number of already-done features before reaching the one that actually failed.</p>
<h2 id="comparison">Comparison</h2>
<table>
  <thead>
      <tr>
          <th></th>
          <th>One Big Template</th>
          <th>Many Small Templates</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Retry granularity</strong></td>
          <td>Entire template restarts</td>
          <td>Only failed feature retries</td>
      </tr>
      <tr>
          <td><strong>Restart cost</strong></td>
          <td>High — full re-apply</td>
          <td>Low — one feature re-applies</td>
      </tr>
      <tr>
          <td><strong>Complexity</strong></td>
          <td>Low — one file, one call</td>
          <td>Medium — ordering, orchestration</td>
      </tr>
      <tr>
          <td><strong>Idempotency needed</strong></td>
          <td>Once per template</td>
          <td>Once per template (still needed)</td>
      </tr>
      <tr>
          <td><strong>DevProxy testability</strong></td>
          <td>Easy to test the whole flow</td>
          <td>Easier to isolate a specific feature</td>
      </tr>
      <tr>
          <td><strong>Best for</strong></td>
          <td>Simple sites, low throttle risk</td>
          <td>Complex sites, production resilience</td>
      </tr>
  </tbody>
</table>
<h2 id="verdict">Verdict</h2>
<p>Neither approach is universally correct. If you are provisioning simple sites with low template complexity and you are not worried about throttling, a monolithic template is perfectly fine and a lot less work to maintain.</p>
<p>But if you are building a production provisioning engine that needs to be resilient — where throttling is a real risk, where retries need to be efficient, and where you care about how long a failure takes to recover from — modular templates win clearly.</p>
<p>The combination that works best for me is: <strong>modular templates</strong> + <strong>PnPClientContext</strong> with conservative retry settings. PnPClientContext handles the low-level CSOM throttling so most 429s never surface at all. When they do surface (and eventually they will), modular templates ensure your retry loop does the minimum amount of work to get back on track.</p>
<h2 id="conclusion">Conclusion</h2>
<p>The biggest takeaway for me is not even the monolith-vs-modular debate. It is that <strong>I never would have found this out without DevProxy</strong>. Happy-path testing showed no difference between the two approaches. It was only when I introduced realistic throttling that the behavior diverged in a way that actually matters.</p>
<p>If you are building any kind of SharePoint automation that makes CSOM or Graph calls under load, DevProxy should be a standard part of your testing setup. It is free, it integrates with your local development environment, and it reveals exactly the kind of subtle failure modes that only appear in production.</p>
<hr>
<h3 id="references">References</h3>
<ul>
<li><a href="https://github.com/pnp/pnpframework">PnP Framework on GitHub</a></li>
<li><a href="https://learn.microsoft.com/en-us/microsoft-cloud/dev/dev-proxy/overview">Dev Proxy overview</a></li>
<li><a href="https://learn.microsoft.com/en-us/sharepoint/dev/general-development/how-to-avoid-getting-throttled-or-blocked-in-sharepoint-online">SharePoint throttling and retry guidance</a></li>
<li><a href="https://pnp.github.io/pnpframework/">PnPClientContext — ExecuteQueryRetry docs</a></li>
</ul>
]]></content:encoded></item><item><title>PnP.Framework Authentication in C#: A Beginner's Guide</title><link>https://jeppe-spanggaard.dk/blogs/pnp-framework-authentication-csharp/</link><pubDate>Tue, 03 Jun 2025 00:00:00 +0000</pubDate><author>Jeppe</author><guid>https://jeppe-spanggaard.dk/blogs/pnp-framework-authentication-csharp/</guid><description>Learn to authenticate C# applications with PnP.Framework and Azure using certificates in this beginner's guide to app registration and permissions.</description><content:encoded><![CDATA[<h2 id="app-registration">App registration</h2>
<p>Go to <a href="https://portal.azure.com">https://portal.azure.com</a> and search for &ldquo;App registrations&rdquo;:
<img src="https://jeppe-spanggaard.dk/images/AzurePortalSearchAppReg_hu_2be0317d5be28fdd.webp" srcset="/images/AzurePortalSearchAppReg_hu_2be0317d5be28fdd.webp 480w" sizes="(max-width: 760px) 100vw, 720px"
    width="480" height="156"
    alt="alt text" style="background:url(data:image/webp;base64,UklGRmIAAABXRUJQVlA4IFYAAABQBACdASoYAAgAP1mQvk0pJKMhMAgBJisJ5wAu/8CHgZbNgyH&#43;07aNzahIAP6Qpu6EMwIw2MnqeHDQTfFMo3hrSUOQmegSAO34/GABBE7jsmqsuAAAAA==) center/cover no-repeat" loading="lazy" decoding="async"></p>
<p>Click &ldquo;New registration&rdquo; and fill out the name, and leave the &ldquo;Supported account types&rdquo; to default for now (Default: Accounts in this organizational directory only (xxx - Single tenant))</p>
<p>And now just press &ldquo;Register&rdquo;</p>
<p>Just like so, you have created your app registration! Take note of the <strong>Application (client) ID</strong> and <strong>Directory (tenant) ID</strong> from the overview page - you&rsquo;ll need these values later in your C# code.</p>
<p><img src="https://jeppe-spanggaard.dk/images/AppRegOverviewPage_hu_c2869f9b03e36559.webp" srcset="/images/AppRegOverviewPage_hu_e913716f99ceec37.webp 480w, /images/AppRegOverviewPage_hu_c2869f9b03e36559.webp 720w" sizes="(max-width: 760px) 100vw, 720px"
    width="720" height="285"
    alt="App Registration Overview Page" style="background:url(data:image/webp;base64,UklGRkgAAABXRUJQVlA4IDwAAADwAwCdASoYAAoAP1mKtkspJKKYBACTFYT0gAAzJ2esdzwpc/l&#43;ifwAAP7T4zOKUnyOrAypj7iJC/RgEAA=) center/cover no-repeat" loading="lazy" decoding="async"></p>
<h3 id="generate-certificate">Generate certificate</h3>
<p>We need a certificate on the app registration, and in our code to authenticate our C# application. Certificates are more secure than client secrets because they&rsquo;re harder to extract and can be managed through your organization&rsquo;s PKI infrastructure.</p>
<p>Open PowerShell as Administrator and run the following commands:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-PowerShell" data-lang="PowerShell"><span style="display:flex;"><span>$cert = New-SelfSignedCertificate `
</span></span><span style="display:flex;"><span>    -Subject <span style="color:#e6db74">&#34;CN=MyPnPAppCert&#34;</span> `
</span></span><span style="display:flex;"><span>    -CertStoreLocation <span style="color:#e6db74">&#34;cert:\CurrentUser\My&#34;</span> `
</span></span><span style="display:flex;"><span>    -KeyExportPolicy Exportable `
</span></span><span style="display:flex;"><span>    -KeySpec Signature `
</span></span><span style="display:flex;"><span>    -KeyLength <span style="color:#ae81ff">2048</span> `
</span></span><span style="display:flex;"><span>    -NotAfter (Get-Date).AddYears(<span style="color:#ae81ff">3</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>$plainText = <span style="color:#e6db74">&#34;MySuperStrongPassword!&#34;</span>
</span></span><span style="display:flex;"><span>$secureString = ConvertTo-SecureString $plainText -AsPlainText -Force
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Export-PfxCertificate `
</span></span><span style="display:flex;"><span>    -Cert $cert `
</span></span><span style="display:flex;"><span>    -FilePath <span style="color:#e6db74">&#34;C:\Temp\cert\pnpappcert.pfx&#34;</span> `
</span></span><span style="display:flex;"><span>    -Password $secureString
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Export-Certificate `
</span></span><span style="display:flex;"><span>    -Cert $cert `
</span></span><span style="display:flex;"><span>    -FilePath <span style="color:#e6db74">&#34;C:\Temp\cert\pnpappcert.cer&#34;</span>        
</span></span></code></pre></div><p>This script creates a self-signed certificate that&rsquo;s valid for 3 years. The certificate gets stored in your personal certificate store, and we export both the .pfx file (containing the private key) and the .cer file (public key only). Make sure the <code>C:\Temp\cert\</code> directory exists before running this script.</p>
<h3 id="upload-certificate">Upload certificate</h3>
<p>We need to upload the generated certificate - the .cer file (NOT the .pfx), to the app registration. The .cer file contains only the public key, while the .pfx contains the private key that should never be shared.</p>
<p>Navigate to your app registration in Azure Portal, go to &ldquo;Certificates &amp; secrets&rdquo;, then click &ldquo;Upload certificate&rdquo;:
<img src="https://jeppe-spanggaard.dk/images/AppRegCertUpload_hu_beb98805c0b9f235.webp" srcset="/images/AppRegCertUpload_hu_4dddca826d70ddb0.webp 480w, /images/AppRegCertUpload_hu_8391d4a5ff602c8a.webp 720w, /images/AppRegCertUpload_hu_beb98805c0b9f235.webp 1200w" sizes="(max-width: 760px) 100vw, 720px"
    width="1200" height="461"
    alt="alt text" style="background:url(data:image/webp;base64,UklGRkAAAABXRUJQVlA4IDQAAACwAwCdASoYAAkAP1mkvk&#43;pJqMhMAgBJisJ6QAAZk9VL9Z6OYRmAAD&#43;0z8ijK3FkkbjgAAA) center/cover no-repeat" loading="lazy" decoding="async"></p>
<p>After uploading, you&rsquo;ll see the certificate listed with its thumbprint. The thumbprint can be useful for identifying which certificate to use if you have multiple certificates.</p>
<h3 id="add-permissions">Add permissions</h3>
<p>Our app registration needs permission to be able to do stuff. There are two types of permissions: Delegated and Application.
The differences between them are:</p>
<ul>
<li><strong>Delegated permissions</strong>: Your app acts on behalf of a signed-in user. The app can only access what the user has access to.</li>
<li><strong>Application permissions</strong>: Your app runs as itself without a signed-in user. The app has access based on what you grant it.</li>
</ul>
<p>For this example we&rsquo;ll use the SharePoint permission &ldquo;Sites.FullControl.All&rdquo; as an application permission, which gives our app full access to all SharePoint sites.</p>
<p>After adding the permission, don&rsquo;t forget to click &ldquo;Grant admin consent&rdquo; - this is crucial! Without admin consent, your app won&rsquo;t be able to use the permissions even if they&rsquo;re assigned.</p>
<p>Now our app registration is setup and ready to be used in our C# code.</p>
<h2 id="c-implementation">C# Implementation</h2>
<p>Now let&rsquo;s write the C# code to authenticate and connect to SharePoint using our certificate. I&rsquo;ll show you a complete working example that you can use as a starting point for your own projects.</p>
<p>First, create a new Console Application in Visual Studio and install the PnP.Framework NuGet package:</p>
<ul>
<li><a href="https://www.nuget.org/packages/PnP.Framework/1.18.0">https://www.nuget.org/packages/PnP.Framework/1.18.0</a></li>
</ul>
<p>You can install it via Package Manager Console: <code>Install-Package PnP.Framework</code></p>
<p>Here&rsquo;s a complete example that connects to SharePoint and demonstrates several common operations:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">using</span> PnP.Framework;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> System.Security.Cryptography.X509Certificates;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Program</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">readonly</span> <span style="color:#66d9ef">string</span> TenantId = <span style="color:#e6db74">&#34;your-tenant-id&#34;</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">readonly</span> <span style="color:#66d9ef">string</span> ClientId = <span style="color:#e6db74">&#34;your-client-id&#34;</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">readonly</span> <span style="color:#66d9ef">string</span> CertificatePath = <span style="color:#e6db74">@&#34;C:\Temp\cert\pnpappcert.pfx&#34;</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">readonly</span> <span style="color:#66d9ef">string</span> CertificatePassword = <span style="color:#e6db74">&#34;MySuperStrongPassword!&#34;</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">readonly</span> <span style="color:#66d9ef">string</span> SiteUrl = <span style="color:#e6db74">&#34;https://yourtenant.sharepoint.com/sites/yoursite&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">async</span> Task Main(<span style="color:#66d9ef">string</span>[] args)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">try</span>
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Load the certificate</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">var</span> certificate = <span style="color:#66d9ef">new</span> X509Certificate2(CertificatePath, CertificatePassword);
</span></span><span style="display:flex;"><span>            
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Create authentication manager</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">var</span> authManager = <span style="color:#66d9ef">new</span> AuthenticationManager(ClientId, certificate, TenantId);
</span></span><span style="display:flex;"><span>            
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Get context</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">using</span> var context = authManager.GetContext(SiteUrl);
</span></span><span style="display:flex;"><span>            
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Load web properties</span>
</span></span><span style="display:flex;"><span>            context.Load(context.Web, w =&gt; w.Title, w =&gt; w.Url);
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">await</span> context.ExecuteQueryAsync();
</span></span><span style="display:flex;"><span>            
</span></span><span style="display:flex;"><span>            Console.WriteLine(<span style="color:#e6db74">$&#34;Connected to: {context.Web.Title}&#34;</span>);
</span></span><span style="display:flex;"><span>            Console.WriteLine(<span style="color:#e6db74">$&#34;Site URL: {context.Web.Url}&#34;</span>);
</span></span><span style="display:flex;"><span>            
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Example: Get all lists</span>
</span></span><span style="display:flex;"><span>            context.Load(context.Web.Lists, lists =&gt; lists.Include(l =&gt; l.Title, l =&gt; l.ItemCount));
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">await</span> context.ExecuteQueryAsync();
</span></span><span style="display:flex;"><span>            
</span></span><span style="display:flex;"><span>            Console.WriteLine(<span style="color:#e6db74">&#34;\nLists in this site:&#34;</span>);
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">foreach</span> (<span style="color:#66d9ef">var</span> list <span style="color:#66d9ef">in</span> context.Web.Lists)
</span></span><span style="display:flex;"><span>            {
</span></span><span style="display:flex;"><span>                Console.WriteLine(<span style="color:#e6db74">$&#34;- {list.Title} ({list.ItemCount} items)&#34;</span>);
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Example: Create a new list</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">await</span> CreateSampleList(context);
</span></span><span style="display:flex;"><span>            
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Example: Upload a document</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">await</span> UploadDocument(context);
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">catch</span> (Exception ex)
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            Console.WriteLine(<span style="color:#e6db74">$&#34;Error: {ex.Message}&#34;</span>);
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">async</span> Task CreateSampleList(ClientContext context)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">try</span>
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">var</span> listCreationInfo = <span style="color:#66d9ef">new</span> ListCreationInformation
</span></span><span style="display:flex;"><span>            {
</span></span><span style="display:flex;"><span>                Title = <span style="color:#e6db74">&#34;Sample List&#34;</span>,
</span></span><span style="display:flex;"><span>                TemplateType = (<span style="color:#66d9ef">int</span>)ListTemplateType.GenericList
</span></span><span style="display:flex;"><span>            };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">var</span> list = context.Web.Lists.Add(listCreationInfo);
</span></span><span style="display:flex;"><span>            context.Load(list);
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">await</span> context.ExecuteQueryAsync();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            Console.WriteLine(<span style="color:#e6db74">$&#34;\nCreated list: {list.Title}&#34;</span>);
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">catch</span> (Exception ex)
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            Console.WriteLine(<span style="color:#e6db74">$&#34;List creation error: {ex.Message}&#34;</span>);
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">async</span> Task UploadDocument(ClientContext context)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">try</span>
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">var</span> docLib = context.Web.Lists.GetByTitle(<span style="color:#e6db74">&#34;Documents&#34;</span>);
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">var</span> fileCreationInfo = <span style="color:#66d9ef">new</span> FileCreationInformation
</span></span><span style="display:flex;"><span>            {
</span></span><span style="display:flex;"><span>                Content = System.Text.Encoding.UTF8.GetBytes(<span style="color:#e6db74">&#34;Hello from PnP Framework!&#34;</span>),
</span></span><span style="display:flex;"><span>                Url = <span style="color:#e6db74">&#34;sample-document.txt&#34;</span>,
</span></span><span style="display:flex;"><span>                Overwrite = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>            };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">var</span> uploadFile = docLib.RootFolder.Files.Add(fileCreationInfo);
</span></span><span style="display:flex;"><span>            context.Load(uploadFile);
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">await</span> context.ExecuteQueryAsync();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            Console.WriteLine(<span style="color:#e6db74">$&#34;\nUploaded file: {uploadFile.Name}&#34;</span>);
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">catch</span> (Exception ex)
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            Console.WriteLine(<span style="color:#e6db74">$&#34;File upload error: {ex.Message}&#34;</span>);
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="understanding-the-code">Understanding the Code</h3>
<p>Let&rsquo;s break down what this code does:</p>
<ol>
<li><strong>Certificate Loading</strong>: We load the .pfx file with the password we set earlier</li>
<li><strong>Authentication Manager</strong>: This handles the OAuth flow using the certificate</li>
<li><strong>Context Creation</strong>: Gets a <code>ClientContext</code> object that represents our connection to SharePoint</li>
<li><strong>Basic Operations</strong>: Shows how to read site properties, list all lists, create a new list, and upload a document</li>
</ol>
<p>The beauty of PnP.Framework is that it abstracts away all the complex authentication details. Once you have the <code>ClientContext</code>, you can use it just like the regular SharePoint CSOM (Client Side Object Model).</p>
<h3 id="important-notes">Important Notes</h3>
<ul>
<li>Replace <code>your-tenant-id</code>, <code>your-client-id</code>, and the site URL with your actual values from Azure Portal</li>
<li>The certificate path should point to your .pfx file (not the .cer file)</li>
<li>Make sure your app has the necessary permissions granted and admin consent given</li>
<li>Store sensitive information like certificates and passwords securely in production (use Azure Key Vault or similar)</li>
<li>Consider using certificate thumbprints instead of file paths in production environments</li>
<li>The <code>using</code> statement ensures proper disposal of the ClientContext</li>
</ul>
<h3 id="production-considerations">Production Considerations</h3>
<p>For production applications, consider these improvements:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#75715e">// Load certificate from certificate store instead of file</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> store = <span style="color:#66d9ef">new</span> X509Store(StoreName.My, StoreLocation.CurrentUser);
</span></span><span style="display:flex;"><span>store.Open(OpenFlags.ReadOnly);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> certificate = store.Certificates.Find(X509FindType.FindByThumbprint, <span style="color:#e6db74">&#34;your-cert-thumbprint&#34;</span>, <span style="color:#66d9ef">false</span>)[<span style="color:#ae81ff">0</span>];
</span></span><span style="display:flex;"><span>store.Close();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Use configuration instead of hardcoded values</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> tenantId = Configuration[<span style="color:#e6db74">&#34;Azure:TenantId&#34;</span>];
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> clientId = Configuration[<span style="color:#e6db74">&#34;Azure:ClientId&#34;</span>];
</span></span></code></pre></div><h2 id="troubleshooting">Troubleshooting</h2>
<p>If you run into issues, here are some common problems and their solutions:</p>
<p><strong>Certificate not found</strong>: Make sure the certificate path is correct and the password matches what you used when creating it. You can verify the certificate exists by checking it in the Windows Certificate Manager (certmgr.msc).</p>
<p><strong>Access denied</strong>: Verify that your app registration has the correct permissions and that admin consent has been granted. Check the Azure Portal to ensure the permissions show as &ldquo;Granted for [Your Organization]&rdquo;.</p>
<p><strong>Tenant ID issues</strong>: You can find your tenant ID in the Azure Portal under Azure Active Directory &gt; Properties. It&rsquo;s also visible in the app registration overview page.</p>
<p><strong>&ldquo;The remote server returned an error: (401) Unauthorized&rdquo;</strong>: This usually means the certificate isn&rsquo;t properly associated with the app registration, or the app doesn&rsquo;t have sufficient permissions. Double-check both the certificate upload and permission configuration.</p>
<p><strong>Certificate validation errors</strong>: If you&rsquo;re using a self-signed certificate in a corporate environment, your organization&rsquo;s security policies might block it. Consider using a certificate issued by your internal CA.</p>
<h3 id="testing-your-setup">Testing Your Setup</h3>
<p>Before diving into complex operations, test your connection with this minimal code:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">try</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> certificate = <span style="color:#66d9ef">new</span> X509Certificate2(CertificatePath, CertificatePassword);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> authManager = <span style="color:#66d9ef">new</span> AuthenticationManager(ClientId, certificate, TenantId);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">using</span> var context = authManager.GetContext(SiteUrl);
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    context.Load(context.Web, w =&gt; w.Title);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">await</span> context.ExecuteQueryAsync();
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    Console.WriteLine(<span style="color:#e6db74">$&#34;Success! Connected to: {context.Web.Title}&#34;</span>);
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">catch</span> (Exception ex)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    Console.WriteLine(<span style="color:#e6db74">$&#34;Connection failed: {ex.Message}&#34;</span>);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h2 id="wrapping-up">Wrapping Up</h2>
<p>You now have a working setup for authenticating with SharePoint using PnP.Framework and certificates. This method is perfect for background services, console applications, or any scenario where you need unattended access to SharePoint.</p>
<p>The certificate-based authentication is more secure than using client secrets since certificates are harder to compromise and can be managed through your organization&rsquo;s certificate infrastructure.</p>
<p>In my next post, I&rsquo;ll show you how to perform common SharePoint operations like creating lists, uploading files, and managing permissions using this authentication setup.</p>
]]></content:encoded></item><item><title>EF for SharePoint</title><link>https://jeppe-spanggaard.dk/blogs/entity-framework-for-sharepoint/</link><pubDate>Sun, 23 Jul 2023 00:00:00 +0000</pubDate><author>Jeppe</author><guid>https://jeppe-spanggaard.dk/blogs/entity-framework-for-sharepoint/</guid><description>Why and how? Wouldn’t it be great to have the ability to retrieve data from SharePoint in the same manner you are familiar with from Entity Framework? :face_with_monocle: At least that’s what I thought when I began working with SharePoint, having worked with EF for several years.
I thought there must be a way to retrieve data from SharePoint without having to work with dictionaries and create custom parsers to convert it into class models.</description><content:encoded><![CDATA[<h2 id="why-and-how">Why and how?</h2>
<p>Wouldn&rsquo;t it be great to have the ability to retrieve data from SharePoint in the same manner you are familiar with from Entity Framework? :face_with_monocle:
At least that&rsquo;s what I thought when I began working with SharePoint, having worked with EF for several years.</p>
<p>I thought there must be a way to retrieve data from SharePoint without having to work with dictionaries and create custom parsers to convert it into class models.</p>
<p>And then it hit me! Why not attempt to replicate the approach used by Entity Framework? EF is user-friendly, easily extensible, and does not require numerous custom parsers.</p>
<p>So that&rsquo;s why I&rsquo;ve started a Proof of Concept (POC), where I aim to implement the functionality from EF. I have made my repository public, so everyone is welcome to provide input and contribute ideas.</p>
<p><a href="https://github.com/jeppesc11/ListItemDynamicMapper">See GitHub repository</a></p>
<h2 id="how-does-it-work">How does it work?</h2>
<p>To make it work, you need to add a number of custom attributes to its class models.</p>
<ul>
<li>SPList</li>
<li>SPField</li>
</ul>
<p><strong>Example class</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-cs" data-lang="cs"><span style="display:flex;"><span><span style="color:#a6e22e">[SPList(listGuid: &#34;128b4e17-691a-4e28-9934-7bd399ba907a&#34;, listTitle: &#34;EmployeeTasks&#34;)]</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">internal</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">EmployeeTaskModel</span> {
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">  [SPField(internalName: &#34;ID&#34;)]</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">string?</span> Id { <span style="color:#66d9ef">get</span>; <span style="color:#66d9ef">set</span>; }
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">  [SPField(internalName: &#34;Title&#34;)]</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">string?</span> Title { <span style="color:#66d9ef">get</span>; <span style="color:#66d9ef">set</span>; }
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">  [SPField(internalName: &#34;EmployeeTask_Done&#34;)]</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">bool?</span> Done { <span style="color:#66d9ef">get</span>; <span style="color:#66d9ef">set</span>; }
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">  [SPField(internalName: &#34;EmployeeTask_Employeer&#34;)]</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">public</span> FieldLookupValue? Employeer { <span style="color:#66d9ef">get</span>; <span style="color:#66d9ef">set</span>; }
</span></span><span style="display:flex;"><span>} 
</span></span></code></pre></div><p><strong>SPList</strong> - It should be thought of as similar to the Table-attribute (e.g [Table(&ldquo;StudentMaster&rdquo;)]) from EF, but it accepts both a GUID and a title. First, the code will attempt to find the list using the GUID, and if not found, it will try to find it using the title.</p>
<p><strong>SPField</strong> - It should be regarded as the Column-attribute from EF, and similarly, it accepts the internal name of the list field. This will be expanded significantly in the future. One of the ideas I have is to allow you to define whether it should be converted to an enum as well.</p>
<h2 id="how-to-use-it">How to use it</h2>
<p>To use this, it doesn&rsquo;t take much effort as it is neatly packed away. Therefore, only a few methods are exposed/displayed.
However, it must be noted that it is still only a POC, and therefore, not everything is thoroughly tested or handled for errors very gracefully.</p>
<p>A prerequisite for using this is to have the <a href="https://pnp.github.io/pnpcore/">PnP Core SDK</a> installed, as all extensions, etc., are built on PnP&rsquo;s web context and models.</p>
<h3 id="get-list-items">Get list items</h3>
<p>Normally, data would need to be extracted in the following way.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-cs" data-lang="cs"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> list = context.Web.Lists.GetById(<span style="color:#66d9ef">new</span> Guid(...), p =&gt; p.Title, p =&gt; p.Items, p =&gt; p.Fields);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> listItems = list.Items.AsRequested();
</span></span></code></pre></div><p>Now, it is neatly packed away, so data is extracted in this manner. Unlike the previous method, this one parses the data to the specified class model.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-cs" data-lang="cs"><span style="display:flex;"><span>IEnumerable&lt;EmployeeTaskModel&gt; items = context.Web.GetItems&lt;EmployeeTaskModel&gt;(p =&gt; ...);
</span></span></code></pre></div><h3 id="update-a-list-item">Update a list item</h3>
<p>Updating a list item can be done incredibly easily and quickly. Just take a look at this method:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-cs" data-lang="cs"><span style="display:flex;"><span><span style="color:#66d9ef">internal</span> EmployeeTaskModel UpdateEmployeeTask(EmployeeTaskModel employeeTask){
</span></span><span style="display:flex;"><span>  EmployeeTaskModel updatedTask = context.Web.UpdateItem(employeeTask);
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> updatedTask;
</span></span><span style="display:flex;"><span>} 
</span></span></code></pre></div><h2 id="to-summarize">To summarize</h2>
<p>This POC was created to determine if it was feasible to extract data from SharePoint in an easy and efficient manner, similar to what we are accustomed to with EF.</p>
<p>I will continue working on this POC with the hope that one day it will evolve beyond just a proof of concept. Perhaps, in the future, it could become a part of the PnP Core SDK? :star_struck:</p>
]]></content:encoded></item></channel></rss>