OpenTelemetry & correlation support for Azure Functions - azure

I have a project written in .net 6. I use Azure Function to send data to Service bus and MediatR. How can I add Open Telemetry to my Azure functions?
[Function("EBcktErroredFunction")]
public Task EBcktErroredFunctionAsync([ServiceBusTrigger("e-bckt-errored", "bckts-creation-finalize")] EBcktErrored payload)
{
return _mediator.Send(
new SyncBcktErroredToStoreCommand
{
SBcktId = payload.BcktId,
EBcktId = payload.ExternalBcktId
}
);
Not related but in the bicep file it looks like this;
resource eBcktErroredSubscription 'Microsoft.ServiceBus/namespaces/topics/subscriptions' = {
name: '${serviceBusName}/e-bckt-errored/${appName}'
properties: {
autoDeleteOnIdle: ''
deadLetteringOnMessageExpiration:
defaultMessageTimeToLive: ''
enableBatchedOperations: true
lockDuration: ''
}
}

You can find a full example on GitHub and a corresponding discussion about "Using OpenTelemetry Sdk with Azure Functions".
Most relevant code (you should look at the full code for reference):
public class Startup : FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder)
{
// OpenTelemetry Resource to be associated with logs, metrics and traces
var openTelemetryResourceBuilder = ResourceBuilder.CreateDefault().AddService("opentelemetry-service");
// Enable Logging with OpenTelemetry
builder.Services.AddLogging( (loggingBuilder) =>
{
// Only Warning or above will be sent to Opentelemetry
loggingBuilder.AddFilter<OpenTelemetryLoggerProvider>("*", LogLevel.Warning);
}
);
builder.Services.AddSingleton<ILoggerProvider, OpenTelemetryLoggerProvider>();
builder.Services.Configure<OpenTelemetryLoggerOptions>( (openTelemetryLoggerOptions) =>
{
openTelemetryLoggerOptions.SetResourceBuilder(openTelemetryResourceBuilder);
openTelemetryLoggerOptions.IncludeFormattedMessage = true;
openTelemetryLoggerOptions.AddConsoleExporter();
}
);
// Enable Tracing with OpenTelemetry
var openTelemetryTracerProvider = Sdk.CreateTracerProviderBuilder()
.SetResourceBuilder(openTelemetryResourceBuilder)
.SetSampler(new AlwaysOnSampler())
.AddAspNetCoreInstrumentation()
.AddConsoleExporter()
.Build();
builder.Services.AddSingleton(openTelemetryTracerProvider);
// Enable Metrics with OpenTelemetry
var openTelemetryMeterProvider = Sdk.CreateMeterProviderBuilder()
.SetResourceBuilder(openTelemetryResourceBuilder)
.AddAspNetCoreInstrumentation()
.AddMeter(Function1.MyMeter.Name)
.AddConsoleExporter(consoleOptions =>
{
consoleOptions.MetricReaderType = MetricReaderType.Periodic;
consoleOptions.AggregationTemporality = AggregationTemporality.Cumulative;
consoleOptions.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = 10000;
})
.Build();
builder.Services.AddSingleton(openTelemetryMeterProvider);
[FunctionName("Function1")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
ILogger log)
{
log.LogWarning("C# HTTP trigger function processed a request.");
MyCounter.Add(1, new("name", "apple"), new("color", "red"));
string name = req.Query["name"];
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
dynamic data = JsonConvert.DeserializeObject(requestBody);
name = name ?? data?.name;
string responseMessage = string.IsNullOrEmpty(name)
? "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response."
: $"Hello, {name}. This HTTP triggered function executed successfully.";
log.LogWarning("Name is {name}", name);
Activity.Current?.SetTag("name", name);
return new OkObjectResult(responseMessage);
}

Related

Access FunctionAppDirectory in .NET 5 Azure Function

I need to have access to FunctionAppDirectory in Azure Function
Here is a simplified version of the function
[Function("Test")]
public static HttpResponseData Test([HttpTrigger(AuthorizationLevel.Function, "post", Route = "Test")] HttpRequestData req,
ExecutionContext context, FunctionContext fContext)
{
var log = fContext.GetLogger(nameof(TestOperations));
log.LogInformation(context?.FunctionAppDirectory?.ToString());
return req.CreateResponse(HttpStatusCode.OK);
}
ExecutionContext here is null.
My Program.cs file
class Program
{
static Task Main(string[] args)
{
var host = new HostBuilder()
.ConfigureAppConfiguration(configurationBuilder =>
{
configurationBuilder.AddCommandLine(args);
})
.ConfigureFunctionsWorkerDefaults()
.ConfigureServices(services =>
{
// Add Logging
services.AddLogging();
})
.Build();
return host.RunAsync();
}
}
Azure Function running in .NET 5
How I can configure binding for ExecutionContext or get FunctionAppDirectory in other ways?
As Alex mentioned in the comment, azure function .net 5 is not support 'context.FunctionAppDirectory' to get the directory of function app now.
In function app 3.0, the 'ExecutionContext' is be designed in the package 'Microsoft.Azure.WebJobs'. But in your code, the ExecutionContext is from 'System.Threading'. So these are different classes.
You can use below code to get the azure function directory in .NET 5.0 azure function:
using System;
using System.Collections.Generic;
using System.Net;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;
namespace FunctionApp6
{
public static class Function1
{
[Function("Function1")]
public static HttpResponseData Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req,
FunctionContext executionContext)
{
var logger = executionContext.GetLogger("Function1");
logger.LogInformation("C# HTTP trigger function processed a request.");
var response = req.CreateResponse(HttpStatusCode.OK);
response.Headers.Add("Content-Type", "text/plain; charset=utf-8");
var local_root = Environment.GetEnvironmentVariable("AzureWebJobsScriptRoot");
var azure_root = $"{Environment.GetEnvironmentVariable("HOME")}/site/wwwroot";
var actual_root = local_root ?? azure_root;
response.WriteString(actual_root);
return response;
}
}
}

Testing a Multipart file upload Azure Function

So I have written a simple Azure Function (AF) that accepts (via Http Post method) an IFormCollection, loops through the file collection, pushes each file into an Azure Blob storage container and returns the url to each file.
The function itself works perfectly when I do a single file or multiple file post through Postman using the 'multipart/form-data' header. However when I try to post a file through an xUnit test, I get the following error:
System.IO.InvalidDataException : Multipart body length limit 16384 exceeded.
I have searched high and low for a solution, tried different things, namely;
Replicating the request object to be as close as possible to Postmans request.
Playing around with the 'boundary' in the header.
Setting 'RequestFormLimits' on the function.
None of these have helped so far.
The details are the project are as follows:
Azure Function v3: targeting .netcoreapp3.1
Startup.cs
public class Startup : FunctionsStartup
{
public IConfiguration Configuration { get; private set; }
public override void Configure(IFunctionsHostBuilder builder)
{
var x = builder;
InitializeConfiguration(builder);
builder.Services.AddSingleton(Configuration.Get<UploadImagesAppSettings>());
builder.Services.AddLogging();
builder.Services.AddSingleton<IBlobService,BlobService>();
}
private void InitializeConfiguration(IFunctionsHostBuilder builder)
{
var executionContextOptions = builder
.Services
.BuildServiceProvider()
.GetService<IOptions<ExecutionContextOptions>>()
.Value;
Configuration = new ConfigurationBuilder()
.SetBasePath(executionContextOptions.AppDirectory)
.AddJsonFile("appsettings.json")
.AddJsonFile("appsettings.Development.json", optional: true)
.AddEnvironmentVariables()
.Build();
}
}
UploadImages.cs
public class UploadImages
{
private readonly IBlobService BlobService;
public UploadImages(IBlobService blobService)
{
BlobService = blobService;
}
[FunctionName("UploadImages")]
[RequestFormLimits(ValueLengthLimit = int.MaxValue,
MultipartBodyLengthLimit = 60000000, ValueCountLimit = 10)]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = "images")] HttpRequest req)
{
List<Uri> returnUris = new List<Uri>();
if (req.ContentLength == 0)
{
string badResponseMessage = $"Request has no content";
return new BadRequestObjectResult(badResponseMessage);
}
if (req.ContentType.Contains("multipart/form-data") && req.Form.Files.Count > 0)
{
foreach (var file in req.Form.Files)
{
if (!file.IsValidImage())
{
string badResponseMessage = $"{file.FileName} is not a valid/accepted Image file";
return new BadRequestObjectResult(badResponseMessage);
}
var uri = await BlobService.CreateBlobAsync(file);
if (uri == null)
{
return new ObjectResult($"Could not blob the file {file.FileName}.");
}
returnUris.Add(uri);
}
}
if (!returnUris.Any())
{
return new NoContentResult();
}
return new OkObjectResult(returnUris);
}
}
Exception Thrown:
The below exception is thrown at the second if statement above, when it tries to process req.Form.Files.Count > 0, i.e.
if (req.ContentType.Contains("multipart/form-data") && req.Form.Files.Count > 0) {}
Message:
System.IO.InvalidDataException : Multipart body length limit 16384 exceeded.
Stack Trace:
MultipartReaderStream.UpdatePosition(Int32 read)
MultipartReaderStream.ReadAsync(Byte[] buffer, Int32 offset, Int32 count, CancellationToken cancellationToken)
StreamHelperExtensions.DrainAsync(Stream stream, ArrayPool`1 bytePool, Nullable`1 limit, CancellationToken cancellationToken)
MultipartReader.ReadNextSectionAsync(CancellationToken cancellationToken)
FormFeature.InnerReadFormAsync(CancellationToken cancellationToken)
FormFeature.ReadForm()
DefaultHttpRequest.get_Form()
UploadImages.Run(HttpRequest req) line 42
UploadImagesTests.HttpTrigger_ShouldReturnListOfUploadedUris(String fileNames)
xUnit Test Project: targeting .netcoreapp3.1
Over to the xUnit Test project, basically I am trying to write an integration test. The project references the AF project and has the following classes:
TestHost.cs
public class TestHost
{
public TestHost()
{
var startup = new TestStartup();
var host = new HostBuilder()
.ConfigureWebJobs(startup.Configure)
.ConfigureServices(ReplaceTestOverrides)
.Build();
ServiceProvider = host.Services;
}
public IServiceProvider ServiceProvider { get; }
private void ReplaceTestOverrides(IServiceCollection services)
{
// services.Replace(new ServiceDescriptor(typeof(ServiceToReplace), testImplementation));
}
private class TestStartup : Startup
{
public override void Configure(IFunctionsHostBuilder builder)
{
SetExecutionContextOptions(builder);
base.Configure(builder);
}
private static void SetExecutionContextOptions(IFunctionsHostBuilder builder)
{
builder.Services.Configure<ExecutionContextOptions>(o => o.AppDirectory = Directory.GetCurrentDirectory());
}
}
}
TestCollection.cs
[CollectionDefinition(Name)]
public class TestCollection : ICollectionFixture<TestHost>
{
public const string Name = nameof(TestCollection);
}
HttpRequestFactory.cs: To create Http Post Request
public static class HttpRequestFactory
{
public static DefaultHttpRequest Create(string method, string contentType, Stream body)
{
var request = new DefaultHttpRequest(new DefaultHttpContext());
var contentTypeWithBoundary = new MediaTypeHeaderValue(contentType)
{
Boundary = $"----------------------------{DateTime.Now.Ticks.ToString("x")}"
};
var boundary = MultipartRequestHelper.GetBoundary(
contentTypeWithBoundary, (int)body.Length);
request.Method = method;
request.Headers.Add("Cache-Control", "no-cache");
request.Headers.Add("Content-Type", contentType);
request.ContentType = $"{contentType}; boundary={boundary}";
request.ContentLength = body.Length;
request.Body = body;
return request;
}
private static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit)
{
var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary);
if (string.IsNullOrWhiteSpace(boundary.Value))
{
throw new InvalidDataException("Missing content-type boundary.");
}
if (boundary.Length > lengthLimit)
{
throw new InvalidDataException(
$"Multipart boundary length limit {lengthLimit} exceeded.");
}
return boundary.Value;
}
}
The MultipartRequestHelper.cs class is available here
And Finally the Test class:
[Collection(TestCollection.Name)]
public class UploadImagesTests
{
readonly UploadImages UploadImagesFunction;
public UploadImagesTests(TestHost testHost)
{
UploadImagesFunction = new UploadImages(testHost.ServiceProvider.GetRequiredService<IBlobService>());
}
[Theory]
[InlineData("testfile2.jpg")]
public async void HttpTrigger_ShouldReturnListOfUploadedUris(string fileNames)
{
var formFile = GetFormFile(fileNames);
var fileStream = formFile.OpenReadStream();
var request = HttpRequestFactory.Create("POST", "multipart/form-data", fileStream);
var response = (OkObjectResult)await UploadImagesFunction.Run(request);
//fileStream.Close();
Assert.True(response.StatusCode == StatusCodes.Status200OK);
}
private static IFormFile GetFormFile(string fileName)
{
string fileExtension = fileName.Substring(fileName.IndexOf('.') + 1);
string fileNameandPath = GetFilePathWithName(fileName);
IFormFile formFile;
var stream = File.OpenRead(fileNameandPath);
switch (fileExtension)
{
case "jpg":
formFile = new FormFile(stream, 0, stream.Length,
fileName.Substring(0, fileName.IndexOf('.')),
fileName)
{
Headers = new HeaderDictionary(),
ContentType = "image/jpeg"
};
break;
case "png":
formFile = new FormFile(stream, 0, stream.Length,
fileName.Substring(0, fileName.IndexOf('.')),
fileName)
{
Headers = new HeaderDictionary(),
ContentType = "image/png"
};
break;
case "pdf":
formFile = new FormFile(stream, 0, stream.Length,
fileName.Substring(0, fileName.IndexOf('.')),
fileName)
{
Headers = new HeaderDictionary(),
ContentType = "application/pdf"
};
break;
default:
formFile = null;
break;
}
return formFile;
}
private static string GetFilePathWithName(string filename)
{
var outputFolder = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
return $"{outputFolder.Substring(0, outputFolder.IndexOf("bin"))}testfiles\\{filename}";
}
}
The test seems to be hitting the function and req.ContentLength does have a value. Considering this, could it have something to do with the way the File Streams are being managed? Perhaps not the right way?
Any inputs on this would be greatly appreciated.
Thanks
UPDATE 1
As per this post, I have also tried setting the ValueLengthLimit and MultipartBodyLengthLimit in the Startup of the Azure Function and/or the Test Project as opposed to attributes on the Azure Function. The exception then changed to:
"The inner stream position has changed unexpectedly"
Following this, I then set the fileStream position in the test project to SeekOrigin.Begin. I started getting the same error:
"Multipart body length limit 16384 exceeded."
It took me a 50km bike ride and a good nights sleep but I finally figured this one out :-).
The Azure function (AF) accepts an HttpRequest object as a parameter with the name of 'req' i.e.
public async Task Run(
[HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = "images")] HttpRequest req)
The hierarchy of the files object in the HttpRequest object (along with the parameter names) is as follows:
HttpRequest -> req
FormCollection -> Form
FormFileCollection -> Files
This is what the AF accepts and one would access the files collection by using req.Form.Files
In my test case, instead of posting a FormCollection object, I was trying to post a Stream of a file to the Azure Function.
var formFile = GetFormFile(fileNames);
var fileStream = formFile.OpenReadStream();
var request = HttpRequestFactory.Create("POST", "multipart/form-data", fileStream);
As a result of this, req.Form had a Stream value that it could not interpret and the req.Form.Files was raising an exception.
In order to rectify this, I had to do the following:
Revert all changes made as part of UPDATE 1. This means that I removed the 'RequestFormLimits' settings from the Startup file and left them as attributes on the functions Run method.
Instantiate a FormFileCollection object and add the IFormFile to it
Instantiate a FormCollection object using this FormFileCollection as a parameter.
Add the FormCollection to the request object.
To achieve the above, I had to make the following changes in code.
Change 'Create' method in the HttpRequestFactory
public static DefaultHttpRequest Create(string method, string contentType, FormCollection formCollection)
{
var request = new DefaultHttpRequest(new DefaultHttpContext());
var boundary = $"----------------------------{DateTime.Now.Ticks.ToString("x")}";
request.Method = method;
request.Headers.Add("Cache-Control", "no-cache");
request.Headers.Add("Content-Type", contentType);
request.ContentType = $"{contentType}; boundary={boundary}";
request.Form = formCollection;
return request;
}
Add a private static GetFormFiles() method
I wrote an additional GetFormFiles() method that calls the existing GetFormFile() method, instantiate a FormFileCollection object and add the IFormFile to it. This method in turn returns a FormFileCollection.
private static FormFileCollection GetFormFiles(string fileNames)
{
var formFileCollection = new FormFileCollection();
foreach (var file in fileNames.Split(','))
{
formFileCollection.Add(GetFormFile(file));
}
return formFileCollection;
}
Change the Testmethod
The test method calls the GetFormFiles() to get a FormFileCollection then
instantiates a FormCollection object using this FormFileCollection as a parameter and then passes the FormCollection object as a parameter to the HttpRequest object instead of passing a Stream.
[Theory]
[InlineData("testfile2.jpg")]
public async void HttpTrigger_ShouldReturnListOfUploadedUris(string fileNames)
{
var formFiles = GetFormFiles(fileNames);
var formCollection = new FormCollection(null, formFiles);
var request = HttpRequestFactory.Create("POST", "multipart/form-data", formCollection);
var response = (OkObjectResult) await UploadImagesFunction.Run(request);
Assert.True(response.StatusCode == StatusCodes.Status200OK);
}
So in the end the issue was not really with the 'RequestFormLimits' but rather with the type of data I was submitting in the POST message.
I hope this answer provides a different perspective to someone that comes across the same error message.
Cheers.

How to send signal r messages to specific user logged in using azure functions?

Hi I am working in azure functions and azure signal r. I have front end application with azure ad authentication. I want to send signal r notification to specific user through azure function. Below is my react code.
constructor(props: IMapUpload) {
super(props);
this.fileUploaderRef = React.createRef<FileUploader>();
this.hubConnection = new signalR.HubConnectionBuilder().withUrl("http://localhost:7071/api")
.configureLogging(signalR.LogLevel.Information)
.build();// https://wadevdvlgenseawe02-webapi.azurewebsites.net/MapUpload
this.hubConnection.start().catch((err: string) => console.log(err));
}
componentDidMount() {
this.hubConnection.on("newMessage", (message: string) => {
console.log(message);
//Pass the Map File Url to Site Details
this.props.onMapFileUpload(message);
this.handleProgress(message);
});
const sarId= this.props.sarId;
this.props.sar?.getMapFileDetails(sarId, null, null);
}
Below is my azure functions
[FunctionName("Negotiate")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "negotiate")] HttpRequest req, IBinder binder)
{
if (req.Headers.ContainsKey("Authorization"))
{
var principal = TryGetPrincipal(req.Headers["Authorization"].ToString());
if (principal != null)
{
var connectionInfo = await binder.BindAsync<SignalRConnectionInfo>(new SignalRConnectionInfoAttribute
{
HubName = "MapUpload",
UserId = principal.FindFirst(ClaimTypes.NameIdentifier).Value
});
return new OkObjectResult(connectionInfo);
}
}
return new UnauthorizedResult();
}
public static ClaimsPrincipal TryGetPrincipal(string jwtToken)
{
IdentityModelEventSource.ShowPII = true;
SecurityToken validatedToken;
TokenValidationParameters validationParameters = new TokenValidationParameters();
validationParameters.ValidateLifetime = true;
validationParameters.ValidAudience = "e51c317b-87e7-4cb3-95f0-37cb52b6f873";
// validationParameters.ValidIssuer = _issuer.ToLower();
validationParameters.IssuerSigningKey = new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(Encoding.UTF8.GetBytes(".Kmt.LP_f2D3.E8MY.TXyve.-sgr6770j_"));
ClaimsPrincipal principal = new JwtSecurityTokenHandler().ValidateToken(jwtToken, validationParameters, out validatedToken);
return principal;
}
[FunctionName("Negotiate")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "negotiate")] HttpRequest req, IBinder binder)
{
if (req.Headers.ContainsKey("Authorization"))
{
var principal = TryGetPrincipal(req.Headers["Authorization"].ToString());
if (principal != null)
{
var connectionInfo = await binder.BindAsync<SignalRConnectionInfo>(new SignalRConnectionInfoAttribute
{
HubName = "MapUpload",
UserId = principal.FindFirst(ClaimTypes.NameIdentifier).Value
});
return new OkObjectResult(connectionInfo);
}
}
return new UnauthorizedResult();
}
public static ClaimsPrincipal TryGetPrincipal(string jwtToken)
{
IdentityModelEventSource.ShowPII = true;
SecurityToken validatedToken;
TokenValidationParameters validationParameters = new TokenValidationParameters();
validationParameters.ValidateLifetime = true;
validationParameters.ValidAudience = "myclientid";
// validationParameters.ValidIssuer = _issuer.ToLower();
validationParameters.IssuerSigningKey = new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(Encoding.UTF8.GetBytes("mysecrete"));
ClaimsPrincipal principal = new JwtSecurityTokenHandler().ValidateToken(jwtToken, validationParameters, out validatedToken);
return principal;
}
[FunctionName("Function1")]
public static Task Run([ServiceBusTrigger("myqueue", Connection = "myconn")]string myQueueItem, ILogger log,
[SignalR(HubName = "MapUpload")] IAsyncCollector<SignalRMessage> signalRMessages)
{
log.LogInformation($"C# ServiceBus queue trigger function processed message: {myQueueItem}");
return signalRMessages.AddAsync(
new SignalRMessage
{
//UserId = "test#mydomain.com",
Target = "newMessage",
Arguments = new[] { myQueueItem }
});
}
If I havelogged in with test#mydomain.com in my front end application then only for test#mydomain.com I want to send message. If I add userid then messages are not getting delivered and If I remove message is getting delivered to everyone. Then I did some research and found I need to add UserId = "{headers.x-ms-client-principal-id}" but after adding this I started getting below error
http://localhost:7071/api/negotiate?negotiateVersion=1 500 (Internal Server Error)
Error: Failed to complete negotiation with the server: Error: Internal Server Error
I am struggling to solve this. Can someone help me what I am doing wrong here? Any help would be greatly appreciated. Thank you
Since you are not using the built-in authentication/authorization feature, the header that you are using is not applicable. Instead, you could extract the upn/email from the JWT token passed and leverage runtime binding to pass this extracted value as the UserId.
With this, you should later be able to send notifications to specific users using the passed UserId.

Azure Function Cosmos DB Output Binding - Custom JsonSerializerSettings

I have an Azure Function with a CosmosDB output binding, like this:
public static class ComponentDesignHttpTrigger
{
[FunctionName("ComponentDesignInserter-Http-From-ComponentDesign")]
public static IActionResult Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "fromComponentDesign")] HttpRequest request,
[CosmosDB(
databaseName: StorageFramework.CosmosDb.DatabaseId,
collectionName: Storage.ComponentDesignCollectionId,
ConnectionStringSetting = "CosmosDBConnection")] out ComponentDesign componentDesignToInsert,
ILogger log)
{
var requestBody = new StreamReader(request.Body).ReadToEnd();
componentDesignToInsert = JsonConvert.DeserializeObject<ComponentDesign>(requestBody);
return new OkObjectResult(componentDesignToInsert);
}
}
In this function componentDesignToInsert is automatically serialized and put into CosmosDB after the function finishes executing. But the default serialization does not put things in camelCase. For this Json.NET lets you provide custom serializer settings, like this:
var settings = new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
};
var json = JsonConvert.SerializeObject(yourObject, settings);
but I'm unsure how I can integrate this with my output binding. How can I accomplish this?
Output binding does not expose the serializer settings at this moment.
One thing you can do though, is leverage your own custom DocumentClient for the operation.
One important thing though is that the DocumentClient instance needs to be static (more details on https://github.com/Azure/azure-functions-host/wiki/Managing-Connections).
private static Lazy<DocumentClient> lazyClient = new Lazy<DocumentClient>(InitializeDocumentClient);
private static DocumentClient documentClient => lazyClient.Value;
private static DocumentClient InitializeDocumentClient()
{
// Perform any initialization here
var uri = new Uri("example");
var authKey = "authKey";
var settings = new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
};
return new DocumentClient(uri, authKey, settings);
}
[FunctionName("ComponentDesignInserter-Http-From-ComponentDesign")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "fromComponentDesign")] HttpRequest request,
ILogger log)
{
var requestBody = new StreamReader(request.Body).ReadToEnd();
var componentDesignToInsert = JsonConvert.DeserializeObject<ComponentDesign>(requestBody);
var collectionUri = UriFactory.GetDocumentCollectionUri(StorageFramework.CosmosDb.DatabaseId, Storage.ComponentDesignCollectionId);
await documentClient.UpsertDocumentAsync(collectionUri, componentDesignToInsert);
return new OkObjectResult(componentDesignToInsert);
}
Another option is to decorate the class with JsonProperty if that suits your scenario.

Getting next message in an Azure Service Bus Subscription with an Azure Function, with an HTTP Trigger

I want to create an Azure Function that will fulfill the following requirements:
Trigger upon an HTTP request
Looks at an Azure Service Bus Subscription and gets the next message based on a set of filters specified in the HTTP Request.
If you are using C# you can do something like this:
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Microsoft.Azure.ServiceBus;
using Microsoft.Azure.ServiceBus.Core;
namespace HttpTriggerSBRead
{
public static class ReadSBOnHttpTrigger
{
const string ServiceBusConnectionString = "{service bus connection string}";
const string TopicName = "{name of your topic}";
const string SubscriptionName = "{name of your subscription}";
[FunctionName("ReadSBOnHttpTrigger")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
ILogger log)
{
string filter = req.Query["filter"];
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
dynamic data = JsonConvert.DeserializeObject(requestBody);
filter = filter ?? data?.filter;
SubscriptionClient sqlFilterOnlySubscriptionClient = new SubscriptionClient(ServiceBusConnectionString,
TopicName, SubscriptionName);
await sqlFilterOnlySubscriptionClient.AddRuleAsync(new RuleDescription
{
Filter = new SqlFilter(filter),
Name = filter
});
await ReceiveMessagesAsync(SubscriptionName, log);
await sqlFilterOnlySubscriptionClient.RemoveRuleAsync(filter);
return filter != null
? (ActionResult)new OkObjectResult($"{filter}")
: new BadRequestObjectResult("Please pass a filter on the query string or in the request body");
}
static async Task ReceiveMessagesAsync(string subscriptionName, ILogger log)
{
string subscriptionPath = EntityNameHelper.FormatSubscriptionPath(TopicName, subscriptionName);
IMessageReceiver subscriptionReceiver = new MessageReceiver(ServiceBusConnectionString, subscriptionPath, ReceiveMode.ReceiveAndDelete);
log.LogInformation($"{DateTime.Now} :: Receiving Messages From Subscription: {subscriptionName}");
var receivedMessage = await subscriptionReceiver.ReceiveAsync(TimeSpan.FromSeconds(30));
if (receivedMessage != null)
{
log.LogInformation($"Lable Property = {receivedMessage.Label}");
}
log.LogInformation($"{DateTime.Now} :: Messages From Subscription: {subscriptionName}");
}
}
}
To use it you need to pass in the filter parameter to your function. For example something like this:
http://localhost:7071/api/ReadSBOnHttpTrigger?filter=sys.Label=%27test%27 or http://localhost:7071/api/ReadSBOnHttpTrigger?filter=sys.To=%27test%27
Just for the reference, I used this code as source with some small modifications:
https://github.com/Azure/azure-service-bus/tree/master/samples/DotNet/GettingStarted/Microsoft.Azure.ServiceBus/TopicSubscriptionWithRuleOperationsSample

Resources