User options basics

Asp.net core Mvc options module uses several providers to fill an hierarchical options dictionary. It has the purpose of adapting request processing to the current environment: browser capabilities, logged user, explicitely specified preferences, and overall application settings.

In turn the option dictionary is used to fill option classes that are injected wherever Asp.net Core accepts injection. Dictinary entries are filled the first time they are requested, in order to avoid uneuseful processing.

Providers may be also two-ways i.e they may detect changes in the options dictionary and update back their sources.

Multiple instances of the same provider with different settings may be added to an application.

Built-in providers are:

  • RequestProvider. That extracts options from url and posted forms. Each param or each form field provides a value with the same name of the parameter/form field.
  • CookieProvider. That extracts options from a cookie. Two-ways, if some other provider updates the option the cookie is updated. Information is stored in the cookie in jSon format as a Key/Value pairs array: [{Key: "myKey", Value="myValue"}....].
  • ClaimsProvider that extracts options from user claims. Two-ways, if some other provider updates the option claims are updated.
  • EntityFrameworkProvider. It extracts options from a DBSet related with the logged user. Two-ways, if some other provider update the option database is updated.
  • ApplicationConfigurationProvider that extracts options from application configuration data.
  • RequestJsonProvider. It extracts infos from a single form field/param in the same format used by the CookieProvider.


      services.AddPreferences()
            .AddPreferencesClass<WelcomeMessage>("UI.Strings.Welcome")
            .AddPreferencesProvider(new ApplicationConfigurationProvider("UI", Configuration)
            {
                SourcePrefix = "CustomUI",
            })
            .AddPreferencesProvider(new ClaimsProvider<ApplicationUser>("UI.Strings.Welcome"){
                Priority=2,
                AutoCreate=true,
                SourcePrefix= "http://www.mvc-controls.com/Welcome"
            })
            .AddPreferencesProvider(new RequestProvider("UI.Strings.Welcome")
            {
                Priority = 10,
                SourcePrefix = "Welcome"
            }) ;
        

The first line declares one of the classes that may be injected. Its property are filled with the entries contained in the options dictionary under the path: "UI.Strings.Welcome". Match is name based and is herarchical: nested properties are inserted into child objects that are created automatically. If a property is decorated with the OptionNameAttribute the name specified there is used instead of the property name.

SourcePrefix specifies the path in the source where to start collecting data, while the path in the dictionary where to store data is passed in the provider constructor, but it may be specified also in the Prefix property that is part of the interface providers must implement. SourcePrefix is not part of providers interface since it depends on the specific source, but it is usually included in all providers for which a source path makes sense. In case several providers fill the same entry the one with the highest Priority wins. When a provider has AutoCreate=true an it doesn't win on some entry it tries to store (if possible) the winning value into its source. Among all built-in providers just the ClaimsProvider, the EntityFrameworkProvider, and the CookieProvider have the capability to store back data in their sources.

In the example above:

The first provider takes all infos contained in the application configuration under "CustomUI" and use them to fill all options dictionary entries under "UI". It is the one with the worst priority (since default priority is 0).

The second provider takes and store(AutoCreate=true) information from user claims whose claim names start with "http://www.mvc-controls.com/Welcome" and insert them in the options dictionary under "UI.Strings.Welcome". From the point of view of path nesting / are converted into dots. It has an average priority (Priority=2).

The last provider takes infos from all url parameters whose names starts with "Welcome" (ie also params like Welcome.nested are included, too) and put them under "UI.Strings.Welcome".

Providers are implementations of the following interface:


    public interface IOptionsProvider
    {
        string Prefix { get; set;}
        uint Priority { get; set; }
        bool CanSave { get;}
        bool Enabled(HttpContext ctx);
        bool AutoSave { get; set; }
        bool AutoCreate { get; set; }
        void Save(HttpContext ctx, IOptionsDictionary dict);
        List<IOptionsProvider> Load(HttpContext ctx, IOptionsDictionary dict);
        
    }
        

Where:

Prefix
Place in the hierarchical dictionary where to store provider data
Priority
Source priority. in case several providers operate on the same entry, the highest priority provider wins
Enabled
A boolean function, that depending on the request peculiarities, enables or disables the provider on the specific request. Most of providers always return true. However, providers like the ClaimsProvider that may operate only with logged users returns false with anonymous requests.
AutoSave
When set to true, the provider should attempt to save back data in its source each time its data are overwritten by an higher priority provider. Obvously, only providers with storage capabilities are supposed to care of this setting. This setting is usefull to store automatically user preferences. For instance, suppose user specifies some preference through a form submit or in a get request. Then the setting is stored in the request dictionary by the RequestProvider that usually has the highest priroity, thus causing lower priority providers like the CookieProvider, or the ClaimsProvider to store permanently the preference, if their AutoSave is set.
AutoCreate
Similar to AutoSave but stronger, since in case the source doesn't exists the provider is supposed to create it (it this makes sense for the specific provider). Usefull, with providers like the CookieProvider, and the EntityFrameworkProvider where the source (cookie or DB table) may be created. In case source creation is not possible fo a specific provider, it is expected to work like AutoSave.
CanSave
Readonly property that specifies if the provider has save capabilities.
Load
Do the actual job of storing provider data in the request dictionary dict. It, must return the list of providers whose data might have been overriden by its data. All list data are automatically computed by IOptionsDictionary dict, so each provider must just collect and return them.
Save
Do the actual job of storing back overwritten dict entries in the source. Providers with no save capability must contain simply an empty implementation. To do this job each provider requires the list of all entries starting with ist Prefix to IOptionsDictionary dict, compares these values with the corresponding values in the source, and modifies them if they differ.

Providers implement List<IOptionsProvider> Load(HttpContext ctx, IOptionsDictionary dict) and void Save(HttpContext ctx, IOptionsDictionary dict) thanks to the IOptionsDictionary methods:


public interface IOptionsDictionary
    {
        IOptionsProvider AddOption(IOptionsProvider provider, string key, string value, uint? priority=null);
        void Remove(string name);
        Func<Type, object> Creator{get; set;}
        object GetOptionObject(string prefix, Type type, object instance = null);
        List<KeyValuePair<string, string>> GetEntries(string prefix, string newPrefix=null);
        List<IOptionsProvider> AddOptionObject(IOptionsProvider currProvider, string key, object value, uint? priority = null, uint jumpComplexTypes = uint.MaxValue);
    }
        

Provider implementations need just to call AddOption to add an entry to the dictionary, and GetEntries to get all entry values that might need to be saved in their sources. All other methods are automatically called by the options engine when needed.

AddOption stores a single entry in the request dictionary, and returns the provider whose value was overriden, if any, otherwise null. Each provider implementation must collect all not null returned values, delete duplicates, and return them as result of its Load implementation. The first AddOption argument is the provider itself (ie this), while the last argument must be the provider Priority

Each provider implementation with save capabilities must call GetEntries in its Save implementation, passing it its Prefix, to get the values of all entries it should save in its source in case they have changed.


Fork me on GitHub