diff --git a/Source/Applications/StreamSplitter/Api/Controllers/ConnectionsController.cs b/Source/Applications/StreamSplitter/Api/Controllers/ConnectionsController.cs
new file mode 100644
index 0000000..7b2d201
--- /dev/null
+++ b/Source/Applications/StreamSplitter/Api/Controllers/ConnectionsController.cs
@@ -0,0 +1,605 @@
+//******************************************************************************************************
+// ConnectionsController.cs - Gbtc
+//
+// Copyright © 2026, Grid Protection Alliance. All Rights Reserved.
+//
+// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
+// the NOTICE file distributed with this work for additional information regarding copyright ownership.
+// The GPA licenses this file to you under the Eclipse Public License -v 1.0 (the "License"); you may
+// not use this file except in compliance with the License. You may obtain a copy of the License at:
+//
+// http://www.opensource.org/licenses/eclipse-1.0.php
+//
+// Unless agreed to in writing, the subject software distributed under the License is distributed on an
+// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
+// License for the specific language governing permissions and limitations.
+//
+// Code Modification History:
+// ----------------------------------------------------------------------------------------------------
+// 06/01/2026 - Marcos Vinicius Snak
+// Generated original version of source code.
+//
+//******************************************************************************************************
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Threading.Tasks;
+using System.Web.Http;
+using System.Web.Http.Description;
+using GSF;
+using GSF.Diagnostics;
+using StreamSplitter.Api.Models;
+
+namespace StreamSplitter.Api.Controllers
+{
+ ///
+ /// Provides read access to the currently configured proxy connections.
+ ///
+ [RoutePrefix("api/connections")]
+ public class ConnectionsController : ApiController
+ {
+ #region [ Members ]
+
+ // Fields
+ private static readonly LogPublisher s_log =
+ Logger.CreatePublisher(typeof(ConnectionsController), MessageClass.Application);
+
+ #endregion
+
+ #region [ Methods ]
+
+ ///
+ /// Returns all currently configured proxy connections.
+ ///
+ /// Array of objects representing all configured connections.
+ /// Array of connections. Empty array when no connections are configured.
+ [HttpGet, Route("")]
+ [ResponseType(typeof(ConnectionDto[]))]
+ public IHttpActionResult GetConnections()
+ {
+ string correlationId = GetCorrelationId();
+
+ s_log.Publish(
+ MessageLevel.Info,
+ "GetConnections",
+ $"GET /api/connections requested. CorrelationId={correlationId}");
+
+ ProxyConnectionCollection configuration = ServiceHost.Current?.CurrentConfiguration;
+
+ if (configuration is null)
+ {
+ s_log.Publish(
+ MessageLevel.Warning,
+ "GetConnections",
+ $"Configuration not yet loaded. Returning empty list. CorrelationId={correlationId}");
+
+ return Ok(Array.Empty());
+ }
+
+ ConnectionDto[] dtos = configuration
+ .Select(c => ConnectionDto.FromProxyConnection(c, ServiceHost.Current.GetRuntimeConnectionState(c.ID)))
+ .ToArray();
+
+ s_log.Publish(
+ MessageLevel.Info,
+ "GetConnections",
+ $"Returning {dtos.Length} connection(s). CorrelationId={correlationId}");
+
+ return Ok(dtos);
+ }
+
+ ///
+ /// Returns the proxy connection identified by .
+ ///
+ /// Unique identifier of the connection.
+ ///
+ /// when found; 404 with a problem detail body when not found.
+ ///
+ /// The requested connection with its current runtime state.
+ /// No connection found with the given ID.
+ [HttpGet, Route("{id:guid}")]
+ [ResponseType(typeof(ConnectionDto))]
+ public IHttpActionResult GetConnectionById(Guid id)
+ {
+ string correlationId = GetCorrelationId();
+
+ s_log.Publish(
+ MessageLevel.Info,
+ "GetConnectionById",
+ $"GET /api/connections/{id} requested. CorrelationId={correlationId}");
+
+ ProxyConnectionCollection configuration = ServiceHost.Current?.CurrentConfiguration;
+ ProxyConnection connection = configuration?[id];
+
+ if (connection is null)
+ {
+ s_log.Publish(
+ MessageLevel.Info,
+ "GetConnectionById",
+ $"Connection {id} not found. CorrelationId={correlationId}");
+
+ return Content(HttpStatusCode.NotFound, new
+ {
+ status = 404,
+ title = "Not Found",
+ detail = $"Connection with ID '{id}' was not found."
+ });
+ }
+
+ ConnectionDto dto = ConnectionDto.FromProxyConnection(
+ connection,
+ ServiceHost.Current.GetRuntimeConnectionState(id));
+
+ s_log.Publish(
+ MessageLevel.Info,
+ "GetConnectionById",
+ $"Returning connection '{connection.Name}'. CorrelationId={correlationId}");
+
+ return Ok(dto);
+ }
+
+ ///
+ /// Creates one or more proxy connections from a JSON array.
+ /// All items are validated before any connection is created (all-or-nothing semantics).
+ ///
+ ///
+ /// Array of connection data. connectionString is required on each item.
+ ///
+ ///
+ /// 201 Created with the array of created objects,
+ /// or 400 Bad Request if any item fails validation.
+ ///
+ ///
+ /// The connectionString uses the GSF key=value pair format, with nested
+ /// sourceSettings and proxySettings sub-strings. Example:
+ ///
+ /// [
+ /// {
+ /// "connectionString": "name=PDC-001;enabled=true;sourceSettings={server=192.168.1.1;port=4712;phasorProtocol=IEEEC37_118V2;accessID=1};proxySettings={port=4713}"
+ /// }
+ /// ]
+ ///
+ ///
+ /// Array of created connections, each with a server-generated id.
+ /// One or more items are missing a required connectionString.
+ /// Service configuration not yet loaded.
+ [HttpPost, Route("")]
+ [ResponseType(typeof(ConnectionDto[]))]
+ public IHttpActionResult CreateConnections([FromBody] CreateConnectionRequest[] requests)
+ {
+ string correlationId = GetCorrelationId();
+
+ s_log.Publish(
+ MessageLevel.Info,
+ "CreateConnections",
+ $"POST /api/connections requested ({requests?.Length ?? 0} item(s)). CorrelationId={correlationId}");
+
+ if (requests is null || requests.Length == 0)
+ {
+ return Content(HttpStatusCode.BadRequest, new
+ {
+ status = 400,
+ title = "Bad Request",
+ detail = "The request body must be a non-empty JSON array of connection objects."
+ });
+ }
+
+ // Validate all items before creating any — all-or-nothing semantics.
+ for (int i = 0; i < requests.Length; i++)
+ {
+ if (string.IsNullOrWhiteSpace(requests[i]?.ConnectionString))
+ {
+ s_log.Publish(
+ MessageLevel.Warning,
+ "CreateConnections",
+ $"Validation failed at index {i}: connectionString is required. CorrelationId={correlationId}");
+
+ return Content(HttpStatusCode.BadRequest, new
+ {
+ status = 400,
+ title = "Bad Request",
+ detail = $"Item at index {i} is missing a required connectionString."
+ });
+ }
+ }
+
+ List created = new List();
+
+ foreach (CreateConnectionRequest request in requests)
+ {
+ ProxyConnection connection = new ProxyConnection { ConnectionString = request.ConnectionString };
+
+ try
+ {
+ ServiceHost.Current.AddConnection(connection);
+ }
+ catch (InvalidOperationException ex)
+ {
+ return Content(HttpStatusCode.ServiceUnavailable, new
+ {
+ status = 503,
+ title = "Service Unavailable",
+ detail = ex.Message
+ });
+ }
+
+ created.Add(ConnectionDto.FromProxyConnection(
+ connection,
+ ServiceHost.Current.GetRuntimeConnectionState(connection.ID)));
+
+ s_log.Publish(
+ MessageLevel.Info,
+ "CreateConnections",
+ $"Connection '{connection.Name}' created. Id={connection.ID}. CorrelationId={correlationId}");
+ }
+
+ return Created("/api/connections", created.ToArray());
+ }
+
+ ///
+ /// Imports connections from a .s3config file sent as multipart/form-data.
+ /// Each connection in the file is assigned a new server-generated ID to ensure
+ /// CREATE semantics regardless of the IDs in the file.
+ ///
+ ///
+ /// 201 Created with the array of imported objects,
+ /// or 400 Bad Request if the file is missing, empty, or not a valid .s3config.
+ ///
+ ///
+ /// Upload a .s3config file exported from StreamSplitterManager using
+ /// File → Save Configuration. The file is a SOAP-serialized ProxyConnectionCollection.
+ /// All connections in the file are created with new server-generated IDs.
+ ///
+ /// Array of imported connections, each with a new server-generated id.
+ /// File missing, empty, or not a valid .s3config.
+ [HttpPost, Route("import")]
+ [ResponseType(typeof(ConnectionDto[]))]
+ public async Task ImportFromFile()
+ {
+ string correlationId = GetCorrelationId();
+
+ s_log.Publish(
+ MessageLevel.Info,
+ "ImportFromFile",
+ $"POST /api/connections/import requested. CorrelationId={correlationId}");
+
+ if (!Request.Content.IsMimeMultipartContent())
+ {
+ return Content(HttpStatusCode.BadRequest, new
+ {
+ status = 400,
+ title = "Bad Request",
+ detail = "Expected multipart/form-data with a .s3config file."
+ });
+ }
+
+ MultipartMemoryStreamProvider provider = new MultipartMemoryStreamProvider();
+ await Request.Content.ReadAsMultipartAsync(provider);
+
+ HttpContent filePart = provider.Contents.FirstOrDefault();
+
+ if (filePart is null)
+ {
+ return Content(HttpStatusCode.BadRequest, new
+ {
+ status = 400,
+ title = "Bad Request",
+ detail = "No file was included in the request."
+ });
+ }
+
+ // Read the part as a stream to avoid a redundant byte[] copy — the
+ // MultipartMemoryStreamProvider already buffers content in a MemoryStream.
+ using Stream fileStream = await filePart.ReadAsStreamAsync();
+
+ if (fileStream.Length == 0)
+ {
+ return Content(HttpStatusCode.BadRequest, new
+ {
+ status = 400,
+ title = "Bad Request",
+ detail = "The uploaded file is empty."
+ });
+ }
+
+ ProxyConnectionCollection imported;
+
+ try
+ {
+ imported = ProxyConnectionCollection.DeserializeConfiguration(fileStream);
+ }
+ catch (Exception ex)
+ {
+ s_log.Publish(
+ MessageLevel.Warning,
+ "ImportFromFile",
+ $"Failed to deserialize file. Error={ex.Message}. CorrelationId={correlationId}");
+
+ return Content(HttpStatusCode.BadRequest, new
+ {
+ status = 400,
+ title = "Bad Request",
+ detail = "The file could not be read as a valid .s3config configuration."
+ });
+ }
+
+ if (imported.Count == 0)
+ {
+ return Content(HttpStatusCode.BadRequest, new
+ {
+ status = 400,
+ title = "Bad Request",
+ detail = "The .s3config file contains no connections."
+ });
+ }
+
+ List created = new List();
+
+ foreach (ProxyConnection source in imported)
+ {
+ // Assign a new ID to guarantee CREATE semantics — never UPDATE an existing connection.
+ ProxyConnection connection = new ProxyConnection
+ {
+ ConnectionString = source.ConnectionString,
+ ConnectionParameters = source.ConnectionParameters
+ };
+
+ ServiceHost.Current.AddConnection(connection);
+
+ created.Add(ConnectionDto.FromProxyConnection(
+ connection,
+ ServiceHost.Current.GetRuntimeConnectionState(connection.ID)));
+ }
+
+ s_log.Publish(
+ MessageLevel.Info,
+ "ImportFromFile",
+ $"Imported {created.Count} connection(s). CorrelationId={correlationId}");
+
+ return Created("/api/connections", created.ToArray());
+ }
+
+ ///
+ /// Partially updates a proxy connection. Only the fields present in the request body
+ /// are changed; omitted fields retain their current values. The connection is
+ /// automatically restarted after the update.
+ ///
+ /// Unique identifier of the connection to update.
+ /// Fields to update. All properties are optional.
+ ///
+ /// 200 OK with the updated , or 404 if not found.
+ ///
+ ///
+ /// Send only the fields that need to change; all others are preserved.
+ /// Disable a connection:
+ /// { "enabled": false }
+ /// Rename a connection:
+ /// { "name": "PDC-001-updated" }
+ /// Update the source settings:
+ /// { "sourceSettings": "server=192.168.1.2;port=4712;phasorProtocol=IEEEC37_118V2;accessID=1" }
+ ///
+ /// Each field is independent — you can combine any of them in the same request.
+ /// For sourceSettings and proxySettings, sending an empty string removes the sub-string.
+ ///
+ ///
+ /// Updated connection with its new runtime state.
+ /// Request body is missing.
+ /// No connection found with the given ID.
+ /// Service configuration not yet loaded.
+ [HttpPatch, Route("{id:guid}")]
+ [ResponseType(typeof(ConnectionDto))]
+ public IHttpActionResult UpdateConnection(Guid id, [FromBody] UpdateConnectionRequest request)
+ {
+ string correlationId = GetCorrelationId();
+
+ s_log.Publish(
+ MessageLevel.Info,
+ "UpdateConnection",
+ $"PATCH /api/connections/{id} requested. CorrelationId={correlationId}");
+
+ if (request is null)
+ {
+ return Content(HttpStatusCode.BadRequest, new
+ {
+ status = 400,
+ title = "Bad Request",
+ detail = "Request body is required."
+ });
+ }
+
+ ProxyConnectionCollection configuration = ServiceHost.Current?.CurrentConfiguration;
+ ProxyConnection existing = configuration?[id];
+
+ if (existing is null)
+ {
+ s_log.Publish(
+ MessageLevel.Info,
+ "UpdateConnection",
+ $"Connection {id} not found. CorrelationId={correlationId}");
+
+ return Content(HttpStatusCode.NotFound, new
+ {
+ status = 404,
+ title = "Not Found",
+ detail = $"Connection with ID '{id}' was not found."
+ });
+ }
+
+ // Merge only the provided sub-fields into the existing connection string.
+ // Omitted fields (null) retain their current values.
+ Dictionary settings = existing.ConnectionString.ParseKeyValuePairs();
+
+ if (request.Name != null)
+ settings["name"] = request.Name;
+
+ if (request.Enabled.HasValue)
+ settings["enabled"] = request.Enabled.Value.ToString().ToLower();
+
+ if (request.SourceSettings != null)
+ {
+ if (string.IsNullOrEmpty(request.SourceSettings))
+ settings.Remove("sourceSettings");
+ else
+ settings["sourceSettings"] = request.SourceSettings;
+ }
+
+ if (request.ProxySettings != null)
+ {
+ if (string.IsNullOrEmpty(request.ProxySettings))
+ settings.Remove("proxySettings");
+ else
+ settings["proxySettings"] = request.ProxySettings;
+ }
+
+ string updatedConnectionString = settings.JoinKeyValuePairs();
+
+ ProxyConnection updated = new ProxyConnection
+ {
+ ConnectionString = updatedConnectionString,
+ ConnectionParameters = existing.ConnectionParameters
+ };
+
+ updated.ID = existing.ID;
+
+ try
+ {
+ ServiceHost.Current.AddConnection(updated);
+ }
+ catch (InvalidOperationException ex)
+ {
+ return Content(HttpStatusCode.ServiceUnavailable, new
+ {
+ status = 503,
+ title = "Service Unavailable",
+ detail = ex.Message
+ });
+ }
+
+ ConnectionDto dto = ConnectionDto.FromProxyConnection(
+ updated,
+ ServiceHost.Current.GetRuntimeConnectionState(id));
+
+ s_log.Publish(
+ MessageLevel.Info,
+ "UpdateConnection",
+ $"Connection '{updated.Name}' updated. Id={id}. CorrelationId={correlationId}");
+
+ return Ok(dto);
+ }
+
+ ///
+ /// Returns the current operational status of the proxy connection identified by
+ /// without retrieving its full configuration.
+ ///
+ /// Unique identifier of the connection.
+ ///
+ /// 200 OK with ; 404 if not found.
+ ///
+ /// Current operational status including state, metrics, and recent messages.
+ /// No connection found with the given ID.
+ [HttpGet, Route("{id:guid}/status")]
+ [ResponseType(typeof(ConnectionStatusDto))]
+ public IHttpActionResult GetConnectionStatus(Guid id)
+ {
+ string correlationId = GetCorrelationId();
+
+ s_log.Publish(
+ MessageLevel.Info,
+ "GetConnectionStatus",
+ $"GET /api/connections/{id}/status requested. CorrelationId={correlationId}");
+
+ ConnectionStatusDto status = ServiceHost.Current?.GetConnectionStatus(id);
+
+ if (status is null)
+ {
+ s_log.Publish(
+ MessageLevel.Info,
+ "GetConnectionStatus",
+ $"Connection {id} not found. CorrelationId={correlationId}");
+
+ return Content(HttpStatusCode.NotFound, new
+ {
+ status = 404,
+ title = "Not Found",
+ detail = $"Connection with ID '{id}' was not found."
+ });
+ }
+
+ return Ok(status);
+ }
+
+ ///
+ /// Removes the proxy connection identified by , stops its data
+ /// flow, and persists the updated configuration.
+ ///
+ /// Unique identifier of the connection to remove.
+ /// 204 No Content on success; 404 if not found.
+ /// Connection removed and data flow stopped.
+ /// No connection found with the given ID.
+ /// Service configuration not yet loaded.
+ [HttpDelete, Route("{id:guid}")]
+ [ResponseType(typeof(void))]
+ public IHttpActionResult DeleteConnection(Guid id)
+ {
+ string correlationId = GetCorrelationId();
+
+ s_log.Publish(
+ MessageLevel.Info,
+ "DeleteConnection",
+ $"DELETE /api/connections/{id} requested. CorrelationId={correlationId}");
+
+ bool removed;
+
+ try
+ {
+ removed = ServiceHost.Current.RemoveConnection(id);
+ }
+ catch (InvalidOperationException ex)
+ {
+ return Content(HttpStatusCode.ServiceUnavailable, new
+ {
+ status = 503,
+ title = "Service Unavailable",
+ detail = ex.Message
+ });
+ }
+
+ if (!removed)
+ {
+ s_log.Publish(
+ MessageLevel.Info,
+ "DeleteConnection",
+ $"Connection {id} not found. CorrelationId={correlationId}");
+
+ return Content(HttpStatusCode.NotFound, new
+ {
+ status = 404,
+ title = "Not Found",
+ detail = $"Connection with ID '{id}' was not found."
+ });
+ }
+
+ s_log.Publish(
+ MessageLevel.Info,
+ "DeleteConnection",
+ $"Connection {id} removed. CorrelationId={correlationId}");
+
+ return StatusCode(HttpStatusCode.NoContent);
+ }
+
+ // Extracts the X-Correlation-Id header value, or generates a new GUID string if absent.
+ private string GetCorrelationId()
+ {
+ if (Request.Headers.TryGetValues("X-Correlation-Id", out IEnumerable values))
+ return values.FirstOrDefault() ?? Guid.NewGuid().ToString();
+
+ return Guid.NewGuid().ToString();
+ }
+
+ #endregion
+ }
+}
diff --git a/Source/Applications/StreamSplitter/Api/Filters/FileUploadOperationFilter.cs b/Source/Applications/StreamSplitter/Api/Filters/FileUploadOperationFilter.cs
new file mode 100644
index 0000000..d910cd1
--- /dev/null
+++ b/Source/Applications/StreamSplitter/Api/Filters/FileUploadOperationFilter.cs
@@ -0,0 +1,65 @@
+//******************************************************************************************************
+// FileUploadOperationFilter.cs - Gbtc
+//
+// Copyright © 2026, Grid Protection Alliance. All Rights Reserved.
+//
+// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
+// the NOTICE file distributed with this work for additional information regarding copyright ownership.
+// The GPA licenses this file to you under the Eclipse Public License -v 1.0 (the "License"); you may
+// not use this file except in compliance with the License. You may obtain a copy of the License at:
+//
+// http://www.opensource.org/licenses/eclipse-1.0.php
+//
+// Unless agreed to in writing, the subject software distributed under the License is distributed on an
+// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
+// License for the specific language governing permissions and limitations.
+//
+// Code Modification History:
+// ----------------------------------------------------------------------------------------------------
+// 06/05/2026 - Marcos Vinicius Snak
+// Generated original version of source code.
+//
+//******************************************************************************************************
+
+using System.Collections.Generic;
+using System.Web.Http.Description;
+using Swashbuckle.Swagger;
+
+namespace StreamSplitter.Api.Filters
+{
+ ///
+ /// Swashbuckle operation filter that replaces the body parameter on the
+ /// ImportFromFile action with a multipart/form-data file picker,
+ /// enabling file upload directly from the Swagger UI.
+ ///
+ public class FileUploadOperationFilter : IOperationFilter
+ {
+ #region [ Methods ]
+
+ ///
+ public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
+ {
+ bool isImport =
+ apiDescription.ActionDescriptor.ControllerDescriptor.ControllerName == "Connections"
+ && apiDescription.ActionDescriptor.ActionName == "ImportFromFile";
+
+ if (!isImport)
+ return;
+
+ operation.consumes = new List { "multipart/form-data" };
+ operation.parameters = new List
+ {
+ new Parameter
+ {
+ name = "file",
+ @in = "formData",
+ required = true,
+ type = "file",
+ description = "Configuration file (.s3config) exported from StreamSplitterManager."
+ }
+ };
+ }
+
+ #endregion
+ }
+}
diff --git a/Source/Applications/StreamSplitter/Api/Models/ConnectionDto.cs b/Source/Applications/StreamSplitter/Api/Models/ConnectionDto.cs
new file mode 100644
index 0000000..478ce65
--- /dev/null
+++ b/Source/Applications/StreamSplitter/Api/Models/ConnectionDto.cs
@@ -0,0 +1,122 @@
+//******************************************************************************************************
+// ConnectionDto.cs - Gbtc
+//
+// Copyright © 2026, Grid Protection Alliance. All Rights Reserved.
+//
+// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
+// the NOTICE file distributed with this work for additional information regarding copyright ownership.
+// The GPA licenses this file to you under the Eclipse Public License -v 1.0 (the "License"); you may
+// not use this file except in compliance with the License. You may obtain a copy of the License at:
+//
+// http://www.opensource.org/licenses/eclipse-1.0.php
+//
+// Unless agreed to in writing, the subject software distributed under the License is distributed on an
+// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
+// License for the specific language governing permissions and limitations.
+//
+// Code Modification History:
+// ----------------------------------------------------------------------------------------------------
+// 06/01/2026 - Marcos Vinicius Snak
+// Generated original version of source code.
+//
+//******************************************************************************************************
+
+using System;
+using GSF;
+using StreamSplitter;
+
+namespace StreamSplitter.Api.Models
+{
+ ///
+ /// Read-only data transfer object representing a .
+ ///
+ public sealed class ConnectionDto
+ {
+ #region [ Constructors ]
+
+ private ConnectionDto()
+ {
+ }
+
+ #endregion
+
+ #region [ Properties ]
+
+ ///
+ /// Gets the unique identifier of the connection.
+ ///
+ public Guid Id { get; private set; }
+
+ ///
+ /// Gets the display name of the connection.
+ ///
+ public string Name { get; private set; }
+
+ ///
+ /// Gets a value indicating whether the connection is enabled.
+ ///
+ public bool Enabled { get; private set; }
+
+ ///
+ /// Gets the current of the connection.
+ ///
+ public ConnectionState ConnectionState { get; private set; }
+
+ ///
+ /// Gets a human-readable description of the current connection state.
+ ///
+ public string ConnectionStateDescription { get; private set; }
+
+ ///
+ /// Gets the raw GSF connection string.
+ ///
+ public string ConnectionString { get; private set; }
+
+ ///
+ /// Gets the source-side settings extracted from the connection string.
+ ///
+ public string SourceSettings { get; private set; }
+
+ ///
+ /// Gets the proxy-side settings extracted from the connection string.
+ ///
+ public string ProxySettings { get; private set; }
+
+ #endregion
+
+ #region [ Static ]
+
+ // Static Methods
+
+ ///
+ /// Creates a new from a and its
+ /// runtime .
+ ///
+ /// Source to map from.
+ ///
+ /// Live read from the associated .
+ /// Pass when no live proxy exists.
+ ///
+ /// A populated .
+ /// is null.
+ public static ConnectionDto FromProxyConnection(ProxyConnection connection, ConnectionState runtimeState)
+ {
+ if (connection is null)
+ throw new ArgumentNullException(nameof(connection));
+
+ return new ConnectionDto
+ {
+ Id = connection.ID,
+ Name = connection.Name,
+ Enabled = connection.Enabled,
+ ConnectionState = runtimeState,
+ ConnectionStateDescription = runtimeState.GetDescription(),
+ ConnectionString = connection.ConnectionString,
+ SourceSettings = connection.SourceSettings,
+ ProxySettings = connection.ProxySettings
+ };
+ }
+
+ #endregion
+ }
+}
diff --git a/Source/Applications/StreamSplitter/Api/Models/ConnectionStatusDto.cs b/Source/Applications/StreamSplitter/Api/Models/ConnectionStatusDto.cs
new file mode 100644
index 0000000..a1035b0
--- /dev/null
+++ b/Source/Applications/StreamSplitter/Api/Models/ConnectionStatusDto.cs
@@ -0,0 +1,121 @@
+//******************************************************************************************************
+// ConnectionStatusDto.cs - Gbtc
+//
+// Copyright © 2026, Grid Protection Alliance. All Rights Reserved.
+//
+// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
+// the NOTICE file distributed with this work for additional information regarding copyright ownership.
+// The GPA licenses this file to you under the Eclipse Public License -v 1.0 (the "License"); you may
+// not use this file except in compliance with the License. You may obtain a copy of the License at:
+//
+// http://www.opensource.org/licenses/eclipse-1.0.php
+//
+// Unless agreed to in writing, the subject software distributed under the License is distributed on an
+// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
+// License for the specific language governing permissions and limitations.
+//
+// Code Modification History:
+// ----------------------------------------------------------------------------------------------------
+// 06/08/2026 - Marcos Vinicius Snak
+// Generated original version of source code.
+//
+//******************************************************************************************************
+
+using System;
+using GSF;
+
+namespace StreamSplitter.Api.Models
+{
+ ///
+ /// Lightweight data transfer object representing the current operational status of a
+ /// and its associated .
+ /// Returned by GET /api/connections/{id}/status.
+ ///
+ public sealed class ConnectionStatusDto
+ {
+ #region [ Properties ]
+
+ /// Gets the unique identifier of the connection.
+ public Guid Id { get; private set; }
+
+ /// Gets the display name of the connection.
+ public string Name { get; private set; }
+
+ /// Gets a value indicating whether the connection is enabled in configuration.
+ public bool Enabled { get; private set; }
+
+ /// Gets the current runtime of the connection.
+ public ConnectionState ConnectionState { get; private set; }
+
+ /// Gets a human-readable description of the current connection state.
+ public string ConnectionStateDescription { get; private set; }
+
+ ///
+ /// Gets the source connection endpoint in the format host:port/accessID.
+ /// null when no live exists (e.g. connection is disabled).
+ ///
+ public string ConnectionInfo { get; private set; }
+
+ ///
+ /// Gets the total number of bytes sent to downstream clients since the proxy started.
+ /// Zero when no live exists.
+ ///
+ public long TotalBytesSent { get; private set; }
+
+ ///
+ /// Gets the total active run time of the proxy in seconds.
+ /// Zero when no live exists.
+ ///
+ public double RunTime { get; private set; }
+
+ ///
+ /// Gets the most recent status messages produced by the proxy (up to 2 048 characters).
+ /// null when no live exists.
+ ///
+ public string RecentStatusMessages { get; private set; }
+
+ #endregion
+
+ #region [ Static ]
+
+ // Static Methods
+
+ ///
+ /// Creates a from the provided service-layer objects.
+ ///
+ /// Connection identifier.
+ ///
+ /// that holds the configuration. Must not be null.
+ ///
+ ///
+ /// Live associated with ,
+ /// or null when the connection is disabled or not yet materialized.
+ ///
+ /// A populated .
+ ///
+ /// is null.
+ ///
+ public static ConnectionStatusDto FromServiceHost(Guid id, ProxyConnection connection, StreamProxy splitter)
+ {
+ if (connection is null)
+ throw new ArgumentNullException(nameof(connection));
+
+ ConnectionState state = splitter?.StreamProxyStatus.ConnectionState ?? ConnectionState.Disabled;
+
+ return new ConnectionStatusDto
+ {
+ Id = id,
+ Name = connection.Name,
+ Enabled = connection.Enabled,
+ ConnectionState = state,
+ ConnectionStateDescription = state.GetDescription(),
+ ConnectionInfo = splitter?.ConnectionInfo,
+ TotalBytesSent = splitter?.TotalBytesSent ?? 0L,
+ RunTime = (double)(splitter?.RunTime ?? 0.0D),
+ RecentStatusMessages = splitter?.StreamProxyStatus.RecentStatusMessages
+ };
+ }
+
+ #endregion
+ }
+}
diff --git a/Source/Applications/StreamSplitter/Api/Models/CreateConnectionRequest.cs b/Source/Applications/StreamSplitter/Api/Models/CreateConnectionRequest.cs
new file mode 100644
index 0000000..abf7295
--- /dev/null
+++ b/Source/Applications/StreamSplitter/Api/Models/CreateConnectionRequest.cs
@@ -0,0 +1,44 @@
+//******************************************************************************************************
+// CreateConnectionRequest.cs - Gbtc
+//
+// Copyright © 2026, Grid Protection Alliance. All Rights Reserved.
+//
+// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
+// the NOTICE file distributed with this work for additional information regarding copyright ownership.
+// The GPA licenses this file to you under the Eclipse Public License -v 1.0 (the "License"); you may
+// not use this file except in compliance with the License. You may obtain a copy of the License at:
+//
+// http://www.opensource.org/licenses/eclipse-1.0.php
+//
+// Unless agreed to in writing, the subject software distributed under the License is distributed on an
+// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
+// License for the specific language governing permissions and limitations.
+//
+// Code Modification History:
+// ----------------------------------------------------------------------------------------------------
+// 06/05/2026 - Marcos Vinicius Snak
+// Generated original version of source code.
+//
+//******************************************************************************************************
+
+namespace StreamSplitter.Api.Models
+{
+ ///
+ /// Request body for creating a new proxy connection via the REST API.
+ ///
+ public class CreateConnectionRequest
+ {
+ #region [ Properties ]
+
+ ///
+ /// Gets or sets the full GSF connection string (key=value pairs).
+ /// The string must include all required parameters such as name, enabled,
+ /// sourceSettings and proxySettings embedded in the standard GSF format.
+ /// This is the only required field; all other connection attributes are
+ /// parsed from it automatically.
+ ///
+ public string ConnectionString { get; set; }
+
+ #endregion
+ }
+}
diff --git a/Source/Applications/StreamSplitter/Api/Models/UpdateConnectionRequest.cs b/Source/Applications/StreamSplitter/Api/Models/UpdateConnectionRequest.cs
new file mode 100644
index 0000000..3dcf6de
--- /dev/null
+++ b/Source/Applications/StreamSplitter/Api/Models/UpdateConnectionRequest.cs
@@ -0,0 +1,64 @@
+//******************************************************************************************************
+// UpdateConnectionRequest.cs - Gbtc
+//
+// Copyright © 2026, Grid Protection Alliance. All Rights Reserved.
+//
+// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
+// the NOTICE file distributed with this work for additional information regarding copyright ownership.
+// The GPA licenses this file to you under the Eclipse Public License -v 1.0 (the "License"); you may
+// not use this file except in compliance with the License. You may obtain a copy of the License at:
+//
+// http://www.opensource.org/licenses/eclipse-1.0.php
+//
+// Unless agreed to in writing, the subject software distributed under the License is distributed on an
+// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
+// License for the specific language governing permissions and limitations.
+//
+// Code Modification History:
+// ----------------------------------------------------------------------------------------------------
+// 06/05/2026 - Marcos Vinicius Snak
+// Generated original version of source code.
+// 06/08/2026 - Marcos Vinicius Snak
+// Removed ConnectionString field; PATCH now uses individual sub-fields only for
+// true partial-update semantics without conflicting override behaviour.
+//
+//******************************************************************************************************
+
+namespace StreamSplitter.Api.Models
+{
+ ///
+ /// Request body for partially updating an existing proxy connection via the REST API.
+ /// All properties are optional — only the fields that are present (non-null) are applied;
+ /// omitted fields retain their current values.
+ ///
+ public class UpdateConnectionRequest
+ {
+ #region [ Properties ]
+
+ ///
+ /// Gets or sets the display name of the connection.
+ /// Null keeps the existing value.
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the connection should be enabled.
+ /// Null keeps the existing value.
+ ///
+ public bool? Enabled { get; set; }
+
+ ///
+ /// Gets or sets the source-side settings sub-string.
+ /// Null keeps the existing value; empty string removes the sub-string entirely.
+ ///
+ public string SourceSettings { get; set; }
+
+ ///
+ /// Gets or sets the proxy-side settings sub-string.
+ /// Null keeps the existing value; empty string removes the sub-string entirely.
+ ///
+ public string ProxySettings { get; set; }
+
+ #endregion
+ }
+}
diff --git a/Source/Applications/StreamSplitter/Api/Startup.cs b/Source/Applications/StreamSplitter/Api/Startup.cs
new file mode 100644
index 0000000..4628088
--- /dev/null
+++ b/Source/Applications/StreamSplitter/Api/Startup.cs
@@ -0,0 +1,91 @@
+//******************************************************************************************************
+// Startup.cs - Gbtc
+//
+// Copyright © 2026, Grid Protection Alliance. All Rights Reserved.
+//
+// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
+// the NOTICE file distributed with this work for additional information regarding copyright ownership.
+// The GPA licenses this file to you under the Eclipse Public License -v 1.0 (the "License"); you may
+// not use this file except in compliance with the License. You may obtain a copy of the License at:
+//
+// http://www.opensource.org/licenses/eclipse-1.0.php
+//
+// Unless agreed to in writing, the subject software distributed under the License is distributed on an
+// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
+// License for the specific language governing permissions and limitations.
+//
+// Code Modification History:
+// ----------------------------------------------------------------------------------------------------
+// 06/01/2026 - Marcos Vinicius Snak
+// Generated original version of source code.
+// 06/05/2026 - Marcos Vinicius Snak
+// Added FileUploadOperationFilter to enable file picker on /import endpoint in Swagger UI.
+// Added XML documentation comments to Swagger via IncludeXmlComments.
+//
+//******************************************************************************************************
+
+using System;
+using System.IO;
+using System.Web.Http;
+using Newtonsoft.Json.Converters;
+using Owin;
+using Swashbuckle.Application;
+using StreamSplitter.Api.Filters;
+
+namespace StreamSplitter.Api
+{
+ ///
+ /// OWIN startup class for the Stream Splitter Web API.
+ /// Configures attribute routing, JSON serialization, Swagger UI, and the Web API middleware.
+ ///
+ ///
+ /// This class is passed directly to WebApp.Start<Startup>() in
+ /// — the [assembly: OwinStartup]
+ /// auto-discovery attribute is intentionally omitted.
+ ///
+ public class Startup
+ {
+ #region [ Methods ]
+
+ ///
+ /// Configures the OWIN middleware pipeline.
+ ///
+ /// The instance to configure.
+ public void Configuration(IAppBuilder app)
+ {
+ HttpConfiguration config = new HttpConfiguration();
+
+ // Enable attribute-based routing ([RoutePrefix] / [Route] on controllers)
+ config.MapHttpAttributeRoutes();
+
+ // Serialize enum values as strings for readability in JSON responses
+ config.Formatters.JsonFormatter.SerializerSettings.Converters
+ .Add(new StringEnumConverter());
+
+ // Remove XML formatter — this API speaks JSON only
+ config.Formatters.Remove(config.Formatters.XmlFormatter);
+
+ // Configure Swashbuckle: single API version, enums as strings in schema
+ config
+ .EnableSwagger(c =>
+ {
+ c.SingleApiVersion("v1", "Stream Splitter API");
+ c.DescribeAllEnumsAsStrings();
+ c.OperationFilter();
+
+ // Load XML documentation generated by the C# compiler so that
+ // controller summaries, parameter descriptions, and remarks with
+ // examples are displayed in the Swagger UI.
+ string xmlPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "StreamSplitter.xml");
+
+ if (File.Exists(xmlPath))
+ c.IncludeXmlComments(xmlPath);
+ })
+ .EnableSwaggerUi();
+
+ app.UseWebApi(config);
+ }
+
+ #endregion
+ }
+}
diff --git a/Source/Applications/StreamSplitter/ServiceHost.cs b/Source/Applications/StreamSplitter/ServiceHost.cs
index 7a9ac20..3dba359 100755
--- a/Source/Applications/StreamSplitter/ServiceHost.cs
+++ b/Source/Applications/StreamSplitter/ServiceHost.cs
@@ -18,6 +18,9 @@
// ----------------------------------------------------------------------------------------------------
// 09/04/2013 - J. Ritchie Carroll
// Generated original version of source code.
+// 06/01/2026 - Marcos Vinicius Snak
+// Added OWIN-based Web API hosting (Story 1): TryStartWebHosting, CurrentConfiguration,
+// ServiceHost.Current static accessor.
//
//******************************************************************************************************
@@ -43,7 +46,9 @@
using GSF.PhasorProtocols;
using GSF.ServiceProcess;
using GSF.Units;
+using Microsoft.Owin.Hosting;
using Microsoft.Win32;
+using StreamSplitter.Api;
namespace StreamSplitter
{
@@ -62,6 +67,9 @@ public sealed partial class ServiceHost : ServiceBase
private const int DefaultMinThreadPoolIOPortSize = (int)(DefaultMinThreadPoolWorkerSize + DefaultMinThreadPoolWorkerSize * 0.2D);
private const int DefaultMaxThreadPoolIOPortSize = (int)(DefaultMaxThreadPoolWorkerSize + DefaultMaxThreadPoolWorkerSize * 0.2D);
private const int DefaultMaxLogFiles = 300;
+ private const bool DefaultWebHostingEnabled = true;
+ private const string DefaultWebHostURL = "http://localhost:8283";
+ private const string ApiConfigChangedBroadcast = "[API_CONFIG_CHANGED]";
// Fields
private AutoResetEvent m_configurationLoadComplete;
@@ -69,6 +77,7 @@ public sealed partial class ServiceHost : ServiceBase
private volatile ProxyConnectionCollection m_currentConfiguration;
private readonly List m_streamSplitters;
private readonly ConcurrentDictionary