/// <summary> /// This method resolves the appropriate transport serialzer from the incoming accept header. /// </summary> /// <param name="rs">The incoming response.</param> /// <param name="transport">The resolve transport serializer.</param> /// <returns>Returns true if the serializer can be resolved.</returns> protected virtual void ExtractHeaders <KT>(HttpResponseMessage rs, RepositoryHolder <KT> holder, IKeyMapper <KT> keyMapper) { string contentId = rs.Headers.Where((h) => h.Key.Equals("x-img-contentid", StringComparison.InvariantCultureIgnoreCase)) .SelectMany((h) => h.Value).FirstOrDefault() ?? ""; string versionId = rs.Headers.Where((h) => h.Key.Equals("x-img-versionid", StringComparison.InvariantCultureIgnoreCase)) .SelectMany((h) => h.Value).FirstOrDefault() ?? ""; holder.KeyReference = new Tuple <string, string>(contentId, versionId); if (!string.IsNullOrEmpty(contentId)) { holder.Key = keyMapper.ToKey(contentId); } }
/// <summary> /// This method marshals the RepositoryHolder and transmits it to the remote Microservice. /// </summary> /// <typeparam Name="KT">The key type.</typeparam> /// <typeparam Name="ET">The entity type.</typeparam> /// <param Name="actionType">The action type.</param> /// <param Name="rq">The repository holder request.</param> /// <returns>Returns an async task that will be signalled when the request completes or times out.</returns> protected override async Task <RepositoryHolder <KT, ET> > TransmitInternal <KT, ET>( string actionType, RepositoryHolder <KT, ET> rq, ProcessOptions?routing = null, IPrincipal principal = null) { try { StatisticsInternal.ActiveIncrement(); var payload = TransmissionPayload.Create(Policy.TransmissionPayloadTraceEnabled); payload.SecurityPrincipal = TransmissionPayload.ConvertToClaimsPrincipal(principal ?? Thread.CurrentPrincipal); // Set the process correlation key to the correlation id if passed through the rq settings if (!string.IsNullOrEmpty(rq.Settings?.CorrelationId)) { payload.Message.ProcessCorrelationKey = rq.Settings.CorrelationId; } bool processAsync = rq.Settings?.ProcessAsync ?? false; payload.Message.ChannelPriority = processAsync ? 0 : 1; payload.Options = routing ?? RoutingDefault ?? ProcessOptions.RouteExternal; payload.Message.Blob = PayloadSerializer.PayloadSerialize(rq); payload.Message.ResponseChannelId = ResponseChannelId; payload.Message.ResponseChannelId = ResponseId.Header.ChannelId; payload.Message.ResponseMessageType = ResponseId.Header.MessageType; payload.Message.ResponseActionType = ResponseId.Header.ActionType; payload.Message.ResponseChannelPriority = payload.Message.ChannelPriority; payload.Message.ChannelId = ChannelId; payload.Message.MessageType = EntityType; payload.Message.ActionType = actionType; payload.MaxProcessingTime = rq.Settings?.WaitTime ?? mDefaultRequestTimespan; return(await OutgoingRequestOut(payload, ProcessResponse <KT, ET>, processAsync)); } catch (Exception ex) { string key = rq != null && rq.Key != null?rq.Key.ToString() : string.Empty; Collector?.LogException($"Error transmitting {actionType}-{key} internally", ex); throw; } }
public PersistenceRepositoryHolder(RepositoryHolder <K, E> holder = null) { IsTimeout = false; Retry = 0; ShouldLogEventSource = true; if (holder != null) { Entity = holder.Entity; Key = holder.Key; KeyReference = holder.KeyReference; Settings = holder.Settings; ResponseCode = holder.ResponseCode; ResponseMessage = holder.ResponseMessage; TraceId = holder.TraceId; } Settings = Settings ?? new RepositorySettings(); }
public PersistencePayloadLogEvent(TransmissionPayload payload, RepositoryHolder request, RepositoryHolder response, LoggingLevel?level = null , Exception ex = null, DispatcherLoggerDirection?direction = null, TimeSpan?processingTime = null) : base(payload, level, ex, direction, processingTime) { if (request != null) { TraceId = request.TraceId; if (request.Settings != null) { BatchId = request.Settings.BatchId; Prefer = request.Settings.Prefer; Headers = request.Settings.Headers; } } if (response != null) { ResponseCode = response.ResponseCode; ResponseMessage = response.ResponseMessage; } }
public virtual async Task <IHttpActionResult> Search(ODataQueryOptions <E> request) { try { //Validate that the request is correctly formed. request.Validate(mODataValidate); //Load the request with the necessary parameters. SearchRequest rq = new SearchRequest(); rq.ODataPopulate(request); //Make the request to the persistence service. RepositorySettings settings = ApiUtility.BuildRepositorySettings(ActionContext); RepositoryHolder <SearchRequest, SearchResponse> response = await mRespository.Search(rq, settings); return(new OData4ServiceDocumentResponse(response, ActionContext.Request.RequestUri)); } catch (Exception vex) { return(BadRequest($"Unable to process Odata request - {vex.Message}")); } }
/// <summary> /// This method marshals the RepositoryHolder and transmits it to the remote Microservice. /// </summary> /// <typeparam Name="KT">The key type.</typeparam> /// <typeparam Name="ET">The entity type.</typeparam> /// <param name="actionType">The action type.</param> /// <param name="rq">The repository holder request.</param> /// <param name="routing"></param> /// <returns>Returns an async task that will be signalled when the request completes or times out.</returns> protected override async Task <RepositoryHolder <KT, ET> > TransmitInternal <KT, ET>(string actionType, RepositoryHolder <KT, ET> rq, ProcessOptions?routing = null, IPrincipal principal = null) { StatisticsInternal.ActiveIncrement(); var payloadRq = TransmissionPayload.Create(mPolicy.TransmissionPayloadTraceEnabled); // Set the originator key to the correlation id if passed through the rq settings if (!string.IsNullOrEmpty(rq.Settings?.CorrelationId)) { payloadRq.Message.ProcessCorrelationKey = rq.Settings.CorrelationId; } bool processAsync = rq.Settings?.ProcessAsync ?? false; payloadRq.Options = ProcessOptions.RouteInternal; var message = payloadRq.Message; payloadRq.MaxProcessingTime = rq.Settings?.WaitTime ?? mDefaultRequestTimespan; payloadRq.MessageObject = rq; message.ChannelId = ChannelId; message.ChannelPriority = processAsync ? 0:-1; message.MessageType = mMessageType; message.ActionType = actionType; message.ResponseChannelId = mResponseChannel; message.ResponseChannelPriority = -1; //Always internal message.Blob = PayloadSerializer.PayloadSerialize(rq); return(await OutgoingRequestOut(payloadRq, ProcessResponse <KT, ET>, processAsync : processAsync)); }
/// <summary> /// This abstract method is used to transmit the request to the appropriate party. /// </summary> /// <typeparam name="KT">The key type.</typeparam> /// <typeparam name="ET">The entity type.</typeparam> /// <param name="actionType">The request action type, i.e. Create/Read etc.</param> /// <param name="rq">The request.</param> /// <param name="routing">The routing options.</param> /// <returns>Returns the request response.</returns> protected abstract Task <RepositoryHolder <KT, ET> > TransmitInternal <KT, ET>(string actionType, RepositoryHolder <KT, ET> rq, ProcessOptions?routing = null, IPrincipal principal = null) where KT : IEquatable <KT>;
/// <summary> /// This method marshals the RepositoryHolder and transmits it to the remote Microservice. /// </summary> /// <typeparam Name="KT">The key type.</typeparam> /// <typeparam Name="ET">The entity type.</typeparam> /// <param Name="actionType">The action type.</param> /// <param Name="rq">The repository holder request.</param> /// <returns>Returns an async task that will be signalled when the request completes or times out.</returns> protected override async Task <RepositoryHolder <KT, ET> > TransmitInternal <KT, ET>(string actionType, RepositoryHolder <KT, ET> rq, ProcessOptions?routing = null) { try { StatisticsInternal.ActiveIncrement(); var payload = TransmissionPayload.Create(); // Set the originator key to the correlation id if passed through the rq settings if (rq.Settings != null && !string.IsNullOrEmpty(rq.Settings.CorrelationId)) { payload.Message.OriginatorKey = rq.Settings.CorrelationId; } bool processAsync = rq.Settings == null ? false : rq.Settings.ProcessAsync; payload.Message.ChannelPriority = processAsync ? 0 : 1; payload.Options = routing ?? RoutingDefault ?? ProcessOptions.RouteExternal; payload.Message.Blob = PayloadSerializer.PayloadSerialize(rq); payload.Message.ResponseChannelId = ResponseChannelId; payload.Message.ResponseChannelPriority = payload.Message.ChannelPriority; payload.Message.ChannelId = ChannelId; payload.Message.MessageType = EntityType; payload.Message.ActionType = actionType; payload.MaxProcessingTime = rq.Settings?.WaitTime ?? mDefaultRequestTimespan; return(await TransmitAsync(payload, ProcessResponse <KT, ET>, processAsync)); } catch (Exception ex) { string key = rq != null && rq.Key != null?rq.Key.ToString() : string.Empty; Logger.LogException(string.Format("Error transmitting {0}-{1} internally", actionType, key), ex); throw; } }
/// <summary> /// This method resolves the appropriate transport serialzer from the incoming accept header. /// </summary> /// <param name="rs">The response</param> /// <param name="data">The response content</param> /// <param name="holder">The repository holder</param> /// <returns>Returns true if the serializer can be resolved.</returns> protected virtual void EntityDeserialize(HttpResponseMessage rs, byte[] data, RepositoryHolder <K, E> holder) { string mediaType = rs.Content.Headers.ContentType.MediaType; if (mTransportSerializers.ContainsKey(mediaType)) { var transport = mTransportSerializers[mediaType]; holder.Entity = transport.GetObject(data); } else { throw new TransportSerializerResolutionException(mediaType); } }
/// <summary> /// This method calls the client using HTTP and returns the response along with the entity in the response if supplied. /// </summary> /// <typeparam name="KT">The key type.</typeparam> /// <typeparam name="ET">The entity type.</typeparam> /// <param name="uri">The request uri.</param> /// <param name="options">The repository settings passed from the caller.</param> /// <param name="content">The HttpContent to send to the API.</param> /// <param name="adjust">Any message adjustment.</param> /// <param name="mapper">Any response adjustment before returning to the caller.</param> /// <param name="deserializer">Deserialize the response content into the entity</param> /// <returns></returns> protected virtual async Task <RepositoryHolder <KT, ET> > CallClient <KT, ET>( KeyValuePair <HttpMethod, Uri> uri , RepositorySettings options , HttpContent content = null , Action <HttpRequestMessage> adjust = null , Action <HttpResponseMessage, RepositoryHolder <KT, ET> > mapper = null , Action <HttpResponseMessage, byte[], RepositoryHolder <KT, ET> > deserializer = null) { var response = new RepositoryHolder <KT, ET>(); try { HttpRequestMessage httpRq = Request(uri.Key, uri.Value); RequestHeadersSet(httpRq); RequestHeadersSetTransport(httpRq); RequestHeadersPreferSet(httpRq, options?.Prefer); RequestHeadersAuth(httpRq); adjust?.Invoke(httpRq); if (content != null) { httpRq.Content = content; } var client = new HttpClient(); // Specify request body var httpRs = await client.SendAsync(httpRq); ResponseHeadersAuth(httpRq, httpRs); //OK, set the response content if set if (httpRs.Content != null && httpRs.Content.Headers.ContentLength > 0) { byte[] httpRsContent = await httpRs.Content.ReadAsByteArrayAsync(); if (httpRs.IsSuccessStatusCode) { deserializer?.Invoke(httpRs, httpRsContent, response); } else { // So that we can see error messages such as schema validation fail response.ResponseMessage = Encoding.UTF8.GetString(httpRsContent); } } //Get any outgoing trace headers and set them in to the response. IEnumerable <string> trace; if (httpRs.Headers.TryGetValues(ApimConstants.AzureTraceHeaderLocation, out trace)) { response.Settings.Prefer.Add(ApimConstants.AzureTraceHeaderLocation, trace.First()); } response.ResponseCode = (int)httpRs.StatusCode; mapper?.Invoke(httpRs, response); } catch (Exception ex) { response.ResponseMessage = FormatExceptionChain(ex); response.ResponseCode = 503; } return(response); }
/// <summary> /// This method registers the specific persistence handler. /// </summary> /// <typeparam Name="KT">The key type.</typeparam> /// <typeparam Name="ET">The entity type.</typeparam> /// <param name="actionType">The action type identifier</param> /// <param name="action">The action to process the command.</param> /// <param name="logEventOnSuccess"></param> /// <param name="preaction"></param> /// <param name="postaction"></param> /// <param name="timeoutcorrect"></param> /// <param name="retryOnTimeout"></param> /// <param name="channelId"></param> /// <param name="entityType"></param> protected virtual void PersistenceCommandRegister <KT, ET>(string actionType , Func <PersistenceRequestHolder <KT, ET>, Task> action , bool logEventOnSuccess = false , Func <PersistenceRequestHolder <KT, ET>, Task <bool> > preaction = null , Func <PersistenceRequestHolder <KT, ET>, Task> postaction = null , Func <PersistenceRequestHolder <KT, ET>, Task <bool> > timeoutcorrect = null , int?retryOnTimeout = null , string channelId = null , string entityType = null ) { Func <TransmissionPayload, List <TransmissionPayload>, Task> actionPayload = async(incoming, outgoing) => { var profileHolder = ProfileStart <KT, ET>(incoming, outgoing); try { var rsMessage = incoming.Message.ToResponse(); rsMessage.ChannelId = incoming.Message.ResponseChannelId; rsMessage.ChannelPriority = incoming.Message.ResponseChannelPriority; rsMessage.MessageType = incoming.Message.MessageType; rsMessage.ActionType = ""; var rsPayload = new TransmissionPayload(rsMessage); bool hasTimedOut = false; try { RepositoryHolder <KT, ET> rqTemp = incoming.MessageObject as RepositoryHolder <KT, ET>; //Deserialize the incoming payloadRq request if (rqTemp == null) { rqTemp = PayloadSerializer.PayloadDeserialize <RepositoryHolder <KT, ET> >(incoming); } profileHolder.Rq = new PersistenceRepositoryHolder <KT, ET>(rqTemp); if (profileHolder.Rq.Timeout == null) { profileHolder.Rq.Timeout = TimeSpan.FromSeconds(10); } bool preactionFailed = false; try { bool retryExceeded = false; do { int attempt = Environment.TickCount; //Create the payloadRs holder, and discard any previous version. profileHolder.Rs = new PersistenceRepositoryHolder <KT, ET>(); if (preaction != null && !(await preaction(profileHolder))) { preactionFailed = true; break; } //Call the specific command to process the action, i.e Create, Read, Update, Delete ... etc. await action(profileHolder); //Flag if the request times out at any point. //This may be required later when checking whether the action was actually successful. hasTimedOut |= profileHolder.Rs.IsTimeout; //OK, if this is not a time out then it is successful if (!profileHolder.Rs.IsTimeout && !profileHolder.Rs.ShouldRetry) { break; } ProfileRetry(profileHolder, attempt); if (profileHolder.Rs.IsTimeout) { Logger.LogMessage(LoggingLevel.Warning, $"Timeout occured for {EntityType} {actionType} for request:{profileHolder.Rq} with response:{profileHolder.Rs}", "DBTimeout"); } profileHolder.Rq.IsRetry = true; //These should not be counted against the limit. if (!profileHolder.Rs.ShouldRetry) { profileHolder.Rq.Retry++; } profileHolder.Rq.IsTimeout = false; retryExceeded = incoming.Cancel.IsCancellationRequested || profileHolder.Rq.Retry > mPolicy.PersistenceRetryPolicy.GetMaximumRetries(incoming); }while (!retryExceeded); //Signal to the underlying comms channel that the message has been processed successfully. incoming.Signal(!retryExceeded); // If we have exceeded the retry limit then Log error if (retryExceeded) { Log(actionType , profileHolder , LoggingLevel.Error , $"Retry limit has been exceeded (cancelled ({incoming.Cancel.IsCancellationRequested})) for {EntityType} {actionType} for {profileHolder.Rq} after {incoming.Message?.FabricDeliveryCount} delivery attempts" , "DBRetry"); profileHolder.result = ResourceRequestResult.RetryExceeded; } } catch (Exception ex) { LogException(actionType, profileHolder, ex); incoming.SignalFail(); profileHolder.result = ResourceRequestResult.Exception; } bool logEventSource = !preactionFailed && logEventOnSuccess && profileHolder.Rs.IsSuccess; if (!profileHolder.Rs.IsSuccess && hasTimedOut && timeoutcorrect != null && profileHolder.result != ResourceRequestResult.Exception) { if (await timeoutcorrect(profileHolder)) { logEventSource = true; Logger.LogMessage(LoggingLevel.Info , string.Format("Recovered timeout sucessfully for {0}-{1} for request:{2} - response:{3}", EntityType, actionType, profileHolder.Rq, profileHolder.Rs) , "DBTimeout"); } else { Logger.LogMessage(LoggingLevel.Error , string.Format("Not recovered timeout for {0}-{1} for request:{2} - response:{3}", EntityType, actionType, profileHolder.Rq, profileHolder.Rs) , "DBTimeout"); } } if (logEventSource && profileHolder.Rs.ShouldLogEventSource) { await LogEventSource(actionType, profileHolder); } if (!preactionFailed && postaction != null) { await postaction(profileHolder); } //Serialize the payloadRs var reposHolder = profileHolder.Rs.ToRepositoryHolder(); rsPayload.MessageObject = reposHolder; rsPayload.Message.Blob = PayloadSerializer.PayloadSerialize(reposHolder); rsPayload.Message.Status = "200"; if (!profileHolder.result.HasValue) { profileHolder.result = ResourceRequestResult.Success; } } catch (Exception ex) { incoming.SignalFail(); rsPayload.Message.Status = "500"; rsPayload.Message.StatusDescription = ex.Message; Logger.LogException($"Error processing message (was cancelled({incoming.Cancel.IsCancellationRequested}))-{EntityType}-{actionType}-{profileHolder.Rq}", ex); profileHolder.result = ResourceRequestResult.Exception; } // check whether we need to send a response message. If this is async and AsyncResponse is not set to true, // then by default we do not send a response message to cut down on unnecessary traffic. if (profileHolder.Rq == null || profileHolder.Rq.Settings == null || !profileHolder.Rq.Settings.ProcessAsync) { outgoing.Add(rsPayload); } } finally { ProfileEnd(profileHolder); } }; if (channelId == null) { channelId = ChannelId ?? string.Empty; } if (entityType == null) { entityType = EntityType; } CommandRegister( channelId.ToLowerInvariant() , entityType.ToLowerInvariant() , actionType.ToLowerInvariant() , actionPayload); }
/// <summary> /// This method marshals the RepositoryHolder and transmits it to the remote Microservice. /// </summary> /// <typeparam Name="KT">The key type.</typeparam> /// <typeparam Name="ET">The entity type.</typeparam> /// <param name="actionType">The action type.</param> /// <param name="rq">The repository holder request.</param> /// <param name="routing"></param> /// <returns>Returns an async task that will be signalled when the request completes or times out.</returns> protected override async Task <RepositoryHolder <KT, ET> > TransmitInternal <KT, ET>(string actionType, RepositoryHolder <KT, ET> rq, ProcessOptions?routing = null) { StatisticsInternal.ActiveIncrement(); var payloadRq = TransmissionPayload.Create(); bool processAsync = rq.Settings?.ProcessAsync ?? false; payloadRq.Options = ProcessOptions.RouteInternal; var message = payloadRq.Message; payloadRq.MaxProcessingTime = rq.Settings?.WaitTime ?? mDefaultRequestTimespan; payloadRq.MessageObject = rq; message.ChannelId = ChannelId; message.ChannelPriority = processAsync ? 0:-1; message.MessageType = mMessageType; message.ActionType = actionType; message.ResponseChannelId = mResponseChannel; message.ResponseChannelPriority = -1; //Always internal message.Blob = PayloadSerializer.PayloadSerialize(rq); return(await TransmitAsync(payloadRq, ProcessResponse <KT, ET>, processAsync : processAsync)); }
public PersistenceRepositoryHolder(RepositoryHolder <object, object> rqTemp) { this.rqTemp = rqTemp; }
/// <summary> /// This method processes the outgoing response. /// </summary> /// <typeparam name="KT">The key type.</typeparam> /// <typeparam name="ET">The entity type.</typeparam> /// <param name="responseHolder">The response message.</param> /// <param name="entitySerializer">The optional serializer.</param> /// <returns>Returns the relevant result.</returns> protected virtual IHttpActionResult ResponseFormat <KT, ET>(RepositoryHolder <KT, ET> responseHolder , TransportSerializer <ET> entitySerializer = null) { ApiResponse response = null; switch (responseHolder.ResponseCode) { case 200: response = new ApiResponse(HttpStatusCode.OK); break; case 201: response = new ApiResponse(HttpStatusCode.Created); break; case 202: response = new ApiResponse(HttpStatusCode.Accepted); break; case 204: response = new ApiResponse(HttpStatusCode.NoContent); break; case 400: return(BadRequest(responseHolder.ResponseMessage ?? string.Empty)); case 404: return(StatusCode(HttpStatusCode.NotFound)); case 405: return(StatusCode(HttpStatusCode.MethodNotAllowed)); case 408: return(StatusCode(HttpStatusCode.RequestTimeout)); case 409: return(StatusCode(HttpStatusCode.Conflict)); case 412: return(StatusCode(HttpStatusCode.PreconditionFailed)); case 417: return(StatusCode(HttpStatusCode.ExpectationFailed)); case 442: return(StatusCode(HttpStatusCode.UnsupportedMediaType)); case 502: return(StatusCode(HttpStatusCode.BadGateway)); case 504: return(StatusCode(HttpStatusCode.GatewayTimeout)); default: LogMessage(LoggingLevel.Error, $"Unexpected error occurred {responseHolder}"); return(StatusCode(HttpStatusCode.InternalServerError)); } if (entitySerializer != null && responseHolder.Entity != null) { response.MediaType = entitySerializer.MediaType; response.Data = entitySerializer.GetData(responseHolder.Entity); } if (responseHolder.KeyReference != null) { response.ContentId = responseHolder.KeyReference.Item1; response.VersionId = responseHolder.KeyReference.Item2; } if (responseHolder.IsCached) { response.IsCached = true; } return(response); }
public OData4ServiceDocumentResponse(RepositoryHolder <SearchRequest, SearchResponse> response, Uri requestUri) { mRequest = requestUri; mResponse = response; }
/// <summary> /// This method calls the client using HTTP and returns the response along with the entity in the response if supplied. /// </summary> /// <typeparam name="KT">The key type.</typeparam> /// <typeparam name="ET">The entity type.</typeparam> /// <param name="uri">The request uri.</param> /// <param name="options">The repository settings passed from the caller.</param> /// <param name="content">The HttpContent to send to the API.</param> /// <param name="adjust">Any message adjustment.</param> /// <param name="mapper">Any response adjustment before returning to the caller.</param> /// <param name="deserializer">Deserialize the response content into the entity</param> /// <returns></returns> protected virtual async Task <RepositoryHolder <KT, ET> > CallClient <KT, ET>( KeyValuePair <HttpMethod, Uri> uri , RepositorySettings options , HttpContent content = null , Action <HttpRequestMessage> adjust = null , Action <HttpResponseMessage, RepositoryHolder <KT, ET> > mapper = null , Action <HttpResponseMessage, byte[], RepositoryHolder <KT, ET> > deserializer = null) { var rs = new RepositoryHolder <KT, ET>(); try { HttpRequestMessage rq = Request(uri.Key, uri.Value); adjust?.Invoke(rq); if (content != null) { rq.Content = content; } if (options?.Prefer != null && options.Prefer.Count > 0) { rq.Headers.Add("Prefer", options.Prefer.Select((k) => string.Format("{0}={1}", k.Key, k.Value))); } var client = new HttpClient(); // Specify request body var response = await client.SendAsync(rq); if (response.Content != null && response.Content.Headers.ContentLength > 0) { byte[] rsContent = await response.Content.ReadAsByteArrayAsync(); if (response.IsSuccessStatusCode) { deserializer?.Invoke(response, rsContent, rs); } else { // So that we can see error messages such as schema validation fail rs.ResponseMessage = Encoding.UTF8.GetString(rsContent); } } //Get any outgoing trace headers and set them in to the response. IEnumerable <string> trace; if (response.Headers.TryGetValues(ApimConstants.AzureTraceHeaderLocation, out trace)) { rs.Settings.Prefer.Add(ApimConstants.AzureTraceHeaderLocation, trace.First()); } rs.ResponseCode = (int)response.StatusCode; mapper?.Invoke(response, rs); } catch (Exception ex) { rs.ResponseMessage = FormatExceptionChain(ex); rs.ResponseCode = 503; } return(rs); }