Добро пожаловать в мой блог! Изначально он не задумывался как блог CRM разработчика, но жизнь сама внесла нужные коррективы. Тут я публикою все свои наблюдения относительно обозначенных в заголовке систем. Если Вы найдете в нем что-то интересное для Вас, как для заказчика, то буду рад сотрудничать с Вами! В моей компетенции 100% задач по MS CRM 3.0/4.0/2011:
MVP 2010, 2011
- Консалтинг
- Проектирование
- Разработка
- Обучение
MVP 2010, 2011
Представления с использованием API
Запись от Артем Enot Грунин размещена 03.09.2010 в 09:05
Теги fetch, savedquery
Многие, наверно, замечали, что в системе содержится ряд представлений, которые не возможно построить при помощи расширенного поиска, например, "организации, без заказов за последние 6 месяцев". При помощи SDK такой запрос построить тоже не возможно, но речь сейчас не о том. Очевидно, что в системе реализован альтернативный (без использования fetch) механизм построения представлений, который мог бы быть полезен разработчику: Query API. Представления хранятся в системе в форме двух объектов SavedQuery - представления которыми владеет организация и UserQuery - пользовательские представления. Оба объекта имеют атрибут fetchxml который реализует доступный нам метод построения представлений. SavedQuery так же имеет атрибут queryapi который используется системой для выполнения сложных запросов, которые невозможно построить используется язык fetch. К сожалению, список доступных queryapi жестко прошит в системе и не поддается расширению законными способами. И тем не менее выход есть! Идея состоит в том, чтобы написать плагин на событие preRetrive сущностей SavedQuery или UserQuery, где любым удобным для нас способом получить список записей аналитического представления, после чего составить fetch запрос содержащий буквальную выборку этих записей и поместить его вместо оригинального значения атрибута fetchxml. В случае, если запрос можно построить через объектную модель CRM его можно просто конвертировать в fetch при помощи запроса QueryExpressionToFetchXmlRequest. В противном случае мы вынуждены использовать SQL. Для идентификации представлений которым нужно "помочь" построить запрос я решил использовать не название, которое может менять в зависимости от требований заказчика, а поле "описание", которое большинство пользователей никогда не увидит. В результате родился такой вот код:
Так как запрос представлений происходит постоянно, с целью повышения производительности решения параметры подключения к базе я передаю через конструктор плагина.
Так же мы вынуждены использовать не фильтрованные представления, так как учетка под которой работает пул (у меня это сетевая служба) может не иметь к ним доступа. И последний момент: в использовании SQL может быть скрытый подлог безопасности: если у пользователя нет прав на просмотр каких то записей, то fetch их и не вернет - это нормально. Однако, представим ситуацию, когда пользователь не имеет права на просмотр заказов, но видит все организации. Если ваш запрос возвращает все организации с заказами на сумму больше миллиона, то пользователь, не имея прав на аналитику по заказам, косвенно сможет оценить их стоимость на основании данных по полученным организациям.
Код:
using System; using Microsoft.Crm.Sdk; using Microsoft.Crm.SdkTypeProxy; using System.Data.SqlClient; using Microsoft.Crm.Sdk.Query; namespace CustomQueryAPI { public class QueryFetchPlugin : IPlugin { private string connectionString; IPluginExecutionContext pluginContext; public QueryFetchPlugin(string unsecure, string secure) { //"Data Source=CRMDEV;Initial Catalog=ICS_MSCRM;Integrated Security=SSPI" connectionString = secure; } public void Execute(IPluginExecutionContext context) { this.pluginContext = context; DynamicEntity query = (DynamicEntity)pluginContext.OutputParameters[ParameterName.BusinessEntity]; string api = (string)query.Properties["description"]; if (String.IsNullOrEmpty(api)) return; // Для систематизации и удобства наращивания api группируются в наборы ClassAPI.MetodAPI string[] paths = api.Split(new string[] { "." }, StringSplitOptions.RemoveEmptyEntries); if (paths.Length < 2) return; string className = paths[0]; string methodName = paths[1]; string myFetch = String.Empty; switch (className) { case "Contact": myFetch = retrieveContact(methodName); break; case "Account": myFetch = retrieveAccount(methodName); break; } if (myFetch != null) query.Properties["fetchxml"] = myFetch; } private string queryExpressionToFetch(QueryBase query) { QueryExpressionToFetchXmlRequest request = new QueryExpressionToFetchXmlRequest(); request.Query = query; ICrmService service = this.pluginContext.CreateCrmService(true); QueryExpressionToFetchXmlResponse responce = (QueryExpressionToFetchXmlResponse)service.Execute(request); return responce.FetchXml; } private string sqlToFetch(string entity, string primarykey, string sql) { string fetchPattern = @"<fetch version=""1.0"" output-format=""xml-platform"" mapping=""logical"" distinct=""false""> <entity name=""{0}""> <attribute name=""{1}""/> <filter type=""and""> <condition attribute=""{1}"" operator=""in""> {2} </condition> </filter> </entity> </fetch>"; string filterValues = String.Empty; using (SqlConnection sqlConnection = new SqlConnection(this.connectionString)) { sqlConnection.Open(); SqlCommand cmd = new SqlCommand(sql, sqlConnection); SqlDataReader reader = cmd.ExecuteReader(); try { while (reader.Read()) { string id = ((Guid)reader[0]).ToString(); filterValues += "<value>" + id + "</value>\n"; } } finally { reader.Close(); } } return String.Format(fetchPattern, entity, primarykey, filterValues); } private string retrieveAccount(string methodName) { string fetch = String.Empty; switch (methodName) { case "NoOpportunitys": string sqlQuery = @"select account.accountid from account left outer join opportunity on account.accountid = opportunity.customerid where (opportunity.OpportunityId is null)"; fetch = sqlToFetch("account", "accountid", sqlQuery); break; } return fetch; } private string retrieveContact(string methodName) { string fetch = String.Empty; switch (methodName) { case "BDayThisMonth": string sqlQuery = "SELECT Contactid FROM Contact WHERE MONTH(GETDATE()) = MONTH(BirthDate)"; fetch = sqlToFetch("contact", "contactid", sqlQuery); break; } return fetch; } } }
Так же мы вынуждены использовать не фильтрованные представления, так как учетка под которой работает пул (у меня это сетевая служба) может не иметь к ним доступа. И последний момент: в использовании SQL может быть скрытый подлог безопасности: если у пользователя нет прав на просмотр каких то записей, то fetch их и не вернет - это нормально. Однако, представим ситуацию, когда пользователь не имеет права на просмотр заказов, но видит все организации. Если ваш запрос возвращает все организации с заказами на сумму больше миллиона, то пользователь, не имея прав на аналитику по заказам, косвенно сможет оценить их стоимость на основании данных по полученным организациям.
Всего комментариев 1
Комментарии
-
Спасибо Konstantin Katsovich за замечание, возможно я опечатался и плагин нужно писать на пост ретрив, как будет возможность проверю и отпишу резюме.
Запись от Артем Enot Грунин размещена 21.04.2011 в 23:56