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
.
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
.
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.
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.
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 P21
name for that type and create the contract.
In the FamilyContractResolver
example the naming used in the P21
and 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 P21Contract
s, 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; }
}