Не найдя причин, почему это не должно быть сделано, мы сделали это. В конечном итоге служба была реализована как служба 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");
}
}
}
Это позволяет нам генерировать пользовательские исключения из API и сериализовать их вызывающей стороне, используя коды состояния HTTP для указания результатов вызова REST. Мы можем добавить код в будущем, чтобы зарегистрировать внутреннее исключение и, при желании, удалить его перед генерацией пользовательского ответа.
Использование:
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;
}