C# and .NET
OpenBim.Ifc is the .NET binding of the IFC core (openbim-ifc-dotnet), published to NuGet. It is C# over the versioned C ABI, and the package carries the C ABI's native library for every platform it supports.
dotnet add package OpenBim.IfcThe binding exposes the record model over STEP -- parse, read and edit attributes, and write -- plus lenient reads, the file header, validation, ifcXML, the reachability lint, the domain views and writing property sets (see Beyond the record model).
Platforms
| Target framework | Hosts |
|---|---|
net8.0 | .NET 8 and later |
netstandard2.0 | .NET Framework 4.6.2 and later, such as Revit 2024 and older, Navisworks and Tekla; Mono; older .NET |
The package holds the native library for win-x64, win-arm64, linux-x64, linux-arm64, osx-x64 and osx-arm64 under runtimes/<rid>/native/. A .NET application picks its runtime's copy itself. A .NET Framework project, such as an AnyCPU add-in, names no runtime identifier, so the package's build targets copy the Windows libraries into its output, and the assembly loads the one for its process from next to itself. A plug-in a .NET 8 host loads by path finds it the same way. Every library is 64-bit.
Read, edit and write
using var model = IfcModel.Parse(data); // or IfcModel.Open("model.ifc")
var schema = model.Schema; // "IFC4"
foreach (var wall in model.IdsOfType("IfcWall"))
{
var name = (Value.Text)model.Attribute(wall, 2); // Text { Value = Wall }
model.SetAttribute(wall, 2, new Value.Text(name.Value + " (checked)"));
}
byte[] written = model.Write(); // STEP, ready to saveIfcModel owns a native model through a SafeHandle: dispose it, or the finalizer releases it. Calls on one model are serialised by the library, so a model may be shared between threads; calls on different models run in parallel.
Attribute values are a closed record hierarchy, one case per STEP form, so nothing is lost in a round trip: Value.Null is $, Value.Derived is *, Value.Unknown is .U. and is never a Value.Bool, Value.Integer (64-bit) is not Value.Real, and Value.Typed keeps a wrapper such as IFCLENGTHMEASURE(2.5) apart from its payload. Values compare by value, lists included, and print in STEP form. No host value converts implicitly: 3 could be an integer or a real, and "x" a text or an enumeration.
Attributes by name
using var model = IfcModel.Parse(data);
var wall = model.IdsOfType("IfcWall").Single();
// Slots as the declared release (here IFC4) defines them, inherited first.
foreach (var attribute in model.AttributeNames(wall))
{
System.Console.WriteLine($"{attribute.Index} {attribute.Name}: {attribute.TypeName}");
// 0 GlobalId: IfcGloballyUniqueId, 1 OwnerHistory: IfcOwnerHistory, 2 Name: IfcLabel, ...
}
var name = model.AttributeByName(wall, "name"); // any case: 'Wall'
model.SetAttributeByName(wall, "Name", new Value.Text("Renamed"));AttributeNames(id) lists an entity's explicit attributes as AttributeInfo records, in slot order with inherited ones first, as the release its header declares defines them: IfcTask.Status is slot 6 in an IFC2X3 file and slot 7 in an IFC4 one. AttributeByName and SetAttributeByName match a name case-insensitively; the positional calls stay raw slot access. An unknown name is refused with unknown-attribute, a write to a slot the entity's type derives (written *) with derived-attribute, and a refused write changes nothing.
Errors
using var model = IfcModel.Parse(data);
try
{
model.Remove(99);
}
catch (IfcException error) when (error.Code == "missing-entity")
{
// error.Status == IfcStatus.MissingEntity; error.Message names #99
}Every refusal throws IfcException. Its Code is the stable code every binding shares (parse, missing-entity, invalid-value, unsupported-schema, ...), never renamed or reused, and its Status is the C ABI's status. A refused call leaves the model unchanged. A disposed model throws ObjectDisposedException.
Beyond the record model
// A damaged export: skip what cannot be read, and say what was skipped.
using var model = IfcModel.Parse(data, ParseOptions.Lenient);
var skipped = model.Diagnostics; // one message per recovery
var header = model.Header; // Header { Name = ..., Author = [...], Schema = [...] }
model.Header = header with { Author = new EquatableList<string>(new[] { "Reviewer" }) };
var report = model.Validate(); // ValidationReport { Conformant = ..., Findings = [...] }
var errors = report.Findings.Where(finding => finding.Severity == "error").ToList();
var xml = model.WriteIfcXml(); // lossless ifcXML; or xsdProfile: "IFC4"
using var fromXml = IfcModel.ParseIfcXml(xml);- Lenient reads.
IfcModel.ParseandIfcModel.Opentake aParseOptionsrecord:OnMalformed.Skip,CheckReferences,AcceptRealWithoutPoint, or theParseOptions.Lenientpreset. Every recovery is listed inDiagnostics. - Header.
model.Headeris aHeaderrecord with everyFILE_DESCRIPTION,FILE_NAMEandFILE_SCHEMAfield; assign a changed copy (with) to replace it. - Validation.
Validate(maxFindings)checks the model against the schema its header declares and returns aValidationReport: counts by severity,Conformant,Truncated, andValidationFindings sorted by severity, rule, entity and slot. - ifcXML.
WriteIfcXml()andIfcModel.ParseIfcXml(data)use this library's lossless layout;xsdProfile: "IFC4"or"IFC4X3_ADD2"selects the buildingSMART XSD layout, which refuses withwritewhat it cannot carry exactly. - Reachability.
UnreachableProducts()lists products no viewer will draw asUnreachableProducts with a stableReason.
Domain views
using var model = IfcModel.Parse(data);
var wall = model.IdsOfType("IfcWall").Single();
// Property sets: the wall's own first, then its type's; values typed.
foreach (var set in model.PropertySets(wall))
{
foreach (var property in set.Properties)
{
System.Console.WriteLine($"{set.Name}.{property.Name} = {property.Value}");
// Pset_WallCommon.IsExternal = IFCBOOLEAN(.T.)
}
}
var material = model.Material(wall); // MaterialAssignment { Kind = "layer-set", Layers = [...] }
var tree = model.SpatialTree(); // SpatialTree { Nodes = [...] }
var classes = model.Classifications(wall); // [Classification { Identification = ... }]The domain views of the Rust facade cross the C ABI as value tapes and arrive as C# records, keyed by entity id with the GlobalId where the entity has one; lists are EquatableList<T>, and IFC values the cases of Value, typed with their declared type. A view reads the model as it is at the call.
- Property sets.
PropertySets(id)returnsPropertySets: the object's own, then those its type object holds, an occurrence property overriding an inherited one.ResolveUnit(measureType, unit)resolves a property's unit, or the project default, exactly to SI. Read against IFC2X3, IFC4 or IFC4X3. - Spatial tree.
SpatialTree()returns aSpatialTreeofSpatialNodes with parents, children, contained and referenced elements. - Classification.
Classifications(id)returns the object's own and its type'sClassifications with theirClassificationSystem. - Material.
Material(id)returns the oneMaterialAssignmentthat applies, its own or its type's, or null. - Systems.
Systems()returns aSystemsView: everyIfcSystemwith members and served structures, and theSystemAnomalys. - Cost.
Cost()returns theCostSchedules andCostItems with theirCostValuetrees. - Georeferencing.
Georeferencing()returns aMapConversionper coordinate operation (IFC4, IFC4X3), resolved with the project length unit.
Two records are named differently from the other bindings: the core's System is IfcSystem, since a type named System would hide the System namespace, and Systems is SystemsView, since a record cannot have a member named like itself. Refusals carry the shared codes: unsupported-schema, invalid-model, missing-reference, budget-exceeded, unsupported and wrong-entity-type.
Writing property sets
// Wall #3 inherits IsExternal from its type: the write overrides it on
// the wall and never changes the type's shared set.
var holders = model.SetProperties(new[]
{
new PropertyEdit(3, "Pset_WallCommon", "IsExternal", new Value.Typed("IFCBOOLEAN", new Value.Bool(false))),
new PropertyEdit(3, "Custom", "Note", new Value.Typed("IFCLABEL", new Value.Text("checked"))),
});
// holders: per edit, the entity now holding the value
var own = model.PropertySets(3).Single(set => set.Source == "occurrence" && set.Name == "Pset_WallCommon");SetProperties(edits) writes and removes property and quantity values as one checked transaction: every PropertyEdit, in order, or, when any is refused, none, and the model is unchanged. An edit addresses a property the way PropertySets reports it, by object, set name and property name, and its Value is that property's Value. PropertyEdit.Removal(object, set, name) removes one; SetProperty and RemoveProperty are the one-edit forms. The result holds, per edit, the entity now holding the value. Values are checked against the declared release and, for a Pset_/Qto_ set, the release's PSD/QTO catalog, which the native library embeds; the refusals are those of the other bindings (invalid-value, template-violation, missing-property, unsupported, wrong-entity-type).
Not bound yet: geometry, and checked multi-edit transactions over arbitrary entities. Use the Rust crates for those.
Creating entities
// IfcModel.Handle(i): the entity operation i of the batch produces.
var ids = model.Author(new[]
{
AuthorOp.Project(new Dictionary<string, Value> { ["Name"] = new Value.Text("Demo") }), // 0
AuthorOp.Placement(), // 1: at the origin
AuthorOp.Spatial("IfcSite", IfcModel.Handle(0), placement: IfcModel.Handle(1)), // 2
AuthorOp.Spatial("IfcBuilding", IfcModel.Handle(2)), // 3
AuthorOp.Placement(relativeTo: IfcModel.Handle(1)), // 4
AuthorOp.Spatial("IfcBuildingStorey", IfcModel.Handle(3), placement: IfcModel.Handle(4)), // 5
AuthorOp.TypeObject("IfcWallType", new Dictionary<string, Value> { ["PredefinedType"] = new Value.Enum("STANDARD") }), // 6
AuthorOp.Placement(relativeTo: IfcModel.Handle(4), location: (1, 2, 0)), // 7
AuthorOp.Product(
"IfcWall",
new Dictionary<string, Value> { ["Name"] = new Value.Text("Wall") },
container: IfcModel.Handle(5), // IfcRelContainedInSpatialStructure
placement: IfcModel.Handle(7),
typeObject: IfcModel.Handle(6)), // IfcRelDefinesByType
});
var wall = ids[8]!.Value; // every IfcRoot got a GlobalIdmodel.Author(ops), with AuthorOp built by its factory methods, creates and edits entities as one checked transaction against the release the header declares: every operation, in order, or, when any is refused, none, and the model is unchanged. An operation names the entity an earlier operation of the same batch produced by IfcModel.Handle(index), anywhere an id goes, attribute values included, and the result holds per operation the id its entity received. CreateEntity(type, attributes) and RemoveWithRelationships(id) are the one-operation forms. A model built from nothing needs a header naming its release first (assign Header).
| Operation | What it writes |
|---|---|
Create | one entity by type and named attributes |
Edit | named attributes of entity; the whole entity is checked again |
Remove | removes entity and takes it out of every relationship; a relationship left without an end goes too |
Project | the model's one IfcProject |
Spatial | a spatial element of type and its IfcRelAggregates under parent |
Product | a product, its IfcRelContainedInSpatialStructure in container and its IfcRelDefinesByType by typeObject |
TypeObject | a type object (IfcWallType, ...) |
AssignType, Contain, Aggregate | one relationship; an object already related is refused |
Placement | an IfcLocalPlacement over an IfcAxis2Placement3D at location, relative to relativeTo; axis and refDirection both or neither |
OwnerHistory | an IfcOwnerHistory with its person, organization and application |
Every record is built by attribute name through ifc-author against the declared release, and refused with the shared codes: a type the release does not declare (unsupported-schema) or an abstract one (wrong-entity-type); an unknown name (unknown-attribute); a value of the wrong type or form, or an aggregate outside its declared bounds (invalid-value); a required attribute left unset (missing-attribute); a derived one set (derived-attribute); a reference to an entity that does not exist (missing-reference) or of a type the attribute does not accept (wrong-entity-type). An object is contained, aggregated and typed once and a model holds one IfcProject (invalid-model); a removal an entity other than a relationship still needs is still-referenced.
An IfcRoot created without a GlobalId gets a fresh one. OwnerHistory is never invented: a builder writes the one it is given on every record it creates, and IFC2X3, which requires it, refuses a record without (missing-attribute).
How it is tested
The package is tested as NuGet installs it: check-dotnet.py packs the .nupkg, installs it from a local feed into a fresh project and runs the C# suite there, the smoke test of the C binding in C#. The .NET workflow does this on Linux, macOS, Windows (also on .NET Framework 4.8) and Windows on Arm; every release does it on each of the six runtimes before it publishes. On every pull request, the gate holds the C# declarations to the C header and the C# records to the shared records, field by field.
API
Generated from the OpenBim.Ifc C# source.
| Member | Description |
|---|---|
new IfcModel() | An empty model. |
static IfcModel Parse(byte[] data, ParseOptions? options = null) | Parse a STEP (.ifc) file from its bytes; options relaxes the strict read. |
static IfcModel ParseIfcXml(byte[] data, string? xsdProfile = null) | Parse an ifcXML document: this library's lossless layout, or with xsdProfile (IFC4 or IFC4X3_ADD2) the buildingSMART XSD layout of that release. |
static IfcModel Open(string path, bool mapped = false, ParseOptions? options = null) | Read a STEP file from disk, once, straight into the model; io when it cannot be read. |
byte[] Write() | Serialize as STEP bytes. |
byte[] WriteIfcXml(string? xsdProfile = null) | Serialize as ifcXML bytes, in the layout ParseIfcXml reads; an XSD-layout write refuses with write what the layout cannot carry. |
Header Header { get; set; } | The STEP file header; assign a changed copy (with) to replace it. |
ValidationReport Validate(int? maxFindings = null) | Validate against the schema the header declares; findings sorted by severity, rule, entity and slot. maxFindings caps the report (default 10,000). |
IReadOnlyList<UnreachableProduct> UnreachableProducts() | Products no viewer will draw, with a stable reason, in id order. |
IReadOnlyList<PropertySet> PropertySets(ulong id) | The property sets, quantity sets and predefined property sets of object id: its own first, then those its type object holds, an occurrence property overriding an inherited one of the same name. |
ResolvedUnit ResolveUnit(string measureType, ulong? unit = null) | The effective unit of a measureType value (IFCAREAMEASURE): unit when given (a property's stated unit), otherwise the project default, resolved exactly to SI. |
IReadOnlyList<ulong?> SetProperties(IEnumerable<PropertyEdit> edits) | Write and remove property and quantity values as one checked transaction: every edit, in order, or none and the model unchanged. |
const ulong HandleBase = 4611686018427387904UL { get; } | The first id of the handle range (2^62): HandleBase + i names the entity operation i of an Author batch produced. |
static ulong Handle(int index) => | The handle of the entity operation index of an Author batch produces, usable wherever a later operation takes an id. |
IReadOnlyList<ulong?> Author(IEnumerable<AuthorOp> ops) | Apply authoring operations as one checked transaction against the release the header declares: every operation, in order, or none and the model unchanged. |
ulong CreateEntity(string type, IEnumerable<KeyValuePair<string, Value>>? attributes = null) | Create one entity of type from named attributes, checked against the declared release (Author with one AuthorOp.Create); returns its id. |
void RemoveWithRelationships(ulong id) | Remove entity id with the relationships that reference it, leaving nothing dangling; refused with still-referenced while an entity other than a relationship needs it. |
ulong SetProperty(ulong obj, string set, string name, Value value, string? setType = null) | Write one value (SetProperties with one edit); returns the id of the entity holding it. |
void RemoveProperty(ulong obj, string set, string name) | Remove one property from obj's own set (SetProperties with one edit). |
SpatialTree SpatialTree() | The spatial containment tree: every container with its parent, sub-containers and contained elements. |
IReadOnlyList<Classification> Classifications(ulong id) | The classifications of object id: its own, then its type's. |
MaterialAssignment? Material(ulong id) | The material association of object id, its own or its type's, or null. |
SystemsView Systems() | Every system with its members and served structures, and the memberships the reader could not honour. |
Cost Cost() | Every cost schedule and cost item; values as authored, typed. |
IReadOnlyList<MapConversion> Georeferencing() | Every coordinate operation resolved with the project length unit; empty when the model has none. |
int Count { get; } | Number of entities. |
string? Schema { get; } | The first FILE_SCHEMA token, e.g. IFC4, or null. |
IReadOnlyList<string> Diagnostics { get; } | Non-fatal problems found while reading, such as the records a lenient read skipped. |
IReadOnlyList<ulong> Ids() | Every entity id, in file order. |
IReadOnlyList<ulong> IdsOfType(string typeName) | Ids of every entity of exactly typeName (case-insensitive); subtypes are not included. |
IReadOnlyList<ulong> IdsOfTypeIncludingSubtypes(string typeName) | Ids of typeName or any subtype, per the file's schema; unsupported-schema when it is not bundled. |
string TypeOf(ulong id) | The upper-case type name of entity id. |
IReadOnlyList<Value> Attributes(ulong id) | Every attribute of entity id, in declaration order. |
Value Attribute(ulong id, int index) | Attribute index of entity id; Value.Null past the end. |
Value SetAttribute(ulong id, int index, Value value) | Set attribute index of entity id, padding a gap past the end with $; returns the old value. |
IReadOnlyList<AttributeInfo> AttributeNames(ulong id) | Every explicit attribute of entity id in slot order, inherited first, as the release the header declares defines them; INVERSE attributes hold no slot and are not listed. |
Value AttributeByName(ulong id, string name) | Attribute name of entity id, matched case-insensitively (Name) and resolved against the declared release; Value.Null when the record stops before its slot. |
Value SetAttributeByName(ulong id, string name, Value value) | Set attribute name of entity id; returns the old value. A derived attribute is refused with derived-attribute, an unknown name with unknown-attribute, and a refused write changes nothing. |
ulong Add(string typeName, IEnumerable<Value> attributes) | Append an entity of typeName; returns its new id. |
void Remove(ulong id) | Remove entity id, leaving references to it dangling. |
IReadOnlyList<DanglingReference> DanglingReferences() | Every reference to an id the model does not contain. |
bool IsDisposed { get; } | Whether Dispose has released the native model. |
void Dispose() | Release the native model. Every later call throws ObjectDisposedException. |
Attribute values are the cases of the closed record Value:
| Value | Meaning |
|---|---|
Value.Null | $: the attribute is not set. |
Value.Derived | *: derived in a supertype; distinct from $. |
Value.Bool(bool Value) | .T. or .F.. |
Value.Unknown | .U.: the third logical state, never a Bool. |
Value.Integer(long Value) | An integer literal (64-bit). |
Value.Real(double Value) | A real literal; finite, or the library refuses it with invalid-value. |
Value.Text(string Value) | A string literal, decoded. |
Value.Binary(string Value) | A binary literal: its hexadecimal digits, as written in the file. |
Value.Enum(string Value) | An enumeration value, e.g. .ELEMENT.. |
Value.Ref(ulong Id) | A reference to an entity, #42. |
Value.List(EquatableList<Value> Items) | An aggregate, (1,2). |
Value.Typed(string Type, Value Value) | A typed value, IFCLENGTHMEASURE(2.5). |