Call .fail(error) without throwing exception

2019-07-19 17:45发布

Using SignalR, is there any possibility to call .fail instead of .done when specific values are returned by hub method?

Perhaps using the SignalR pipeline?

public bool Delete(int addressId)
{
    // User should not be able to delete default address
    if(AddressService.IsDefaultAddressOfCustomer(addressId))
        return false; // Should call .fail() on client

    AddressService.Delete(addressId);
    return true; // Should call .done() on client
}

The alternative would be to throw an exception but I would like to avoid that since the error is not really a server fault, but a user fault.

2条回答
成全新的幸福
2楼-- · 2019-07-19 18:08

Assuming you are really convinced an exception is not the right tool for you, you could use some custom attribute you would define to mark methods where a false return value must be translated into an error, and then intercept any incoming call with BuildIncoming from HubPipelineModule:

http://msdn.microsoft.com/en-us/library/microsoft.aspnet.signalr.hubs.hubpipelinemodule.buildincoming(v=vs.118).aspx

From inside there you can intercept the call to your original method, inspect if it's marked with your attribute and if it returned false, if it's the case you can throw an exception from there. The bottom line is, you would still throw an exception to make it call .fail() client-side, but that exception would not bloat your business logic. Something like this:

public class FailPipelineModule : HubPipelineModule
{
    public override Func<IHubIncomingInvokerContext, Task<object>> BuildIncoming(Func<IHubIncomingInvokerContext, Task<object>> invoke)
    {
        return base.BuildIncoming(context =>
        {
            var r = (bool)(invoke(context)).Result;
            if (context.MethodDescriptor.Attributes.Any(a => typeof(FailAttribute) == a.GetType()) && !r)
                throw new ApplicationException("false");
            return Task.FromResult((object)r);
        });
    }
}

You'll need to define FailAttribute, use it to mark your hub's method and register FailPipelineModule at startup.

查看更多
smile是对你的礼貌
3楼-- · 2019-07-19 18:22

As Wasp indicated, with the SignalR JavaScript client, the promise is only rejected if hubResult.Error is set, which only happens when an exception is thrown while the request is processed. There's no way to modify that using the hub pipeline.

In general, I'd probably stick with using exceptions in that case, but if you're looking for another alternative, you could also modify the client-side jquery.signalr-*.js code, specifically the hubProxy prototype invoke method. It has a condition to decide whether to resolve or reject the promise:

if (result.Error) {
   // code to reject ...
} else {
    connection.log("Invoked " + that.hubName + "." + methodName);
    d.resolveWith(that, [result.Result]);
}

and you could modify the else block:

if (result.Error) {
    // code to reject ...
} else {
    connection.log("Invoked " + that.hubName + "." + methodName);
    if (typeof result.Result === "boolean" && !result.Result) {
        d.rejectWith(that, [result.Result]);
    } else {
        d.resolveWith(that, [result.Result]);
    }
}

Then all hub methods which return false would reject the promise. The advantage over Wasp's solution would be not having to create attributes and overall less code. The drawback is that it might be less maintainable since you're manually editing the SignalR code (so if you do that, it should definitely be documented somewhere, and you'd have to minify the script yourself instead of using the packaged one).

A more maintainable client-side alternative would be to wrap the hub API and return your own deferred from that, e.g. like this:

var myHub = {
    server: $.connection.myHub.server,
    init: function() {
        for (var methodName in this.server) {
            if (this.server.hasOwnProperty(methodName)) {
                this[methodName] = function() {
                    var deferred = $.Deferred();
                    this.server[methodName].apply(this.server, arguments)
                        .done(function(result) {
                            // reject if server hub method returned false
                            if (result === false) deferred.reject(result);
                            deferred.resolve(result);
                        });

                    return deferred.promise();
                };
            }
        }
    }
};
myHub.init();

Then instead of calling

$.connection.myHub.server.someMethod("hello", "world")
    .done(function(result) { })
    .fail(function(result) { });

you would call

myHub.someMethod("hello", "world")
    .done(function(result) { })
    .fail(function(result) { });

That way, you'd have full control over how return values are interpreted.

查看更多
登录 后发表回答