Skip to content

Latest commit

 

History

History
108 lines (77 loc) · 12.7 KB

README.md

File metadata and controls

108 lines (77 loc) · 12.7 KB

durable-mvc-starter

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:

  1. 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-side DurableEntityProxy<TEntity> helper and/or via this client-side DurableEntitySet.signalEntity() method.
  2. 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 this serve-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.
  3. 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 by DurableEntitySet<TState> is marked as observable, so the only thing left to be done is to write some JSX for rendering.
  4. This negotiate-signalr function, that allows the client to connect to Azure SignalR.
  5. This manage-entities function, that exposes Entity states to the client and handles signals sent from it.
  6. 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.

Prerequisites

How to run locally

  • 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.

How to deploy to Azure

You can deploy the contents of this same repo with this Deploy to Azure 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.

How to define your entities

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.

How to bind to your entities on the client

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:

How to handle authentication

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'),
});