TOP

Using the API

Applies to: ASN.1/C# 4.5

Intuitive API

Once your schema is compiled into a set of C# files, add a few lines of code to your main() function and you can build your encoding application. The build must reference the encoder/decoder runtime library (asn1csrt.dll), which the generated code depends on.

ASN.1 API

ASN.1 API

The following example contains an application which assumes the MySch.asn schema file that includes a MyMod module with a MyValue value:

namespace SimpleApp
{
    class Program
    {
        static void Main()
        {
            // instantiate encoder/decoder(s)
            var codec = new MySch.BerCodec();

            // encode my value into a buffer
            byte[] buffer = codec.Encode(MySch.MyMod.Values.MyVal);
        }
    }
}

For the most part, the API manipulates two kinds of ASN.1 objects: codecs and PDUs (data).

Namespaces

The OSS ASN.1 Tools for C# API consists of two types of namespaces:

Schema-specific
A set of compiler generated classes for your particular schema, that is, PDUs and values (if any are defined in the schema), including their time-optimized coding routines (MySchema.MyModule, for example).
Oss.Asn1
A set of classes that is independent of a particular schema and implements the generic behavior of ASN.1 types (object identifiers, bit strings), exceptions, primitive types, encoding/decoding routines, and utilities.

Oss.Asn1 classes are implemented as .NET runtime library (an assembly), which your application must include as a reference in order to be successfully built.

Byte Arrays and Streams

The previous example shows you how to encode a value into a buffer; however, you can also encode into a BER file using a System.IO stream and file:

using (Stream berfile = File.Create("PduEncoding.ber"))
     codec.Encode(pdu, berfile);

Data Objects

The representation names match the schema names, and the types follow the ASN.1 types, therefore you can easily access generated data. Primitive types are represented by int, bool, and string, while constructed types (SEQUENCE, CHOICE, ENUMERATED, BIT STRING, etc.) are represented by a wrapper class. For example:

ASN.1 C#
MyMod DEFINITIONS AUTOMATIC TAGS ::= BEGIN
MyPdu ::= SEQUENCE {
-- declaration of PDU fields

ia5str IA5String (SIZE (1..60)),
 
batting-average REAL, --<DECIMAL>--

handedness ENUMERATED {
   left-handed(-1), 
   right-handed(240),
   ambidextrous(0)}, 

ch CHOICE {
     a [2] INTEGER, 
     b [3] BOOLEAN, 
     c [4] BIT STRING },

bstr	BIT STRING 

uStr UniversalString,

. . .
var pdu = new Mysch.MyMod.MyPdu();

// setting PDU fields

pdu.Ia5str = "Ia5str";

pdu.BattingAverage = 250e-3M;

pdu.Handedness = Mysch.MyMod.MyPdu.HandednessType.RightHanded;



pdu.Ch.A = 129;




pdu.Bstr.Set(new byte[] {0xAA,0x55});

pdu.UStr = new int[] {0x1D161}; //U+1D161  MUSICAL SYMBOL     
. . . 

Schema fields marked as DEFAULT or OPTIONAL are represented as nullable types (native .NET types can also be nullable, for example, int?, bool? etc). You are not required to transmit optional or default values, therefore you can omit these fields during encoding/decoding (by setting them to null). Here is an example of a schema and an application using such fields:

ASN.1 C#
Order ::= SEQUENCE 
{
     id  IA5String,
     cnt INTEGER DEFAULT 1,

     tax BOOLEAN OPTIONAL
}
public class Order : Oss.Asn1.BasePdu
{
        public string Id { get; set; }
        public int? Cnt { get; set; }
        public static int DefaultCnt  { get; }
        public bool? Tax { get; set; }
  	. . . .
}

The application can encode/decode by comparing or using the default value of the field:

// Encoding
if (Order.DefaultCnt.Equals(myOrd.Cnt)) // equals the default?
    myOrd.Cnt = null;                   // do not transmit 
codec.Encode(myOrd, stream);

. . . .

// Decoding
codec.Decode(stream, myOrd);
if (myOrd.Cnt == null)            // was Cnt transmitted?
    myOrd.Cnt = Order.DefaultCnt; // No, use default value

NOTE: For performance reasons, codecs do not compare DEFAULT values to decide whether to encode them. It is the application's responsibility to omit these values during encoding (set them to null) so that they are not transmitted.

Using Class Initializers

You can set data during object construction using C# initializer syntax, as illustrated in the following example, which initializes the BIT STRING member of the CHOICE element shown above:

var bits = new MyPdu.ChType() 
{ 
   C = new Oss.Asn1.BitString ( 
       new byte[] {0xAA ,0xAA ,0xA0 }, // bit values
       20 )                            // length 20 bits
};

If you are unsure about how to initialize a certain field, you can declare a value in the schema and then look in the generated Values.cs file.

This example initializes a SEQUENCE OF BIT STRINGs:

ASN.1 C#
-- define  
seqOfbstr SEQUENCE OF BIT STRING
. . .
-- declare a value
seqOfbstr 
{
    '11110000'B, 
    '00001111'B
}
// create an instance of seqOfbstr
var SeqOfbstr = new List() 
{
  new Oss.Asn1.BitString (
        new byte[] {0xF0},8), 
  new Oss.Asn1.BitString (
        new byte[] {0x0F},8)
}

Schema Information

You can add additional ASN.1 schema information (such as exact schema names, optionality, etc.) by using the -genSchemaInfo compiler option. When this option is specified, the compiler adds .NET attributes from both namespaces: native (System.Runtime.Serialization) and OSS-specific (Oss.Asn1Schema). For more information about C# attributes, see the .NET documentation.

The ASN.1 C# compiler generates the following attributes when the -genschemainfo compiler option is specified:

Attribute Description
[DataContract]
Generated for every class that represents SET, SEQUENCE, CHOICE, or ENUMERATED, where:
  • Name - specifies the schema name, if any, of the type.
  • Namespace - specifies the name of the schema module where the corresponding type is defined (omitted if Name is not present).
[CollectionDataContract]
Generated for every class that represents SET OF or SEQUENCE OF, where:
  • Name - specifies the ASN.1 name, if any, of the type.
  • Namespace - specifies the ASN.1 name of the module where the corresponding type is defined (omitted if Name is not present).
  • ItemName - specifies the ASN.1 name, if any, of the element.
[DataMember]
Generated for every C# property that represents a field of SET, SEQUENCE, or CHOICE, where:
  • Name - specifies the schema name, if any, of the field.
  • IsRequred - set to false if the field is OPTIONAL or has a DEFAULT value; otherwise it is set to true. The property is omitted on extension fields.
  • ItemName - specifies the ASN.1 name, if any, of the element.
[EnumMember]
Generated for every member of C# enum that represents ENUMERATED, named numbers, or named bits, where:
  • Value - specifies the schema name of the enumerator.

Open Types

A PDU with an open type contains a field of a type that can be identified at runtime, usually by a special "id" field and/or by an ASN.1 constraint. The open type can be encoded and decoded independently of its containing (outer) PDU and is represented by an inner PDU object wrapped into an oss.asn1.OpenType class. The oss.asn1.OpenType class contains the inner PDU in either encoded (a byte array) or decoded form (a regular PDU class). Therefore the open type field can be encoded/decoded either automatically in a single encode/decode operation performed on the "outer" PDU, or manually by a separate encode/decode operation performed on the "inner" PDU.

Below are examples of manual and automatic encoding and decoding. In these examples, outerPdu.Id is used for type identification with its values predefined in the schema (MyModule.Values.MyId1):

Automatic Encoding

You can encode an open type along with the containing PDU in a single encode operation.

outerPdu.Ot = new OpenType(new MyModule.MyOt1() { . . . }); // Set OT
outerPdu.Id = MyModule.Values.MyId1.Id; // Set the Id for OT	
       codec.Encode(outerPdu, data);
Manual Encoding

You can pre-encode the innerPdu separately or you can use existing (encoded elsewhere) raw bytes:

 // type/id defined in the schema as MyOt1/MyId1
var innerPdu = new MyModule.MyOt1() { . . . };
byte[] buffer = codec.Encode(innerPdu);   // pre-encode OT 
. . . 
outerPdu.Ot = new OpenType(buffer);       // Set encoded OT
outerPdu.Id = MyModule.Values.MyId1.Id;   // Set the Id

or

// "unknown" type, not defined in the schema 
outerPdu.Ot = new OpenType(new byte[] { . . . });      // raw bytes encoded OT
outerPdu.Id = new ObjectIdentifier("1 2 3 4 . . .");   // Set the Id

then

codec.Encode(outerPdu, data);

When decoding, the decoder must know whether to decode the open type field along with the containing PDU (automatic) or to leave the field intact (manual). This is indicated by the AutoDecode option, which the application can set to true or false.

Automatic Decoding

The decoder resolves the type using a lookup table, also known as a component relation constraint, defined in the schema by the information object set. Unresolved types (no match in the table) cause the open type field to remain in the encoded form, which can still be manually decoded later, provided the type is known.

codec.DecoderOptions.AutoDecode = true;
. . .
// decode both PDUs, outer and inner
codec.Decode(data, outerPdu); 
. . .
if (outerPdu.Ot.Decoded == null)
{
// dump bytes in HEX
ValueNotationFormatter.Print(outerPdu.Ot.Encoded, outerPdu.Ot.Encoded.Length);
throw new Exception("received unknown open type");
}            

// use inner PDU
if (outerPdu.Id == MyModule.Values.MyId1.Id) // type MyOt1
                innerPdu = (MyModule.MyOt1) outerPdu.Ot.Decoded;
else if (outerPdu.Id == MyModule.Values.MyId2.Id) // type MyOt2
                innerPdu = (MyModule.MyOt2) outerPdu.Ot.Decoded;

In some complex cases, auto decoding of an open type is not possible, because the decoder is unable to identify the type. These cases are as follows:

  • The open type is constrained by a component relation constraint, but the bytes in the encoding do not provide unique identification (the corresponding information object set does not contain a matching entry or contains multiple matching entries). The following cannot be automatically decoded at this time, but may be in the future.
  • The open type is constrained by a component relation constraint, but the type's encoding bytes come before its identification bytes in the encoding stream.
  • The open type is constrained by a component relation constraint, but is nested inside a SET.
  • The open type is constrained by a component relation constraint, but is nested inside a CONTAINING constraint while identified by a field outside of it.
  • The open type is not constrained by a component relation constraint, but is constrained by a simple ASN.1 type constraint.

In all cases, during automatic decoding of a PDU that contains one or more open types, any open type field can be returned to the application in its encoded form (see OpenType.Encoded and OpenType.Decoded, where the Decoded member will be set to null), so the application can decode the field by providing its type explicitly, for example, codec.Decode <MyModule.MyOt> (outerPdu.Ot).

Manual Decoding

It is useful when no type information is given by the component relation constraint, but the application still knows the type, or when encoding/decoding of the open type is optional and/or deferred.

codec.DecoderOptions.AutoDecode = false;
. . .
// decode outer PDU, leave inner PDU in the encoded form
codec.Decode(data, outerPdu); 
. . .
// deferred decode of the inner PDU 
if (outerPdu.Id == MyModule.Values.MyId1.Id) 
                innerPdu = codec.Decode<MyModule.MyOt1>(outerPdu.Ot); 
else if (outerPdu.Id == new ObjectIdentifier("1 2 3 4 14"))  
                innerPdu = codec.Decode>MyModule.MyOtherOt>(outerPdu.Ot);
else
// unexpected/unknown type, dump bytes in HEX
                ValueNotationFormatter.Print(outerPdu.Ot.Encoded,  outerPdu.Ot.Encoded.Length);

Error Reporting/Exceptions

All errors that occur during encoding, decoding, or any other functionality provided by the API, such as formatted printing, are reported to the application by exceptions. There are only a few exceptions that are thrown by the codec library; they are defined under the Oss.Asn1 namespace.

Asn1InvalidDataException and Asn1InvalidEncodingException indicate a problem with the data being encoded or decoded.

Asn1OutputFullException indicates a problem related to the size of the preallocated output buffer.

exception.Message provides a more detailed description of the error that caused the exception and, when applicable, also includes the responsible PDU field and type. This is also true for exceptions thrown outside the codec library (for example, by the .NET framework), so debugging or troubleshooting is greatly simplified.

Generally, there are two ways an application can handle encoding/decoding exceptions:

  • Handle expected exceptions: catch a specific exception type that the application expects and knows how to handle, for example:
  • byte[] buffer = new byte[10];  // small, but probably fits all the values 
    
           bool encoded = false;
           while (!encoded)
           {
               try
               {
                   codec.Encode(pdu, buffer);
                   encoded = true;
               }
               catch (Asn1OutputFullException e) 
               {
      	           // Oh, bigger than we thought? Well, we know how to handle it.
                   buffer = new byte[buffer.Length * 2];
               }
           }
  • Troubleshoot unexpected exceptions: catch all exceptions while debugging or troubleshoot at a later time by logging. Note that ToString() includes exception details, such as stack trace, etc.
    using (StreamWriter logFile = new StreamWriter("log.txt"))
    {
        try
        {
            // encode/decode
    		. . .
        }
        catch (Exception e)
        {
            logFile.WriteLine(e.ToString());
        }
    }

Encoder/Decoder Limitations

The following limitations apply to the E-XER encoder/decoder:

  1. The encoder does not enforce namespace constraints for ANY-ELEMENT and ANY-ATTRIBUTE fields.
  2. The encoder does not ensure that attributes specified by the ANY-ATTRIBUTES field have unique names.
  3. When it encounters the XMLIdentifierList alternative of a BIT STRING encoding, the decoder throws an exception.
  4. The compiler may fail to detect an invalid use of the DEFAULT-FOR-EMPTY encoding instruction. In this case, the encoder may generate an invalid encoding that does not conform to the X.693 requirements.
  5. The decoder incorrectly decodes the value of a SEQUENCE that has the DEFAULT-FOR-EMPTY and the USE-NIL encoding instructions when the value of the nil attribute is false or the nil attribute is omitted.
  6. An extension field with the UNTAGGED encoding instruction may be decoded incorrectly.
  7. A value of a SET may be decoded incorrectly when one of its fields has the ANY-ELEMENT encoding instruction applied.
  8. When decoding from a Stream or TextReader object, the XML conformance level is always set to fragment. If the document conformance level is preferable, XmlReader should be used as the input source.
  9. When decoding from a Stream or TextReader object, the decoder employs internal buffering. For this reason, the input Stream or the TextReader object should not contain any data other than concatenated XML fragments.

This documentation applies to the OSS® ASN.1 Tools for C# release 4.5 and later.

Copyright © 2017 OSS Nokalva, Inc. All rights reserved.
No part of this publication may be reproduced, stored in a retrieval system, or transmitted in any form or by any means electronic, mechanical, photocopying, recording or otherwise, without the prior permission of OSS Nokalva, Inc.
Every distributed copy of the OSS® ASN.1 Tools for C# is associated with a specific license and related unique license number. That license determines, among other things, what functions of the OSS ASN.1 Tools for C# are available to you.