This is a sample project I created to learn Go. There's an existing SDK for working with Azure Digital Twin but only for the management plain, so I thought I'd try and write a small application which uses the Query API, but also handle the authentication flow. I've previously written a Fluent API for the Digital Twin Query API in C# and wanted to try something like it for Go using Generics. That's what this has become.
The authentication flow needs to read some values from environment variables. You can do this by either
creating the environment variables, or by creating a .env
file.
TWIN_URL=https://<twin instance>.<region>.digitaltwins.azure.net
TWIN_CLIENT_ID=<application id>
TWIN_CLIENT_SECRET=<client secret>
TWIN_TENANT_ID=<directory id>
The builder process uses models which implement the models.IModel
interface. Each twin in Azure Digital Twin
has a $dtId
, $etag
, and $metadata
value, so there is a GenericModel
type which can be inherited so that
these items are handled for you. Each model needs to specify its model type so that its type can be validated
if needed. An example model would look as follows.
package myontology
import (
"azure-adt-example/digitaltwin/models"
)
type MyOntologyType struct {
models.GenericModel
Name string `json:"name"`
}
func (MyOntologyType) Model() string {
return "dtmi:digitaltwins:rec_3_3:agents:MyOntologyType;1"
}
func (MyOntologyType) Alias() string {
return models.GetModelAlias[MyOntologyType]()
}
func (MyOntologyType) ValidationClause() string {
return models.ModelValidationClause[MyOntologyType]()
}
The Alias
and ValidationClause
can be manually specified, but there are helper methods to generate the
correct values. The GetModelAlias
returns the name of the type in lowercase.
Once the models for the ontology have been specified the twin can be queried as follows.
config := azuread.NewTwinConfiguration()
// An authentication token can be provided, but if it isn't then the client will get it's own
client := digitaltwin.NewClient(config, nil)
from := rec33.Company{}
// Create a new builder using Company as the base twin type
builder := digitaltwin.NewBuilder(from, false)
var err error
// Add a join from Company to Building where the company "owns" the building
if err = builder.AddJoin(from, rec33.Building{}, "owns", false); err != nil {
log.Fatal(err)
}
// Add a join to Level where it is part of the building
if err = builder.AddJoin(rec33.Building{}, rec33.Level{}, "isPartOf", true); err != nil {
log.Fatal(err)
}
// Add a where clause for the query
if err = builder.WhereId(from, "<company id>"); err != nil {
log.Fatal(err)
}
// Execute the generated query and return the company, building, and level objects from the results.
// There are also methods for a single return type and 2 return types
results, err := digitaltwin.ExecuteBuilder3[rec33.Company, rec33.Building, rec33.Level](client, builder)
if err != nil {
log.Fatal(err)
}
// Output the names of the company, building, and level. Because of Generics the `TwinX` fields are
// typed and so access to all of the properties is available
for _, row := range results {
fmt.Printf("%s is part of %s owned by %s\n", row.Twin3.Name, row.Twin2.Name, row.Twin1.Name)
}
This is a side project for teaching myself, but I'm putting it out there in case anyone else finds it useful.
There's still a number of things I want to implement.
- Extend return types up to 5 types (5 is the maximum number of relationships in a query unless using
MATCH
which is still in preview) - Clean the interface up a bit to make it more obvious
- Add where clause building which includes all the available functions from Digital Twin
- UNIT TESTING