Skip to content

Commit dfacc6a

Browse files
authored
Merge pull request #310474 from dominicbetts/aio-connector-build-tutorial
AIO: Add Akri connector walkthrough
2 parents 3697c79 + ab9ba41 commit dfacc6a

11 files changed

Lines changed: 675 additions & 179 deletions

articles/iot-operations/develop-edge-apps/howto-build-akri-connectors-vscode.md

Lines changed: 1 addition & 176 deletions
Original file line numberDiff line numberDiff line change
@@ -77,182 +77,7 @@ In this example, you create an HTTP/REST connector using the C# language, build
7777

7878
1. The extension creates a new workspace named by using the connector name you chose in the previous step. The workspace includes the scaffolding for a polling telemetry connector written in the C# language.
7979

80-
To represent the thermostat status and data point configuration, add the following model files to the project:
81-
82-
1. Create a file called **ThermostatStatus.cs** in the `<connector_name>` folder in the workspace with the following content. This file models the JSON response. Replace the `<connector_name>` placeholder with the name you chose for the connector:
83-
84-
```c#
85-
using System.Text.Json.Serialization;
86-
87-
namespace <connector_name>
88-
{
89-
internal class ThermostatStatus
90-
{
91-
[JsonPropertyName("desiredTemperature")]
92-
public double? DesiredTemperature { get; set; }
93-
94-
[JsonPropertyName("currentTemperature")]
95-
public double? CurrentTemperature { get; set; }
96-
}
97-
}
98-
```
99-
100-
1. Create a file called **DataPointConfiguration.cs** in the `<connector_name>` folder in the workspace with the following content. This file models the JSON response. Replace the `<connector_name>` placeholder with the name you chose for the connector:
101-
102-
```c#
103-
using System.Text.Json.Serialization;
104-
105-
namespace <connector_name>
106-
{
107-
public class DataPointConfiguration
108-
{
109-
[JsonPropertyName("HttpRequestMethod")]
110-
public string? HttpRequestMethod { get; set; }
111-
}
112-
}
113-
```
114-
115-
Implement the `SampleDatasetAsync` method in the provided `DatasetSampler` class. The method takes a `Dataset` as a parameter. A `Dataset` contains the data points for the connector to process.
116-
117-
1. Open the file `<connector_name>/DatasetSampler.cs` in your VS Code workspace.
118-
119-
1. Add a constructor to the `DatasetSampler` class to pass in the required data for processing the endpoint data. The class uses the `HttpClient` and `EndpointProfileCredentials` to connect to and authenticate with the asset endpoint:
120-
121-
```c#
122-
private readonly HttpClient _httpClient;
123-
private readonly string _assetName;
124-
private readonly EndpointCredentials? _credentials;
125-
126-
private readonly static JsonSerializerOptions _jsonSerializerOptions = new()
127-
{
128-
AllowTrailingCommas = true,
129-
};
130-
131-
public DatasetSampler(HttpClient httpClient, string assetName, EndpointCredentials? credentials)
132-
{
133-
_httpClient = httpClient;
134-
_assetName = assetName;
135-
_credentials = credentials;
136-
}
137-
138-
public ValueTask DisposeAsync()
139-
{
140-
_httpClient.Dispose();
141-
return ValueTask.CompletedTask;
142-
}
143-
```
144-
145-
1. Modify the `GetSamplingIntervalAsync` method to return a sampling interval of three seconds:
146-
147-
```c#
148-
public Task<TimeSpan> GetSamplingIntervalAsync(AssetDataset dataset, CancellationToken cancellationToken = default)
149-
{
150-
return Task.FromResult(TimeSpan.FromSeconds(3));
151-
}
152-
```
153-
154-
1. In the `SampleDatasetAsync` method, add the following code to retrieve each `DataPoint` from the `DataSet` and extract the data source paths. These paths are URLs used to fetch the data from the REST endpoint. The `currentTemperature` and `desiredTemperature` data points were modeled previously in the `ThermostatStatus` class:
155-
156-
```c#
157-
AssetDatasetDataPointSchemaElement httpServerDesiredTemperatureDataPoint = dataset.DataPoints!.Where(x => x.Name!.Equals("desiredTemperature"))!.First();
158-
HttpMethod httpServerDesiredTemperatureHttpMethod = HttpMethod.Parse(JsonSerializer.Deserialize<DataPointConfiguration>(httpServerDesiredTemperatureDataPoint.DataPointConfiguration!, _jsonSerializerOptions)!.HttpRequestMethod);
159-
string httpServerDesiredTemperatureRequestPath = httpServerDesiredTemperatureDataPoint.DataSource!;
160-
161-
AssetDatasetDataPointSchemaElement httpServerCurrentTemperatureDataPoint = dataset.DataPoints!.Where(x => x.Name!.Equals("currentTemperature"))!.First();
162-
HttpMethod httpServerCurrentTemperatureHttpMethod = HttpMethod.Parse(JsonSerializer.Deserialize<DataPointConfiguration>(httpServerDesiredTemperatureDataPoint.DataPointConfiguration!, _jsonSerializerOptions)!.HttpRequestMethod);
163-
string httpServerCurrentTemperatureRequestPath = httpServerCurrentTemperatureDataPoint.DataSource!;
164-
```
165-
166-
1. In the same method, set up the authentication by using the provided credentials if authenticated endpoints are in use:
167-
168-
This code extracts the credentials and adds them to the authorization header. The `DatasetSampler` implements basic authentication with username and password credentials.
169-
170-
```c#
171-
if (_credentials != null && _credentials.Username != null && _credentials.Password != null)
172-
{
173-
// Note that this sample uses username + password for authenticating the connection to the asset. In general,
174-
// x509 authentication should be used instead (if available) as it is more secure.
175-
string httpServerUsername = _credentials.Username;
176-
string httpServerPassword = _credentials.Password;
177-
var byteArray = Encoding.ASCII.GetBytes($"{httpServerUsername}:{httpServerPassword}");
178-
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(byteArray));
179-
}
180-
```
181-
182-
1. Then add code to make an HTTP request to the endpoint, deserialize the response, and extract both the `CurrentTemperature` and `DesiredTemperature` properties and place them in a `ThermostatStatus` object:
183-
184-
```c#
185-
// In this sample, both the datapoints have the same datasource, so only one HTTP request is needed.
186-
var currentTemperatureHttpResponse = await _httpClient.GetAsync(httpServerCurrentTemperatureRequestPath);
187-
var desiredTemperatureHttpResponse = await _httpClient.GetAsync(httpServerDesiredTemperatureRequestPath);
188-
189-
if (currentTemperatureHttpResponse.StatusCode == System.Net.HttpStatusCode.Unauthorized
190-
|| desiredTemperatureHttpResponse.StatusCode == System.Net.HttpStatusCode.Unauthorized)
191-
{
192-
throw new Exception("Failed to authorize request to HTTP server. Check credentials configured in rest-server-device-definition.yaml.");
193-
}
194-
195-
currentTemperatureHttpResponse.EnsureSuccessStatusCode();
196-
desiredTemperatureHttpResponse.EnsureSuccessStatusCode();
197-
198-
ThermostatStatus thermostatStatus = new()
199-
{
200-
CurrentTemperature = (JsonSerializer.Deserialize<ThermostatStatus>(await currentTemperatureHttpResponse.Content.ReadAsStreamAsync(), _jsonSerializerOptions)!).CurrentTemperature,
201-
DesiredTemperature = (JsonSerializer.Deserialize<ThermostatStatus>(await desiredTemperatureHttpResponse.Content.ReadAsStreamAsync(), _jsonSerializerOptions)!).DesiredTemperature
202-
};
203-
```
204-
205-
1. Add the `async` keyword to the method:
206-
207-
```csharp
208-
public async Task<byte[]> SampleDatasetAsync(AssetDataset dataset, CancellationToken cancellationToken = default)
209-
```
210-
211-
212-
1. Next, serialize the status to JSON and return the response to the endpoint. In this example, the HTTP response payload already matches the expected message schema, so no translation is necessary:
213-
214-
```c#
215-
// The HTTP response payload matches the expected message schema, so return it as-is
216-
return Encoding.UTF8.GetBytes(JsonSerializer.Serialize(thermostatStatus));
217-
```
218-
219-
1. Finally, import the necessary types:
220-
221-
```c#
222-
using Azure.Iot.Operations.Connector.Files;
223-
using System.Net.Http.Headers;
224-
using System.Text;
225-
using System.Text.Json;
226-
```
227-
228-
The final version of the code looks like [DatasetSampler](https://raw.githubusercontent.com/Azure/iot-operations-sdks/refs/heads/main/dotnet/samples/Connectors/PollingRestThermostatConnector/ThermostatStatusDatasetSampler.cs). For resilience, add the try-catch block to your code to process any exceptions.
229-
230-
Implement the `CreateDatasetSampler` method in the `DatasetSamplerProvider` class. This class creates `DataSetSampler` objects to inject into the application as required.
231-
232-
1. Open the `<connector_name>/DatasetSamplerProvider.cs` file in your VS Code workspace.
233-
234-
1. In the `CreateDatasetSampler` method, return a `DatasetSampler` along with the `endpointCredentials`, if the dataset name is `thermostat_status`:
235-
236-
```c#
237-
if (dataset.Name.Equals("thermostat_status"))
238-
{
239-
if (device.Endpoints != null
240-
&& device.Endpoints.Inbound != null
241-
&& device.Endpoints.Inbound.TryGetValue(inboundEndpointName, out var inboundEndpoint))
242-
{
243-
var httpClient = new HttpClient()
244-
{
245-
BaseAddress = new Uri(inboundEndpoint.Address),
246-
};
247-
248-
return new DatasetSampler(httpClient, assetName, endpointCredentials);
249-
}
250-
}
251-
252-
throw new InvalidOperationException($"Unrecognized dataset with name {dataset.Name} on asset with name {assetName}");
253-
```
254-
255-
The final version of the code looks like [DatasetSamplerProvider](https://raw.githubusercontent.com/Azure/iot-operations-sdks/refs/heads/main/dotnet/samples/Connectors/PollingRestThermostatConnector/RestThermostatDatasetSamplerProvider.cs).
80+
[!INCLUDE [akri-connector-code](../includes/akri-connector-code.md)]
25681

25782
Next, build the project to confirm there are no errors. Use the VS Code command **Azure IoT Operations Akri Connectors: Build an Akri Connector** and choose the **Release** mode. This command shows the build progress in the **OUTPUT** console and notifies you when the build completes. You can then see a new Docker image named `<connector_name>` with tag `release` locally in Docker Desktop.
25883

0 commit comments

Comments
 (0)