Extending the Object-Relational Mapping

Applies to: SharePoint Foundation 2010

The object-relational mapping provided by the LINQ to SharePoint provider does not cover every possible scenario in which you might want LINQ access to a Microsoft SharePoint Foundation content database in your business logic. The following are situations in which you may need to extend the mapping.

  • SPMetal can only generate code for the fields of a content type. There is no built-in mapping to the properties of SPListItem objects, such as the Properties property bag or the Attachments property.

  • SPMetal cannot generate code for fields that use a custom field data type.

  • SPMetal cannot read the future, so it cannot map fields (columns) that users will add to lists after your solution is deployed.

  • You may not always be able to rerun SPMetal when the design of a target list is changed and new fields are added to it. For example, other development teams may be targeting the SPMetal-generated code.

For these reasons, the ICustomMapping interface is provided so that you can extend the object-relational mapping.

Extension of a LINQ to SharePoint Solution

The following are the basic tasks for extending the object-relational mapping.

  • Getting the SPListItem properties or new columns mapped to new properties of the content type class.

  • Handling concurrency conflicts with respect to the new columns.

To enable easy accomplishment of these tasks, LINQ to SharePoint provides the ICustomMapping interface. A class that implements this interface can map new columns to new properties. It can also extend the concurrency conflict resolution system to account for the new columns.

Mapping Columns that Use Custom Field Types

In your extension code file, begin by redeclaring the class that represents the content type of the list with the new columns. The class must be marked partial (Partial in Visual Basic). (It must also have been declared partial in the original code file, which typically is generated by SPMetal. The SPMetal tool, which we recommend that you use, automatically declares all content type classes that it generates to be partial.) Do not repeat the attribute decorations from the original declaration, but indicate that the class implements the ICustomMapping interface.

Within the class, declare the three methods of the interface. The following is an example where the content type is called Book.

public partial class Book : ICustomMapping
{
    public void MapFrom(object listItem)
    {
    }

    public void MapTo(object listItem)
    {
    }

    public void Resolve(RefreshMode mode, object originalListItem, object databaseObject)
    {
    }

    // New property declarations go here.

}

Decorate the MapFrom(Object) method with a CustomMappingAttribute attribute. Within this attribute, construct an array of Strings that contain the internal names of the columns and assign the array to the Columns property.

Note

The internal name of a column cannot be obtained in the UI of SharePoint Foundation. You need to obtain it through the object model by using the InternalName property.

The following example shows how the CustomMappingAttribute is used to create an array of internal names for two new columns that have been added to a Books list.

  • ISBN is a column that uses a custom field data type (called ISBNData) that is designed to hold the ISBN number of a book.

  • UPC-A is a column that uses a custom field type (called UPCAData) that is intended to hold structured data that can be used to generate a UPC-A type of bar code for a book. Its internal name is "UPCA".

[CustomMapping(Columns = new String[] { "ISBN", "UPCA" })]
public void MapFrom(object listItem)
{
}

The MapFrom(Object) method is used by LINQ to SharePoint to set property values from the content database. The parameter passed to it is an object that represents a list item fetched from the content database. The method must have a line, for each new column, that assigns the column’s field value to the property that you want to use to represent the column in your extension of the object-relational mapping.

The MapTo(Object) method is used to save a value to the corresponding field of the content database. The parameter passed to it is an object that represents a list item fetched from the content database. The method must have a line, for each property that represents a new column, that assigns the property’s value to the column’s field in the content database.

Important

Do not call the MapFrom(Object) or MapTo(Object) methods from your own code.

The following example shows how to implement these methods.

[CustomMapping(Columns = new String[] { "ISBN", "UPCA" })]
public void MapFrom(object listItem)
{
    SPListItem item = (SPListItem)listItem;
    this.ISBN = item["ISBN"];
    this.UPCA = item["UPCA"];
}

public void MapTo(object listItem)
{
    SPListItem item = (SPListItem)listItem;
    item["ISBN"] = this.ISBN;
    item["UPCA"] = this.UPCA;
}

You can add any other logic you need, such as validation logic, to either method.

Mapping SPListItem Properties

In general you must extend the object-relational mapping if you need to access, in your LINQ to SharePoint code, properties of SPListItem objects other than the fields of the item’s content type. In this scenario, you pass the string "*" as the only member of the Columns property of the CustomMappingAttribute. The logic of the MapFrom(Object) and MapTo(Object) methods simply map the SPListItem property to a corresponding property of your content type class. Writing the calling code will be probably easier if you use the same name for the property in your class as the name of the corresponding property in the SPListItem class. The following is an example.

[CustomMapping(Columns = new String[] { "*" })]
public void MapFrom(object listItem)
{
    SPListItem item = (SPListItem)listItem;
    this.File = item.File;
}

public void MapTo(object listItem)
{
    SPListItem item = (SPListItem)listItem;
    item.File = this.File;
}

The "*" string tells LINQ to SharePoint to get the entire SPListItem object from the database, all of the properties as well as all of the fields. There is one property for which it is not necessary to do this. If the only property of SPListItem that you want to map is Attachments, you can use "Attachments" as a string in the Columns array and LINQ to SharePoint will fetch not only the boolean field (column) "Attachments" but also the Attachments property.

Mapping Columns that Users Add Post-Deployment

Using "*" as the sole item in the Columns array also enables you to map columns that users add to the list after your solution is deployed to members of a Dictionary<TKey, TValue> property in your content type class, where TKey is String and TValue is Object. You do this by mapping the internal name of each field to a dictionary entry with a key of the same name. The following is an example.

[CustomMapping(Columns = new String[] { "*" })]
public void MapFrom(object listItem)
{
    SPListItem item = (SPListItem)listItem;
    foreach (var field in item.Fields)
    {
        this.Properties[field.InternalName] = item[field.InternalName];
    }
}

public void MapTo(object listItem)
{
    SPListItem item = (SPListItem)listItem;
    foreach (var kvp in this.Properties)
    {
        item[kvp.Key] = this.Properties[kvp.Key];
    }
}

Mapping the List Item’s Property Bag

The ICustomMapping methods can also be used to map properties to particular hash table entries within the database field that corresponds to the SPListItem.Properties property. In this scenario, you do not declare a property bag property in your content type class. Instead, declare a separate property for each property in the list item’s property bag that you want to map. Implement the methods of ICustomMapping as shown in the following example.

[CustomMapping(Columns = new String[] { "*" })]
public void MapFrom(object listItem)
{
    this.PreviousManager = ((SPListItem)listItem).Properties["PreviousManager"];
}

public void MapTo(object listItem)
{
    ((SPListItem)listItem).Properties["PreviousManager"] = this.PreviousManager;
}

Managing Concurrency Conflicts for the New Columns

To ensure that your properties are participating in the object change tracking system, be sure that the set accessor of the properties is calling the content type class’s OnPropertyChanging and OnPropertyChanged methods as shown in the following example. These methods are part of the code generated by SPMetal. They handle the PropertyChanging and PropertyChanged events, respectively. The following is an example for one of the columns discussed earlier in this topic that uses a custom field type. Note the custom field type is ISBNData.

private ISBNData iSBN;

public ISBNData ISBN 
{
    get 
    {
        return iSBN;
    }
    set 
    {
        if ((value != iSBN)) 
        {
            this.OnPropertyChanging("ISBN", iSBN);
            iSBN = value;
            this.OnPropertyChanged("ISBN");
        }
    }
}

Note

Do not put a ColumnAttribute on the property declaration.

Implement the Resolve(RefreshMode, Object, Object) method to enlist the new columns in the resolution process for concurrency conflicts. The ObjectChangeConflict.Resolve() and MemberChangeConflict.Resolve() methods check each list item to see if its content type implements ICustomMapping. For those that do, each of these methods calls the ICustomMapping.Resolve(RefreshMode, Object, Object) method. The RefreshMode value that is passed to ICustomMapping.Resolve(RefreshMode, Object, Object) is the same one that was passed to the calling method.

The following is an example of an implementation of ICustomMapping.Resolve(RefreshMode, Object, Object).

Important

The implementation only writes to the properties that represent the new columns that are being mapped by the ICustomMapping methods. Your implementation must follow the same policy. The old properties have already been resolved by the ObjectChangeConflict.Resolve() or MemberChangeConflict.Resolve() method by the time that ICustomMapping.Resolve(RefreshMode, Object, Object) is called. You risk undoing what the two former methods have done, if your implementation writes to any of the old properties.

public void Resolve(RefreshMode mode, object originalListItem, object databaseListItem)
{
    SPListItem originalItem = (SPListItem)originalListItem;
    SPListItem databaseItem = (SPListItem)databaseListItem;

    ISBNData originalISBNValue = (ISBNData)originalItem["ISBN"];
    ISBNData dbISBNValue = (ISBNData)databaseItem["ISBN"];

    UPCAData originalUPCAValue = (UPCAData)originalItem["UPCA"];
    UPCAData dbUPCAValue = (UPCAData)databaseItem["UPCA"];

    if (mode == RefreshMode.OverwriteCurrentValues)
    {
        this.ISBN = dbISBNValue;
        this.UPCA = dbUPCAValue;
    }
    else if (mode == RefreshMode.KeepCurrentValues)
    {
        databaseItem["ISBN"] = this.ISBN;
        databaseItem["UPCA"] = this.UPCA;        
    }
    else if (mode == RefreshMode.KeepChanges)
    {
        if (this.ISBN != originalISBNValue)
        {
            databaseItem["ISBN"] = this.ISBN;
        }
        else if (this.ISBN == originalISBNValue && this.ISBN != dbISBNValue)
        {
            this.ISBN = dbISBNValue;
        }

        if (this.UPCA != originalUPCAValue)
        {
            databaseItem["UPCA"] = this.UPCA;
        }
        else if (this.UPCA == originalUPCAValue && this.UPCA != dbUPCAValue)
        {
            this.UPCA = dbUPCAValue;
        }
    }
} 

In the case of mapping the list item’s property bag, the implementation of Resolve is shown in this example.

public void Resolve(RefreshMode mode, object originalListItem, object databaseListItem)
{
    SPListItem originalItem = (SPListItem)originalListItem;
    SPListItem databaseItem = (SPListItem)databaseListItem;

    string originalPreviousManagerValue = 
                originalItem.Properties["PreviousManager"].ToString();
    string dbPreviousManagerValue = 
                databaseItem.Properties["PreviousManager"].ToString();
    
    if (mode == RefreshMode.OverwriteCurrentValues)
    {
        this.PreviousManager = dbPreviousManagerValue;
    }
    else if (mode == RefreshMode.KeepCurrentValues)
    {
        databaseItem.Properties["PreviousManager"] = this.PreviousManager;
    }
    else if (mode == RefreshMode.KeepChanges)
    {
        if (this.PreviousManager != originalISBNValue)
        {
            databaseItem.Properties["PreviousManager"] = this.PreviousManager;
        }
        else if (this.PreviousManager == originalISBNValue && this.PreviousManager != dbPreviousManagerValue)
        {
            this.PreviousManager = dbPreviousManagerValue;
        }
    }      
}