I am running a SharePoint workflow programmatically from a SharePoint event receiver by calling workflowManager.StartWorkflow().
The workflow is setting some workflow variables during execution. Is there a way to access the last value of these variables after the workflow has terminated (e.g., the call to StartWorkflow() returns)?
Here's my sample code that demonstrates my intent:
public override void ItemAdded(SPItemEventProperties properties)
{
SPWorkflow workflow = null;
SPWorkflowManager workflowManager = null;
try
{
base.ItemAdded(properties);
workflowManager = properties.OpenWeb().Site.WorkflowManager;
var workflowAssociation = properties.ListItem.ParentList.WorkflowAssociations[0];
workflow = workflowManager.StartWorkflow(properties.ListItem, workflowAssociation, "<Data></Data>");
// I can read any fields that were updated by the WF
SPListItem item = properties.ListItem.ParentList.GetItemById(properties.ListItemId);
string validationResult = (string) item["ValidationResult"];
// how can I access any workflow variables created during execution?
}
catch (Exception ex)
{
Debug.WriteLine(ex);
if (workflow != null && workflowManager != null)
{
SPWorkflowManager.CancelWorkflow(workflow);
}
throw;
}
}
Maybe write them out to a List? This might your least messy option.
Alternatively, there's a SPWorkflow.Xml property that "returns a string that represents the workflow instance in XML format" but I've never tried it out. Though passing this back into SharePoint could be awkward.
Related
I have multiple subscriptions from Cloud PubSub to read based on certain prefix pattern using Apache Beam. I extend PTransform class and implement expand() method to read from multiple subscriptions and do Flatten transformation to the PCollectionList (multiple PCollection on from each subscription). I have a problem to pass subscription prefix as ValueProvider into the expand() method, since expand() is called on template creation time, not when launching the job. However, if I only use 1 subscription, I can pass ValueProvider into PubsubIO.readStrings().fromSubscription().
Here's some sample code.
public class MultiPubSubIO extends PTransform<PBegin, PCollection<PubsubMessage>> {
private ValueProvider<String> prefixPubsub;
public MultiPubSubIO(#Nullable String name, ValueProvider<String> prefixPubsub) {
super(name);
this.prefixPubsub = prefixPubsub;
}
#Override
public PCollection<PubsubMessage> expand(PBegin input) {
List<String> myList = null;
try {
// prefixPubsub.get() will return error
myList = PubsubHelper.getAllSubscription("projectID", prefixPubsub.get());
} catch (Exception e) {
LogHelper.error(String.format("Error getting list of subscription : %s",e.toString()));
}
List<PCollection<PubsubMessage>> collectionList = new ArrayList<PCollection<PubsubMessage>>();
if(myList != null && !myList.isEmpty()){
for(String subs : myList){
PCollection<PubsubMessage> pCollection = input
.apply("ReadPubSub", PubsubIO.readMessagesWithAttributes().fromSubscription(this.prefixPubsub));
collectionList.add(pCollection);
}
PCollection<PubsubMessage> pubsubMessagePCollection = PCollectionList.of(collectionList)
.apply("FlattenPcollections", Flatten.pCollections());
return pubsubMessagePCollection;
} else {
LogHelper.error(String.format("No subscription with prefix %s found", prefixPubsub));
return null;
}
}
public static MultiPubSubIO read(ValueProvider<String> prefixPubsub){
return new MultiPubSubIO(null, prefixPubsub);
}
}
So I'm thinking of how to use the same way PubsubIO.read().fromSubscription() to read from ValueProvider. Or am I missing something?
Searched links:
extract-value-from-valueprovider-in-apache-beam - Answer talked about using DoFn, while I need PTransform that receives PBegin.
Unfortunately this is not possible currently:
It is not possible for the value of a ValueProvider to affect transform expansion - at expansion time, it is unknown; by the time it is known, the pipeline shape is already fixed.
There is currently no transform like PubsubIO.read() that can accept a PCollection of topic names. Eventually there will be (it is enabled by Splittable DoFn), but it will take a while - nobody is working on this currently.
You can use MultipleReadFromPubSub from apache beam io module https://beam.apache.org/releases/pydoc/2.27.0/_modules/apache_beam/io/gcp/pubsub.html
topic_1 = PubSubSourceDescriptor('projects/myproject/topics/a_topic')
topic_2 = PubSubSourceDescriptor(
'projects/myproject2/topics/b_topic',
'my_label',
'my_timestamp_attribute')
subscription_1 = PubSubSourceDescriptor(
'projects/myproject/subscriptions/a_subscription')
results = pipeline | MultipleReadFromPubSub(
[topic_1, topic_2, subscription_1])
I have created a generic inquiry in Acumatica that returns work orders that are open. We know that there is a print action that can be applied however, we want to print out multiple instances of the work orders. So we do not have to print them individually. Is there currently a solution for mass printing or can someone point me in the right direction of maybe a work around? I know there is a mass action print all function that can be found however, it does not do anything.
Just like in any other action redirecting users to the generated report(s), the Service Orders' Print Service Order button throws a PXReportRequiredException to open generated report in new window:
public class ServiceOrderEntry : PXGraph<ServiceOrderEntry, FSServiceOrder>
{
...
public PXAction<FSServiceOrder> printServiceOrder;
[PXUIField]
[PXButton]
public virtual void PrintServiceOrder()
{
if (this.IsDirty) Actions.PressSave();
if (ServiceOrderRecords.Current == null) return;
Dictionary<string, string> serviceOrderParameters =
GetServiceOrderParameters(ServiceOrderRecords.Current, false);
if (serviceOrderParameters.Count != 0)
{
throw new PXReportRequiredException(serviceOrderParameters,
"SD641000", PXBaseRedirectException.WindowMode.NewWindow, string.Empty);
}
}
...
}
If you add Service Orders' Print Service Order action as a mass action to Generic Inquiry (as shown on the screenshots below), the GI mass action will generate and open Service Order report form only for the first selected Service Order. Processing of any consequent Service Order will not be possible due to the PXReportRequiredException thrown to show the report form generated for the first Service Order.
In order to merge Service Order report forms generated for several Service Orders into a single report, you should create a custom processing screen and use the AddSibling method on a PXReportRequiredException instance. The AddSibling method will append the reports generated for 2nd+ Service Order to the PXReportRequiredException instance initially created for the first processed Service Order. After all selected Service Orders has been processed, a single PXReportRequiredException will be thrown to redirect the user to Report Viewer displaying all generated reports at once.
public class PrintServiceOrderProcess : PXGraph<PrintServiceOrderProcess>
{
public PXCancel<FSServiceOrder> Cancel;
public PXProcessing<FSServiceOrder> ServiceOrderRecords;
public PrintServiceOrderProcess()
{
ServiceOrderRecords.SetProcessDelegate(list => PrintServiceOrders(list));
}
public static void PrintServiceOrders(IEnumerable<FSServiceOrder> list)
{
PXReportRequiredException ex = null;
foreach (var order in list)
{
Dictionary<string, string> parameters = new Dictionary<string, string>();
string srvOrdType = SharedFunctions
.GetFieldName<FSServiceOrder.srvOrdType>(true);
string refNbr = SharedFunctions
.GetFieldName<FSServiceOrder.refNbr>(true);
parameters[srvOrdType] = order.SrvOrdType;
parameters[refNbr] = order.RefNbr;
if (ex == null)
{
ex = new PXReportRequiredException(parameters, "SD641000", "SD641000");
}
else
{
ex.AddSibling("SD641000", parameters, false);
}
}
if (ex != null) throw ex;
}
}
I have class to keep PowerShell session. So that I can access the powershell session without to create a new session . Below is my snippet code
public class PowerShellSession : IHttpHandler, IRequiresSessionState
{
public bool IsReusable
{
get
{
return false;
}
}
public void ProcessRequest(HttpContext context)
{
throw new NotImplementedException();
}
public PowerShell PowerShell2010()
{
if(HttpContext.Current.Session == null)
{
WSManConnectionInfoSession connExch = new WSManConnectionInfoSession();
var session = connExch.GetExchangeConnectionSession(2010);
Runspace runspace = RunspaceFactory.CreateRunspace(session);
runspace.Open();
PowerShell Shell = PowerShell.Create();
Shell.Runspace = runspace;
HttpContext.Current.Session["PowerShell2010"] = Shell;
return Shell;
}
if (HttpContext.Current.Session["PowerShell2010"] != null)
{
WSManConnectionInfoSession connExch = new WSManConnectionInfoSession();
var session = connExch.GetExchangeConnectionSession(2010);
Runspace runspace = RunspaceFactory.CreateRunspace(session);
runspace.Open();
PowerShell Shell = PowerShell.Create();
Shell.Runspace = runspace;
HttpContext.Current.Session["PowerShell2010"] = Shell;
return Shell;
}
else
{
return (PowerShell)HttpContext.Current.Session["PowerShell2010"];
}
}
}
The problem is my code always return "Object Reference not set to an instance of an object" when I try to set value to session.
Here the code to set value on session
HttpContext.Current.Session["PowerShell2010"] = Shell;
Did I do something wrong?
I have no experience using Powershell. Having said that, a big part of the problem seems to be that your if() statements are incorrect.
First you check if(HttpContext.Current.Session == null) which is TRUE if the Session object can not be found in the current context. But then you proceed by attempting to use that Session object anyway, so no wonder you get the error that you are getting.
The next one seems wrong too: if (HttpContext.Current.Session["PowerShell2010"] != null), which would be TRUE if a previous attempt to store a Powershell object was successful. But then you proceed to create and store a new Powershell object, which totally defeats the cache that you apparently wanted. You need to replace this with == null, assuming that you will find a way to get to the Session object in the first place.
And last but not least, to have a bigger chance of getting to the HTTP Session object:
make sure that Session state is enabled in your web server and/or Web.Config files;
run as much of the code above in an MVC Controller class, instead of in a Type Library class or something like that. Or pass the HTTP Session object from the MVC Controller Action-method into the Type Library method using a parameter.
There are some database operations I need to execute before the end of the final attempt of my Hangfire background job (I need to delete the database record related to the job)
My current job is set with the following attribute:
[AutomaticRetry(Attempts = 5, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
With that in mind, I need to determine what the current attempt number is, but am struggling to find any documentation in that regard from a Google search or Hangfire.io documentation.
Simply add PerformContext to your job method; you'll also be able to access your JobId from this object. For attempt number, this still relies on magic strings, but it's a little less flaky than the current/only answer:
public void SendEmail(PerformContext context, string emailAddress)
{
string jobId = context.BackgroundJob.Id;
int retryCount = context.GetJobParameter<int>("RetryCount");
// send an email
}
(NB! This is a solution to the OP's problem. It does not answer the question "How to get the current attempt number". If that is what you want, see the accepted answer for instance)
Use a job filter and the OnStateApplied callback:
public class CleanupAfterFailureFilter : JobFilterAttribute, IServerFilter, IApplyStateFilter
{
public void OnStateApplied(ApplyStateContext context, IWriteOnlyTransaction transaction)
{
try
{
var failedState = context.NewState as FailedState;
if (failedState != null)
{
// Job has finally failed (retry attempts exceeded)
// *** DO YOUR CLEANUP HERE ***
}
}
catch (Exception)
{
// Unhandled exceptions can cause an endless loop.
// Therefore, catch and ignore them all.
// See notes below.
}
}
public void OnStateUnapplied(ApplyStateContext context, IWriteOnlyTransaction transaction)
{
// Must be implemented, but can be empty.
}
}
Add the filter directly to the job function:
[CleanupAfterFailureFilter]
public static void MyJob()
or add it globally:
GlobalJobFilters.Filters.Add(new CleanupAfterFailureFilter ());
or like this:
var options = new BackgroundJobServerOptions
{
FilterProvider = new JobFilterCollection { new CleanupAfterFailureFilter () };
};
app.UseHangfireServer(options, storage);
Or see http://docs.hangfire.io/en/latest/extensibility/using-job-filters.html for more information about job filters.
NOTE: This is based on the accepted answer: https://stackoverflow.com/a/38387512/2279059
The difference is that OnStateApplied is used instead of OnStateElection, so the filter callback is invoked only after the maximum number of retries. A downside to this method is that the state transition to "failed" cannot be interrupted, but this is not needed in this case and in most scenarios where you just want to do some cleanup after a job has failed.
NOTE: Empty catch handlers are bad, because they can hide bugs and make them hard to debug in production. It is necessary here, so the callback doesn't get called repeatedly forever. You may want to log exceptions for debugging purposes. It is also advisable to reduce the risk of exceptions in a job filter. One possibility is, instead of doing the cleanup work in-place, to schedule a new background job which runs if the original job failed. Be careful to not apply the filter CleanupAfterFailureFilter to it, though. Don't register it globally, or add some extra logic to it...
You can use OnPerforming or OnPerformed method of IServerFilter if you want to check the attempts or if you want you can just wait on OnStateElection of IElectStateFilter. I don't know exactly what requirement you have so it's up to you. Here's the code you want :)
public class JobStateFilter : JobFilterAttribute, IElectStateFilter, IServerFilter
{
public void OnStateElection(ElectStateContext context)
{
// all failed job after retry attempts comes here
var failedState = context.CandidateState as FailedState;
if (failedState == null) return;
}
public void OnPerforming(PerformingContext filterContext)
{
// do nothing
}
public void OnPerformed(PerformedContext filterContext)
{
// you have an option to move all code here on OnPerforming if you want.
var api = JobStorage.Current.GetMonitoringApi();
var job = api.JobDetails(filterContext.BackgroundJob.Id);
foreach(var history in job.History)
{
// check reason property and you will find a string with
// Retry attempt 3 of 3: The method or operation is not implemented.
}
}
}
How to add your filter
GlobalJobFilters.Filters.Add(new JobStateFilter());
----- or
var options = new BackgroundJobServerOptions
{
FilterProvider = new JobFilterCollection { new JobStateFilter() };
};
app.UseHangfireServer(options, storage);
Sample output :
I have the following POST edit action method, which mainly perform two Update actions:-
Edit the object on the external system suing API calls.
Edit the object on our system database.
[HttpPost]
public ActionResult Create(RackJoin rj, FormCollection formValues)
{string controllername = RouteData.Values["controller"].ToString();
if (ModelState.IsValid)
{ var message = "";
var status = "";
long assetid = new long();
XmlDocument doc = new XmlDocument();
using (var client = new WebClient())
{
var query = HttpUtility.ParseQueryString(string.Empty);
foreach (string key in formValues)
{
query[key] = this.Request.Form[key];
}
query["username"] = System.Web.Configuration.WebConfigurationManager.AppSettings["ApiUserName"];
query["password"] = System.Web.Configuration.WebConfigurationManager.AppSettings["ApiPassword"];
string apiurl = System.Web.Configuration.WebConfigurationManager.AppSettings["ApiURL"];
var url = new UriBuilder(apiurl);
url.Query = query.ToString();
try
{
string xml = client.DownloadString(url.ToString());
doc.LoadXml(xml);
status = doc.SelectSingleNode("/operation/operationstatus").InnerText;
message = doc.SelectSingleNode("/operation/message").InnerText;
}
catch (WebException ex)
{
ModelState.AddModelError(string.Empty, "Error occurred:" + ex.InnerException);
}
}
if (status.ToUpper() == "SUCCESS")
{
repository.InsertOrUpdateRack(rj.Rack, User.Identity.Name, rj.Resource.RESOURCEID);
repository.Save();
return RedirectToAction("Index");
}
else
{
ModelState.AddModelError(string.Empty, message.ToString());
}
}
}
catch (DbUpdateConcurrencyException ex)
{
As shown in the above code I will not do a repository.save() to update the object on our system, unless the API return a “success”.
But currently I am facing the following problem:-
If the API return a “success” but a concurrency exception occurred, then the API will update the object on the external system, but the object will not be updated on our system?
So is there a way to handle this situation?
There's no easy way to solve this situation. One way to handle it would be to ask the designers of the external API expose a method allowing to commit the transaction done in a previous call. Basically your first call will make modifications to the external system but with some boolean flag indicating that those changes are still pending. Then you update your system and in case of success you would call the external API to flag the data from pending to valid.
If you have no control over the external API and it makes the changes to the data from the first call irreversible, then I am afraid that you do not have much choices left. You might remember the state of the object you are modifying on the external system before calling the API and in case of an exception on your system, revert back to the previous state by calling the API with the previous values.