Modelar el comportamiento de la cancelación en los flujos de trabajo

Las actividades se pueden cancelar dentro de un flujo de trabajo, por ejemplo, mediante una actividad Parallel que cancele las bifurcaciones incompletas cuando su propiedad CompletionCondition se evalúe como true o desde fuera del flujo de trabajo, si el host llama al método Cancel. Para proporcionar un control de la cancelación, los autores del flujo de trabajo pueden utilizar la actividad CancellationScope, la actividad CompensableActivity o crear actividades personalizadas que proporcionen lógica de cancelación. En este tema se proporciona información general sobre la cancelación en flujos de trabajo.

Cancelación, compensación y transacciones

Las transacciones ofrecen a la aplicación la posibilidad de anular (revertir) todos los cambios ejecutados dentro de la transacción si se produce algún error en cualquier momento del proceso de transacción. Sin embargo, no todos los trabajos que puede que resulte necesario cancelar o deshacer son adecuados para las transacciones, como los trabajos de ejecución prolongada o aquellos que no implican recursos transaccionales. La compensación proporciona un modelo para deshacer un trabajo no transaccional completado previamente si se produce un error en el flujo de trabajo posteriormente. La cancelación proporciona un modelo para que el flujo de trabajo y los autores de la actividad puedan controlar un trabajo no transaccional que no se ha completado. Si una actividad no ha completado su ejecución y se cancela, se invocará su lógica de la cancelación si está disponible.

Nota

Para más información sobre las transacciones y la compensación, consulte Transacciones y Compensación.

Usar CancellationScope

La actividad CancellationScope tiene dos secciones que pueden contener actividades secundarias: Body y CancellationHandler. La propiedad Body es donde se colocan las actividades que constituyen la lógica de la actividad y la propiedad CancellationHandler es donde se colocan las actividades que proporcionan la lógica de cancelación para la actividad. Una actividad solo puede cancelar si no se ha completado. En el caso de la actividad CancellationScope, la finalización hace referencia a la realización de las actividades de la propiedad Body. Si se programa una solicitud de cancelación y no se han completado las actividades de la propiedad Body, el objeto CancellationScope se marcará como Canceled y las actividades CancellationHandler se ejecutarán.

Cancelar un flujo de trabajo desde el host

Un host puede cancelar un flujo de trabajo llamando al método Cancel de la instancia de WorkflowApplication que hospeda el flujo de trabajo. En el ejemplo siguiente, se crea un flujo de trabajo con un objeto CancellationScope. Se invoca el flujo de trabajo y, a continuación, el host llama al método Cancel. La ejecución principal del flujo de trabajo se detiene, se invoca la propiedad CancellationHandler del objeto CancellationScope y, a continuación, el flujo de trabajo se completa con el estado Canceled.

Activity wf = new CancellationScope
{
    Body = new Sequence
    {
        Activities =
        {
            new WriteLine
            {
                Text = "Starting the workflow."
            },
            new Delay
            {
                Duration = TimeSpan.FromSeconds(5)
            },
            new WriteLine
            {
                Text = "Ending the workflow."
            }
        }
    },
    CancellationHandler = new WriteLine
    {
        Text = "CancellationHandler invoked."
    }
};

// Create a WorkflowApplication instance.
WorkflowApplication wfApp = new WorkflowApplication(wf);

// Subscribe to any desired workflow lifecycle events.
wfApp.Completed = delegate(WorkflowApplicationCompletedEventArgs e)
{
    if (e.CompletionState == ActivityInstanceState.Faulted)
    {
        Console.WriteLine("Workflow {0} Terminated.", e.InstanceId);
        Console.WriteLine("Exception: {0}\n{1}",
            e.TerminationException.GetType().FullName,
            e.TerminationException.Message);
    }
    else if (e.CompletionState == ActivityInstanceState.Canceled)
    {
        Console.WriteLine("Workflow {0} Canceled.", e.InstanceId);
    }
    else
    {
        Console.WriteLine("Workflow {0} Completed.", e.InstanceId);
    }
};

// Run the workflow.
wfApp.Run();

Thread.Sleep(TimeSpan.FromSeconds(1));

wfApp.Cancel();

Cuando se invoca este flujo de trabajo, en la consola se muestra el siguiente resultado.

Iniciando el flujo de trabajo.
CancellationHandler invocado.Flujo de trabajo b30ebb30-df46-4d90-a211-e31c38d8db3c cancelado.

Nota

Cuando una actividad CancellationScope se cancela y se invoca la propiedad CancellationHandler, es responsabilidad del autor del flujo de trabajo determinar el progreso de la actividad antes de cancelarse para proporcionar la lógica de cancelación adecuada. La propiedad CancellationHandler no proporciona ninguna información sobre el progreso de la actividad cancelada.

Un flujo de trabajo también se puede cancelar desde el host si una excepción no controlada traspasa la raíz del flujo de trabajo y el controlador de la propiedad OnUnhandledException devuelve Cancel. En este ejemplo el flujo de trabajo se inicia y, a continuación, genera una ApplicationException. El flujo de trabajo no controla la excepción y, en consecuencia, se invoca al controlador de la propiedad OnUnhandledException. El controlador indica al tiempo de ejecución que cancele el flujo de trabajo y se invoca la propiedad CancellationHandler de la actividad CancellationScope actualmente en ejecución.

Activity wf = new CancellationScope
{
    Body = new Sequence
    {
        Activities =
        {
            new WriteLine
            {
                Text = "Starting the workflow."
            },
            new Throw
            {
                 Exception = new InArgument<Exception>((env) =>
                     new ApplicationException("An ApplicationException was thrown."))
            },
            new WriteLine
            {
                Text = "Ending the workflow."
            }
        }
    },
    CancellationHandler = new WriteLine
    {
        Text = "CancellationHandler invoked."
    }
};

// Create a WorkflowApplication instance.
WorkflowApplication wfApp = new WorkflowApplication(wf);

// Subscribe to any desired workflow lifecycle events.
wfApp.OnUnhandledException = delegate(WorkflowApplicationUnhandledExceptionEventArgs e)
{
    // Display the unhandled exception.
    Console.WriteLine("OnUnhandledException in Workflow {0}\n{1}",
        e.InstanceId, e.UnhandledException.Message);

    // Instruct the runtime to cancel the workflow.
    return UnhandledExceptionAction.Cancel;
};

wfApp.Completed = delegate(WorkflowApplicationCompletedEventArgs e)
{
    if (e.CompletionState == ActivityInstanceState.Faulted)
    {
        Console.WriteLine("Workflow {0} Terminated.", e.InstanceId);
        Console.WriteLine("Exception: {0}\n{1}",
            e.TerminationException.GetType().FullName,
            e.TerminationException.Message);
    }
    else if (e.CompletionState == ActivityInstanceState.Canceled)
    {
        Console.WriteLine("Workflow {0} Canceled.", e.InstanceId);
    }
    else
    {
        Console.WriteLine("Workflow {0} Completed.", e.InstanceId);
    }
};

// Run the workflow.
wfApp.Run();

Cuando se invoca este flujo de trabajo, en la consola se muestra el siguiente resultado.

Iniciando el flujo de trabajo.
OnUnhandledException en el flujo de trabajo 6bb2d5d6-f49a-4c6d-a988-478afb86dbe9Se generó una excepción ApplicationException.CancellationHandler invocado.Flujo de trabajo 6bb2d5d6-f49a-4c6d-a988-478afb86dbe9 cancelado.

Cancelar una actividad desde dentro de un flujo de trabajo

Una actividad también puede cancelarse mediante su elemento primario. Por ejemplo, si una actividad Parallel tiene varias bifurcaciones en ejecución y su propiedad CompletionCondition se evalúa como true, se cancelarán sus bifurcaciones incompletas. En este ejemplo, se crea una actividad Parallel con dos bifurcaciones. Su propiedad CompletionCondition está establecida como true, por lo que la actividad Parallel se completa cuando lo hace una de sus bifurcaciones. En este ejemplo, la bifurcación 2 se completa, por lo que se cancela la bifurcación 1.

Activity wf = new Parallel
{
    CompletionCondition = true,
    Branches =
    {
        new CancellationScope
        {
            Body = new Sequence
            {
                Activities =
                {
                    new WriteLine
                    {
                        Text = "Branch 1 starting."
                    },
                    new Delay
                    {
                         Duration = TimeSpan.FromSeconds(2)
                    },
                    new WriteLine
                    {
                        Text = "Branch 1 complete."
                    }
                }
            },
            CancellationHandler = new WriteLine
            {
                Text = "Branch 1 canceled."
            }
        },
        new WriteLine
        {
            Text = "Branch 2 complete."
        }
    }
};

// Create a WorkflowApplication instance.
WorkflowApplication wfApp = new WorkflowApplication(wf);

wfApp.Completed = delegate(WorkflowApplicationCompletedEventArgs e)
{
    if (e.CompletionState == ActivityInstanceState.Faulted)
    {
        Console.WriteLine("Workflow {0} Terminated.", e.InstanceId);
        Console.WriteLine("Exception: {0}\n{1}",
            e.TerminationException.GetType().FullName,
            e.TerminationException.Message);
    }
    else if (e.CompletionState == ActivityInstanceState.Canceled)
    {
        Console.WriteLine("Workflow {0} Canceled.", e.InstanceId);
    }
    else
    {
        Console.WriteLine("Workflow {0} Completed.", e.InstanceId);
    }
};

// Run the workflow.
wfApp.Run();

Cuando se invoca este flujo de trabajo, en la consola se muestra el siguiente resultado.

Iniciando la rama 1.
Se completó la rama 2.Se canceló la rama 1.Flujo de trabajo e0685e24-18ef-4a47-acf3-5c638732f3be completado. Las actividades también se cancelan si una excepción traspasa la raíz de la actividad, pero se controla en un nivel más alto del flujo de trabajo. En este ejemplo, la lógica principal del flujo de trabajo consta de una actividad Sequence. El objeto Sequence se especifica como la propiedad Body de una actividad CancellationScope que está incluida en una actividad TryCatch. El cuerpo del objeto Sequence produce una excepción, que es controlada por el elemento principal de la actividad TryCatch y el objeto Sequence se cancela.

Activity wf = new TryCatch
{
    Try = new CancellationScope
    {
        Body = new Sequence
        {
            Activities =
            {
                new WriteLine
                {
                    Text = "Sequence starting."
                },
                new Throw
                {
                     Exception = new InArgument<Exception>((env) =>
                         new ApplicationException("An ApplicationException was thrown."))
                },
                new WriteLine
                {
                    Text = "Sequence complete."
                }
            }
        },
        CancellationHandler = new WriteLine
        {
            Text = "Sequence canceled."
        }
    },
    Catches =
    {
        new Catch<ApplicationException>
        {
            Action = new ActivityAction<ApplicationException>
            {
                Handler  = new WriteLine
                {
                    Text = "Exception caught."
                }
            }
        }
    }
};

// Create a WorkflowApplication instance.
WorkflowApplication wfApp = new WorkflowApplication(wf);

wfApp.Completed = delegate(WorkflowApplicationCompletedEventArgs e)
{
    if (e.CompletionState == ActivityInstanceState.Faulted)
    {
        Console.WriteLine("Workflow {0} Terminated.", e.InstanceId);
        Console.WriteLine("Exception: {0}\n{1}",
            e.TerminationException.GetType().FullName,
            e.TerminationException.Message);
    }
    else if (e.CompletionState == ActivityInstanceState.Canceled)
    {
        Console.WriteLine("Workflow {0} Canceled.", e.InstanceId);
    }
    else
    {
        Console.WriteLine("Workflow {0} Completed.", e.InstanceId);
    }
};

// Run the workflow.
wfApp.Run();

Cuando se invoca este flujo de trabajo, en la consola se muestra el siguiente resultado.

Iniciando secuencia.
Secuencia cancelada.Excepción detectada.Flujo de trabajo e3c18939-121e-4c43-af1c-ba1ce977ce55 completado.

Producir excepciones en CancellationHandler

Cualquier excepción producida por la propiedad CancellationHandler de un objeto CancellationScope resulta grave para el flujo de trabajo. Si existe alguna posibilidad de que se escapen excepciones de un objeto CancellationHandler, utilice un objeto TryCatch en la propiedad CancellationHandler para detectar y controlar estas excepciones.

Cancelación mediante CompensableActivity

Al igual que la actividad CancellationScope, CompensableActivity tiene una propiedad CancellationHandler. Si se cancela un objeto CompensableActivity, se invoca cualquier actividad presente en su propiedad CancellationHandler. Esto puede ser útil para deshacer un trabajo compensable completado parcialmente. Para información sobre cómo usar CompensableActivity en tareas de compensación y cancelación, consulte Compensación.

Cancelación mediante actividades personalizadas

Los autores de actividades personalizadas pueden implementar la lógica de cancelación en sus actividades personalizadas de varias maneras diferentes. Las actividades personalizadas que derivan de Activity pueden implementar la lógica de cancelación colocando CancellationScope u otra actividad personalizada que contiene lógica de cancelación en el cuerpo de la actividad. Las actividades derivadas AsyncCodeActivity y NativeActivity pueden invalidar su método Cancel respectivo y proporcionar lógica de cancelación allí. Las actividades derivadas de CodeActivity no proporcionan ninguna disposición para cancelación porque todo el trabajo se realiza en una sola ráfaga de ejecución cuando el tiempo de ejecución llama al método Execute. Si no se ha llamado todavía al método de ejecución y se cancela una actividad basada en CodeActivity, la actividad se cierra con el estado Canceled y no se llama al método Execute.

Cancelación utilizando NativeActivity

Las actividades derivadas NativeActivity pueden invalidar el método Cancel para proporcionar lógica de cancelación personalizada. Si no se invalida este método, se aplica la lógica de cancelación de flujo de trabajo predeterminada. La cancelación predeterminada es el proceso que se produce para NativeActivity que no invalida el método Cancel o cuyo método Cancel llama al método NativeActivityCancel base. Cuando se cancela una actividad, el tiempo de ejecución marca la actividad para cancelarla y controla automáticamente determinadas tareas de limpieza. Si la actividad solo tiene marcadores pendientes, estos se quitarán y la actividad se marcará como Canceled. Cualquier actividad secundaria pendiente de la actividad cancelada se cancelará a su vez. Cualquier intento de programación de actividades secundarias adicionales se ignorará y la actividad se marcará como Canceled. Si alguna de las actividades secundarias pendientes se completa con el estado Canceled o Faulted, la actividad se marcará como Canceled. Tenga en cuenta que se puede omitir una solicitud de cancelación. Si una actividad no tiene marcadores pendientes ni actividades secundarias en ejecución y no programa ningún elemento de trabajo adicional tras haber sido marcada para cancelarse, se completará correctamente. Esta cancelación predeterminada resulta suficiente para muchos escenarios, pero si se necesita lógica de cancelación adicional, se pueden utilizar actividades de cancelación integradas o personalizadas.

En el siguiente ejemplo, se define la invalidación Cancel de una actividad NativeActivity personalizada basada en ParallelForEach. Cuando se cancela la actividad, esta invalidación controla la lógica de cancelación de la actividad. Este ejemplo forma parte del ejemplo ParallelForEach no genérico.

protected override void Cancel(NativeActivityContext context)  
{  
    // If we do not have a completion condition then we can just  
    // use default logic.  
    if (this.CompletionCondition == null)  
    {  
        base.Cancel(context);  
    }  
    else  
    {  
        context.CancelChildren();  
    }  
}  

Las actividades derivadas NativeActivity pueden determinar si la propiedad IsCancellationRequested ha solicitado la cancelación y marcarse a sí mismas como canceladas llamando al método MarkCanceled. La llamada al método MarkCanceled no completa la actividad de forma inmediata. Como es habitual, el tiempo de ejecución completará la actividad cuando no tenga ningún trabajo pendiente, pero si se llama al método MarkCanceled, el estado final será Canceled en lugar de Closed.

Cancelación utilizando AsyncCodeActivity

Las actividades basadas en AsyncCodeActivity también pueden proporcionar lógica de cancelación personalizada invalidando el método Cancel. Si no se invalida este método, no se realizará ningún control de la cancelación si la actividad se cancela. En el siguiente ejemplo, se define la invalidación Cancel de una actividad AsyncCodeActivity personalizada basada en ExecutePowerShell. Cuando la actividad se cancela, pone en práctica el comportamiento de cancelación deseado.

// Called by the runtime to cancel the execution of this asynchronous activity.
protected override void Cancel(AsyncCodeActivityContext context)
{
    Pipeline pipeline = context.UserState as Pipeline;
    if (pipeline != null)
    {
        pipeline.Stop();
        DisposePipeline(pipeline);
    }
    base.Cancel(context);
}

Las actividades derivadas AsyncCodeActivity pueden determinar si la propiedad IsCancellationRequested ha solicitado la cancelación y marcarse a sí mismas como canceladas llamando al método MarkCanceled.