I'm trying to call two rest APIs (Azure Web Service APIs) in a Azure Function. I have written the below code but When I try to run it, the first API is only getting executed but the second one isn't.
The Logic should be like if the first API gets a response status as 200 Ok then only, it should proceed to call the next API.
namespace FunctionChainingApp
{
public static class FunctionChaining
{
[FunctionName("FunctionChaining")]
public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)]HttpRequestMessage req, ILogger log)
{
log.LogInformation("C# HTTP trigger function processed a request.");
try
{
// Call Your API
HttpClient abcdeClient = new HttpClient();
HttpRequestMessage abcdeRequest = new HttpRequestMessage(HttpMethod.Get, string.Format("https://abcde.azurewebsites.net/api/abcde/GetabcdeDetails"));
//Read Server Response
HttpResponseMessage abcdeResponse = await abcdeClient.SendAsync(abcdeRequest);
bool abcdeStatusCode200 = await abcdeResponse.Content.ReadAsAsync<bool>();
if (abcdeStatusCode200 == true)
{
// Call Your API
HttpClient vwxyzClient = new HttpClient();
HttpRequestMessage vwxyzRequest = new HttpRequestMessage(HttpMethod.Get, string.Format("https://vwxyz.azurewebsites.net/api/vwxyz/GetvwxyzDefaultDetails"));
//Read Server Response
HttpResponseMessage vwxyzResponse = await vwxyzClient.SendAsync(vwxyzRequest);
bool vwxyzStatusCode200 = await vwxyzResponse.Content.ReadAsAsync<bool>();
}
else
{
// Call Your API
abcdeRequest = new HttpRequestMessage(HttpMethod.Get, string.Format("https://abcde.azurewebsites.net/api/abcde/GetabcdeDetails"));
}
return req.CreateResponse(HttpStatusCode.OK);
}
catch (Exception ex)
{
return req.CreateResponse(HttpStatusCode.OK, "The Called Scheduler Failed : {0}", string.Format(ex.Message));
}
}
}
public class ResponseModel
{
public bool abcdeStatusCode200 { get; set; }
public bool vwxyzStatusCode200 { get; set; }
}
}
You need to separate these two functions and call them within DurableOrchestrationClient
var output = new List<string>();
output.Add(await context.CallActivityAsync<string>("CallAPI1", "test2019"));
output.Add(await context.CallActivityAsync<string>("CallAPI2", "test2"));
EXAMPLE
Related
I am following the documentation here to return the blocking response https://learn.microsoft.com/en-us/azure/active-directory-b2c/add-api-connector?pivots=b2c-user-flow#example-of-a-blocking-response from api connector to azure ad b2c, however even after constructing the right response as shown in the documentation, I am still not able to show the blocking page for b2c user flow.
Note that this connector gets invoked at sign-in.
I have verified that the response from api which seems correct and looks like below
{
"version": "1.0.0",
"action": "ShowBlockPage",
"userMessage": "You must have a local account registered for Contoso."
}
With this, was hoping to see a blocking page as below (screenshot from docs) but b2c does not show it and goes straight to the connected application.
What did I miss? any pointers would be appreciated. TIA.
Here is my api connector's code
public static async Task<IActionResult> Run(HttpRequest req, ILogger log)
{
log.LogInformation("C# HTTP trigger function processed a request.");
// Get the request body
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
dynamic data = JsonConvert.DeserializeObject(requestBody);
// If input data is null, show block page
if (data == null)
{
return (ActionResult)new OkObjectResult(new ResponseContent("ShowBlockPage", "There was a problem with your request."));
}
// Print out the request body
log.LogInformation("Request body: " + requestBody);
// check for issuer
if(data.identities != null)
{
string issuer = data.identities[0].issuer;
log.LogInformation("issuer detected: " + issuer);
if(issuer == "github.com")
{
log.LogInformation("Returning an error!");
//return (ActionResult)new BadRequestObjectResult(new ResponseContent("ValidationError", "Please provide a Display Name with at least five characters."));
return (ActionResult)new OkObjectResult(new ResponseContent("ShowBlockPage", "You must have a local account registered for Contoso."));
}
}
// Validation passed successfully, return `Allow` response.
return (ActionResult)new OkObjectResult(new ResponseContent()
{
jobTitle = "This value return by the API Connector"//,
// You can also return custom claims using extension properties.
//extension_CustomClaim = "my custom claim response"
});
}
and here is the ResponseContent class
public class ResponseContent
{
public const string ApiVersion = "1.0.0";
public ResponseContent()
{
this.version = ResponseContent.ApiVersion;
this.action = "Continue";
}
public ResponseContent(string action, string userMessage)
{
this.version = ResponseContent.ApiVersion;
this.action = action;
this.userMessage = userMessage;
if (action == "ValidationError")
{
this.status = "400";
}
}
public string version { get; }
public string action { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string userMessage { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string status { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string jobTitle { get; set; }
}
We ended up creating Microsoft support ticket for this and got response that this is by design. ShowBlockPage response works only with sign-up user flows and not with sign-in user flows.
I have an implementation, where I am calling an autoquery operation via the service gateway. The service gateway will successfully call both internal and external operations. However, any autoquery operation fails because it is not getting the connection string set. These same autoquery operations work just fine when called directly and not through the gateway.
Here is the stack trace.
at ServiceStack.OrmLite.OrmLiteConnectionFactory.CreateDbConnection() in C:\\BuildAgent\\work\\27e4cc16641be8c0\\src\\ServiceStack.OrmLite\\OrmLiteConnectionFactory.cs:line 70\r\n at ServiceStack.OrmLite.OrmLiteConnectionFactory.OpenDbConnection() in C:\\BuildAgent\\work\\27e4cc16641be8c0\\src\\ServiceStack.OrmLite\\OrmLiteConnectionFactory.cs:line 95\r\n at ServiceStack.ServiceStackHost.GetDbConnection(IRequest req) in C:\\BuildAgent\\work\\3481147c480f4a2f\\src\\ServiceStack\\ServiceStackHost.Runtime.cs:line 691\r\n at ServiceStack.AutoQuery.GetDb(Type type, IRequest req) in C:\\BuildAgent\\work\\3481147c480f4a2f\\src\\ServiceStack.Server\\AutoQueryFeature.cs:line 598\r\n at ServiceStack.AutoQuery.CreateQuery[From](IQueryDb`1 dto, Dictionary`2 dynamicParams, IRequest req) in C:\\BuildAgent\\work\\3481147c480f4a2f\\src\\ServiceStack.Server\\AutoQueryFeature.cs:line 608\r\n at IDOE.SecurityPortal.Api.ServiceInterface.OrganizationUserStaffTypeService.Get(QueryOrganizationUserStaffTypes query) in E:\\source\\repos\\Azure - Security Portal\\src\\IDOE.SecurityPortal\\IDOE.SecurityPortal.Api.ServiceInterface\\OrganizationUserStaffTypeService.cs:line 47\r\n at ServiceStack.Host.ServiceRunner`1.<ExecuteAsync>d__15.MoveNext() in C:\\BuildAgent\\work\\3481147c480f4a2f\\src\\ServiceStack\\Host\\ServiceRunner.cs:line 133
Database Connection Registration in startup.cs
var dbFacotry = container.Resolve<IDbConnectionFactory>();
dbFacotry.RegisterConnection("SecPortal", AppSettings.Get<string>("SQLSERVER-SECPORTAL-CONNECTIONSTRING"), SqlServer2017Dialect.Provider);
dbFacotry.RegisterConnection("EdfiMdm", AppSettings.Get<string>("SQLSERVER-EDFIMDM-CONNECTIONSTRING"), SqlServer2017Dialect.Provider);
Plugins.Add(new AutoQueryFeature { IncludeTotal = true });
AutoQuery Definition
[Authenticate]
[RequiredClaim("scope", "secprtl-read")]
[Route("/files", Verbs = "GET")]
[ConnectionInfo(NamedConnection = "SecPortal")]
public class QueryFiles : QueryDb<Types.File>
{
[QueryDbField(Field = "Id", Template = "({Value} IS NULL OR {Field} = {Value})")]
public int? Id { get; set; }
[QueryDbField(Field = "FileName", Template = "({Value} IS NULL OR UPPER({Field}) LIKE UPPER({Value}))", ValueFormat = "%{0}%")]
public string FileName { get; set; }
[QueryDbField(Field = "UserId", Template = "({Value} IS NULL OR UPPER({Field}) LIKE UPPER({Value}))", ValueFormat = "%{0}%")]
public string UserId { get; set; }
[QueryDbField(Field = "StateOrganizationId", Template = "({Value} IS NULL OR UPPER({Field}) LIKE UPPER({Value}))", ValueFormat = "%{0}%")]
public string StateOrganizationId { get; set; }
[QueryDbField(Field = "Notes", Template = "({Value} IS NULL OR UPPER({Field}) LIKE UPPER({Value}))", ValueFormat = "%{0}%")]
public string Notes { get; set; }
}
Code calling the service
public class ContactService : Service
{
public ContactService()
{
}
public async Task<object> Post(PostContact request)
{
try
{
var files = base.Gateway.Send(new QueryFiles() { });
return new Contact() { Name = request.Name };
}
catch (Exception ex)
{
throw ex;
}
}
}
Custom Service Gateway
public class CustomServiceGatewayFactory : ServiceGatewayFactoryBase
{
private IRequest request;
public override IServiceGateway GetServiceGateway(IRequest request)
{
this.request = request;
return base.GetServiceGateway(request);
}
public override IServiceGateway GetGateway(Type requestType)
{
var isLocal = HostContext.Metadata.RequestTypes.Contains(requestType);
if (isLocal)
{
return base.localGateway;
}
else
{
return new JsonServiceClient("https://localhost:6001")
{
BearerToken = request.GetBearerToken()
};
}
}
}
Custom service gateway registration in startup.cs
container.Register<IServiceGatewayFactory>(x => new CustomServiceGatewayFactory()).ReusedWithin(ReuseScope.None);
The call being made in the service class is a local call. Calling an external service that uses autoquery works just fine. I can also call the local service directly with no problem.
I created a custom autoquery method in the service interface, I noticed that the db connection info was not populated on the request.items array. So I manually added that information to the request, and it worked as expected. So somehow, in my setup, the autoquery operations that are called locally, the db connection info is not getting added to the request object.
Request Filter Attributes like [ConnectionInfo] is only applied on HTTP Requests, not internal Service Gateway requests.
The Connection info isn't configured because it's not annotated on your PostContact Service Request DTO that calls the in-procces Service Gateway.
You can have the [ConnectionInfo] on the QueryFiles AutoQuery Request DTO attached to the current Request with:
public async Task<object> Post(PostContact request)
{
try
{
typeof(QueryFiles).FirstAttribute<ConnectionInfoAttribute>()
.Execute(Request,Response,request);
var files = base.Gateway.Send(new QueryFiles() { });
return new Contact() { Name = request.Name };
}
catch (Exception ex)
{
throw ex;
}
}
I have HTTP trigger which invokes orchestra function. In HTTP trigger function I receive json string which I deserialize into object . However I have no idea how to pass this object to orchestra function or activity functions.
I have tried to include it as parameter of orchestra function but it gives me error
HTTP trigger
#load "../Shared/jsonObject.csx"
public static async Task<HttpResponseMessage> Run(
HttpRequestMessage req,
DurableOrchestrationClient starter,
string functionName,
ILogger log)
{
HttpContent requestContent = req.Content;
string jsonContent = requestContent.ReadAsStringAsync().Result;
jsonObject bsObj = JsonConvert.DeserializeObject<jsonObject> (jsonContent);
// Pass the function name as part of the route
string instanceId = await starter.StartNewAsync("Orchestra", bsObj);
log.LogInformation($"Started orchestration with ID = '{instanceId}'.");
return starter.CreateCheckStatusResponse(req, instanceId);
}
Orchestra function
#load "../Shared/jsonObject.csx"
[FunctionName("Orchestra")]
public static async void Run(DurableOrchestrationContext context)
{
var output = await context.CallActivityAsync<int>("checkConditions", bsObj);
}
Activity functionn not implemented yet
#load "../Shared/jsonObject.csx"
public static string Run(jsonObject bsObj)
{
return "done";
}
[Error] run.csx(19,74): error CS0103: The name 'bsObj' does not exist in the current context
Please try running the below code
HTTP Trigger function.
[FunctionName("HttpTriggerCSharp")]
public static async Task<HttpResponseMessage> Run(
[HttpTrigger(AuthorizationLevel.Function, "post")]HttpRequestMessage req,
[OrchestrationClient]DurableOrchestrationClient starter,
ILogger log)`
{
HttpContent requestContent = req.Content;
string jsonContent = requestContent.ReadAsStringAsync().Result;
JObject bsObj = JsonConvert.DeserializeObject<JObject>(jsonContent);
// Pass the function name as part of the route
string instanceId = await starter.StartNewAsync("Orchestra", bsObj);
log.LogInformation($"Started orchestration with ID = '{instanceId}'.");
return starter.CreateCheckStatusResponse(req, instanceId);
}
Orchestration Function
[FunctionName("Orchestra")]
public static async Task RunOrchestrator(
[OrchestrationTrigger] DurableOrchestrationContext context)
{
var bsObj = context.GetInput<JObject>();
var output = await context.CallActivityAsync<JObject>("checkConditions", bsObj);
}
Activity Function
[FunctionName("checkConditions")]
public static string SayHello([ActivityTrigger] JObject bsObj, ILogger log)
{
//Assign input value to any variable
var json = bsObj;
//Logic of the code
return "done";
}
I have referenced this doc for durable functions extention and this doc for HTTP Triggered functions.
I am trying to implement service gateway pattern according to Service Gateway tutorial to support in-process handing via InProcessServiceGateway and external calling via JsonServiceClient in case ServiceStack service is deployed standalone. I use ServiceStack 4.5.8 version.
Validation feature works fine, but with InProcessServiceGateway, the failed validation throws ValidationException which directly results in a ServiceStack.FluentValidation.ValidationException in the client rather than populating ResponseStatus property of MyResponseDto. And I also tried GlobalRequestFilters and ServiceExceptionHandlers, both of them seem to work fine to capture ValidationException only with JsonHttpClient but InProcessServiceGateway.
Is there any way to make ValidationException thrown by InProcessServiceGateway captured and translated into Dto's ResponseStatus? Thanks.
My AppHost:
//Register CustomServiceGatewayFactory
container.Register<IServiceGatewayFactory>(x => new CustomServiceGatewayFactory()).ReusedWithin(ReuseScope.None);
//Validation feature
Plugins.Add(new ValidationFeature());
//Note: InProcessServiceGateway cannot reach here.
GlobalRequestFilters.Add((httpReq, httpRes, requestDto) =>
{
...
});
//Note: InProcessServiceGateway cannot reach here.
ServiceExceptionHandlers.Add((httpReq, request, ex) =>
{
...
});
My CustomServiceGatewayFactory:
public class CustomServiceGatewayFactory : ServiceGatewayFactoryBase
{
private IRequest _req;
public override IServiceGateway GetServiceGateway(IRequest request)
{
_req = request;
return base.GetServiceGateway(request);
}
public override IServiceGateway GetGateway(Type requestType)
{
var standaloneHosted = false;
var apiBaseUrl = string.Empty;
var apiSettings = _req.TryResolve<ApiSettings>();
if (apiSettings != null)
{
apiBaseUrl = apiSettings.ApiBaseUrl;
standaloneHosted = apiSettings.StandaloneHosted;
}
var gateway = !standaloneHosted
? (IServiceGateway)base.localGateway
: new JsonServiceClient(apiBaseUrl)
{
BearerToken = _req.GetBearerToken()
};
return gateway;
}
}
My client base controller (ASP.NET Web API):
public virtual IServiceGateway ApiGateway
{
get
{
var serviceGatewayFactory = HostContext.AppHost.TryResolve<IServiceGatewayFactory>();
var serviceGateway = serviceGatewayFactory.GetServiceGateway(HttpContext.Request.ToRequest());
return serviceGateway;
}
}
My client controller action (ASP.NET Web API):
var response = ApiGateway.Send<UpdateCustomerResponse>(new UpdateCustomer
{
CustomerGuid = customerGuid,
MobilePhoneNumber = mobilePhoneNumber
ValidationCode = validationCode
});
if (!response.Success)
{
return this.Error(response, response.ResponseStatus.Message);
}
My UpdateCustomer request DTO:
[Route("/customers/{CustomerGuid}", "PUT")]
public class UpdateCustomer : IPut, IReturn<UpdateCustomerResponse>
{
public Guid CustomerGuid { get; set; }
public string MobilePhoneNumber { get; set; }
public string ValidationCode { get; set; }
}
My UpdateCustomerValidator:
public class UpdateCustomerValidator : AbstractValidator<UpdateCustomer>
{
public UpdateCustomerValidator(ILocalizationService localizationService)
{
ValidatorOptions.CascadeMode = CascadeMode.StopOnFirstFailure;
RuleFor(x => x.ValidationCode)
.NotEmpty()
.When(x => !string.IsNullOrWhiteSpace(x.MobilePhoneNumber))
.WithErrorCode(((int)ErrorCode.CUSTOMER_VALIDATIONCODE_EMPTY).ToString())
.WithMessage(ErrorCode.CUSTOMER_VALIDATIONCODE_EMPTY.GetLocalizedEnum(localizationService, Constants.LANGUAGE_ID));
}
}
My UpdateCustomerResponse DTO:
public class UpdateCustomerResponse
{
/// <summary>
/// Return true if successful; return false, if any error occurs.
/// </summary>
public bool Success { get; set; }
/// <summary>
/// Represents error details, populated only when any error occurs.
/// </summary>
public ResponseStatus ResponseStatus { get; set; }
}
ServiceStack 4.5.8's InProcessServiceGateway source code:
private TResponse ExecSync<TResponse>(object request)
{
foreach (var filter in HostContext.AppHost.GatewayRequestFilters)
{
filter(req, request);
if (req.Response.IsClosed)
return default(TResponse);
}
if (HostContext.HasPlugin<ValidationFeature>())
{
var validator = ValidatorCache.GetValidator(req, request.GetType());
if (validator != null)
{
var ruleSet = (string)(req.GetItem(Keywords.InvokeVerb) ?? req.Verb);
var result = validator.Validate(new ValidationContext(
request, null, new MultiRuleSetValidatorSelector(ruleSet)) {
Request = req
});
if (!result.IsValid)
throw new ValidationException(result.Errors);
}
}
var response = HostContext.ServiceController.Execute(request, req);
var responseTask = response as Task;
if (responseTask != null)
response = responseTask.GetResult();
return ConvertToResponse<TResponse>(response);
}
ServiceStack's Service Gateways now convert validation exceptions into WebServiceExceptions from this commit which is available from v4.5.13 that's now available on MyGet.
I am trying to POST to my ServiceStack service and retrieve the Location header from the response of my CREATED entity. I am not sure whether using IReturn is valid but I am not sure how to access the Response headers from my client.
Can someone help me understand how to interact with the HttpResult properly? There is a test case at the bottom of the code to demonstrate what I want to do.
Here's the codz:
public class ServiceStackSpike
{
public class AppHost : AppHostHttpListenerBase
{
public AppHost() : base("TODOs Tests", typeof(Todo).Assembly) { }
public override void Configure(Container container)
{
//noop
}
}
[Route("/todos", "POST")]
public class Todo:IReturn<HttpResult>
{
public long Id { get; set; }
public string Content { get; set; }
public int Order { get; set; }
public bool Done { get; set; }
}
public class TodosService : Service
{
public object Post(Todo todo)
{
//do stuff here
var result = new HttpResult(todo,HttpStatusCode.Created);
result.Headers[HttpHeaders.Location] = "/tada";
return result;
}
}
public class NewApiTodosTests : IDisposable
{
const string BaseUri = "http://localhost:82/";
AppHost appHost;
public NewApiTodosTests()
{
appHost = new AppHost();
appHost.Init();
appHost.Start(BaseUri);
}
[Fact]
public void Run()
{
var restClient = new JsonServiceClient(BaseUri);
var todo = restClient.Post(new Todo { Content = "New TODO", Order = 1 });
Assert.Equal(todo.Headers[HttpHeaders.Location], "/tada"); //=>fail
}
public void Dispose()
{
appHost.Dispose();
appHost = null;
}
}
}
See the Customizing HTTP Responses ServiceStack wiki page for all the different ways of customizing the HTTP Response.
A HttpResult is just one way customize the HTTP Response. You generally want to include the Absolute Url if you're going to redirect it, e.g:
public object Post(Todo todo)
{
var todo = ...;
return new HttpResult(todo, HttpStatusCode.Created) {
Location = base.Request.AbsoluteUri.CombineWith("/tada")
};
}
Note HTTP Clients will never see a HttpResult DTO. HttpResult is not a DTO itself, it's only purpose is to capture and modify the customized HTTP Response you want.
All ServiceStack Clients will return is the HTTP Body, which in this case is the Todo Response DTO. The Location is indeed added to the HTTP Response headers, and to see the entire HTTP Response returned you should use a HTTP sniffer like Fiddler, WireShark or Chrome's WebInspector.
If you want to access it using ServiceStack's HTTP Clients, you will need to add a Response Filter that gives you access to the HttpWebResponse, e.g:
restClient.ResponseFilter = httpRes => {
Assert.Equal(httpRes.Headers[HttpHeaders.Location], "/tada");
};
Todo todo = restClient.Post(new Todo { Content = "New TODO", Order = 1 });
Inspecting Response Headers using Web Request Extensions
Another lightweight alternative if you just want to inspect the HTTP Response is to use ServiceStack's Convenient WebRequest extension methods, e.g:
var url = "http://path/to/service";
var json = url.GetJsonFromUrl(httpRes => {
Assert.Equal(httpRes.Headers[HttpHeaders.Location], "/tada");
});