作为一个好奇实验,我进行了一项测试,以从REST服务中抛出自定义WebFaultException<OurException>
。过去,我从WCF服务中抛出了自定义异常,而不是使用DataContract
和DataMember
创建伪“异常”。这样做在REST中没有多大意义,但我很好奇。
当设置了内部异常时,我没想到的是陷入了401 UNAUTHORIZED
循环中。即使对于我们自己的自定义异常,一个简单的异常也可以完美地序列化。如果内部异常与外部异常的类型相同,则没有问题。但是我捕获和包装的所有内容都陷入了进行REST调用,抛出异常,对客户端的401 UNAUTHORIZED
响应(带有密码提示)的重复循环,然后在输入密码后再次进行其余调用-重复
我终于破解了WebFaultException<T>
类的源代码并找到了这个宝石:
[Serializable]
[System.Diagnostics.CodeAnalysis.SuppressMessage
("Microsoft.Design", "CA1032:ImplementStandardExceptionConstructors",
Justification = "REST Exceptions cannot contain InnerExceptions or messages")]
public class WebFaultException {...}
那么为什么它们不能包含内部异常?一切都可以独立地很好地进行序列化,因此在WebFaultException
的内部工作中,由于某些众所周知的原因,它必须未被实施或被明确阻止。
有什么作用?
界面:
[OperationContract]
[FaultContract(typeof(OurException))]
[WebInvoke(Method = "GET", ResponseFormat = WebMessageFormat.Xml,
BodyStyle = WebMessageBodyStyle.Bare, UriTemplate = "/TestCustomException")]
string TestCustomException();
服务方法:
public string TestCustomException() {
throw new WebFaultException<OurException>(new OurException("Nope.", new Exception("Still Nope.")), HttpStatusCode.InternalServerError);
}
答案 0 :(得分:0)
没有发现为什么不应该这样做的原因,我们做到了。该服务最终实现为Web API 2服务。我们有一个自定义的异常和一个异常过滤器,它模仿默认的异常行为,但允许我们指定HTTP状态代码(Microsoft将所有内容都设为503)。内部异常可以很好地序列化...有人可能会争辩说我们不应该包含它们,而且如果服务不在我们部门之外,我们也不会这样做。目前,决定是否与域控制器联系失败是呼叫方的责任,这是呼叫方的责任。
RestSharp不久前删除了Newtonsoft依赖项,但不幸的是,它现在提供的反序列化器(在此回答时为v106.4)无法处理基类中的公共成员-实际上,它只是为了对简单的序列化进行反序列化,非继承类型。因此,我们不得不重新添加Newtonsoft反序列化器……使用这些内部序列化序列就可以了。
无论如何,这是我们最终得到的代码:
[Serializable]
public class OurAdApiException : OurBaseWebException {
/// <summary>The result of the action in Active Directory </summary>
public AdActionResults AdResult { get; set; }
/// <summary>The result of the action in LDS </summary>
public AdActionResults LdsResult { get; set; }
// Other constructors snipped...
public OurAdApiException(SerializationInfo info, StreamingContext context) : base(info, context) {
try {
AdResult = (AdActionResults) info.GetValue("AdResult", typeof(AdActionResults));
LdsResult = (AdActionResults) info.GetValue("LdsResult", typeof(AdActionResults));
}
catch (ArgumentNullException) {
// blah
}
catch (InvalidCastException) {
// blah blah
}
catch (SerializationException) {
// yeah, yeah, yeah
}
}
[SecurityPermission(SecurityAction.LinkDemand, Flags = SerializationFormatter)]
public override void GetObjectData(SerializationInfo info, StreamingContext context) {
base.GetObjectData(info, context);
// 'info' is guaranteed to be non-null by the above call to GetObjectData (will throw an exception there if null)
info.AddValue("AdResult", AdResult);
info.AddValue("LdsResult", LdsResult);
}
}
并且:
[Serializable]
public class OurBaseWebException : OurBaseException {
/// <summary>
/// Dictionary of properties relevant to the exception </summary>
/// <remarks>
/// Basically seals the property while leaving the class inheritable. If we don't do this,
/// we can't pass the dictionary to the constructors - we'd be making a virtual member call
/// from the constructor. This is because Microsoft makes the Data property virtual, but
/// doesn't expose a protected setter (the dictionary is instantiated the first time it is
/// accessed if null). #why
/// If you try to fully override the property, you get a serialization exception because
/// the base exception also tries to serialize its Data property </remarks>
public new IDictionary Data => base.Data;
/// <summary>The HttpStatusCode to return </summary>
public HttpStatusCode HttpStatusCode { get; protected set; }
public InformationSecurityWebException(SerializationInfo info, StreamingContext context) : base(info, context) {
try {
HttpStatusCode = (HttpStatusCode) info.GetValue("HttpStatusCode", typeof(HttpStatusCode));
}
catch (ArgumentNullException) {
// sure
}
catch (InvalidCastException) {
// fine
}
catch (SerializationException) {
// we do stuff here in the real code
}
}
[SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
public override void GetObjectData(SerializationInfo info, StreamingContext context) {
base.GetObjectData(info, context);
info.AddValue(nameof(HttpStatusCode), HttpStatusCode, typeof(HttpStatusCode));
}
}
最后,我们的异常过滤器:
public override void OnException(HttpActionExecutedContext context) {
// Any custom AD API Exception thrown will be serialized into our custom response
// Any other exception will be handled by the Microsoft framework
if (context.Exception is OurAdApiException contextException) {
try {
// This lets us use HTTP Status Codes to indicate REST results.
// An invalid parameter value becomes a 400 BAD REQUEST, while
// a configuration error is a 503 SERVICE UNAVAILABLE, for example.
// (Code for CreateCustomErrorResponse available upon request...
// we basically copied the .NET framework code because it wasn't
// possible to modify/override it :(
context.Response = context.Request.CreateCustomErrorResponse(contextException.HttpStatusCode, contextException);
}
catch (Exception exception) {
exception.Swallow($"Caught an exception creating the custom response; IIS will generate the default response for the object");
}
}
}
这允许我们使用HTTP状态代码指示REST调用结果,从API引发自定义异常并将其序列化到调用方。我们将来可能会添加代码来记录内部异常,并有选择地在生成自定义响应之前将其剥离。
用法:
catch (UserNotFoundException userNotFoundException) {
ldsResult = NotFound;
throw new OurAdApiException($"User '{userCN}' not found in LDS", HttpStatusCode.NotFound, adResult, ldsResult, userNotFoundException);
}
从RestSharp调用者反序列化:
public IRestResponse<T> Put<T, W, V>(string ResourceUri, W MethodParameter) where V : Exception
where T : new() {
// Handle to any logging context established by caller; null logger if none was configured
ILoggingContext currentContext = ContextStack<IExecutionContext>.CurrentContext as ILoggingContext ?? new NullLoggingContext();
currentContext.ThreadTraceInformation("Building the request...");
RestRequest request = new RestRequest(ResourceUri, Method.PUT) {
RequestFormat = DataFormat.Json,
OnBeforeDeserialization = serializedResponse => { serializedResponse.ContentType = "application/json"; }
};
request.AddBody(MethodParameter);
currentContext.ThreadTraceInformation($"Executing request: {request} ");
IRestResponse<T> response = _client.Execute<T>(request);
#region - Handle the Response -
if (response == null) {
throw new OurBaseException("The response from the REST service is null");
}
// If you're not following the contract, you'll get a serialization exception
// You can optionally work with the json directly, or use dynamic
if (!response.IsSuccessful) {
V exceptionData = JsonConvert.DeserializeObject<V>(response.Content);
throw exceptionData.ThreadTraceError();
}
// Timed out, aborted, etc.
if (response.ResponseStatus != ResponseStatus.Completed) {
throw new OurBaseException($"Request failed to complete: Status '{response.ResponseStatus}'").ThreadTraceError();
}
#endregion
return response;
}