Table of Contents

Application context

What is the application context ?

The application context is a set of keys/values.

Its scope is the application which means that there is only one instance accessible by all UI views.

It is accessible through the static class ApplicationContext.

Add a value

To add a value, you have to use the Add method. If the key is already present, an exception is thrown.

Example :

ApplicationContext.Add("AgencyId", "AG1");

Set a value

To set a value, you have to use the Set method. If the key is already present, the value is replaced.

Example :

ApplicationContext.Set("AgencyId", "AG1");

Read a value

There are several ways to read a value:

string agencyId = ApplicationContext.GetValue<string>("AgencyId");
if (ApplicationContext.TryGetValue("AgencyId", out object agencyId))
{
    ...
}

Reactivity

Use in UI expressions

It is possible to use the application context in UI expressions.

The UI is reactive to application context changes which means that when a value changes, the UI will update.

Example of a visibility expression for a UI view action where it is visible when the value of the "AgencyId" key is "AG1" :

return ApplicationContext.GetValue<string>("AgencyId") == "AG1";

Handle changes out of an UI expression

When you want handle changes of a value of the application context out of an UI expression, you can use the AddListenerOnValueChanged method.

Example :

ApplicationContext.AddListenerOnValueChanged("AgencyId", (value) =>
{
    // Do something
});

You should not forget to remove the listener when it is no longer needed.

For example, in the Initialized event rule of a UI view :

ApplicationContext.AddListenerOnValueChanged("AgencyId", Methods.OnAgencyIdChanged);

To remove the listener, you have to use the RemoveListenerOnValueChanged method in the Closing event rule of the UI view :

ApplicationContext.RemoveListenerOnValueChanged("AgencyId", Methods.OnAgencyIdChanged);

UI view templates

The syntax to use the application context in UI view templates is : @ApplicationContext.[KeyName]

Example : Accessing the ClusterTitle value of the application context.

<text>
    @ApplicationContext.ClusterTitle
</text>

Instantiate application context with values from the server

It is possible to instantiate the application context with values from the server.

To do so, you have to implement a tenant resolved interceptor. To learn about this feature, you can read the following page: Tenant resolved interceptor.

Share values from client to server

It is possible to share values from the client to the server. To do this on the client side, use the AddApiContextValue method or the SetApiContextValue method :

ApplicationContext.AddApiContextValue("Culture", "fr-FR");

The difference between the AddApiContextValue method and the SetApiContextValue method is that, if the key is already present, the AddApiContextValue method throws an exception whereas the SetApiContextValue method replaces the value.

On the server side, you have to inject the IApplicationContext interface to get the value. For example, on a server method :

/// <summary>
/// Represents the implementation of the <see cref="IMyServerMethod"/> interface.
/// </summary>
public class MyServerMethod : IMyServerMethod
{
    private readonly IApplicationContext _applicationContext;

    /// <summary>
    /// Initializes a new instance of the <see cref="MyServerMethod"/> interface.
    /// </summary>
    /// <param name="applicationContext">Application context.</param>
    public MyServerMethod(IApplicationContext applicationContext)
    {
        _applicationContext = applicationContext;
    }

    /// <inheritdoc/>
    public void Execute()
    {
        string culture = _applicationContext.GetStringValue("Culture");

        // ...
    }
}

The values added to the client application context by the Add method are not sent to the server, you must use the AddApiContextValue method. On the other hand, when using the AddApiContextValue method, the value is not stored in the client application context.

By default, application context values, instantiated by the tenant resolved interceptors, are sent to the server.

Internally, the values are sent in the headers of all the HTTP requests (the header keys are prefixed by "neos-"). That is why the types of values sent are limited : You can only have strings, integers, decimals, booleans and GUID.

Note

From version 1.13.0, the interface IApplicationContext is available in domain layer.

Warning

From version 1.13.0, the interface IApplicationContext is available in a new namespace GroupeIsa.Neos.Shared.Metadata, the use of the interface GroupeIsa.Neos.Application.Metadata.IApplicationContext is obsolete and generates a warning.

Use in API calls

There are three ways to pass the value of a context key when consuming a cluster Rest API. These ways are presented in order of priority.

1 - HTTP header

The value can be passed in a header of the HTTP request. This header must be the name of the key prefixed with neos-.

If the HTTP call is initiated from a web browser, the value can be passed into a cookie, the name of the cookie is the name of the key prefixed with neos-.

3 - Query string

It is also possible to pass the value in a query string of the URL, the name of the query string is the name of the key prefixed with neos-.

Checking the allowed values

Implementation of IUserContexPermissions

If the value passed to ApplicationContext during an API Rest consumption needs to be checked, as for example it is used to filter data, the IUserContextPermissions interface should be implemented.

Example of implementation :

The Company entity has a 1-n relationship to the UserCompany entity. At each company we can set a list of authorized users for the company. We want to check that the value of the CompanyId key is part of the one configured for the authenticated user.

In a business assembly of the cluster create a class implementing the IUserContextPermissions interface:

public class UserContextPermissions: IUserContextPermissions

The UserContextKeys property must then be implemented.
This property must provide the keys to verify. The values of all other keys in the context are not checked.

/// <inheritdoc/>
public IEnumerable<string> UserContextKeys
{
    get
    {
        yield return "CompanyId";
    }
}

Then implement the GetUserContextAuthorizedValues

/// <inheritdoc/>
public IEnumerable<object?> GetUserContextAuthorizedValues(string? tenantId, string userId, string key)
{
    return key switch
    {
        "CompanyId" => GetAuthorizedCompanies(userId).Cast<object>(),
        _ => Array.Empty<object>(),
    };
}

private IEnumerable<int?> GetAuthorizedCompanies(string userId)
{
    // to allow not to pass the value of UserContextKeys
    yield return null;
    foreach(int value in _companyRepository.GetQuery()
        .Where(c => c.UserCompanyList.Any(u => u.UserId == userId))
        .Select(c => c.Id))
    {
        yield return value;
    }
}

For the companyId key we will supply the companies for the authenticated user. If we wish to allow the CompanyId key to not be supplied we must also return null.

The full code for the class UserContextPermissions:

 public class UserContextPermissions : IUserContextPermissions
{
    private readonly ICompanyRepository _companyRepository;

    public UserContextPermissions(ICompanyRepository companyRepository)
    {
        _companyRepository = companyRepository;
    }

    /// <inheritdoc/>
    public IEnumerable<string> UserContextKeys
    {
        get
        {
            yield return "CompanyId";
        }
    }

    /// <inheritdoc/>
    public IEnumerable<object?> GetUserContextAuthorizedValues(string? tenantId, string userId, string key)
    {
        return key switch
        {
            "CompanyId" => GetAuthorizedCompanies(userId).Cast<object>(),
            _ => Array.Empty<object>(),
        };
    }

    private IEnumerable<int?> GetAuthorizedCompanies(string userId)
    {
        // to allow not to pass the value of UserContextKeys
        yield return null;
        foreach(int value in _companyRepository.GetQuery()
            .Where(c => c.UserCompanyList.Any(u => u.UserId == userId))
            .Select(c => c.Id))
        {
            yield return value;
        }
    }
}

This class must be configured in the service container with a scoped life time in the Startup class of the business assembly:

public static class Startup
{
    public static void ConfigureServices(IServiceCollection services)
    {
        services.AddScoped<IUserContextPermissions, UserContextPermissions>();
    }
}

Permissions cache control

Neos maintains a per-tenant and per-user cache of permissions. The GetUserContextAuthorizedValues method is called only once per tenant and user, the response is cached.
It is possible to manage this cache.

The GroupeIsa.Neos.Shared.Contexts.IUserContextAuthorizationCacheManagement interface is used to control the cache.

For example, we want to manage the cache for CompanyId when the company configuration is modified.

To do this we create a saving rule on the UserCompany entity in the domain layer:

 public class Saving : ISavingRule<UserCompany>
{
    private readonly IBusinessRuleContext _businessRuleContext;
    private readonly IUserCompanyRepository _userCompanyRepository;

    public Saving(IBusinessRuleContext businessRuleContext, IUserCompanyRepository userCompanyRepository)
    {
        _businessRuleContext = businessRuleContext;
        _userCompanyRepository = userCompanyRepository;
    }

    /// <inheritdoc/>
    public async Task OnSavingAsync(ISavingRuleArguments<UserCompany> args)
    {
        var removedUsers = args.DeletedItems.Select(u => (u.CompanyId, u.UserId)).ToList();
        foreach (UserCompany item in args.ModifiedItems)
        {
            var originalItem = _userCompanyRepository.GetOriginal(item);
            if (originalItem.UserId != item.UserId)
            {
                removedUsers.Add((originalItem.CompanyId, originalItem.UserId));
            }
        }

        _businessRuleContext.TryAdd("RemovedCompanyUsers",removedUsers);

        _businessRuleContext.TryAdd("AddedCompanyUsers", args.CreatedItems.Select(u => (u.CompanyId, u.UserId)).ToList());

        await Task.CompletedTask;
    }
}

This rule allows us to set up two collections in the context, one containing added users, the other containing deleted users (by deletion or code modification).

Then we create a saved rule on the UserCompany entity in the domain layer:

 public class Saved : ISavedRule<UserCompany>
{
    private readonly IBusinessRuleContext _businessRuleContext;
    private readonly IUserContextAuthorizationCacheManagement _userContextAuthorizationCacheManagement;


    public Saved(IUserContextAuthorizationCacheManagement userContextAuthorizationCacheManagement, IBusinessRuleContext businessRuleContext)
    {
        _userContextAuthorizationCacheManagement = userContextAuthorizationCacheManagement;
        _businessRuleContext = businessRuleContext;
    }

    /// <inheritdoc/>
    public async Task OnSavedAsync(ISavedRuleArguments<UserCompany> args)
    {
        if(_businessRuleContext.TryGet("RemovedCompanyUsers", out List<(int companyId, string userId)>? removedList))
        {
            foreach (var (companyId, userId) in removedList)
            {
                _userContextAuthorizationCacheManagement.RemoveAuthorizedValue(userId, "CompanyId",companyId) ;
            }
        }

        if (_businessRuleContext.TryGet("AddedCompanyUsers", out List<(int companyId, string userId)>? addedList))
        {
            foreach (var (_, userId) in addedList)
            {
                _userContextAuthorizationCacheManagement.ResetAuthorizedValues(userId, "CompanyId");
            }
        }

        await Task.CompletedTask;
    }
}

For each user removed from a company, the cache is deleted for the authenticated user, the current tenant and the impacted company.
For each user added, the cache is deleted for the authenticated user, the current tenant and all companies.