Initial commit.
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -180,15 +180,15 @@ DocProject/Help/Html2
|
|||||||
DocProject/Help/html
|
DocProject/Help/html
|
||||||
|
|
||||||
# Click-Once directory
|
# Click-Once directory
|
||||||
publish/
|
[Pp]ublish/
|
||||||
|
|
||||||
# Publish Web Output
|
# Publish Web Output
|
||||||
*.[Pp]ublish.xml
|
*.[Pp]ublish.xml
|
||||||
*.azurePubxml
|
*.azurePubxml
|
||||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||||
# but database connection strings (with potential passwords) will be unencrypted
|
# but database connection strings (with potential passwords) will be unencrypted
|
||||||
*.pubxml
|
#*.pubxml
|
||||||
*.publishproj
|
#*.publishproj
|
||||||
|
|
||||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||||
|
|||||||
45
ApiClient/ApiClient.csproj
Normal file
45
ApiClient/ApiClient.csproj
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<TargetFramework>net10.0-windows</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<UseWPF>true</UseWPF>
|
||||||
|
|
||||||
|
<RootNamespace>ClientApiPoC.ApiClient</RootNamespace>
|
||||||
|
|
||||||
|
<PublishSingleFile>true</PublishSingleFile>
|
||||||
|
<SelfContained>true</SelfContained>
|
||||||
|
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
||||||
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
|
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
|
||||||
|
<PublishTrimmed>false</PublishTrimmed>
|
||||||
|
|
||||||
|
<Company>Mike Schumann</Company>
|
||||||
|
<Copyright>Copyright © 2026 $(Company)</Copyright>
|
||||||
|
<AssemblyVersion>0.1.0</AssemblyVersion>
|
||||||
|
<FileVersion>$(AssemblyVersion)</FileVersion>
|
||||||
|
<NeutralLanguage>en-US</NeutralLanguage>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
|
||||||
|
<DebugType>portable</DebugType>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)'=='Release'">
|
||||||
|
<DebugType>none</DebugType>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.5" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.5" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Shared.Wpf\Shared.Wpf.csproj" />
|
||||||
|
<ProjectReference Include="..\Shared\Shared.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
8
ApiClient/App.xaml
Normal file
8
ApiClient/App.xaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<Application x:Class="ClientApiPoC.ApiClient.App"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:local="clr-namespace:ClientApiPoC.ApiClient">
|
||||||
|
<Application.Resources>
|
||||||
|
|
||||||
|
</Application.Resources>
|
||||||
|
</Application>
|
||||||
33
ApiClient/App.xaml.cs
Normal file
33
ApiClient/App.xaml.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace ClientApiPoC.ApiClient {
|
||||||
|
public partial class App : Application {
|
||||||
|
private IHost _host;
|
||||||
|
|
||||||
|
public App() {
|
||||||
|
_host = Host.CreateDefaultBuilder().ConfigureServices(services => {
|
||||||
|
// ViewModels:
|
||||||
|
services.AddTransient<MainWindowViewModel>();
|
||||||
|
// Views:
|
||||||
|
services.AddSingleton<MainWindow>();
|
||||||
|
}).Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async void OnStartup(StartupEventArgs e) {
|
||||||
|
await _host.StartAsync();
|
||||||
|
|
||||||
|
var window = _host.Services.GetRequiredService<MainWindow>();
|
||||||
|
window.Show();
|
||||||
|
|
||||||
|
base.OnStartup(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async void OnExit(ExitEventArgs e) {
|
||||||
|
await _host.StopAsync();
|
||||||
|
_host.Dispose();
|
||||||
|
base.OnExit(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
ApiClient/AssemblyInfo.cs
Normal file
10
ApiClient/AssemblyInfo.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
[assembly: ThemeInfo(
|
||||||
|
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
|
||||||
|
//(used if a resource is not found in the page,
|
||||||
|
// or application resource dictionaries)
|
||||||
|
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
|
||||||
|
//(used if a resource is not found in the page,
|
||||||
|
// app, or any theme specific resource dictionaries)
|
||||||
|
)]
|
||||||
62
ApiClient/MainWindow.xaml
Normal file
62
ApiClient/MainWindow.xaml
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<wpf:MvvmWindow x:TypeArguments="local:MainWindowViewModel"
|
||||||
|
x:Class="ClientApiPoC.ApiClient.MainWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:local="clr-namespace:ClientApiPoC.ApiClient"
|
||||||
|
xmlns:wpf="clr-namespace:ClientApiPoC.Shared.Wpf;assembly=Shared.Wpf"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
Title="API-Client"
|
||||||
|
Width="512"
|
||||||
|
Height="384"
|
||||||
|
ResizeMode="CanMinimize">
|
||||||
|
<Grid RowDefinitions="AUTO, AUTO, *"
|
||||||
|
ColumnDefinitions="*, AUTO"
|
||||||
|
Margin="12">
|
||||||
|
|
||||||
|
<Grid Grid.Row="0"
|
||||||
|
Grid.Column="0"
|
||||||
|
RowDefinitions="AUTO, AUTO"
|
||||||
|
ColumnDefinitions="*">
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="0"
|
||||||
|
Grid.Column="0"
|
||||||
|
Text="Base-URL:" />
|
||||||
|
|
||||||
|
<TextBox Grid.Row="1"
|
||||||
|
Grid.Column="0"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
IsEnabled="{Binding ControlsEnabled}"
|
||||||
|
Text="{Binding ApiServiceUrl}" />
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Button Grid.Row="0"
|
||||||
|
Grid.Column="1"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
Padding="5"
|
||||||
|
Margin="5, 0, 0, 0"
|
||||||
|
IsDefault="True"
|
||||||
|
Content="HTTP-GET"
|
||||||
|
IsEnabled="{Binding ControlsEnabled}"
|
||||||
|
Command="{Binding HttpGetButtonCommand}" />
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="1"
|
||||||
|
Grid.Column="0"
|
||||||
|
Grid.ColumnSpan="2"
|
||||||
|
Margin="0, 5, 0, 0"
|
||||||
|
Text="Result:" />
|
||||||
|
|
||||||
|
<TextBox Grid.Row="2"
|
||||||
|
Grid.Column="0"
|
||||||
|
Grid.ColumnSpan="2"
|
||||||
|
Padding="5"
|
||||||
|
HorizontalScrollBarVisibility="Auto"
|
||||||
|
VerticalScrollBarVisibility="Auto"
|
||||||
|
IsReadOnly="True"
|
||||||
|
FontFamily="Consolas"
|
||||||
|
Text="{Binding RequestResultDataString, Mode=OneWay}" />
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
</wpf:MvvmWindow>
|
||||||
10
ApiClient/MainWindow.xaml.cs
Normal file
10
ApiClient/MainWindow.xaml.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using ClientApiPoC.Shared.Wpf;
|
||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace ClientApiPoC.ApiClient {
|
||||||
|
public partial class MainWindow : MvvmWindow<MainWindowViewModel> {
|
||||||
|
public MainWindow(MainWindowViewModel viewModel) : base(viewModel) {
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
92
ApiClient/MainWindowViewModel.cs
Normal file
92
ApiClient/MainWindowViewModel.cs
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
using ClientApiPoC.Shared;
|
||||||
|
using ClientApiPoC.Shared.Models;
|
||||||
|
using ClientApiPoC.Shared.Wpf;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Windows.Input;
|
||||||
|
|
||||||
|
namespace ClientApiPoC.ApiClient {
|
||||||
|
public class MainWindowViewModel : BaseViewModel {
|
||||||
|
#region Properties
|
||||||
|
private string _apiServiceUrl = "http://localhost:5228/";
|
||||||
|
public string ApiServiceUrl {
|
||||||
|
get => _apiServiceUrl;
|
||||||
|
set {
|
||||||
|
_apiServiceUrl = value;
|
||||||
|
OnPropertyChanged(nameof(ApiServiceUrl));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _isFetchingData = false;
|
||||||
|
public bool IsFetchingData {
|
||||||
|
get => _isFetchingData;
|
||||||
|
set {
|
||||||
|
_isFetchingData = value;
|
||||||
|
OnPropertyChanged(nameof(IsFetchingData));
|
||||||
|
OnPropertyChanged(nameof(ControlsEnabled));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ControlsEnabled => !this.IsFetchingData;
|
||||||
|
|
||||||
|
private ExampleRequestResultModel? _requestResultData = null;
|
||||||
|
public ExampleRequestResultModel? RequestResultData {
|
||||||
|
get => _requestResultData;
|
||||||
|
set {
|
||||||
|
_requestResultData = value;
|
||||||
|
OnPropertyChanged(nameof(RequestResultData));
|
||||||
|
OnPropertyChanged(nameof(RequestResultDataString));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string RequestResultDataString {
|
||||||
|
get {
|
||||||
|
if (this.RequestResultData == null) return "NULL";
|
||||||
|
var options = new JsonSerializerOptions() {
|
||||||
|
IndentCharacter = ' ',
|
||||||
|
IndentSize = 2,
|
||||||
|
NewLine = "\n",
|
||||||
|
WriteIndented = true
|
||||||
|
};
|
||||||
|
return JsonSerializer.Serialize(this.RequestResultData, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Commands
|
||||||
|
public ICommand HttpGetButtonCommand { get; }
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public MainWindowViewModel() {
|
||||||
|
this.HttpGetButtonCommand = new AsyncRelayCommand(HttpGetButtonActionAsync);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HttpGetButtonActionAsync() {
|
||||||
|
if (this.IsFetchingData) return;
|
||||||
|
try {
|
||||||
|
this.IsFetchingData = true;
|
||||||
|
var url = this.ApiServiceUrl;
|
||||||
|
if (string.IsNullOrWhiteSpace(url)) throw new InvalidOperationException("No base url specified.");
|
||||||
|
while (url.EndsWith('/')) url = url[..^1];
|
||||||
|
url += Routes.EXAMPLE_REQUEST;
|
||||||
|
using (var httpClient = Tools.CreateHttpClient()) {
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||||
|
var response = await httpClient.SendAsync(request);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
var responseString = await response.Content.ReadAsStringAsync();
|
||||||
|
var options = new JsonSerializerOptions() {
|
||||||
|
PropertyNameCaseInsensitive = true
|
||||||
|
};
|
||||||
|
var result = JsonSerializer.Deserialize<ExampleRequestResultModel>(responseString, options);
|
||||||
|
this.RequestResultData = result;
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
await HandleErrorAsync(ex);
|
||||||
|
} finally {
|
||||||
|
this.IsFetchingData = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
ApiClient/Properties/PublishProfiles/FolderProfile.pubxml
Normal file
15
ApiClient/Properties/PublishProfiles/FolderProfile.pubxml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
|
||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<Configuration>Release</Configuration>
|
||||||
|
<Platform>Any CPU</Platform>
|
||||||
|
<PublishDir>..\Publish\</PublishDir>
|
||||||
|
<PublishProtocol>FileSystem</PublishProtocol>
|
||||||
|
<_TargetId>Folder</_TargetId>
|
||||||
|
<TargetFramework>net10.0-windows</TargetFramework>
|
||||||
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
|
<SelfContained>true</SelfContained>
|
||||||
|
<PublishReadyToRun>false</PublishReadyToRun>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
41
ApiService/ApiService.csproj
Normal file
41
ApiService/ApiService.csproj
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
|
||||||
|
<RootNamespace>ClientApiPoC.ApiService</RootNamespace>
|
||||||
|
|
||||||
|
<PublishSingleFile>true</PublishSingleFile>
|
||||||
|
<SelfContained>true</SelfContained>
|
||||||
|
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
||||||
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
|
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
|
||||||
|
<PublishTrimmed>false</PublishTrimmed>
|
||||||
|
|
||||||
|
<Company>Mike Schumann</Company>
|
||||||
|
<Copyright>Copyright © 2026 $(Company)</Copyright>
|
||||||
|
<AssemblyVersion>0.1.0</AssemblyVersion>
|
||||||
|
<FileVersion>$(AssemblyVersion)</FileVersion>
|
||||||
|
<NeutralLanguage>en-US</NeutralLanguage>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
|
||||||
|
<DebugType>portable</DebugType>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)'=='Release'">
|
||||||
|
<DebugType>none</DebugType>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.5" />
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.1.5" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Shared\Shared.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
27
ApiService/Controllers/ExampleRequestController.cs
Normal file
27
ApiService/Controllers/ExampleRequestController.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using ClientApiPoC.Shared;
|
||||||
|
using ClientApiPoC.Shared.Models;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace ClientApiPoC.ApiService.Controllers {
|
||||||
|
[ApiController]
|
||||||
|
[Route(Routes.EXAMPLE_REQUEST)]
|
||||||
|
public class ExampleRequestController : ControllerBase {
|
||||||
|
private readonly TunnelServer _tunnel;
|
||||||
|
|
||||||
|
public ExampleRequestController(TunnelServer tunnel) {
|
||||||
|
if (tunnel == null) throw new ArgumentNullException(nameof(tunnel));
|
||||||
|
_tunnel = tunnel;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<ExampleRequestResultModel> Get() {
|
||||||
|
// Daten der Clients abfragen...
|
||||||
|
var clients = await _tunnel.GetDataFromAllClientsAsync();
|
||||||
|
var result = new ExampleRequestResultModel() {
|
||||||
|
ServerMessage = "Hello, world!",
|
||||||
|
Clients = clients
|
||||||
|
};
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
ApiService/Program.cs
Normal file
34
ApiService/Program.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
using ClientApiPoC.Shared;
|
||||||
|
using ClientApiPoC.Shared.SignalR;
|
||||||
|
|
||||||
|
namespace ClientApiPoC.ApiService {
|
||||||
|
public class Program {
|
||||||
|
public static void Main(string[] args) {
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
builder.Services.AddControllers();
|
||||||
|
builder.Services.AddOpenApi();
|
||||||
|
|
||||||
|
builder.Services.AddTunnelHub();
|
||||||
|
builder.Services.AddTransient<TunnelServer>();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
// Configure the HTTP request pipeline.
|
||||||
|
if (app.Environment.IsDevelopment()) {
|
||||||
|
app.MapOpenApi();
|
||||||
|
app.UseSwaggerUI(options => {
|
||||||
|
options.SwaggerEndpoint("/openapi/v1.json", "ClientApiPoC");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
app.UseAuthorization();
|
||||||
|
|
||||||
|
app.MapControllers();
|
||||||
|
|
||||||
|
app.MapTunnelHub(Routes.TUNNEL_PATH);
|
||||||
|
|
||||||
|
app.Run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
ApiService/Properties/PublishProfiles/FolderProfile.pubxml
Normal file
20
ApiService/Properties/PublishProfiles/FolderProfile.pubxml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
|
||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<DeleteExistingFiles>true</DeleteExistingFiles>
|
||||||
|
<ExcludeApp_Data>false</ExcludeApp_Data>
|
||||||
|
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
|
||||||
|
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
|
||||||
|
<LastUsedPlatform>Any CPU</LastUsedPlatform>
|
||||||
|
<PublishProvider>FileSystem</PublishProvider>
|
||||||
|
<PublishUrl>..\Publish\ApiService\</PublishUrl>
|
||||||
|
<WebPublishMethod>FileSystem</WebPublishMethod>
|
||||||
|
<_TargetId>Folder</_TargetId>
|
||||||
|
<SiteUrlToLaunchAfterPublish />
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
|
<ProjectGuid>bb6e670a-3d3d-430e-4dd4-25ad2c7d9c26</ProjectGuid>
|
||||||
|
<SelfContained>true</SelfContained>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
14
ApiService/Properties/launchSettings.json
Normal file
14
ApiService/Properties/launchSettings.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"profiles": {
|
||||||
|
"http": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"launchUrl": "swagger",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
},
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"applicationUrl": "http://*:5228"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"$schema": "https://json.schemastore.org/launchsettings.json"
|
||||||
|
}
|
||||||
24
ApiService/TunnelServer.cs
Normal file
24
ApiService/TunnelServer.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
using ClientApiPoC.Shared.SignalR;
|
||||||
|
using ClientApiPoC.Shared.Models;
|
||||||
|
|
||||||
|
namespace ClientApiPoC.ApiService {
|
||||||
|
public class TunnelServer : TunnelServerBase {
|
||||||
|
public TunnelServer(IHubContext<TunnelHub> hubContext, ClientTracker clientTracker) : base(hubContext, clientTracker) { }
|
||||||
|
|
||||||
|
public async Task<IEnumerable<ClientResultModel>> GetDataFromAllClientsAsync() {
|
||||||
|
var timestampServer = DateTime.UtcNow;
|
||||||
|
var results = new List<ClientResultModel>();
|
||||||
|
var clients = this.GetAllClients();
|
||||||
|
foreach (var client in clients) {
|
||||||
|
var clientData = await client.Value.InvokeAsync<ClientDataModel>("GetClientData", timestampServer, CancellationToken.None);
|
||||||
|
var result = new ClientResultModel() {
|
||||||
|
ClientId = client.Key,
|
||||||
|
ClientData = clientData
|
||||||
|
};
|
||||||
|
results.Add(result);
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
ApiService/appsettings.Development.json
Normal file
8
ApiService/appsettings.Development.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
ApiService/appsettings.json
Normal file
16
ApiService/appsettings.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*",
|
||||||
|
"Kestrel": {
|
||||||
|
"Endpoints": {
|
||||||
|
"Http": {
|
||||||
|
"Url": "http://*:5228"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
ApiService/dotnet-tools.json
Normal file
13
ApiService/dotnet-tools.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"isRoot": true,
|
||||||
|
"tools": {
|
||||||
|
"dotnet-ef": {
|
||||||
|
"version": "10.0.5",
|
||||||
|
"commands": [
|
||||||
|
"dotnet-ef"
|
||||||
|
],
|
||||||
|
"rollForward": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
ClientApiPoC.slnx
Normal file
7
ClientApiPoC.slnx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<Solution>
|
||||||
|
<Project Path="ApiClient/ApiClient.csproj" />
|
||||||
|
<Project Path="ApiService/ApiService.csproj" />
|
||||||
|
<Project Path="OnPremiseApp/OnPremiseApp.csproj" Id="ddccc69b-ec86-473b-b34f-317e5ff1e4cf" />
|
||||||
|
<Project Path="Shared.Wpf/Shared.Wpf.csproj" />
|
||||||
|
<Project Path="Shared/Shared.csproj" />
|
||||||
|
</Solution>
|
||||||
8
OnPremiseApp/App.xaml
Normal file
8
OnPremiseApp/App.xaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<Application x:Class="ClientApiPoC.OnPremiseApp.App"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:local="clr-namespace:ClientApiPoC.OnPremiseApp">
|
||||||
|
<Application.Resources>
|
||||||
|
|
||||||
|
</Application.Resources>
|
||||||
|
</Application>
|
||||||
36
OnPremiseApp/App.xaml.cs
Normal file
36
OnPremiseApp/App.xaml.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using ClientApiPoC.OnPremiseApp.Services;
|
||||||
|
|
||||||
|
namespace ClientApiPoC.OnPremiseApp {
|
||||||
|
public partial class App : Application {
|
||||||
|
private IHost _host;
|
||||||
|
|
||||||
|
public App() {
|
||||||
|
_host = Host.CreateDefaultBuilder().ConfigureServices(services => {
|
||||||
|
// Services:
|
||||||
|
services.AddSingleton<ClientDataService>();
|
||||||
|
// ViewModels:
|
||||||
|
services.AddTransient<MainWindowViewModel>();
|
||||||
|
// Views:
|
||||||
|
services.AddSingleton<MainWindow>();
|
||||||
|
}).Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async void OnStartup(StartupEventArgs e) {
|
||||||
|
await _host.StartAsync();
|
||||||
|
|
||||||
|
var window = _host.Services.GetRequiredService<MainWindow>();
|
||||||
|
window.Show();
|
||||||
|
|
||||||
|
base.OnStartup(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async void OnExit(ExitEventArgs e) {
|
||||||
|
await _host.StopAsync();
|
||||||
|
_host.Dispose();
|
||||||
|
base.OnExit(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
OnPremiseApp/AssemblyInfo.cs
Normal file
10
OnPremiseApp/AssemblyInfo.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
[assembly: ThemeInfo(
|
||||||
|
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
|
||||||
|
//(used if a resource is not found in the page,
|
||||||
|
// or application resource dictionaries)
|
||||||
|
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
|
||||||
|
//(used if a resource is not found in the page,
|
||||||
|
// app, or any theme specific resource dictionaries)
|
||||||
|
)]
|
||||||
76
OnPremiseApp/MainWindow.xaml
Normal file
76
OnPremiseApp/MainWindow.xaml
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
<wpf:MvvmWindow x:TypeArguments="local:MainWindowViewModel"
|
||||||
|
x:Class="ClientApiPoC.OnPremiseApp.MainWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:local="clr-namespace:ClientApiPoC.OnPremiseApp"
|
||||||
|
xmlns:wpf="clr-namespace:ClientApiPoC.Shared.Wpf;assembly=Shared.Wpf"
|
||||||
|
xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
Title="On-Premise Gateway"
|
||||||
|
Width="384"
|
||||||
|
Height="AUTO"
|
||||||
|
MaxHeight="144"
|
||||||
|
ResizeMode="CanMinimize"
|
||||||
|
WindowStartupLocation="CenterScreen">
|
||||||
|
<Grid RowDefinitions="AUTO, AUTO"
|
||||||
|
ColumnDefinitions="*, AUTO"
|
||||||
|
Margin="12">
|
||||||
|
|
||||||
|
<Grid Grid.Row="0"
|
||||||
|
Grid.Column="0"
|
||||||
|
RowDefinitions="AUTO, AUTO"
|
||||||
|
ColumnDefinitions="*">
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="0"
|
||||||
|
Grid.Column="0"
|
||||||
|
Text="Base-URL:" />
|
||||||
|
|
||||||
|
<TextBox Grid.Row="1"
|
||||||
|
Grid.Column="0"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
IsEnabled="{Binding UrlTextFieldEnabled}"
|
||||||
|
Text="{Binding ApiServiceUrl}" />
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Button Grid.Row="0"
|
||||||
|
Grid.Column="1"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
Padding="5"
|
||||||
|
Margin="5, 0, 0, 0"
|
||||||
|
Content="{Binding ConnectButtonText}"
|
||||||
|
IsEnabled="{Binding ConnectButtonEnabled}"
|
||||||
|
Command="{Binding ToggleConnectionCommand}" />
|
||||||
|
|
||||||
|
<Grid Grid.Row="1"
|
||||||
|
Grid.Column="0"
|
||||||
|
RowDefinitions="AUTO, AUTO"
|
||||||
|
ColumnDefinitions="*"
|
||||||
|
Margin="0, 12, 0, 0">
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="0"
|
||||||
|
Grid.Column="0"
|
||||||
|
Text="Local client data:" />
|
||||||
|
|
||||||
|
<TextBox Grid.Row="1"
|
||||||
|
Grid.Column="0"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Text="{Binding LocalClientData, UpdateSourceTrigger=PropertyChanged}">
|
||||||
|
</TextBox>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Button Grid.Row="1"
|
||||||
|
Grid.Column="1"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
Padding="5"
|
||||||
|
Margin="5, 12, 0, 0"
|
||||||
|
IsDefault="True"
|
||||||
|
Content="Set data"
|
||||||
|
IsEnabled="{Binding SetLocalClientDataButtonEnabled}"
|
||||||
|
Command="{Binding SetLocalClientDataCommand}" />
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
</wpf:MvvmWindow>
|
||||||
9
OnPremiseApp/MainWindow.xaml.cs
Normal file
9
OnPremiseApp/MainWindow.xaml.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using ClientApiPoC.Shared.Wpf;
|
||||||
|
|
||||||
|
namespace ClientApiPoC.OnPremiseApp {
|
||||||
|
public partial class MainWindow : MvvmWindow<MainWindowViewModel> {
|
||||||
|
public MainWindow(MainWindowViewModel viewModel) : base(viewModel) {
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
114
OnPremiseApp/MainWindowViewModel.cs
Normal file
114
OnPremiseApp/MainWindowViewModel.cs
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using ClientApiPoC.OnPremiseApp.Services;
|
||||||
|
using ClientApiPoC.Shared;
|
||||||
|
using ClientApiPoC.Shared.Wpf;
|
||||||
|
using ClientApiPoC.Shared.SignalR;
|
||||||
|
|
||||||
|
namespace ClientApiPoC.OnPremiseApp {
|
||||||
|
public class MainWindowViewModel : BaseViewModel {
|
||||||
|
private ClientDataService _clientDataService;
|
||||||
|
private TunnelClient _tunnelClient;
|
||||||
|
|
||||||
|
#region Properties
|
||||||
|
private string _apiServiceUrl = "http://localhost:5228/";
|
||||||
|
public string ApiServiceUrl {
|
||||||
|
get => _apiServiceUrl;
|
||||||
|
set {
|
||||||
|
_apiServiceUrl = value;
|
||||||
|
OnPropertyChanged(nameof(ApiServiceUrl));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _tunnelClientIsConnecting = false;
|
||||||
|
public bool TunnelClientIsConnecting {
|
||||||
|
get => _tunnelClientIsConnecting;
|
||||||
|
set {
|
||||||
|
_tunnelClientIsConnecting = value;
|
||||||
|
OnPropertyChanged(nameof(TunnelClientIsConnecting));
|
||||||
|
OnPropertyChanged(nameof(ConnectButtonEnabled));
|
||||||
|
OnPropertyChanged(nameof(UrlTextFieldEnabled));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TunnelClientConnected => (_tunnelClient.IsConnected || _tunnelClient.IsConnecting);
|
||||||
|
|
||||||
|
public string ConnectButtonText => (this.TunnelClientConnected ? "Disconnect" : "Connect");
|
||||||
|
|
||||||
|
public bool ConnectButtonEnabled => !this.TunnelClientIsConnecting;
|
||||||
|
|
||||||
|
public bool UrlTextFieldEnabled => !this.TunnelClientIsConnecting && !this.TunnelClientConnected;
|
||||||
|
|
||||||
|
private string _localClientData = "";
|
||||||
|
public string LocalClientData {
|
||||||
|
get => _localClientData;
|
||||||
|
set {
|
||||||
|
_localClientData = value;
|
||||||
|
OnPropertyChanged(nameof(LocalClientData));
|
||||||
|
OnPropertyChanged(nameof(SetLocalClientDataButtonEnabled));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SetLocalClientDataButtonEnabled {
|
||||||
|
get {
|
||||||
|
if (string.IsNullOrEmpty(this.LocalClientData) && string.IsNullOrEmpty(_clientDataService.ClientData)) return false;
|
||||||
|
return !string.Equals(this.LocalClientData, _clientDataService.ClientData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Commands
|
||||||
|
public ICommand ToggleConnectionCommand { get; }
|
||||||
|
|
||||||
|
public ICommand SetLocalClientDataCommand { get; }
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public MainWindowViewModel(ClientDataService clientDataService) {
|
||||||
|
if (clientDataService == null) throw new ArgumentNullException(nameof(clientDataService));
|
||||||
|
_clientDataService = clientDataService;
|
||||||
|
_tunnelClient = new TunnelClient();
|
||||||
|
_tunnelClient.PropertyChanged += TunnelClient_PropertyChanged;
|
||||||
|
this.ToggleConnectionCommand = new AsyncRelayCommand(ToggleConnectionAsync);
|
||||||
|
this.SetLocalClientDataCommand = new AsyncRelayCommand(SetLocalClientDataAsync);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TunnelClient_PropertyChanged(object? sender, PropertyChangedEventArgs e) {
|
||||||
|
OnPropertyChanged(nameof(TunnelClientConnected));
|
||||||
|
OnPropertyChanged(nameof(ConnectButtonText));
|
||||||
|
OnPropertyChanged(nameof(UrlTextFieldEnabled));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ToggleConnectionAsync() {
|
||||||
|
if (this.TunnelClientIsConnecting) return;
|
||||||
|
try {
|
||||||
|
this.TunnelClientIsConnecting = true;
|
||||||
|
if (this.TunnelClientConnected) {
|
||||||
|
await _tunnelClient.DisconnectAsync();
|
||||||
|
} else {
|
||||||
|
var url = this.ApiServiceUrl;
|
||||||
|
if (string.IsNullOrWhiteSpace(url)) throw new Exception("Please provide a base url.");
|
||||||
|
while (url.EndsWith('/')) url = url[..^1];
|
||||||
|
url += Routes.TUNNEL_PATH;
|
||||||
|
await _tunnelClient.ConnectAsync(url, _clientDataService.ConfigureTunnelActions);
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
await HandleErrorAsync(ex);
|
||||||
|
} finally {
|
||||||
|
this.TunnelClientIsConnecting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SetLocalClientDataAsync() {
|
||||||
|
try {
|
||||||
|
string? newClientData = this.LocalClientData;
|
||||||
|
if (string.IsNullOrEmpty(newClientData)) newClientData = null;
|
||||||
|
_clientDataService.ClientData = newClientData;
|
||||||
|
} catch (Exception ex) {
|
||||||
|
await HandleErrorAsync(ex);
|
||||||
|
} finally {
|
||||||
|
OnPropertyChanged(nameof(SetLocalClientDataButtonEnabled));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
45
OnPremiseApp/OnPremiseApp.csproj
Normal file
45
OnPremiseApp/OnPremiseApp.csproj
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<TargetFramework>net10.0-windows</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<UseWPF>true</UseWPF>
|
||||||
|
|
||||||
|
<RootNamespace>ClientApiPoC.OnPremiseApp</RootNamespace>
|
||||||
|
|
||||||
|
<PublishSingleFile>true</PublishSingleFile>
|
||||||
|
<SelfContained>true</SelfContained>
|
||||||
|
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
||||||
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
|
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
|
||||||
|
<PublishTrimmed>false</PublishTrimmed>
|
||||||
|
|
||||||
|
<Company>Mike Schumann</Company>
|
||||||
|
<Copyright>Copyright © 2026 $(Company)</Copyright>
|
||||||
|
<AssemblyVersion>0.1.0</AssemblyVersion>
|
||||||
|
<FileVersion>$(AssemblyVersion)</FileVersion>
|
||||||
|
<NeutralLanguage>en-US</NeutralLanguage>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
|
||||||
|
<DebugType>portable</DebugType>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)'=='Release'">
|
||||||
|
<DebugType>none</DebugType>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.5" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.5" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Shared.Wpf\Shared.Wpf.csproj" />
|
||||||
|
<ProjectReference Include="..\Shared\Shared.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
15
OnPremiseApp/Properties/PublishProfiles/FolderProfile.pubxml
Normal file
15
OnPremiseApp/Properties/PublishProfiles/FolderProfile.pubxml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
|
||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<Configuration>Release</Configuration>
|
||||||
|
<Platform>Any CPU</Platform>
|
||||||
|
<PublishDir>..\Publish\</PublishDir>
|
||||||
|
<PublishProtocol>FileSystem</PublishProtocol>
|
||||||
|
<_TargetId>Folder</_TargetId>
|
||||||
|
<TargetFramework>net10.0-windows</TargetFramework>
|
||||||
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
|
<SelfContained>true</SelfContained>
|
||||||
|
<PublishReadyToRun>false</PublishReadyToRun>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
21
OnPremiseApp/Services/ClientDataService.cs
Normal file
21
OnPremiseApp/Services/ClientDataService.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using Microsoft.AspNetCore.SignalR.Client;
|
||||||
|
using ClientApiPoC.Shared.Models;
|
||||||
|
|
||||||
|
namespace ClientApiPoC.OnPremiseApp.Services {
|
||||||
|
public class ClientDataService {
|
||||||
|
public string? ClientData { get; set; } = null;
|
||||||
|
|
||||||
|
public async Task<ClientDataModel> GetClientDataAsync(DateTime timestampServer) {
|
||||||
|
return new ClientDataModel() {
|
||||||
|
Data = this.ClientData,
|
||||||
|
TimestampServerUtc = timestampServer,
|
||||||
|
TimestampClientUtc = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ConfigureTunnelActions(HubConnection tunnelConnection) {
|
||||||
|
if (tunnelConnection == null) throw new ArgumentNullException(nameof(tunnelConnection));
|
||||||
|
tunnelConnection.On<DateTime, ClientDataModel>("GetClientData", GetClientDataAsync);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
README.md
21
README.md
@@ -1,2 +1,21 @@
|
|||||||
# ClientApiPoC
|
# ClientApiPoC
|
||||||
|
|
||||||
|
Proof-of-Concept for transferring local data from a client to a public web service without port-forwarding, using SignalR.
|
||||||
|
|
||||||
|
## ApiService
|
||||||
|
|
||||||
|
Web service, serving HTTP-API for Clients (*ApiClient*). It also serves a SignalR server that local clients (*OnPremiseApp*) can connect to.
|
||||||
|
|
||||||
|
This service can call a function on all connected local clients (*OnPremiseApp*) to receive local data. The local client doesn't have any HTTP-API or what so ever, nor is port-forwarding needed.
|
||||||
|
|
||||||
|
API clients (*ApiClient*) can receive data from all connected local clients (*OnPremiseApp*) using the standard HTTP-API.
|
||||||
|
|
||||||
|
## ApiClient
|
||||||
|
|
||||||
|
This application only connects to the public HTTP-API (*ApiService*) to receive data from local clients (*OnPremiseApp*) and shows the request result.
|
||||||
|
|
||||||
|
## OnPremiseApp
|
||||||
|
|
||||||
|
This application connects to the *ApiService* via SignalR and transfers local data via the SignalR tunnel when the API service requests this.
|
||||||
|
|
||||||
|
No Data ist actively sent to the service; the service requests data if needed.
|
||||||
|
|||||||
11
Shared.Wpf/BaseViewModel.cs
Normal file
11
Shared.Wpf/BaseViewModel.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace ClientApiPoC.Shared.Wpf {
|
||||||
|
public abstract class BaseViewModel : NotifyPropertyChangedBase {
|
||||||
|
protected async Task HandleErrorAsync(Exception ex) {
|
||||||
|
if (ex == null) return;
|
||||||
|
MessageBox.Show(ex.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
Shared.Wpf/MvvmWindow.cs
Normal file
13
Shared.Wpf/MvvmWindow.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace ClientApiPoC.Shared.Wpf {
|
||||||
|
public abstract class MvvmWindow<TViewModel> : Window where TViewModel : BaseViewModel {
|
||||||
|
protected TViewModel ViewModel { get; }
|
||||||
|
|
||||||
|
public MvvmWindow(TViewModel viewModel) : base() {
|
||||||
|
if (viewModel == null) throw new ArgumentNullException(nameof(viewModel));
|
||||||
|
this.ViewModel = viewModel;
|
||||||
|
this.DataContext = this.ViewModel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
Shared.Wpf/Shared.Wpf.csproj
Normal file
23
Shared.Wpf/Shared.Wpf.csproj
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0-windows</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<UseWPF>true</UseWPF>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<RootNamespace>ClientApiPoC.Shared.Wpf</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
|
||||||
|
<DebugType>portable</DebugType>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)'=='Release'">
|
||||||
|
<DebugType>none</DebugType>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Shared\Shared.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
9
Shared/Models/ClientDataModel.cs
Normal file
9
Shared/Models/ClientDataModel.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace ClientApiPoC.Shared.Models {
|
||||||
|
public class ClientDataModel {
|
||||||
|
public string? Data { get; set; } = null;
|
||||||
|
|
||||||
|
public DateTime TimestampClientUtc { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
public DateTime TimestampServerUtc { get; set; } = DateTime.MinValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
7
Shared/Models/ClientResultModel.cs
Normal file
7
Shared/Models/ClientResultModel.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace ClientApiPoC.Shared.Models {
|
||||||
|
public class ClientResultModel {
|
||||||
|
public string ClientId { get; set; } = "";
|
||||||
|
|
||||||
|
public ClientDataModel? ClientData { get; set; } = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
7
Shared/Models/ExampleRequestResultModel.cs
Normal file
7
Shared/Models/ExampleRequestResultModel.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace ClientApiPoC.Shared.Models {
|
||||||
|
public class ExampleRequestResultModel {
|
||||||
|
public string? ServerMessage { get; set; }
|
||||||
|
|
||||||
|
public IEnumerable<ClientResultModel> Clients { get; set; } = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
12
Shared/NotifyPropertyChangedBase.cs
Normal file
12
Shared/NotifyPropertyChangedBase.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace ClientApiPoC.Shared {
|
||||||
|
public abstract class NotifyPropertyChangedBase : INotifyPropertyChanged {
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
|
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) {
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
Shared/Routes.cs
Normal file
9
Shared/Routes.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace ClientApiPoC.Shared {
|
||||||
|
public static class Routes {
|
||||||
|
private const string BASE_PATH = "/api/";
|
||||||
|
|
||||||
|
public const string TUNNEL_PATH = "/tunnel";
|
||||||
|
|
||||||
|
public const string EXAMPLE_REQUEST = $"{BASE_PATH}ExampleRequest";
|
||||||
|
}
|
||||||
|
}
|
||||||
23
Shared/Shared.csproj
Normal file
23
Shared/Shared.csproj
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<RootNamespace>ClientApiPoC.Shared</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
|
||||||
|
<DebugType>portable</DebugType>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)'=='Release'">
|
||||||
|
<DebugType>none</DebugType>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="10.0.5" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
18
Shared/SignalR/ClientTracker.cs
Normal file
18
Shared/SignalR/ClientTracker.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace ClientApiPoC.Shared.SignalR {
|
||||||
|
public class ClientTracker {
|
||||||
|
private ConcurrentDictionary<string, string> _knownClientIds = new();
|
||||||
|
|
||||||
|
public ICollection<string> CurrentClientIds => _knownClientIds.Values;
|
||||||
|
|
||||||
|
public bool TryAddClientId(string clientId) {
|
||||||
|
if (string.IsNullOrWhiteSpace(clientId)) throw new ArgumentNullException(nameof(clientId));
|
||||||
|
return _knownClientIds.TryAdd(clientId, clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryRemoveClientId(string clientId) {
|
||||||
|
return _knownClientIds.TryRemove(clientId, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
84
Shared/SignalR/TunnelClient.cs
Normal file
84
Shared/SignalR/TunnelClient.cs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
using Microsoft.AspNetCore.SignalR.Client;
|
||||||
|
|
||||||
|
namespace ClientApiPoC.Shared.SignalR {
|
||||||
|
public class TunnelClient : NotifyPropertyChangedBase, IDisposable, IAsyncDisposable {
|
||||||
|
#region IDisposable-Implementierung
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
protected virtual void Dispose(bool disposing) {
|
||||||
|
if (!_disposed) {
|
||||||
|
if (disposing) {
|
||||||
|
_ = this.DisposeAsync();
|
||||||
|
}
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() {
|
||||||
|
Dispose(true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync() {
|
||||||
|
await DisconnectAsync();
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private HubConnection? _connection = null;
|
||||||
|
|
||||||
|
public bool IsConnected => (this.ConnectionState == HubConnectionState.Connected);
|
||||||
|
|
||||||
|
public bool IsConnecting => (this.ConnectionState == HubConnectionState.Connecting || this.ConnectionState == HubConnectionState.Reconnecting);
|
||||||
|
|
||||||
|
protected HubConnectionState ConnectionState => (_connection?.State ?? HubConnectionState.Disconnected);
|
||||||
|
|
||||||
|
public async Task ConnectAsync(string url, Action<HubConnection> configureConnection) {
|
||||||
|
try {
|
||||||
|
if (string.IsNullOrWhiteSpace(url)) throw new ArgumentNullException(nameof(url));
|
||||||
|
if (this.IsConnected) throw new InvalidOperationException("SignalR connection is already established.");
|
||||||
|
_connection = new HubConnectionBuilder().WithUrl(url).WithAutomaticReconnect().Build();
|
||||||
|
_connection.Reconnecting += Connection_Reconnecting;
|
||||||
|
_connection.Reconnected += Connection_Reconnected;
|
||||||
|
_connection.Closed += Connection_Closed;
|
||||||
|
configureConnection(_connection);
|
||||||
|
await _connection.StartAsync();
|
||||||
|
} finally {
|
||||||
|
await UpdateStateAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DisconnectAsync() {
|
||||||
|
try {
|
||||||
|
if (_connection == null) return;
|
||||||
|
try {
|
||||||
|
if (this.IsConnected) await _connection.StopAsync();
|
||||||
|
} catch { }
|
||||||
|
_connection.Reconnecting -= Connection_Reconnecting;
|
||||||
|
_connection.Reconnected -= Connection_Reconnected;
|
||||||
|
_connection.Closed -= Connection_Closed;
|
||||||
|
await _connection.DisposeAsync();
|
||||||
|
_connection = null;
|
||||||
|
} finally {
|
||||||
|
await UpdateStateAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Connection_Reconnecting(Exception? arg) {
|
||||||
|
await UpdateStateAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Connection_Reconnected(string? arg) {
|
||||||
|
await UpdateStateAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Connection_Closed(Exception? arg) {
|
||||||
|
await UpdateStateAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UpdateStateAsync() {
|
||||||
|
OnPropertyChanged(nameof(IsConnected));
|
||||||
|
OnPropertyChanged(nameof(IsConnecting));
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
Shared/SignalR/TunnelHub.cs
Normal file
20
Shared/SignalR/TunnelHub.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
|
||||||
|
namespace ClientApiPoC.Shared.SignalR {
|
||||||
|
public sealed class TunnelHub : Hub {
|
||||||
|
private ClientTracker _clientTracker;
|
||||||
|
|
||||||
|
public TunnelHub(ClientTracker clientTracker) : base() {
|
||||||
|
if (clientTracker == null) throw new ArgumentNullException(nameof(clientTracker));
|
||||||
|
_clientTracker = clientTracker;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override sealed async Task OnConnectedAsync() {
|
||||||
|
_ = _clientTracker.TryAddClientId(this.Context.ConnectionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override sealed async Task OnDisconnectedAsync(Exception exception) {
|
||||||
|
_ = _clientTracker.TryRemoveClientId(this.Context.ConnectionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
Shared/SignalR/TunnelHubExtensions.cs
Normal file
21
Shared/SignalR/TunnelHubExtensions.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace ClientApiPoC.Shared.SignalR {
|
||||||
|
public static class TunnelHubExtensions {
|
||||||
|
public static IServiceCollection AddTunnelHub(this IServiceCollection services) {
|
||||||
|
services.AddSignalR(options => {
|
||||||
|
// Nachrichtengröße auf max. 16MB bringen (Standard = 32KB).
|
||||||
|
options.MaximumReceiveMessageSize = (16 * 1024 * 1024);
|
||||||
|
});
|
||||||
|
services.AddSingleton<ClientTracker>();
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HubEndpointConventionBuilder MapTunnelHub(this IEndpointRouteBuilder app, [StringSyntax("Route")] string pattern) {
|
||||||
|
return app.MapHub<TunnelHub>(pattern);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
Shared/SignalR/TunnelServerBase.cs
Normal file
36
Shared/SignalR/TunnelServerBase.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
|
||||||
|
namespace ClientApiPoC.Shared.SignalR {
|
||||||
|
public abstract class TunnelServerBase {
|
||||||
|
protected IHubContext<TunnelHub> HubContext { get; private set; }
|
||||||
|
|
||||||
|
protected ClientTracker Clients { get; private set; }
|
||||||
|
|
||||||
|
public TunnelServerBase(IHubContext<TunnelHub> hubContext, ClientTracker clientTracker) {
|
||||||
|
if (hubContext == null) throw new ArgumentNullException(nameof(hubContext));
|
||||||
|
if (clientTracker == null) throw new ArgumentNullException(nameof(clientTracker));
|
||||||
|
this.HubContext = hubContext;
|
||||||
|
this.Clients = clientTracker;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ISingleClientProxy? TryGetClient(string clientId) {
|
||||||
|
ISingleClientProxy? client;
|
||||||
|
try {
|
||||||
|
client = this.HubContext.Clients.Client(clientId);
|
||||||
|
} catch {
|
||||||
|
client = null;
|
||||||
|
}
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected IDictionary<string, ISingleClientProxy> GetAllClients() {
|
||||||
|
var results = new Dictionary<string, ISingleClientProxy>();
|
||||||
|
var allClientIds = this.Clients.CurrentClientIds;
|
||||||
|
foreach (var clientId in allClientIds) {
|
||||||
|
var client = TryGetClient(clientId);
|
||||||
|
if (client != null) results.Add(clientId, client);
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
Shared/Tools.cs
Normal file
15
Shared/Tools.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using System.Net.Http;
|
||||||
|
|
||||||
|
namespace ClientApiPoC.Shared {
|
||||||
|
public static class Tools {
|
||||||
|
public static HttpClient CreateHttpClient() {
|
||||||
|
var httpClientHandler = new HttpClientHandler() {
|
||||||
|
ServerCertificateCustomValidationCallback = (message, cert, chain, sslPolicyErrors) => {
|
||||||
|
// HACK: Allen SSL-Zertifikaten vertrauen!
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return new HttpClient(httpClientHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user