28.11 The Ice::Context Parameter
Methods on a proxy are overloaded with a trailing parameter of type const Ice::Context & (C++),
java.util.Map (Java), or
Dictionary<string, string> (C#). The Slice definition of this parameter is as follows:
module Ice {
local dictionary<string, string> Context;
};
As you can see, a context is a dictionary that maps strings to strings or, conceptually, a context is a collection of name–value pairs. The contents of this dictionary (if any) are implicitly marshaled with every request to the server, that is, if the client populates a context with a number of name–value pairs and uses that context for an invocation, the name–value pairs that are sent by the client are available to the server.
On the server side, the operation implementation can access the received Context via the
ctx member of the
Ice::Current parameter (see
Section 28.6) and extract the name–value pairs that were sent by the client.
Contexts provide a means of sending an unlimited number of parameters from client to server without having to mention these parameters in the signature of an operation. For example, consider the following definition:
struct Address {
// ...
};
interface Person {
string setAddress(Address a);
// ...
};
Assuming that the client has a proxy to a Person object, it could do something along the following lines:
PersonPrx p = ...;
Address a = ...;
Ice::Context ctx;
ctx["write policy"] = "immediate";
p‑>setAddress(a, ctx);
PersonPrx p = ...;
Address a = ...;
java.util.Map ctx = new java.util.HashMap();
ctx.put("write policy", "immediate");
p.setAddress(a, ctx);
using System.Collections.Generic;
PersonPrx p = ...;
Address a = ...;
Dictionary<string, string> ctx = new Dictionary<string, string>();
ctx["write policy"] = "immediate";
p.setAddress(a, ctx);
On the server side, we can extract the policy value set by the client to influence how the implementation of
setAddress works. A C++ implementation might look like:
void
PersonI::setAddress(const Address& a, const Ice::Current& c)
{
Ice::Context::const_iterator i = c.ctx.find("write policy");
if (i != c.ctx.end() && i‑>second == "immediate") {
// Update the address details and write through to the
// data base immediately...
} else {
// Write policy was not set (or had a bad value), use
// some other database write strategy.
}
}
For this example, the server examines the value of the context with the key "write policy" and, if that value is
"immediate", writes the update sent by the client straight away; if the write policy is not set or contains a value that is not recognized, the server presumably applies a more lenient write policy (such as caching the update in memory and writing it later). The Java version of the operation implementation is essentially identical, so we do not show it here.
Instead of passing a context explicitly with an invocation, you can also use a per-proxy context. Per-proxy contexts allow you to set a context on a particular proxy once and, thereafter, whenever you use that proxy to invoke an operation, the previously-set context is sent with each invocation. The proxy base class provides a member function,
ice_context, to do this:
namespace IceProxy {
namespace Ice {
class Object : /* ... */ {
public:
Ice::ObjectPrx
ice_context(const Ice::Context&) const;
// ...
};
}
}
package Ice;
public interface ObjectPrx {
ObjectPrx ice_context(java.util.Map newContext);
// ...
}
namespace Ice
{
public interface ObjectPrx
{
ObjectPrx ice_context(Dictionary<string, string>
newContext);
// ...
}
}
ice_context creates a new proxy that stores the passed context. Note that the return type of
ice_context is
ObjectPrx. Therefore, before you can use the newly-created proxy, you must down-cast it to the correct type. For example, in C++:
Ice::Context ctx;
ctx["write policy"] = "immediate";
PersonPrx p1 = ...;
PersonPrx p2 = PersonPrx::uncheckedCast(p1‑>ice_context(ctx));
Address a = ...;
p1‑>setAddress(a); // Sends no context
p2‑>setAddress(a); // Sends ctx implicitly
Ice::Context ctx2;
ctx2["write policy"] = "delayed";
p2‑>setAddress(a, ctx2); // Sends ctx2
As the example illustrates, once we have created the p2 proxy, any invocation via
p2 implicitly sends the previously-set context. The final line of the example illustrates that it is possible to explicitly send a context for an invocation even if the proxy has an implicit context—an explicit context always overrides any implicit context.
Note that, once you have set a per-proxy context, that context becomes immutable: if you change the context you have passed to
ice_context later, that does not affect the per-proxy context of any proxies you previously created with that context because each proxy on which you set a per-proxy context makes a copy of the dictionary and stores that copy.
You can retrieve the per-proxy context by calling ice_getContext on the proxy. If a proxy has no implicit context, the returned dictionary is empty. For C++, the signature is:
namespace IceProxy {
namespace Ice {
class Object : /* ... */ {
public:
Ice::Context ice_getContext() const;
// ...
};
}
}
package Ice;
public interface ObjectPrx {
java.util.Map ice_getContext();
// ...
}
namespace Ice
{
public interface ObjectPrx
{
Dictionary<string, string> ice_getContext();
}
}
In addition to the explicit and the implicit per-proxy contexts we described in the preceding sections, you can also establish an implicit context on a communicator. This implicit context is sent with all invocations made via proxies created by that communicator, provided that you do not supply an explicit context with the call.
module Ice {
local interface Communicator
{
ImplicitContext getImplicitContext();
// ...
};
};
getImplicitContext returns the implicit context. If a communicator has no implicit context, the operation returns a null proxy.
local interface ImplicitContext
{
Context getContext();
void setContext(Context newContext);
string get(string key);
string put(string key, string value);
string remove(string key);
bool containsKey(string key);
};
The getContext operation returns the currently-set context dictionary. The
setContext operation replaces the currently-set context in its entirety.
This operation adds the key–value pair specified by
key and
value. It returns the previous value associated with
key; if no value was previously associated with
key, it returns the empty string. It is legal to add the empty string as a value.
This operation returns true if key is currently set and false, otherwise. You can use this operation to distinguish between a key–value pair that was explicitly added with an empty string as a value, and a key–value pair that was never added at all.
You establish the implicit context on a communicator by setting a property, Ice.ImplicitContext. This property controls whether a communicator has an implicit context and, if so, at what scope the context applies. The property can be set to the following values:
With this setting (or if Ice.ImplicitContext is not set at all), the communicator has no implicit context, and
getImplicitContext returns a null proxy.
•
If you send an invocation via a proxy that has a per-proxy context, and the communicator also has an implicit context, the contents of the per-proxy and implicit context dictionaries are combined, so the
combination of context entries of both contexts is transmitted to the server. If the per-proxy context and the implicit context contain the same key, but with different values, the
per-proxy value takes precedence.
The purpose of Ice::Context is to permit services to be added to Ice that require some contextual information with every request. Contextual information can be used by services such as a transaction service (to provide the context of a currently established transaction) or a security service (to provide an authorization token to the server). IceStorm (see
Chapter 41) uses the context to provide an optional
cost parameter to the service that influences how the service propagates messages to down-stream subscribers.
In general, services that require such contextual information can be implemented much more elegantly using contexts because this hides explicit Slice parameters that would otherwise have to be supplied by the application programmer with every call.
In addition, contexts, because they are optional, permit a single Slice definition to apply to implementations that use the context, as well as to implementations that do not use it. In this way, to add transactional semantics to an existing service, you do not need to modify the Slice definitions to add an extra parameter to each operation. (Adding an extra parameter would not only be inconvenient for clients, but would also split the type system into two halves: without contexts, we would need different Slice definitions for transactional and non-transactional implementations of (conceptually) a single service.)
Finally, per-proxy contexts permit context information to be passed by through intermediate parts of your program without cooperation of those intermediate parts. For example, suppose you set a per-proxy context on a proxy and then pass that proxy to another function. When that function uses the proxy to invoke an operation, the per-proxy context will still be sent. In other words, per-proxy contexts allow you to transparently propagate information via intermediaries that are ignorant of the presence of any context.
Keep in mind though that this works only within a single process. If you stringify a proxy or transmit it as a parameter over the wire, the per-proxy context is
not preserved. (Ice does not write the per-proxy context into stringified proxies and does not marshal the per-proxy context when a proxy is marshaled.)
Contexts are a powerful mechanism for transparent propagation of context information,
if used correctly. In particular, you may be tempted to use contexts as a means of versioning an application as it evolves over time. For example, version 2 of your application may accept two parameters on an operation that, in version 1, used to accept only a single parameter. Using contexts, you could supply the second parameter as a name–value pair to the server and avoid changing the Slice definition of the operation in order to maintain backward compatibility.
We strongly urge you to resist any temptation to use contexts in this manner. The strategy is fraught with problems:
The value of a context is a string, but the application data that is to be sent might be a number, or it might be something more complex, such as a structure with several members. This forces you to encode the value into a string and decode the value again on the server side. Such parsing is tedious and error prone, and far less efficient than sending strongly-typed parameters. In addition, the server has to deal with string values that fail to decode correctly (for example, because of an encoding error made by the client).
None of the preceding problems can arise if you use proper Slice parameters: parameters cannot be accidentally omitted and they are strongly typed, making it much less likely for the client to accidentally send a meaningless value.
If you are concerned about how to evolve an application over time without breaking backward compatibility, Ice facets are better suited to this task (see
Chapter 30). Contexts are meant to be used to transmit simple tokens (such as a transaction identifier) for services that cannot be reasonably implemented without them; you should restrict your use of contexts to that purpose and resist any temptation to use contexts for any other purpose.
Finally, be aware that, if a request is routed via one or more Ice routers, contexts may be dropped by intermediate routers if they consider them illegal. This means that, in general, you cannot rely on an arbitrary context value that is created by an application to actually still be present when a request arrives at the server—only those context values that are known to routers and that are considered legitimate are passed on. It follows that you should not abuse contexts to pass things that really should be passed as parameters.