diff --git a/src/api/Synapse.Api.Http/ClusterResourceController.cs b/src/api/Synapse.Api.Http/ClusterResourceController.cs
index 39cdd0eb..fe31ca82 100644
--- a/src/api/Synapse.Api.Http/ClusterResourceController.cs
+++ b/src/api/Synapse.Api.Http/ClusterResourceController.cs
@@ -20,15 +20,10 @@ namespace Synapse.Api.Http;
/// The service used to mediate calls
/// The service used to serialize/deserialize data to/from JSON
public abstract class ClusterResourceController(IMediator mediator, IJsonSerializer jsonSerializer)
- : ResourceController(mediator)
+ : ResourceController(mediator, jsonSerializer)
where TResource : class, IResource, new()
{
- ///
- /// Gets the service used to serialize/deserialize data to/from JSON
- ///
- protected IJsonSerializer JsonSerializer { get; } = jsonSerializer;
-
///
/// Gets the resource with the specified name
///
@@ -97,13 +92,17 @@ public virtual async Task>> Watc
///
/// A comma-separated list of label selectors, if any
/// A
- /// A new
+ /// A new awaitable
[HttpGet("watch/sse")]
[ProducesResponseType(typeof(IAsyncEnumerable), (int)HttpStatusCode.OK)]
[ProducesErrorResponseType(typeof(Neuroglia.ProblemDetails))]
- public virtual async Task WatchResourcesUsingSSE(string? labelSelector = null, CancellationToken cancellationToken = default)
+ public virtual async Task WatchResourcesUsingSSE(string? labelSelector = null, CancellationToken cancellationToken = default)
{
- if (!this.TryParseLabelSelectors(labelSelector, out var labelSelectors)) return this.InvalidLabelSelector(labelSelector!);
+ if (!this.TryParseLabelSelectors(labelSelector, out var labelSelectors))
+ {
+ await WriteInvalidLabelSelectorResponseAsync(labelSelector!, cancellationToken).ConfigureAwait(false);
+ return;
+ }
var response = await this.Mediator.ExecuteAsync(new WatchResourcesQuery(null, labelSelectors), cancellationToken).ConfigureAwait(false);
this.Response.Headers.ContentType = "text/event-stream";
this.Response.Headers.CacheControl = "no-cache";
@@ -119,7 +118,6 @@ public virtual async Task WatchResourcesUsingSSE(string? labelSel
}
}
catch (Exception ex) when (ex is TaskCanceledException || ex is OperationCanceledException) { }
- return this.Ok();
}
///
@@ -142,11 +140,11 @@ public virtual async Task>> Moni
///
/// The name of the cluster resource to monitor
/// A
- /// A new
+ /// A new awaitable
[HttpGet("{name}/monitor/sse")]
[ProducesResponseType(typeof(IAsyncEnumerable), (int)HttpStatusCode.OK)]
[ProducesErrorResponseType(typeof(Neuroglia.ProblemDetails))]
- public virtual async Task MonitorResourceUsingSSE(string name, CancellationToken cancellationToken = default)
+ public virtual async Task MonitorResourceUsingSSE(string name, CancellationToken cancellationToken = default)
{
var response = await this.Mediator.ExecuteAsync(new MonitorResourceQuery(name, null), cancellationToken).ConfigureAwait(false);
this.Response.Headers.ContentType = "text/event-stream";
@@ -163,7 +161,6 @@ public virtual async Task MonitorResourceUsingSSE(string name, Ca
}
}
catch (Exception ex) when (ex is TaskCanceledException || ex is OperationCanceledException) { }
- return this.Ok();
}
///
diff --git a/src/api/Synapse.Api.Http/NamespacedResourceController.cs b/src/api/Synapse.Api.Http/NamespacedResourceController.cs
index 9c38896d..22803073 100644
--- a/src/api/Synapse.Api.Http/NamespacedResourceController.cs
+++ b/src/api/Synapse.Api.Http/NamespacedResourceController.cs
@@ -20,15 +20,10 @@ namespace Synapse.Api.Http;
/// The service used to mediate calls
/// The service used to serialize/deserialize data to/from JSON
public abstract class NamespacedResourceController(IMediator mediator, IJsonSerializer jsonSerializer)
- : ResourceController(mediator)
+ : ResourceController(mediator, jsonSerializer)
where TResource : class, IResource, new()
{
- ///
- /// Gets the service used to serialize/deserialize data to/from JSON
- ///
- protected IJsonSerializer JsonSerializer { get; } = jsonSerializer;
-
///
/// Gets the resource with the specified name and namespace
///
@@ -150,13 +145,17 @@ public virtual async Task>> Watc
/// The namespace the resources to watch belong to
/// A comma-separated list of label selectors, if any
/// A
- /// A new
+ /// A new awaitable
[HttpGet("{namespace}/watch/sse")]
[ProducesResponseType(typeof(IAsyncEnumerable), (int)HttpStatusCode.OK)]
[ProducesErrorResponseType(typeof(Neuroglia.ProblemDetails))]
- public virtual async Task WatchResourcesUsingSSE(string @namespace, string? labelSelector = null, CancellationToken cancellationToken = default)
+ public virtual async Task WatchResourcesUsingSSE(string @namespace, string? labelSelector = null, CancellationToken cancellationToken = default)
{
- if (!this.TryParseLabelSelectors(labelSelector, out var labelSelectors)) return this.InvalidLabelSelector(labelSelector!);
+ if (!this.TryParseLabelSelectors(labelSelector, out var labelSelectors))
+ {
+ await WriteInvalidLabelSelectorResponseAsync(labelSelector!, cancellationToken).ConfigureAwait(false);
+ return;
+ }
var response = await this.Mediator.ExecuteAsync(new WatchResourcesQuery(@namespace, labelSelectors), cancellationToken).ConfigureAwait(false);
this.Response.Headers.ContentType = "text/event-stream";
this.Response.Headers.CacheControl = "no-cache";
@@ -172,7 +171,6 @@ public virtual async Task WatchResourcesUsingSSE(string @namespac
}
}
catch (Exception ex) when(ex is TaskCanceledException || ex is OperationCanceledException) { }
- return this.Ok();
}
///
@@ -197,11 +195,11 @@ public virtual async Task>> Moni
/// The namespace the resource to monitor belongs to
/// The name of the resource to monitor
/// A
- /// A new
+ /// A new awaitable
[HttpGet("{namespace}/{name}/monitor/sse")]
[ProducesResponseType(typeof(IAsyncEnumerable), (int)HttpStatusCode.OK)]
[ProducesErrorResponseType(typeof(Neuroglia.ProblemDetails))]
- public virtual async Task MonitorResourceUsingSSE(string name, string @namespace, CancellationToken cancellationToken = default)
+ public virtual async Task MonitorResourceUsingSSE(string name, string @namespace, CancellationToken cancellationToken = default)
{
var response = await this.Mediator.ExecuteAsync(new MonitorResourceQuery(name, @namespace), cancellationToken).ConfigureAwait(false);
this.Response.Headers.ContentType = "text/event-stream";
@@ -218,7 +216,6 @@ public virtual async Task MonitorResourceUsingSSE(string name, st
}
}
catch (Exception ex) when (ex is TaskCanceledException || ex is OperationCanceledException) { }
- return this.Ok();
}
///
diff --git a/src/api/Synapse.Api.Http/ResourceController.cs b/src/api/Synapse.Api.Http/ResourceController.cs
index fe11974e..bcda43e3 100644
--- a/src/api/Synapse.Api.Http/ResourceController.cs
+++ b/src/api/Synapse.Api.Http/ResourceController.cs
@@ -18,7 +18,8 @@ namespace Synapse.Api.Http;
///
/// The type of to manage
/// The service used to mediate calls
-public abstract class ResourceController(IMediator mediator)
+/// The service used to serialize/deserialize data to/from JSON.
+public abstract class ResourceController(IMediator mediator, IJsonSerializer jsonSerializer)
: Controller
where TResource : class, IResource, new()
{
@@ -28,6 +29,11 @@ public abstract class ResourceController(IMediator mediator)
///
protected IMediator Mediator { get; } = mediator;
+ ///
+ /// Gets the service used to serialize/deserialize data to/from JSON.
+ ///
+ protected IJsonSerializer JsonSerializer { get; } = jsonSerializer;
+
///
/// Creates a new resource of the specified type
///
@@ -117,4 +123,30 @@ protected IActionResult InvalidLabelSelector(string labelSelector)
return this.ValidationProblem("Bad Request", statusCode: (int)HttpStatusCode.BadRequest, title: "Bad Request", modelStateDictionary: this.ModelState);
}
+ ///
+ /// Writes to the response the description of a validation problem that occurred while processing the request.
+ ///
+ /// A .
+ /// A new awaitable .
+ protected virtual async Task WriteValidationProblemResponseAsync(CancellationToken cancellationToken)
+ {
+ var problem = new ValidationProblemDetails(ModelState);
+ var json = JsonSerializer.SerializeToText(problem);
+ Response.StatusCode = (int)HttpStatusCode.BadRequest;
+ Response.ContentType = MediaTypeNames.Application.Json;
+ await Response.WriteAsync(json, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Writes to the response the description of an error that occurred while parsing the request's label selector.
+ ///
+ /// The invalid label selector.
+ /// A .
+ /// A new awaitable .
+ protected virtual async Task WriteInvalidLabelSelectorResponseAsync(string labelSelector, CancellationToken cancellationToken)
+ {
+ ModelState.AddModelError(nameof(labelSelector), $"The specified value '{labelSelector}' is not a valid comma-separated label selector list");
+ await WriteValidationProblemResponseAsync(cancellationToken).ConfigureAwait(false);
+ }
+
}
diff --git a/src/api/Synapse.Api.Http/Usings.cs b/src/api/Synapse.Api.Http/Usings.cs
index 103d6595..041a32e9 100644
--- a/src/api/Synapse.Api.Http/Usings.cs
+++ b/src/api/Synapse.Api.Http/Usings.cs
@@ -27,4 +27,5 @@
global using Synapse.Resources;
global using System.Collections.Concurrent;
global using System.Net;
+global using System.Net.Mime;
global using System.Text;