Strongly typed SharePoint list operations using the repository pattern in F#

24 Dec 2012

This post is part of series:

Part 4 – Strongly typed SharePoint list operations using the repository pattern in F#
Part 3 – Bringing together repository and tree structure in business layer
Part 2 – Creating and working with a custom tree data structure in C#
Part 1 – Strongly typed SharePoint list operations using the repository pattern

As an experiment, I wanted to re-implement the SharePoint repository pattern from Part 1 in F#.

open System
open System.Xml.Linq
open Microsoft.SharePoint
open Microsoft.SharePoint.Taxonomy

type LegalDocument =
    { Title: string
      ApprovalStatus: string
      Locations: seq<string>
      Level: int
      ProcessHierarchy: Map<Guid, string> 
      DocumentId: string
      ParentDocumentId: string
      Url: Uri }

type LegalDocumentRepository() =
    let LinkFilename = "LinkFilename"
    let FileLeafRef = "FileLeafRef"
    let ModerationStatus = "_ModerationStatus"
    let Location = "Location_Legal"
    let ProcessHierarchyLevel = "ProcessHierarchyLevel"
    let ProcessHierarchyLegal = "ProcessHierarchyLegal"
    let DocumentId = "_dlc_DocId"
    let ParentDocumentId = "Parent_x0020_Document_x0020_Id"

    let xn s = XName.Get s

    member __.Query (list: SPList) (caml: XElement): SPListItemCollection =
        let query = 
                ViewFields = ([LinkFilename; 
                               ParentDocumentId] |> List.fold (fun acc fld -> 
                                   acc + XElement(xn "FieldRef", XAttribute(xn "Name", xn fld)).ToString()) ""),
                Query = caml.ToString(),
                ViewAttributes =  @"Scope=""RecursiveAll""")
        list.GetItems query

    member this.GetApprovedDocuments (list: SPList) =
        let caml = 
            XElement(xn "Where",
                XElement(xn "Eq",
                    XElement(xn "FieldRef", XAttribute(xn "Name", ModerationStatus)),
                    XElement(xn "Value", XAttribute(xn "Type", xn "ModStat"), "Approved")))
        this.Query list caml |> this.Map

    member __.Map items =
        let levelMapper o =
            let s = o |> string
            match (s |> String.IsNullOrEmpty) with
            | true -> 0
            | false -> 
                let v = s.Split [|':'|]
                match v.Length with
                | 0 -> 0
                | _ -> v.[0] |> int

        let locationMapper (o: obj) = 
            o :?> TaxonomyFieldValueCollection |> (fun f -> f.Label)

        let processHierarchyMapper (o: obj) =
            let t = o :?> TaxonomyFieldValueCollection
            t |> (fun f -> Guid(f.TermGuid), f.Label)
              |> Map.ofSeq

        let parentDocumentIdMapper o =
            let s = o |> string
            match (String.IsNullOrEmpty s) with
            | true -> ""
            | false -> s.Trim()

        let urlPrefix = items.List.ParentWeb.Url + "/"
        items |> Seq.cast<SPListItem> 
              |> (fun i ->
                    { Title = i.[LinkFilename] |> string
                      ApprovalStatus =  i.[ModerationStatus] |> string
                      Locations = locationMapper i.[Location]
                      Level = levelMapper i.[ProcessHierarchyLevel]
                      ProcessHierarchy = processHierarchyMapper i.[ProcessHierarchyLegal] 
                      DocumentId = i.[DocumentId] |> string 
                      ParentDocumentId = parentDocumentIdMapper i.[ParentDocumentId]
                      Url = Uri (urlPrefix + i.Url) })

let main args =
    use siteCollection = new SPSite "http://intranet"
    use site = siteCollection.OpenWeb "Legal"
    let list = site.Lists.["LegalDocuments"]
    let repository = LegalDocumentRepository()
    let documents = repository.GetApprovedDocuments list

    documents |> Seq.iter (printfn "%A")

In C#, the cast from string to LINQ to XML type is implicit. In F#, casts are generally explicit so we require the small xn helper function. Using a record type over a regular LegalDocument class has the benefit that all members must be explicitly set on creation or the compiler will issue an error. This comes in handy when modifying LegalDocument during the application life-cycle as the compiler will make sure we update all places where it’s used.

Have comments or questions? Please drop me an email or tweet to @ronnieholm.