Basic project setup and scaffolding for creating serverless web applications based on Azure Durable Entities, Azure SignalR Service, React+MobX and TypeScript.
The gist of this architectural approach is to define a strongly-typed state (a TypeScript class with no methods, like this one), then implement your server-side state transformation logic in form of a Durable Entity (like this one) and then render your state on the client with some JSX (like this one). Once the state changes on the server, its changes are incrementally propagated to the client via SignalR and automatically re-rendered thanks to MobX.
Why is it called 'Durable MVC'? Because it looks like MVC (Model-View-Controller), but instead of controllers the logic is implemented in form of Durable Entities.
The project in this repo is technically a pre-configured Azure Functions Node.js project with all code intended to be written in TypeScript. And it includes the following scaffolding:
- Server-side base classes that allow to define and implement your Durable Entities with a class-based syntax. You derive your Entity class from DurableEntity<TState> and implement your signal handlers in form of methods. The state is available to you via
this.state
property and it is automatically loaded/persisted and propagated to the client. Then you can send signals to your Entity via this server-sideDurableEntityProxy<TEntity>
helper and/or via this client-sideDurableEntitySet.signalEntity()
method. - Client-side React+MobX+TypeScript project located in this
/ui
sub-folder. It is automatically (re)built along with the main project, and its output (static HTML/JS/CSS files) is served with thisserve-statics
Function. TypeScript class definitions placed into this/ui/src/shared
folder are shared by both server and client projects, so this is where you define your states. - Client-side container for your entities - DurableEntitySet<TState>. Once you defined and implemented your entity, you can then bind to a single particular instance of it with DurableEntitySet.attachEntity<TState>() or DurableEntitySet.createEntity<TState>() static methods. But much more typical is to bind to an observable collection of entities of a certain type, and for that you just need to create an instance of
DurableEntitySet<TState>
and then bind to its items property, which is an observable array (newly-created entities are automatically added to it and destroyed entities are automatically removed from it). Everything returned byDurableEntitySet<TState>
is marked as observable, so the only thing left to be done is to write some JSX for rendering. - This
negotiate-signalr
function, that allows the client to connect to Azure SignalR. - This
manage-entities
function, that exposes Entity states to the client and handles signals sent from it. - A basic sample Entity. Here is its state, here is its class and here is its rendering.
More examples you can find in this separate repo. Also check this blog post for more details.
- Azure Functions Core Tools globally installed on your devbox.
- An instance of Azure SignalR Service configured in Serverless mode.
- Clone this repo.
- In the main project's root folder (the one that contains host.json) create a local.settings.json file, which should look like this:
{ "IsEncrypted": false, "Values": { "AzureWebJobsStorage": "<connection-string-to-your-azure-storage-account>", "AzureSignalRConnectionString": "<connection-string-to-your-azure-signalr-service-instance>", "AzureSignalRHubName": "DurableMvcTestHub", "FUNCTIONS_WORKER_RUNTIME": "node" } }
- In that same root folder run:
npm install npm run build func start
- Navigate to
http://localhost:7071
with your browser.
In a matter of seconds a new instance of CounterEntity will be created and rendered in your browser. Try to open that page in multiple browser tabs and observe the state being automatically synchronized across them. Also try to kill/restart the Functions host process (func.exe) and observe the state being preserved.
Once created, you can also monitor your Durable Entities with Durable Functions Monitor.
You can deploy the contents of this same repo with this button. It will create a Function App instance, an underlying Storage account and an Azure SignalR service instance. Don't forget to remove those resources once done.
Once you cloned this repo and added some code to your copy, you then deploy it in the same way as you would normally deploy an Azure Functions Node.js project.
Anywhere in your codebase (except the ui
folder) create a class derived from DurableEntity<TState>. Methods of that class, that you intend to make your signal handlers, are expected to take zero or one parameter. The state is available to your code via this.state
property, and it will be loaded/saved automatically.
The default visibility level for an entity is VisibilityEnum.ToOwnerOnly
(which means that only the creator will be able to access it from the client and change notifications will only be sent to the creator), to change it override the DurableEntity.visibility property.
To do a custom state initialization for a newly created entity instance override the DurableEntity.initializeState() method.
The required boilerplate (index.ts
and function.json
files) for exposing your class as a Durable Entity will then be autogenerated for you. Once autogenerated, you will be able to modify those files, e.g. add more bindings, if your entity requires them.
DurableEntitySet provides static methods, that return a single observable state object:
- createEntity() - creates an entity with given key, if not created yet.
- attachEntity() - doesn't create anything, just tries to attach to an existing entity.
To get an observable collection of entities of a certain type create an instance of DurableEntitySet<TState> class and then bind to its items property. Newly added entities will automatically appear there and removed (destroyed) entities will automatically be dropped.
To send signals to your entities use:
- DurableEntitySet.signalEntity() - sends a signal in a fire-and-forget manner.
- DurableEntitySet.callEntity() - 'calls' an entity aka returns a Promise, that will be resolved once the sent signal actually gets processed.
To determine which entity instances should be visible to which particular user, backend code needs to be able to identify that user somehow. By default it relies on Easy Auth module while doing that, which means that Easy Auth needs to be properly configured for your Azure Functions app instance (and this instance needs to run in Azure).
When you configure it for so called server-directed flow (aka cookie-based, aka no client-side SDK involved), then that's basically it - the backend will identify the calling user automatically, using the authentication cookie that comes with each request.
In many cases though you might want to implement client-side authentication with some client-side SDK, e.g. MSAL. In that case the backend will expect an access token to be passed with every request, and you will need to provide that access token by calling the DurableEntitySet.setup()
method like that:
DurableEntitySet.setup({
// Implement this method to provide DurableEntitySet with an access token
accessTokenFactory: () => {
const myAccessToken = "oauth-access-token-obtained-from-somewhere";
return Promise.resolve(myAccessToken);
}
});
Note that accessTokenFactory
should be a method returning a promise. It will be called every time an access token is needed.
For demo and test purposes (e.g. when running everything on your local devbox) you might just want to provide some test user name. Then call the DurableEntitySet.setup()
method like this:
DurableEntitySet.setup({
fakeUserNamePromise: Promise.resolve('test-anonymous-user'),
});