The PnP Core SDK model
The model in PnP Core SDK is what the SDK users use to interact with Microsoft 365: it defines the model classes (e.g. List), their fields (Title, Description,...) and their operations (e.g. GetAsync or Get). This model has a public part (interfaces) and an implementation (internal, partial classes). In order to translate the model into respective SharePoint REST and/or Microsoft Graph v1.0 or beta queries the model needs to be decorated with attributes. These attributes drive the needed API calls to Microsoft 365 and the serialization of returned responses (JSON) into the model. As a contributor, extending and enriching the model is how you provide functionality to the developers that will be using this SDK.
Where is the code?
The PnP Core SDK is maintained in the PnP GitHub repository: https://github.com/pnp/pnpcore. You'll find:
- The code of the PnP Core SDK in the
src\sdk
folder - Examples of how to use the PnP Core SDK in the
src\samples
folder - Generated code to speed up contribution of new model logic in the
src\generated
folder - The source of the documentation you are reading right now in the
docs
folder
Setting up your environment for building the PnP Core SDK
Starting to code is simple, pull down the code from GitHub and then use either Visual Studio 2019 or Visual Studio Code. More details can be found in our setup article.
General model principles
The model design principles are agnostic to whether the model will be populated via a SharePoint REST or Microsoft Graph call, and therefore starting here to understand the general model principles is advised. Once you understand the model design principles you can learn more about how to decorate the model to work with either SharePoint REST and/or Microsoft Graph. Below picture gives an overview of the used classes in the model based on the Team model implementation:
In the model there are 2 types of classes:
- The majority of the model is built from model classes
- Model classes often live in a collection, so we do have model collection classes
Each of these classes has a public model implemented via interfaces and an internal model implemented via internal partial classes.
Model classes
The model classes are the most commonly used classes in our domain model as they represent a Microsoft 365 object that can be queried via either the SharePoint REST or the Microsoft Graph interface. Samples of model classes are Web, Team, List,...
Public model
The public model is built via public interfaces. Below sample shows the public model for a SharePoint List
/// <summary>
/// Public interface to define a List object of SharePoint Online
/// </summary>
[ConcreteType(typeof(List))]
public interface IList : IDataModel<IList>, IDataModelGet<IList>, IDataModelLoad<IList>, IDataModelUpdate, IDataModelDelete, IQueryableDataModel
{
/// <summary>
/// The Unique ID of the List object
/// </summary>
public Guid Id { get; }
/// <summary>
/// Gets or sets the list title
/// </summary>
public string Title { get; set; }
/// <summary>
/// Gets or sets the description of the list
/// </summary>
public string Description { get; set; }
// Other properties left for brevity
}
Each public model:
- Uses a public interface (e.g.
IList
in our example) with public fields - Uses the
ConcreteType
attribute to define the implementation type that belongs to this interface - Has inline documentation on the model class and fields
- Always implements the
IDataModel<TModel>
interface whereTModel
is the actual interface (e.g.IList
in above sample) - Optionally implements the
IDataModelLoad<TModel>
interface whenever load functionality is needed on this model class (most of the models have this) - Optionally implements the
IDataModelGet<TModel>
interface whenever get functionality is needed on this model class (most of the models have this) - Optionally implements the
IDataModelUpdate
interface whenever update functionality in needed on this model class - Optionally implements the
IDataModelDelete
interface whenever delete functionality is needed on this model class - Optionally implements the
IQueryableDataModel
interface whenever the model supports LINQ querying. This goes hand in hand with using theQueryableDataModelCollection
base class for the model's collection class
The properties in the model use either basic .Net data types, enumerations, other model/collection types or so called complex types:
// Simple .Net type
public string Title { get; set; }
// Enum
public ListReadingDirection Direction { get; set; }
// Other model/collection types
public IListItemCollection Items { get; }
// Complex types (sample comes from the Team model class)
public ITeamFunSettings FunSettings { get; set; }
Note
When a property is read-only you only need to provide a ´get´ in the public model.
Internal implementation
The internal model implementation is what brings the public model to life: this split approach ensures that library consumers only work with the public model, and as such the library implementation can be updated without breaking the public contract with library consumers. Here's a snippet of the List.cs
class:
[SharePointType("SP.List", Uri = "_api/Web/Lists(guid'{Id}')", Update = "_api/web/lists/getbyid(guid'{Id}')", LinqGet = "_api/web/lists")]
[GraphType(Get = "sites/{Parent.GraphId}/lists/{GraphId}", LinqGet = "sites/{Parent.GraphId}/lists")]
internal partial class List : BaseDataModel<IList>, IList
{
public List()
{
// Handler to construct the Add request for this list
AddApiCallHandler = () =>
{
var entity = EntityManager.GetClassInfo(GetType(), this);
var addParameters = new
{
__metadata = new { type = entity.SharePointType },
BaseTemplate = TemplateType,
Title
}.AsExpando();
string body = JsonSerializer.Serialize(addParameters, typeof(ExpandoObject));
return new ApiCall($"_api/web/lists", ApiType.SPORest, body);
};
}
public Guid Id { get => GetValue<Guid>(); set => SetValue(value); }
[GraphProperty("displayName")]
public string Title { get => GetValue<string>(); set => SetValue(value); }
[GraphProperty("description")]
public string Description { get => GetValue<string>(); set => SetValue(value); }
public IFolder RootFolder { get => GetModelValue<IFolder>(); }
[GraphProperty("items", Get = "/sites/{Web.GraphId}/lists/{GraphId}/items?expand=fields")]
public IListItemCollection Items { get => GetModelCollectionValue<IListItemCollection>(); }
// Other properties left for brevity
[KeyProperty(nameof(Id))]
public override object Key { get => this.Id; set => this.Id = Guid.Parse(value.ToString()); }
}
Each model class:
Inherits from the
BaseDataModel<TModel>
class and implementsTModel
Is an internal, partial class
Does have a public default constructor
Can implement event handlers which are used to (see the Event Handlers page for more details):
- Optionally customize the JSON to Model mapping via the
MappingHandler = (FromJson input)
handler - Implement the API call for doing an Add operation via the
AddApiCallHandler = async ()
handler - Optionally implement API call overrides that allow you to update the generated API call before it's sent off to the server. There are these handlers:
GetApiCallOverrideHandler = async (ApiCall apiCall)
,UpdateApiCallOverrideHandler = async (ApiCall apiCall)
andDeleteApiCallOverrideHandler = async (ApiCall apiCall)
- Optionally implement property validation (prevent property updates, alter values) via the
ValidateUpdateHandler = (ref FieldUpdateRequest fieldUpdateRequest)
handler
- Optionally customize the JSON to Model mapping via the
Contains class level attributes that are used to define the requests to Microsoft 365 and serialization of the received data. These attributes are explained in more detail in their respective chapters later on
Has public properties that:
- Use the
GetValue
andSetValue
inherited methods to get and set simple property values - Use the
GetModelValue
and optionallySetModelValue
base class methods to get and set model property values (e.g.IFolder
) - Use the
GetModelCollectionValue
base class method to get a model collection property value (e.g.IListItemCollection
)
- Use the
Has a
Key
property override which can be used to set/get the key value. The Key is used to organize objects in collectionsHas property attributes that are used to define the requests to Microsoft 365 and serialization of the received data. These attributes are explained in more detail in their respective chapters later on
Model specific methods can be foreseen. These methods provide additional operations on the model class
Collection classes
Collection classes contain zero or more model class instances, so for example the ListCollection
will contain zero or more List
model class instances.
Public model
The public model is built via public interfaces. Below sample shows the public model for a SharePoint ListCollection
/// <summary>
/// Public interface to define a collection of List objects of SharePoint Online
/// </summary>
[ConcreteType(typeof(ListCollection))]
public interface IListCollection : IDataModelCollection<IList>, IDataModelCollectionLoad<IList>, IQueryable<IList>, IDataModelCollectionDeleteByGuidId, IAsyncEnumerable<IList>
{
/// <summary>
/// Adds a new list
/// </summary>
/// <param name="title">Title of the list</param>
/// <param name="templateType">Template type</param>
/// <returns>Newly added list</returns>
public Task<IList> AddAsync(string title, int templateType);
/// <summary>
/// Select a list by title
/// </summary>
/// <param name="title">The title to search for</param>
/// <param name="selectors">The expressions declaring the fields to select</param>
/// <returns>The resulting list instance, if any</returns>
public Task<IList> GetByTitleAsync(string title, params Expression<Func<IList, object>>[] selectors);
// Other methods omitted for brevity
}
Each public model interface for a Collection class:
- Uses a public interface (e.g.
IListCollection
in our example) with optionally public methods - Contains the implementation of the methods defined in the public interface
- Has inline documentation on the model class and methods
- Always implements the
IDataModelCollection<TModel>
interface whereTModel
is the actual interface (e.g.IList
in above sample) - Optionally implements the
IDataModelCollectionLoad<TModel>
interface whereTModel
is the actual interface (e.g.IList
in above sample) whenever the collection can loaded - Optionally implements the
IQueryable<TModel>
interface whereTModel
is the actual interface (e.g.IList
in above sample) whenever the model can be queried using linq queries - Optionally implements the
IAsyncEnumerable<TModel>
interface whenever the data in the collection can be asynchronously enumerated - Optionally implements either the
IDataModelCollectionDeleteByGuidId
,IDataModelCollectionDeleteByIntegerId
orIDataModelCollectionDeleteByStringId
interface matching the data type of the collection model's key if you want to offer aDeleteById
method on the model collection. You should only do this if you've also implemented theIDataModelDelete
on the collection's model
Optionally a collection interface defines methods which add behavior to the collection.
Internal implementation
For the internal collection class implementation, we've opted to use internal partial classes. Here's a snippet of the ListCollection.cs
class, which is linq queryable:
internal partial class ListCollection : QueryableDataModelCollection<IList>, IListCollection
{
public ListCollection(PnPContext context, IDataModelParent parent, string memberName = null)
: base(context, parent, memberName)
{
this.PnPContext = context;
this.Parent = parent;
}
// Other methods omitted for brevity
public async Task<IList> AddAsync(string title, int templateType)
{
if (title == null)
{
throw new ArgumentNullException(nameof(title));
}
if (templateType == 0)
{
throw new ArgumentException($"{nameof(templateType)} cannot be 0");
}
var newList = CreateNewAndAdd() as List;
newList.Title = title;
newList.TemplateType = templateType;
return await newList.AddAsync().ConfigureAwait(false) as List;
}
public async Task<IList> GetByTitleAsync(string title, params Expression<Func<IList, object>>[] selectors)
{
if (title == null)
{
throw new ArgumentNullException(nameof(title));
}
return await BaseDataModelExtensions.BaseLinqGetAsync(this, l => l.Title == title, selectors).ConfigureAwait(false);
}
// Other methods omitted for brevity
}
If the collection is not linq queryable the collection class is very simple:
internal partial class TeamAppCollection : BaseDataModelCollection<ITeamApp>, ITeamAppCollection
{ }
Each collection class:
- Inherits from either the
BaseDataModelCollection<TModel>
for regular collections or from theQueryableDataModelCollection<TModel>
class for linq queryable collections and implements the previously created collection interface (e.g.IListCollection
) - Is an internal, partial class
- Implements a specific constructor in case the class inherits from
QueryableDataModelCollection<TModel>
- Can use the
CreateNewAndAdd
collection base class method to create a new instance and add it to the collection
Decorating the model
The model, collections and complex type classes you create can be populated via either SharePoint REST queries, Microsoft Graph queries or both. Depending on the needed query approach you'll need to decorate the model classes and/or fields with properties. It's these properties that drive the automatic query generation.
When you populate your model via SharePoint REST queries then continue here, in case the model is populated via Microsoft Graph continue here.