File: System\Activities\Statements\TransactionScope.cs
Project: ndp\cdf\src\NetFx40\System.Activities\System.Activities.csproj (System.Activities)
//-----------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation.  All rights reserved.
//-----------------------------------------------------------------------------
 
namespace System.Activities.Statements
{
    using System;
    using System.Activities;
    using System.Activities.Validation;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Runtime;
    using System.Transactions;
    using System.Windows.Markup;
    using System.Activities.Expressions;
    using System.Collections.ObjectModel;
 
    [ContentProperty("Body")]
    public sealed class TransactionScope : NativeActivity
    {
        const IsolationLevel defaultIsolationLevel = default(IsolationLevel);
 
        InArgument<TimeSpan> timeout;
        bool isTimeoutSetExplicitly;
        Variable<RuntimeTransactionHandle> runtimeTransactionHandle;
        bool abortInstanceOnTransactionFailure;
        bool abortInstanceFlagWasExplicitlySet;
        Delay nestedScopeTimeoutWorkflow;
        Variable<bool> delayWasScheduled;
        Variable<TimeSpan> nestedScopeTimeout;
        Variable<ActivityInstance> nestedScopeTimeoutActivityInstance;
        static string runtimeTransactionHandlePropertyName = typeof(RuntimeTransactionHandle).FullName;
        const string AbortInstanceOnTransactionFailurePropertyName = "AbortInstanceOnTransactionFailure";
        const string IsolationLevelPropertyName = "IsolationLevel";
        const string BodyPropertyName = "Body";
 
        public TransactionScope()
            : base()
        {
            this.timeout = new InArgument<TimeSpan>(TimeSpan.FromMinutes(1));
            this.runtimeTransactionHandle = new Variable<RuntimeTransactionHandle>();
            this.abortInstanceOnTransactionFailure = true;
            this.nestedScopeTimeout = new Variable<TimeSpan>();
            this.delayWasScheduled = new Variable<bool>();
            this.nestedScopeTimeoutActivityInstance = new Variable<ActivityInstance>();
 
            base.Constraints.Add(ProcessParentChainConstraints());
            base.Constraints.Add(ProcessChildSubtreeConstraints());
        }
 
        [DefaultValue(null)]
        public Activity Body
        {
            get;
            set;
        }
 
        [DefaultValue(true)]
        public bool AbortInstanceOnTransactionFailure
        {
            get
            {
                return this.abortInstanceOnTransactionFailure;
            }
            set
            {
                this.abortInstanceOnTransactionFailure = value;
                this.abortInstanceFlagWasExplicitlySet = true;
            }
        }
 
        public IsolationLevel IsolationLevel
        {
            get;
            set;
        }
 
        public InArgument<TimeSpan> Timeout
        {
            get
            {
                return this.timeout;
            }
 
            set
            {
                this.timeout = value;
                this.isTimeoutSetExplicitly = true;
            }
        }
 
        Delay NestedScopeTimeoutWorkflow
        {
            get
            {
                if (this.nestedScopeTimeoutWorkflow == null)
                {
                    this.nestedScopeTimeoutWorkflow = new Delay
                    {
                        Duration = new InArgument<TimeSpan>(this.nestedScopeTimeout)
                    };
 
                }
                return this.nestedScopeTimeoutWorkflow;
            }
        }
 
        protected override bool CanInduceIdle
        {
            get
            {
                return true;
            }
        }
 
        [EditorBrowsable(EditorBrowsableState.Never)]
        public bool ShouldSerializeIsolationLevel()
        {
            return this.IsolationLevel != defaultIsolationLevel;
        }
 
        [EditorBrowsable(EditorBrowsableState.Never)]
        public bool ShouldSerializeTimeout()
        {
            return this.isTimeoutSetExplicitly;
        }
 
        protected override void CacheMetadata(NativeActivityMetadata metadata)
        {
            RuntimeArgument timeoutArgument = new RuntimeArgument("Timeout", typeof(TimeSpan), ArgumentDirection.In, false);
            metadata.Bind(this.Timeout, timeoutArgument);
            metadata.SetArgumentsCollection(new Collection<RuntimeArgument> { timeoutArgument });
            metadata.AddImplementationChild(this.NestedScopeTimeoutWorkflow);
 
            if (this.Body != null)
            {
                metadata.AddChild(this.Body);
            }
 
            metadata.AddImplementationVariable(this.runtimeTransactionHandle);
            metadata.AddImplementationVariable(this.nestedScopeTimeout);
            metadata.AddImplementationVariable(this.delayWasScheduled);
            metadata.AddImplementationVariable(this.nestedScopeTimeoutActivityInstance);
        }
 
        Constraint ProcessParentChainConstraints()
        {
            DelegateInArgument<TransactionScope> element = new DelegateInArgument<TransactionScope> { Name = "element" };
            DelegateInArgument<ValidationContext> validationContext = new DelegateInArgument<ValidationContext> { Name = "validationContext" };
            DelegateInArgument<Activity> parent = new DelegateInArgument<Activity> { Name = "parent" };
 
            return new Constraint<TransactionScope>
            {
                Body = new ActivityAction<TransactionScope, ValidationContext>
                {
                    Argument1 = element,
                    Argument2 = validationContext,
                    Handler = new Sequence
                    {
                        Activities = 
                        {
                            new ForEach<Activity>
                            {
                                Values = new GetParentChain
                                {
                                    ValidationContext = validationContext,
                                },
                                Body = new ActivityAction<Activity>
                                {
                                    Argument = parent,
                                    Handler = new Sequence                                   
                                    {
                                        Activities = 
                                        {
                                            new If()
                                            {
                                                Condition = new Equal<Type, Type, bool>
                                                {
                                                    Left = new ObtainType
                                                    {
                                                        Input = parent,
                                                    },
                                                    Right = new InArgument<Type>(context => typeof(TransactionScope))
                                                },
                                                Then = new Sequence
                                                {
                                                    Activities = 
                                                    {
                                                        new AssertValidation
                                                        {
                                                            IsWarning = true,
                                                            Assertion = new AbortInstanceFlagValidator
                                                            {
                                                                ParentActivity = parent,
                                                                TransactionScope = new InArgument<TransactionScope>(element)
                                                            },
                                                            Message = new InArgument<string>(SR.AbortInstanceOnTransactionFailureDoesNotMatch),
                                                            PropertyName = AbortInstanceOnTransactionFailurePropertyName
                                                        },
                                                        new AssertValidation
                                                        {
                                                            Assertion = new IsolationLevelValidator
                                                            {
                                                                ParentActivity = parent,
                                                                //CurrentIsolationLevel = new InArgument<IsolationLevel>(context => element.Get(context).IsolationLevel)
                                                                CurrentIsolationLevel = new InArgument<IsolationLevel>
                                                                {
                                                                    Expression = new IsolationLevelValue
                                                                    {
                                                                        Scope = element
                                                                    }
                                                                }
                                                            },
                                                            Message = new InArgument<string>(SR.IsolationLevelValidation),
                                                            PropertyName = IsolationLevelPropertyName
                                                        }                                                      
 
                                                    }
                                                }
                                                
                                            }
                                        }
                                    }                                   
                                }
                            }
                        }
                    }
                }
            };
        }
 
        Constraint ProcessChildSubtreeConstraints()
        {
            DelegateInArgument<TransactionScope> element = new DelegateInArgument<TransactionScope> { Name = "element" };
            DelegateInArgument<ValidationContext> validationContext = new DelegateInArgument<ValidationContext> { Name = "validationContext" };
            DelegateInArgument<Activity> child = new DelegateInArgument<Activity> { Name = "child" };
            Variable<bool> nestedCompensableActivity = new Variable<bool>();
 
            return new Constraint<TransactionScope>
            {
                Body = new ActivityAction<TransactionScope, ValidationContext>
                {
                    Argument1 = element,
                    Argument2 = validationContext,
                    Handler = new Sequence
                    {
                        Variables = { nestedCompensableActivity },
                        Activities = 
                        {                            
                            new ForEach<Activity>
                            {
                                Values = new GetChildSubtree
                                {
                                    ValidationContext = validationContext,
                                },
                                Body = new ActivityAction<Activity>
                                {
                                    Argument = child,
                                    Handler = new Sequence                                   
                                    {
                                        Activities = 
                                        {
                                            new If()
                                            {
                                                Condition = new Equal<Type, Type, bool>()
                                                {
                                                     Left = new ObtainType
                                                     {
                                                          Input = new InArgument<Activity>(child)
                                                     },
                                                     Right = new InArgument<Type>(context => typeof(CompensableActivity))
                                                },
                                                Then = new Assign<bool>
                                                {
                                                    To = new OutArgument<bool>(nestedCompensableActivity),
                                                    Value = new InArgument<bool>(true)
                                                }
                                            }
                                        }
                                    }
                                }
                            },
                            new AssertValidation()
                            {
                                 Assertion = new InArgument<bool>(new Not<bool, bool> { Operand = new VariableValue<bool> { Variable = nestedCompensableActivity } }),
                                 Message = new InArgument<string>(SR.CompensableActivityInsideTransactionScopeActivity),
                                 PropertyName = BodyPropertyName
                            }
                        }
                    }
                }
            };
        }
 
        protected override void Execute(NativeActivityContext context)
        {
            RuntimeTransactionHandle transactionHandle = this.runtimeTransactionHandle.Get(context);
            Fx.Assert(transactionHandle != null, "RuntimeTransactionHandle is null");
 
            RuntimeTransactionHandle foundHandle = context.Properties.Find(runtimeTransactionHandlePropertyName) as RuntimeTransactionHandle;
            if (foundHandle == null)
            {
                //Note, once the property is registered, we cannot change the state of this flag
                transactionHandle.AbortInstanceOnTransactionFailure = this.AbortInstanceOnTransactionFailure;
                context.Properties.Add(transactionHandle.ExecutionPropertyName, transactionHandle);
            }
            else
            {
                //nested case
                //foundHandle.IsRuntimeOwnedTransaction will be true only in the Invoke case within an ambient Sys.Tx transaction. 
                //If this TSA is nested inside the ambient transaction from Invoke, then the AbortInstanceFlag is always false since the RTH corresponding to the ambient
                //transaction has this flag as false. In this case, we ignore if this TSA has this flag explicitly set to true. 
                if (!foundHandle.IsRuntimeOwnedTransaction && this.abortInstanceFlagWasExplicitlySet && (foundHandle.AbortInstanceOnTransactionFailure != this.AbortInstanceOnTransactionFailure))
                {
                    throw FxTrace.Exception.AsError(new InvalidOperationException(SR.AbortInstanceOnTransactionFailureDoesNotMatch));
                }
 
                if (foundHandle.SuppressTransaction)
                {
                    throw FxTrace.Exception.AsError(new InvalidOperationException(SR.CannotNestTransactionScopeWhenAmbientHandleIsSuppressed(this.DisplayName)));
                }
                transactionHandle = foundHandle;
            }
 
            Transaction transaction = transactionHandle.GetCurrentTransaction(context);
            //Check if there is already a transaction (Requires Semantics)
            if (transaction == null)
            {
                //If not, request one..
                transactionHandle.RequestTransactionContext(context, OnContextAcquired, null);
            }
            else
            {
                //Most likely, you are inside a nested TSA
                if (transaction.IsolationLevel != this.IsolationLevel)
                {
                    throw FxTrace.Exception.AsError(new InvalidOperationException(SR.IsolationLevelValidation));
                }
 
                //Check if the nested TSA had a timeout specified explicitly
                if (this.isTimeoutSetExplicitly)
                {
                    TimeSpan timeout = this.Timeout.Get(context);
                    this.delayWasScheduled.Set(context, true);
                    this.nestedScopeTimeout.Set(context, timeout);
 
                    this.nestedScopeTimeoutActivityInstance.Set(context, context.ScheduleActivity(this.NestedScopeTimeoutWorkflow, new CompletionCallback(OnDelayCompletion)));
                }
 
                //execute the Body under the current runtime transaction
                ScheduleBody(context);
            }
        }
 
        void OnContextAcquired(NativeActivityTransactionContext context, object state)
        {
            Fx.Assert(context != null, "ActivityTransactionContext was null");
 
            TimeSpan transactionTimeout = this.Timeout.Get(context);
            TransactionOptions transactionOptions = new TransactionOptions()
            {
                IsolationLevel = this.IsolationLevel,
                Timeout = transactionTimeout
            };
 
            context.SetRuntimeTransaction(new CommittableTransaction(transactionOptions));
 
            ScheduleBody(context);
        }
 
        void ScheduleBody(NativeActivityContext context)
        {
            if (this.Body != null)
            {
                context.ScheduleActivity(this.Body, new CompletionCallback(OnCompletion));
            }
        }
 
        void OnCompletion(NativeActivityContext context, ActivityInstance instance)
        {
            RuntimeTransactionHandle transactionHandle = this.runtimeTransactionHandle.Get(context);
            Fx.Assert(transactionHandle != null, "RuntimeTransactionHandle is null");
 
            if (this.delayWasScheduled.Get(context))
            {
                transactionHandle.CompleteTransaction(context, new BookmarkCallback(OnTransactionComplete));
            }
            else
            {
                transactionHandle.CompleteTransaction(context);
            }
        }
 
        void OnDelayCompletion(NativeActivityContext context, ActivityInstance instance)
        {
            if (instance.State == ActivityInstanceState.Closed)
            {
                RuntimeTransactionHandle handle = context.Properties.Find(runtimeTransactionHandlePropertyName) as RuntimeTransactionHandle;
                Fx.Assert(handle != null, "Internal error.. If we are here, there ought to be an ambient transaction handle");
                handle.GetCurrentTransaction(context).Rollback();
            }
        }
 
        void OnTransactionComplete(NativeActivityContext context, Bookmark bookmark, object state)
        {
            Fx.Assert(this.delayWasScheduled.Get(context), "Internal error..Delay should have been scheduled if we are here");
            ActivityInstance delayActivityInstance = this.nestedScopeTimeoutActivityInstance.Get(context);
            if (delayActivityInstance != null)
            {
                context.CancelChild(delayActivityInstance);
            }
        }
 
        //
        class ObtainType : CodeActivity<Type>
        {
            public ObtainType()
            {
            }
 
            public InArgument<Activity> Input
            {
                get;
                set;
            }
 
            protected override void CacheMetadata(CodeActivityMetadata metadata)
            {
                RuntimeArgument inputArgument = new RuntimeArgument("Input", typeof(Activity), ArgumentDirection.In);
                if (this.Input == null)
                {
                    this.Input = new InArgument<Activity>();
                }
                metadata.Bind(this.Input, inputArgument);
 
                RuntimeArgument resultArgument = new RuntimeArgument("Result", typeof(Type), ArgumentDirection.Out);
                if (this.Result == null)
                {
                    this.Result = new OutArgument<Type>();
                }
                metadata.Bind(this.Result, resultArgument);
 
                metadata.SetArgumentsCollection(
                    new Collection<RuntimeArgument>
                {
                    inputArgument,
                    resultArgument
                });
            }
 
            protected override Type Execute(CodeActivityContext context)
            {
                return this.Input.Get(context).GetType();
            }
        }
 
        class IsolationLevelValue : CodeActivity<IsolationLevel>
        {
            public InArgument<TransactionScope> Scope
            {
                get;
                set;
            }
 
            protected override void CacheMetadata(CodeActivityMetadata metadata)
            {
                RuntimeArgument scopeArgument = new RuntimeArgument("Scope", typeof(TransactionScope), ArgumentDirection.In);
                if (this.Scope == null)
                {
                    this.Scope = new InArgument<TransactionScope>();
                }
                metadata.Bind(this.Scope, scopeArgument);
 
                RuntimeArgument resultArgument = new RuntimeArgument("Result", typeof(IsolationLevel), ArgumentDirection.Out);
                if (this.Result == null)
                {
                    this.Result = new OutArgument<IsolationLevel>();
                }
                metadata.Bind(this.Result, resultArgument);
 
                metadata.SetArgumentsCollection(
                    new Collection<RuntimeArgument>
                {
                    scopeArgument,
                    resultArgument
                });
            }
 
            protected override IsolationLevel Execute(CodeActivityContext context)
            {
                return this.Scope.Get(context).IsolationLevel;
            }
        }
 
        class IsolationLevelValidator : CodeActivity<bool>
        {
            public InArgument<Activity> ParentActivity
            {
                get;
                set;
            }
 
            public InArgument<IsolationLevel> CurrentIsolationLevel
            {
                get;
                set;
            }
 
            protected override void CacheMetadata(CodeActivityMetadata metadata)
            {
                RuntimeArgument parentActivityArgument = new RuntimeArgument("ParentActivity", typeof(Activity), ArgumentDirection.In);
                if (this.ParentActivity == null)
                {
                    this.ParentActivity = new InArgument<Activity>();
                }
                metadata.Bind(this.ParentActivity, parentActivityArgument);
 
                RuntimeArgument isoLevelArgument = new RuntimeArgument("CurrentIsolationLevel", typeof(IsolationLevel), ArgumentDirection.In);
                if (this.CurrentIsolationLevel == null)
                {
                    this.CurrentIsolationLevel = new InArgument<IsolationLevel>();
                }
                metadata.Bind(this.CurrentIsolationLevel, isoLevelArgument);
 
                RuntimeArgument resultArgument = new RuntimeArgument("Result", typeof(bool), ArgumentDirection.Out);
                if (this.Result == null)
                {
                    this.Result = new OutArgument<bool>();
                }
                metadata.Bind(this.Result, resultArgument);
 
                metadata.SetArgumentsCollection(
                    new Collection<RuntimeArgument>
                {
                    parentActivityArgument,
                    isoLevelArgument,
                    resultArgument
                });
            }
 
            protected override bool Execute(CodeActivityContext context)
            {
                Activity parent = this.ParentActivity.Get(context);
 
                if (parent != null)
                {
                    TransactionScope transactionScope = parent as TransactionScope;
                    Fx.Assert(transactionScope != null, "ParentActivity was not of expected type");
 
                    if (transactionScope.IsolationLevel != this.CurrentIsolationLevel.Get(context))
                    {
                        return false;
                    }
                    else
                    {
                        return true;
                    }
                }
                else
                {
                    return true;
                }
            }
        }
 
        class AbortInstanceFlagValidator : CodeActivity<bool>
        {
            public InArgument<Activity> ParentActivity
            {
                get;
                set;
            }
 
            public InArgument<TransactionScope> TransactionScope
            {
                get;
                set;
            }
 
            protected override void CacheMetadata(CodeActivityMetadata metadata)
            {
                RuntimeArgument parentActivityArgument = new RuntimeArgument("ParentActivity", typeof(Activity), ArgumentDirection.In);
                if (this.ParentActivity == null)
                {
                    this.ParentActivity = new InArgument<Activity>();
                }
                metadata.Bind(this.ParentActivity, parentActivityArgument);
 
                RuntimeArgument txScopeArgument = new RuntimeArgument("TransactionScope", typeof(TransactionScope), ArgumentDirection.In);
                if (this.TransactionScope == null)
                {
                    this.TransactionScope = new InArgument<TransactionScope>();
                }
                metadata.Bind(this.TransactionScope, txScopeArgument);
 
                RuntimeArgument resultArgument = new RuntimeArgument("Result", typeof(bool), ArgumentDirection.Out);
                if (this.Result == null)
                {
                    this.Result = new OutArgument<bool>();
                }
                metadata.Bind(this.Result, resultArgument);
 
                metadata.SetArgumentsCollection(
                    new Collection<RuntimeArgument>
                {
                    parentActivityArgument,
                    txScopeArgument,
                    resultArgument
                });
            }
 
            protected override bool Execute(CodeActivityContext context)
            {
                Activity parent = this.ParentActivity.Get(context);
 
                if (parent != null)
                {
                    TransactionScope parentTransactionScope = parent as TransactionScope;
                    Fx.Assert(parentTransactionScope != null, "ParentActivity was not of expected type");
                    TransactionScope currentTransactionScope = this.TransactionScope.Get(context);
 
                    if (parentTransactionScope.AbortInstanceOnTransactionFailure != currentTransactionScope.AbortInstanceOnTransactionFailure)
                    {
                        //If the Inner TSA was default and still different from outer, we dont flag validation warning. See design spec for all variations
                        if (!currentTransactionScope.abortInstanceFlagWasExplicitlySet)
                        {
                            return true;
                        }
                        else
                        {
                            return false;
                        }
                    }
                    else
                    {
                        return true;
                    }
                }
                else
                {
                    return true;
                }
            }
        }
    }
}