Plugins: Triggers

This type of extensions grants the ability to manipulate while a certain action is taking place e.g. document is being deleted or index is being updated.

Triggers can be divided into four categories:
* PUT triggers
* DELETE triggers
* Read triggers
* Index Query triggers
* Index Update triggers

PUT triggers

To create his own trigger, one must inherit from the AbstractPutTrigger or AbstractAttachmentPutTrigger, but before we will do that let's look at them more closely.

public abstract class AbstractPutTrigger
{
	public virtual VetoResult AllowPut(
		string key,
		RavenJObject document,
		RavenJObject metadata,
		TransactionInformation transactionInformation)
	{
		return VetoResult.Allowed;
	}

	public virtual void OnPut(
		string key,
		RavenJObject document,
		RavenJObject metadata,
		TransactionInformation transactionInformation)
	{
	}

	public virtual void AfterPut(
		string key,
		RavenJObject document,
		RavenJObject metadata,
		Etag etag,
		TransactionInformation transactionInformation)
	{
	}

	public virtual void AfterCommit(
		string key,
		RavenJObject document,
		RavenJObject metadata,
		Etag etag)
	{
	}

	public virtual void Initialize() { }

	public virtual void SecondStageInit() { }

	public DocumentDatabase Database { get; set; }
}

public abstract class AbstractAttachmentPutTrigger
{
	public virtual VetoResult AllowPut(
		string key,
		Stream data,
		RavenJObject metadata)
	{
		return VetoResult.Allowed;
	}

	public virtual void OnPut(
		string key,
		Stream data,
		RavenJObject metadata) { }

	public virtual void AfterPut(
		string key,
		Stream data,
		RavenJObject metadata,
		Etag etag) { }

	public virtual void AfterCommit(
		string key,
		Stream data,
		RavenJObject metadata,
		Etag etag) { }

	public virtual void SecondStageInit() { }

	public virtual void Initialize() { }

	public DocumentDatabase Database { get; set; }
}

public class VetoResult
{
	public static VetoResult Allowed
	{
		get { return new VetoResult(true, "allowed"); }
	}

	public static VetoResult Deny(string reason)
	{
		return new VetoResult(false, reason);
	}

	private VetoResult(bool allowed, string reason)
	{
		IsAllowed = allowed;
		Reason = reason;
	}

	public bool IsAllowed { get; private set; }

	public string Reason { get; private set; }
}

where:
* AllowPut is used to grant or deny the put operation.
* OnPut is used to perform any logic just before the document is saved to the disk.
* AfterPut is used to perform any logic after the document was inserted but still in the same transaction as in OnPut method.
* AfterCommit is used to perform any logic after the transaction was committed.
* Initialize and SecondStageInit are used to trigger the initialization process.

Example - Security trigger

public class SecurityTrigger : AbstractPutTrigger
{
	public override VetoResult AllowPut(string key, RavenJObject document, RavenJObject metadata, TransactionInformation transactionInformation)
	{
		JsonDocument doc = Database.Documents.Get(key, transactionInformation);
		if (doc == null) // new document
			return VetoResult.Allowed;

		if (doc.Metadata["Document-Owner"] == null)// no security
			return VetoResult.Allowed;

		if (doc.Metadata["Document-Owner"].Value<string>() == Thread.CurrentPrincipal.Identity.Name)
			return VetoResult.Allowed;

		return VetoResult.Deny("You are not the document owner, cannot modify document");
	}

	public override void OnPut(string key, RavenJObject document, RavenJObject metadata, TransactionInformation transactionInformation)
	{
		if (metadata["Document-Owner"] == null) // user didn't explicitly set it
		{
			// modify the metadata to the current user
			metadata["Document-Owner"] = RavenJObject.FromObject(Thread.CurrentPrincipal.Identity.Name);
		}
	}
}

Most of the logic is in the AllowPut method, where we check the existing owner (by checking the current version of the document) and reject the update if the owner doesn't match. In the OnPut method, we ensure that the metadata we need is set up correctly. To control attachment putting, similar trigger can be created.

DELETE triggers

Delete triggers is similar in shape to put triggers, yet in contrast to put triggers they control the delete operations. To build your own trigger, you must inherit from AbstractDeleteTrigger or AbstractAttachmentDeleteTrigger.

public abstract class AbstractDeleteTrigger
{
	public virtual VetoResult AllowDelete(
		string key,
		TransactionInformation transactionInformation)
	{
		return VetoResult.Allowed;
	}

	public virtual void OnDelete(
		string key,
		TransactionInformation transactionInformation) { }

	public virtual void AfterDelete(
		string key,
		TransactionInformation transactionInformation) { }

	public virtual void AfterCommit(string key) { }

	public virtual void SecondStageInit() { }

	public virtual void Initialize() { }

	public DocumentDatabase Database { get; set; }
}

public abstract class AbstractAttachmentDeleteTrigger
{
	public virtual VetoResult AllowDelete(string key)
	{
		return VetoResult.Allowed;
	}

	public virtual void OnDelete(string key) { }

	public virtual void AfterDelete(string key) { }

	public virtual void AfterCommit(string key) { }

	public virtual void SecondStageInit() { }

	public virtual void Initialize() { }

	public DocumentDatabase Database { get; set; }
}

where:
* AllowDelete is used to grant or deny the delete operation.
* OnDelete is used to perform any logic just before the document is deleted.
* AfterDelete is used to perform any logic after the document has been deleted but still in the same transaction as in OnDelete method.
* AfterCommit is used to perform any logic after the transaction was committed.
* Initialize and SecondStageInit are used to trigger the initialization process.

Example - Cascading deletes

public class CascadeDeleteTrigger : AbstractDeleteTrigger
{
	public override void OnDelete(string key, TransactionInformation txInfo)
	{
		JsonDocument document = Database.Documents.Get(key, txInfo);
		if (document == null)
			return;

		Database.Documents.Delete(document.Metadata.Value<string>("Cascade-Delete"), null, txInfo);
	}
}

In this case, we perform another delete operation as a part of the current delete operation. This operation is performed under the same transaction as the original operation.

Read triggers

Another type of triggers is used to control the access to documents and manipulate their context when performing read operations. As in the case of the previous triggers, two classes were introduced: the AbstractReadTrigger and AbstractAttachmentReadTrigger.

public abstract class AbstractReadTrigger
{
	public virtual ReadVetoResult AllowRead(
		string key,
		RavenJObject metadata,
		ReadOperation operation,
		TransactionInformation transactionInformation)
	{
		return ReadVetoResult.Allowed;
	}

	public virtual void OnRead(
		string key,
		RavenJObject document,
		RavenJObject metadata,
		ReadOperation operation,
		TransactionInformation transactionInformation)
	{
	}

	public virtual void Initialize() { }

	public virtual void SecondStageInit() { }

	public DocumentDatabase Database { get; set; }
}

public abstract class AbstractAttachmentReadTrigger
{
	public virtual ReadVetoResult AllowRead(
		string key,
		Stream data,
		RavenJObject metadata,
		ReadOperation operation)
	{
		return ReadVetoResult.Allowed;
	}

	public virtual void OnRead(
		string key,
		Attachment attachment)
	{
	}

	public virtual void OnRead(AttachmentInformation information)
	{
	}

	public virtual void SecondStageInit() { }

	public virtual void Initialize() { }

	public DocumentDatabase Database { get; set; }

}

public class ReadVetoResult
{
	public static ReadVetoResult Allowed
	{
		get { return new ReadVetoResult(ReadAllow.Allow, "allowed"); }
	}

	public static ReadVetoResult Ignore
	{
		get { return new ReadVetoResult(ReadAllow.Ignore, "ignore"); }
	}

	public static ReadVetoResult Deny(string reason)
	{
		return new ReadVetoResult(ReadAllow.Deny, reason);
	}

	private ReadVetoResult(ReadAllow allowed, string reason)
	{
		Veto = allowed;
		Reason = reason;
	}

	public ReadAllow Veto { get; private set; }

	public enum ReadAllow
	{
		Allow,
		Deny,
		Ignore
	}

	public string Reason { get; private set; }
}

where:
* AllowRead is used to grant or deny the read operation.
* OnRead is used to perform any logic just before the document is read e.g. modify the document or document metadata (modified values are transient and are not saved to the database).
* Initialize and SecondStageInit are used to trigger the initialization process.

Example - Information hiding

public class SecurityReadTrigger : AbstractReadTrigger
{
	public override ReadVetoResult AllowRead(string key, RavenJObject metadata, ReadOperation operation, TransactionInformation transactionInformation)
	{
		if (metadata.Value<string>("Document-Owner") == Thread.CurrentPrincipal.Identity.Name)
			return ReadVetoResult.Allowed;

		if (operation == ReadOperation.Load)
			return ReadVetoResult.Deny("You don't have permission to read this document");

		return ReadVetoResult.Ignore;
	}
}

In the example above, we only let the owner of a document read it. You can see that a Read trigger can deny the read to the user (returning an error to the user) or ignore the read (hiding the presence of the document). You can also make decisions based on whether that specific document was requested, or if the document was read as a part of a query.

Example - Linking document on the server side

public class EmbedLinkDocument : AbstractReadTrigger
{
	public override void OnRead(string key, RavenJObject document, RavenJObject metadata, ReadOperation operation, TransactionInformation transactionInformation)
	{
		string linkName = metadata.Value<string>("Raven-Link-Name");
		string link = metadata.Value<string>("Raven-Link");
		if (link == null)
			return;

		JsonDocument linkedDocument = Database.Documents.Get(link, transactionInformation);
		document.Add(linkName, linkedDocument.ToJson());
	}
}

In this case, we detect if a document with a link was requested, and we stitch such document together with its link to create a single document.

Index Query triggers

Query triggers have been introduced to extend the query parsing capabilities and provide users with a way to modify the queries before they are executed against the index. To write your own query trigger, you must inherit from AbstractIndexQueryTrigger class.

public abstract class AbstractIndexQueryTrigger
{
	public virtual void Initialize() { }

	public virtual void SecondStageInit() { }

	public DocumentDatabase Database { get; set; }

	public abstract Query ProcessQuery(string indexName, Query query, IndexQuery originalQuery);
}

where:
* ProcessQuery is used to perform any logic on the query provided.
* Initialize and SecondStageInit are used to trigger the the initialization process.

Example - Combining current query with our additional custom logic

public class CustomQueryTrigger : AbstractIndexQueryTrigger
{
	private const string SpecificIndexName = "Specific/Index";

	public override Query ProcessQuery(string indexName, Query query, IndexQuery originalQuery)
	{
		if (indexName != SpecificIndexName)
			return query;

		PrefixQuery customQuery = new PrefixQuery(new Term("CustomField", "CustomPrefix"));

		return new BooleanQuery
		{
			{ query, Occur.MUST },
			{ customQuery, Occur.MUST}
		};
	}
}

Index Update triggers

Index Update triggers allow users to perform custom actions every time an index entry has been created or deleted. To write your own trigger we must consider two classes. The AbstractIndexUpdateTrigger and AbstractIndexUpdateTriggerBatcher defined below.

public abstract class AbstractIndexUpdateTrigger
{
	public virtual void Initialize() { }

	public virtual void SecondStageInit() { }

	public abstract AbstractIndexUpdateTriggerBatcher CreateBatcher(int indexId);

	public DocumentDatabase Database { get; set; }
}

where:
* CreateBatcher is used to construct a batcher for a given index.
* Initialize and SecondStageInit are used to trigger the initialization process.

public abstract class AbstractIndexUpdateTriggerBatcher : IDisposable
{
	public virtual void OnIndexEntryDeleted(string entryKey, Document document = null) { }

	public virtual bool RequiresDocumentOnIndexEntryDeleted { get { return false; } }

	public virtual void OnIndexEntryCreated(string entryKey, Document document) { }

	public virtual void Dispose() { }

	public virtual void AnErrorOccured(Exception exception) { }
}

where:
* OnIndexEntryDeleted is executed when index entry is being removed from the index. The provided key may represent an already deleted document.
* OnIndexEntryCreated is executed when specified document with a given key is being inserted. The changes introduced to the provided lucene document will be written to the Lucene index.
* AnErrorOccured is used to notify the batcher that an error occurred.

Example - Creating static snapshot from the indexed document

public class SnapshotShoppingCartUpdateTrigger : AbstractIndexUpdateTrigger
{
	public override AbstractIndexUpdateTriggerBatcher CreateBatcher(int indexId)
	{
		return new SnapshotShoppingCartBatcher(indexId, Database);
	}
}

public class SnapshotShoppingCartBatcher : AbstractIndexUpdateTriggerBatcher
{
	private readonly string indexName;

	private readonly DocumentDatabase database;

	public SnapshotShoppingCartBatcher(int indexId, DocumentDatabase database)
	{
		indexName = database.IndexDefinitionStorage.GetIndexDefinition(indexId).Name;
		this.database = database;
	}

	public override void OnIndexEntryCreated(string entryKey, Document document)
	{
		if (indexName != "Aggregates/ShoppingCart")
			return;

		RavenJObject shoppingCart = RavenJObject.Parse(document.GetField("Aggregate").StringValue);
		string shoppingCartId = document.GetField("Id").StringValue;

		PutResult result = database.Documents.Put("shoppingcarts/" + shoppingCartId + "/snapshots/", null, shoppingCart, new RavenJObject(), null);
		document.Add(new Field("Snapshot", result.Key, Field.Store.YES, Field.Index.NOT_ANALYZED));
	}
}

This index works on the following index in order to create a static snapshot of the indexed document whenever it is indexed. Note that we use identity insert here (the key we use ends with '/') so we will have documents like this:

  • shoppingcarts/12/snapshots/1
  • shoppingcarts/12/snapshots/2
  • shoppingcarts/12/snapshots/3

This is useful if we want to keep a record of all the changes introduced to the index. Note that we also change the document to store the snapshot key for this particular version.