Context Class

Assembly: CodeEffects.Rule.Repository.dll
Namespace: CodeEffects.Rule.Repository

Summary

Provides the static methods that your application calls to run the Rule Repository: answer the requests of the repository browser client, check source definitions into rule storage, rebuild the member usage index, check the version of the database, and load the live rules for the Rules Engine.

Syntax

public class Context

Remarks

Context is the whole server-side entry point of the Rule Repository. Every member is static, and you never create an instance. The three methods that most hosts use are SyncSources(..), which puts the source objects in rule storage, GetResponse(..), which serves the browser client, and GetRules(..), which loads the live rules for evaluation. The other methods help you start up safely, test your permissions, and translate the user interface. For storing and versioning rules, see Rule Storage in Rule Repository.

The usual host wiring

A host connects the repository to its application with a database provider, one endpoint, and a configuration that it builds for every request. The database provider (IDbProvider) connects to the rule storage database. The repository supports SQL Server first, and other database systems follow. The steps are:

  1. Check and prepare the database when the application starts. Call CheckDatabase(..) once, and stop or log when IsCurrent is false. Call SyncSources(..) to put your source objects in rule storage. It is safe to call it on every start. When its result lists aliases in AliasesToReindex, pass them to Reindex(..).
  2. Expose one endpoint. The repository browser client sends all of its requests, as JSON, to one URL of your application. The body of that endpoint is a single call: read the request body as a string, call GetResponse(config, dbProvider, requestJson), and return the string it gives you with the content type application/json. Nothing in between needs to read or change the JSON.
  3. Build the configuration from the signed-in user. The configuration says who the current user is (CurrentUserId, required) and what that user may do (AccessMode and ActionList, or your own CanPerform). Build it on the server, on every request, from the identity that your application authenticated and from the role of that user. See IRepositoryConfiguration and Configuration. The repository has no sign-in, no roles, and no user directory. Granting permissions is your application's job. Nothing is allowed until you allow it: a configuration that does not set AccessMode lets the user look at the grids and do nothing.
  4. Catch the exceptions. Catch RepositoryPermissionException and RepositoryException around the call, log them, and return their ResponseJson property, so the user sees a general message with a control number and your log holds the full failure.
app.MapPost("/api/rules", async (HttpRequest request) =>
{
	string requestJson;

	using (var reader = new StreamReader(request.Body))
		requestJson = await reader.ReadToEndAsync();

	var config = new Configuration
	{
		CurrentUserId = request.HttpContext.User.Identity.Name,
		AccessMode = ActionPermissionMode.AllowAllExcept,
		ActionList = new HashSet<ActionPermissionType>
		{
			ActionPermissionType.SourceDelete,
			ActionPermissionType.RuleDelete
		}
	};

	try
	{
		return Results.Content(Context.GetResponse(config, dbProvider, requestJson), "application/json");
	}
	catch (RepositoryPermissionException ex)
	{
		logger.LogWarning("Refused {Action}: {Message}", ex.Action, ex.Message);
		return Results.Content(ex.ResponseJson, "application/json");
	}
	catch (RepositoryException ex)
	{
		logger.LogError(ex, "Request failed: {ControlNumber}", ex.ControlNumber);
		return Results.Content(ex.ResponseJson, "application/json");
	}
}).RequireAuthorization();

The repository protects its own requests and output. Every request after the first one carries an anti-forgery token that the repository issued to that user, and GetResponse(..) refuses a request without a valid one. The rest is the part of your application:

  • Require your application's own sign-in on the endpoint. Without it, anyone who can reach the URL can read and change rules.
  • Fill CurrentUserId on the server from the authenticated identity, and never from the request. The anti-forgery token is bound to that ID.
  • Do not allow cross-origin requests with credentials to the endpoint, so that no policy lets a page on another site read the first answer and take the token from it.
  • Accept application/json only and put a size limit on the request body. The repository refuses a request that is longer than the RequestLength of its RequestLimits (3 MB by default).
  • Let only your application write to the rule storage database. The key that signs the tokens is stored in it.

Methods

  • CheckDatabase(..)

    Returns: CodeEffects.Rule.Connection.SchemaCheckResult - The version the database is at, the version this library needs, and whether that is enough.

    public static SchemaCheckResult CheckDatabase(IDbProvider dbProvider)

    Reads the schema version of the rule storage database and compares it with the version that this version of the Rule Repository library needs. Call it once at startup, before the repository serves anything, and act on the answer: stop, or log it and go on.

    Parameters

    • dbProvider (CodeEffects.Rule.Connection.IDbProvider) - The database provider of the Rule Repository.

    Exceptions

    • CodeEffects.Rule.Common.RuleException - The provider is null.
    • The error of the database provider when the database cannot be reached. A database that cannot be reached is not a schema to report on.

    Remarks

    The library never calls this method by itself and keeps nothing: each call reads the database once. The update scripts come from the Code Effects portal and you run them, in order. The library never changes the schema. A database that records no schema version answers with a CurrentVersion of null, and IsCurrent is false. A database at a later version than the library needs is current.

    When you do not call the method, you are told anyway, later. A statement that fails on a table or column that is not there makes the library read the version, and when the database is behind, the failure carries the control number CE19004 and the same message, in place of the error of the database.

    Example

      SchemaCheckResult schema = Context.CheckDatabase(dbProvider);
    
      if (!schema.IsCurrent)
      	throw new InvalidOperationException(schema.Message);
    
  • GetAliasNames(..)

    Returns: System.Collections.Generic.IList<System.String> - The alias of every source object stored in the repository.

    public static IList<string> GetAliasNames(IDbProvider dbProvider)

    Returns the alias name of every source object that is already stored in the repository. Use it to decide which of your source definitions to pass to SyncSources(..), or which source objects to pass to Reindex(..). Passing all of your sources to SyncSources(..) every time is also fine: an unchanged source is read and compared, and nothing is written.

    Parameters

    • dbProvider (CodeEffects.Rule.Connection.IDbProvider) - The database provider of the Rule Repository.

    Exceptions

    Example

      IList<string> aliases = Context.GetAliasNames(dbProvider);
      ReindexResult result = Context.Reindex(dbProvider, aliases.Where(a => a.StartsWith("MyApp.")));
    
  • GetDefaultEditorHelpXml()

    Returns: System.Xml.XmlDocument - The rule editors' default English Help XML document.

    public static XmlDocument GetDefaultEditorHelpXml()

    Returns the default Help XML of the rule editors inside the repository: their menus, operators, clauses, flow words and Help String messages, in English. It is the same document the stand-alone Rule Editor's Control.GetDefaultHelpXml() returns. Save it, translate the text between the tags, and set the result on the EditorHelpXml or EditorHelpXmlFile property of your configuration to show the editors in that language. See Help XML and Multilingual Support.

    Remarks

    Every call returns a new document, so you may change it freely.

    Example

      Context.GetDefaultEditorHelpXml().Save(@"C:\Translations\editor.help.xml");
    
      // After translating it
      var config = new Configuration
      {
      	UiXmlFile = @"C:\Translations\fr\repository.ui.xml",
      	EditorHelpXmlFile = @"C:\Translations\fr\editor.help.xml"
      };
    
  • GetDefaultUiXml()

    Returns: System.Xml.XmlDocument - The default English user interface document.

    public static XmlDocument GetDefaultUiXml()

    Returns the default user interface XML document: every piece of static text that the repository browser client shows, in English. Save it to a file, translate the text between the tags without changing anything else, and set the result on the UiXml or UiXmlFile property of your configuration to run the repository in that language.

    Remarks

    Every call returns a new document, so you may change it freely. The repository checks your document against the default one when it reads it, and refuses a document that lacks a node of the default or declares a node the default does not.

    Example

      XmlDocument ui = Context.GetDefaultUiXml();
      ui.Save(@"C:\Translations\repository.ui.xml");
    
      // After translating the file, load it and give it to the configuration of each request.
      var translated = new XmlDocument();
      translated.Load(@"C:\Translations\repository.ui.fr.xml");
    
      var config = new Configuration { UiXml = translated };
    
  • GetRequestToken(..)

    Returns: System.String - The anti-forgery token of the current user.

    public static string GetRequestToken(IRepositoryConfiguration config, IDbProvider dbProvider)

    Returns an anti-forgery token for the current user, good for 24 hours. It is for a host that builds repository requests on the server instead of in the browser client of the repository: its own tools, its tests. The browser client never needs it, because it receives its token in the answer to its first request.

    Parameters

    Exceptions

    Remarks

    The token is signed with a key of the repository. The key is kept in the rule storage database and is created the first time it is needed. The token is good only for the CurrentUserId of the configuration. Never hand a token to a browser in any way other than the first answer of the repository.

    Example

      string token = Context.GetRequestToken(config, dbProvider);
    
  • GetResponse(..)

    Returns: System.String - The response JSON: the data that was asked for, or a warning that the user should see.

    public static string GetResponse(IRepositoryConfiguration config, IDbProvider dbProvider, string requestJson)
    public static Response GetResponse(IRepositoryConfiguration config, IDbProvider dbProvider, Request request)

    Answers one request of the repository browser client. The first overload is the whole body of your repository endpoint: pass it the JSON that the client posted, exactly as it arrived, and return what it gives you. The second overload is for a host that has already deserialized the request into the request model of the repository. It returns the response model and throws the same exceptions. Those two models are an internal contract between the client and the repository, so use the string overload.

    Parameters

    • config (CodeEffects.Rule.Repository.IRepositoryConfiguration) - The configuration of the current user: who the user is and what the user may do. Build it for every request.
    • dbProvider (CodeEffects.Rule.Connection.IDbProvider) - The database provider of the Rule Repository.
    • requestJson (System.String) - The request, exactly as the browser client sent it.

    Exceptions

    • CodeEffects.Rule.Repository.RepositoryPermissionException - The permissions of the current user do not allow the request. Log it, because denials are worth auditing, and return its ResponseJson, a warning that carries the current permissions of the user.
    • CodeEffects.Rule.Repository.RepositoryException - The request failed: it is empty, longer than the limit, or not valid JSON; the configuration or the provider is null; the configuration names no current user; the database failed; or the set of permissions is not complete. Log it with your own error handling, and return its ResponseJson so that the user sees a general message with the control number. Alternatively, let it propagate, and the client reports a failed call.

    Remarks

    The method handles a request in this order: it refuses a request that is longer than the RequestLength of the limits of the configuration, reads the JSON, checks the anti-forgery token (every request except the first request of the client and its test request must carry one), checks the request against the permissions of the user before any other work, and then does the work. A request that fails on a deadlock or a dropped connection is run once more.

    Warnings are answers, not failures. A record that someone else has changed, a validation that failed, and an expired page are returned in the response and are never thrown. The one refusal that is thrown is a missing permission.

    A set of permissions that is not complete fails every request, the first one included, so that you see it on the first page load. Test your roles with ValidatePermissions(..).

    Example

      try
      {
      	return Context.GetResponse(config, dbProvider, requestJson);
      }
      catch (RepositoryPermissionException ex)
      {
      	logger.LogWarning("Refused {Action}: {Message}", ex.Action, ex.Message);
      	return ex.ResponseJson;
      }
      catch (RepositoryException ex)
      {
      	logger.LogError(ex, "Request failed: {ControlNumber}", ex.ControlNumber);
      	return ex.ResponseJson;
      }
    
  • GetRules(..)

    Returns: CodeEffects.Rule.Repository.LoadedRuleset - The ruleset of the live rules the query names.

    public static LoadedRuleset GetRules(IDbProvider dbProvider, RuleQuery query)

    Loads the live versions of the rules that a RuleQuery names, and the live versions of every rule that they call, in one ruleset for the Rules Engine. It is the method for an application that evaluates its rules without the Audit library. An application that has the Audit library loads its rules through AuditRecorder.GetRules(..) instead, so that its evaluations are recorded.

    Parameters

    • dbProvider (CodeEffects.Rule.Connection.IDbProvider) - The database provider of the Rule Repository.
    • query (CodeEffects.Rule.Common.Models.RuleQuery) - Which rules to load.

    Exceptions

    • CodeEffects.Rule.Common.RuleException - The provider or the query is null; no rule with a live version matches the query; a rule that was asked for by ID or by name does not exist or has no live version; a rule calls a rule that has no live version; or a live version is not a rule's XML. The message says which.
    • CodeEffects.Rule.Connection.ConnectionException - The database is behind the schema that this version of the library needs. The message says which update scripts to run.

    Remarks

    Only live versions are loaded: a rule version is evaluated once someone makes it live. The ruleset holds the called rules as well, so the engine finds them without a rule resolver. The method reads the database every time it is called. Keep the evaluator that you build from the ruleset for as long as the ruleset is current, and call the method again to pick up newer versions. See LoadedRuleset for how to use the result.

    Example

      LoadedRuleset ruleset = Context.GetRules(dbProvider, RuleQuery.BySource("MyApp.Orders.Order"));
    
      var evaluator = new Evaluator<Order>(ruleset.Xml, new EvaluationParameters());
    
      bool approved = evaluator.Evaluate(order, ruleset.Rule("Approve large orders").Id);
    
  • Reindex(..)

    Returns: CodeEffects.Rule.Repository.ReindexResult - What was done, with one entry for each source object.

    public static ReindexResult Reindex(IDbProvider dbProvider, IEnumerable<string> aliases = null)

    Rebuilds the member usage index of source objects that are already in the repository: the catalog of members that every version of each source object declares, and the members and reusable rules that every version of every rule bound to it uses.

    Parameters

    • dbProvider (CodeEffects.Rule.Connection.IDbProvider) - The database provider of the Rule Repository.
    • aliases (System.Collections.Generic.IEnumerable<System.String>) - The aliases of the source objects to reindex, as GetAliasNames(..) returns them. null, the default, reindexes every source object in the repository. A list that names none reindexes nothing. Blank entries and duplicates are ignored.

    Exceptions

    Remarks

    SyncSources(..) and the saving of rules keep the index current as they write. This method is for what was stored before: a repository that was upgraded, rules that were migrated in from outside the repository, and a release that changes how the index is computed. You decide when to run it, and over which source objects. Nothing in the repository runs it by itself. The AliasesToReindex property of the result of SyncSources(..) lists every source object that needs it.

    It can run while the repository is in use. A rule save and the reindex of its source object take the same lock first, so a save waits while its source object is reindexed, and the reindex waits while a save finishes. A source sync takes that lock later in its transaction, so a sync and a reindex of the same source object can deadlock. Both retry once. A large repository is best reindexed at a quiet time.

    Each source object is reindexed in its own transaction. One that fails is rolled back and the others go on. A use that a rule version makes and that resolves to nothing is not stored and does not fail anything. It is listed in the Unresolved property of that source object's entry in the result. A source object that is reindexed successfully is recorded as current, and drops out of AliasesToReindex until a release changes how the index is computed.

    Anything that writes rule versions around the repository's own saves, a migration that inserts rows directly for example, leaves their references unwritten and should be followed by a reindex of the source objects it touched.

    Example

      SyncResult sync = Context.SyncSources(dbProvider, settings);
    
      if (sync.AliasesToReindex.Count > 0)
      	Context.Reindex(dbProvider, sync.AliasesToReindex);
    
  • SyncSources(..)

    Returns: CodeEffects.Rule.Repository.SyncResult - What was done, with one entry for each source definition you passed.

    public static SyncResult SyncSources(IDbProvider dbProvider, SourceSettings settings)

    Checks a set of source definitions into rule storage. It stores the ones it has not seen, and creates a new version of the ones that have changed. A source definition can be a Source XML document, a .NET type, or an Adaptive Source provider.

    Parameters

    • dbProvider (CodeEffects.Rule.Connection.IDbProvider) - The database provider of the Rule Repository.
    • settings (CodeEffects.Rule.Repository.SourceSettings) - The source definitions to check, and how new versions are handled. Each definition is checked on its own.

    Exceptions

    • CodeEffects.Rule.Common.RuleException - The provider or the settings are null.
    • CodeEffects.Rule.Repository.RepositoryException - One or more source definitions failed. Every definition is still processed, and one that fails does not stop the others. The SyncResult property of the exception holds the complete result, so you can log which ones failed and why. Their users never see the failure: a bad definition is for its programmers to fix.

    Remarks

    This is a feature of its own, not a step that the rest of the repository waits on. Nothing else in the repository calls it, and the repository runs without it, but there would be no source objects to write rules against. Call it whenever it suits your application: at startup, from an administration screen, on a schedule, or from a deployment step. Call it at least once before anyone writes a rule, and after that whenever the evaluated types may have changed. It takes no configuration, because it is your own call, made on the server, with no user session behind it. To credit a person or a service with the work, set UserId on the settings.

    Calling it repeatedly is safe and cheap. A definition that matches what is stored writes nothing. One that differs only in its labels updates the stored copy in place. Only a real change creates a version. What is not safe is passing several definitions of the same source object in one call: see SourceSettings.

    The method keeps the member usage index of what it writes current, but it does not rebuild the index of what was stored before. That is Reindex(..), and you decide when to run it. The AliasesToReindex property of the result lists every source object that needs it. The result also lists the source objects whose embedded types have drifted from their own source objects in SourcesOutOfSync.

    Example

      var settings = new SourceSettings();
      settings.SourceTypes.Add(typeof(Order));
    
      try
      {
      	SyncResult sync = Context.SyncSources(dbProvider, settings);
      }
      catch (RepositoryException ex)
      {
      	foreach (var source in ex.SyncResult?.Sources ?? new List<SourceResult>())
      		if (source.Outcome == SourceOutcome.Failed)
      			logger.LogError(source.Error, "{Alias} {Number} {Message}", source.AliasName, source.ControlNumber, source.Message);
    
      	throw;
      }
    
  • ValidatePermissions(..)

    Returns: System.Collections.Generic.IList<System.String> - One English sentence for each problem, or an empty list when the set is complete.

    public static IList<string> ValidatePermissions(IRepositoryConfiguration config)

    Says what is wrong with the set of permissions that a configuration gives, one sentence for each problem. For the tests of your own role mappings. A set that is not complete fails every request with a RepositoryException that carries the control number CE10218, and this method reports the same problems without any request.

    Parameters

    Exceptions

    Remarks

    A set is complete when every permission that it grants has the permissions it requires granted too. DraftUpdate needs DraftView, and DraftConvert needs DraftView and either RuleCreate or RuleVersionCreate. A Configuration with AccessMode set to AllowAllExcept is always complete, because it withholds whatever depends on what it withholds. The method reads nothing from the database, and it asks CanPerform of your own implementation once for each permission.

    Example

      var support = new Configuration
      {
      	AccessMode = ActionPermissionMode.AllowOnly,
      	ActionList = new HashSet<ActionPermissionType> { ActionPermissionType.DraftUpdate }
      };
    
      // Prints: DraftUpdate (52) requires DraftView (50).
      foreach (string problem in Context.ValidatePermissions(support))
      	Console.WriteLine(problem);
    

Ask on Stack Overflow - Yes, it's still active
Ask in GitHub Discussions

p102

l097 --

l102

p101

×