views:

1358

answers:

4

I have a class that represents credit card details. To represent valid from and expiration months and years I am using four properties of type int:

public int ValidFromMonth { get; set; }
public int ValidFromYear { get; set; }
public int ExpiresEndMonth { get; set; }
public int ExpiresEndYear { get; set; }

I am XML Serializing this class for consumption by a third party. That third party requires my month and year values to be prefixed with a leading zero if the value is less than 10

<validFromMonth>02</validFromMonth>
<validFromYear>09</validFromYear>
<expiresEndMonth>10</expiresEndMonth>
<expiresEndYear>14</expiresEndYear>

Does .NET support any attribution (or is it possible for me to create a custom attribute) that will enforce this rule, possibly using a format string (e.g. {0:00})?

Note: I know that I could add my own string properties that do the formatting internally, and add an [XmlIgnore] attribute to my int properties, but this feels like a second-rate solution.

Edit

After some consideration I am wondering if this is actually just not feasible. Serialization would be no problem, but in order for deserialization to work you would need to un-format the serialized string. In the trivial example above this would be easy, but I am not sure that it could be made to work in the more general case.

Edit2

The XML Schema that defines the two-digit requirement is below.

Simple type definitions:

<xs:simpleType name="CreditCardMonthType">
  <xs:annotation>
   <xs:documentation>Two digit month</xs:documentation>
  </xs:annotation>
  <xs:restriction base="xs:string">
   <xs:minLength value="2" />
   <xs:maxLength value="2" />
  </xs:restriction>
 </xs:simpleType>
<xs:simpleType name="CreditCardYearType">
  <xs:annotation>
   <xs:documentation>Two digit year</xs:documentation>
  </xs:annotation>
  <xs:restriction base="xs:string">
   <xs:minLength value="2" />
   <xs:maxLength value="2" />
  </xs:restriction>
</xs:simpleType>

Credit card definition that uses these types:

<xs:attribute name="ExpiryMonth" type="CreditCardMonthType" use="required">
 <xs:annotation>
  <xs:documentation>Credit/debt card's expiry month.</xs:documentation>
 </xs:annotation>
</xs:attribute>
<xs:attribute name="ExpiryYear" type="CreditCardYearType" use="required">
 <xs:annotation>
  <xs:documentation>Credit/debt card's expiry year.</xs:documentation>
 </xs:annotation>
</xs:attribute>
<xs:attribute name="StartMonth" type="CreditCardMonthType" use="optional">
 <xs:annotation>
  <xs:documentation>Switch card's start month.</xs:documentation>
 </xs:annotation>
</xs:attribute>
<xs:attribute name="StartYear" type="CreditCardYearType" use="optional">
 <xs:annotation>
  <xs:documentation>Switch card's start year.</xs:documentation>
 </xs:annotation>
</xs:attribute>
+3  A: 

This is a lot of code, but it does what you want. The gist is that you can create a new class (LeadingZero in this example) and implement IXmlSerializable to control how you read/write from the XML stream. Hope this helps:

    using System;
    using System.IO;
    using System.Xml.Serialization;

namespace StackOverflow
{
    [Serializable]
    public class LeadingZero : IXmlSerializable
    {
        public int Value { get; set; }

        public LeadingZero()
        {
            Value = 0;
        }

        public LeadingZero(int value)
        {
            this.Value = value;
        }

        public override string ToString()
        {
            return Value.ToString("00");
        }

        #region IXmlSerializable Members

        public System.Xml.Schema.XmlSchema GetSchema()
        {
            return null;
        }

        public void ReadXml(System.Xml.XmlReader reader)
        {
            string s = reader.ReadElementString();
            int i;
            if (int.TryParse(s, out i))
            {
                Value = i;
            }
        }

        public void WriteXml(System.Xml.XmlWriter writer)
        {
            writer.WriteString(Value.ToString("00"));
        }

        #endregion
    }

    [Serializable]
    public class Complex
    {
        public LeadingZero ValidFromMonth { get; set; }
        public LeadingZero ValidFromYear { get; set; }
        public LeadingZero ExpiresEndMonth { get; set; }
        public LeadingZero ExpiresEndYear { get; set; }
    }

    class Program
    {
        static void Main()
        {
            var seven = new LeadingZero(7);

            XmlSerializer xml = new XmlSerializer(typeof(LeadingZero));

            StringWriter writer;

            writer = new StringWriter();
            xml.Serialize(writer, seven);

            string s = writer.ToString();

            Console.WriteLine(seven);
            Console.WriteLine();
            Console.WriteLine(s);

            Console.WriteLine();
            var newSeven = xml.Deserialize(new StringReader(s)) as LeadingZero;
            Console.WriteLine(newSeven ?? new LeadingZero(0));

            var complicated = new Complex()
            {
                ValidFromMonth = new LeadingZero(7),
                ValidFromYear = new LeadingZero(2009),
                ExpiresEndMonth = new LeadingZero(6),
                ExpiresEndYear = new LeadingZero(2010)
            };

            Console.WriteLine();
            writer = new StringWriter();

            xml = new XmlSerializer(typeof(Complex));
            xml.Serialize(writer, complicated);
            s = writer.ToString();
            Console.WriteLine(s);

            var newComplicated = xml.Deserialize(new StringReader(s)) as Complex;
            if (newComplicated != null)
            {
                Console.WriteLine();
                Console.WriteLine("Woo hoo!");
            }

            Console.ReadLine();
        }
    }
}

This is the output that I got:

07

<?xml version="1.0" encoding="utf-16"?>
<LeadingZero>07</LeadingZero>

07

<?xml version="1.0" encoding="utf-16"?>
<Complex xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http:/
/www.w3.org/2001/XMLSchema">
  <ValidFromMonth>07</ValidFromMonth>
  <ValidFromYear>2009</ValidFromYear>
  <ExpiresEndMonth>06</ExpiresEndMonth>
  <ExpiresEndYear>2010</ExpiresEndYear>
</Complex>

Woo hoo!
Erich Mirabal
+2  A: 

OK, ignore my previous code sample (I'll leave it up since it might help somebody else, though). I just remembered you can do this using XmlEnumAttribute:

public enum LeadingZeroMonth
{
    [XmlEnum("01")]
    January,

    ...

    [XmlEnum("12")]
    December
}

and then change your usage to the enum:

public LeadingZeroMonth ValidFromMonth { get; set; }

This is actually a very nice way since you now have an enum for the month (which is really what you should've done from the beginning).

Erich Mirabal
This is excellent - but I think I will still need to use your LeadingZero class approach for years.
Richard Ev
Probably. That's why I left it up. I thought they were different enough to warrant keeping them as separate answers (depending on your needs). I really would see about having them expand the year field to 4 digits. I mean, come on, they are already using a verbose format (XML), just how much do they think they are saving by using only two digits for the year? But I understand if you are stuck using they schema. Either way, these two things should help accomplish what you need.
Erich Mirabal
+1  A: 

This sort of requirement often comes from companies that don't understand XML. Rather than assume this is the case here, I'll ask: did they supply you with XML schema that describes the leading-zero day format? If so, could you post the part of it that defines the day?


EDIT based on edit

Thanks for posting the schema. It confirmed the other thing I was concerned about. Your integers are not integers. Note the <restriction base="xs:string"/>. These are strings, not integers.

John Saunders
I have updated my answer accordingly
Richard Ev
+1  A: 

Disadvatage using XmlEnum is that it can't be nullable

i will Recommand

    [XmlIgnore]
    private int? _startMonth;

    /// <remarks/>
    [XmlAttributeAttribute]
    public string StartMonth
    {
        get { return _startMonth == null ? null : _startMonth.ToString().PadLeft(2, '0'); }
        set { _startMonth = string.IsNullOrEmpty(value) ? (int?)null : int.Parse(value); }
    }

This will allow you to make attribute nullable

Thanks for the information - we just discovered that XmlEnum can't be nullable, which means that this approach is no longer suitable for our needs.
Richard Ev