Contents
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
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).
The OSS ASN.1 Tools for C# API consists of two types of namespaces:
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.
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);
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.
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<Oss.Asn1.BitString>() { new Oss.Asn1.BitString ( new byte[] {0xF0},8), new Oss.Asn1.BitString ( new byte[] {0x0F},8) } |
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:
|
[CollectionDataContract] |
Generated for every class that represents SET OF or SEQUENCE OF, where:
|
[DataMember] |
Generated for every C# property that represents a field of SET, SEQUENCE, or CHOICE, where:
|
[EnumMember] |
Generated for every member of C# enum that represents ENUMERATED, named numbers, or named bits, where:
|
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):
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);
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.
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:
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 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);
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:
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]; } }
using (StreamWriter logFile = new StreamWriter("log.txt")) { try { // encode/decode . . . } catch (Exception e) { logFile.WriteLine(e.ToString()); } }
The following limitations apply to the E-XER encoder/decoder:
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
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.
Only the BER, DER, PER, UPER, CPER, CUPER, OER, and COER binary encoding rules support partial decoding.
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:
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.Continue; } 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.
Note: Partial decoding is a chargeable feature in non-evaluation licenses. Contact Sales to obtain pricing information.
The ASN.1/C# Tools can parse ASN.1 value notation data into C# objects that represent values defined by value notation.
To parse values specified in ASN.1 value notation format:
The Bcas.asn schema file is used in this example.
BCASModule DEFINITIONS ::= BEGIN BBCard ::= SEQUENCE { name IA5String (SIZE (1..60)), team IA5String (SIZE (1..60)), age INTEGER (1..100), position IA5String (SIZE (1..60)), handedness ENUMERATED {left-handed(0), right-handed(1), ambidextrous(2)}, batting-average REAL } END
The following source code shows the sample C# application that parses the BBCard ASN.1 type values:
namespace SimpleApp { class Program { static void Main() { // The value notation for the value of the BBCard type string myCard = @"{ name ""Casey"", team ""Mudville Nine"", age 32, position ""left field"", handedness ambidextrous, batting-average {mantissa 250, base 10, exponent -3} }"; // Instantiate the ValueNotationParser var parser = new Bcas.ValueNotationParser(); // Parse the value notation contained in the myCard string var myCardPdu1 = parser.ParseValue(myCard, new Bcas.BCASModule.BBCard()); // Print the C# object parsed from the value notation in the // myCard string System.Console.WriteLine("myCardPdu1 " + myCardPdu1); // Create the string containing the ASN.1 value assignment for the // value of BBCard string myCardAssignment = "myCard BBCard ::= " + myCard; // Parse the value assignment contained in the myCardAssignment // string. The parser will detect the type of the value from the // ASN.1 type name specified in the value assignment (BBCard). var myCardPdu2 = parser.ParseValueAssignment(myCardAssignment); // Print the C# object parsed from the value assignment in the // myCardAssignment string System.Console.WriteLine("myCardPdu2 " + myCardPdu2); // Parse the value assignment contained in the myCardAssignment // string. The type of the value is specified explicitly by the // empty instance of the BBCard class. var myCardPdu3 = parser.ParseValueAssignment(myCardAssignment, new Bcas.BCASModule.BBCard()); // Print the parsed C# object. System.Console.WriteLine("myCardPdu3 " + myCardPdu3); // Parse the input containing multiple values of the BBCard type string myCards = @"-- first value { name ""Casey"", team ""Mudville Nine"", age 32, position ""left field"", handedness ambidextrous, batting-average {mantissa 250, base 10, exponent -3} } -- second value { name ""Doug"", team ""Octopus Eight"", age 27, position ""right field"", handedness right-handed, batting-average {mantissa 250, base 10, exponent -3} }"; // Instantiate the ValueNotationReader using (var reader = new Bcas.ValueNotationReader( new System.IO.StringReader(myCards))) { Oss.Asn1.BasePdu myCardPdu; // Parse all the values contained in the myCards while ((myCardPdu = reader.ReadValue(new Bcas.BCASModule.BBCard())) != null) System.Console.WriteLine("myCardPdu " + myCardPdu); } } } }
The following methods automatically determine the value type from the ASN.1 type name specified in the ASN.1 value assignment:
The following methods allow you to specify the value type explicitly. The value type is identified by the "BasePdu" parameter, which is an empty instance of the C# class that represents the value type:
Type ":" Valueonly the "typereference" alternative of the "Type" production is supported. Also, the type specified by "typereference" must be a PDU.
NOTE: The ASN.1 value notation parser generated by the -avn option is a chargeable feature in non-evaluation licenses. Contact Sales to obtain pricing information.
The -genSizeValueRangeAttributes option instructs the ASN.1 compiler to generate custom SizeAttribute and ValueRangeAttribute attributes from the Oss.Asn1 namespace. The attributes include the effective minimum and maximum values of permitted counts, lengths, and integer values that are computed based on the subtype constraints present in the input ASN.1 schema.
SizeAttribute is generated for the following types that have effective size constraints:
ValueRangeAttribute is generated for INTEGER types that have effective value range constraints and are not marked with the HUGE directive.
The SizeAttribute and ValueRangeAttribute attributes are attached to C# properties that represent fields associated with ASN.1 types that have size or value range constraints. Subtype constraints applied to elements of SEQUENCE OF and SET OF types are mapped to the attributes attached to the corresponding parent collection class.
Attribute | Description |
---|---|
SizeAttribute |
Generated for:
|
ValueRangeAttribute |
Generated for:
|
Attributes can be retrieved by an application using C# Reflection.
using System; using System.Reflection; using System.Collections.Generic; using System.Text; using System.IO; using Oss.Asn1; using Bcas; using Bcas.BcasModule; public static class TestCustomAttributes { . . . public static int Main(string[] args) { try { PropertyInfo[] props = typeof(BBCard).GetProperties(); foreach (PropertyInfo prop in props) { ProcessAttributes( prop, Attribute.GetCustomAttributes(prop) ); // Custom attributes associated with Size and ValueRange constraints applied to // elements of SEQUENCE OF and SET OF types are attached to the parent collection // class as a target. Type propType = prop.PropertyType; if (propType.BaseType.Namespace == "System.Collections.Generic" && propType.GetProperty("Item") != null) { // Get the property of the collection element. PropertyInfo itemProp = propType.GetProperty("Item"); // Access the element's constraints from the attributes applied // to the parent's collection class. ProcessAttributes( itemProp, Attribute.GetCustomAttributes(propType) ); } } } catch (Exception e) { Console.WriteLine("ERROR: " + e.Message + "\n"); } return 0; } }
Where the ProcessAttributes method may look like follows:
. . .
public static void ProcessAttributes (PropertyInfo prop, Attribute[] attributes)
{
Type propType = prop.PropertyType;
foreach (Attribute attr in attributes)
{
if (attr.GetType() == typeof(SizeAttribute))
{
SizeAttribute szAttr = (SizeAttribute)attr;
int szmin = szAttr.Min;
int szmax = szAttr.Max;
. . .
}
else if (attr.GetType() == typeof(ValueRangeAttribute))
{
ValueRangeAttribute vrAttr = (ValueRangeAttribute)attr;
if (vrAttr.OperandType == typeof(int))
{
int vrmin = (int)vrAttr.Min;
int vrmax = (int)vrAttr.Max;
. . .
}
else if (vrAttr.OperandType == typeof(long))
{
long vrmin = (long)vrAttr.Min;
long vrmax = (long)vrAttr.Max;
. . .
}
else if (vrAttr.OperandType == typeof(ulong))
{
ulong vrmin = (ulong)vrAttr.Min;
ulong vrmax = (ulong)vrAttr.Max;
. . .
}
}
}
}
This documentation applies to the OSS® ASN.1 Tools for C# release 5.3 and later.
Copyright © 2024 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.