Virtual entities in Dataverse – Why and how to use them?
September 15, 2021
6 minutes
Link copied!
Why would you want to implement Virtual Entities in the Dataverse? What if they could be used to simplify your data model to make it easier and faster to find things? Here, we’ll see why and how to leverage this tool.
What are virtual entities used for?
Overall, virtual entities can be used to surface data in the Dataverse without storing it there (hence the term virtual). The example provided in the documentation is getting files available in Dropbox surfaced in a subgrid. They can also be used for surfacing entities that are already in the Dataverse by regrouping information together in a different form. Consider the following two scenarios.
Two examples of uses for virtual entities
a) Virtual entities – Regrouping quote benefits across quotes
To begin with, a first situation where virtual entities can be useful is in the creation of queries. There is no doubt that document automation saves a lot of time. However, various problems can slow down this functionality. When this is the case, some reports can be painfully slow to run. And that takes away from the customer experience.
We encountered this problem when designing our TANDEM solution. Sometimes, it could take up to 5 minutes to produce one document. Moreover, the report was complex, gathering information from related entities two levels deep (1=>N=>N), resulting in multiple queries. It seems that this is where the delays came from.
In this case, we had an insurance policy, and its renewal. The renewal was related to several quotes, all of which have different quote benefits (for example, Life Insurance, Dental coverage, and Health coverage). In one row, we wanted to present all the quote benefits for Life Insurance of the different quotes related to that renewal of the policy. And similarly, another row for dental coverage, and another for health coverage.
To solve this problem, we created a virtual entity which made the necessary queries using FetchXML Builder and surfaced the information in a virtual entity view ready to be consumed by the report, dropping its generation time to a few seconds.
b) Virtual entities – Dynamically filtering the view in a subgrid
Another situation where virtual entities can be used is to fulfill the need of changing the FetchXML of a subgrid dynamically based on filtering criteria. This seems to be difficult to do in a supported way. It would be possible to create a UserQuery view (viewSelector.setCurrentView()) on the fly to do this or to override the RetrieveMultiple message with a plugin.
However, it is also possible to use a virtual entity (and I would argue simpler) to simply present the data of the real entity and do the filtering in the background. Consider for example a view that filters based on a value on the form. The view links to the current record and is refreshed when the field value changes (after a save). The virtual entity supports the operation.
How to implement virtual entities?
The initial setup for creating a virtual entity is well described here in a 4-part series. If you choose to do this, it would be better to use only one virtual data provider for all the virtual entities, otherwise you cannot relate them together. This means, for instance, that if you have a virtual contact lookup on a virtual account, you need to use the same provider for the Dataverse to accept that kind of lookup.
1. Detailed Steps
Let’s see how to implement this step by step. First, overall, we will show accounts under another account. For the simplest example possible, we could show the accounts under the parent account of the current one. In the figure below, we would see all four accounts below the parent account, namely Adventure Works, Fourth Coffee, Fabrikam and Litware.
1a) First let’s register a new data provider using the latest plugin registration tool.
1b) From there, we also create a new data source entity.
1c) This data source can be reused on all virtual entities, should we have more than one.
1d) Pick your assembly and the class where the code of the plugin runs.
1e) Add a record to indicate the source.
1f) Select your Data Provider from the list and create the record.
It should look like this once created:
2. Now create the virtual entity
2a) Tick virtual entity and choose the data source record you just created.
2b) Create the following field:
Lookup to Account, jff_realacountid
2c) Create you plugin project and add the following two Nuget packages:
2d) You will need a visitor:
using Microsoft.Xrm.Sdk.Query;
using System;
namespace Dataverse.CustomPlugins
{
public class VirtualAccountVisitor : QueryExpressionVisitorBase
{
public Guid RealEntityId { get; private set; }
public PagingInfo PageInfo { get; private set; }
public ColumnSet ColumnSet { get; private set; }
public override QueryExpression Visit(QueryExpression query)
{
var filter = query.Criteria;
if (filter.Conditions.Count > 0)
{
foreach (ConditionExpression condition in filter.Conditions)
{
if (condition.Operator == ConditionOperator.Equal && condition.Values.Count > 0)
{
if (condition.AttributeName == "jff_realacountid")
{
if (condition.Values[0] is Guid)
{
Guid realEntity = (Guid)condition.Values[0];
RealEntityId = realEntity;
}
}
}
}
if (query.PageInfo != null)
{
VisitPagingInfo(query.PageInfo);
}
}
return query;
}
protected override PagingInfo VisitPagingInfo(PagingInfo pageInfo)
{
PageInfo = pageInfo;
return pageInfo;
}
}
}
2e) And you will also need the provider:
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using System.IO;
using System.Text;
using System.Xml;
namespace Dataverse.CustomPlugins
{
public class VirtualAccountProvidder : PluginBase
{
public VirtualAccountProvidder()
{
RegisterEvent("jff_virtualaccount", EventOperation.RetrieveMultiple, ExecutionStage.MainOperation, RetrieveVirtualAccounts);
}
private void RetrieveVirtualAccounts(LocalPluginContext localContext)
{
var virtualQuery = (QueryExpression)localContext.PluginExecutionContext.InputParameters["Query"];
VirtualAccountVisitor visitor = new VirtualAccountVisitor();
virtualQuery.Accept(visitor);
EntityCollection ec = new EntityCollection();
localContext.TracingService.Trace($"Guid referenced is {visitor.RealEntityId}");
localContext.TracingService.Trace(virtualQuery.ToString());
var parent = localContext.OrganizationService.Retrieve("account", visitor.RealEntityId, new ColumnSet("parentaccountid"));
var parentaccount = parent.GetAttributeValue("parentaccountid");
if (parentaccount != null)
{
string query = $@"";
EntityReference sourceEntityReference = new EntityReference("account", parentaccount.Id);
EntityCollection sourceEntityCollection;
if (visitor.PageInfo != null)
{
string xml = CreateXml(query, visitor.PageInfo.PagingCookie, visitor.PageInfo.PageNumber, visitor.PageInfo.Count);
sourceEntityCollection = localContext.OrganizationService.RetrieveMultiple(new FetchExpression(xml));
}
else
{
sourceEntityCollection = localContext.OrganizationService.RetrieveMultiple(new FetchExpression(query));
}
foreach (var entity in sourceEntityCollection.Entities)
{
Entity virtualEntity = MapVirtualEntity(sourceEntityReference, entity);
ec.Entities.Add(virtualEntity);
}
ec.TotalRecordCount = sourceEntityCollection.TotalRecordCount;
ec.PagingCookie = sourceEntityCollection.PagingCookie;
ec.MoreRecords = sourceEntityCollection.MoreRecords;
}
localContext.PluginExecutionContext.OutputParameters["BusinessEntityCollection"] = ec;
}
private Entity MapVirtualEntity(EntityReference sourceEntityReference, Entity sourceEntity)
{
Entity virtualEntity = new Entity("jff_virtualaccount");
virtualEntity.Attributes.Add("jff_virtualaccountid", sourceEntity.Id);
//virtualEntity.Attributes.Add("id", sourceEntity.Id);
virtualEntity.Attributes.Add("jff_realacountid", new EntityReference(sourceEntity.LogicalName, sourceEntity.Id));
virtualEntity.Attributes.Add("jff_name", sourceEntity.Attributes["name"]);
return virtualEntity;
}
public string CreateXml(string xml, string cookie, int page, int count)
{
StringReader stringReader = new StringReader(xml);
var reader = new XmlTextReader(stringReader);
// Load document
XmlDocument doc = new XmlDocument();
doc.Load(reader);
return CreateXml(doc, cookie, page, count);
}
public string CreateXml(XmlDocument doc, string cookie, int page, int count)
{
XmlAttributeCollection attrs = doc.DocumentElement.Attributes;
if (cookie != null)
{
XmlAttribute pagingAttr = doc.CreateAttribute("paging-cookie");
pagingAttr.Value = cookie;
attrs.Append(pagingAttr);
}
XmlAttribute pageAttr = doc.CreateAttribute("page");
pageAttr.Value = System.Convert.ToString(page);
attrs.Append(pageAttr);
XmlAttribute countAttr = doc.CreateAttribute("count");
countAttr.Value = System.Convert.ToString(count);
attrs.Append(countAttr);
XmlAttribute returnTotal = doc.CreateAttribute("returntotalrecordcount");
returnTotal.Value = "true";
attrs.Append(returnTotal);
XmlAttribute version = doc.CreateAttribute("version");
version.Value = "1.0";
attrs.Append(version);
XmlAttribute output = doc.CreateAttribute("output-format");
output.Value = "xml-platform";
attrs.Append(output);
XmlAttribute mapping = doc.CreateAttribute("mapping");
mapping.Value = "logical";
attrs.Append(mapping);
XmlAttribute distinct = doc.CreateAttribute("distinct");
distinct.Value = "false";
attrs.Append(distinct);
XmlAttribute nolock = doc.CreateAttribute("no-lock");
nolock.Value = "false";
attrs.Append(nolock);
StringBuilder sb = new StringBuilder(1024);
StringWriter stringWriter = new StringWriter(sb);
XmlTextWriter writer = new XmlTextWriter(stringWriter);
doc.WriteTo(writer);
writer.Close();
return sb.ToString();
}
}
}
2f) This works as expected when added to the form.
You can see that this can be used to solve the problem mentioned earlier to show related records. This would not be possible out-of-the-box due to the limitations of subgrids. For this reason, virtual entities are a powerful addition to the Dataverse!