GithubHelp home page GithubHelp logo

servicetitan / stl.fusion Goto Github PK

View Code? Open in Web Editor NEW
1.8K 108.0 106.0 114.55 MB

Build real-time apps (Blazor included) with less than 1% of extra code responsible for real-time updates. Host 10-1000x faster APIs relying on transparent and nearly 100% consistent caching. We call it DREAM, or Distributed REActive Memoization, and it's here to turn real-time on!

License: MIT License

C# 99.78% Batchfile 0.05% HTML 0.09% JavaScript 0.07% Shell 0.01%
blazor blazor-server blazor-webassembly realtime realtime-tracking real-time websockets caching caching-library caching-memory

stl.fusion's Introduction

๐Ÿ‘พ Fusion: the "real-time on!" switch that actually exists

Build Coverage NuGet Version MIT License
Discord Server Commit Activity Downloads

Fusion is a .NET library that implements ๐Ÿฆ„ Distributed REActive Memoization (DREAM) โ€“ a novel abstraction somewhat similar to MobX or Flux, but designed to deal with an arbitrary large state spanning across your backend microservices, API servers, and reaching even every client of your app.

Fusion solves a set of infamously hard problems with a single hammer:

Problem So you don't need...
๐Ÿ“‡ Caching Redis, memcached, ...
๐Ÿคน Real-time cache invalidation No good solutions -
it's an infamously hard problem
๐Ÿš€ Real-time updates SignalR, WebSockets, gRPC, ...
๐Ÿคฌ Network chattiness A fair amount of code
๐Ÿ”Œ Offline mode support A fair amount of code
๐Ÿ“ฑ Client-side state management MobX, Flux/Redux, Recoil, ...
๐Ÿ’ฐ Single codebase for Blazor WebAssembly, Server, and Hybrid/MAUI No good alternatives

And the best part is: Fusion does all of that transparently for you, so Fusion-based code is almost identical to a code that doesn't involve it. All you need is to:

  • "Implement" IComputeService (a tagging interface) on your Fusion service to ensure call intercepting proxy is generated for it in compile time.
  • Mark methods requiring "Fusion behavior" with [ComputeMethod] + declare them as virtual
  • Register the service via serviceCollection.AddFusion().AddService<MyService>()
  • Resolve and use them usual - i.e., pass them as dependencies, call their methods, etc.

The magic happens when [ComputeMethod]-s are invoked:

  1. When Fusion knows that a value for a given call (think (serviceInstance, method, args...) cache key) is still consistent, Fusion returns it instantly, without letting the method to run.
  2. And when the value isn't cached or tagged as inconsistent, Fusion lets the method run, but captures new value's dependencies in process. "Dependency" is one [ComputeMethod] call triggered during the evaluation of another [ComputeMethod] call.

The second step allows Fusion to track which values are expected to change when one of them changes. It's quite similar to lot traceability, but implemented for arbitrary functions rather than manufacturing processes.

The last piece of a puzzle is Computed.Invalidate() block allowing to tag cached results as "inconsistent with the ground truth". Here is how you use it:

var avatars = await GetUserAvatars(userId);
using (Computed.Invalidate()) {
    // Any [ComputeMethod] invoked inside this block doesn't run normally,
    // but invalidates the result of the identical call instead.
    // Such calls complete synchronously and return completed Task<TResult>, 
    // so you don't need to await them.

    _ = userService.GetUser(userId);
    foreach (var avatar in avatars)
        _ = userAvatarService.GetAvatar(userId, avatar.Id);
}

The invalidation is always transitive: if GetUserProfile(3) calls GetUserAvatar("3:ava1"), and GetUserAvatar("3:ava1") gets invalidated, GetUserProfile(3) gets invalidated as well.

To make it work, Fusion maintains a dictionary-like structure that tracks recent and "observed" call results:

  • Key: (serviceInstance, method, call arguments...)
  • Value: [Computed], which stores the result, consistency state (Computing, Consistent, Invalidated) and dependent-dependency links. Computed<T> instances are nearly immutable: once constructed, they can only transition to Inconsistent state.

You can "pull" the Computed<T> instance "backing" certain call like this:

var computed1 = await Computed.Capture(() => GetUserProfile(3));
// You can await await for its invalidation:
await computed1.WhenInvalidated();
Assert.IsFalse(computed1.IsConsistent());
// And recompute it:
var computed2 = await computed1.Recompute();

So any Computed<T> is observable. Moreover, it can be a "replica" of a remote Computed<T> instance that mirrors its state in your local process, so the dependency graph can be distributed. To make it work, Fusion uses its own WebSocket-based RPC protocol, which is quite similar to any other RPC protocol:

  1. To "send" the call to a remote peer, client sends "call" message
  2. The peer responds to it with "call result" message. So far there is no difference with any other RPC protocol.
  3. And here is the unique step: the peer may later send a message telling that the call result it sent earlier was invalidated.

Step 3 doesn't change much in terms of network traffic: it's either zero or one extra message per call (i.e. 3 messages instead of 2 in the worst case). But this small addition allows Compute Service Clients to know precisely when a given cached call result becomes inconsistent.

The presence of step 3 makes a huge difference: any cached & still consistent result is as good as the data you'll get from the remote server, right? So it's totally fine to resolve a call that "hits" such a result locally, incurring no network round-trip!

Finally, any Compute Service Client behaves as a similar local Compute Service. Look at this code:

string GetUserName(id)
    => (await userService.GetUser(id)).Name;

You can't tell whether userService here is a local compute service or a compute service client, right?

  • Both options are of the same base type (e.g. IUserService). The implementations are different though: Fusion service client is registered via fusion.AddClient<TInterface>() vs fusion.AddServer<TInterface, TService>() for the server.
  • And behave identically:
    • Every call you make to userService terminates instantly if its previous result is still consistent
    • And if GetUserName is a method of another computed service (a local one), computed value backing GetUser(id) call that it makes would automatically extend Fusion's dependency graph for GetUserName(id) call!

So Fusion abstracts away the "placement" of a service, and does it much better than conventional RPC proxies: Fusion proxies aren't "chatty" by default!

Documentation

If you prefer slides, check out "Why real-time web apps need Blazor and Fusion?" talk - it explains how many problems we tackle are connected, how Fusion addresses the root cause, and how to code a simplified version of Fusion's key abstraction in C#.

The slides are slightly outdated - e.g. now Fusion clients use Stl.Rpc rather than HTTP to communicate with the server, but all the concepts they cover are still intact.

Quick Start, Cheat Sheet, and the Tutorial are the best places to start from.

Check out Samples; some of them are covered further in this document.

"What is your evidence?"*

All of this sounds way too good to be true, right? That's why there are lots of visual proofs in the remaining part of this document. But if you'll find anything concerning in Fusion's source code or samples, please feel free to grill us with questions on Discord!

Let's start with some big guns:

Check out Actual Chat โ€“ a very new chat app built by the minds behind Fusion.

Actual Chat fuses real-time audio, live transcription, and AI assistance to let you communicate with utmost efficiency. With clients for WebAssembly, iOS, Android, and Windows, it boasts nearly 100% code sharing across these platforms. Beyond real-time updates, several of its features, like offline mode, are powered by Fusion.

We're posting some code examples from Actual Chat codebase here, so join this chat to learn how we use it in a real app.

Now, the samples:

Below is Fusion+Blazor Sample delivering real-time updates to 3 browser windows:

Play with live version of this sample right now!

The sample supports both Blazor Server and Blazor WebAssembly hosting modes. And even if you use different modes in different windows, Fusion still keeps in sync literally every bit of a shared state there, including the sign-in state:

Is Fusion fast?

Yes, it's incredibly fast. Here is an RPC call duration distribution for one of the most frequent calls on Actual Chat:

IChats.GetTile reads a small "chat tile" - typically 5 entries pinned to a specific ID range, so it can be efficiently cached. And even for these calls the typical response time is barely measurable: every X axis mark is 10x larger than the previous one, so the highest peak you see is at 0.03ms!

The next bump at ~ 4-5ms is when the service actually goes to the DB - i.e. it's the time you'd expect to see without Fusion. The load would be way higher though, coz the calls you see on this chart are the calls which "made it" to the server - in other words, they weren't eliminated by the client / its Fusion services.

A small synthetic benchmark in Fusion test suite compares "raw" Entity Framework Core-based Data Access Layer (DAL) against its version relying on Fusion:

Calls/s PostgreSQL MariaDB SQL Server Sqlite
Single reader 1.02K 645.77 863.33 3.79K
960 readers (high concurrency) 12.96K 14.52K 16.66K 16.50K
Single reader + Fusion 9.54M 9.28M 9.05M 8.92M
960 readers + Fusion 145.95M 140.29M 137.70M 141.40M

The raw output for this test on Ryzen Threadripper 3960X is here. The number of readers looks crazy at first, but it is tweaked to maximize the output for non-Fusion version of DAL (the readers are asynchronous, so they mostly wait for DB response there).

Fusion's transparent caching ensures every API call result your code produces is cached, and moreover, even when such results are recomputed, they mostly use other cached dependencies instead of hitting a much slower storage (DB in this case).

And interestingly, even when there are no "layers" of dependencies (think only "layer zero" is there), Fusion manages to speed up the API calls this test runs by 8,000 to 12,000 times.

What makes Fusion fast:

  • The concept itself is all about eliminating any unnecessary computation. Think msbuild, but for your method call results: what's computed and consistent is never recomputed.
  • Fusion caches call results in memory, so if it's a hit, they're instantly available. No round-trips to external caches, no serialization/deserialization, etc.
  • Moreover, there is also no cloning: what's cached is the .NET object or struct returned from a call, so any call result is "shared". It's way more CPU cache-friendly than e.g. deserializing a new copy on any hit.
  • Fusion uses its own Stl.Interception library to intercept method calls, and although there is no benchmark yet, these are the fastest call interceptors available on .NET - they're marginally faster than e.g. the ones provided by Castle.DynamicProxy. They don't box call arguments and require just 1 allocation per call.
  • The same is true about Stl.Rpc - a part of Fusion responsible for its RPC calls. Its preliminary benchmark results show it is ~ 1.5x faster than SignalR, and ~ 3x faster than gRPC.
  • Stl.Rpc uses the fastest serializers available on .NET โ€“ MemoryPack by default (it doesn't require runtime IL Emit), though you can also use MessagePack (it's slightly faster, but requires IL Emit) or anything else you prefer.
  • All critical execution paths in Fusion are heavily optimized. Archived version of this page shows the performance on above test currently 3x better than it was 2 years ago.

Does Fusion scale?

Yes. Fusion does something similar to what any MMORPG game engine does: even though the complete game state is huge, it's still possible to run the game in real time for 1M+ players, because every player observes a tiny fraction of a complete game state, and thus all you need is to ensure the observed part of the state fits in RAM.

And that's exactly what Fusion does:

  • It spawns the observed part of the state on-demand (i.e. when you call a Compute Service method)
  • Ensures the dependency graph backing this part of the state stays in memory while someone uses it
  • Destroys what's unobserved.

Check out "Scaling Fusion Services" part of the Tutorial to see a much more robust description of how Fusion scales.

Enough talk. Show me the code!

A typical Compute Service looks as follows:

public class ExampleService : IComputeService
{
    [ComputeMethod]
    public virtual async Task<string> GetValue(string key)
    { 
        // This method reads the data from non-Fusion "sources",
        // so it requires invalidation on write (see SetValue)
        return await File.ReadAllTextAsync(_prefix + key);
    }

    [ComputeMethod]
    public virtual async Task<string> GetPair(string key1, string key2)
    { 
        // This method uses only other [ComputeMethod]-s or static data,
        // thus it doesn't require invalidation on write
        var v1 = await GetNonFusionData(key1);
        var v2 = await GetNonFusionData(key2);
        return $"{v1}, {v2}";
    }

    public async Task SetValue(string key, string value)
    { 
        // This method changes the data read by GetValue and GetPair,
        // but since GetPair uses GetValue, it will be invalidated 
        // automatically once we invalidate GetValue.
        await File.WriteAllTextAsync(_prefix + key, value);
        using (Computed.Invalidate()) {
            // This is how you invalidate what's changed by this method.
            // Call arguments matter: you invalidate only a result of a 
            // call with matching arguments rather than every GetValue 
            // call result!
            _ = GetValue(key);
        }
    }
}

[ComputeMethod] indicates that every time you call this method, its result is "backed" by Computed Value, and thus it captures dependencies when it runs and instantly returns the result, if the current computed value is still consisntent.

Compute services are registered ~ almost like singletons:

var services = new ServiceCollection();
var fusion = services.AddFusion(); // It's ok to call it many times
// ~ Like service.AddSingleton<[TService, ]TImplementation>()
fusion.AddService<ExampleService>();

Check out CounterService from HelloBlazorServer sample to see the actual code of compute service.

Now, I guess you're curious how the UI code looks like with Fusion You'll be surprised, but it's as simple as it could be:

// MomentsAgoBadge.razor
@inherits ComputedStateComponent<string>
@inject IFusionTime _fusionTime

<span>@State.Value</span>

@code {
    [Parameter] 
    public DateTime Value { get; set; }

    protected override Task<string> ComputeState()
        => _fusionTime.GetMomentsAgo(Value) ;
}

MomentsAgoBadge is Blazor component displays "N [seconds/minutes/...] ago" string. The code above is almost identical to its actual code, which is a bit more complex due to null handling.

You see it uses IFusionTime - one of built-in compute services that provides GetUtcNow and GetMomentsAgo methods. As you might guess,the results of these methods are invalidated automatically; check out FusionTime service to see how it works.

But what's important here is that MomentsAgoBadge is inherited from ComputedStateComponent - an abstract type which provides ComputeState method. As you might guess, this method behaves like a [Compute Method].

ComputedStateComponent<T> exposes State property (of ComputedState<T> type), which allows you to get the most recent output of ComputeState()' via its Value property. "State" is another key Fusion abstraction - it implements a "wait for invalidation and recompute" loop similar to this one:

var computed = await Computed.Capture(_ => service.Method(...));
while (true) {
    await computed.WhenInvalidated();
    computed = await computed.Update();
}

The only difference is that it does this in a more robust way - in particular, it allows you to control the delays between the invalidation and the update, access the most recent non-error value, etc.

Finally, ComputedStateComponent automatically calls StateHasChanged() once its State gets updated to make sure the new value is displayed.

So if you use Fusion, you don't need to code any reactions in the UI. Reactions (i.e. partial updates and re-renders) happen automatically due to dependency chains that connect your UI components with the data providers they use, which in turn are connected to data providers they use, and so on - till the very basic "ingredient providers", i.e. compute methods that are invalidated on changes.

If you want to see a few more examples of similarly simple UI components, check out:

Why Fusion is a game changer for real-time apps?

Real-time typically implies you use events to deliver change notifications to every client which state might be impacted by this change, so you have to:

  1. Know which clients to notify about a particular event. This alone is a fairly hard problem - in particular, you need to know what every client "sees" now. Sending events for anything that's out of the "viewport" (e.g. a post you may see, but don't see right now) doesn't make sense, because it's a huge waste that severely limits the scalability. Similarly to MMORPG, the "visible" part of the state is tiny in comparison to the "available" one for most of web apps too.
  2. Apply events to the client-side state. Kind of an easy problem too, but note that you should do the same on server side as well, and keeping the logic in two completely different handlers in sync for every event is a source of potential problems in future.
  3. Make UI to properly update its event subscriptions on every client-side state change. This is what client-side code has to do to ensure p.1 properly works on server side. And again, this looks like a solvable problem on paper, but things get much more complex if you want to ensure your UI provides a truly eventually consistent view. Just think in which order you'd run "query the initial data" and "subscribe to the subsequent events" actions to see some issues here.
  4. Throttle down the rate of certain events (e.g. "like" events for every popular post). Easy on paper, but more complex if you want to ensure the user sees eventually consistent view on your system. In particular, this implies that every event you send "summarizes" the changes made by it and every event you discard, so likely, you'll need a dedicated type, producer, and handlers for each of such events.

And Fusion solves all these problems using a single abstraction allowing it to identifying and track data dependencies automatically.

Why Fusion is a game changer for Blazor apps with complex UI?

Fusion allows you to create truly independent UI components. You can embed them in any part of UI without any need to worry of how they'll interact with each other.

This makes Fusion a perfect fit for micro-frontends on Blazor: the ability to create loosely coupled UI components is paramount there.

Besides that, if your invalidation logic is correct, Fusion guarantees that your UI state is eventually consistent.

You might think all of this works only in Blazor Server mode. But no, all these UI components work in Blazor WebAssembly mode as well, which is another unique feature Fusion provides. Any Compute Service can be substituted with Compute Service Client, which not simply proxies the calls, but also completely kills the chattiness you'd expect from a regular client-side proxy.

Next Steps

  • Read Quick Start, Cheat Sheet, or the whole Tutorial
  • Check out Samples
  • Join our Discord Server to ask questions and track project updates. If you're curious, "why Discord," the server was created long before the first line of Actual Chat's code was written. However, a Fusion-powered alternative will be available quite soon :)

Posts And Other Content

P.S. If you've already spent some time learning about Fusion, please help us to make it better by completing Fusion Feedback Form (1โ€ฆ3 min).

stl.fusion's People

Contributors

adampaquette avatar alexis-kochetov avatar alexyakunin avatar alivedevil avatar crui3er avatar dependabot-preview[bot] avatar dependabot[bot] avatar frolyo avatar hypercodeplace avatar iqmulator avatar leonardo-ferreira avatar maheshwarist avatar meenzen avatar pintrarakha-st avatar riesvriend avatar themodem avatar timeshift92 avatar tyrrrz avatar wdichler avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

stl.fusion's Issues

Fusion client is trying to reconnect to a dead publisher for too long

The log part indicating this:

    Line 2:       R-da52z70rnU90aPED: connecting to ws://localhost:9000/fusion/ws?publisherId=P-vdFui-KP5JB3VFI&clientId=R-da52z70rnU90aPED...
    Line 14:       R-da52z70rnU90aPED: connecting to ws://localhost:9000/fusion/ws?publisherId=P-do-jCITwOmPKrjyx&clientId=R-da52z70rnU90aPED...
    Line 21:       R-da52z70rnU90aPED: connecting to ws://localhost:9000/fusion/ws?publisherId=P-lYvW9eC0qQ7tE3bG&clientId=R-da52z70rnU90aPED...
    Line 23:       R-da52z70rnU90aPED: connecting to ws://localhost:9000/fusion/ws?publisherId=P-vdFui-KP5JB3VFI&clientId=R-da52z70rnU90aPED...
    Line 35:       R-da52z70rnU90aPED: connecting to ws://localhost:9000/fusion/ws?publisherId=P-do-jCITwOmPKrjyx&clientId=R-da52z70rnU90aPED...
    Line 42:       R-da52z70rnU90aPED: connecting to ws://localhost:9000/fusion/ws?publisherId=P-lYvW9eC0qQ7tE3bG&clientId=R-da52z70rnU90aPED...
    Line 49:       R-da52z70rnU90aPED: connecting to ws://localhost:9000/fusion/ws?publisherId=P-vdFui_-KP5JB3VFI&clientId=R-da52z70rnU90aPED...

Exception:

info: Stl.Fusion.Client.WebSocketChannelProvider[0]
      R-da52z70rnU90aPED: connecting to ws://localhost:9000/fusion/ws?publisherId=P-vdFui_-KP5JB3VFI&clientId=R-da52z70rnU90aPED...
fail: Stl.Fusion.Client.WebSocketChannelProvider[0]
      R-da52z70rnU90aPED: error
      System.Net.WebSockets.WebSocketException (0x80004005): The server returned status code '400' when status code '101' was expected.
         at System.Net.WebSockets.WebSocketHandle.ConnectAsync(Uri uri, CancellationToken cancellationToken, ClientWebSocketOptions options)
         at Stl.Fusion.Client.WebSocketChannelProvider.CreateChannel(Symbol publisherId, CancellationToken cancellationToken)
fail: Stl.Fusion.Client.WebSocketChannelProvider[0]
      R-da52z70rnU90aPED: error
      System.Net.WebSockets.WebSocketException (0x80004005): The server returned status code '400' when status code '101' was expected.
         at System.Net.WebSockets.WebSocketHandle.ConnectAsync(Uri uri, CancellationToken cancellationToken, ClientWebSocketOptions options)
         at Stl.Fusion.Client.WebSocketChannelProvider.CreateChannel(Symbol publisherId, CancellationToken cancellationToken)
info: Stl.Fusion.Client.WebSocketChannelProvider[0]
      R-da52z70rnU90aPED: connecting to ws://localhost:9000/fusion/ws?publisherId=P-do-jCITwOmPKrjyx&clientId=R-da52z70rnU90aPED...
fail: Stl.Fusion.Client.WebSocketChannelProvider[0]
      R-da52z70rnU90aPED: error
      System.Net.WebSockets.WebSocketException (0x80004005): The server returned status code '400' when status code '101' was expected.
         at System.Net.WebSockets.WebSocketHandle.ConnectAsync(Uri uri, CancellationToken cancellationToken, ClientWebSocketOptions options)
         at Stl.Fusion.Client.WebSocketChannelProvider.CreateChannel(Symbol publisherId, CancellationToken cancellationToken)
info: Stl.Fusion.Client.WebSocketChannelProvider[0]
      R-da52z70rnU90aPED: connecting to ws://localhost:9000/fusion/ws?publisherId=P-lYvW9eC0qQ7tE3bG&clientId=R-da52z70rnU90aPED...
info: Stl.Fusion.Client.WebSocketChannelProvider[0]
      R-da52z70rnU90aPED: connecting to ws://localhost:9000/fusion/ws?publisherId=P-vdFui_-KP5JB3VFI&clientId=R-da52z70rnU90aPED...
fail: Stl.Fusion.Client.WebSocketChannelProvider[0]
      R-da52z70rnU90aPED: error
      System.Net.WebSockets.WebSocketException (0x80004005): The server returned status code '400' when status code '101' was expected.
         at System.Net.WebSockets.WebSocketHandle.ConnectAsync(Uri uri, CancellationToken cancellationToken, ClientWebSocketOptions options)
         at Stl.Fusion.Client.WebSocketChannelProvider.CreateChannel(Symbol publisherId, CancellationToken cancellationToken)
fail: Stl.Fusion.Client.WebSocketChannelProvider[0]
      R-da52z70rnU90aPED: error
      System.Net.WebSockets.WebSocketException (0x80004005): The server returned status code '400' when status code '101' was expected.
         at System.Net.WebSockets.WebSocketHandle.ConnectAsync(Uri uri, CancellationToken cancellationToken, ClientWebSocketOptions options)
         at Stl.Fusion.Client.WebSocketChannelProvider.CreateChannel(Symbol publisherId, CancellationToken cancellationToken)
info: Stl.Fusion.Client.WebSocketChannelProvider[0]
      R-da52z70rnU90aPED: connecting to ws://localhost:9000/fusion/ws?publisherId=P-do-jCITwOmPKrjyx&clientId=R-da52z70rnU90aPED...
fail: Stl.Fusion.Client.WebSocketChannelProvider[0]
      R-da52z70rnU90aPED: error
      System.Net.WebSockets.WebSocketException (0x80004005): The server returned status code '400' when status code '101' was expected.
         at System.Net.WebSockets.WebSocketHandle.ConnectAsync(Uri uri, CancellationToken cancellationToken, ClientWebSocketOptions options)
         at Stl.Fusion.Client.WebSocketChannelProvider.CreateChannel(Symbol publisherId, CancellationToken cancellationToken)
info: Stl.Fusion.Client.WebSocketChannelProvider[0]
      R-da52z70rnU90aPED: connecting to ws://localhost:9000/fusion/ws?publisherId=P-lYvW9eC0qQ7tE3bG&clientId=R-da52z70rnU90aPED...
fail: Stl.Fusion.Client.WebSocketChannelProvider[0]
      R-da52z70rnU90aPED: error
      System.Net.WebSockets.WebSocketException (0x80004005): The server returned status code '400' when status code '101' was expected.
         at System.Net.WebSockets.WebSocketHandle.ConnectAsync(Uri uri, CancellationToken cancellationToken, ClientWebSocketOptions options)
         at Stl.Fusion.Client.WebSocketChannelProvider.CreateChannel(Symbol publisherId, CancellationToken cancellationToken)
info: Stl.Fusion.Client.WebSocketChannelProvider[0]
      R-da52z70rnU90aPED: connecting to ws://localhost:9000/fusion/ws?publisherId=P-vdFui_-KP5JB3VFI&clientId=R-da52z70rnU90aPED...
fail: Stl.Fusion.Client.WebSocketChannelProvider[0]
      R-da52z70rnU90aPED: error
      System.Net.WebSockets.WebSocketException (0x80004005): The server returned status code '400' when status code '101' was expected.
         at System.Net.WebSockets.WebSocketHandle.ConnectAsync(Uri uri, CancellationToken cancellationToken, ClientWebSocketOptions options)
         at Stl.Fusion.Client.WebSocketChannelProvider.CreateChannel(Symbol publisherId, CancellationToken cancellationToken)

Rewrite AsyncLockSet

Its current impl. "works" just because there are no robust tests:

  • It uses Dictionary<...> inside AsyncLocal - I somehow decided it's fine, but obviously it's not (what's inside AsyncLocal isn't automatically thread-safe).
  • There are no tests for it

Recently I rewrote regular AsyncLock (+ added tests), and it makes sense to either try implementing AsyncLockSet on top of it (con: potentially many AsyncLocals w/ DP-style recursive calls involving AsyncLockSet), or think of how to do this better.

A DateTime value was incorrectly passed to the server in Korean.

in Korean,

The DateTime is sent to the server like this:

2022. 7. 29. ์˜ค์ „ 12:00:00

image

Looks like I'll have to send it like this:

2022-07-29T00:00:00

This value is the log delivered to 'HelloBlazorHybrid' 'Fetch Data' of 'Stl.Fusion.Samples' and can be checked:

info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished HTTP/2 GET https://localhost:7464/api/weatherForecast/getForecast?startDate=2022.+7.+29.+%EC%98%A4%EC%A0%84+12%3A00%3A00 - - - 400 - application/problem+json;+charset=utf-8 0.4104ms

It looks like it should be delivered like this:

https://localhost:7464/api/weatherForecast/getForecast?startDate=2022-07-29T00:00:00

image

Assistance Needed: List Method Not Retrieving Values and Not Subscribing to Updates

Hi @alexyakunin ๐Ÿ‘‹,

First off, a huge thanks for creating such a wonderful library! Iโ€™ve been navigating through Stl.Fusion and have stumbled upon a little hiccup that Iโ€™m hoping you could possibly assist me with.

Hereโ€™s a snippet of code where Iโ€™m experiencing the issue:

public interface ITripMonitor : IComputeService
{
    Task<int> AddOrUpdate(int tripId, CancellationToken cancellationToken = default);
    Task Remove(int tripId, CancellationToken cancellationToken = default);
    [ComputeMethod]
    Task<List<int>> List(CancellationToken cancellationToken = default);
    [ComputeMethod]
    Task<int> Count(CancellationToken cancellationToken = default);
}
public class TripMonitor : ITripMonitor
{
    private ImmutableList<int> _tripruning = ImmutableList<int>.Empty;
    private readonly IMutableState<int> _count;
    public TripMonitor(IStateFactory stateFactory)
        => _count = stateFactory.NewMutable<int>(0);
    public virtual Task<int> AddOrUpdate(int tripId, CancellationToken cancellationToken = default)
    {
        if (Computed.IsInvalidating())
            return null!;
        using var invalidating = Computed.Invalidate();
        _tripruning = _tripruning.RemoveAll(i => i == tripId).Add(tripId);
        _count.Value +=1;
        return Task.FromResult(tripId);
    }
    public virtual async Task Remove(int tripId, CancellationToken cancellationToken = default)
    {
        if (Computed.IsInvalidating())
            return;
        using var invalidating = Computed.Invalidate();
        _tripruning = _tripruning.RemoveAll(i => i == tripId);
        _count.Value -= 1;
    }
    public virtual Task<List<int>> List(CancellationToken cancellationToken = default)
    {
        return Task.FromResult(_tripruning.ToList());
    }
    public virtual async Task<int> Count(CancellationToken cancellationToken = default)
    {
        return await _count.Use(cancellationToken);
    }
}


//===============================//
@using Stl.CommandR.Commands;
@using Stl.Fusion.Blazor;
@using CleanArchitecture.Blazor.Application.Services;
@using Stl.Fusion.Extensions;
@using Stl.Fusion.UI;
@using Stl.Fusion;
@inherits ComputedStateComponent<string>
@inject ITripMonitor _tripMonitor
@inject UICommander UICommander
@inject IFusionTime _time
@{
    var state = State.ValueOrDefault;
    var error = State.Error;
}
@state
<br>
@error
<br>
<button class="btn btn-primary" @onclick="add">Add</button>
@code {
    protected override Task OnInitializedAsync() => State.Update().AsTask();
    protected override async Task<string> ComputeState(CancellationToken cancellationToken)
    {
        var result = await _tripMonitor.List(cancellationToken);
        var count = await _tripMonitor.Count(cancellationToken);
        return $"{count} / {string.Join(',',result.ToArray())} ";
    }
    async Task add()
    {
        var v = Random.Shared.Next(100);
        await UICommander.Run(LocalCommand.New(() => _tripMonitor.AddOrUpdate(v)));
    }
}
image

Currently, Iโ€™m scratching my head a bit ๐Ÿค” because the List method doesnโ€™t seem to be fetching values, nor subscribing to updates as I initially anticipated. Iโ€™ve tried a few things, but seem to be spinning my wheels a bit.

If you could shed some light on what might be going awry and any pointers on how to rectify it, Iโ€™d be ever so grateful! ๐Ÿ™

Thanks so much for your time and assistance!

Issue: STL Fussion not able to deal with recursive loops in classes to serialize

I've been trying to build an app based on Stl.Fusion but I'm running into some issues with serialisation of data objects from Server to Client.

The reason being that my DbClasses contain a recursive loop. E.g.:

    public record DbShop : LongKeyedEntity
    {
        [Required, MaxLength(120)]
        public string Name { get; set; } = "";

        public ICollection<DbShopItem> ShopItems { get; set; } = new List<DbShopItem>();



        [MaxLength(9999)]
        public string XPathPrice { get; set; } = "";
        [MaxLength(9999)]
        public string XPathAvailability { get; set; } = "";
        [MaxLength(9999)]
        public string AvailabilityText { get; set; } = "";
    }

    public record DbShopItem : LongKeyedEntity
    {
        [Required, MaxLength(120)]
        public string Name { get; init; } = "";

        [Required, MaxLength(9999)]
        public string Url { get; init; } = "";

        [MaxLength(9999)]
        public string LastPrice { get; init; } = "";
        public bool LastAvailable { get; init; } = false;
        [MaxLength(9999)]
        public string LastAvailableText { get; init; } = "";


        [Required, ForeignKey("DbStonkyShop")]
        public long DbStonkyShopId { get; init; }

        //[Required]
        //public DbStonkyShop? DbStonkyShop { get; init; }
    }

Normally you can configure ASP.NET Core controllers to ignore this by adding the following code:

services.AddMvc().AddNewtonsoftJson(options =>
{
    options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
})

However when I do this I run into some issues where STL Fusion makes use of their own JSON serializers which ignore this configuration.

For example the JsonNetSerializer class has its own DefaultSettings. Which can't be changed.

It seems though that in other places the normal JsonConvert.Serialize(...) method is used. With this you can actually override defaults by using JsonConvert.DefaultSettings.

Assistance/Improvement for Factories/Proxies using ActivatorUtilities

I'd like to know whether there is any better way of creating a Factory, that intercepts methods calls on that interface for passing these on to ActivatorUtilities, or if there is interest in this being integrated into Stl.Interception as a native supported pattern:

// should be singleton or scoped in MEDI
public sealed class ActivatorInterceptor(IServiceProvider services) : Interceptor
{
    private readonly ConcurrentDictionary<MethodInfo, ObjectFactory> _factories = [];
    private readonly IServiceProvider _services = services;

    public override TResult Intercept<TResult>(Invocation invocation)
    {
        var m = _factories.GetOrAdd(invocation.Method, CreateFactory<TResult>);
        return (TResult)m(_services, invocation.Arguments.ToArray());
    }

    private static ObjectFactory CreateFactory<T>(MethodInfo method) => CreateFactory<T>(method.GetParameters());

    private static ObjectFactory CreateFactory<T>(ParameterInfo[] parameters)
    {
        var arguments = new Type[parameters.Length];
        for (int i = 0; i < parameters.Length; i++)
        {
            arguments[i] = parameters[i].ParameterType;
        }

        return ActivatorUtilities.CreateFactory(typeof(T), arguments);
    }
}

public interface IFactory<TFactory> where TFactory : IFactory<TFactory>, IRequiresAsyncProxy;

Registering a factory:

public static IServiceCollection AddFactory<TFactory>(this IServiceCollection services) where TFactory : class, IFactory<TFactory>, IRequiresAsyncProxy
{
	services.AddSingleton(CreateFactory);
	return services;

	static TFactory CreateFactory(IServiceProvider services)
	{
		var interceptor = services.GetRequiredService<ActivatorInterceptor>();
		return services.ActivateProxy<TFactory>(interceptor);
	}
}

The interface is to make sure, that Stl.Generated-proxies are found correctly, and are available for consumption - this could probably be reduced to AddFactory<T> () where T : IRequiresAsyncProxy, but for me I implemented it this way for now.

And eventually could be used for something like:

public interface IViewModelFactory : IFactory<IViewModelFactory>, IRequiresFullProxy
{
    ConcreteViewModelTypeA ConcreteViewModelTypeA(int arg1, int arg2);
}

which returns an instance of

public record class ConcreteViewModelTypeA(
    int arg1,
    int arg2,
    ISomeService service,
    IOtherService otherService);

Regarding ArgumentList.ToArray() is probably not the best way to go about it (ObjectFactory is defined as delegate object? ObjectFactory(IServiceProvider, params object[] args), but I didn't see any other way as I figured ArgumentList.GetInvoker() isn't going to cut it, as it won't create the params-array when called.

blazor wasm only (no server side blazor) can't get auth schemes

I based my code on a new dotnet 6 wasm blazor project and as far as i can tell i implemented everything needed on both side trying my hardest to work out the parts that were serverside blazor in the samples vs wasm blazor and got the an app mostly working very similar to the blazor sample but the auth dropdown does not work because the schema list is empty. looking at the sample since it ran the host page serverside it uses c# code on the server to get the list of schemas using

var authSchemas = await _serverAuthHelper.GetSchemas(HttpContext);
    var sessionId = _serverAuthHelper.Session.Id.Value;

and then

<script>
        window.FusionAuth.schemas = "@authSchemas";
        window.FusionAuth.sessionId = "@sessionId";
    </script>

to set varable even the wasm version can access but if you are running a wasm with no serverside blazer the host page is only index.html so can't contain the c# serverside code to extract the values above.

Can you provide instructions or sample for a blazer example with out serverside blazer and auth support?

Use SourceGenerator instead of `Castle.DynamicProxy` to create proxy types?

When a client is published to a platform that does not support System.Reflection.Emit, (eg. IL2CPP in Unity, AOT compiled on iOS, futural Blazor based on Mono AOT, or any other AOT scenes.), dynamic proxy will surely not work. Thus static proxy should be used instead in order to cover more scenes. (It also helps a lot in performance.)

.NET 5 introduces a new SourceGenerator as an expanded feature of code analyzer, which allows code analyzers to generate additional sources to be compiled alongside the project. It can also be used as a code analyzer to validate the code of computed services (eg. Every method marked with ComputeMethodAttribute should be declared to be virtual and return some Task.).

There are more mature alternatives to SourceGenerator such as Mono.Cecil (that Fody uses to generate proxies), and Roslyn that Orleans uses to generate proxies. They could also be used before .NET 5 and C# 9.0 is formally released.

Sign the assemblies that could be signed

Mostly needed for .NET Framework projects that rely on assembly signing.

Everything except RestEase dependencies (i.e. except Stl.Fusion.Client & Blazor assemblies) can be signed.

[StateHasChangedAsync] System.NullReferenceException: 'Object reference not set to an instance of an object.'

Hello, how can fix, this error?

System.NullReferenceException
  HResult=0x80004003
  Message=Object reference not set to an instance of an object.
  Source=Blazorise
  StackTrace:
   at Blazorise.BaseComponent.get_ClassNames()
   at Blazorise.Bootstrap.Button.BuildRenderTree(RenderTreeBuilder builder)
   at Microsoft.AspNetCore.Components.Rendering.ComponentState.RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment renderFragment, Exception& renderFragmentException)
--- End of stack trace from previous location ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at Microsoft.AspNetCore.Components.Rendering.HtmlRenderer.HandleException(Exception exception)
   at Microsoft.AspNetCore.Components.RenderTree.Renderer.HandleExceptionViaErrorBoundary(Exception error, ComponentState errorSourceOrNull)
   at Microsoft.AspNetCore.Components.RenderTree.Renderer.ProcessRenderQueue()
--- End of stack trace from previous location ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at Microsoft.AspNetCore.Components.Rendering.HtmlRenderer.HandleException(Exception exception)
   at Microsoft.AspNetCore.Components.RenderTree.Renderer.ProcessRenderQueue()
   at Microsoft.AspNetCore.Components.RenderTree.Renderer.ProcessPendingRender()
   at Microsoft.AspNetCore.Components.RenderTree.Renderer.AddToRenderQueue(Int32 componentId, RenderFragment renderFragment)
   at Microsoft.AspNetCore.Components.ComponentBase.StateHasChanged()
   at Stl.Fusion.Blazor.ComponentExt.<>c__DisplayClass11_0.<StateHasChangedAsync>g__Invoker|0() in C:\Projects\C#\Stl.Fusion\src\Stl.Fusion.Blazor\ComponentExt.cs:line 108

  This exception was originally thrown at this call stack:
    [External Code]
    Stl.Fusion.Blazor.ComponentExt.StateHasChangedAsync.__Invoker|0() in ComponentExt.cs

image

Implement ConcurrentTimerSet + RefHolder.Hold / Release on top if it

Right now ComputedRegistry & ReplicaRegistry take care of KeepAlive too, i.e. they ensure the strong ref is held to the instance for a desirable period. Though this part could be generalized & completely separated as:

public interface ITimerSet<TKey> {
    Moment? TryGet(TKey key);
    bool Add(TKey key, Moment fireTime); 
    bool AddOrUpdate(TKey key, Moment fireTime, Func<TKey, Moment, Moment, bool>? mustUpdate = null);
    bool Remove(TKey key, Moment fireTime);
}

public class CoarseTimerSet<TKey> : ITimerSet<TKey> {
   private Action<TKey, Moment> _fireHandler;

   public int BucketCount { get; } // always power of 2, indicates how many buckets each level has
   public TimeSpan MinDelay { get; } // min precision, e.g. 1 second
   public TimeSpan MaxDelay { get; } // level count ~= log2(MaxDelay / MinDelay)

   ...
}

Long story short, to keep an object alive for KeepAlive time (as it's done in e.g. IComputed.Touch method), we add a "timer" with its key to CoarseTimerSet (assuming this timer set has no fire handler).

Exponential growth of buffer size?

I couldn't help to notice here that the buffer pool grows exponentially by a factor of 2... isn't that kinda dangerous? I mean, you should be fine for the first 10-12 bits, but after that you will start moving/copying fairly large portions of memory...

Have you consider using a strategy similar to RecyclableStream? Where a pool of smaller buffers are provisioned and then concatenated when more memory is needed... The growth of the total allocated pool grows linearly (according to the blockSize) but the large buffers can also grow exponentially... This is the big gain over the MemoryPool by the way

Fix SafeJsonSerializer

https://github.com/servicetitan/Stl/blob/master/src/Stl/Serialization/SafeJsonNetSerializer.cs

Currently it "solves" two problems:

  1. Safe denationalization - the Verifier delegate is used on deserialization to tell if the root type is fine to deserialize, and this piece is totally ok to keep
  2. But overall, it doesn't allow to serialize everything b/c it serializes only the top level type name, so anything nested that needs a type (e.g. Exception) won't deserialize.

#2 works the way it does to actually address the issue w/ WASM: mscorlib is named differently there, so this serializer "fixes" this issue by normalizing its name - and it can do this nicely only b/c there is a single place it needs to fix (root type name).

The right fix, though, would be to fix mscorlib name everywhere during the serialization. Not sure how to do this w/ JSON.NET, but pretty sure it's possible. And if this is done, the serializer would be a complete fit for Stl.Fusion needs:

  • Safe deserialization will be used there on "update request" end (Publisher, etc.) - the types should be limited just to a few message types there, the rest should be banned.
  • And no-filter deserialization will be used on "update/invalidate receiver" end (i.e. WASM client), where it's totally safe to desalinize everything (it's still a browser-sandboxed .NET).

Exception when using together with Moq 4.18.1

I suspect it may be caused by the fact that Moq 4.18.1 uses the new Castle.Core (DynamicProxy) version 5.0.0, while Stl uses 4.4.1 (the issue appears when upgrading Moq 4.16.1 to 4.18.1).

Code causing exception:

IServicesCollection Register(IServicesCollection services)
{
        return services
            .UseRegisterAttributeScanner()
            .RegisterFrom(assemblies)
            .Services;
}

Stacktrace:

System.Reflection.ReflectionTypeLoadException : Unable to load one or more of the requested types.
Could not load type 'Implementation' from assembly 'Stl.Fusion, Version=2.4.59.4351, Culture=neutral, PublicKeyToken=7c239217ec78b545' because the parent type is sealed.
Could not load type 'Implementation' from assembly 'Stl.Fusion, Version=2.4.59.4351, Culture=neutral, PublicKeyToken=7c239217ec78b545' because the parent type is sealed.
  ----> System.TypeLoadException : Could not load type 'Implementation' from assembly 'Stl.Fusion, Version=2.4.59.4351, Culture=neutral, PublicKeyToken=7c239217ec78b545' because the parent type is sealed.
  ----> System.TypeLoadException : Could not load type 'Implementation' from assembly 'Stl.Fusion, Version=2.4.59.4351, Culture=neutral, PublicKeyToken=7c239217ec78b545' because the parent type is sealed.
   at System.Reflection.RuntimeModule.GetTypes(RuntimeModule module)
   at System.Reflection.RuntimeModule.GetTypes()
   at Stl.Extensibility.MatchingTypeFinder.<>c.<.ctor>b__8_0(Assembly a)
   at System.Linq.Enumerable.SelectManySingleSelectorIterator`2.MoveNext()
   at Stl.Extensibility.MatchingTypeFinder..ctor(IEnumerable`1 candidates)
   at Stl.Extensibility.MatchingTypeFinder..ctor(IEnumerable`1 assemblies)
   at Stl.Extensibility.MatchingTypeFinder..ctor()
   at Stl.Fusion.Interception.ArgumentHandlerProvider.Options..ctor()
   at Stl.Fusion.FusionBuilder..ctor(IServiceCollection services)
   at Stl.Fusion.ServiceCollectionExt.AddFusion(IServiceCollection services)
   at Stl.Fusion.RegisterComputeServiceAttribute.Register(IServiceCollection services, Type implementationType)
   at Stl.RegisterAttributes.RegisterAttributeScanner.Register(IEnumerable`1 services, Boolean filterByScope, Boolean filterByType)
   at Stl.RegisterAttributes.RegisterAttributeScanner.RegisterFrom(Assembly[] assemblies)

Possible renames

Hi, I'm thinking of a few renames:

Renames in Stl.Fusion:

  • IComputed -> IDependency
  • IComputeService -> IFusionService, [ComputeMethod] -> [FusionMethod]
  • IReplicaService -> IFusionClient

Overall, above changes will replace "computed" and "replica" keywords that have special meaning in Fusion with seemingly a bit more clear variants.

All related types will be renamed to match these changes.

And similar renames in Stl:

  • ServiceAttributeBase -> RegisterAttributeBase, all of its descendants will get Register prefix
  • MatchingTypeFinder -> BehaviorFinder

Do you think it makes sense to do this?

Improve Purge in ComputedRegistry & ReplicaRegistry

There is a number of problems:

  1. Default capacity should be ~ 100-200 in WASM (it's almost 8K now - I tuned it for server-side scenarios)
  2. Use precise counter instead of stochastic counter in WASM (1 core = no point)
  3. Use a separate dict + maybe something similar to generations for strong refs. Ideally, strong refs should be replaced to weak ones as quickly as possible & Purge cycles in this part must be fairly frequent (1s or so), otherwise -- combined w/ a huge capacity & stochastic counters -- it leads to issues like #7 .

Live sample at https://fusion-samples.servicetitan.com/ is broken in wasm mode

When attempting to run the demo https://fusion-samples.servicetitan.com/ and selecting wasm.

Receive error

An error has occurred. This application may no longer respond until reloaded. [Reload](https://fusion-samples.servicetitan.com/)

console shows error

blazor.webassembly.js:1 crit: Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100]
      Unhandled exception rendering component: Arg_NullReferenceException
System.NullReferenceException: Arg_NullReferenceException
   at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.UpdateRetainedChildComponent(DiffContext& , Int32 , Int32 )
   at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.AppendDiffEntriesForFramesWithSameSequence(DiffContext& , Int32 , Int32 )
   at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.AppendDiffEntriesForRange(DiffContext& , Int32 , Int32 , Int32 , Int32 )
   at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.ComputeDiff(Renderer , RenderBatchBuilder , Int32 , ArrayRange`1 , ArrayRange`1 )
   at Microsoft.AspNetCore.Components.Rendering.ComponentState.RenderIntoBatch(RenderBatchBuilder , RenderFragment , Exception& )
   at Microsoft.AspNetCore.Components.RenderTree.Renderer.RenderInExistingBatch(RenderQueueEntry )
   at Microsoft.AspNetCore.Components.RenderTree.Renderer.ProcessRenderQueue()

Discussion: Allow for invalidation without recomputation

In real world application we do not control all the data we need to display in our applications and therefore don't know when they have updated. Let's take a 3rd party Api for example.

We could of cause invalidate the data every n minutes but that would trigger a complete recomputation of our user interface (or at least the parts that are effected).

It would be great if we had some kind of mechanism to invalidate the cached result of a ComputedMethod without triggering recomputation but instead, reevaluate the Method the next time the result is needed.

This would have to work all the way up to so the next layer would't just be cached.

Add Stl.Async-way of awaiting WaitHandle

I'd like to see some general-purpose ready-to-use implementation of Task WaitOneAsync(this WaitHandle, CancellationToken).
Maybe going as far as returning a ValueTask, though that may be challenging here.

Proposed API:

Task WaitOneAsync(this WaitHandle, CancellationToken = default)
Task WaitOneAsync(this WaitHandle, int Timeout, CancellationToken = default)

Add step-by-step tutorial

I'm thinking of the following approach:

  • Root-level "tutorial" folder contains numbered sections (p1_simple_computed, p2_[...], etc.)
  • Section = a directory with a single file (or maybe N files, if needed) + readme.md
  • Launcher = https://github.com/dotnet/try

Stl.Interception gets trimmed away

Trying to get Stl.Interception TypedFactory to not get trimmed away in .NET 8.
image

Might need some C# 12 features to get working correctly: Interceptors, such that the proxy is known at compile time and isn't stripped away completely.

Are the locks and ConfigureAwait calls needed?

This is a question rather than an issue, so apologies if this is the wrong way to post it.

I was looking at the docs and samples and see a lot of places where they add lock statements and chain ConfigureAwait(false) onto tasks. Normally this isn't how I'd expect Blazor components to be authored, so wonder if you could clarify the reason. The typical advice we give is:

  • Don't use ConfigureAwait(false) in Blazor or any other .NET UI framework, because it means your code escapes from the UI framework's synchronization context. In the case of Blazor, this means the framework's exception handling logic will no longer work properly, and you could end up with unhandled exceptions that take down your entire server instance. While it's true that ConfigureAwait(false) provides an incredibly small perf benefit, the benefit is not significant and definitely isn't worth being in an unsupported scenario where there aren't any guarantees about error handling.
    • Note: for the same reason, don't use .ContinueWith anywhere, as that also behaves like ConfigureAwait(false). Instead, make sure all tasks use await to go on to their continuation.
  • As long as you're not using ConfigureAwait(false), the framework's synchronization context takes care of ensuring that only one thread at a time is processing your component's methods (within a single circuit). As such you no longer have to worry about using lock - you have the same simplified threading model as a JavaScript application, at least for instance methods on your components.

It seems like there's an opportunity to simplify the samples, as well as fixing the error handling problems, by eliminating those things.

Of course it's also possible that I'm misunderstanding something about how Stl.Fusion works and there are important reasons why those things are required. I'd be interested if this is something you could clarify. Thanks!

Authorization

In the replicated service scenario, is this going around ASP.NET Core authorization? It appears that when a computed value is re-computed, it wouldn't be sent through the normal authorization handlers that a normal request would go through - am I interpreting that incorrectly?

Issue: Submitting an object with an ICollection to a service results in 415 error (Only with WebAssembly)

Hi all,

Today I've been working on building an application based on the Blazor sample. To make everything a bit easier for me, rather then passing individual values I instead pass around full objects from the Frontend to the Backend.

For example in the Shop.razor file:

await ShopService.UpdateShop(locals.Current);

locals.Current is of type DbShop:

    public record DbShop : LongKeyedEntity
    {
        [Required, MaxLength(120)]
        public string Name { get; set; } = "";

        public ICollection<DbShopItem> ShopItems { get; set; } = new List<ShopItem>();

        [MaxLength(9999)]
        public string XPathPrice { get; set; } = "";
        [MaxLength(9999)]
        public string XPathAvailability { get; set; } = "";
        [MaxLength(9999)]
        public string AvailabilityText { get; set; } = "";
    }

When I execute this code with server side blazor everything runs fine, however if I run this with WebAssembly I see the following error:

{type: "https://tools.ietf.org/html/rfc7231#section-6.5.13", title: "Unsupported Media Type",โ€ฆ}
status: 415
title: "Unsupported Media Type"
traceId: "00-71d8e4fd07aae244b05d21e0e0d6c6b5-2b78ddb4e971ba46-00"
type: "https://tools.ietf.org/html/rfc7231#section-6.5.13"

This is the request url:

http://localhost:5005/api/shop/UpdateShop?shop=DbShop+{+Id+%3D+1%2C+Name+%3D+Alternatef%2C+ShopItems+%3D+System.Collections.Generic.List`1%5BSamples.Blazor.Common.Models.DbShopItem%5D%2C+XPathPrice+%3D+dinggggawefwe%2C+XPathAvailability+%3D+dinggggg%2C+AvailabilityText+%3D++}

What I do find strange is the fact that everything is posted in the URL rather then the body. Is this intended?

Get rid of duplicate messages in Publisher-Replica comm. channel

Currently there are no checks for duplicate / repetitive messages & they are truly sent. On the other hand, that's the reason I introduced LTag (~ ETag in web) in IComputed, i.e. there is a good way to do this: just track the most recently sent (LTag, IsConsistent) pair & send something like "same state as you saw it last time" in case it's the same.

Commands are executed twice?

Running the HelloCart sample from https://github.com/servicetitan/Stl.Fusion.Samples it can be observed that InMemoryProductService.Edit() is executed TWICE each time the client modifies a product (after initialization)

Steps to reproduce:

  • Clone https://github.com/servicetitan/Stl.Fusion.Samples
  • Run HelloCart example
  • Select option #1
  • After Initialize() has run, but before entering a new product=price in the console, place a breakpoint in InMemoryProductService line 9
  • Enter a product=price value in the console and hit Enter
  • Observe breakpoint being hit twice.

Add custom exception types

This is a "meta issue": currently I mostly throw exceptions of existing exception types from mscorlib. This isn't good in many cases, i.e. adding a few custom types would definitely make it easier to identify certain error scenarios from user code.

On a positive side, I always throw exceptions via Error.* methods, i.e. it's actually pretty easy to identify all the scenarios that would benefit from custom-typed exceptions & update them accordingly.

The StateChanged handler in StatefulComponentBase occasionally throws ObjectDisposedException

I first ran into this issue when modifying the TodoPage.razor, changing the page's livestate to be dependent on multiple layers of Computed methods. It appears that when the blazor component state is being invalidated though the Fusion framework, the the call to the component's StateHasChanged() is sometimes not scheduled correctly causing this exception.

Exception:

Cannot process pending renders after the renderer has been disposed. Object name: 'Renderer'.
   at Microsoft.AspNetCore.Components.RenderTree.Renderer.ProcessPendingRender()
   at Microsoft.AspNetCore.Components.RenderTree.Renderer.AddToRenderQueue(Int32 componentId, RenderFragment renderFragment)
   at Microsoft.AspNetCore.Components.ComponentBase.StateHasChanged()
   at Stl.Fusion.Blazor.StatefulComponentBase.<>c__DisplayClass26_0.<.ctor>b__1() in /_/src/Stl.Fusion.Blazor/StatefulComponentBase.cs:line 38
   at Microsoft.AspNetCore.Components.Rendering.RendererSynchronizationContext.<>c.<InvokeAsync>b__8_0(Object state)

Call stack

 	Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.ComponentBase.StateHasChanged()	Unknown
>	Stl.Fusion.Blazor.dll!Stl.Fusion.Blazor.StatefulComponentBase..ctor.AnonymousMethod__1() Line 38	C#
 	Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.Rendering.RendererSynchronizationContext.InvokeAsync.AnonymousMethod__8_0(object state)	Unknown
 	Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.Rendering.RendererSynchronizationContext.ExecuteSynchronously(System.Threading.Tasks.TaskCompletionSource<object> completion, System.Threading.SendOrPostCallback d, object state)	Unknown
 	System.Private.CoreLib.dll!System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state)	Unknown
 	Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.Rendering.RendererSynchronizationContext.ExecuteBackground(Microsoft.AspNetCore.Components.Rendering.RendererSynchronizationContext.WorkItem item)	Unknown
 	System.Private.CoreLib.dll!System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(System.Threading.Thread threadPoolThread, System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state)	Unknown
 	System.Private.CoreLib.dll!System.Threading.Tasks.Task.ExecuteWithThreadLocal(ref System.Threading.Tasks.Task currentTaskSlot, System.Threading.Thread threadPoolThread)	Unknown
 	System.Private.CoreLib.dll!System.Threading.ThreadPoolWorkQueue.Dispatch()	Unknown

Ill provide a pull request with a reproduction of the issue and a suggested fix/workaround.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.