我是微软Dynamics 365 & Power Platform方面的工程师/顾问罗勇,也是2015年7月到2018年6月连续三年Dynamics CRM/Business Solutions方面的微软最有价值专家(Microsoft MVP),欢迎关注我的微信公众号 MSFTDynamics365erLuoYong ,回复431或者20201221可方便获取本文,同时可以在第一间得到我发布的最新博文信息,follow me!
前面的博文 介绍并配置Dynamics 365中的虚拟实体Virtual Entity 讲了通过配置的方法来使用虚拟实体,今天我们来讲需要编码才能使用的虚拟实体。主要参考的官方文档包括 Custom virtual entity data providers 和 Sample: Generic virtual entity data provider plug-in 。
这首先需要创建自定义数据提供方(custom data provider),分为两种自定义数据提供方,分别是通用的自定义数据提供方(Generic)和特定的自定义数据提供方(Targeted),我们今天演示下通用的自定义数据提供方来为虚拟实体提供数据。
和创建插件程序集差不多,需要创建一个框架为.NET Framework 4.6.2 的 Class Library (.NET Framework) 项目。我这里将自动生成Class1.cs文件删除。
首先通过Nuget添加对 Microsoft.CrmSdk.Data 的引用。
安装时候会自动安装Microsoft.CrmSdk.CoreAssemblies ,安装完成后界面如下。
像开发插件一样,需要为该程序集添加签名。
然后一般至少需要添加两个类,分别为RetrieveMutiple 和 Retrieve消息准备,我这里使用的示例代码如下。值得注意的是虚拟实体的主键,比如我这里是 ly_testvirtualentityid 一定要赋值,否则会出错。
using Microsoft.Crm.Sdk.Messages; using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Data.Exceptions; using Microsoft.Xrm.Sdk.Extensions; using Microsoft.Xrm.Sdk.Query; using System; using System.Collections.Generic; using System.IO; using System.Net.Http; using System.Runtime.Serialization.Json; using System.Text; using System.Threading.Tasks; namespace LuoYongCustomDataProvider { public class RetrieveMultiple : IPlugin { public void Execute(IServiceProvider serviceProvider) { ITracingService tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService)); IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext)); IOrganizationServiceFactory factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory)); IOrganizationService orgSvc = factory.CreateOrganizationService(context.UserId); try { QueryExpression query = context.InputParameterOrDefault<QueryExpression>("Query"); var filter = query.Criteria; //下面的代码可以用来查看查询使用的QueryExpression转换后的FetchXml QueryExpressionToFetchXmlRequest req = new QueryExpressionToFetchXmlRequest(); req.Query = query; QueryExpressionToFetchXmlResponse resp = (QueryExpressionToFetchXmlResponse)orgSvc.Execute(req); tracingService.Trace($"Query expression to fetchxml={resp.FetchXml}"); var transferedFetchXml = resp.FetchXml.Replace("ly_testvirtualentityid", "accountid"); transferedFetchXml = transferedFetchXml.Replace("ly_testvirtualentity","account"); transferedFetchXml = transferedFetchXml.Replace("ly_name", "name"); transferedFetchXml = transferedFetchXml.Replace("ly_createdon", "createdon"); transferedFetchXml = transferedFetchXml.Replace("ly_website", "websiteurl"); tracingService.Trace($"Transfered Fetchxml={transferedFetchXml}"); EntityCollection results = new EntityCollection(); var queryResults = ExecuteQuery(tracingService, transferedFetchXml).Result; queryResults.ForEach(t => { results.Entities.Add(new Entity("ly_testvirtualentity") { Id = t.accountid, Attributes = { ["ly_testvirtualentityid"] = t.accountid, ["ly_name"] = t.name, ["ly_createdon"] = Convert.ToDateTime(t.createdon).ToUniversalTime(), ["ly_website"] = t.websiteurl } }); }); tracingService.Trace($"results.count = {results.Entities.Count}"); context.OutputParameters["BusinessEntityCollection"] = results; } catch (Exception e) { tracingService.Trace($"{e.Message} {e.StackTrace}"); if (e.InnerException != null) tracingService.Trace($"{e.InnerException.Message} {e.InnerException.StackTrace}"); throw new InvalidPluginExecutionException(e.Message); } } private static async Task<List<Result>> ExecuteQuery(ITracingService tracer, string fetchXml) { if (string.IsNullOrEmpty(fetchXml)) { fetchXml = @"<fetch version='1.0' mapping='logical' distinct='false'><entity name='account'><attribute name='name' /><attribute name='accountid' /><attribute name='websiteurl' /><attribute name='createdon' /><order attribute='name' descending='false' /><filter type='and'><condition attribute='statecode' operator='eq' value='0' /></filter></entity></fetch>"; } System.Collections.Generic.List<KeyValuePair<string, string>> vals = new System.Collections.Generic.List<KeyValuePair<string, string>>(); vals.Add(new KeyValuePair<string, string>("client_id", @"bd41df00-ae2b-4d94-aa12-18fb231c0b51")); vals.Add(new KeyValuePair<string, string>("resource", @"https://logicalinventorycenter.crm5.dynamics.com/")); vals.Add(new KeyValuePair<string, string>("grant_type", "client_credentials")); vals.Add(new KeyValuePair<string, string>("client_secret", @"8Es-j~RZG~-w.1Q7D_546r5IWZIq9XkqIp")); string tokenUrl = string.Format("https://login.windows.net/{0}/oauth2/token", @"4a54995a-f092-4738-806e-c8427bdc39fb"); using (HttpClient httpClient = new HttpClient()) { httpClient.DefaultRequestHeaders.Add("Cache-Control", "no-cache"); HttpContent content = new FormUrlEncodedContent(vals); HttpResponseMessage hrm = httpClient.PostAsync(tokenUrl, content).Result; if (hrm.IsSuccessStatusCode) { string data = await hrm.Content.ReadAsStringAsync(); //tracer.Trace($"Get token response = {data}"); var authenticationResponse = Utility.DeserializeDictionary(data); var request = new HttpRequestMessage(HttpMethod.Get, $"https://logicalinventorycenter.crm5.dynamics.com/api/data/v9.1/accounts?fetchXml={fetchXml}"); request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", authenticationResponse["access_token"]); request.Headers.Add("OData-MaxVersion", "4.0"); request.Headers.Add("OData-Version", "4.0"); var queryResponseStr = httpClient.SendAsync(request).Result.Content.ReadAsStringAsync().Result; //tracer.Trace($"Execute query result = {queryResponseStr}"); return ((QueryResult)Utility.DeserializeObject(queryResponseStr, typeof(QueryResult))).value; } else { throw new InvalidPluginExecutionException("Get token error!"); } } } } }
using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Data.Exceptions; using Microsoft.Xrm.Sdk.Extensions; using Microsoft.Xrm.Sdk.Query; using System; using System.Collections.Generic; using System.IO; using System.Net.Http; using System.Runtime.Serialization.Json; using System.Text; using System.Threading.Tasks; namespace LuoYongCustomDataProvider { public class Retrieve : IPlugin { public void Execute(IServiceProvider serviceProvider) { ITracingService tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService)); IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext)); IOrganizationServiceFactory factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory)); IOrganizationService orgSvc = factory.CreateOrganizationService(context.UserId); try { EntityReference target = (EntityReference)context.InputParameters["Target"]; tracingService.Trace($"Target: {target.Id.ToString()}"); var queryResults = ExecuteQueryById(tracingService, target.Id).Result; var newEntity = new Entity("ly_virtualaccount"); newEntity.Id = queryResults.accountid; newEntity["ly_testvirtualentityid"] = queryResults.accountid; newEntity["ly_name"] = queryResults.name; newEntity["ly_createdon"] = Convert.ToDateTime(queryResults.createdon).ToUniversalTime(); newEntity["ly_website"] = queryResults.websiteurl; context.OutputParameters["BusinessEntity"] = newEntity; } catch (Exception e) { tracingService.Trace($"{e.Message} {e.StackTrace}"); if (e.InnerException != null) tracingService.Trace($"{e.InnerException.Message} {e.InnerException.StackTrace}"); throw new InvalidPluginExecutionException(e.Message); } } private static async Task<Result> ExecuteQueryById(ITracingService tracingService, Guid Id) { System.Collections.Generic.List<KeyValuePair<string, string>> vals = new System.Collections.Generic.List<KeyValuePair<string, string>>(); vals.Add(new KeyValuePair<string, string>("client_id", @"bd41df00-ae2b-4d94-aa12-18fb231c0b51")); vals.Add(new KeyValuePair<string, string>("resource", @"https://logicalinventorycenter.crm5.dynamics.com/")); vals.Add(new KeyValuePair<string, string>("grant_type", "client_credentials")); vals.Add(new KeyValuePair<string, string>("client_secret", @"8Es-j~RZG~-w.1Q7D_546r5IWZIq9XkqIp")); string tokenUrl = string.Format("https://login.windows.net/{0}/oauth2/token", @"4a54995a-f092-4738-806e-c8427bdc39fb"); using (HttpClient httpClient = new HttpClient()) { httpClient.DefaultRequestHeaders.Add("Cache-Control", "no-cache"); HttpContent content = new FormUrlEncodedContent(vals); HttpResponseMessage hrm = httpClient.PostAsync(tokenUrl, content).Result; if (hrm.IsSuccessStatusCode) { string data = await hrm.Content.ReadAsStringAsync(); tracingService.Trace($"Get token response = {data}"); var authenticationResponse = Utility.DeserializeDictionary(data); var request = new HttpRequestMessage(HttpMethod.Get, $"https://logicalinventorycenter.crm5.dynamics.com/api/data/v9.1/accounts({Id})?$select=name,websiteurl,createdon"); request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", authenticationResponse["access_token"]); request.Headers.Add("OData-MaxVersion", "4.0"); request.Headers.Add("OData-Version", "4.0"); var queryResponseStr = httpClient.SendAsync(request).Result.Content.ReadAsStringAsync().Result; tracingService.Trace($"Execute query result = {queryResponseStr}"); return ((Result)Utility.DeserializeObject(queryResponseStr, typeof(Result))); } else { throw new InvalidPluginExecutionException("Get token error!"); } } } } }
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.Serialization.Json; using System.Text; using System.Threading.Tasks; namespace LuoYongCustomDataProvider { public class Utility { public static Dictionary<string, string> DeserializeDictionary(string josnStr) { Dictionary<string, string> returnVal = null; if (!string.IsNullOrEmpty(josnStr)) { DataContractJsonSerializerSettings serializerSettings = new DataContractJsonSerializerSettings { UseSimpleDictionaryFormat = true }; using (var ms = new MemoryStream(Encoding.Unicode.GetBytes(josnStr))) { DataContractJsonSerializer deseralizer = new DataContractJsonSerializer(typeof(Dictionary<string, string>), serializerSettings); returnVal = (Dictionary<string, string>)deseralizer.ReadObject(ms); } } return returnVal; } public static Object DeserializeObject(string josnStr, Type type) { Object returnVal = null; if (!string.IsNullOrEmpty(josnStr)) { DataContractJsonSerializerSettings serializerSettings = new DataContractJsonSerializerSettings { UseSimpleDictionaryFormat = true }; using (var ms = new MemoryStream(Encoding.Unicode.GetBytes(josnStr))) { DataContractJsonSerializer deseralizer = new DataContractJsonSerializer(type); returnVal = deseralizer.ReadObject(ms); } } return returnVal; } } }
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace LuoYongCustomDataProvider { public class Result { public Guid accountid { get; set; } public string name { get; set; } public string createdon { get; set; } public string websiteurl { get; set; } } public class QueryResult { public List<Result> value { get; set; } } }
再需要将刚才的程序集使用插件注册工具的 Register > Register New Assembly 注册下。
然后需要注册一个新的Data Provider, 还是使用插件注册工具 Register > Register New Data Provider .
我的设置如下,注意Data Source Entity选择Create New Data Source ,Assembly选择我刚才注册,Retrieve和RetrieveMultiple分别选择前面创建的Class。
这时候会弹出新窗口,我的设置如下,然后点击 Create 按钮。
创建成功后在Register New Data Provider界面点击 Register 按钮。
创建完了界面如下所示:
还没完,需要在Dynamics 365中导航到 Advanced Settings > Settings > Administration > Virtual Entity Datasources ,新建一个Data Source,选择我们前面步骤创建的Data Provider,点击OK。
输入Name,并保存记录。
然后就是新建的虚拟实体,选择我们新建的Data Source,保存好后发布我们去看效果。
如果有错误,可以开启插件日志,看看具体错误内容。我这里看到效果如下:
我为这个虚拟实体启用了快速搜索,比如将name列设定为快速查找列,搜索也是正常。