ProConcepts Geodatabase

Rich Ruh edited this page Jun 27, 2018 · 26 revisions

Functionality that uses the fine-grained Geodatabase API. Geodatabase API functionality is found in ArcGIS.Core.dll. The Geodatabase API is commonly used in conjunction with map exploration, map authoring, and editing.

Language:      C# and Visual Basic
Subject:       Geodatabase
Contributor:   ArcGIS Pro SDK Team <>
Organization:  Esri,
Date:          5/10/2018
ArcGIS Pro:    2.2
Visual Studio: 2015, 2017

In this topic


  • The ArcGIS.Core.Data API is a DML-only (Data Manipulation Language) API. This means that all schema creation and modification operations such as creating tables and feature classes, creating and modifying fields, enabling attachments, and so on, need to be performed using the Geoprocessing API.

  • Almost all of the methods in the ArcGIS.Core.Data API should be called on the Main CIM Thread (MCT), as stated in the API reference. These method calls should be wrapped inside the QueuedTask.Run call. Failure to do so will result in ConstructedOnWrongThreadException being thrown.

Resource Management

In the majority of cases you can safely rely on the built-in garbage collection provided by .NET to handle memory management issues. The garbage collector, when it runs, reclaims all the memory from “dead” .NET objects as well as compacting the memory used by “live” objects to reduce the size of the managed heap (you can read more about garbage collection here). However, because the Pro SDK Core.Data API uses unmanaged resources (i.e. resources not managed by garbage collection), they must be explicitly released by the application. Unmanaged resources include file locks, database connections, and network connections, amongst others.

There are two patterns in .NET for handling unmanaged resources, an implicit control pattern that uses Object.Finalize, and an explicit control pattern that uses IDisposable.Dispose. The ArcGIS.Core.Data API provides both. The preferred pattern is to explicitly release the underlying unmanaged resources by calling Dispose once you are done using them. Dispose frees up the unmanaged resources releasing any underlying file locks or active database connections (You can also use a “using” construct that will call Dispose for you - there are many examples of “using” in the Pro snippets and samples).

The implicit control pattern that uses Finalize should be considered a fail-safe to allow unmanaged resources to still be freed even if the developer forgets to call Dispose or chooses not to. However, the unmanaged resources will not be freed until the object is garbage collected (at some future point in time) and so any file locks or database connections held by the object will remain in use until it is finalized. Depending on the implicit release of unmanaged resources can lead to unexpected behavior, such as resources still being locked, or connections consumed even though the object that acquired them has since gone out of scope.

Although the usage of Dispose may appear obvious, developers should be careful about code that unintentionally acquires Core.Data instances. For example, consider code that chains together sequences of .NET calls to navigate across the geodatabase hierarchy. The following two statements that appear to be convenient and compact are actually problematic:  

Geodatabase gdb = featureLayer.GetFeatureClass().GetDatastore() as Geodatabase;


var id = row.GetTable().GetID();

In the first case, a feature class instance is acquired and in the second case, a table instance is acquired and neither of them is disposed.  The developer is probably unaware that they indirectly instantiated a feature class and/or table instance that will hold on to their unmanaged resources until such a time as they are garbage collected. Instead, the correct way to code these statements is to explicitly acquire the Core.Data instances as variables and Dispose of them after their use. A using statement is a convenient way of accomplishing this:  

using (FeatureClass featureClass = featureLayer.GetFeatureClass())
using (Geodatabase gdb = featureClass.GetDatastore() as Geodatabase)
  // etc.
using (Table table = row.GetTable())
  var id = table.GetID();
  // etc.

Another benefit to the using statement is that it will ensure Dispose is called even if your code, executing within the scope of the using, throws an exception.

Another use case to consider is when an API method returns a list of Core.Data objects. For example, consider Table.GetControllerDatasets(), which returns an IReadOnlyList<Dataset>. In this case, a using statement is not sufficient, and each of the list items must be individually disposed.

  IReadOnlyList<Dataset> controllerDatasets = table.GetControllerDatasets();

  // Do something with the list

  foreach (Dataset dataset in controllerDatasets)

Once Dispose has been called on an instance, calling any of its methods or properties that access the unmanaged resource(s) (which is just about all of them) will result in an ArcGIS.Core.ObjectDisconnectedException.


A datastore is a container of spatial and non-spatial datasets, such as feature classes, raster datasets, and tables.

In the ArcGIS.Core.Data API, Datastore is an abstract class that represents any object that serves as a container for datasets. For example, Geodatabase inherits from Datastore and supports the file geodatabase, enterprise geodatabase as well as web geodatabase (i.e., feature service) Datastore types.

Note: Datastore is an abstraction that is conceptually equivalent to Workspace in the ArcObjects API.


Conceptually, an ArcGIS geodatabase is a collection of geographic datasets of various types held in a common file system folder, administered by a REST service, or stored in a multiuser relational DBMS (such as Oracle, Microsoft SQL Server, PostgreSQL, SAP HANA, or IBM DB2). Geodatabases come in many sizes, have varying numbers of users, and can scale from small, single-user databases built on files up to larger workgroup, department, and enterprise geodatabases accessed by many users.

In the ArcGIS.Core.Data API, the Geodatabase class represents the native data structure for ArcGIS and is the primary data format used for editing and data management. While ArcGIS works with geographic information in numerous geographic information system (GIS) file formats, it is designed to work with and leverage the capabilities of the geodatabase.

File geodatabases, enterprise geodatabases and web geodatabases (i.e., feature service) can be opened using the Geodatabase class, which exposes an overloaded list of constructors to support different types of geodatabases.

To open a file geodatabase, an instance of FileGeodatabaseConnectionPath should be passed to the Geodatabase constructor as follows:

Geodatabase fileGeodatabase = new Geodatabase(new FileGeodatabaseConnectionPath(new Uri(@"path\to\the\file\geodatabase")));

There are two ways to open an enterprise geodatabase, via an SDE connection file or a set of connection properties:

Geodatabase enterpriseGeodatabaseViaConnectionFile = new Geodatabase(new DatabaseConnectionFile(new Uri(@"path\to\the\sde\file")));

DatabaseConnectionProperties connectionProperties = new DatabaseConnectionProperties(EnterpriseDatabaseType.SQLServer)
  AuthenticationMode = AuthenticationMode.DBMS,
  Instance           = "machineName\\instanceName",
  Database           = "databaseName",
  User               = "username",
  Password           = "Not1234"
  Version            = "dbo.DEFAULT"

Geodatabase enterpriseGeodatabaseViaConnectionProperties = new Geodatabase(connectionProperties);

As an aside, if you have a connection file and wish to extract the connection properties, this can be accomplished with the static method DatabaseClient.GetDatabaseConnectionProperties(). This can be useful if you wish to replace the User and Password properties inside a connection file.

Another way of obtaining the Geodatabase object is through ArcGIS.Core.Data.Dataset.GetDatastore(). This returns the ArcGIS.Core.Data.Datastore reference. If the underlying datastore is a geodatabase, you can cast it to a Geodatabase to access the Geodatabase interface.


QueryDef is a construct that allows for querying the Geodatabase Datastore to obtain a cursor. QueryDefs can be used to generate a cursor from a single table based on a query with a where clause, prefix clause, postfix clause, subfields, and the table on which the query should be executed. They can also be used to create a join between two or more tables within the geodatabase.

The QueryDef class provides the properties to specify the query details. The Evaluate method on the Geodatabase class takes a QueryDef object and returns the RowCursor providing access to the rows that satisfy the query. The Evaluate method also has a Boolean parameter to specify whether recycling should be used while returning the successive rows from the cursor.

QueryDef queryDef = new QueryDef
  Tables      = "Highways",
  WhereClause = "TYPE = 'Paved Undivided'",

using (RowCursor rowCursor = geodatabase.Evaluate(queryDef, false))
  while (rowCursor.MoveNext())
    using (Row row = rowCursor.Current)
      Feature feature = row as Feature;
      Geometry shape  = feature.GetShape();

      string type = Convert.ToString(row["TYPE"]); // Will be "Paved Undivided" for each row.
        Table table = row.GetTable(); // Will always throw exception because rows retrieved from QueryDef do not have a parent table.
      catch (NotSupportedException exception)
        // Handle not supported exception.

The following are some things to consider when performing a QueryDef evaluation:

  • There cannot be more than one column of the same name in the subfields in the QueryDef for enterprise geodatabases.
  • Field Aliases are not supported in the Subfields property on the QueryDef.
  • When there is a Shape field specified in the subfields, the objects returned from the RowCursor.Current are Feature objects.
  • When there is a join involved, only the Shape field of the left side table in the join is supported for QueryDef evaluation.

Note: Since geometries are immutable, they are managed by the .NET Garbage Collector. They are not recyclable in the same sense as Row, which implements IDisposable (recycling expects that the processing for the object is completed before the memory is reclaimed, and this becomes impossible in a multithreaded environment since it can be used concurrently). In future releases, support may be added for recycling geometries.


Query tables are virtual tables that represent queries involving one or more tables from the same geodatabase. When the query table is created, it is returned as a read-only table or feature class depending on whether or not a shape column is involved. Some of the intended primary uses for the query table are adding the query table to the map as a layer, using it as any other read-only table or feature class to perform a search, and using the table to work with geoprocessing analysis tools. Selections against QueryTables are not supported on enterprise geodatabases. When added to the map, query tables are persisted within the project when saved. If changes are made to the tables involved in the query, they are reflected in the query table since it is a virtual table.

To create a query table, the first step is to create a QueryDef. The QueryDef is then used to create a QueryTableDescription. Other properties that can be set include the name of the query table, and a comma-delimited list of key fields to manufacture the ObjectID. When the MakeCopy boolean property is set to true, and the key fields are not set, a local client-side copy of the data is used to generate the query table. Finally, the QueryTableDescription is used to obtain an ArcGIS.Core.Data.Table by invoking Geodatabase.OpenQueryTable(QueryTableDescription).

using (Geodatabase geodatabase = new Geodatabase(new FileGeodatabaseConnectionPath(new Uri("path\\to\\gdb"))))
  QueryDef queryDef = new QueryDef
    Tables    = "CommunityAddress JOIN MunicipalBoundary on CommunityAddress.Municipality = MunicipalBoundary.Name",
    SubFields = "CommunityAddress.OBJECTID, CommunityAddress.Shape, CommunityAddress.SITEADDID, CommunityAddress.ADDRNUM, CommunityAddress.FULLNAME, CommunityAddress.FULLADDR, CommunityAddress.MUNICIPALITY, MunicipalBoundary.Name, MunicipalBoundary.MUNITYP, MunicipalBoundary.LOCALFIPS",

  QueryTableDescription queryTableDescription = new QueryTableDescription(queryDef)
    Name        = "CommunityAddrJounMunicipalBoundr",
    PrimaryKeys = geodatabase.GetSQLSyntax().QualifyColumnName("CommunityAddress", "OBJECTID")
  Table queryTable = geodatabase.OpenQueryTable(queryTableDescription);


The Database Datastore represents an enterprise database or a SQLite database. It provides access to database capabilities such as opening tables and feature classes, obtaining definitions, listing tables, and so on, but it does not support geodatabase functionality such as versioning, archiving, validation, relationship classes, and so on. While an enterprise geodatabase could be opened through the Database Datastore, this approach is not recommended because it would only provide access to the capabilities associated with the database.

An instance of Database Datastore can be opened using one of the three constructors that take the following type of Connector -- DatabaseConnectionProperties, DatabaseConnectionFile or SQLiteConnectionPath.

DatabaseConnectionProperties specifies the enterprise database platform (specified by the EnterpriseDatabaseType enum), and the connection details specific to that platform can be specified using the properties on DatabaseConnectionProperties.

// SQL Server
DatabaseConnectionProperties databaseConnectionProperties = new DatabaseConnectionProperties(EnterpriseDatabaseType.SQLServer)
  AuthenticationMode = AuthenticationMode.DBMS,
  Instance           = "machine\\instance",
  Database           = "database",
  User               = "username",
  Password           = "password"

Database database = new Database(databaseConnectionProperties); 

DatabaseConnectionFile accepts a Uri to the file location of a .sde connection file.

Database database = new Database(new DatabaseConnectionFile(new Uri("path\\to\\.sde\\file")));

SQLiteConnectionPath accepts a Uri to the SQLite database file location.

Database database = new Database(new SQLiteConnectionPath(new Uri("path\\to\\sqlite\\file")));
Query layers

A query layer allows the results of a SQL query to be accessed as a read-only table or feature class. The following steps are used to create a query layer:

  1. Create a QueryDescription using Database.GetQueryDescription. QueryDescription is described in more detail later in this section.
  2. Modify the QueryDescription (may not be required, depending on the query) with details such as object ID columns, shape type, SR ID, and spatial reference.
  3. Pass the QueryDescription to Database.OpenTable to get a table or feature class representing a query layer

One requirement of a query layer is that it must have a unique ID. This can be a natural unique ID (an existing field used as the Object ID in ArcGIS) or a mapped unique ID (a virtual field created by mapping the values of one or more existing fields to an integer). The results of a query must have one of the following:

  • A non-nullable integer field (a natural unique ID)
  • A combination of one or more integer, string, or GUID fields that result in a unique tuple (a mapped unique ID)

In the following table, a query result with a string field and an integer field that are not individually unique but form unique tuples (and contain no null values) can be used for ID mapping:

County State ESRI_OID
Adams Colorado 0
Adams Ohio 1
Addison Vermont 2

The following requirements and restrictions apply:

  • Uniqueness—If a single field is used (natural or mapped), all values must be unique. If multiple fields are used, the tuples in each row must be unique. If non-unique values are used for mapping, no error will be raised, but unexpected behavior can occur and analysis results may be incorrect.
  • No null values—If a mapped ID is used, none of the fields used to create the ID can contain a null value. If a null value is used, an error will occur during mapping.
  • Negative natural values—If a natural unique ID is used, the field should not contain negative values. Rows with a natural unique ID less than 0 are ignored.
  • Geometry fields—A geometry field is not required; a maximum of one geometry field can be in the result set.

One special case to keep in mind is multiple geometry types in the same shape field. This is generally not valid in ArcGIS Pro, but a query layer can be created by specifying that only rows of one type should be used. For example, if there are both point and polygon geometries in the shape column, the QueryDescription.SetShapeType(GeometryType) method can be used to specify which geometry type should be considered for this query layer.

A QueryDescription is an intermediate object used in the creation of a table or feature class representing a query layer. As explained previously, a QueryDescription is obtained from the Database.GetQueryDescription method. This method auto-detects the properties of the query description if possible. For example, if a non-nullable integer field is found in the query result, the QueryDescription.IsObjectIDMappedColumnRequired() will be false and the QueryDescription.GetObjectIDColumnName() specifies the integer field selected. When ID mapping is required and an appropriate mapping field cannot be determined automatically, the QueryDescription.IsObjectIDMappedColumnRequired() will be true and the QueryDescription.SetObjectIDFields() must be used to specify the mapped unique ID before it can be used to create a query class.

The GetQueryDescription method has the following three overloads:

  • GetQueryDescription(string tableName)—Creates a QueryDescription object associated with the single (i.e., standalone) table specified by the tableName argument. In other words, the resulting object will cause a query layer to be created with all the fields of a database table with default inferences for parameters such as ShapeType, SpatialReference, and so on.
  • GetQueryDescription(ArcGIS.Core.Data.Table)—Given a reference to an ArcGIS.Core.Data.Table, this method can be used to discover the parameters of the corresponding query layer represented by the Table reference. This can also be useful to customize the query description before creating another table or feature class representing another query layer.
  • GetQueryDescription(string queryStatement, string queryLayerName)—When a query layer with custom fields or joins between multiple tables is needed, the first parameter to this method is the string that specifies the required query and the second parameter is the name of the resulting query layer.

The QueryDescription object defines several getters that can be used to determine what the query result will look like; for example, the QueryDescription.GetFields() method returns a read-only list for the query result. QueryDescription also defines the following getters/setters that can be used to define how the query layer will be created:

  • GetSpatialReference/SetSpatialReference—The spatial reference that will be applied to the feature class when it is created; to define a spatial reference, the SetSpatialReference method should be used.
  • GetShapeType/SetShapeType—The type of geometry found in the shape field. If the shape field contains multiple geometry types, setting this property specifies which subset of rows will be retrieved from the query class (only rows matching a single geometry type can be used).
  • GetObjectIDFields/SetObjectIDFields—If a natural ID column is found, GetObjectIDFields will return its name. If mapping is required, the SetObjectIDFields method should be used to specify the field(s) to use.
  • GetSRID/SetSRID—The SRID corresponding to the spatial reference that will be applied to the feature class when it is created; to specify an SRID, SetSRID should be used.

Feature services

The Geodatabase class can represent a feature service (i.e., web geodatabase) and provide access to the datasets contained in that service. The Geodatabase class has an overloaded constructor which takes a ServiceConnectionProperties object, which specifies the URL to the feature service and, optionally, the credentials to connect to the feature service. Whether the credentials are required depends on the feature service deployment type.

The three kinds of environments in which feature services can be hosted are as follows:

  • ArcGIS Online
  • ArcGIS Server sites federated with an ArcGIS Enterprise portal
  • Stand-alone ArcGIS Server sites (not federated with a portal)

For feature services hosted on ArcGIS Online, the feature service connection uses the ArcGIS Online credentials with which the user signed in on ArcGIS Pro. In the case of feature services on ArcGIS Server sites federated with an Enterprise portal, the credentials must be specified by the user when adding the portal connection in ArcGIS Pro. For additional details, see Add a portal connection.

ServiceConnectionProperties serviceConnectionProperties = new ServiceConnectionProperties(new Uri(""));
Geodatabase geodatabase = new Geodatabase(serviceConnectionProperties);

For feature services hosted on a stand-alone ArcGIS server site (which is not federated with an Enterprise portal), the credentials must be passed in by assigning them to the User and Password properties on the ServiceConnectionProperties object.

ServiceConnectionProperties serviceConnectionProperties = new ServiceConnectionProperties(new Uri(""))
  User     = "username",
  Password = "password",
Geodatabase geodatabase = new Geodatabase(serviceConnectionProperties);


A shapefile is a simple, non-topological format for storing the geometric location and attribute information of geographic features. Geographic features in a shapefile can be represented by points, lines, or polygons (areas). The datastore containing shapefiles can also contain dBASE tables, which can store additional attributes that can be joined to a shapefile's features.

The only data types supported by shapefile via the FileSystemDatastore are tables and feature classes. Other than feature classes and tables, most dataset types are not supported by shapefile data stores. For example, neither relationship classes nor feature datasets can be opened in shapefile datastores, and, by extension, neither can datasets that require feature datasets (such as topologies and utility networks).

The following include some limitations you should take into consideration:

  • Alias and model names are not supported (this includes datasets as well as fields).
  • Length and area fields are not maintained for shapefiles.
  • Annotation feature classes and dimension feature class are not supported.
  • QueryDefs are not supported.
  • Domains and rules are not supported.
  • Enabling z-values on a Shapefile also requires enabling m-values.
  • DateTime field values in shapefiles are not true DateTime values; rather, they are only Date values. If a DateTime value is stored in a shapefile DateTime field, the date portion of the value will be maintained correctly, but the time value will not. To model DateTime values in shapefiles, create one or more additional fields to store the time portion of the value. For example, one field could be used to store the number of seconds since midnight, or three fields could be used to store hours, minutes, and seconds separately.
  • Null values are not supported by shapefiles. One approach to working around this is to represent nulls using values that would not typically occur in the data. For example, in a shapefile containing cities, a value of –9999 could be used to represent a null (unknown) population.
  • Index names are not maintained on shapefiles. They will have the same name as the fields on which they were created.
  • The Validate method on the Table/FeatureClass is not supported.
  • The Differences method on the Table/FeatureClass is not supported.

A shapefile can be opened using the FileSystemDatastore constructor, which takes a FileSystemConnectionPath whose constructor is specified as FileSystemDatastoreType.Shapefile:

FileSystemConnectionPath connectionPath = new FileSystemConnectionPath(new Uri("path\\to\\shapefiles\\directory"), FileSystemDatastoreType.Shapefile);
FileSystemDatastore shapefile = new FileSystemDatastore(connectionPath);

The OpenDataset<T> generic method can be used to open a Table or FeatureClass. T must be Table or FeatureClass. If T is any other types, an InvalidOperationException will be thrown. The OpenDataset method can be called on a shapefile with or without the file extension. If the OpenDataset method is called on a .shp file, the object returned is a FeatureClass and can be cast to a FeatureClass reference. If the OpenDataset method is called on a .dbf file (in the absence of a corresponding .shp file), the object returned is a Table object.

To obtain metadata about the shapefile FeatureClass or Table, the GetDefinition<T> generic method can be used to obtain the Definition, which provides details such as the fields, indexes, feature class name, object ID field name, and so on. T must be TableDefinition or FeatureClassDefinition. If T is any other types, an InvalidOperationException will be thrown. The methods on the Definition class obtained from the Shapefile feature class that are not supported are as follows:

  • GetAliasName
  • GetModelName
  • GetCreatedAtField
  • GetCreatorField
  • GetDefaultSubtypeCode
  • GetSubtypeField
  • GetSubtypes
  • GetEditedAtField
  • GetEditorField
  • GetGlobalIDField
  • HasGlobalID
  • IsEditorTrackingEnabled
  • IsTimeInUTC
  • GetAreaField
  • GetLengthField
FileSystemConnectionPath connectionPath = new FileSystemConnectionPath(new Uri("path\\to\\shapefiles\\directory"), FileSystemDatastoreType.Shapefile);
FileSystemDatastore shapefile = new FileSystemDatastore(connectionPath);

FeatureClass footPrints = shapefile.OpenDataset<FeatureClass>("BuildingFootprints");
FeatureClass easements = shapefile.OpenDataset<FeatureClass>("Easements.shp"); // The name can be provided with or without the extension.
TableDefinition easementsDefinition = shapefile.GetDefinition<TableDefinition>("Easements");
ObjectIDs vs FeatureIDs

Shapefiles always contain a Feature ID (FID) field that can generally be used in the same way as an ObjectID. For example, Selection objects created from feature classes obtained from shapefiles use FIDs in lieu of ObjectIDs. As with ObjectIDs, FIDs cannot be edited, and the value of IField.Type for FID fields returns a value of esriFieldTypeOID.

The major difference between ObjectIDs and FIDs is that whereas ObjectIDs are permanent identifiers of a feature or row in a geodatabase, FIDs simply represent the current (zero-based) position of a feature or a row in a shapefile. This means that a feature can have different FIDs from one session to the next, even if it has not been modified in any way.

Consider a shapefile with an integer field and three point features as shown in the following table:

Feature ID Object ID
0 0
1 1
2 2

If the feature with an FID of 1 is deleted after edits are stored, the shapefile's attribute table will be changed as shown in the following table:

Feature ID Object ID
0 0
1 2

When a workflow requires that features have a static identifier, use an integer field other than the FID field.

Field and index restrictions

Shapefiles are restricted in the types of fields that can be used relative to a geodatabase. For example, the following field types are not supported:

  • Globally unique identifier (GUID)
  • GlobalID
  • Binary large object (BLOB)
  • Raster

Additionally, field names are restricted to 10 characters, and, as previously mentioned, alias and model names are not supported.

Working with feature data


The Dataset abstract class represents any entity in ArcGIS Pro that is a meaningful and coherent collection of data. For example, ArcGIS.Core.Data.Table inherits from Dataset and represents a collection of data that conforms to a specified schema (for example, each row has the same fields).

The classes that represent Dataset entities and inherit from Dataset are described below.


Tables are a type of dataset containing zero or more rows with one or more columns (or fields). All rows in a table have the same columns and a single value (or no value) associated with each column.

In the geodatabase, attributes are managed in tables based on the following simple, yet essential, relational data concepts:

  • Tables contain rows.
  • All rows in a table have the same columns.
  • Each column has a data type, such as integer, decimal number, character, or date.
  • A series of relational functions and operators (such as SQL) is available to operate on the tables and their data elements.

In the ArcGIS.Core.Data API, Table objects can be obtained in the following ways:

Table table = geodatabase.OpenDataset<Table>("TableName");

In case of a Geodatabase object representing a FeatureService, the OpenDataset can be called with the string representation of the ID for the table

Table table = geodatabase.OpenDataset<Table>("2");
ArcGIS.Desktop.Mapping.Layer selectedLayer = MapView.Active.GetSelectedLayers()[0];
if (selectedLayer is ArcGIS.Desktop.Mapping.FeatureLayer)
  ArcGIS.Core.Data.Table table = (selectedLayer as FeatureLayer).GetTable();
  • From the Row object:
ArcGIS.Desktop.Editing.Events.RowChangedEvent.Subscribe(args =>
  ArcGIS.Core.Data.Row row     = args.Row;
  ArcGIS.Core.Data.Table table = row.GetTable();
Feature class

Feature classes are homogeneous collections of common features, each having the same spatial representation—such as points, lines, or polygons—and a common set of attribute columns—for example, a line feature class for representing a road centerline. In ArcGIS Pro, the currently supported feature classes that are commonly used in the geodatabase are points, lines, and polygons.

In the following images, points, lines, and polygons are used to represent three datasets for the same area: park and recreation area locations as points, roads as lines, and facility sites as polygons

In the ArcGIS.Core.Data API, the FeatureClass class inherits from the Table class. Obtaining FeatureClass objects is similar to obtaining Table objects as shown in the following examples:

FeatureClass table = geodatabase.OpenDataset<FeatureClass>("FeatureClassName");

In case of a Geodatabase object representing a FeatureService, the OpenDataset can be called with the string representation of the ID for the feature class

FeatureClass featureClass = geodatabase.OpenDataset<FeatureClass>("2");
ArcGIS.Desktop.Mapping.Layer selectedLayer = MapView.Active.GetSelectedLayers()[0];
if (selectedLayer is ArcGIS.Desktop.Mapping.FeatureLayer)
  Table table = (selectedLayer as FeatureLayer).GetTable();
  if(table is FeatureClass)
    FeatureClass featureClass = table as FeatureClass;
ArcGIS.Desktop.Editing.Events.RowChangedEvent.Subscribe(args =>
  Row row = args.Row;
  if(row is Feature)
    FeatureClass featureClass = row.GetTable() as FeatureClass;
Opening FeatureClass as a table

You can open a FeatureClass object using the following code:

Table table = geodatabase.OpenDataset<Table>("FeatureClassName");

The table is an ArcGIS.Core.Data.Table reference, but in reality, it's an ArcGIS.Core.Data.FeatureClass object. You could cast the table reference as a FeatureClass, and the cast would work as expected.

Feature dataset

A feature dataset is a collection of related feature classes that share a common coordinate system. Feature datasets are used spatially. Their primary purpose is to organize related feature classes into a common dataset for building a topology, a network dataset, or a terrain dataset. Rather than storing data, it acts as a container for other datasets and maintains the extent of its contained datasets and a common spatial reference.

The ArcGIS.Core.Data.FeatureDataset object can only be obtained from the Geodatabase object.

FeatureDataset FeatureDataset = geodatabase.OpenDataset<FeatureDataset>("FeatureDatasetName");
Relationship class

A GIS integrates information about various types of geographic and non-geographic entities, many of which can be related.

  • Geographic entities can relate to other geographic entities. For example, a building can be associated with a parcel.
  • Geographic entities can relate to non-geographic entities. For example, a parcel of land can be associated with an owner.
  • Non-geographic entities can relate to other non-geographic entities. For example, a parcel owner can be assigned a tax code.

Relationship classes in the geodatabase manage the associations between objects in one dataset (feature class or table) and objects in another. Objects at either end of the relationship can be features with geometry or records in a table.

Relationship classes help validate, and in some cases ensure, referential integrity. For example, the deletion or modification of one feature could delete or alter a related feature. Furthermore, a relationship class is stored in the geodatabase, which makes it accessible to anyone who uses the geodatabase. Relationship classes support the following cardinalities:

  • One-to-one
  • One-to-many
  • Many-to-many

Note: The many-to-many relationship classes are considered attributed relationship classes. They can still be opened as relationship classes, but they can also be opened as attributed relationship classes.

The ArcGIS.Core.Data.RelationshipClass object can only be obtained from the Geodatabase object.

RelationshipClass relationshipClass = geodatabase.OpenDataset<RelationshipClass>("RelClassName");

For more information on working with relationship classes, see Working with relationship classes.

Relationship classes in a feature service

Since relationship classes without a backing table are not first-class entities in feature services, they do not have IDs or aliases assigned to them. Therefore, the only way to open non-attributed relationship classes is to specify the origin and destination layers that are involved in the relationship class.

To open relationship classes from a Geodatabase object representing a feature service, the Geodatabase.OpenRelationshipClass method takes a pair of strings representing layer IDs of the origin and destination layers. The first parameter is the origin layer ID and the second parameter is the destination layer ID. The return value is an IReadOnlyList of Relationship class objects, because there could be more than one relationship between the specified layers. Note that the order of the parameters makes a difference as to what relationship classes are returned.

IReadOnlyList<RelationshipClass> srcToDestRelClasses = geodatabase.OpenRelationshipClass("0", "2"));
IReadOnlyList<RelationshipClass> destToSrcRelClasses = geodatabase.OpenRelationshipClass("2", "0")); // Not the same as above.
Attributed relationship classes

Any relationship class—whether simple or composite and of any particular cardinality—can have attributes. Relationship classes with attributes are stored in a table in the database. This table contains at least the foreign key to the origin feature class or table and the foreign key to the destination feature class or table.

An attributed relationship can also contain any other attribute. For example, if you consider an attributed relationship class that relates a feature class that stores water laterals and a feature class that stores hydrants, water lateral objects have their own attributes, and hydrant objects have their own attributes. The relationship class describes which water laterals feed which hydrants. Because you want to store some kind of information about that relationship—such as the type of riser connecting the two—you can store this information as attributes in the relationship class.

In the ArcGIS.Core.Data API, the AttributedRelationshipClass class inherits from RelationshipClass. All RelationshipClasses that have an intermediate table are treated as attributed relationship classes, regardless of whether or not user-defined attributes exist.

The ArcGIS.Core.Data.AttributedRelationshipClass object can only be obtained from the Geodatabase object.

AttributedRelationshipClass attrRelationshipClass = geodatabase.OpenDataset<AttributedRelationshipClass>("AttrRelClassName");

For more information on working with relationship classes, see Working with relationship classes.

Attributed relationship classes in a feature service

Attributed relationship classes have a corresponding table backing them, and the tables have IDs associated with them. Thus, to open attributed relationship classes, the OpenDataset method is used with the string representation of the ID for the backing table.

AttributedRelationshipClass attributedRelationshipClass = geodatabase.OpenDataset<AttributedRelationshipClass>("4"));


Definition is a concept that represents the metadata about the dataset. While the dataset provides a way to access the data that it encapsulates, the definition describes the dataset's schema, unique properties associated with it, and so on.

Note: Definition is an abstraction that is conceptually equivalent to DataElement in the ArcObjects API.

It should be noted that all the classes that inherit from the ArcGIS.Core.Data.Dataset abstract class have methods, which are unique behaviors expected from that dataset. However, all the metadata must be accessed via the Definition abstract class.

One of the reasons for separating the metadata into a Definition is that opening a Definition is a lightweight operation when compared to opening a dataset. For example, if you only need information regarding Polyline FeatureClasses, getting and filtering definitions is more efficient than opening all the FeatureClasses to do the same.

The classes that inherit from the Definition abstract class are as follows:

  • TableDefinition—Metadata for an ArcGIS.Core.Data.Table dataset. Some major objects accessible from TableDefinition are Fields, Subtypes, Indexes, names of the ObjectIDField, SubtypeField, GlobalIDField, and so on.
  • FeatureClassDefinition—Inherits from TableDefinition and has additional access to metadata specific to FeatureClasses, especially methods to access ShapeField, ShapeType, SpatialReference, Area, Length, and so on.
  • FeatureDatasetDefinition—Metadata that provides access to the SpatialReference and Extent of the FeatureDataset.
  • RelationshipClassDefinition—Metadata for an ArcGIS.Core.Data.RelationshipClass providing access to information such as the cardinality, origin and destination datasets, Origin Foreign Key field, if it's composite, and if it represents the RelationshipClass relating the dataset and its Attachment Table.
  • AttributedRelationshipClassDefinition—Inherits from RelationshipClassDefinition and provides additional access to the Destination Foreign Key field, fields representing the Attribute column names, and so on.
  • UnknownDefinition—Represents an attempt to access a definition of a dataset that is not yet supported by the API. This includes geometric networks, cadastral fabrics, and topologies.

The two options for accessing Definitions are as follows:

  • Opening a Definition from a Geodatabase—Typically this is used when it is not anticipated that the dataset will be opened.
FeatureDatasetDefinition definition = geodatabase.GetDefinition<FeatureDatasetDefinition>("FeatureDatasetName");
  • Opening the Definition from the dataset—This is used when the dataset is already open and the reference is accessible.
ArcGIS.Desktop.Mapping.Layer selectedLayer = MapView.Active.GetSelectedLayers()[0];
if (selectedLayer is ArcGIS.Desktop.Mapping.FeatureLayer)
  using (Table table = (selectedLayer as FeatureLayer).GetTable())
    TableDefinition definition = table.GetDefinition();


Tables, feature classes, and attributed relationship classes are composed of fields, which are synonymous with columns. The fields represent the schema of the corresponding dataset.

In ArcGIS.Core.Data, the fields are always returned as a read-only list of ArcGIS.Core.Data.Field objects accessible by calling GetFields() on the following objects:

For the same dataset, the collection of fields obtained at the same time from the following sets of objects should be equivalent:

  • TableDefinition, RowCursor, RowBuffer, and Row
  • FeatureClassDefinition, RowCursor, RowBuffer, and Feature
  • AttributedRelationshipClassDefinition and AttributedRelationship

After obtaining the read-only collection, you can use LINQ to filter the list as shown below:

using (Geodatabase geodatabase = new Geodatabase(new FileGeodatabaseConnectionPath(new Uri("path\\to\\gdb"))))
using (FeatureClass enterpriseFeatureClass = geodatabase.OpenDataset<FeatureClass>("LocalGovernment.GDB.FacilitySite"))
  FeatureClassDefinition facilitySiteDefinition = enterpriseFeatureClass.GetDefinition();
  IReadOnlyList<Field> fields = facilitySiteDefinition.GetFields();

  IEnumerable<Field> dateFields = fields.Where(field => field.FieldType == FieldType.Date);


In a geodatabase, you can use rules to enforce different types of constraints on the tables and feature classes. The rules include attribute and relationship rules. Attribute rules are the most common, and they're created using domains. Domains are used to specify permissible values that can be assigned to a field.

The following are the different types of domains in a geodatabase:

  • Coded value—Specifies a list of valid values, each with a string representation.
  • Range domains—Specifies a range of valid values through minimum and maximum numeric values.

In the ArcGIS.Core.Data API, a ArcGIS.Core.Data.Domain is a first-class entity modeled as an abstract class. There are two classes, RangeDomain and CodedValueDomain, that inherit from the Domain abstract class. The Domain provides access to the Name, FieldType, and Description of the Domain. RangeDomain provides additional access to the Min and Max values. The CodedValueDomain provides access to the code-value pairs returned as a C# Generic Sorted List of <object,string> where the key is the code.

Domains can be accessed in two ways. If a Domain is accessed on the Field object's GetDomain method. It returns a Domain reference, which has to be safely cast based on the underlying Domain type: CodedValueDomain or RangeDomain. The GetDomain method has an optional Subtype parameter. This can be used to obtain the domain assigned for the field corresponding to that subtype value represented by the Subtype object passed in as the argument. If there is no domain assigned for the specified subtype, a null value is returned. If there is no subtype passed in, the domain returned is the default domain for the field. If no domain is assigned to a field, a null value is returned.

Another way to access Domains is via the Geodatabase object's GetDomains method. Calling this method will return a read-only list of Domain instances whose derived types are CodedValueDomain and RangeDomain.

using (Geodatabase geodatabase = new Geodatabase(new FileGeodatabaseConnectionPath(new Uri("path\\to\\gdb"))))
using (FeatureClass enterpriseFeatureClass = geodatabase.OpenDataset<FeatureClass>("LocalGovernment.GDB.FacilitySite"))
  FeatureClassDefinition facilitySiteDefinition = enterpriseFeatureClass.GetDefinition();
  IReadOnlyList<Field> fields = facilitySiteDefinition.GetFields();

  Field doubleField = fields.First(field => field.FieldType == FieldType.Double);

  Domain domain = doubleField.GetDomain();

  if (domain is RangeDomain)
    RangeDomain rangeDomain = domain as RangeDomain;
    // The reason we can directly call convert to double because the domain was obtained from a double Field.
    double minValue = Convert.ToDouble(rangeDomain.GetMinValue());
    double maxValue = Convert.ToDouble(rangeDomain.GetMaxValue());

  if (domain is CodedValueDomain)
    CodedValueDomain codedValueDomain = domain as CodedValueDomain;
    SortedList<object, string> codedValuePairs = codedValueDomain.GetCodedValuePairs();
    IEnumerable<KeyValuePair<object, string>> filteredPairs = codedValuePairs.Where(pair => Convert.ToDouble(pair.Key) > 20.0d);
    double code = Convert.ToDouble(codedValueDomain.GetCodedValue("sampleValue"));
    string value = codedValueDomain.GetName(21.45d);


Subtypes are a way to partition objects in a single table into groups with similar rules and behavior. Although subtypes share a common set of fields, each group can have its own attribute and relationship rules, as well as different default values at creation time. An example of this is parcels of land that are often divided into residential, commercial, and industrial subtypes. Subtypes are defined for a single table or feature class and cannot be shared.

Subtypes for the table or feature class can be obtained via TableDefinition and FeatureClassDefinition. Using the GetSubtypes method returns a read-only list of subtype objects. Each subtype object provides access to the subtype code and the subtype name.

using (Geodatabase geodatabase = new Geodatabase(new FileGeodatabaseConnectionPath(new Uri("path\\to\\gdb"))))
using (FeatureClass enterpriseFeatureClass = geodatabase.OpenDataset<FeatureClass>("Fittings"))
  FeatureClassDefinition featureClassDefinition = enterpriseFeatureClass.GetDefinition();
  Subtype bendSubtype = featureClassDefinition.GetSubtypes().First(subtype => subtype.GetName().Equals("BEND"));
  Subtype sleeveSubtype = featureClassDefinition.GetSubtypes().First(subtype => subtype.GetName().Equals("SLEEVE"));

  IReadOnlyList<Field> listOfFields = featureClassDefinition.GetFields();
  int typeCodeDescFieldIndex        = featureClassDefinition.FindField("TYPE_CODE_DESC");
  Domain domainForBend      = listOfFields[typeCodeDescFieldIndex].GetDomain(bendSubtype);
  string bendDomainName     = domainForBend.GetName();
  Domain domainForSleeve    = listOfFields[typeCodeDescFieldIndex].GetDomain(sleeveSubtype);
  string sleeveDomainName   = domainForSleeve.GetName();
  Domain domainForNoSubtype = listOfFields[typeCodeDescFieldIndex].GetDomain();
  string masterDomainName   = domainForNoSubtype.GetName();

Querying data

The following are the two ways to query data using this API:

  • Selection—Access either the entire list of object IDs or the list object IDs for rows/features matching a criteria. Selection is typically used for obtaining object IDs that satisfy a criteria so that you can, for example, highlight the features on the map. The advantage of using selection is that it's a lightweight operation compared to getting the entire row. Selection is also helpful for combining multiple disparate selections from the same Table/FeatureClass into a single selection.
  • Search—Used to access not only the object ID, but also values for all or a subset of fields on the Table/FeatureClass. Search is also required when any editing of data has to be performed.

A query filter is used to restrict the records retrieved from the database during a query (with the QueryFilter.WhereClause property), specify which fields of the query result are populated (with the QueryFilter.SubFields property), order the returned result in a certain order (with the QueryFilter.PostfixClause property), and possibly get only unique/distinct results (using the QueryFilter.PrefixClause property). Query filters are used to create cursors and selections and are common throughout the Geodatabase API.

ArcGIS.Core.Data.QueryFilter represents the class for creating a query filter. It is one of the very few objects in the ArcGIS.Core.Data API that is thread-agnostic (that is, you can create a QueryFilter on a non-MCT thread).

QueryFilter queryFilter = new QueryFilter();

string whereClause = "OWNER_NAME = 'ADA IAN'";
queryFilter.WhereClause = whereClause;

string prefixClause = "DISTINCT";
queryFilter.PrefixClause = prefixClause;

string postfixClause = "ORDER BY OWNER_NAME";
queryFilter.PostfixClause = postfixClause;

string subFields = "OBJECTID, OWNER_NAME";
queryFilter.SubFields = subFields;

ArcGIS.Core.Data.SpatialQueryFilter inherits from ArcGIS.Core.Data.QueryFilter and has additional properties to specify the filter geometry and spatial relationship type (using the ArcGIS.Core.Data.SpatialRelationship enum) to use for the filter geometry to filter the results.

MapPoint minPoint = new MapPointBuilder(-123, 41).ToGeometry();
MapPoint maxPoint = new MapPointBuilder(-121, 43).ToGeometry();

SpatialQueryFilter spatialFilter = new SpatialQueryFilter
  WhereClause         = "OWNER_NAME = 'ADA IAN'",
  FilterGeometry      = new EnvelopeBuilder(minPoint, maxPoint).ToGeometry(),
  SpatialRelationship = SpatialRelationship.Intersects,
Where clause

A Where clause is a component of a Structured Query Language (SQL) statement that defines attribute constraints on a query. Where clauses are used as properties by QueryFilter and SpatialQueryFilter. Where clauses can consist of one-to-many expressions, linked by logical connectors (AND and OR). The following is the valid format for a Where clause expression:

operand1 predicate operand2

Operands can consist of a field name from a table, a numeric constant, a character string, a simple arithmetic expression, a function call, or a subquery.

Predicate Meaning Example
= Is equal to TYPE = 3
<> Is not equal to PROVINCE <> 'NS'
>= Is greater than or equal to POPULATION >= 10000
<= Is less than or equal to AVG_TEMP <= 25
> Is greater than HEIGHT > 10
< Is less than SPD_LIMIT < 65
[NOT] BETWEEN Is between a minimum and maximum value DIAMETER BETWEEN 5 AND 10
[NOT] IN Is in a list or the results of a subquery TYPE IN ('City', 'Town')
[NOT] EXISTS Checks a subquery for results EXISTS (SELECT * FROM PARCELS WHERE TYPE='RES')
[NOT] LIKE Matches a string pattern CITY_NAME LIKE 'Montr_al'

The following are the two types of wildcards that can be used with the LIKE predicate:

  • Single-character wildcard (underscore, _)
  • Multiple-character wildcard (percent sign, %)

Strings containing single-character wildcards evaluate to true if the wildcard corresponds to a single character in the comparison string, whereas strings containing multiple-character wildcards evaluate to true if the wildcard corresponds with zero-to-many characters in the comparison string.

The following are example statements:

  • 'Belmont' LIKE '%mont' (evaluates to true)
  • 'Belmont' LIKE '%elmont%' (evaluates to true)
  • 'Belmont' LIKE 'Belmont_' (evaluates to false)
  • 'Belmont' LIKE 'Bel_ont' (evaluates to true)
ObjectID Queries

The ObjectIDs property on the QueryFilter class provides a shortcut for fetching a set of rows that correspond to a set of ObjectIDs. Notably, some underlying database systems limit the number of values that can be specified within an SQL in clause. This property works around this limitation by automatically breaking up the request into multiple requests, as needed. The ObjectIDs property can also be combined with a Where clause.

public RowCursor SearchingATable(Table table, IReadOnlyList<long> objectIDs)
  QueryFilter queryFilter = new QueryFilter()
    ObjectIDs = objectIDs

  return table.Search(queryFilter);

In the ArcGIS.Core.Data API, to obtain a Selection, the Select method must be called on the Table/FeatureClass or on a Selection object. The Select method takes the following two parameters:

  • QueryFilter—Can be either an ArcGIS.Core.Data.QueryFilter for Table/FeatureClass or an ArcGIS.Core.Data.SpatialQueryFilter for FeatureClass. The default value is null, which means that the Selection will not filter any object IDs.
  • SelectionOption—Takes an enum value that can be SelectionOption.Normal, SelectionOption.None, or SelectionOption.OnlyOne. This is a default parameter with a default value of SelectionOption.Normal.
    • SelectionOption.Normal—Gives all the records that match the filter.
    • SelectionOption.OnlyOne—Gives the first record matching the filter.
    • SelectionOption.None—Gives an empty selection. This is intended to be used for combining multiple selections.
using (Geodatabase geodatabase = new Geodatabase(new DatabaseConnectionFile(new Uri("path\\to\\sde\\file\\sdefile.sde"))))
using (Table enterpriseTable = geodatabase.OpenDataset<Table>("LocalGovernment.GDB.piCIPCost"))
  QueryFilter anotherQueryFilter = new QueryFilter { WhereClause = "FLOOR = 1 AND WING = 'E'" };

  // Use SelectionOption.Normal to select all matching entries.
  using (Selection anotherSelection = enterpriseTable.Select(anotherQueryFilter, SelectionType.ObjectID, SelectionOption.Normal))

  // This can be used to get one record that matches the criteria. No assumptions can be made about which record satisfying the criteria is selected.
  using (Selection onlyOneSelection = enterpriseTable.Select(anotherQueryFilter, SelectionType.ObjectID, SelectionOption.OnlyOne))
    // This will always be one.
    int singleRecordCount = onlyOneSelection.GetCount();

    // This will always have one record.
    IReadOnlyList<long> listWithOneValue = onlyOneSelection.GetObjectIDs(); 
  // This can be used to obtain an empty selection, which can be used as a container to combine results from different selections.
  using (Selection emptySelection = enterpriseTable.Select(anotherQueryFilter, SelectionType.ObjectID, SelectionOption.Empty))

Searching a Table or FeatureClass is necessary for reading and modifying the data in rows matching the query specified by ArcGIS.Core.Data.QueryFilter or ArcGIS.Core.Data.SpatialQueryFilter.

The result of the search is an ArcGIS.Core.Data.RowCursor, which has an interface similar to the System.Collections.IEnumerator interface (with the exception of the Reset method). RowCursor.MoveNext() returns a Boolean representing whether the last record in the result has been returned. If the result is empty, the first call to RowCursor.MoveNext() will return false. Typically, to access all the rows in RowCursor, MoveNext is used in conjunction with a while loop. If RowCursor.MoveNext() is true, RowCursor.Current gives the ArcGIS.Core.Data.Row reference. The underlying object is ArcGIS.Core.Data.Row or ArcGIS.Core.Data.Feature for a Table or a FeatureClass, respectively. If the object is really a Feature object, it can be cast to a Feature to access the Shape.

To Search a Table or FeatureClass, you can use the Search method, which has the following two parameters:

  • QueryFilter—Specifies the filter for fetching the rows matching the filter. The default value is null, and a null value fetches all the rows in the table.
  • Boolean for Recycling—Specifies whether successive rows obtained from the row cursor will use a new memory location for each row or use the same memory location as MoveNext is called. Recycling is explained in more detail in later sections.
using (Geodatabase geodatabase = new Geodatabase(new DatabaseConnectionFile(new Uri("path\\to\\sde\\file\\sdefile.sde"))))
using (FeatureClass schoolBoundaryFeatureClass = geodatabase.OpenDataset<FeatureClass>("LocalGovernment.GDB.SchoolBoundary"))
  // Using a spatial query filter to find all features that have a certain district name and lying within a given Polygon.
  SpatialQueryFilter spatialQueryFilter = new SpatialQueryFilter
    WhereClause    = "DISTRCTNAME = 'Indian Prairie School District 204'",
    FilterGeometry = new PolygonBuilder(new List<Coordinate2D>
      new Coordinate2D(1021880, 1867396),
      new Coordinate2D(1028223, 1870705),
      new Coordinate2D(1031165, 1866844)

    SpatialRelationship = SpatialRelationship.Within

  // Without Recycling
  using (RowCursor indianPrairieCursor = schoolBoundaryFeatureClass.Search(spatialQueryFilter, false))
    while (indianPrairieCursor.MoveNext())
      using (Feature feature = (Feature)indianPrairieCursor.Current)
        int nameFieldIndex  = feature.FindField("NAME");
        string districtName = Convert.ToString(feature["DISTRCTNAME"]);
        double area         = Convert.ToDouble(feature["SCHOOLAREA"]);
        string name         = Convert.ToString(feature[nameFieldIndex]);
        Geometry geometry   = feature.GetShape();
Working with rows and features

ArcGIS.Core.Data.Feature inherits from ArcGIS.Core.Data.Row and adds the methods for getting and setting the shape on the feature.

On Row (and Feature), there is an integer-based indexer and a string-based indexer for accessing the attributes of the row. If the index of the field is known, the integer-based indexer is used, and the string-based indexer is used when the field name is known. The return value is always System.Object and must be cast into the corresponding type based on the field type inferred from the corresponding Field object.

To work with geometries, GetShape() and SetShape(Geometry) must be used. Something to keep in mind while modifying shapes is that since Geometry is an immutable object, when you set the Shape to a new geometry, the corresponding Builder needs to be used to build a new geometry (possibly using the existing geometry obtained from Feature.GetShape()).


Recycling can be used for better performance while accessing complex and large datasets and is typically used to render the shapes on the map or populate a table in the UI.

To enable recycling, the second parameter to the Search is set to true (which is the default parameter value). When using recycling, for successive Row references obtained while calling MoveNext on the RowCursor, all references obtained in previous iterations of MoveNext will point to the current Row the RowCursor.Current is referring to. This means that once MoveNext is called, the previous row is no longer available.

using (Geodatabase geodatabase = new Geodatabase(new DatabaseConnectionFile(new Uri("path\\to\\sde\\file\\sdefile.sde"))))
using (FeatureClass schoolBoundaryFeatureClass = geodatabase.OpenDataset<FeatureClass>("LocalGovernment.GDB.SchoolBoundary"))
  QueryFilter queryFilter = new QueryFilter
    WhereClause = "DISTRCTNAME = 'Indian Prairie School District 204'"

  using (RowCursor indianPrairieCursor = schoolBoundaryFeatureClass.Search(queryFilter, true))
      Row row1 = null;
      if (indianPrairieCursor.MoveNext())
        row1 = indianPrairieCursor.Current;
      Row row2 = null;
      if (indianPrairieCursor.MoveNext())
        row2 = indianPrairieCursor.Current;

      // If the code reaches this point, row1 and row2 are referencing the second row object obtained 
      // when the MoveNext was called the second time.
        if(row1 != null)
        if(row2 != null)
SQL Syntax

While crafting queries, creating new fields, or creating new tables that have to be executed against multiple platforms (such as file geodatabases, SQL Server enterprise geodatabases, Oracle enterprise geodatabases, shapefiles, and so on), there are subtle differences to be taken into consideration.

Some of these considerations are as follows:

  • The specific function name for the current flavor of database against which the query is executing
  • The correct qualification for the table name or column name
  • The owner name for the given fully-qualified table name
  • Whether string comparison is case sensitive
  • The timestamp, date, or time format accepted or returned
  • Whether the table or field name can start with a certain character or contain a certain character
  • Whether a given word is considered as a keyword for the database
  • What SQL clauses or predicates can be used in the query

These considerations are necessary to be able to develop applications that can work with multiple data platforms.

The SQLSyntax class, obtained from the Datastore, provides the capability to answer these questions and write code that will be successful in its execution. See Using SQLSyntax to form platform agnostic queries for an example.


Joins are used to combine fields from two tables into a single table representation. The tables that participate in a Join can be from the same or different datastores. Joins are created from relationship classes. The relationship class can be from a geodatabase or be defined in memory as a virtual relationship class. The virtual relationship class (also used for Relate) is an important concept as it is by this means that a Join can join tables from different datastores. A few examples would be an enterprise feature class joined with a query layer or a feature class in an shapeFile with a feature class in a file geodatabase. The capability of Joins to be performed across datastores separates them from QueryDefs. Joins do not support one-to-many cardinality. Joins are read-only; however, they reflect data changes that are made to the base tables. Creating a Join requires two tables (or feature classes) and a relationship class.

To obtain a Join in ArcGIS.Core.Data API, the first step is to obtain the relationship class. This can be a RelationshipClass that is opened from the geodatabase, or you can create an in-memory RelationshipClass using the VirtualRelationshipClassDescription. To create a VirtualRelationshipClassDescription you need both the left and right tables. For example,

ArcGIS.Desktop.Mapping.Layer selectedLayer = MapView.Active.GetSelectedLayers()[0];
if (selectedLayer is ArcGIS.Desktop.Mapping.FeatureLayer)
  using (Geodatabase geodatabase = new Geodatabase(new DatabaseConnectionFile(new Uri("path\\to\\sde\\file\\sdefile.sde"))))
  using (FeatureClass leftFeatureClass = geodatabase.OpenDataset<FeatureClass>("LocalGovernment.GDB.SchoolBoundary"))
  using (Table rightTable = (selectedLayer as FeatureLayer).GetTable())
    FeatureClassDefinition leftFeatureClassDefinition = leftFeatureClass.GetDefinition();
    IReadOnlyList<Field> listOfFields = leftFeatureClassDefinition.GetFields();

    Field originPrimaryKey      = listOfFields.FirstOrDefault(field => field.Name.Equals("primaryKeyField"));
    Field destinationForeignKey = listOfFields.FirstOrDefault(field => field.Name.Equals("foreignKeyField"));

    VirtualRelationshipClassDescription relationshipClassDescription = new VirtualRelationshipClassDescription(originPrimaryKey, destinationForeignKey, RelationshipCardinality.OneToMany);
    RelationshipClass relationshipClass = leftFeatureClass.RelateTo(rightTable, relationshipClassDescription);

Once you have a relationship class, use the JoinDescription to specify the attributes of the Join such as the following:

  • RelationshipClass: This is a mandatory argument for the join.
  • JoinType: Whether the join is an inner join or a left outer join.
  • JoinDirection: Whether the join goes from the left table to the right table (Forward) or right table to the left table (Backward).
  • TargetFields: The fields from the left table that must be included. (Note that if the Join is Backward, the target fields are from the right table.)
  • QueryFilter: To filter the contents of the Join based on a query.
  • Selection: The selection to be used in creating the join.
JoinDescription joinDescription = new JoinDescription(relationshipClass)
  JoinType      = JoinType.LeftOuterJoin,
  JoinDirection = JoinDirection.Backward

using (Join join         = new Join(joinDescription))
using (Table joinedTable = join.GetJoinedTable())
  // The joinedTable is an ArcGIS.Core.Data.Table reference that can be either an ArcGIS.Core.Data.Table or an
  // ArcGIS.Core.Data.FeatureClass (depending upon whether the join has a Shape column).

Some things to keep in mind about Joins:

  • Definitions are not supported on Joins since they are temporary in nature.
  • Selections on the Table obtained from the join are based on distinct values in the unique identifier column. So, if you have a Join using a relationship class that is a one-to-many relationship class and the same row on the left table matches multiple rows on the right table, the selection will return only one of the matches, but the search will return all of them.
  • Every attempt will be made to perform the join on the server side (executing the sql query, which would include the join to return the results) to improve performance. In some cases, this might not be possible (for example, when the left and right tables are from different datastores), and the join will be performed on the client side.
  • The JoinDescription.ErrorOnFailureToProcessJoinOnServerSide property can be set to force the join to take place on the server. If the join cannot be executed on the server, an error is returned.
Sorting Tables

The geodatabase also provides the ability to create row cursors in a sorted order. This is accomplished through the Table.Sort() method. Sort takes a TableSortDescription object as an argument, which combines a QueryFilter and a set of SortDescriptions.

The SortDescription class allows sorting on a particular field. Properties on this object allow the caller to specify whether the sort is ascending or descending, and case-sensitive or case-insensitive.

The following snippet returns a sorts a World Cities table by country name, and then city name:

public RowCursor SortWorldCities(FeatureClass worldCitiesTable)
  using (FeatureClassDefinition featureClassDefinition = worldCitiesTable.GetDefinition())
    Field countryField = featureClassDefinition.GetFields().First(x => x.Name.Equals("COUNTRY_NAME"));
    Field cityNameField = featureClassDefinition.GetFields().First(x => x.Name.Equals("CITY_NAME"));

    // Create SortDescription for Country field
    SortDescription countrySortDescription = new SortDescription(countryField);
    countrySortDescription.CaseSensitivity = CaseSensitivity.Insensitive;
    countrySortDescription.SortOrder = SortOrder.Ascending;

    // Create SortDescription for City field
    SortDescription citySortDescription = new SortDescription(cityNameField);
    citySortDescription.CaseSensitivity = CaseSensitivity.Insensitive;
    citySortDescription.SortOrder = SortOrder.Ascending;

    // Create our TableSortDescription
    TableSortDescription tableSortDescription = new TableSortDescription(new List<SortDescription>() { countrySortDescription, citySortDescription });

    return worldCitiesTable.Sort(tableSortDescription);
Calculating Statistics

The geodatabase provides powerful tools to calculate statistics on table fields. These statistics are computed on the server if supported. Statistics are calculated by calling the CalculateStatistics() method on the Table class.

CalculateStatistics() takes a TableStatisticsDescription object to specify how the statistics should be calculated.

The QueryFilter property allows the table rows to be filtered before statistics calculation takes place. The optional GroupBy property will group the rows together, and the optional OrderBy property will sort those groups. If the underlying datastore doesn't support Group By and Order By (e.g., shape files), these properties are ignored.

Finally, the StatisticsDescriptions property contains a set of statistics to calculate.

The StatisticsDescription class allows the caller to specify a set of different functions to calculate on a given field. The classes above can be use to calculate multiple statistics on multiple fields in a single call to Table.CalculateStatistics().

The return value is a set of TableStatisticsResult objects. If no GroupBy was specified, or if the underlying datastore doesn't support GroupBy, one TableStatisticsResult is returned. Otherwise, one TableStatisticsResult instance is returned for each field grouping.

The GroupBy property on the TableStatisticsResult object describes the grouping that was returned. The key-value pair represents the Field and value of that field for the group. One key-value pair is returned for each field in the original GroupBy.

The StatisticsResults list contains a list of the statistics that were calculated.

In the StatisticsResults object, only the appropriate properties are filled-in, based on which statistics were requested for each field.

For example, consider a Country table that contains three fields of interest— Region, Population_1990, and Population_2000. There are four regions, "North", "South", "East" and "West." For each of these regions we want to compute the sum and average of the Population_1990 and Population_2000 fields. To perform this calculation, assemble a TableStatisticsDescription as follows:

The Table.CalculateStatistics() method would return a list of TableStatisticsResults objects like the following:

The following code sample matches the example above:

// Calculate the Sum and Average of the Population_1990 and Population_2000 fields, grouped and ordered by Region
public void CalculateStatistics(FeatureClass countryFeatureClass)
  using (FeatureClassDefinition featureClassDefinition = countryFeatureClass.GetDefinition())
    // Get fields
    Field regionField = featureClassDefinition.GetFields().First(x => x.Name.Equals("Region"));
    Field pop1990Field = featureClassDefinition.GetFields().First(x => x.Name.Equals("Population_1990"));
    Field pop2000Field = featureClassDefinition.GetFields().First(x => x.Name.Equals("Population_2000"));

    // Create StatisticsDescriptions
    StatisticsDescription pop1990StatisticsDescription = new StatisticsDescription(pop1990Field, new List<StatisticsFunction>() { StatisticsFunction.Sum, StatisticsFunction.Average });
    StatisticsDescription pop2000StatisticsDescription = new StatisticsDescription(pop2000Field, new List<StatisticsFunction>() { StatisticsFunction.Sum, StatisticsFunction.Average });

    // Create TableStatisticsDescription
    TableStatisticsDescription tableStatisticsDescription = new TableStatisticsDescription(new List<StatisticsDescription>() { pop1990StatisticsDescription, pop2000StatisticsDescription });
    tableStatisticsDescription.GroupBy = new List<Field>() { regionField };
    tableStatisticsDescription.OrderBy = new List<SortDescription>() { new SortDescription(regionField) };

    // Calculate Statistics
    IReadOnlyList<TableStatisticsResult> statisticsResults = countryFeatureClass.CalculateStatistics(tableStatisticsDescription);

    // Code to process results goes here...
When to use different query constructs

There are many different constructs for querying data such as searching a table or featureclass, QueryDefs, QueryTables, Query Layers, and Joins. The following shows the advantages of certain constructs in specific scenarios:

  • A simple query involving a single registered table in a geodatabase: Table.Search or FeatureClass.Search is the best suited for this purpose.
  • A one-off query with a join within a geodatabase to obtain the data: QueryDef is the best suited since it returns a row cursor with the data matching the query.
  • Using the same query joining two or more tables within the geodatabase multiple times: QueryTable may be better suited, since there can be performance advantages of doing so, compared to executing the QueryDef evaluation multiple times.
  • Adding the result of a query involving a single table within a geodatabase to the map or using the result in a geoprocessing tool: QueryTable is the best suited construct for this purpose. In addition, QueryTable would also work with versioned data (when using a descendant version) or archived data (when using a historical moment).
  • Adding the result of a query joining two or more tables within a geodatabase to the map or using the result in a geoprocessing tool: In this case, either a QueryTable or a Join is suitable, since both contructs allow recursive joins (creating a join from a joined table or creating a querytable from a querytable), and they work with versioned and archived data.
  • Executing a simple query or a join within a database (without a geodatabase schema): Query Layer is the best suited construct.
  • Executing a join that spans across multiple datastores (such as a file geodatabase and a sql server database): Join is the only construct that allows joining data from multiple datastores.


Versioning allows multiple users to edit spatial and tabular data simultaneously in a long transactional environment. Users can directly modify the database without having to extract data or lock features in advance.

The following are the four primary abilities that are provided by the API:

  • Lists all the versions in a geodatabase, including properties of the versions
  • Connect to a specific version
  • Lists the differences between Tables and FeatureClasses from different versions
  • Reconcile and post

The VersionManager object can be accessed from the Geodatabase object. VersionManager provides access to the currently connected version; a list of historical versions; and a list of all public versions, protected versions, and private versions owned by the currently connected user. It should also be noted that VersionManager is only accessible if the IsVersioningSupported method on the Geodatabase object returns true.

The versions returned are ArcGIS.Core.Data.Version objects, which provide access to the following:

  • Attributes such as name, access type, and description
  • Parent Version object
  • List of child Version objects
  • Whether the current user is the owner of the version

To connect to a version represented by a Version object obtained from VersionManager, you can call the Connect method on the Version object, which returns the corresponding Geodatabase object so that datasets in that version of the geodatabase can be obtained.

To find differences, one workflow could be as follows:

  1. Get the Geodatabase object using one of the methods described in Geodatabase.
  2. Use VersionManager to find the other Version object of interest.
  3. Get the Geodatabase object for the other version using Connect().
  4. Open the corresponding Table/FeatureClasses from both Geodatabase objects.
  5. Get the DifferenceCursor using the Differences method.

The type of difference can be specified as a parameter to the Differences method using the ArcGIS.Core.Data.DifferenceType enum.

Version differences is not yet supported on feature service workspaces.

using (Geodatabase geodatabase = new Geodatabase(new DatabaseConnectionFile(new Uri("path\\to\\sde\\file\\sdefile.sde"))))
using (VersionManager versionManager = geodatabase.GetVersionManager())
  IReadOnlyList<Version> versionList = versionManager.GetVersions();
  IReadOnlyList<Version> childVersionList = null;

    IEnumerable<Version> publicVersions = versionList.Where(version => version.GetAccessType() == VersionAccessType.Public);

    // The default version will have a null Parent.
    Version defaultVersion = versionList.First(version => version.GetParent() == null);

    childVersionList = defaultVersion.GetChildren();
    Version qaVersion = childVersionList.First(version => version.GetName().Contains("QA"));

    using (Geodatabase qaVersionGeodatabase = qaVersion.Connect())
      FeatureClass currentFeatureClass = geodatabase.OpenDataset<FeatureClass>("featureClassName");
      FeatureClass qaFeatureClass      = qaVersionGeodatabase.OpenDataset<FeatureClass>("featureClassName");

      DifferenceCursor differenceCursor = currentFeatureClass.Differences(qaFeatureClass, DifferenceType.DeleteNoChange);

      while (differenceCursor.MoveNext())
        // The current row value refers to the row on the source Table/FeatureClass.
        // Thus, for DeleteNoChange differences, the row will be null.
        using (Row current = differenceCursor.Current)

        long objectID = differenceCursor.ObjectID;
        // Do something with the object Id.
    if (versionList != null)
      foreach (Version version in versionList)

    if (childVersionList != null)
      foreach (Version version in childVersionList)

Reconciling and posting of versions is accomplished by the Reconcile() method on the Version class. This method takes a ReconcileDescription object as an argument. The object specifies the conflict resolution method, target version (use null to specify the default version), and whether or not to post after the reconcile (there is no separate Post method).

Working with relationship classes

The following operations can be performed on RelationshipClass:


Attachments allow you to add files to individual features and can be images, PDFs, text documents, or any other type of file. For example, if you have a feature representing a building, you could use attachments to add multiple photographs of the building taken from several angles, along with PDF files containing the building's deed and tax information.

To add attachments, you first need to enable attachments on the feature class or table. When you enable attachments, a new table is created to contain the attachment files, and a new relationship class is created to relate the features to the attached files.

Since ArcGIS.Core.Data API is a DML only API, one option to enable attachments programmatically is to use the Geoprocessing API.

IReadOnlyList<string> parameters = Geoprocessing.MakeValueArray("path\\to\\sde\\connection.sde\\FeatureClassName");
IGPResult gpResult = await Geoprocessing.ExecuteToolAsync("management.EnableAttachments", parameters);

Determining whether a table or feature class has attachments enabled can be done using the IsAttachmentEnabled property on the Table and FeatureClass objects.

Adding attachments

To add an attachment, a new attachment object can be created using the Attachment constructor. The Name, ContentType, and Data can be obtained and set using the methods on the new attachment. Once all the information has been set, the AddAttachment method on the Row can be called with the newly created Attachment object to add the attachment. An example of adding attachments can be found at Adding attachments.

To set the data on the attachment, a memory stream must be created from the file on disk. One way of accomplishing this is described in Obtaining a MemoryStream from a file.

Accessing attachments

To get attachments, the GetAttachments method on the Row can be used. If there are no arguments passed in, all the attachments for that row are returned. If there is a list of attachment IDs passed in as the first argument, the attachments matching the IDs are returned. The second argument is a Boolean, which is used to specify whether or not the data of the attachment should be returned. For an example of accessing attachments, see Updating attachments.

Updating attachments

After getting the attachments from the row or feature, the getters and setters on the attachments can be used to set the Name, Content Type, and Data. Then, the updated attachment objects can be used to call UpdateAttachment on the Row or Feature. For an example of accessing attachments, see Updating attachments.

Deleting attachments

To delete an attachment, you need to get the attachment from the row first, and use DeleteAttachments on the Row or Feature by passing in a list of attachment IDs to be deleted. The return value is a dictionary of attachment IDs to the exception thrown when the deletion of the corresponding attachment failed. If the dictionary is empty, the requested attachments were deleted successfully. If no arguments are passed to DeleteAttachments, all the attachments for that row are deleted. For an example of deleting attachments, see Deleting attachments.

Editing datastores

The ArcGIS.Core.Data API provides support for performing multiple edits as a single transaction (also known as long transactions). The following are the two modes under which editing in a transaction can be accomplished:

  • Editing in addin mode
  • Editing in standalone mode

Editing in add-in mode

This is the normal mode of performing edits in a transaction while authoring add-ins for ArcGIS Pro. The EditOperation class provides the support for editing in a transaction in ArcGIS Pro. For more details, see Advanced Editing in the ArcGIS Pro ProConcepts Editing documentation.

Editing in stand-alone mode

When the ArcGIS.Core API is used outside ArcGIS Pro (for example, in a console application) using the ArcGIS.CoreHost API support, it is referred to as stand-alone mode. In this mode, when performing edits in a transaction, the ArcGIS.Core.Data.Geodatabase.ApplyEdits method should be used.

using (RowCursor rowCursor = featureClass.Search(new QueryFilter { WhereClause = "NAME = 'Kern'" }, false))
  if (!rowCursor.MoveNext())

  geodatabase.ApplyEdits(() =>
    using (Row row = rowCursor.Current)
      row["NAME"] = "Kern-Another";

    using (RowBuffer rowBuffer = featureClass.CreateRowBuffer())
      rowBuffer[FileGDBPathsAndNames.CountyNameColumn] = "Santa Ana";
      rowBuffer[FileGDBPathsAndNames.StateNameColumn]  = "California";

      using (Feature firstFeature = featureClass.CreateRow(rowBuffer))

    using (RowBuffer rowBuffer = featureClass.CreateRowBuffer())
      rowBuffer[FileGDBPathsAndNames.CountyNameColumn] = "Irvine";
      rowBuffer[FileGDBPathsAndNames.StateNameColumn]  = "California";

      using (Feature secondFeature = featureClass.CreateRow(rowBuffer))

As shown in the above example, the ApplyEdits method takes an Action delegate, which encapsulates all the edits that must be performed in a transaction. The transaction is commited only if there are no exceptions thrown during the execution of the Action delegate. If there is any unhandled exception thrown during the execution, all the edits made up to that point are rolled back, and the transaction is aborted.

The following are the differences between how EditOperation.ExecuteAsync and Geodatabase.ApplyEdits work:

  • The ApplyEdits method can only be used to make edits within a single geodatabase, while the ExecuteAsync on the EditOperation allows interleaving edits to multiple geodatabases.
  • Unlike ExecuteAsync, ApplyEdits does not support Undo/Redo functionality.
  • While ExecuteAsync requires a SaveEdits or DiscardEdits call to close the edit session, ApplyEdits implicitly closes both the edit session and edit operation.
  • ApplyEdits does not support updating the map.

It should be noted that ApplyEdits is intended for editing outside ArcGIS Pro. For all editing in an add-in, EditOperation should be used.

Miscellaneous topics

MCT exclusions

As explained previously, all methods in the ArcGIS.Core.Data API must be invoked on the Main CIM Thread (MCT). However, some exclusions exist. ArcGIS.Core.Data.SpatialQueryFilter and ArcGIS.Core.Data.QueryFilter can be constructed on any thread; however, Search and Select using the QueryFilter still have to be called on the MCT.

When in doubt, refer to the triple-slash document to determine whether the method can be invoked on any thread.

Unique instancing

If you have previously developed code in ArcObjects that depends on the unique instancing characteristics of the Geodatabase API, you can use the Handle property on the ArcGIS.Core.Data API to implement the equivalent logic in the Pro SDK. The Handle can be used for efficient use as a dictionary key and for equality checks equivalent to comparing the pointers in ArcObjects.

As previously mentioned in the opening Architecture section, remember to explicitly Dispose unmanaged resources. For example, avoid code like the following:  

  if (row.GetTable().Handle == _handle)

when evaluating an object’s handle . This code will instantiate a managed Core.Data table instance whose reference to unmanaged resources will not be released until it is garbage collected. This could also have additional negative consequences if the code were being called in a loop that was creating millions of table instances that were not being disposed and had to be garbage collected.   Instead, an explicit reference to the table should be acquired each time and disposed after its Handle property has been evaluated.

  using (Table table = row.GetTable())
    if (table.Handle == _handle)
      // etc.

Known issues

When you obtain an ArcGIS.Core.Data.Table reference from FeatureLayer, for datastores other than file geodatabases and enterprise geodatabases and for methods other than Search and Select, there may be unexpected exceptions raised. In future releases, appropriate methods on the Table object for all the datastores will be supported.


Developing with ArcGIS Pro












    Relational Operations



Map Authoring

Map Exploration

    Map Tools




Utility Network

Workflow Manager


    2.0 Migration

Clone this wiki locally
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.