Skip to content

Commit

Permalink
Upgrade Orleans Chirper sample to dotnet 9
Browse files Browse the repository at this point in the history
  • Loading branch information
egil committed Jan 26, 2025
1 parent 0a82190 commit 94ca230
Show file tree
Hide file tree
Showing 12 changed files with 67 additions and 65 deletions.
2 changes: 1 addition & 1 deletion orleans/Chirper/Chirper.Client/Chirper.Client.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
Expand Down
17 changes: 4 additions & 13 deletions orleans/Chirper/Chirper.Client/ShellHostedService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,12 @@

namespace Chirper.Client;

public sealed partial class ShellHostedService : BackgroundService
public sealed partial class ShellHostedService(IClusterClient client, IHostApplicationLifetime applicationLifetime) : BackgroundService
{
private readonly IClusterClient _client;
private readonly IHostApplicationLifetime _applicationLifetime;

private ChirperConsoleViewer? _viewer;
private IChirperViewer? _viewerRef;
private IChirperAccount? _account;

public ShellHostedService(IClusterClient client, IHostApplicationLifetime applicationLifetime)
{
_client = client;
_applicationLifetime = applicationLifetime;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
ShowHelp(true);
Expand All @@ -35,7 +26,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
}
else if (command is null or "/quit")
{
_applicationLifetime.StopApplication();
applicationLifetime.StopApplication();
return;
}
else if (command.StartsWith("/user "))
Expand All @@ -44,7 +35,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await Unobserve();
var username = match.Groups["username"].Value;
_account = _client.GetGrain<IChirperAccount>(username);
_account = client.GetGrain<IChirperAccount>(username);

AnsiConsole.MarkupLine("[bold grey][[[/][bold lime]✓[/][bold grey]]][/] The current user is now [navy]{0}[/]", username);
}
Expand Down Expand Up @@ -121,7 +112,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
if (_viewerRef is null)
{
_viewer = new ChirperConsoleViewer(_account.GetPrimaryKeyString());
_viewerRef = _client.CreateObjectReference<IChirperViewer>(_viewer);
_viewerRef = client.CreateObjectReference<IChirperViewer>(_viewer);
}

await _account.SubscribeAsync(_viewerRef);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
namespace System.Collections.Generic;
namespace System.Collections.Generic;

/// <summary>
/// Helper extensions for enumerables.
/// Helper extensions for <see cref="IEnumerable{T}">.
/// </summary>
public static class EnumerableExtensions
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
namespace Chirper.Grains.Models;
namespace Chirper.Grains.Models;

/// <summary>
/// Data object representing one Chirp message entry
/// </summary>
[GenerateSerializer]
[GenerateSerializer, Immutable]
public record class ChirperMessage(
/// <summary>
/// The message content for this chirp message entry.
Expand Down
2 changes: 1 addition & 1 deletion orleans/Chirper/Chirper.Grains/Chirper.Grains.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
Expand Down
77 changes: 35 additions & 42 deletions orleans/Chirper/Chirper.Grains/ChirperAccount.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Collections.Immutable;
using System.Collections.Immutable;
using Chirper.Grains.Models;
using Microsoft.Extensions.Logging;
using Orleans.Concurrency;
Expand All @@ -7,8 +7,12 @@
namespace Chirper.Grains;

[Reentrant]
public sealed class ChirperAccount : Grain, IChirperAccount
public sealed class ChirperAccount(
[PersistentState(stateName: "account", storageName: "AccountState")] IPersistentState<ChirperAccountState> state,
ILogger<ChirperAccount> logger) : Grain, IChirperAccount
{
private static string GrainType => nameof(ChirperAccount);

/// <summary>
/// Size for the recently received message cache.
/// </summary>
Expand All @@ -29,28 +33,17 @@ public sealed class ChirperAccount : Grain, IChirperAccount
/// This list is not part of state and will not survive grain deactivation.
/// </summary>
private readonly HashSet<IChirperViewer> _viewers = new();
private readonly ILogger<ChirperAccount> _logger;
private readonly IPersistentState<ChirperAccountState> _state;

/// <summary>
/// Allows state writing to happen in the background.
/// </summary>
private Task? _outstandingWriteStateOperation;

public ChirperAccount(
[PersistentState(stateName: "account", storageName: "AccountState")] IPersistentState<ChirperAccountState> state,
ILogger<ChirperAccount> logger)
{
_state = state;
_logger = logger;
}

private static string GrainType => nameof(ChirperAccount);
private string GrainKey => this.GetPrimaryKeyString();

public override Task OnActivateAsync(CancellationToken _)
{
_logger.LogInformation("{GrainType} {GrainKey} activated.", GrainType, GrainKey);
logger.LogInformation("{GrainType} {GrainKey} activated.", GrainType, GrainKey);

return Task.CompletedTask;
}
Expand All @@ -59,49 +52,49 @@ public async ValueTask PublishMessageAsync(string message)
{
var chirp = CreateNewChirpMessage(message);

_logger.LogInformation("{GrainType} {GrainKey} publishing new chirp message '{Chirp}'.",
logger.LogInformation("{GrainType} {GrainKey} publishing new chirp message '{Chirp}'.",
GrainType, GrainKey, chirp);

_state.State.MyPublishedMessages.Enqueue(chirp);
state.State.MyPublishedMessages.Enqueue(chirp);

while (_state.State.MyPublishedMessages.Count > PublishedMessagesCacheSize)
while (state.State.MyPublishedMessages.Count > PublishedMessagesCacheSize)
{
_state.State.MyPublishedMessages.Dequeue();
state.State.MyPublishedMessages.Dequeue();
}

await WriteStateAsync();

// notify viewers of new message
_logger.LogInformation("{GrainType} {GrainKey} sending new chirp message to {ViewerCount} viewers.",
logger.LogInformation("{GrainType} {GrainKey} sending new chirp message to {ViewerCount} viewers.",
GrainType, GrainKey, _viewers.Count);

_viewers.ForEach(_ => _.NewChirp(chirp));

// notify followers of a new message
_logger.LogInformation("{GrainType} {GrainKey} sending new chirp message to {FollowerCount} followers.",
GrainType, GrainKey, _state.State.Followers.Count);
logger.LogInformation("{GrainType} {GrainKey} sending new chirp message to {FollowerCount} followers.",
GrainType, GrainKey, state.State.Followers.Count);

await Task.WhenAll(_state.State.Followers.Values.Select(_ => _.NewChirpAsync(chirp)).ToArray());
await Task.WhenAll(state.State.Followers.Values.Select(_ => _.NewChirpAsync(chirp)).ToArray());
}

public ValueTask<ImmutableList<ChirperMessage>> GetReceivedMessagesAsync(int number, int start)
{
if (start < 0) start = 0;
if (start + number > _state.State.RecentReceivedMessages.Count)
if (start + number > state.State.RecentReceivedMessages.Count)
{
number = _state.State.RecentReceivedMessages.Count - start;
number = state.State.RecentReceivedMessages.Count - start;
}

return ValueTask.FromResult(
_state.State.RecentReceivedMessages
state.State.RecentReceivedMessages
.Skip(start)
.Take(number)
.ToImmutableList());
}

public async ValueTask FollowUserIdAsync(string username)
{
_logger.LogInformation(
logger.LogInformation(
"{GrainType} {UserName} > FollowUserName({TargetUserName}).",
GrainType,
GrainKey,
Expand All @@ -111,7 +104,7 @@ public async ValueTask FollowUserIdAsync(string username)

await userToFollow.AddFollowerAsync(GrainKey, this.AsReference<IChirperSubscriber>());

_state.State.Subscriptions[username] = userToFollow;
state.State.Subscriptions[username] = userToFollow;

await WriteStateAsync();

Expand All @@ -121,7 +114,7 @@ public async ValueTask FollowUserIdAsync(string username)

public async ValueTask UnfollowUserIdAsync(string username)
{
_logger.LogInformation(
logger.LogInformation(
"{GrainType} {GrainKey} > UnfollowUserName({TargetUserName}).",
GrainType,
GrainKey,
Expand All @@ -132,7 +125,7 @@ await GrainFactory.GetGrain<IChirperPublisher>(username)
.RemoveFollowerAsync(GrainKey);

// remove this publisher from the subscriptions list
_state.State.Subscriptions.Remove(username);
state.State.Subscriptions.Remove(username);

// save now
await WriteStateAsync();
Expand All @@ -142,10 +135,10 @@ await GrainFactory.GetGrain<IChirperPublisher>(username)
}

public ValueTask<ImmutableList<string>> GetFollowingListAsync() =>
ValueTask.FromResult(_state.State.Subscriptions.Keys.ToImmutableList());
ValueTask.FromResult(state.State.Subscriptions.Keys.ToImmutableList());

public ValueTask<ImmutableList<string>> GetFollowersListAsync() =>
ValueTask.FromResult(_state.State.Followers.Keys.ToImmutableList());
ValueTask.FromResult(state.State.Followers.Keys.ToImmutableList());

public ValueTask SubscribeAsync(IChirperViewer viewer)
{
Expand All @@ -162,50 +155,50 @@ public ValueTask UnsubscribeAsync(IChirperViewer viewer)
public ValueTask<ImmutableList<ChirperMessage>> GetPublishedMessagesAsync(int number, int start)
{
if (start < 0) start = 0;
if (start + number > _state.State.MyPublishedMessages.Count)
if (start + number > state.State.MyPublishedMessages.Count)
{
number = _state.State.MyPublishedMessages.Count - start;
number = state.State.MyPublishedMessages.Count - start;
}
return ValueTask.FromResult(
_state.State.MyPublishedMessages
state.State.MyPublishedMessages
.Skip(start)
.Take(number)
.ToImmutableList());
}

public async ValueTask AddFollowerAsync(string username, IChirperSubscriber follower)
{
_state.State.Followers[username] = follower;
state.State.Followers[username] = follower;
await WriteStateAsync();
_viewers.ForEach(cv => cv.NewFollower(username));
}

public ValueTask RemoveFollowerAsync(string username)
{
_state.State.Followers.Remove(username);
state.State.Followers.Remove(username);
return WriteStateAsync();
}

public async Task NewChirpAsync(ChirperMessage chirp)
{
_logger.LogInformation(
logger.LogInformation(
"{GrainType} {GrainKey} received chirp message = {Chirp}",
GrainType,
GrainKey,
chirp);

_state.State.RecentReceivedMessages.Enqueue(chirp);
state.State.RecentReceivedMessages.Enqueue(chirp);

// only relevant when not using fixed queue
while (_state.State.RecentReceivedMessages.Count > ReceivedMessagesCacheSize) // to keep not more than the max number of messages
while (state.State.RecentReceivedMessages.Count > ReceivedMessagesCacheSize) // to keep not more than the max number of messages
{
_state.State.RecentReceivedMessages.Dequeue();
state.State.RecentReceivedMessages.Dequeue();
}

await WriteStateAsync();

// notify any viewers that a new chirp has been received
_logger.LogInformation(
logger.LogInformation(
"{GrainType} {GrainKey} sending received chirp message to {ViewerCount} viewers",
GrainType,
GrainKey,
Expand Down Expand Up @@ -246,7 +239,7 @@ private async ValueTask WriteStateAsync()
if (_outstandingWriteStateOperation is null)
{
// If after the initial write is completed, no other request initiated a new write operation, do it now.
currentWriteStateOperation = _state.WriteStateAsync();
currentWriteStateOperation = state.WriteStateAsync();
_outstandingWriteStateOperation = currentWriteStateOperation;
}
else
Expand Down
2 changes: 1 addition & 1 deletion orleans/Chirper/Chirper.Grains/ChirperAccountState.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Chirper.Grains.Models;
using Chirper.Grains.Models;

namespace Chirper.Grains;

Expand Down
2 changes: 1 addition & 1 deletion orleans/Chirper/Chirper.Server/Chirper.Server.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
Expand Down
3 changes: 3 additions & 0 deletions orleans/Chirper/Chirper.sln
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Chirper.Server", "Chirper.Server\Chirper.Server.csproj", "{2C967379-D025-4EBB-96C0-1A4C6CBE4B4B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Chirper.Client", "Chirper.Client\Chirper.Client.csproj", "{3BE53D35-938D-47D8-B135-501786B145DC}"
ProjectSection(ProjectDependencies) = postProject
{2C967379-D025-4EBB-96C0-1A4C6CBE4B4B} = {2C967379-D025-4EBB-96C0-1A4C6CBE4B4B}
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1223A865-0D72-4BF7-8FCD-016A48F3EC68}"
ProjectSection(SolutionItems) = preProject
Expand Down
15 changes: 15 additions & 0 deletions orleans/Chirper/Chirper.slnLaunch
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[
{
"Name": "Server + Client",
"Projects": [
{
"Path": "Chirper.Client\\Chirper.Client.csproj",
"Action": "Start"
},
{
"Path": "Chirper.Server\\Chirper.Server.csproj",
"Action": "Start"
}
]
}
]
2 changes: 1 addition & 1 deletion orleans/Chirper/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ There is also an `IChirperViewer` observer interface for applications to subscri

## Sample prerequisites

This sample is written in C# and targets .NET 8.0. It requires the [.NET 8.0 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) or later.
This sample is written in C# and targets .NET 9.0. It requires the [.NET 9.0 SDK](https://dotnet.microsoft.com/download/dotnet/9.0) or later.

## Building the sample

Expand Down

0 comments on commit 94ca230

Please sign in to comment.