Skip to content

Commit

Permalink
Merge pull request #232 from intelligentplant/feature/misc-improvements
Browse files Browse the repository at this point in the history
Miscellaneous improvements
  • Loading branch information
wazzamatazz authored Oct 13, 2022
2 parents c895b04 + 3a133ac commit a044a99
Show file tree
Hide file tree
Showing 18 changed files with 234 additions and 43 deletions.
10 changes: 5 additions & 5 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,16 @@
<PackageVersion Include="Grpc.Core" Version="2.46.3" />
<PackageVersion Include="Grpc.Net.Client" Version="2.49.0" />
<PackageVersion Include="Grpc.Tools" Version="2.49.1" />
<PackageVersion Include="IntelligentPlant.BackgroundTasks" Version="8.1.0" />
<PackageVersion Include="IntelligentPlant.BackgroundTasks.AspNetCore" Version="8.1.0" />
<PackageVersion Include="IntelligentPlant.BackgroundTasks.DependencyInjection" Version="8.1.0" />
<PackageVersion Include="IntelligentPlant.BackgroundTasks" Version="8.2.0" />
<PackageVersion Include="IntelligentPlant.BackgroundTasks.AspNetCore" Version="8.2.0" />
<PackageVersion Include="IntelligentPlant.BackgroundTasks.DependencyInjection" Version="8.2.0" />
<PackageVersion Include="Jaahas.HttpRequestTransformer" Version="2.2.0" />
<PackageVersion Include="JsonSchema.Net.Generation" Version="3.0.3" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
<PackageVersion Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.9" />
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.10" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="6.0.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="6.0.9" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="6.0.10" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="2.2.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="2.2.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="2.2.0" />
Expand Down
2 changes: 1 addition & 1 deletion examples/ExampleHostedAdapter/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ internal static class Constants {
/// <summary>
/// The ID of the hosted adapter.
/// </summary>
public const string AdapterId = "$default";
public const string AdapterId = "e445a468-19ee-456c-9aac-e26288475a45";

/// <summary>
/// The path to the adapter settings JSON file.
Expand Down
25 changes: 24 additions & 1 deletion examples/ExampleHostedAdapter/Pages/Index.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,23 @@
ViewData["Title"] = "Home";
}

@Html.HiddenFor(x => x.Adapter.Descriptor.Id)

<div id="adapter-status">
@await Html.PartialAsync("_AdapterStatusPartial")
</div>

<div class="toast-container position-fixed top-0 end-0 p-3">
<div id="adapter-id-copied-notification" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body">
Adapter ID copied to clipboard
</div>
<button type="button" class="btn-close me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
</div>

@section Scripts {
<script defer>
$(() => {
Expand All @@ -24,7 +37,7 @@
try {
const response = await $.get('@Url.Page("Index", "Status")');
$('#adapter-status').html(response);
setLastUpdatedTime();;
setLastUpdatedTime();
}
finally {
updateInProgress = false;
Expand All @@ -36,6 +49,16 @@
setInterval(async () => {
await updateStatus();
}, 15000);
document.addEventListener('click', async (evt) => {
if (!event.composedPath().some(x => x.matches('.copy-adapter-id'))) {
return;
}
await navigator.clipboard.writeText($('#@Html.IdFor(x => x.Adapter.Descriptor.Id)').val());
const toast = new bootstrap.Toast($('#adapter-id-copied-notification')[0]);
toast.show();
});
});
</script>
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,12 @@
</div>
</div>
<div class="col">
<div class="pb-3"><code>@Model.Adapter.Descriptor.Id</code></div>
<div class="pb-3">
<code>@Model.Adapter.Descriptor.Id</code>
<button type="button" class="btn btn-sm copy-adapter-id" style="padding:0.15rem; border:none;" title="Copy adapter ID to clipboard">
<i class="fa-solid fa-copy fa-fw"></i>
</button>
</div>
</div>
</div>

Expand Down
18 changes: 15 additions & 3 deletions examples/ExampleHostedAdapter/README_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,17 @@ See [here](https://github.com/intelligentplant/AppStoreConnect.Adapters/src/Data

> By default, App Store Connect will validate the SSL certificate for your ASP.NET Core application. If you are using a self-signed certificate such as the ASP.NET Core developer certificate generated by the .NET SDK, certificate validation will fail if the certificate has not been installed to a certificate store where it can be accessed by the App Store Connect service identity.
You can connect to the adapter host using REST API calls or SignalR. Use SignalR if your adapter supports push functionality such as real-time value change subscriptions.
You can connect to the adapter host using REST API calls, SignalR, or gRPC. Use gRPC or SignalR if your adapter supports push functionality such as real-time value change subscriptions.

> App Store Connect must be running on Windows 11/Windows Server 2022 or later to be able to use gRPC.

## REST API

To connect a local App Store Connect instance to your adapter using the REST API, configure a new `App Store Connect Adapter (HTTP Proxy)` data source in the App Store Connect UI, using the following settings:

- `Address`: https://localhost:44300/
- `Adapter ID`: $default
- `Adapter ID`: e445a468-19ee-456c-9aac-e26288475a45

Note that you must disable SSL certificate verification during local development unless you have installed the ASP.NET Core development certificate to a certificate store that can be accessed by the App Store Connect service identity.

Expand All @@ -58,7 +60,17 @@ Note that you must disable SSL certificate verification during local development
To connect a local App Store Connect instance to your adapter using an ASP.NET Core SignalR proxy, configure a new `App Store Connect Adapter (SignalR Proxy)` data source in the App Store Connect UI, using the following settings:

- `Address`: https://localhost:44300/
- `Adapter ID`: $default
- `Adapter ID`: e445a468-19ee-456c-9aac-e26288475a45

Note that you must disable SSL certificate verification during local development unless you have installed the ASP.NET Core development certificate to a certificate store that can be accessed by the App Store Connect service identity.


## gRPC

To connect a local App Store Connect instance to your adapter using a gRPC proxy, configure a new `App Store Connect Adapter (gRPC Proxy)` data source in the App Store Connect UI, using the following settings:

- `Address`: https://localhost:44300/
- `Adapter ID`: e445a468-19ee-456c-9aac-e26288475a45

Note that you must disable SSL certificate verification during local development unless you have installed the ASP.NET Core development certificate to a certificate store that can be accessed by the App Store Connect service identity.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,20 @@ public interface IEventMessagePushWithTopics : IAdapterFeature {
/// active subscription should be created. Some adapters will only emit event messages
/// when they have at least one active subscriber.
/// </param>
/// <param name="channel">
/// A channel that will add topics to or remove topics from the subscription.
/// <param name="subscriptionUpdates">
/// An <see cref="IAsyncEnumerable{T}"/> that will add topics to or remove topics from
/// the subscription.
/// </param>
/// <param name="cancellationToken">
/// The cancellation token for the subscription.
/// </param>
/// <returns>
/// A channel reader that will emit event messages as they occur.
/// An <see cref="IAsyncEnumerable{T}"/> that will emit event messages as they occur.
/// </returns>
IAsyncEnumerable<EventMessage> Subscribe(
IAdapterCallContext context,
CreateEventMessageTopicSubscriptionRequest request,
IAsyncEnumerable<EventMessageSubscriptionUpdate> channel,
IAsyncEnumerable<EventMessageSubscriptionUpdate> subscriptionUpdates,
CancellationToken cancellationToken
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public interface ISnapshotTagValuePush : IAdapterFeature {
/// <param name="request">
/// A request describing the subscription settings.
/// </param>
/// <param name="channel">
/// <param name="subscriptionUpdates">
/// An <see cref="IAsyncEnumerable{T}"/> that will add tags to or remove tags from the
/// subscription.
/// </param>
Expand All @@ -37,7 +37,7 @@ public interface ISnapshotTagValuePush : IAdapterFeature {
IAsyncEnumerable<TagValueQueryResult> Subscribe(
IAdapterCallContext context,
CreateSnapshotTagValueSubscriptionRequest request,
IAsyncEnumerable<TagValueSubscriptionUpdate> channel,
IAsyncEnumerable<TagValueSubscriptionUpdate> subscriptionUpdates,
CancellationToken cancellationToken
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public static IMvcBuilder AddDataCoreAdapterMvc(this IMvcBuilder builder) {

builder.AddApplicationPart(typeof(MvcConfigurationExtensions).Assembly);
builder.Services.AddTransient<DataCore.Adapter.AspNetCore.IApiDescriptorProvider, DataCore.Adapter.AspNetCore.Mvc.Internal.ApiDescriptorProvider>();
builder.AddJsonOptions(options => options.JsonSerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter()));

return builder;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
using System.ComponentModel.DataAnnotations;
using System.Threading;
using System.Threading.Tasks;

using DataCore.Adapter.AspNetCore.SignalR.Client.Clients;

using Microsoft.AspNetCore.SignalR.Client;

namespace DataCore.Adapter.AspNetCore.SignalR.Client {
Expand Down Expand Up @@ -290,8 +292,15 @@ protected virtual void Dispose(bool disposing) {
return;
}

if (disposing && _disposeConnection) {
Task.Run(async () => await _hubConnection.DisposeAsync().ConfigureAwait(false)).GetAwaiter().GetResult();
if (disposing) {
if (_disposeConnection) {
_ = Task.Run(async () => {
try {
await _hubConnection.DisposeAsync().ConfigureAwait(false);
}
catch { }
});
}
}

_isDisposed = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,21 @@ public AdaptersClient(AdapterSignalRClient client) {
/// <returns>
/// A channel that will return information about the available adapters.
/// </returns>
public async Task<ChannelReader<AdapterDescriptor>> FindAdaptersAsync(
public async IAsyncEnumerable<AdapterDescriptor> FindAdaptersAsync(
FindAdaptersRequest request,
[EnumeratorCancellation]
CancellationToken cancellationToken = default
) {
AdapterSignalRClient.ValidateObject(request);

var connection = await _client.GetHubConnection(true, cancellationToken).ConfigureAwait(false);
return await connection.StreamAsChannelAsync<AdapterDescriptor>(
await foreach (var item in connection.StreamAsync<AdapterDescriptor>(
"FindAdapters",
request,
cancellationToken
).ConfigureAwait(false);
).ConfigureAwait(false)) {
yield return item;
}
}


Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
namespace DataCore.Adapter.AspNetCore.SignalR.Client {
using System;

namespace DataCore.Adapter.AspNetCore.SignalR.Client {

/// <summary>
/// Describes the compatibility level of the SignalR server that a <see cref="AdapterSignalRClient"/>
/// is connecting to.
/// </summary>
[Obsolete("ASP.NET Core 2.x is no longer supported", false)]
public enum CompatibilityLevel {
/// <summary>
/// The host application is running ASP.NET Core 3.x or later.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,7 @@ public static ISignalRServerBuilder AddDataCoreAdapterSignalR(this ISignalRServe
throw new ArgumentNullException(nameof(builder));
}

builder.AddJsonProtocol(options => {
options.PayloadSerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter());
});
builder.AddJsonProtocol(options => options.PayloadSerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter()));

builder.Services.AddTransient<DataCore.Adapter.AspNetCore.IApiDescriptorProvider, DataCore.Adapter.AspNetCore.SignalR.Internal.ApiDescriptorProvider>();
return builder;
Expand Down
75 changes: 65 additions & 10 deletions src/DataCore.Adapter.Http.Proxy/HttpAdapterProxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,16 @@ private set {
/// </summary>
private readonly AdapterHttpClient _client;

/// <summary>
/// The last health check result that was received from the remote adapter.
/// </summary>
private HealthCheckResult? _lastRemoteHealthCheckResult;

/// <summary>
/// Lock for reading/writing <see cref="_lastRemoteHealthCheckResult"/>.
/// </summary>
private readonly Nito.AsyncEx.AsyncReaderWriterLock _lastRemoteHealthCheckResultLock = new Nito.AsyncEx.AsyncReaderWriterLock();


/// <summary>
/// Creates a new <see cref="HttpAdapterProxy"/> object.
Expand Down Expand Up @@ -384,6 +394,7 @@ private async Task RunPollingRemoteHealthSubscriptionAsync(CancellationToken can
if (interval > TimeSpan.Zero) {
do {
await Task.Delay(interval, cancellationToken).ConfigureAwait(false);

OnHealthStatusChanged();
} while (!cancellationToken.IsCancellationRequested);
}
Expand All @@ -401,9 +412,26 @@ private async Task RunPollingRemoteHealthSubscriptionAsync(CancellationToken can
/// A <see cref="Task"/> that will monitor for changes in the remote adapter health.
/// </returns>
private async Task RunPushRemoteHealthSubscriptionAsync(CancellationToken cancellationToken) {
OnHealthStatusChanged();

var client = GetSignalRClient(new DefaultAdapterCallContext());
await foreach (var item in client.Client.Adapters.CreateAdapterHealthChannelAsync(RemoteDescriptor.Id, cancellationToken).ConfigureAwait(false)) {
OnHealthStatusChanged();
while (!cancellationToken.IsCancellationRequested) {
try {
await foreach (var item in client.Client.Adapters.CreateAdapterHealthChannelAsync(RemoteDescriptor.Id, cancellationToken).ConfigureAwait(false)) {
using (await _lastRemoteHealthCheckResultLock.WriterLockAsync(cancellationToken).ConfigureAwait(false)) {
_lastRemoteHealthCheckResult = CreateRemoteAdapterHealthCheckResult(item);
}
OnHealthStatusChanged();
}
}
catch {
if (!cancellationToken.IsCancellationRequested) {
using (await _lastRemoteHealthCheckResultLock.WriterLockAsync(cancellationToken).ConfigureAwait(false)) {
_lastRemoteHealthCheckResult = null;
}
OnHealthStatusChanged();
}
}
}
}

Expand Down Expand Up @@ -438,14 +466,7 @@ CancellationToken cancellationToken
.CheckAdapterHealthAsync(RemoteDescriptor.Id, context?.ToRequestMetadata(), cancellationToken)
.ConfigureAwait(false);

return new HealthCheckResult(
Resources.HealthCheck_DisplayName_RemoteAdapter,
result.Status,
result.Description,
result.Error,
result.Data,
result.InnerResults
);
return CreateRemoteAdapterHealthCheckResult(result);
}
catch (Exception e) {
return HealthCheckResult.Unhealthy(
Expand All @@ -456,13 +477,47 @@ CancellationToken cancellationToken
}


/// <summary>
/// Converts a <see cref="HealthCheckResult"/> received from a remote adapter into a local
/// <see cref="HealthCheckResult"/> for the proxy.
/// </summary>
/// <param name="result">
/// The <see cref="HealthCheckResult"/> received from the remote adapter.
/// </param>
/// <returns>
/// A new <see cref="HealthCheckResult"/> for use in the local proxy.
/// </returns>
private static HealthCheckResult CreateRemoteAdapterHealthCheckResult(HealthCheckResult result) {
return new HealthCheckResult(
Resources.HealthCheck_DisplayName_RemoteAdapter,
result.Status,
result.Description,
result.Error,
result.Data,
result.InnerResults
);
}


/// <inheritdoc/>
protected override async Task<IEnumerable<HealthCheckResult>> CheckHealthAsync(IAdapterCallContext context, CancellationToken cancellationToken) {
var results = new List<HealthCheckResult>(await base.CheckHealthAsync(context, cancellationToken).ConfigureAwait(false));
if (!IsRunning) {
return results;
}

using (await _lastRemoteHealthCheckResultLock.ReaderLockAsync(cancellationToken).ConfigureAwait(false)) {
if (_lastRemoteHealthCheckResult != null) {
results.Add(HealthCheckResult.Composite(
Resources.HealthCheck_DisplayName_Connection,
new[] { _lastRemoteHealthCheckResult.Value },
Resources.HealthChecks_RemoteHeathDescription
));

return results;
}
}

results.Add(
HealthCheckResult.Composite(
Resources.HealthCheck_DisplayName_Connection,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
},
"sourceName": "ExampleHostedAdapter",
"preferNameDirectory": true,
"guids": [],
"guids": [
"e445a468-19ee-456c-9aac-e26288475a45"
],
"symbols": {
"Framework": {
"type": "parameter",
Expand Down
Loading

0 comments on commit a044a99

Please sign in to comment.