private Status GenerateSuggestions(WorkflowInstance workflowInstance, ServerEntity entity, Dictionary<string, string> suggestionList) { Item item = entity as Item; if (item == null) { TraceLog.TraceError("Entity is not an Item"); return Status.Error; } try { BingSearch bingSearch = new BingSearch(); // retrieve and format the search template, or if one doesn't exist, use $(Intent) string searchTemplate = null; if (InputParameters.TryGetValue(ActivityParameters.SearchTemplate, out searchTemplate) == false) searchTemplate = String.Format("$({0})", ActivityVariables.Intent); string query = ExpandVariables(workflowInstance, searchTemplate); if (String.IsNullOrWhiteSpace(query)) { TraceLog.TraceInfo("No query to issue Bing"); return Status.Complete; } // make a synchronous webservice call to bing // // Async has the problem that the caller of this method assumes that a // populated suggestionList will come out. If it doesn't, the state will execute // again and trigger a fresh set of suggestions to be generated. Eventually all // queries will return and populate the suggestions DB with duplicate data. // This can be fixed once we move to a "real" workflow system such as WF. var results = bingSearch.Query(query); foreach (var r in results) { WebResult result = r as WebResult; if (result != null) suggestionList[result.Title] = result.Url; } } catch (Exception ex) { TraceLog.TraceException("Bing query failed", ex); return Status.Error; } // this activity is typically last and once links have been generated, no need // to keep the workflow instance around return Status.Complete; }
private Status GenerateSuggestions(WorkflowInstance workflowInstance, ServerEntity entity, Dictionary<string, string> suggestionList) { Item item = entity as Item; if (item == null) { TraceLog.TraceError("Entity is not an Item"); return Status.Error; } // TODO: get subject attributes from the Contacts folder, Facebook, and Cloud AD // these will hang off of the contact as well as in the workflow InstanceData // inexact match return Status.Pending; }
private Status GenerateSuggestions(WorkflowInstance workflowInstance, ServerEntity entity, Dictionary<string, string> suggestionList) { Item item = entity as Item; if (item == null) { TraceLog.TraceError("GenerateSuggestions: non-Item passed in"); return Status.Error; } // HACK: hardcode names for now until the graph queries are in place foreach (var like in "Golf;Seattle Sounders;Malcolm Gladwell".Split(';')) { suggestionList[like] = like; } return Status.Pending; }
private Status GenerateSuggestions(WorkflowInstance workflowInstance, ServerEntity entity, Dictionary<string, string> suggestionList) { Item item = entity as Item; if (item == null) { TraceLog.TraceError("Entity is not an Item"); return Status.Error; } // make sure the subject was identified - if not move the state forward string subjectItem = GetInstanceData(workflowInstance, ActivityVariables.Contact); if (subjectItem == null) return Status.Complete; // set up the FB API context FBGraphAPI fbApi = new FBGraphAPI(); // get the current user User user = UserContext.GetUser(item.UserID, true); if (user == null) { TraceLog.TraceError("Could not find the user associated with item " + item.Name); return Status.Error; } UserCredential cred = user.GetCredential(UserCredential.FacebookConsent); if (cred != null && cred.AccessToken != null) { fbApi.AccessToken = cred.AccessToken; } else { // user not having a FB token is not an error condition, but there is no way to generate suggestions // just move forward from this state return Status.Complete; } Item subject = null; try { subject = JsonSerializer.Deserialize<Item>(subjectItem); // if the subjectItem is a reference, chase it down while (subject.ItemTypeID == SystemItemTypes.Reference) { FieldValue refID = subject.GetFieldValue(FieldNames.EntityRef); Guid refid = new Guid(refID.Value); subject = UserContext.Items.Include("FieldValues").Single(i => i.ID == refid); } } catch (Exception ex) { TraceLog.TraceException("Could not deserialize subject Item", ex); return Status.Error; } FieldValue fbID = subject.GetFieldValue(FieldNames.FacebookID); if (fbID == null || fbID.Value == null) { TraceLog.TraceError(String.Format("Could not find FacebookID for Contact {0}", subject.Name)); return Status.Complete; } try { // issue the query against the Facebook Graph API var results = fbApi.Query(fbID.Value, FBQueries.Likes); foreach (var like in results) { string name = (string) like[FBQueryResult.Name]; suggestionList[name] = name; } } catch (Exception ex) { TraceLog.TraceException("Error calling Facebook Graph API", ex); return Status.Complete; } return Status.Complete; }
private Item MakeContact(WorkflowInstance workflowInstance, Item item, ADQueryResult subject) { DateTime now = DateTime.UtcNow; /* FieldValue contactsField = GetFieldValue(item, TargetFieldName, true); Guid listID = contactsField.Value != null ? new Guid(contactsField.Value) : Guid.NewGuid(); // if the contacts sublist under the item doesn't exist, create it now if (contactsField.Value == null) { Item list = new Item() { ID = listID, Name = TargetFieldName, IsList = true, FolderID = item.FolderID, ItemTypeID = SystemItemTypes.Reference, ParentID = item.ID, UserID = item.UserID, Created = now, LastModified = now, }; contactsField.Value = listID.ToString(); UserContext.Items.Add(list); UserContext.SaveChanges(); // add a Suggestion with a RefreshEntity FieldName to the list, to tell the UI that the // workflow changed the Item SignalEntityRefresh(workflowInstance, item); } */ // try to find an existing contact using matching heuristic var contact = GetContact(item.UserID, subject); // if the contact wasn't found, create the new contact (detached) - it will be JSON-serialized and placed into // the suggestion value field if (contact == null) { contact = new Item() { ID = Guid.NewGuid(), Name = subject.Name, FolderID = item.FolderID, ItemTypeID = SystemItemTypes.Contact, ParentID = null /*listID*/, UserID = item.UserID, FieldValues = new List<FieldValue>(), Created = now, LastModified = now, }; } // add various FieldValues to the contact if available try { // add sources string sources = String.Join(",", subject.IDs.Select(id => id.Source)); contact.GetFieldValue(FieldNames.Sources, true).Value = sources; // add birthday if (subject.Birthday != null) contact.GetFieldValue(FieldNames.Birthday, true).Value = ((DateTime)subject.Birthday).ToString("d"); // add facebook ID string fbID = subject.IDs.Single(id => id.Source == ADQueryResultValue.FacebookSource).Value; if (fbID != null) contact.GetFieldValue(FieldNames.FacebookID, true).Value = fbID; } catch (Exception) { } return contact; }
private Status GenerateSuggestions(WorkflowInstance workflowInstance, ServerEntity entity, Dictionary<string, string> suggestionList) { Item item = entity as Item; if (item == null) { TraceLog.TraceError("Entity is not an Item"); return Status.Error; } User user = UserContext.GetUser(item.UserID, true); if (user == null) { TraceLog.TraceError("Could not find the user associated with Item " + item.Name); return Status.Error; } ADGraphAPI adApi = new ADGraphAPI(); string adRefreshToken = null; // check for FB and/or AD credentials UserCredential cred = user.GetCredential(UserCredential.FacebookConsent); if (cred != null && cred.AccessToken != null) { adApi.FacebookAccessToken = cred.AccessToken; } cred = user.GetCredential(UserCredential.CloudADConsent); if (cred != null && cred.RenewalToken != null) { adRefreshToken = cred.RenewalToken; } if (adApi.FacebookAccessToken == null && adRefreshToken == null) { // user not having either token is not an error condition, but there is no way to generate suggestions // just move forward from this state return Status.Complete; } // if a refresh token exists for AD, get an access token from Azure ACS for the Azure AD service if (adRefreshToken != null) { try { AccessTokenRequestWithRefreshToken request = new AccessTokenRequestWithRefreshToken(new Uri(AzureOAuthConfiguration.GetTokenUri())) { RefreshToken = adRefreshToken, ClientId = AzureOAuthConfiguration.GetClientIdentity(), ClientSecret = AzureOAuthConfiguration.ClientSecret, Scope = AzureOAuthConfiguration.RelyingPartyRealm, }; OAuthMessage message = OAuthClient.GetAccessToken(request); AccessTokenResponse authzResponse = message as AccessTokenResponse; adApi.ADAccessToken = authzResponse.AccessToken; // workaround for ACS trashing the refresh token if (!String.IsNullOrEmpty(authzResponse.RefreshToken)) { TraceLog.TraceInfo("Storing new CloudAD refresh token"); user.AddCredential(UserCredential.CloudADConsent, authzResponse.AccessToken, null, authzResponse.RefreshToken); UserContext.SaveChanges(); } } catch (Exception ex) { TraceLog.TraceException("Could not contact ACS to get an access token", ex); // Facebook credentials are not available if (adApi.FacebookAccessToken == null) return Status.Pending; // could be a temporary outage, do not move off this state } } // extract a subject hint if one hasn't been discovered yet string subjectHint = GetInstanceData(workflowInstance, ActivityVariables.SubjectHint); if (String.IsNullOrEmpty(subjectHint)) { try { Phrase phrase = new Phrase(item.Name); if (phrase.Task != null) { subjectHint = phrase.Task.Subject; if (!String.IsNullOrWhiteSpace(subjectHint)) StoreInstanceData(workflowInstance, ActivityVariables.SubjectHint, subjectHint); } } catch (Exception ex) { TraceLog.TraceException("Could not initialize NLP engine", ex); } } // get contacts from Cloud AD and Facebook via the AD Graph Person service // TODO: also get local contacts from the Contacts folder try { var results = adApi.Query(subjectHint ?? ""); foreach (var subject in results) { // serialize an existing contact corresponding to the subject, // or generate a new serialized contact if one wasn't found Item contact = MakeContact(workflowInstance, item, subject); suggestionList[contact.Name] = JsonSerializer.Serialize(contact); } } catch (Exception ex) { TraceLog.TraceException("Could not contact Person Service", ex); return Status.Error; } // inexact match return Status.Pending; }
/// <summary> /// Check and process the target field - if it is on the item, store the value in the /// state bag and return true /// </summary> /// <param name="workflowInstance">WorkflowInstance to process</param> /// <param name="item">Item to check</param> /// <returns>true for success, false if target field was not found</returns> protected bool CheckTargetField(WorkflowInstance workflowInstance, Item item) { if (TargetFieldName == null) return false; // if the target field has been set, this state can terminate try { FieldValue targetField = item.GetFieldValue(TargetFieldName); if (targetField != null && targetField.Value != null) { StoreInstanceData(workflowInstance, OutputParameterName, targetField.Value); StoreInstanceData(workflowInstance, ActivityVariables.LastStateData, targetField.Value); TraceLog.TraceDetail(String.Format("Target field {0} was set to {1} for activity {2}", TargetFieldName, targetField.Value, Name)); return true; } } catch (Exception) { // not an error condition if the target field wasn't found or the value is empty } return false; }
private Item MakeContact(WorkflowInstance workflowInstance, Item item, string name) { DateTime now = DateTime.UtcNow; // create the new contact (detached) - it will be JSON-serialized and placed into // the suggestion value field var itemID = Guid.NewGuid(); Item contact = new Item() { ID = itemID, Name = name, FolderID = item.FolderID, ItemTypeID = SystemItemTypes.Contact, ParentID = null, UserID = item.UserID, FieldValues = new List<FieldValue>() { new FieldValue() // sources { FieldName = FieldNames.Sources, ItemID = itemID, Value = name == "Mike Abbott" ? "Facebook,Directory" : name == "Mike Smith" ? "Facebook" : name == "Mike Maples" ? "Directory" : "" // hardcode for now }, new FieldValue() // facebook ID of the person { FieldName = FieldNames.FacebookID, ItemID = itemID, Value = "100003631822064" // hardcode to Mike Abbott for now }, }, Created = now, LastModified = now, }; return contact; }
/// <summary> /// This method will add a Suggestion with a RefreshEntity FieldName and State to the Suggestions /// for this ServerEntity. By convention, this will tell the UI to refresh the Entity. This method /// is called when an Activity changes the Item (e.g. a DueDate is parsed out of the Name, a Contacts /// FieldName is created, etc). /// </summary> /// <param name="workflowInstance"></param> /// <param name="entity"></param> protected void SignalEntityRefresh(WorkflowInstance workflowInstance, ServerEntity entity) { var sugg = new Suggestion() { ID = Guid.NewGuid(), EntityID = entity.ID, EntityType = entity.GetType().Name, WorkflowType = workflowInstance.WorkflowType, WorkflowInstanceID = workflowInstance.ID, State = SuggestionTypes.RefreshEntity, SuggestionType = SuggestionTypes.RefreshEntity, DisplayName = SuggestionTypes.RefreshEntity, GroupDisplayName = SuggestionTypes.RefreshEntity, Value = null, TimeSelected = null }; SuggestionsContext.Suggestions.Add(sugg); SuggestionsContext.SaveChanges(); }
/// <summary> /// Process activity data (typically created through a user selection) /// </summary> /// <param name="workflowInstance">Instance to store data into</param> /// <param name="data">Data to process</param> protected Status ProcessActivityData(WorkflowInstance workflowInstance, object data) { var suggList = data as List<Suggestion>; if (suggList != null) { // return true if a user has selected an action foreach (var sugg in suggList) { if (sugg.ReasonSelected == Reasons.Chosen || sugg.ReasonSelected == Reasons.Like) { StoreInstanceData(workflowInstance, OutputParameterName, sugg.Value); StoreInstanceData(workflowInstance, ActivityVariables.LastStateData, sugg.Value); TraceLog.TraceInfo(String.Format("User selected suggestion {0} in group {1} for activity {2}", sugg.DisplayName, sugg.GroupDisplayName, Name)); return Status.Complete; } } // return false if the user hasn't yet selected an action but suggestions were already generated // for the current state (we don't want a duplicate set of suggestions) return Status.Pending; } // if the data can't be cast into a suggestion list, there is a serious error - move the workflow forward // (otherwise it will be stuck forever) TraceLog.TraceError("Data passed in is not a list of suggestions"); return Status.Error; }
/// <summary> /// Get the value from the instance data bag, stored by key /// </summary> /// <param name="workflowInstance">Instance to retrieve the data from</param> /// <param name="key">Key of the value to return</param> /// <returns>Value for the key</returns> protected static string GetInstanceData(WorkflowInstance workflowInstance, string key) { if (key == null) return null; JsonValue dict = JsonValue.Parse(workflowInstance.InstanceData); try { return (string) dict[key]; } catch (Exception) { return null; } }
/// <summary> /// This function takes a string containing a JSON-serialized array of terms /// Each term may have zero or more variables defined (bracketed in "$()") /// At the end of execution, all variables will be bound from the workflow's InstanceData and /// the resultant (space-concatenated) string returned. If an unbound variable is found, the term is discarded. /// Ex: ["Choose from", "$(Subject)'s", "likes"] will return "Choose from Mike's likes" if Subject is /// bound to "Mike", otherwise will return "Choose from likes". /// </summary> /// <param name="workflowInstance"></param> /// <param name="stringTemplate"></param> /// <returns></returns> protected static string FormatStringTemplate(WorkflowInstance workflowInstance, string stringTemplate) { if (stringTemplate == null) return ""; try { JArray array = JArray.Parse(stringTemplate); StringBuilder returnString = new StringBuilder(); foreach (var str in array) { returnString.Append(ExpandVariables(workflowInstance, str.ToString())); returnString.Append(" "); } return returnString.ToString().Trim(); } catch (Exception) { // if the string template is not a JSON-serialized array, then just return the string as-is return stringTemplate; } }
/// <summary> /// Expand each of the variables in a format string based on the InstanceData /// Variable syntax is $(varname) /// </summary> /// <param name="workflowInstance">Workflow instance to operate over</param> /// <param name="formatExpr">Format expression to expand</param> /// <returns></returns> protected static string ExpandVariables(WorkflowInstance workflowInstance, string formatExpr) { if (formatExpr == null) return ""; StringBuilder returnString = new StringBuilder(); // successively substitute all variables until none are left, or an // unbound variable is found. If the latter, ignore the whole term string newExpr = SubstituteNextVariable(workflowInstance, formatExpr); while (newExpr != null && newExpr != formatExpr) { formatExpr = newExpr; newExpr = SubstituteNextVariable(workflowInstance, formatExpr); } if (newExpr != null) returnString.Append(newExpr); return returnString.ToString(); }
/// <summary> /// Canned Execution method for an Activity for processing Items. This method will: /// 1. validate the entity as an Item /// 2. verify the item type /// 3. check whether the target field is set on the Item and set state appropriately /// 4. check whether the user made a selection (via the data parameter) and set state appropriately /// 5. if none of this is true, add a set of suggestions from a suggestion function passed in /// </summary> /// <param name="workflowInstance">Workflow instance to process</param> /// <param name="entity">Entity to process</param> /// <param name="data">User selection data passed in</param> /// <param name="suggestionFunction">Suggestion function to invoke</param> /// <returns>return value for the Function</returns> protected Status Execute( WorkflowInstance workflowInstance, ServerEntity entity, object data, Guid expectedItemType, Func<WorkflowInstance, ServerEntity, Dictionary<string, string>, Status> suggestionFunction) { Item item = entity as Item; if (item == null) { TraceLog.TraceError("Entity is not an Item"); return Status.Error; } if (VerifyItemType(item, expectedItemType) == false) return Status.Error; // if the target field has been set, this state can terminate if (CheckTargetField(workflowInstance, item)) return Status.Complete; // check for user selection if (data != null) return ProcessActivityData(workflowInstance, data); return CreateSuggestions(workflowInstance, entity, suggestionFunction); }
/// <summary> /// Create a set of suggestions from the results of calling the suggestion function /// </summary> /// <param name="workflowInstance">Workflow instance to operate over</param> /// <param name="item">Item to process</param> /// <param name="suggestionFunction">Suggestion generation function</param> /// <returns>Complete if an exact match was found, Pending if multiple suggestions created</returns> protected Status CreateSuggestions( WorkflowInstance workflowInstance, ServerEntity entity, Func<WorkflowInstance, ServerEntity, Dictionary<string,string>, Status> suggestionFunction) { // analyze the item for possible suggestions var suggestions = new Dictionary<string, string>(); Status status = suggestionFunction.Invoke(workflowInstance, entity, suggestions); TraceLog.TraceDetail(String.Format("Retrieved {0} suggestions from activity {1}", suggestions.Count, Name)); // if the function completed with an error, or without generating any data, return (this is typically a fail-fast state) if (status == Status.Error || suggestions.Count == 0) return status; // if an "exact match" was discovered without user input, store it now and return if (status == Status.Complete && suggestions.Count == 1) { string s = null; foreach (var value in suggestions.Values) s = value; StoreInstanceData(workflowInstance, ActivityVariables.LastStateData, s); StoreInstanceData(workflowInstance, OutputParameterName, s); TraceLog.TraceDetail(String.Format("Exact match {0} was found for activity {1}", s, Name)); return status; } // construct the group display name string groupDisplayName = GroupDisplayName; if (groupDisplayName == null) groupDisplayName = workflowInstance.State; else groupDisplayName = FormatStringTemplate(workflowInstance, groupDisplayName); // get the suggestion parent ID if available var parentID = GetInstanceData(workflowInstance, ActivityVariables.ParentID); // add suggestions received from the suggestion function try { int num = 0; foreach (var s in suggestions.Keys) { // limit to four suggestions if (num++ == 4) break; var sugg = new Suggestion() { ID = Guid.NewGuid(), ParentID = parentID == null ? (Guid?) null : new Guid(parentID), EntityID = entity.ID, EntityType = entity.GetType().Name, WorkflowType = workflowInstance.WorkflowType, WorkflowInstanceID = workflowInstance.ID, State = workflowInstance.State, SuggestionType = SuggestionType, DisplayName = s, GroupDisplayName = groupDisplayName, SortOrder = num, Value = suggestions[s], TimeSelected = null }; SuggestionsContext.Suggestions.Add(sugg); TraceLog.TraceDetail(String.Format("Created suggestion {0} in group {1} for activity {2}", s, groupDisplayName, Name)); } SuggestionsContext.SaveChanges(); return status; } catch (Exception ex) { TraceLog.TraceException("Activity execution failed", ex); return Status.Error; } }
private Status GenerateSuggestions(WorkflowInstance workflowInstance, ServerEntity entity, Dictionary<string, string> suggestionList) { Item item = entity as Item; if (item == null) { TraceLog.TraceError("Entity is not an Item"); return Status.Error; } // if an intent was already matched, return it now var intentFV = item.GetFieldValue(ExtendedFieldNames.Intent); if (intentFV != null && !String.IsNullOrWhiteSpace(intentFV.Value)) { var wt = SuggestionsContext.WorkflowTypes.FirstOrDefault(t => t.Type == intentFV.Value); if (wt != null) { suggestionList[wt.Type] = wt.Type; return Status.Complete; } } // run NLP engine over the task name to extract intent (verb / noun) as well as a subject hint string name = item.Name; string verb = null; string noun = null; string subject = null; try { Phrase phrase = new Phrase(name); if (phrase.Task != null) { verb = phrase.Task.Verb.ToLower(); noun = phrase.Task.Article.ToLower(); subject = phrase.Task.Subject; if (!String.IsNullOrWhiteSpace(subject)) StoreInstanceData(workflowInstance, ActivityVariables.SubjectHint, subject); } } catch (Exception ex) { TraceLog.TraceException("Could not initialize NLP engine", ex); } // if NLP failed (e.g. data file not found), do "poor man's NLP" - assume a structure like <verb> <noun> [{for,with} <subject>] if (verb == null) { string sentence = name.ToLower(); // remove extra whitespace and filler words StringBuilder sb = new StringBuilder(); foreach (var word in sentence.Split(new char[] { ' ', '\n', '\t' }, StringSplitOptions.RemoveEmptyEntries)) { bool add = true; foreach (var filler in "a;an;the".Split(';')) if (word == filler) { add = false; break; } if (add) sb.AppendFormat("{0} ", word); } sentence = sb.ToString().Trim(); // poor man's NLP - assume first word is verb, second word is noun string[] parts = sentence.Split(' '); if (parts.Length >= 2) { verb = parts[0]; noun = parts[1]; } if (parts.Length >= 4) { if (parts[2] == "for" || parts[2] == "with") { // capitalize and store the word following "for" in the SubjectHint workflow parameter subject = parts[3]; subject = subject.Substring(0, 1).ToUpper() + subject.Substring(1); StoreInstanceData(workflowInstance, ActivityVariables.SubjectHint, subject); } } } // try to find an intent that exactly matches the verb/noun extracted by the NLP step Intent intent = SuggestionsContext.Intents.FirstOrDefault(i => i.Verb == verb && i.Noun == noun); if (intent != null) { var wt = SuggestionsContext.WorkflowTypes.FirstOrDefault(t => t.Type == intent.WorkflowType); if (wt != null) { suggestionList[intent.WorkflowType] = wt.Type; return Status.Complete; } } // get a list of all approximate matches and surface as suggestions to the user var intentList = SuggestionsContext.Intents.Where(i => i.Verb == verb || i.Noun == noun); foreach (var i in intentList) suggestionList[i.WorkflowType] = i.WorkflowType; // if there are no suggestions, we can terminate this workflow if (suggestionList.Count == 0) { TraceLog.TraceError("No possible intents were found, terminating workflow"); return Status.Error; } return Status.Pending; }
/// <summary> /// Store a value for a key on the instance data bag /// </summary> /// <param name="workflowInstance">Instance to retrieve the data from</param> /// <param name="key">Key to store under</param> /// <param name="data">Data to store under the key</param> protected void StoreInstanceData(WorkflowInstance workflowInstance, string key, string data) { if (key == null) return; JsonValue dict = JsonValue.Parse(workflowInstance.InstanceData); dict[key] = data; workflowInstance.InstanceData = dict.ToString(); SuggestionsContext.SaveChanges(); }
/// <summary> /// This function finds the next occurrence of a variable in the format $(varname) /// It will attempt to substitute a value from the workflow's instance data, using /// varname as the key /// </summary> /// <param name="workflowInstance">Workflow instance to operate over</param> /// <param name="formatString">Format string that contains variables to substitute</param> /// <returns>null if the variable wasn't bound, the new string if it was (or if no variables were found)</returns> private static string SubstituteNextVariable(WorkflowInstance workflowInstance, string formatString) { if (formatString == null) return null; StringBuilder returnString = new StringBuilder(); int pos = formatString.IndexOf("$("); if (pos < 0) return formatString; int end = formatString.IndexOf(')', pos); if (end < 0) return formatString; string variableName = formatString.Substring(pos + 2, Math.Max(0, end - pos - 2)); returnString.Append(formatString.Substring(0, pos)); string value = GetInstanceData(workflowInstance, variableName); if (value == null) return null; // the variable wasn't bound returnString.Append(value); returnString.Append(formatString.Substring(end + 1)); return returnString.ToString(); }
private Status GenerateSuggestions(WorkflowInstance workflowInstance, ServerEntity entity, Dictionary<string, string> suggestionList) { Item item = entity as Item; if (item == null) { TraceLog.TraceError("Entity is not an Item"); return Status.Complete; } // HACK: hardcode names for now until the graph queries are in place foreach (var subject in "Mike Maples;Mike Smith;Mike Abbott".Split(';')) { Item contact = MakeContact(workflowInstance, item, subject); suggestionList[subject] = JsonSerializer.Serialize(contact); } // inexact match return Status.Pending; }
/// <summary> /// Factory method to construct a typed activity based on its definition /// </summary> /// <param name="definition">Json value corresponding to activity definition</param> /// <param name="instance">Workflow instance</param> /// <returns>typed activity</returns> public static WorkflowActivity CreateActivity(JObject definition, WorkflowInstance instance) { string typeName = (string) definition["Name"]; WorkflowActivity activity = CreateActivity(typeName); if (activity == null) return null; foreach (var prop in definition) { switch (prop.Value.Type) { case JTokenType.Object: // a sub-object (typically a nested activity) - store as a string activity.InputParameters[prop.Key] = prop.Value.ToString(); break; case JTokenType.Array: // treat as a string template and format appropriately activity.InputParameters[prop.Key] = FormatStringTemplate(instance, prop.Value.ToString()); break; default: // treat as a string and expand any variables activity.InputParameters[prop.Key] = ExpandVariables(instance, (string)prop.Value.ToString()); break; } } return activity; }
private Status CreateContact(WorkflowInstance workflowInstance, Item item) { DateTime now = DateTime.UtcNow; FieldValue contactsField = item.GetFieldValue(TargetFieldName, true); Guid listID = contactsField.Value != null ? new Guid(contactsField.Value) : Guid.NewGuid(); // if the contacts sublist under the item doesn't exist, create it now if (contactsField.Value == null) { Item list = new Item() { ID = listID, Name = TargetFieldName, IsList = true, FolderID = item.FolderID, ItemTypeID = SystemItemTypes.Reference, ParentID = item.ID, UserID = item.UserID, Created = now, LastModified = now, }; contactsField.Value = listID.ToString(); try { UserContext.Items.Add(list); UserContext.SaveChanges(); } catch (Exception ex) { TraceLog.TraceException("Creating Contact sublist failed", ex); return Status.Error; } } // get the subject out of the InstanceData bag Item contact = null; try { var contactString = GetInstanceData(workflowInstance, OutputParameterName); contact = JsonSerializer.Deserialize<Item>(contactString); } catch (Exception ex) { TraceLog.TraceException("Deserializing contact failed", ex); return Status.Error; } // update the contact if it already exists, otherwise add a new contact try { Item dbContact = UserContext.Items.Include("FieldValues").Single(c => c.ID == contact.ID); foreach (var fv in contact.FieldValues) { // add or update each of the fieldvalues var dbfv = dbContact.GetFieldValue(fv.FieldName, true); dbfv.Copy(fv); } dbContact.LastModified = now; } catch (Exception) { Folder folder = FindDefaultFolder(contact.UserID, contact.ItemTypeID); if (folder != null) contact.FolderID = folder.ID; UserContext.Items.Add(contact); } try { UserContext.SaveChanges(); } catch (Exception ex) { TraceLog.TraceException("Creating or adding Contact failed", ex); return Status.Error; } // add a contact reference to the contact list Guid refID = Guid.NewGuid(); var contactRef = new Item() { ID = refID, Name = contact.Name, ItemTypeID = SystemItemTypes.Reference, FolderID = item.FolderID, ParentID = listID, UserID = contact.UserID, Created = now, LastModified = now, FieldValues = new List<FieldValue>() { new FieldValue() { FieldName = FieldNames.EntityRef, ItemID = refID, Value = contact.ID.ToString() }, new FieldValue() { FieldName = FieldNames.EntityType, ItemID = refID, Value = EntityTypes.Item } } }; try { UserContext.Items.Add(contactRef); UserContext.SaveChanges(); } catch (Exception ex) { TraceLog.TraceException("Creating Contact reference failed", ex); return Status.Error; } // add a Suggestion with a RefreshEntity FieldName to the list, to tell the UI that the // workflow changed the Item SignalEntityRefresh(workflowInstance, item); return Status.Complete; }
/// <summary> /// Start a workflow of a certain type, passing it an entity and some instance data to start /// </summary> /// <param name="userContext"></param> /// <param name="suggestionsContext"></param> /// <param name="type">String representing the workflow type</param> /// <param name="entity">Entity to associate with the workflow</param> /// <param name="instanceData">Instance data to pass into the workflow</param> public static void StartWorkflow(UserStorageContext userContext, SuggestionsStorageContext suggestionsContext, string type, ServerEntity entity, string instanceData) { WorkflowInstance instance = null; try { Workflow workflow = null; // get the workflow definition out of the database try { var wt = suggestionsContext.WorkflowTypes.Single(t => t.Type == type); workflow = JsonSerializer.Deserialize<Workflow>(wt.Definition); } catch (Exception ex) { TraceLog.TraceException("Could not find or deserialize workflow definition", ex); return; } // don't start a workflow with no states if (workflow.States.Count == 0) return; // store the database contexts workflow.UserContext = userContext; workflow.SuggestionsContext = suggestionsContext; // create the new workflow instance and store in the workflow DB DateTime now = DateTime.Now; instance = new WorkflowInstance() { ID = Guid.NewGuid(), EntityID = entity.ID, EntityName = entity.Name, WorkflowType = type, State = workflow.States[0].Name, InstanceData = instanceData ?? "{}", Created = now, LastModified = now, LockedBy = WorkflowHost.Me, }; suggestionsContext.WorkflowInstances.Add(instance); suggestionsContext.SaveChanges(); TraceLog.TraceInfo("Starting workflow " + type); // invoke the workflow and process steps until workflow is blocked for user input or is done workflow.Run(instance, entity); // unlock the workflowinstance instance.LockedBy = null; suggestionsContext.SaveChanges(); } catch (Exception ex) { TraceLog.TraceException("StartWorkflow failed", ex); if (instance != null && instance.LockedBy == WorkflowHost.Me) { // unlock the workflowinstance instance.LockedBy = null; suggestionsContext.SaveChanges(); } } }