Friday, November 9, 2012

Serialization of entities (Entity Framework)

I want to tell you about interesting issue I faced when tried to serialize an entity in Microsoft Entity Framework.

I'm going to use Northwind database for demonstration purposes. Suppose you've added the Northwind model to your solution. And you are interested in three entity classes: Customer, Order and Order_Detail. Here is the corresponding diagram:
 You want to serialize particular customer with all his orders and order details. Consider the following code:

using System.Data.Objects;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using System.Xml;
using System.Xml.Serialization;
using Serializing;

namespace EntitiesSerialization
{
    class Program
    {
        static void Main()
        {
            using (var context = new SerializingContext())
            {
                context.ContextOptions.LazyLoadingEnabled = false;
                var query = context.Customers.Include("Orders.Order_Details");
                var customer = query.Where("it.CustomerID = @Id", new ObjectParameter("Id", "ALFKI")).First();                
                var serializer = new XmlSerializer(typeof (Customers), new[] {typeof(Orders), typeof(Order_Details)});
                serializer.Serialize(new XmlTextWriter("test.xml", Encoding.UTF8), customer);                
            }
        }
    }
}

As you can see, I use XmlSerializer to serialize the customer to xml file. Just to make sure that LazyLoading doesn't affect the solution I've turned it off and included all entities explicitly. I pass Order and Order_Details types to XmlSerializer constructor as extraTypes. Unfortunatelly this solution won't work as it should. Result xml file contains only customer fields, no orders and order details are persisted:

<?xml version="1.0" encoding="utf-8"?>
<Customers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <EntityKey>
    <EntitySetName>Customers</EntitySetName>
    <EntityContainerName>SerializingContext</EntityContainerName>
    <EntityKeyValues>
      <EntityKeyMember>
        <Key>CustomerID</Key>
        <Value xsi:type="xsd:string">ALFKI</Value>
      </EntityKeyMember>
    </EntityKeyValues>
  </EntityKey>
  <CustomerID>ALFKI</CustomerID>
  <CompanyName>Alfreds Futterkiste</CompanyName>
  <ContactName>Maria Anders</ContactName>
  <ContactTitle>Sales Representative</ContactTitle>
  <Address>Obere Str. 57</Address>
  <City>Berlin</City>
  <PostalCode>12209</PostalCode>
  <Country>Germany</Country>
  <Phone>030-0074321</Phone>
  <Fax>030-0076545</Fax>
</Customers>

One way to make this work is described here. You can use DataContractSerializer to persist all the data to xml. Like this:

DataContractSerializer serializer = new DataContractSerializer(customer.GetType());
serializer.WriteObject(new XmlTextWriter("test.xml", Encoding.UTF8), customer);

Another option is to use binary formatter. In the following code I perform sequental serialization/deserialization of the customer object:

var binaryFormatter = new BinaryFormatter();
var stream = new MemoryStream();
binaryFormatter.Serialize(stream, customer);
stream.Position = 0;
var result = binaryFormatter.Deserialize(stream);

Thursday, November 1, 2012

Setting the value of new properties for old pages.

PageTypeBuilder is a great tool and I can't imagine developing against EPiServer CMS without it. But it has a well known issue: after a new property is added to page type old pages of this type have this property empty. It becomes worth if new property has required attribute set to true. You can't update old pages without filling this property.

In this post I want to share one possible solution. The idea is simple. Get all pages of certain type, find all required attributes with specified default value. Then iterate through these pages and set the required properties with their default values.

The first thing we need to do is to find all pages of certain type. It can be done using FindPagesWithCriteria method. To create search criteria we will need an ID of page type. So we have to use one of three overloads of PageType.Load method. Of course we can hardcode the guid or name, but more convenient way is to lookup this data. Here is what we've got for now:

using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using EPiServer;
using EPiServer.Core;
using EPiServer.DataAbstraction;
using EPiServer.DataAccess;
using EPiServer.Filters;
using PageTypeBuilder;

public class DefaultValuesSetter<TPageData> where TPageData : PageData
{        
 private const string PageTypeCriteriaName = "PageTypeID";

 private readonly PageType pageType;
 private readonly IList<PageDefinition> targetPageDefinitions;

 public DefaultValuesSetter()
 {
  pageType = GetPageType();
  targetPageDefinitions = GetRequiredDefinitionsWithDefaultValue();
 }

 public virtual IList<TPageData> GetPages()
 {
  if (pageType == null)
  {
   return new List<TPageData>();
  }

  var criteria = new PropertyCriteria
         {
          Condition = CompareCondition.Equal,
          Name = PageTypeCriteriaName,
          Type = PropertyDataType.PageType,
          Value = pageType.ID.ToString(CultureInfo.InvariantCulture),
          Required = true
         };  

  return DataFactory.Instance
   .FindPagesWithCriteria(PageReference.RootPage, new PropertyCriteriaCollection {criteria})
   .OfType<TPageData>()
   .ToList();
 } 

 private static PageType GetPageType()
 {
  var pageTypeId = PageTypeResolver.Instance.GetPageTypeID(typeof (TPageData));
  return pageTypeId.HasValue 
       ? PageType.Load(pageTypeId.Value) 
       : null;
 } 
}

The next thing to deal with is searching for property attributes. As I mentioned before we are interested in required properties with not empty default value. Here is the code:

private IList<PageDefinition> GetRequiredDefinitionsWithDefaultValue()
 {
  return pageType.Definitions
   .Where(def => def.Required && 
    def.DefaultValueType == DefaultValueType.Value && 
    def.DefaultValue != null)
   .ToList();
 }

Finally we need to iterate through all found pages and set their required properties to default values:

public virtual void SetDefaultValues(IList<TPageData> pages)
 {
  foreach (TPageData page in pages)
  {
   var pageData = page.CreateWritableClone();
   foreach (PageDefinition definition in targetPageDefinitions)
   {
    if (pageData[definition.Name] == null)
    {
     pageData[definition.Name] = definition.DefaultValue;
    }
   }

   try
   {
    DataFactory.Instance.Save(pageData, SaveAction.Publish);
   }
   catch (Exception exception)
   {
    // TODO: add logging here    
   }
  }
 }

We're done with infrastructure. Now suppose you've created a new page type class:

[PageType(Name = "TestPageType")]
public class TestPageType : TypedPageData
{
}

You've created a page of this type in CMS and decided to add a property to it:

[PageType(Name = "TestPageType")]
public class TestPageType : TypedPageData
{
 [PageTypeProperty(            
  EditCaption = "Test Property",            
  Type = typeof(PropertyString),
  Required = true,            
  DefaultValue = "My default value",
  DefaultValueType = DefaultValueType.Value)]
 public virtual string TestProperty { get; set; }        
}

To update the page you can use the following code:

var setter = new DefaultValuesSetter<TestPageType>();
var pages = setter.GetPages();
setter.SetDefaultValues(pages);

Happy coding!