Search code examples
c#.netwcfmessagecontractwcf-extensions

WCF: "Cancel" operation Call Dependent on Request Message Contract


Here's my contract:

    [ServiceContract]
public interface IMyServiceContract {
    [OperationContract]
    OperationResponse1 Operation1(OperationRequest1 req);

    [OperationContract]
    OperationResponse2 Operation2(OperationRequest2 req);
    }

OperationRequest1 and OperationRequest2 both inherit from BaseOperationRequest, which holds credential information for all requests that come in to the service:

    [MessageContract]
public abstract class BaseOperationRequest {
    [MessageHeader(MustUnderstand = true)]
    public Guid Token { get; set; }

    [MessageHeader(MustUnderstand = true)]
    public string Password { get; set; }

    private User User { get; set; }
}

OperationResponse1 and OperationResponse2 both inherit from a base class too:

    [MessageContract]
public abstract class BaseOperationResponse {
    [MessageHeader(MustUnderstand = true)]
    public bool Success { get; set; }

    [MessageHeader(MustUnderstand = true)]
    public ServiceErrors ErrorCode { get; set; }
}

ErrorCode is an enumeration.

As you can see on the request, I have two message headers, and one internal object that does not get deserialized as part of the SOAP message. The reason for this is that I want to inject this object in to the request before it is processed by my service implementation. Every single operation implementation will use this object, and I don't want each operation to make two calls to my data layer.

I would like to use WCF extensiblity (via an attribute) to perform two tasks:

  1. Authenticate the requesting user.
  2. Populate "User" on the incoming request class with a complex / composite business object, for use within every operation.

I've investigated IOperationInvoker, IDispatchMessageFormatter and IDispatchMessageInspector, but I've not found any of them to be quite suitable enough.

FYI, here's a primitive example implementation of my service, without any fancy WCF extensibility (or my repository / data layer calls):

public class MyService: IMyServiceContract {
    public OperationResponse1 Operation1(OperationRequest1 req) {
        if(req.Token == new Guid("GUID VALUE") && req.Password == "password") {
            // Perform some actions....

            return new OperationResponse1 {
                Success = true
            }
        } else {
            return new OperationResponse1 {
                Success = false,
                Error = "You are not authenticated"
            }
        }
    }
    public OperationResponse2 Operation2(OperationRequest2 req) {
        if(req.Token == new Guid("GUID VALUE") && req.Password == "password") {
            // Perform some actions....

            return new OperationResponse2 {
                Success = true
            }
        } else {
            return new OperationResponse2 {
                Success = false,
                Error = "You are not authenticated"
            }
        }
    }
}

IOperationInvoker seems to be the most suitable extension point, but I can't quite figure out how to "cancel" the operation and override the response to the client. Here's where I got to:

/// <summary>
/// Provides an invoker that can be used to authenticate a BaseOperationRequest message.
/// </summary>
public class UserAuthenticationInvoker : IOperationInvoker {
    /// <summary>
    /// The original operation invoker.
    /// </summary>
    private IOperationInvoker _originalInvoker;

    /// <summary>
    /// The injected User service, for authentication.
    /// </summary>
    [Inject]
    public IUserService UserService { get; set; }


    public UserAuthenticationInvoker(IOperationInvoker originalInvoker) {
        _originalInvoker = originalInvoker;
    }

    #region Implementation of IOperationInvoker {

    public object[] AllocateInputs() {
        return _originalInvoker.AllocateInputs();
    }

    public object Invoke(object instance, object[] inputs, out object[] outputs) {
        // Validate base request
        if(!(inputs[0] is BaseOperationRequest)) {
            throw new ArgumentException("The request object must inherit from BaseOperationRequest in order for User authentication to take place.");
        }

        // Get BaseOperationRequest
        var req = (BaseOperationRequest)inputs[0];

        // Authenticate the User
        var authResult = UserService.AuthenticateUser(new AuthenticateUserRequest {
            Token = req.Token,
            Password = req.Password
        });
        if(authResult.Success) {
            // This is where I get stuck - do I need to modify "outputs"? If so, how do I tell the invoker that I want a particular response to be returned, and to cancel the rest of the operation?
            return _originalInvoker.Invoke(instance, inputs, outputs);
        }
        return _originalInvoker.Invoke(instance, inputs, out outputs);
    }

    public IAsyncResult InvokeBegin(object instance, object[] inputs, AsyncCallback callback, object state) {
        throw new NotImplementedException("The operation cannot be invoked asynchronously.");
    }

    public object InvokeEnd(object instance, out object[] outputs, IAsyncResult result) {
        throw new NotImplementedException("The operation cannot be invoked asynchronously.");
    }

    public bool IsSynchronous {
        get { return true; }
    }

    #endregion
}

Solution

  • Answered this myself. Ended up modifying the "input" to the _originalInvoker by implementing some dynamic types, and moving the injection to the attribute so I could mock / unit test the invoker. Here's my code:

    Attribute:

        [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
        public class AuthenticateUserAttribute : Attribute, IOperationBehavior {
            #region Implementation of IOperationBehavior
    
            public void Validate(OperationDescription operationDescription) {
            }
    
            public void ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation) {
                // Manual injection
                var userService = NinjectWebCommon.Kernel.Get<IUserService>();
    
                // Assign the custom authentication invoker, passing in the original operation invoker
                dispatchOperation.Invoker = new UserAuthenticationInvoker(dispatchOperation.Invoker, operationDescription.SyncMethod.ReturnType, userService);
            }
    
            public void ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation) {
                throw new NotImplementedException("This behaviour cannot be applied to a server operation.");
            }
    
            public void AddBindingParameters(OperationDescription operationDescription, BindingParameterCollection bindingParameters) {
            }
    
            #endregion
        }
    

    IOperationInvoker:

    public class UserAuthenticationInvoker : IOperationInvoker {
        /// <summary>
        ///     The original operation invoker.
        /// </summary>
        private readonly IOperationInvoker _originalInvoker;
    
        private readonly Type _responseType;
    
        private readonly IUserService _userService;
    
        public SupplierAuthenticationInvoker(IOperationInvoker originalInvoker, Type responseType, IUserService userService) {
            _originalInvoker = originalInvoker;
            _responseType = responseType;
            _userService = userService;
        }
    
        #region Implementation of IOperationInvoker {
    
        public object[] AllocateInputs() {
            return _originalInvoker.AllocateInputs();
        }
    
        public object Invoke(object instance, object[] inputs, out object[] outputs) {
            // Validate base objects request
            if(!(inputs[0] is BaseOperationRequest)) throw new ArgumentException("The request object must inherit from BaseOperationRequest in order for user authentication to take place.");
    
            dynamic response = Activator.CreateInstance(_responseType);
            if(!(response is BaseOperationResponse)) throw new InvalidOperationException("The response object must inherit from BaseOperationResponsein order for user authentication to take place.");
    
            var req = (BaseOperationRequest)inputs[0];
    
            // Authenticate the user
            var authResult = _userService.AuthenticateUser(new AuthenticateUserRequest {
                Token = req.Token,
                Password = req.Password
            });
    
            if(!authResult.Success) {
                // Authentication failed, return reflected response object.
                outputs = new object[0];
                // Set response headers
                response.Success = false;
                response.ErrorCode = ServiceErrors.AuthErrorInvalidTokenOrPassword;
    
                return response;
            }
            // Authentication success, set the user and call the original method
            dynamic modifiedInput = inputs;
            modifiedInput[0].User = authResult.User;
    
            var invoked = _originalInvoker.Invoke(instance, modifiedInput, out outputs);
    
            return invoked;
        }
    
        public IAsyncResult InvokeBegin(object instance, object[] inputs, AsyncCallback callback, object state) {
            throw new NotImplementedException("The operation cannot be invoked asynchronously.");
        }
    
        public object InvokeEnd(object instance, out object[] outputs, IAsyncResult result) {
            throw new NotImplementedException("The operation cannot be invoked asynchronously.");
        }
    
        public bool IsSynchronous {
            get { return true; }
        }
    
        #endregion
    }
    

    Invoker is then applied to the service contract as follows:

    [ServiceContract]
    public interface IMyServiceContract {
        [OperationContract]
        [AuthenticateUser]
        OperationResponse1 Operation1(OperationRequest1 req);
    
        [OperationContract]
        [AuthenticateUser]
        OperationResponse2 Operation2(OperationRequest2 req);
    }