Skip to content

Commit

Permalink
Upgrade wasm server to dotnet 9
Browse files Browse the repository at this point in the history
  • Loading branch information
egil committed Jan 19, 2025
1 parent d17c827 commit 6d3cfcd
Show file tree
Hide file tree
Showing 17 changed files with 130 additions and 137 deletions.
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>BlazorWasm</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.3" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.0" PrivateAssets="all" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
namespace BlazorWasm.Models;
namespace BlazorWasm.Models;

public record class TodoItem
{
public Guid Key { get; init; }
public string Title { get; init; } = null!;
public bool IsDone { get; init; }
public Guid OwnerKey { get; init; }
public DateTime Timestamp { get; init; }
}
41 changes: 25 additions & 16 deletions orleans/Blazor/BlazorWasm/BlazorWasm.Client/Pages/Todo.razor
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,36 @@
@inject ApiService ApiService

<h1>Todo (@todos.Count(todo => !todo.IsDone))</h1>

@foreach (var todo in todos)
@foreach (var todo in todos.OrderBy(x => x.Timestamp))
{
<div class="input-group mb-3">
<div class="input-group-prepend">
<div class="input-group-text">
<input type="checkbox" checked="@todo.IsDone"
@onchange="@(async e => await HandleTodoDoneAsync(todo, (bool)e.Value!))" />
</div>
<button class="btn btn-outline-secondary" type="button"
@onclick="@(async e => await HandleDeleteTodoAsync(todo))">
<span class="oi oi-trash" aria-hidden="true"></span>
</button>
<div class="input-group-text">
<input class="form-check-input mt-0"
type="checkbox"
checked="@todo.IsDone"
@onchange="@(() => HandleTodoDoneAsync(todo, !todo.IsDone))"
aria-label="Toggle task status">
</div>
<input class="form-control" value="@todo.Title"
@onchange="@(async e => await HandleTodoTitleChangeAsync(todo, (string)e.Value!))" />
<button class="btn btn-outline-danger" type="button" @onclick="@(() => HandleDeleteTodoAsync(todo))">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0z" />
<path d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4zM2.5 3h11V2h-11z" />
</svg>
</button>
<InputText class="form-control"
aria-label="Task"
Value="@todo.Title"
ValueChanged="@(title => HandleTodoTitleChangeAsync(todo, title))"
ValueExpression="() => todo.Title" />
</div>
}

<input placeholder="Something todo" @bind="@newTodo" />
<button @onclick="@AddTodoAsync">Add Todo</button>
<form @onsubmit="@AddTodoAsync">
<div class="input-group mb-3">
<input type="text" @bind="@newTodo" class="form-control" placeholder="Something todo" aria-label="Something todo" aria-describedby="button-add">
<button class="btn btn-outline-secondary" type="submit" id="button-add">Add Todo</button>
</div>
</form>

@code {

Expand Down Expand Up @@ -81,4 +90,4 @@

todos.Remove(item);
}
}
}
21 changes: 7 additions & 14 deletions orleans/Blazor/BlazorWasm/BlazorWasm.Client/Services/ApiService.cs
Original file line number Diff line number Diff line change
@@ -1,31 +1,24 @@
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Options;
using BlazorWasm.Models;
using System.Net.Http.Json;

namespace BlazorWasm.Services;

public class ApiService
public class ApiService(IOptions<ApiServiceOptions> options, HttpClient client)
{
private readonly HttpClient _client;
private readonly ApiServiceOptions _options;

public ApiService(IOptions<ApiServiceOptions> options, HttpClient client)
{
_client = client;
_options = options.Value;
}
private readonly ApiServiceOptions _options = options.Value;

public Task<WeatherInfo[]?> GetWeatherForecastAsync() =>
_client.GetFromJsonAsync<WeatherInfo[]>($"{_options.BaseAddress}/Weather");
client.GetFromJsonAsync<WeatherInfo[]>($"{_options.BaseAddress}/Weather");

public Task<IEnumerable<TodoItem>?> GetTodosAsync(Guid ownerKey) =>
_client.GetFromJsonAsync<IEnumerable<TodoItem>>($"{_options.BaseAddress}/todo/list/{ownerKey}");
client.GetFromJsonAsync<IEnumerable<TodoItem>>($"{_options.BaseAddress}/todo/list/{ownerKey}");

public Task SetTodoAsync(TodoItem item) =>
_client.PostAsJsonAsync($"{_options.BaseAddress}/todo", item);
client.PostAsJsonAsync($"{_options.BaseAddress}/todo", item);

public Task DeleteTodoAsync(Guid itemKey) =>
_client.DeleteAsync($"{_options.BaseAddress}/todo/{itemKey}");
client.DeleteAsync($"{_options.BaseAddress}/todo/{itemKey}");
}

public static class ApiServiceBuilderExtensions
Expand Down

Large diffs are not rendered by default.

This file was deleted.

49 changes: 27 additions & 22 deletions orleans/Blazor/BlazorWasm/BlazorWasm.Server/Api/TodoController.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc;
using BlazorWasm.Grains;
using BlazorWasm.Models;
using System.ComponentModel.DataAnnotations;
Expand All @@ -24,30 +24,35 @@ public Task DeleteAsync([Required] Guid itemKey) =>
[HttpGet("list/{ownerKey}", Name = "list")]
public async Task<IEnumerable<TodoItem>> ListAsync([Required] Guid ownerKey)
{
// Get all item keys for this owner.
var keys =
await _factory.GetGrain<ITodoManagerGrain>(ownerKey)
.GetAllAsync();
// get all the todo item keys for this owner
var itemKeys = await _factory
.GetGrain<ITodoManagerGrain>(ownerKey)
.GetAllAsync();

// Fast path for empty owner.
if (keys.Length is 0) return Array.Empty<TodoItem>();
// fan out to get the individual items from the cluster in parallel
// issue all individual requests at the same time
var tasks = itemKeys
.Select(async itemId =>
{
var item = await _factory
.GetGrain<ITodoGrain>(itemId)
.GetAsync();

// Fan out and get all individual items in parallel.
// Issue all requests at the same time.
var tasks =
keys.Select(key => _factory.GetGrain<ITodoGrain>(key).GetAsync())
.ToList();
// we can get a null result if the individual grain failed to unregister
// in this case we can finish the job here
if (item is null)
{
await _factory
.GetGrain<ITodoManagerGrain>(ownerKey)
.UnregisterAsync(itemId);
}

// Compose the result as requests complete
var result = new List<TodoItem>();
for (var i = 0; i < keys.Length; ++i)
{
var item = await tasks[i];
if (item is null) continue;
result.Add(item);
}
return item;
});

var result = await Task.WhenAll(tasks);

return result;
return result.OfType<TodoItem>();
}

[HttpPost]
Expand All @@ -58,7 +63,7 @@ public async Task<ActionResult> PostAsync([FromBody] TodoItemModel model)
return BadRequest(ModelState);
}

var item = new TodoItem(model.Key, model.Title, model.IsDone, model.OwnerKey);
var item = new TodoItem(model.Key, model.Title, model.IsDone, model.OwnerKey, DateTime.UtcNow);
await _factory.GetGrain<ITodoGrain>(item.Key).SetAsync(item);
return Ok();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc;
using BlazorWasm.Grains;
using BlazorWasm.Models;

namespace Sample.Silo.Api;

[ApiController]
[ApiVersion("1")]
[Route("api/[controller]")]
public class WeatherController : ControllerBase
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<ServerGarbageCollection>true</ServerGarbageCollection>
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="5.1.0" />
<PackageReference Include="Microsoft.Orleans.Server" Version="8.0.0" />
<PackageReference Include="Microsoft.Orleans.Streaming" Version="8.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="Microsoft.Orleans.Server" Version="9.0.1" />
<PackageReference Include="Microsoft.Orleans.Streaming" Version="9.0.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ namespace BlazorWasm.Grains;
public interface ITodoManagerGrain : IGrainWithGuidKey
{
Task RegisterAsync(Guid itemKey);

Task UnregisterAsync(Guid itemKey);

Task<ImmutableArray<Guid>> GetAllAsync();
Task<ImmutableHashSet<Guid>> GetAllAsync();
}
72 changes: 32 additions & 40 deletions orleans/Blazor/BlazorWasm/BlazorWasm.Server/Grains/TodoGrain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,87 +3,79 @@

namespace BlazorWasm.Grains;

public class TodoGrain : Grain, ITodoGrain
public class TodoGrain(
ILogger<TodoGrain> logger,
[PersistentState("State")] IPersistentState<TodoGrain.State> state) : Grain, ITodoGrain
{
private readonly ILogger<TodoGrain> _logger;
private readonly IPersistentState<State> _state;

private string GrainType => nameof(TodoGrain);
private Guid GrainKey => this.GetPrimaryKey();

public TodoGrain(
ILogger<TodoGrain> logger,
[PersistentState("State")] IPersistentState<State> state)
{
_logger = logger;
_state = state;
}
private Guid GrainKey => this.GetPrimaryKey();

public Task<TodoItem?> GetAsync() => Task.FromResult(_state.State.Item);
public Task<TodoItem?> GetAsync() => Task.FromResult(state.State.Item);

public async Task SetAsync(TodoItem item)
{
// ensure the key is consistent
// Ensure the key is consistent
if (item.Key != GrainKey)
{
throw new InvalidOperationException();
}

// save the item
_state.State.Item = item;
await _state.WriteStateAsync();
// Save the item
state.State = state.State with { Item = item };
await state.WriteStateAsync();

// register the item with its owner list
// Register the item with its owner list
await GrainFactory.GetGrain<ITodoManagerGrain>(item.OwnerKey)
.RegisterAsync(item.Key);

// for sample debugging
_logger.LogInformation(
// For sample debugging
logger.LogInformation(
"{@GrainType} {@GrainKey} now contains {@Todo}",
GrainType, GrainKey, item);

// notify listeners - best effort only
this.GetStreamProvider("MemoryStreams")
.GetStream<TodoNotification>(StreamId.Create(nameof(ITodoGrain), item.OwnerKey))
// Notify listeners - best effort only
var streamId = StreamId.Create(nameof(ITodoGrain), item.OwnerKey);
this.GetStreamProvider("MemoryStreams").GetStream<TodoNotification>(streamId)
.OnNextAsync(new TodoNotification(item.Key, item))
.Ignore();
}

public async Task ClearAsync()
{
// fast path for already cleared state
if (_state.State.Item is null) return;
// Fast path for already cleared state
if (state.State.Item is null) return;

// hold on to the keys
var itemKey = _state.State.Item.Key;
var ownerKey = _state.State.Item.OwnerKey;
// Hold on to the keys
var itemKey = state.State.Item.Key;
var ownerKey = state.State.Item.OwnerKey;

// unregister from the registry
// Unregister from the registry
await GrainFactory.GetGrain<ITodoManagerGrain>(ownerKey)
.UnregisterAsync(itemKey);

// clear the state
await _state.ClearStateAsync();
// Clear the state
await state.ClearStateAsync();

// for sample debugging
_logger.LogInformation(
// For sample debugging
logger.LogInformation(
"{@GrainType} {@GrainKey} is now cleared",
GrainType, GrainKey);

// notify listeners - best effort only
this.GetStreamProvider("MemoryStreams")
.GetStream<TodoNotification>(StreamId.Create(nameof(ITodoGrain), itemKey))
// Notify listeners - best effort only
var streamId = StreamId.Create(nameof(ITodoGrain), ownerKey);
this.GetStreamProvider("MemoryStreams").GetStream<TodoNotification>(streamId)
.OnNextAsync(new TodoNotification(itemKey, null))
.Ignore();

// no need to stay alive anymore
// No need to stay alive anymore
DeactivateOnIdle();
}

[GenerateSerializer]
public class State
[GenerateSerializer, Immutable]
public sealed record class State
{
[Id(0)]
public TodoItem? Item { get; set; }
public TodoItem? Item { get; init; }
}
}
Loading

0 comments on commit 6d3cfcd

Please sign in to comment.