Table of Contents
Last updated: 5/15/2025

P21 Importer/Exporter


In this chapter we will look at how we can create an importer/exporter with the help of Eurostep.Lang.Express.Format.P21.dll that contains a general POCO P21 serializer/deserializer. P21 is a file based exchange format so we will create two new classes that inherit from StreamImporter and StreamExporter.

P21 Importer/Exporter

Because the serializer and the deserializer works with any POCO it needs some settings to work with a new model.

public override IEnumerable<BaseObject> Import()
{
    var settings = new P21Settings(new FamilyContractResolver());
    ExchangeFile file = P21Deserializer.Deserialize(Stream, settings);

    if (HeaderValidation)
    {
        ValidateFamilyHeader(file);
    }

    Header = file.HeaderSection;
    return file.DataSection.OfType<BaseObject>();
}

The importer has a header property, this header corresponds to the P21 Header section that all P21 file have and we can see that it is set in the end of the Import method.

var settings = new P21Settings(new SasContractResolver());
file = P21Deserializer.Deserialize(stream, settings);

This part is the deserialization of the P21 file into the object model, and as mentioned there is a need to provide some settings for it to work, the FamilyContractResolver will be covered later.

Header = file.HeaderSection;
return file.DataSection.OfType<BaseObject>();

The last part is extracting the output of the P21 deserialization. And a P21 ExchangeFile has two sections, first is the header section containing metadata about the file and second is the data section. The data section contains all the object instances in the file. The data section is the result to return in our Import method.

Before looking at the FamilyContractResolver let us look at the implementation of the StreamExporter from the same example.

public override void Export(IEnumerable<BaseObject> objects)
{
    var settings = new P21Settings(new FamilyContractResolver());

    var serializer = new P21Serializer(settings);

    var file = new ExchangeFile() { DataSection = objects.ToArray() };

    file.HeaderSection = Header;

    // The user can't override the FILE_SCHEMA and timestamp
    file.HeaderSection.FileSchema.Information.Clear();
    file.HeaderSection.FileSchema.Information.Add("Family { 1 0 }");
    file.HeaderSection.FileName.TimeStamp = DateTime.Now;

    serializer.Serialize(file, Stream);
}

Instead of getting an ExchangeFile we create one, adding the repository objects to the DataSection and also adding metadata to the HeaderSection.

The P21Serializer takes the same P21Settings and FamilyContractResolver as the deserializer in the previous example. After the Serialize method has been called our P21 output stream is created.

IContractResolver


In both the importer and exporter examples the P21Settings required an instance of the FamilyContractResolver. This is an implementation of the IContractResolver interface.

public class FamilyContractResolver : IContractResolver
{
    private const string Namespace = "Demo.Toolbox.Family.Model";

    public P21Contract Resolve(Type type)
    {
        return new P21Contract(type.Name, type);
    }

    public P21Contract Resolve(string name)
    {
        string fullname = string.Format("{0}.{1}", Namespace, name);

        var type = typeof(Male).Assembly.GetType(fullname, false, true);
        if (type == null)
        {
            throw new P21DeserializeException(
                string.Format("Unable to resolve type '{0}'", name));
        }

        return new P21Contract(name, type);
    }
}

As can be seen in the implementation above the IContractResolver defines two Resolve methods, both returning a P21Contract.

(De)Serialize

The P21Contract is the bidirectional contract between P21 and .Net. What this means is that the contract knows what P21 type corresponds to what .Net type. The contract is used by the P21 deserializer and the P21 serializer to know what type or name to use and also the order of properties etc.

The two resolve methods defined by the IContractResolver interface are responsible for resolving the contract from two different angles.

Deserialize Details

The resolve method that takes a string as input is used by the deserializer, the name of the type used in the P21 file is passed as input and the method is responsible to find the .Net type corresponding to that name.

Serialize Details

The resolve method that takes a System.Type as input is used by the serializer, the type passed is the type of the object that will be serialized. The resolve method is then responsible for finding the P21name for that type and create the contract.

In the FamilyContractResolver example the naming used in the P21and the one used in .Net is the same except the casing of the letters. But in some scenarios a lookup table is needed to find the origin name and type.

public P21Contract Resolve(Type type)
{
    return new P21Contract(type.Name, type);
}

If we are working with for example a subset POCO model of what will be in the P21 our Resolve method will not be able to resolve all P21Contracts, in the example above that would result in the P21DeserializeException being raised. Instead of raising that exception Unresolved could be returned, all instances with an Unresolved contract will be ignored and we are able to operate on the sub-set data.

Complex instances


Some P21 files has something called "complex instances", those are P21 instances represented by a combination of many normal P21 instances. With an implementation of the type P21ComplexInstanceConverter it is possible to convert from and to complex instances.

internal class ComplexInstanceConverter : P21ComplexInstanceConverter
{
    public override bool CanConvertFrom(P21ComplexInstance complexInstance)
    {
	...
    }

    public override P21Instance ConvertFrom(P21ComplexInstance complexInstance, IContractResolver resolver)
    {
	...
    }

    public override bool CanConvertTo(P21Instance instance)
    {
	...
    }

    public override P21ComplexInstance ConvertTo(P21Instance instance, IContractResolver resolver)
    {
	...
    }
}

The complex instance convert work in such a way that a complex instance is converted to a normal instance (ConvertFrom method) that is then deserialized to a single .Net class. That .Net class is then converted back to a complex instance with the ConvertTo method.

The P21Instance is an abstract serialized or deserialized instance representation used by the P21 serializer and deserialzer.

The implementation needs to be registered with the P21Settings in order to be used by the serializer/deserializer.

var settings = new P21DeserializeSettings(new PDMSContractResolver());
settings.ComplexInstanceConverters.Add(new VolumeUnitWithConversionBasedUnitComplexInstanceConverter());

Named Type


In P21 there are named types, these are simple types that have been given a different name. This is usually done because the name given has some significance (e.g. the name type MeasuredValue is a double but the name also implies that the value has been obtained by measuring something).

By implementing the P21NamedTypeHandler it is possible to handle how named types are convert from and convert to a POCO model.

internal class PDMSNamedTypes : P21NamedTypeHandler
{
    public override bool IsNamedType(object o)
    {
    }

    public override object ConvertFrom(P21NamedType namedType)
    {
    }

    public override P21NamedType CovertTo(object value)
    {
    }
}

These implementations must be registered with the P21Settings in order to be used by the serializer/deserializer.

var settings = new P21DeserializeSettings(new PDMSContractResolver());
settings.NamedTypeHandler = new PDMSNamedTypes();

Property Value Converter


In some cases there are type declarations in an EXPRESS schema that are represented as an "EXPRESS simple type" that could be represented in a better way within the .NET POCO model.

TYPE DateTimeString = STRING;
WHERE
  XSDDATETIME: SELF LIKE '####-##-##T##:##:##Z';
END_TYPE;

This would be better represented as a System.DateTime in .NET. This is accomplished by implementing a P21PropertyValueConverter. The value converter for the above example could look like this:

public class PsmDateTimeStringValue : P21PropertyValueConverter
{
    public override object ConvertOnWrite(object o)
    {
        if (o is DateTime? && ((DateTime?)o).HasValue)
        {
            o = ((DateTime?)o).Value;
        }

        if (o is DateTime)
        {
            return ((DateTime)o).ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss'Z'");
        }

        return o;
    }

    public override object ConvertOnRead(object o, Type type)
    {
        var value = o as string;
        if (string.IsNullOrWhiteSpace(value))
        {
            if (type == typeof(DateTime?))
            {
                return null;
            }

            if (type == typeof(DateTime))
            {
                throw new NullReferenceException(string.Format("Non optional DateTime can't be null"));
            }
        }
        else
        {
            var f = "yyyy-MM-dd'T'HH:mm:ss'Z'";
            DateTime r;
            if (DateTime.TryParseExact(
                value,
                f,
                CultureInfo.InvariantCulture,
                DateTimeStyles.AdjustToUniversal,
                out r))
            {
                return r;
            }

            if (DateTime.TryParse(value, out r))
            {
                return r;
            }

            throw new FormatException(string.Format("The provided string \"{0}\" can not be parsed to a DateTime, correct format is \"{1}\"", value, f));
        }

        return o;
    }
}

This would convert from a string to a System.DateTime when deserialized and and written as a correctly formatted "date string" when when serialized.

In the POCO model we must indicate that this System.DateTime should be treated as DateTimeString when serializing/deserializing, this is done by decorating the properties that used the DateTimeString with our implemented PsmDateTimeStringValue.

public class Approval : ...
{
    ...
    [PsmDateTimeStringValue]
    public DateTime? PlannedDate { get; set; }

    [PsmDateTimeStringValue]
    public DateTime? ActualDate { get; set; }
}