Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/sdk-csharp-nuget.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Build & Publish NuGet to GitHub Registry
name: StarFederation.Datastar Nuget

on:
workflow_dispatch:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/sdk-fsharp-nuget.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Build & Publish NuGet to GitHub Registry
name: StarFederation.Datastar.FSharp NuGet

on:
workflow_dispatch:
Expand Down
2 changes: 1 addition & 1 deletion build/consts_fsharp.qtpl
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ module Consts =

{%- for _, enum := range data.Enums -%}
module {%s enum.Name.Pascal %} =
let toString this =
let inline toString this =
match this with
{%- for _, entry := range enum.Values -%}
| {%s enum.Name.Pascal %}.{%s entry.Name.Pascal %} -> "{%s entry.Value %}"
Expand Down
11 changes: 7 additions & 4 deletions examples/dotnet/csharp/HelloWorld/Program.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Diagnostics;
using System.Text.Json.Serialization;
using StarFederation.Datastar.DependencyInjection;

Expand All @@ -22,18 +23,20 @@ public static void Main(string[] args)
WebApplication app = builder.Build();
app.UseStaticFiles();

app.MapGet("/hello-world", async (IDatastarServerSentEventService sse, IDatastarSignalsReaderService signals) =>
app.MapGet("/hello-world", async (IDatastarService datastarService) =>
{
Signals mySignals = await signals.ReadSignalsAsync<Signals>();
Signals? mySignals = await datastarService.ReadSignalsAsync<Signals>();
Debug.Assert(mySignals != null, nameof(mySignals) + " != null");

for (int index = 0; index < Message.Length; ++index)
{
await sse.MergeFragmentsAsync($"""<div id="message">{Message[..index]}</div>""");
await datastarService.PatchElementsAsync($"""<div id="message">{Message[..index]}</div>""");
if (!char.IsWhiteSpace(Message[index]))
{
await Task.Delay(TimeSpan.FromMilliseconds(mySignals.Delay.GetValueOrDefault(0)));
}
}
await sse.MergeFragmentsAsync($"""<div id="message">{Message}</div>""");
await datastarService.PatchElementsAsync($"""<div id="message">{Message}</div>""");
});

app.Run();
Expand Down
26 changes: 7 additions & 19 deletions examples/dotnet/fsharp/HelloWorld/Program.fs
Original file line number Diff line number Diff line change
@@ -1,26 +1,16 @@
namespace HelloWorld
#nowarn "20"
open System
open System.Collections.Generic
open System.IO
open System.Linq
open System.Text
open System.Threading
open System.Threading.Tasks
open Microsoft.AspNetCore
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Hosting
open Microsoft.AspNetCore.Http
open Microsoft.AspNetCore.HttpsPolicy
open Microsoft.Extensions.Configuration
open Microsoft.Extensions.DependencyInjection
open Microsoft.Extensions.Hosting
open Microsoft.Extensions.Logging
open StarFederation.Datastar.FSharp

module Program =
[<CLIMutable>]
type MySignals = { delay:float }
type MySignals = { delay:int }

let [<Literal>] Message = "Hello, world!"

Expand All @@ -33,20 +23,18 @@ module Program =
app.UseStaticFiles()

app.MapGet("/hello-world", Func<IHttpContextAccessor, Task>(fun ctx -> task {
let sseHandler : ISendServerEvent = ServerSentEventHttpHandler ctx.HttpContext.Response
do! sseHandler.StartServerEventStream()
do! ServerSentEventGenerator.StartServerEventStreamAsync(ctx.HttpContext.Response)

let signalsHandler : IReadSignals = SignalsHttpHandler ctx.HttpContext.Request
let! signals = signalsHandler.ReadSignalsAsync<MySignals>()
let! signals = ServerSentEventGenerator.ReadSignalsAsync<MySignals>(ctx.HttpContext.Request)
let delayMs = (signals |> ValueOption.map _.delay |> ValueOption.defaultValue 1000)

[0 .. (Message.Length - 1)]
|> Seq.map (fun length -> Message[0..length])
|> Seq.map (fun message -> $"""<div id="message">{message}</div>""")
|> Seq.map ServerSentEventGenerator.MergeFragments
|> Seq.map (fun sse -> async {
do! sse |> sseHandler.SendServerEvent |> Async.AwaitTask
do! Async.Sleep(TimeSpan.FromMilliseconds(delayMs)) })
|> Seq.map (fun element -> async {
do! ServerSentEventGenerator.PatchElementsAsync(ctx.HttpContext.Response, element) |> Async.AwaitTask
do! Async.Sleep delayMs
} )
|> Async.Sequential
|> Async.RunSynchronously
}))
Expand Down
22 changes: 10 additions & 12 deletions sdk/dotnet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,21 @@ using System.Text.Json;
using System.Text.Json.Serialization;

// add as an ASP Service
// allows injection of IServerSentEventService, to respond to a request with a Datastar friendly ServerSentEvent
// and ISignals, to read the signals sent by the client
// allows injection of IDatastarService, to respond to a request with a Datastar friendly ServerSentEvent
// and to read the signals sent by the client
builder.Services.AddDatastar();

// displayDate - merging a fragment
app.MapGet("/displayDate", async (IDatastarServerSentEventService sse) =>
app.MapGet("/displayDate", async (IDatastarService datastarService) =>
{
string today = DateTime.Now.ToString("%y-%M-%d %h:%m:%s");
await sse.MergeFragmentsAsync($"""<div id='target'><span id='date'><b>{today}</b><button data-on-click="@get('/removeDate')">Remove</button></span></div>""");
await datastarService.PatchElementsAsync($"""<div id='target'><span id='date'><b>{today}</b><button data-on-click="@get('/removeDate')">Remove</button></span></div>""");
});

// removeDate - removing a fragment
app.MapGet("/removeDate", async (IDatastarServerSentEventService sse) => { await sse.RemoveFragmentsAsync("#date"); });
app.MapGet("/removeDate", async (IDatastarService datastarService) => { await datastarService.RemoveFragmentAsync("#date"); });

public record Signals {
public record MySignals {
[JsonPropertyName("input")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Input { get; init; } = null;
Expand All @@ -52,11 +52,11 @@ public record Signals {
}

// changeOutput - reads the signals, update the Output, and merge back
app.MapPost("/changeOutput", async (IDatastarServerSentEventService sse, IDatastarSignalsReaderService dsSignals) => ...
app.MapPost("/changeOutput", async (IDatastarService datastarService) => ...
{
Signals signals = await dsSignals.ReadSignalsAsync<Signals>();
Signals newSignals = new() { Output = $"Your Input: {signals.Input}" };
await sse.MergeSignalsAsync(newSignals.Serialize());
MySignals signals = await datastarService.ReadSignalsAsync<MySignals>();
MySignals newSignals = new() { Output = $"Your Input: {signals.Input}" };
await datastarService.PatchSignalsAsync(newSignals.Serialize());
});
```

Expand All @@ -78,8 +78,6 @@ public IActionResult Test_GetSignals([FromSignals] MySignals signals) => ...

public IActionResult Test_GetValues([FromSignals] string myString, [FromSignals] int myInt) => ...

public IActionResult Test_GetInner([FromSignals] MySignals.InnerSignals myInner) => ...

public IActionResult Test_GetInnerPathed([FromSignals(Path = "myInner")] MySignals.InnerSignals myInnerOther) => ...

public IActionResult Test_GetInnerValues([FromSignals(Path = "myInner.myInnerString")] string myInnerStringOther, [FromSignals(Path = "myInner.myInnerInt")] int myInnerIntOther) => ...
Expand Down
65 changes: 34 additions & 31 deletions sdk/dotnet/csharp/src/DependencyInjection/Services.cs
Original file line number Diff line number Diff line change
@@ -1,25 +1,29 @@

using System.Security.Cryptography;
using System.Text.Json;
using Microsoft.FSharp.Collections;
using Microsoft.Extensions.Primitives;
using Microsoft.FSharp.Core;
using Core = StarFederation.Datastar.FSharp;

namespace StarFederation.Datastar.DependencyInjection;

public interface IDatastarServerSentEventService
public interface IDatastarService
{
void AddHeaders(params KeyValuePair<string, string>[] httpHeaders);
Task StartServerEventStream();
Task MergeFragmentsAsync(string fragments, MergeFragmentsOptions? options = null);
Task RemoveFragmentsAsync(string selector, RemoveFragmentsOptions? options = null);
Task MergeSignalsAsync(string dataSignals, MergeSignalsOptions? options = null);
Task RemoveSignalsAsync(IEnumerable<string> paths, EventOptions? options = null);
Task StartServerEventStreamAsync(IEnumerable<KeyValuePair<string, StringValues>>? additionalHeaders = null);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these methods accept an optional CancellationToken?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is closed. Please create a new issue if you’d like to discuss further.

Copy link
Contributor Author

@SpiralOSS SpiralOSS Jul 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these methods accept an optional CancellationToken?

@wiserockryan, I can take care of this on the weekend.

Task StartServerEventStreamAsync(IEnumerable<KeyValuePair<string, string>>? additionalHeaders = null);

Task PatchElementsAsync(string fragments, PatchElementsOptions? options = null);

Task RemoveElementAsync(string selector, RemoveElementOptions? options = null);

/// <summary>
/// Note: If TType is string then it is assumed that it is an already serialized Signals, otherwise serialize with jsonSerializerOptions
/// </summary>
Task PatchSignalsAsync<TType>(TType signals, JsonSerializerOptions? jsonSerializerOptions = null, PatchSignalsOptions? patchSignalsOptions = null);

/// <summary>
/// Execute a JS script on the client. Note: Do NOT include "&lt;script&gt;" encapsulation
/// </summary>
Task ExecuteScriptAsync(string script, ExecuteScriptOptions? options = null);
}

public interface IDatastarSignalsReaderService
{
/// <summary>
/// Get the serialized signals as a stream
/// </summary>
Expand All @@ -38,38 +42,37 @@ public interface IDatastarSignalsReaderService
Task<TType?> ReadSignalsAsync<TType>(JsonSerializerOptions? options = null);
}

internal class ServerSentEventService(Core.ISendServerEvent handler) : IDatastarServerSentEventService
internal class DatastarService(Core.ServerSentEventGenerator serverSentEventGenerator) : IDatastarService
{
public void AddHeaders(params KeyValuePair<string, string>[] httpHeaders) => _additionalHeaders.AddRange(httpHeaders ?? []);

public Task StartServerEventStream() => handler.StartServerEventStream(_additionalHeaders.Select(kv => kv.AsTuple()).ToArray());
public Task StartServerEventStreamAsync(IEnumerable<KeyValuePair<string, StringValues>>? additionalHeaders) =>
serverSentEventGenerator.StartServerEventStreamAsync(additionalHeaders ?? []);

public Task MergeFragmentsAsync(string fragments, MergeFragmentsOptions? options = null) => handler.SendServerEvent(Core.ServerSentEventGenerator.MergeFragments(fragments, options ?? new()));
public Task StartServerEventStreamAsync(IEnumerable<KeyValuePair<string, string>>? additionalHeaders) =>
serverSentEventGenerator.StartServerEventStreamAsync(additionalHeaders?.Select(kvp => new KeyValuePair<string, StringValues>(kvp.Key, new StringValues(kvp.Value))) ?? []);

public Task RemoveFragmentsAsync(string selector, RemoveFragmentsOptions? options = null) => handler.SendServerEvent(Core.ServerSentEventGenerator.RemoveFragments(selector, options ?? new()));
public Task PatchElementsAsync(string fragments, PatchElementsOptions? options = null) =>
serverSentEventGenerator.PatchElementsAsync(fragments, options ?? Core.PatchElementsOptions.Defaults);

public Task MergeSignalsAsync(string dataSignals, MergeSignalsOptions? options = null) => handler.SendServerEvent(Core.ServerSentEventGenerator.MergeSignals(dataSignals, options ?? new()));
public Task RemoveElementAsync(string selector, RemoveElementOptions? options = null) =>
serverSentEventGenerator.RemoveElementAsync(selector, options ?? Core.RemoveElementOptions.Defaults);

public Task RemoveSignalsAsync(IEnumerable<string> paths, EventOptions? options = null) => handler.SendServerEvent(Core.ServerSentEventGenerator.RemoveSignals(paths, options ?? new()));
public Task PatchSignalsAsync<TType>(TType signals, JsonSerializerOptions? jsonSerializerOptions = null, PatchSignalsOptions? patchSignalsOptions = null) =>
serverSentEventGenerator.PatchSignalsAsync(signals as string ?? JsonSerializer.Serialize(signals, jsonSerializerOptions), patchSignalsOptions ?? Core.PatchSignalsOptions.Defaults);

public Task ExecuteScriptAsync(string script, ExecuteScriptOptions? options = null) => handler.SendServerEvent(Core.ServerSentEventGenerator.ExecuteScript(script, options ?? new()));
public Task ExecuteScriptAsync(string script, ExecuteScriptOptions? options = null) =>
serverSentEventGenerator.ExecuteScriptAsync(script, options ?? Core.ExecuteScriptOptions.Defaults);

private List<KeyValuePair<string, string>> _additionalHeaders = new();
}

internal class SignalsReaderService(Core.IReadSignals handler) : IDatastarSignalsReaderService
{
public Stream GetSignalsStream() => handler.GetSignalsStream();
public Stream GetSignalsStream() => serverSentEventGenerator.GetSignalsStream();

public async Task<string?> ReadSignalsAsync()
{
string? signals = await handler.ReadSignalsAsync();
string? signals = await serverSentEventGenerator.ReadSignalsAsync();
return String.IsNullOrEmpty(signals) ? null : signals;
}

public async Task<TType?> ReadSignalsAsync<TType>(JsonSerializerOptions? jsonSerializerOptions = null)
{
FSharpValueOption<TType> read = await handler.ReadSignalsAsync<TType>(jsonSerializerOptions ?? JsonSerializerOptions.Default);
return read.IsSome ? read.Value : default(TType?);
FSharpValueOption<TType> read = await serverSentEventGenerator.ReadSignalsAsync<TType>(jsonSerializerOptions ?? Core.JsonSerializerOptions.SignalsDefault);
return read.IsSome ? read.Value : default;
}
}
14 changes: 4 additions & 10 deletions sdk/dotnet/csharp/src/DependencyInjection/ServicesProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,12 @@ public static IServiceCollection AddDatastar(this IServiceCollection serviceColl
{
serviceCollection
.AddHttpContextAccessor()
.AddScoped<IDatastarSignalsReaderService>(svcPvd =>
.AddScoped<IDatastarService>(svcPvd =>
{
IHttpContextAccessor? httpContextAccessor = svcPvd.GetService<IHttpContextAccessor>();
Core.IReadSignals signalsHttpHandler = new Core.SignalsHttpHandler(httpContextAccessor!.HttpContext!.Request);
return new SignalsReaderService(signalsHttpHandler);
})
.AddScoped<IDatastarServerSentEventService>(svcPvd =>
{
IHttpContextAccessor? httpContextAccessor = svcPvd.GetService<IHttpContextAccessor>();
Core.ISendServerEvent sseHttpHandler = new Core.ServerSentEventHttpHandler(httpContextAccessor!.HttpContext!.Response);
return new ServerSentEventService(sseHttpHandler);
Core.ServerSentEventGenerator serverSentEventGenerator = new(httpContextAccessor);
return new DatastarService(serverSentEventGenerator);
});
return serviceCollection;
}
}
}
Loading
Loading