TOP

Using the API

Applies to: ASN.1/C# 5.0

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

Manual decoding 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.
  10. The following ASN.1 types are currently not supported by CPER: SET OF, GeneralString, and GraphicString. The runtime throws an exception on an attempt to encode or decode a value of these types.

Partial Decoding

The partial decoding feature enables decoding of particular fields from input messages while the remaining fields are ignored. Using the partial decoding feature, you can

  • Extract data without writing code to access deeply nested fields of complex PDUs.
  • Reduce the memory footprint of applications that contain minimal information in their PDUs.

The DecodePartial() method of a Codec object does not return the decoded PDU value. Instead, it invokes user-defined callback methods when decoding each field marked by the OSS.DataCallback or OSS.InfoCallback compiler directive and, optionally, passes the decoded field value to it. Your callback method can analyze the decoded value, and by setting the return code, instructs the decoder to either terminate or continue decoding.

Remarks

Only the BER, DER, PER, UPER, CPER, CUPER, OER, and COER binary encoding rules support partial decoding.

Example

For the following ASN.1 syntax, the ZIP code nested within homeAddress will be extracted while the ZIP code nested within the company address is ignored:

M DEFINITIONS AUTOMATIC TAGS ::= BEGIN
   Subscriber ::= SEQUENCE {
     name VisibleString,
     company Company,
     homeAddress Address
   }
   Company ::= SEQUENCE {
     name VisibleString,
     address Address
   }
   Address ::= SEQUENCE {
     zipcode INTEGER( 0..99999 ),
     addressline VisibleString( SIZE (1..64) )
   }
END

First, apply the following directives and compile the specification with either the -enablePartialDecoding or -partialDecodeOnly option.

--<OSS.DataCallback M.Address.zipcode "MyZipcode">--
--<OSS.InfoCallback M.Subscriber.homeAddress "HomeAddressField">--

The options instruct the compiler to generate the PartialContentHandler interface with the following callback method declarations:

Oss.Asn1.ContentHandlerResponse BeginMyZipcode();
Oss.Asn1.ContentHandlerResponse EndMyZipcode(int Value);
Oss.Asn1.ContentHandlerResponse BeginHomeAddressField();
Oss.Asn1.ContentHandlerResponse EndHomeAddressField();

The Oss.Asn1.ContentHandlerResponse C# enum type defines possible return codes from the callback methods:

  • Continue - Continue with normal processing.
  • Skip - If returned from a "Begin" callback, skip to the end of the field, suppressing further callbacks that could occur inside the field, including the "End" field callback. If returned from an "End" callback, skip to the end of the PDU, consuming all the encoded stream data without any further callback invocations.
  • Abort - Terminate partial decode procedure and return to the caller immediately.

Then, define the generic subclass of Oss.Asn1.ContentHandler and the generated PartialContentHandler interface and implement the methods as follows:

class MyPartialContentHandler<T> : Oss.Asn1.ContentHandler<T>, 
PartialContentHandler where T : Oss.Asn1.BasePdu, new()
{
     private bool InsideHomeAddress = false;

         public Oss.Asn1.ContentHandlerResponse BeginMyZipcode()
         {
             return InsideHomeAddress ? 
Oss.Asn1.ContentHandlerResponse.Continue : 
Oss.Asn1.ContentHandlerResponse.Skip;
         }
         public Oss.Asn1.ContentHandlerResponse EndMyZipcode(int Value)
         {
         // Consume ZIP code
         // ...
         return Oss.Asn1.ContentHandlerResponse.Continue;
         }
         public Oss.Asn1.ContentHandlerResponse BeginHomeAddressField()
         {
             InsideHomeAddress = true;
         return Oss.Asn1.ContentHandlerResponse.Skip;
         }
         public Oss.Asn1.ContentHandlerResponse EndHomeAddressField()
         {
         return Oss.Asn1.ContentHandlerResponse.Skip;
         }
}

Finally, invoke the partial decode operation as follows:

codec.DecodePartial(source, new MyPartialContentHandler<Subscriber>());

Alternatively, you can apply OSS.InfoCallback to Subscriber.company rather than Subscriber.homeAddress:

--<OSS.InfoCallback M.Subscriber.company "Company">--

Define the content handler as follows:

class MyPartialContentHandler<T> : Oss.Asn1.ContentHandler<T>, 
PartialContentHandler where T : Oss.Asn1.BasePdu, new()
{
         public Oss.Asn1.ContentHandlerResponse BeginMyZipcode()
         {
         return Oss.Asn1.ContentHandlerResponse.Continue;
         }
         public Oss.Asn1.ContentHandlerResponse EndMyZipcode(int Value)
         {
         // Consume ZIP code
         // ...
         return Oss.Asn1.ContentHandlerResponse.Continue;
         }
         public Oss.Asn1.ContentHandlerResponse BeginCompany()
         {
         return Oss.Asn1.ContentHandlerResponse.Skip;
         }
         public Oss.Asn1.ContentHandlerResponse EndCompany()
         {
         return Oss.Asn1.ContentHandlerResponse.Continue;
         }
}

You would get the same result: the homeAddress ZIP code is extracted and the company address ZIP code is ignored.

See Also


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

Copyright © 2020 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.