| Package | Info |
|---|---|
| Smart.IO.ByteMapper | |
| Smart.IO.ByteMapper.AspNetCore |
Fixed-length binary/text byte array mapping library for .NET with source generator support.
- Attribute-based mapping between C# objects and fixed-length byte arrays
- Source generator produces zero-overhead reader/writer code at compile time
- NativeAOT / trimming compatible
- ASP.NET Core MVC input/output formatter integration
Annotate a class with [Map(size)] to declare the total byte size, then use converter attributes on each property to define its offset and format.
using Smart.IO.ByteMapper;
// Total: 59 bytes (57 data + 2-byte CRLF delimiter)
[Map(59, Delimiter = new byte[] { 0x0D, 0x0A })]
public sealed class SampleData
{
[MapText(0, 13)]
public string Code { get; set; } = default!;
// CodePage 932 = Shift-JIS
[MapText(13, 20, CodePage = 932)]
public string Name { get; set; } = default!;
[MapNumberText<int>(33, 6)]
public int Qty { get; set; }
[MapNumberText<decimal>(39, 10, Style = NumberStyles.Number)]
public decimal Price { get; set; }
[MapDateTimeText<DateTime>(49, 8, "yyyyMMdd")]
public DateTime Date { get; set; }
}Create a static partial class and mark methods with [ByteReader] and [ByteWriter]. The source generator emits the implementation at compile time.
using Smart.IO.ByteMapper;
internal static partial class SampleDataMappers
{
[ByteReader]
public static partial void Read(ReadOnlySpan<byte> source, SampleData target);
[ByteWriter]
public static partial void Write(Span<byte> destination, SampleData source);
}var record = new SampleData
{
Code = "ABC0001",
Name = "Sample",
Qty = 100,
Price = 1234.56m,
Date = new DateTime(2025, 1, 15)
};
// Write object to byte array
var buffer = new byte[59];
SampleDataMappers.Write(buffer, record);
// Read byte array back into object
var readBack = new SampleData();
SampleDataMappers.Read(buffer, readBack);| Attribute | Target types | Description |
|---|---|---|
[MapBinary<T>] |
short, int, long, float, double, decimal, ... |
Binary numeric value; Endian = Big (default) or Little |
[MapByte] |
byte |
Single raw byte |
[MapBytes] |
byte[] |
Raw byte array with optional filler |
[MapText] |
string |
Text with encoding (CodePage), Trim, and Padding |
[MapBoolean] |
bool, bool? |
Single byte; configurable TrueValue / FalseValue / NullValue |
[MapNumberText<T>] |
short, int, long, float, double, decimal |
Number as text with Format, Padding, Style, Culture |
[MapDateTimeText<T>] |
DateTime, DateTimeOffset, DateOnly, TimeOnly (and nullable variants) |
Date/time as text with Format and Style |
Each converter also has a [Map...Member] form (e.g. [MapTextMember]) for describing a profile layout without re-declaring members — see Profile-based layout switching.
[Map] accepts named options in addition to the total size.
// 57 data bytes + 2-byte CRLF delimiter = 59 total
[Map(59, Delimiter = new byte[] { 0x0D, 0x0A })]
public sealed class SampleData { /* ... */ }| Option | Default | Description |
|---|---|---|
Delimiter |
null |
Delimiter bytes written at the tail of each record; occupies the last Delimiter.Length bytes within Size |
UseDelimiter |
true |
When false, the Delimiter is not written even if set |
Encoding, padding, filler, endianness, and boolean byte values are configured per property on the converter attributes (see the Converters table).
In addition to property-level converters, type-level attributes allow defining fixed fillers and constants within the byte layout.
[Map(20)]
[MapFiller(10, 5)] // fill bytes 10–14 with 0x20
[MapConstant(15, new byte[] { 0x01, 0x00 })] // embed constant bytes at offset 15
public sealed class MyRecord { ... }Smart.IO.ByteMapper.AspNetCore adds MVC input/output formatters so controllers can directly consume and produce fixed-length binary streams.
using Smart.IO.ByteMapper.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddByteMapperFormatters(o =>
{
o.SupportedMediaTypes.Add("text/x-fixedrecord");
});
builder.Services.AddControllers();
builder.Services.AddOptions<MvcOptions>()
.Configure<ByteMapperOutputFormatter, ByteMapperInputFormatter>(
(mvc, output, input) =>
{
mvc.OutputFormatters.Insert(0, output);
mvc.InputFormatters.Insert(0, input);
});Add [ByteMapperEndpoint] to the mapper class to register it with the formatter's ByteMapperRegistry.
using Smart.IO.ByteMapper;
using Smart.IO.ByteMapper.AspNetCore;
[ByteMapperEndpoint]
public static partial class SampleDataMappers
{
[ByteReader]
public static partial void Read(ReadOnlySpan<byte> source, SampleData target);
[ByteWriter]
public static partial void Write(Span<byte> destination, SampleData source);
}[Route("api/[controller]/[action]")]
public sealed class RecordController : Controller
{
[Produces("text/x-fixedrecord")]
[HttpGet]
public SampleData[] GetAll() => repository.GetAll();
[HttpPost]
public IActionResult Post([FromBody] SampleData[] values)
{
// values are deserialized from the fixed-length binary request body
return Ok(new { count = values.Length });
}
}When the same entity needs to be serialized with a different byte layout, define a profile class with [MapProfile] and describe each field with class-level [Map...Member] attributes that reference the target members by name. The profile has no members of its own, so the target entity is never duplicated.
// Profile: only Code + Name (35 bytes), targeting the existing SampleData entity
[MapProfile(35)]
[MapTextMember(nameof(SampleData.Code), 0, 13)]
[MapTextMember(nameof(SampleData.Name), 13, 20, CodePage = 932)]
public sealed class SampleDataCodeNameProfile
{
}
// Controller action using the profile
[Produces("text/x-fixedrecord")]
[HttpGet]
[ByteMapperProfile(typeof(SampleDataCodeNameProfile))]
public SampleData[] GetCodeName() => repository.GetAll();Every converter has a matching [Map...Member] form ([MapTextMember], [MapBinaryMember<T>], [MapBooleanMember], …) that takes the target member name as its first argument, followed by the same parameters as the property-level attribute.
[Map] describes an entity's own layout (converter attributes on its properties); [MapProfile] describes a profile layout ([Map...Member] attributes on the class). Keeping them separate avoids confusing the two, and mismatched combinations are reported:
| Situation | Diagnostic |
|---|---|
[Map...Member] used under [Map] |
SBM0015 (warning, ignored) |
property-level converter attributes under [MapProfile] |
SBM0016 (warning, ignored) |
both [Map] and [MapProfile] on one type |
SBM0017 (error) |