diff --git a/README.md b/README.md index 4b6cb7c..ac21c01 100644 --- a/README.md +++ b/README.md @@ -6,3 +6,4 @@ Bindings: - [Query Parameter](./docs/query-parameter.md) - [Multipart Request](./docs/multipart-request.md) +- [Request Value Lookup](./docs/request-value-lookup.md) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 7b8accf..858638b 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -69,6 +69,25 @@ stages: command: "build" arguments: "--no-restore --configuration $(BUILD_CONFIGURATION)" projects: "**/*.csproj" + - task: DotNetCoreCLI@2 + displayName: "Run Unit Tests" + inputs: + command: "test" + projects: "**/*Tests/*UnitTests.csproj" + arguments: '--configuration $(BUILD_CONFIGURATION) --collect "Code coverage"' + testRunTitle: "Unit Tests" + - task: DotNetCoreCLI@2 + displayName: "Run Integration Tests" + env: + AFTU_RUN_AZURITE: true + AFTU_FUNC_APP_PATH: '../../../../Integration.FunctionApp/bin/Release/netcoreapp3.1' + AFTU_WRITE_LOG: ${{ parameters.WRITE_LOG }} + AFTU_AZURITE_SILENT: ${{ parameters.AZURITE_SILENT }} + inputs: + command: "test" + projects: "**/*Tests/*IntegrationTests.csproj" + arguments: '--configuration $(BUILD_CONFIGURATION) --collect "Code coverage"' + testRunTitle: "Integration Tests" - task: PowerShell@2 displayName: 'Set package version' name: SetVersion @@ -88,18 +107,6 @@ stages: nobuild: true versioningScheme: 'byEnvVar' versionEnvVar: 'SETVERSION_UPDATED_PACKAGE_VERSION' - - task: DotNetCoreCLI@2 - displayName: "Run Tests" - env: - AFTU_RUN_AZURITE: true - AFTU_FUNC_APP_PATH: '../../../../Integration.FunctionApp/bin/Release/netcoreapp3.1' - AFTU_WRITE_LOG: ${{ parameters.WRITE_LOG }} - AFTU_AZURITE_SILENT: ${{ parameters.AZURITE_SILENT }} - inputs: - command: "test" - projects: "**/*Tests/*IntegrationTests.csproj" - arguments: '--configuration $(BUILD_CONFIGURATION) --collect "Code coverage"' - testRunTitle: "Integration Tests" - task: Bash@3 condition: and(always(), eq(${{ parameters.WRITE_LOG }}, true)) inputs: diff --git a/docs/request-value-lookup.md b/docs/request-value-lookup.md new file mode 100644 index 0000000..dc5e2e5 --- /dev/null +++ b/docs/request-value-lookup.md @@ -0,0 +1,42 @@ +# Request Value Lookup + +Get a value from one of multiple places in a request. Currently supports: + +- Query +- Header + +## Attribute + +| Name | Type | Description | +| -------- | ---------------------- | ----------------------------- | +| Location | `RequestValueLocation` | Location to check for value | +| Name | `string` | Priamary key name to look for | +| Aliases | `string[]` | Alternate key names | + +## Example + +```csharp +public static class VersionedHttpTrigger +{ + [FunctionName(nameof(VersionedHttpTrigger))] + public static async Task RunAsync( + [HttpTrigger(AuthorizationLevel.Function, "get", Route = "version")] + HttpRequest req, ILogger log, [ + RequestValue( + Location = RequestValueLocation.Header | RequestValueLocation.Query, + Name = "apiVersion", + Aliases = new[] { "x-api-version" } + )] + string version + ) + { + if (string.IsNullOrEmpty(version)) + { + return new BadRequestResult(); + } + + log.LogInformation("Triggered for version {Version}", version); + return new OkObjectResult(version); + } +} +``` diff --git a/src/Integration.FunctionApp.IntegrationTests/Functions/MultipartHttpTriggerTests.cs b/src/Integration.FunctionApp.IntegrationTests/Functions/MultipartHttpTriggerTests.cs index 4394a36..2ac40fc 100644 --- a/src/Integration.FunctionApp.IntegrationTests/Functions/MultipartHttpTriggerTests.cs +++ b/src/Integration.FunctionApp.IntegrationTests/Functions/MultipartHttpTriggerTests.cs @@ -1,3 +1,4 @@ +using System.Net; using System.Net.Http; using System.Net.Mime; using System.Text; @@ -48,5 +49,26 @@ public async Task MultipartHttpTrigger_ContentWithFiles_ReturnsValue() Assert.AreEqual(jsonData.Username, model.Username); Assert.That.BlobExists("multipart-files", "greeting.txt"); } + + [TestMethod] + [StartFunctions(nameof(MultipartHttpTrigger))] + public async Task MultipartHttpTrigger_ContentWithoutFilesAndInvalidBody_ReturnsBadRequest() + { + var multipart = new MultipartFormDataContent(); + var jsonData = new MultipartRequestBodyData + { + Email = "john@doe.local" + }; + multipart.Add(new StringContent(JsonConvert.SerializeObject(jsonData), Encoding.UTF8, + MediaTypeNames.Application.Json)); + + var request = new HttpRequestMessage(HttpMethod.Post, "api/test/multipart") + { + Content = multipart + }; + + var response = await Fixture.Client.SendAsync(request); + Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode); + } } } \ No newline at end of file diff --git a/src/Integration.FunctionApp.IntegrationTests/Functions/VersionedHttpTriggerTests.cs b/src/Integration.FunctionApp.IntegrationTests/Functions/VersionedHttpTriggerTests.cs new file mode 100644 index 0000000..1ee23e9 --- /dev/null +++ b/src/Integration.FunctionApp.IntegrationTests/Functions/VersionedHttpTriggerTests.cs @@ -0,0 +1,46 @@ +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Integration.FunctionApp.Functions; +using JoachimDalen.AzureFunctions.TestUtils.Attributes; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Integration.FunctionApp.IntegrationTests.Functions +{ + [TestClass] + public class VersionedHttpTriggerTests : BaseFunctionTest + { + [TestMethod] + [StartFunctions(nameof(VersionedHttpTrigger))] + public async Task VersionedHttpTrigger_ValueInQuery_ReturnsValue() + { + var response = await Fixture.Client.GetAsync("/api/version?apiVersion=v1"); + var responseBody = await response.Content.ReadAsStringAsync(); + + Assert.IsTrue(response.IsSuccessStatusCode); + Assert.AreEqual("v1", responseBody); + } + + [TestMethod] + [StartFunctions(nameof(VersionedHttpTrigger))] + public async Task VersionedHttpTrigger_ValueInHeader_ReturnsValue() + { + var request = new HttpRequestMessage(HttpMethod.Get, "/api/version"); + request.Headers.Add("x-api-version", "v2"); + var response = await Fixture.Client.SendAsync(request); + var responseBody = await response.Content.ReadAsStringAsync(); + + Assert.IsTrue(response.IsSuccessStatusCode); + Assert.AreEqual("v2", responseBody); + } + + [TestMethod] + [StartFunctions(nameof(VersionedHttpTrigger))] + public async Task VersionedHttpTrigger_ValueInQuery_ReturnsError() + { + var response = await Fixture.Client.GetAsync("/api/version"); + + Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode); + } + } +} \ No newline at end of file diff --git a/src/Integration.FunctionApp/Functions/MultipartHttpTrigger.cs b/src/Integration.FunctionApp/Functions/MultipartHttpTrigger.cs index b63cda1..fa6e76a 100644 --- a/src/Integration.FunctionApp/Functions/MultipartHttpTrigger.cs +++ b/src/Integration.FunctionApp/Functions/MultipartHttpTrigger.cs @@ -24,6 +24,12 @@ public static async Task RunAsync( [Blob("multipart-files", FileAccess.Write)] CloudBlobContainer blobContainer) { + if (!multipartRequest.IsValid) + { + return new BadRequestObjectResult(multipartRequest.ValidationResults); + } + + foreach (var requestFile in multipartRequest.Files) { var blob = blobContainer.GetBlockBlobReference(requestFile.FileName); diff --git a/src/Integration.FunctionApp/Functions/VersionedHttpTrigger.cs b/src/Integration.FunctionApp/Functions/VersionedHttpTrigger.cs new file mode 100644 index 0000000..dd2e866 --- /dev/null +++ b/src/Integration.FunctionApp/Functions/VersionedHttpTrigger.cs @@ -0,0 +1,37 @@ +using System.Threading.Tasks; +using System.Web.Http; +using JoachimDalen.AzureFunctions.Extensions.Attributes; +using JoachimDalen.AzureFunctions.Extensions.Models; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Extensions.Http; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +namespace Integration.FunctionApp.Functions +{ + public static class VersionedHttpTrigger + { + [FunctionName("VersionedHttpTrigger")] + public static async Task RunAsync( + [HttpTrigger(AuthorizationLevel.Function, "get", Route = "version")] + HttpRequest req, ILogger log, [ + RequestValue(Location = RequestValueLocation.Header | RequestValueLocation.Query, Name = "apiVersion", + Aliases = new[] + { + "x-api-version" + }) + ] + string version + ) + { + if (string.IsNullOrEmpty(version)) + { + return new BadRequestResult(); + } + + log.LogInformation("Triggered for version {Version}", version); + return new OkObjectResult(version); + } + } +} \ No newline at end of file diff --git a/src/Integration.FunctionApp/Models/MultipartRequestBodyData.cs b/src/Integration.FunctionApp/Models/MultipartRequestBodyData.cs index 30aa7d5..1de2f92 100644 --- a/src/Integration.FunctionApp/Models/MultipartRequestBodyData.cs +++ b/src/Integration.FunctionApp/Models/MultipartRequestBodyData.cs @@ -1,8 +1,12 @@ -namespace Integration.FunctionApp.Models +using System.ComponentModel.DataAnnotations; + +namespace Integration.FunctionApp.Models { public class MultipartRequestBodyData { + [Required] public string Username { get; set; } + public string Email { get; set; } } } \ No newline at end of file diff --git a/src/JoachimDalen.AzureFunctions.Extensions.UnitTests/HttpRequestExtensionsTests.cs b/src/JoachimDalen.AzureFunctions.Extensions.UnitTests/HttpRequestExtensionsTests.cs new file mode 100644 index 0000000..780eedd --- /dev/null +++ b/src/JoachimDalen.AzureFunctions.Extensions.UnitTests/HttpRequestExtensionsTests.cs @@ -0,0 +1,60 @@ +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Mime; +using System.Text; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JoachimDalen.AzureFunctions.Extensions.UnitTests +{ + [TestClass] + public class HttpRequestExtensionsTests + { + [DataTestMethod] + [DataRow("form-data;name=\"file\"; filename=\"fileOne.png\"")] + [DataRow("form-data;name=\"file\"; filename=fileOne.png")] + public void GetEscapedContentDispositionFileName_Data_ReturnsCorrect(string contentDisposition) + { + var contentDispositionHeaderValue = ContentDispositionHeaderValue.Parse(contentDisposition); + Assert.AreEqual("fileOne.png", contentDispositionHeaderValue.GetEscapedContentDispositionFileName()); + } + + [TestMethod] + public async Task HasFiles_Files_ReturnsTrue() + { + var bytes = Encoding.UTF8.GetBytes("Hello"); + var multipart = new MultipartFormDataContent + { + {new ByteArrayContent(bytes, 0, bytes.Length), "file", "greeting.txt"} + }; + + var mp = (await multipart.ReadAsMultipartAsync()).Contents.First(); + Assert.IsTrue(mp.HasFiles("file")); + } + + [TestMethod] + public async Task HasFiles_StringContent_ReturnsFalse() + { + var multipart = new MultipartFormDataContent + { + new StringContent("Hello", Encoding.UTF8, MediaTypeNames.Text.Plain) + }; + + var mp = (await multipart.ReadAsMultipartAsync()).Contents.First(); + Assert.IsFalse(mp.HasFiles("file")); + } + + [TestMethod] + public async Task HasData_JsonContent_ReturnsTrue() + { + var multipart = new MultipartFormDataContent + { + new StringContent("Hello", Encoding.UTF8, MediaTypeNames.Application.Json) + }; + + var mp = (await multipart.ReadAsMultipartAsync()).Contents.First(); + Assert.IsTrue(mp.HasData()); + } + } +} \ No newline at end of file diff --git a/src/JoachimDalen.AzureFunctions.Extensions.UnitTests/JoachimDalen.AzureFunctions.Extensions.UnitTests.csproj b/src/JoachimDalen.AzureFunctions.Extensions.UnitTests/JoachimDalen.AzureFunctions.Extensions.UnitTests.csproj new file mode 100644 index 0000000..12de9e2 --- /dev/null +++ b/src/JoachimDalen.AzureFunctions.Extensions.UnitTests/JoachimDalen.AzureFunctions.Extensions.UnitTests.csproj @@ -0,0 +1,20 @@ + + + + netcoreapp3.1 + + false + + + + + + + + + + + + + + diff --git a/src/JoachimDalen.AzureFunctions.Extensions.UnitTests/TestUtils.cs b/src/JoachimDalen.AzureFunctions.Extensions.UnitTests/TestUtils.cs new file mode 100644 index 0000000..ad1a7e7 --- /dev/null +++ b/src/JoachimDalen.AzureFunctions.Extensions.UnitTests/TestUtils.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Net.Mime; +using System.Text; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Internal; +using Microsoft.Extensions.Primitives; +using Newtonsoft.Json; + +namespace JoachimDalen.AzureFunctions.Extensions.UnitTests +{ + internal static class TestUtils + { + internal static HttpRequest GetRequest(HttpMethod method, string url, object payload, + Dictionary headers = null, Dictionary query = null) + { + var context = new DefaultHttpContext + { + Request = + { + Path = url, + + Body = new MemoryStream(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(payload))), + Method = method.ToString() + }, + }; + + if (headers != null) + { + foreach (var (key, value) in headers) + { + context.Request.Headers.Add(key, value); + } + } + + if (query != null) + { + context.Request.Query = new QueryCollection(query); + } + + return context.Request; + } + + internal static HttpRequestMessage GetRequestMessage(HttpMethod method, string url, object payload, + Dictionary headers) + { + var request = new HttpRequestMessage(method, url) + { + Content = new StringContent(JsonConvert.SerializeObject(payload), Encoding.UTF8, + MediaTypeNames.Application.Json) + }; + + if (headers != null) + { + foreach (var (key, value) in headers) + { + request.Headers.Add(key, value); + } + } + + return request; + } + + internal static HttpRequestMessage GetMultipartRequest(object payload, + Tuple[] files = null) + { + var multipart = new MultipartFormDataContent + { + new StringContent(JsonConvert.SerializeObject(payload), Encoding.UTF8, + MediaTypeNames.Application.Json) + }; + + if (files != null) + { + foreach (var (byteArrayContent, name, filename) in files) + { + multipart.Add(byteArrayContent, name, filename); + } + } + + var request = new HttpRequestMessage(HttpMethod.Post, "api/test/multipart") + { + Content = multipart + }; + + return request; + } + } +} \ No newline at end of file diff --git a/src/JoachimDalen.AzureFunctions.Extensions.UnitTests/ValueProviders/MultipartRequestValueProviderTests.cs b/src/JoachimDalen.AzureFunctions.Extensions.UnitTests/ValueProviders/MultipartRequestValueProviderTests.cs new file mode 100644 index 0000000..4feacbc --- /dev/null +++ b/src/JoachimDalen.AzureFunctions.Extensions.UnitTests/ValueProviders/MultipartRequestValueProviderTests.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Net.Http; +using System.Net.Mime; +using System.Text; +using System.Threading.Tasks; +using JoachimDalen.AzureFunctions.Extensions.Attributes; +using JoachimDalen.AzureFunctions.Extensions.Models; +using JoachimDalen.AzureFunctions.Extensions.ValueProviders; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json; + +namespace JoachimDalen.AzureFunctions.Extensions.UnitTests.ValueProviders +{ + [TestClass] + public class MultipartRequestValueProviderTests + { + [TestMethod] + public async Task GetValueAsync_NoFiles_ReturnsData() + { + var request = TestUtils.GetMultipartRequest(new MultipartRequestPayload + { + Email = "john@doe.local" + }); + + var attribute = new MultipartRequestAttribute(); + var valueProvider = new MultipartRequestValueProvider(attribute, request, null); + + var val = await valueProvider.GetValueAsync(); + + if (val is MultipartRequestData multipartRequestData) + { + Assert.AreEqual(0, multipartRequestData.Files.Length); + Assert.AreEqual("john@doe.local", multipartRequestData.Data.Email); + } + else + { + Assert.Fail("value is not correct type"); + } + } + + [TestMethod] + public async Task GetValueAsync_RequiredDataMissing_Validates() + { + var request = TestUtils.GetMultipartRequest(new ValidatedMultipartRequestPayload + { + Email = "john@doe.local" + }); + + var attribute = new MultipartRequestAttribute { ValidateData = true }; + var valueProvider = + new MultipartRequestValueProvider(attribute, request, null); + + var val = await valueProvider.GetValueAsync(); + + if (val is MultipartRequestData multipartRequestData) + { + Assert.IsFalse(multipartRequestData.IsValid); + Assert.AreEqual(1, multipartRequestData.ValidationResults.Count()); + } + else + { + Assert.Fail("value is not correct type"); + } + } + + + [TestMethod] + public async Task GetValueAsync_WithFiles_FindsAllFiles() + { + var files = new List>(); + var bytes = Encoding.UTF8.GetBytes("Hello"); + var bytes2 = Encoding.UTF8.GetBytes("Goodbye"); + files.Add(new Tuple(new ByteArrayContent(bytes, 0, bytes.Length), "file", + "greeting.txt")); + files.Add(new Tuple(new ByteArrayContent(bytes2, 0, bytes2.Length), "file", + "goodbye.txt")); + + var request = TestUtils.GetMultipartRequest(new ValidatedMultipartRequestPayload + { + Email = "john@doe.local" + }, files.ToArray()); + + var attribute = new MultipartRequestAttribute(); + var valueProvider = + new MultipartRequestValueProvider(attribute, request, null); + + var val = await valueProvider.GetValueAsync(); + + if (val is MultipartRequestData multipartRequestData) + { + Assert.AreEqual(2, multipartRequestData.Files.Length); + var fileContent = + Encoding.UTF8.GetString(multipartRequestData.Files.First(x => x.FileName == "greeting.txt") + .Content); + Assert.AreEqual("Hello", fileContent); + } + else + { + Assert.Fail("value is not correct type"); + } + } + + class MultipartRequestPayload + { + public string Email { get; set; } + } + + class ValidatedMultipartRequestPayload + { + [Required] + public string Username { get; set; } + + public string Email { get; set; } + } + } +} \ No newline at end of file diff --git a/src/JoachimDalen.AzureFunctions.Extensions.UnitTests/ValueProviders/RequestValueProviderTests.cs b/src/JoachimDalen.AzureFunctions.Extensions.UnitTests/ValueProviders/RequestValueProviderTests.cs new file mode 100644 index 0000000..0a6c528 --- /dev/null +++ b/src/JoachimDalen.AzureFunctions.Extensions.UnitTests/ValueProviders/RequestValueProviderTests.cs @@ -0,0 +1,152 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Threading.Tasks; +using JoachimDalen.AzureFunctions.Extensions.Attributes; +using JoachimDalen.AzureFunctions.Extensions.Models; +using JoachimDalen.AzureFunctions.Extensions.ValueProviders; +using Microsoft.Extensions.Primitives; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JoachimDalen.AzureFunctions.Extensions.UnitTests.ValueProviders +{ + [TestClass] + public class RequestValueProviderTests + { + [TestMethod] + public async Task GetValueAsync_Header_ReturnsValue() + { + var request = TestUtils.GetRequest(HttpMethod.Get, "/test", new + { + Email = "john@doe.local" + }, new Dictionary + { + { "x-api-key", "myApiKey" } + }); + + var attribute = new RequestValueAttribute + { + Name = "x-api-key", Location = RequestValueLocation.Header + }; + + var valueProvider = new RequestValueProvider(request, attribute, GetInfo("myStringParam"), true, null); + var val = await valueProvider.GetValueAsync(); + Assert.AreEqual("myApiKey", val); + } + + [TestMethod] + public async Task GetValueAsync_Query_ReturnsValue() + { + var request = TestUtils.GetRequest(HttpMethod.Get, "/test", new + { + Email = "john@doe.local" + }, null, new Dictionary + { + { "apiKey", "myApiKey" } + }); + + var attribute = new RequestValueAttribute + { + Name = "apiKey", Location = RequestValueLocation.Query + }; + + var valueProvider = new RequestValueProvider(request, attribute, GetInfo("myStringParam"), true, null); + var val = await valueProvider.GetValueAsync(); + Assert.AreEqual("myApiKey", val); + } + + [TestMethod] + public async Task GetValueAsync_None_ReturnsNull() + { + var request = TestUtils.GetRequest(HttpMethod.Get, "/test", new + { + Email = "john@doe.local" + }); + + var attribute = new RequestValueAttribute + { + Name = "apiKey", Location = RequestValueLocation.Query + }; + + var valueProvider = new RequestValueProvider(request, attribute, GetInfo("myStringParam"), true, null); + var val = await valueProvider.GetValueAsync(); + Assert.AreEqual(null, val); + } + + [TestMethod] + public async Task GetValueAsync_AliasValueInQuery_ReturnsValue() + { + var request = TestUtils.GetRequest(HttpMethod.Get, "/test", + new + { + Email = "john@doe.local" + }, + new Dictionary + { + { "x-api-keys", "headerApiKey" } + }, + new Dictionary + { + { "apiKey", "queryApiKey" } + } + ); + + var attribute = new RequestValueAttribute + { + Name = "x-api-key", + Aliases = new[] { "apiKey" }, + Location = RequestValueLocation.Query | RequestValueLocation.Header + }; + + var valueProvider = new RequestValueProvider(request, attribute, GetInfo("myStringParam"), true, null); + var val = await valueProvider.GetValueAsync(); + Assert.AreEqual("queryApiKey", val); + } + + [TestMethod] + public async Task GetValueAsync_AliasValueInHeader_ReturnsValue() + { + var request = TestUtils.GetRequest(HttpMethod.Get, "/test", + new + { + Email = "john@doe.local" + }, + new Dictionary + { + { "x-api-key", "headerApiKey" } + }, + new Dictionary + { + { "apisKey", "queryApiKey" } + } + ); + + var attribute = new RequestValueAttribute + { + Name = "apiKey", + Aliases = new[] { "x-api-key" }, + Location = RequestValueLocation.Query | RequestValueLocation.Header + }; + + var valueProvider = new RequestValueProvider(request, attribute, GetInfo("myStringParam"), true, null); + var val = await valueProvider.GetValueAsync(); + Assert.AreEqual("headerApiKey", val); + } + + private ParameterInfo GetInfo(string paramName) + { + var classType = typeof(TestDemoClass); + var method = classType.GetMethod(nameof(TestDemoClass.MyTaskMethod)); + return method.GetParameters().FirstOrDefault(x => x.Name == paramName); + } + + private class TestDemoClass + { + public Task MyTaskMethod(string myStringParam) + { + return Task.CompletedTask; + } + } + } +} \ No newline at end of file diff --git a/src/JoachimDalen.AzureFunctions.Extensions.sln b/src/JoachimDalen.AzureFunctions.Extensions.sln index 7c7f46d..eb5f69b 100644 --- a/src/JoachimDalen.AzureFunctions.Extensions.sln +++ b/src/JoachimDalen.AzureFunctions.Extensions.sln @@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Integration.FunctionApp", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Integration.FunctionApp.IntegrationTests", "Integration.FunctionApp.IntegrationTests\Integration.FunctionApp.IntegrationTests.csproj", "{6837DAE9-0AE4-44CE-8B5D-0CEE6EECD5F0}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JoachimDalen.AzureFunctions.Extensions.UnitTests", "JoachimDalen.AzureFunctions.Extensions.UnitTests\JoachimDalen.AzureFunctions.Extensions.UnitTests.csproj", "{A21ABC14-2EAA-4761-9F7D-25F739D4A912}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,9 +29,14 @@ Global {6837DAE9-0AE4-44CE-8B5D-0CEE6EECD5F0}.Debug|Any CPU.Build.0 = Debug|Any CPU {6837DAE9-0AE4-44CE-8B5D-0CEE6EECD5F0}.Release|Any CPU.ActiveCfg = Release|Any CPU {6837DAE9-0AE4-44CE-8B5D-0CEE6EECD5F0}.Release|Any CPU.Build.0 = Release|Any CPU + {A21ABC14-2EAA-4761-9F7D-25F739D4A912}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A21ABC14-2EAA-4761-9F7D-25F739D4A912}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A21ABC14-2EAA-4761-9F7D-25F739D4A912}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A21ABC14-2EAA-4761-9F7D-25F739D4A912}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {3E221860-C5B2-4CD1-B303-F92FAB2FF730} = {C0447612-812F-4D92-8FAA-14F8EC7132F0} {6837DAE9-0AE4-44CE-8B5D-0CEE6EECD5F0} = {C0447612-812F-4D92-8FAA-14F8EC7132F0} + {A21ABC14-2EAA-4761-9F7D-25F739D4A912} = {C0447612-812F-4D92-8FAA-14F8EC7132F0} EndGlobalSection EndGlobal diff --git a/src/JoachimDalen.AzureFunctions.Extensions/Attributes/RequestValueAttribute.cs b/src/JoachimDalen.AzureFunctions.Extensions/Attributes/RequestValueAttribute.cs new file mode 100644 index 0000000..bd01098 --- /dev/null +++ b/src/JoachimDalen.AzureFunctions.Extensions/Attributes/RequestValueAttribute.cs @@ -0,0 +1,26 @@ +using System; +using JoachimDalen.AzureFunctions.Extensions.Models; +using Microsoft.Azure.WebJobs.Description; + +namespace JoachimDalen.AzureFunctions.Extensions.Attributes +{ + [Binding] + [AttributeUsage(AttributeTargets.Parameter)] + public class RequestValueAttribute : Attribute + { + /// + /// Location to look for value + /// + public RequestValueLocation Location { get; set; } + + /// + /// Primary name of value + /// + public string Name { get; set; } + + /// + /// Secondary names to look for when first does not match. + /// + public string[] Aliases { get; set; } + } +} \ No newline at end of file diff --git a/src/JoachimDalen.AzureFunctions.Extensions/BindingProviders/MultipartBindingProvider.cs b/src/JoachimDalen.AzureFunctions.Extensions/BindingProviders/MultipartBindingProvider.cs index a9fe115..cbc0896 100644 --- a/src/JoachimDalen.AzureFunctions.Extensions/BindingProviders/MultipartBindingProvider.cs +++ b/src/JoachimDalen.AzureFunctions.Extensions/BindingProviders/MultipartBindingProvider.cs @@ -7,6 +7,7 @@ using JoachimDalen.AzureFunctions.Extensions.Bindings; using JoachimDalen.AzureFunctions.Extensions.Models; using Microsoft.Azure.WebJobs.Host.Bindings; +using Microsoft.Azure.WebJobs.Logging; using Microsoft.Extensions.Logging; namespace JoachimDalen.AzureFunctions.Extensions.BindingProviders @@ -15,9 +16,11 @@ public class MultipartBindingProvider : IBindingProvider { private readonly ILogger _logger; - public MultipartBindingProvider(ILogger logger) + public MultipartBindingProvider(ILoggerFactory loggerFactory) { - _logger = logger; + _logger = loggerFactory != null + ? loggerFactory.CreateLogger(LogCategories.Bindings) + : throw new ArgumentNullException(nameof(loggerFactory)); } public Task TryCreateAsync(BindingProviderContext context) @@ -62,7 +65,7 @@ private IBinding CreateBodyBinding(ILogger log, Type T, MultipartRequestAttribut { var type = typeof(MultipartRequestBinding<>).MakeGenericType(T); var a_Context = Activator.CreateInstance(type, log, attribute); - return (IBinding) a_Context; + return (IBinding)a_Context; } } } \ No newline at end of file diff --git a/src/JoachimDalen.AzureFunctions.Extensions/BindingProviders/QueryParamBindingProvider.cs b/src/JoachimDalen.AzureFunctions.Extensions/BindingProviders/QueryParamBindingProvider.cs index 470bb6e..938fada 100644 --- a/src/JoachimDalen.AzureFunctions.Extensions/BindingProviders/QueryParamBindingProvider.cs +++ b/src/JoachimDalen.AzureFunctions.Extensions/BindingProviders/QueryParamBindingProvider.cs @@ -5,6 +5,7 @@ using JoachimDalen.AzureFunctions.Extensions.Attributes; using JoachimDalen.AzureFunctions.Extensions.Bindings; using Microsoft.Azure.WebJobs.Host.Bindings; +using Microsoft.Azure.WebJobs.Logging; using Microsoft.Extensions.Logging; namespace JoachimDalen.AzureFunctions.Extensions.BindingProviders @@ -22,9 +23,11 @@ public class QueryParamBindingProvider : IBindingProvider typeof(object) }; - public QueryParamBindingProvider(ILogger logger) + public QueryParamBindingProvider(ILoggerFactory loggerFactory) { - _logger = logger; + _logger = loggerFactory != null + ? loggerFactory.CreateLogger(LogCategories.Bindings) + : throw new ArgumentNullException(nameof(loggerFactory)); } public Task TryCreateAsync(BindingProviderContext context) @@ -52,7 +55,7 @@ public Task TryCreateAsync(BindingProviderContext context) var type = typeof(QueryParamBinding<>).MakeGenericType(context.Parameter.ParameterType); var binding = - (IBinding) Activator.CreateInstance(type, _logger, attribute, isUserTypeBinding, context.Parameter); + (IBinding)Activator.CreateInstance(type, _logger, attribute, isUserTypeBinding, context.Parameter); return Task.FromResult(binding); } diff --git a/src/JoachimDalen.AzureFunctions.Extensions/BindingProviders/RequestValueBindingProvider.cs b/src/JoachimDalen.AzureFunctions.Extensions/BindingProviders/RequestValueBindingProvider.cs new file mode 100644 index 0000000..93f12bf --- /dev/null +++ b/src/JoachimDalen.AzureFunctions.Extensions/BindingProviders/RequestValueBindingProvider.cs @@ -0,0 +1,63 @@ +using System; +using System.Globalization; +using System.Reflection; +using System.Threading.Tasks; +using JoachimDalen.AzureFunctions.Extensions.Attributes; +using JoachimDalen.AzureFunctions.Extensions.Bindings; +using Microsoft.Azure.WebJobs.Host.Bindings; +using Microsoft.Azure.WebJobs.Logging; +using Microsoft.Extensions.Logging; + +namespace JoachimDalen.AzureFunctions.Extensions.BindingProviders +{ + public class RequestValueBindingProvider : IBindingProvider + { + private readonly ILogger _logger; + + private static readonly Type[] SupportedTypes = + { + typeof(Guid), + typeof(bool), + typeof(int), + typeof(string), + typeof(object) + }; + + public RequestValueBindingProvider(ILoggerFactory loggerFactory) + { + _logger = loggerFactory != null + ? loggerFactory.CreateLogger(LogCategories.Bindings) + : throw new ArgumentNullException(nameof(loggerFactory)); + } + + public Task TryCreateAsync(BindingProviderContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var parameter = context.Parameter; + var attribute = context.Parameter.GetCustomAttribute(); + + if (attribute == null) + { + return Task.FromResult(null); + } + + var isSupportedTypeBinding = BindingHelpers.MatchParameterType(parameter, SupportedTypes); + var isUserTypeBinding = !isSupportedTypeBinding && BindingHelpers.IsValidUserType(parameter.ParameterType); + if (!isSupportedTypeBinding && !isUserTypeBinding) + { + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, + "Can't bind RequestValueAttribute to type '{0}'.", parameter.ParameterType)); + } + + var binding = + (IBinding)Activator.CreateInstance(typeof(RequestValueBinding), _logger, attribute, isUserTypeBinding, + context.Parameter); + + return Task.FromResult(binding); + } + } +} \ No newline at end of file diff --git a/src/JoachimDalen.AzureFunctions.Extensions/Bindings/RequestValueBinding.cs b/src/JoachimDalen.AzureFunctions.Extensions/Bindings/RequestValueBinding.cs new file mode 100644 index 0000000..f4b7d7f --- /dev/null +++ b/src/JoachimDalen.AzureFunctions.Extensions/Bindings/RequestValueBinding.cs @@ -0,0 +1,52 @@ +using System; +using System.Reflection; +using System.Threading.Tasks; +using JoachimDalen.AzureFunctions.Extensions.Attributes; +using JoachimDalen.AzureFunctions.Extensions.ValueProviders; +using Microsoft.AspNetCore.Http; +using Microsoft.Azure.WebJobs.Host.Bindings; +using Microsoft.Azure.WebJobs.Host.Protocols; +using Microsoft.Extensions.Logging; + +namespace JoachimDalen.AzureFunctions.Extensions.Bindings +{ + public class RequestValueBinding : IBinding + { + private readonly ILogger _logger; + private readonly RequestValueAttribute _attribute; + private readonly ParameterInfo _parameter; + private readonly bool _isUserTypeBinding; + + public RequestValueBinding(ILogger logger, RequestValueAttribute attribute, bool isUserTypeBinding, + ParameterInfo parameter) + { + _logger = logger; + _attribute = attribute; + _isUserTypeBinding = isUserTypeBinding; + _parameter = parameter; + } + + + public async Task BindAsync(object value, ValueBindingContext context) + { + return null; + } + + public Task BindAsync(BindingContext context) + { + if (!context.BindingData.ContainsKey(Constants.DefaultRequestKey)) + { + throw new InvalidOperationException("Failed to find request in binding context"); + } + + var request = context.BindingData[Constants.DefaultRequestKey] as HttpRequest; + return Task.FromResult(new RequestValueProvider (request, _attribute, _parameter, + _isUserTypeBinding, _logger)); + } + + public ParameterDescriptor ToParameterDescriptor() + => new ParameterDescriptor(); + + public bool FromAttribute => true; + } +} \ No newline at end of file diff --git a/src/JoachimDalen.AzureFunctions.Extensions/ConfigProviders/ConfigProvider.cs b/src/JoachimDalen.AzureFunctions.Extensions/ConfigProviders/ConfigProvider.cs index 40f4f0a..0292885 100644 --- a/src/JoachimDalen.AzureFunctions.Extensions/ConfigProviders/ConfigProvider.cs +++ b/src/JoachimDalen.AzureFunctions.Extensions/ConfigProviders/ConfigProvider.cs @@ -7,17 +7,18 @@ namespace JoachimDalen.AzureFunctions.Extensions.ConfigProviders { public class ConfigProvider : IExtensionConfigProvider { - private readonly ILogger _logger; + private readonly ILoggerFactory _loggerFactory; - public ConfigProvider(ILogger logger) + public ConfigProvider(ILoggerFactory loggerFactory) { - _logger = logger; + _loggerFactory = loggerFactory; } public void Initialize(ExtensionConfigContext context) { - context.AddBindingRule().Bind(new QueryParamBindingProvider(_logger)); - context.AddBindingRule().Bind(new MultipartBindingProvider(_logger)); + context.AddBindingRule().Bind(new QueryParamBindingProvider(_loggerFactory)); + context.AddBindingRule().Bind(new MultipartBindingProvider(_loggerFactory)); + context.AddBindingRule().Bind(new RequestValueBindingProvider(_loggerFactory)); } } } \ No newline at end of file diff --git a/src/JoachimDalen.AzureFunctions.Extensions/Converters.cs b/src/JoachimDalen.AzureFunctions.Extensions/Converters.cs new file mode 100644 index 0000000..ab78d9e --- /dev/null +++ b/src/JoachimDalen.AzureFunctions.Extensions/Converters.cs @@ -0,0 +1,45 @@ +using System; + +namespace JoachimDalen.AzureFunctions.Extensions +{ + internal static class Converters + { + internal static bool TryCreateValue(object input, Type inputType, out object value) + { + value = default; + var convertType = Nullable.GetUnderlyingType(inputType) ?? inputType; + + if (input == null) return default; + + if (convertType == typeof(string)) + { + value = input.ToString(); + return true; + } + + if (convertType == typeof(Guid)) + { + if (!Guid.TryParse(input.ToString(), out Guid guid)) + { + return false; + } + + value = guid; + return true; + } + + if (convertType == typeof(int)) + { + if (!int.TryParse(input.ToString(), out int intVal)) + { + return false; + } + + value = intVal; + return true; + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/JoachimDalen.AzureFunctions.Extensions/JoachimDalen.AzureFunctions.Extensions.csproj b/src/JoachimDalen.AzureFunctions.Extensions/JoachimDalen.AzureFunctions.Extensions.csproj index 970bac0..e0134d7 100644 --- a/src/JoachimDalen.AzureFunctions.Extensions/JoachimDalen.AzureFunctions.Extensions.csproj +++ b/src/JoachimDalen.AzureFunctions.Extensions/JoachimDalen.AzureFunctions.Extensions.csproj @@ -18,4 +18,10 @@ + + + <_Parameter1>$(AssemblyName).UnitTests + + + diff --git a/src/JoachimDalen.AzureFunctions.Extensions/Models/MultipartRequestData.cs b/src/JoachimDalen.AzureFunctions.Extensions/Models/MultipartRequestData.cs index 9a3916d..c232dd8 100644 --- a/src/JoachimDalen.AzureFunctions.Extensions/Models/MultipartRequestData.cs +++ b/src/JoachimDalen.AzureFunctions.Extensions/Models/MultipartRequestData.cs @@ -1,8 +1,14 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using JoachimDalen.AzureFunctions.Extensions.Abstractions; + namespace JoachimDalen.AzureFunctions.Extensions.Models { - public class MultipartRequestData + public class MultipartRequestData : IValidatable { public T Data { get; set; } public MultipartFile[] Files { get; set; } + public bool IsValid { get; set; } + public IEnumerable ValidationResults { get; set; } } } \ No newline at end of file diff --git a/src/JoachimDalen.AzureFunctions.Extensions/Models/RequestValueLocation.cs b/src/JoachimDalen.AzureFunctions.Extensions/Models/RequestValueLocation.cs new file mode 100644 index 0000000..f23d8a2 --- /dev/null +++ b/src/JoachimDalen.AzureFunctions.Extensions/Models/RequestValueLocation.cs @@ -0,0 +1,11 @@ +using System; + +namespace JoachimDalen.AzureFunctions.Extensions.Models +{ + [Flags] + public enum RequestValueLocation + { + Query = 1, + Header = 2 + } +} \ No newline at end of file diff --git a/src/JoachimDalen.AzureFunctions.Extensions/ValueProviders/MultipartRequestValueProvider.cs b/src/JoachimDalen.AzureFunctions.Extensions/ValueProviders/MultipartRequestValueProvider.cs index abe9e98..a880eaf 100644 --- a/src/JoachimDalen.AzureFunctions.Extensions/ValueProviders/MultipartRequestValueProvider.cs +++ b/src/JoachimDalen.AzureFunctions.Extensions/ValueProviders/MultipartRequestValueProvider.cs @@ -17,7 +17,8 @@ public class MultipartRequestValueProvider : IValueProvider private readonly HttpRequestMessage _request; private readonly ILogger _logger; - public MultipartRequestValueProvider(MultipartRequestAttribute attribute, HttpRequestMessage request, ILogger logger) + public MultipartRequestValueProvider(MultipartRequestAttribute attribute, HttpRequestMessage request, + ILogger logger) { _attribute = attribute; _request = request; @@ -26,44 +27,43 @@ public MultipartRequestValueProvider(MultipartRequestAttribute attribute, HttpRe public async Task GetValueAsync() { - try - { - var contents = (await _request.Content.ReadAsMultipartAsync()).Contents; - var dataContent = contents.Where(x => x.HasData())?.FirstOrDefault(); - - T dataResult = default; - if (dataContent != null) - { - var stringContent = await dataContent.ReadAsStringAsync(); - dataResult = JsonConvert.DeserializeObject(stringContent); - } + var contents = (await _request.Content.ReadAsMultipartAsync()).Contents; + var dataContent = contents.Where(x => x.HasData())?.FirstOrDefault(); - var filesContents = contents.Where(content => content.HasFiles(_attribute.FileName)); + T dataResult = default; + if (dataContent != null) + { + var stringContent = await dataContent.ReadAsStringAsync(); + dataResult = JsonConvert.DeserializeObject(stringContent); + } - var files = new List(); + var filesContents = contents.Where(content => content.HasFiles(_attribute.FileName)); - foreach (var uploadedFile in filesContents) - { - var fileName = uploadedFile.Headers?.ContentDisposition?.GetEscapedContentDispositionFileName(); - var fileContents = await uploadedFile.ReadAsByteArrayAsync(); - files.Add(new MultipartFile - { - FileName = fileName, - Content = fileContents - }); - } + var files = new List(); - return new MultipartRequestData + foreach (var uploadedFile in filesContents) + { + var fileName = uploadedFile.Headers?.ContentDisposition?.GetEscapedContentDispositionFileName(); + var fileContents = await uploadedFile.ReadAsByteArrayAsync(); + files.Add(new MultipartFile { - Data = dataResult, - Files = files?.ToArray() - }; + FileName = fileName, + Content = fileContents + }); } - catch (Exception ex) + + var container = new MultipartRequestData { - _logger.LogCritical(ex, "Error deserializing object from body"); - throw ex; + Data = dataResult, + Files = files?.ToArray() + }; + + if (_attribute.ValidateData) + { + container.Validate(dataResult); } + + return container; } public Type Type => typeof(object); diff --git a/src/JoachimDalen.AzureFunctions.Extensions/ValueProviders/QueryParamValueProvider.cs b/src/JoachimDalen.AzureFunctions.Extensions/ValueProviders/QueryParamValueProvider.cs index b8857e8..4d79d4c 100644 --- a/src/JoachimDalen.AzureFunctions.Extensions/ValueProviders/QueryParamValueProvider.cs +++ b/src/JoachimDalen.AzureFunctions.Extensions/ValueProviders/QueryParamValueProvider.cs @@ -32,133 +32,87 @@ public QueryParamValueProvider(HttpRequest request, QueryParamAttribute attribut public async Task GetValueAsync() { - try + if (_isUserTypeBinding) { - if (_isUserTypeBinding) + var isContainerType = BindingHelpers.IsOfGenericType(_parameter, typeof(QueryParamContainer<>)); + object container = null; + PropertyInfo[] properties; + Type containerValueType = null; + if (isContainerType) { - var isContainerType = BindingHelpers.IsOfGenericType(_parameter, typeof(QueryParamContainer<>)); - object container = null; - PropertyInfo[] properties; - Type containerValueType = null; - if (isContainerType) - { - containerValueType = _parameter.ParameterType.GetGenericArguments()?.FirstOrDefault(); - container = Activator.CreateInstance(containerValueType); - properties = containerValueType.GetProperties(); - } - else + containerValueType = _parameter.ParameterType.GetGenericArguments()?.FirstOrDefault(); + container = Activator.CreateInstance(containerValueType); + properties = containerValueType.GetProperties(); + } + else + { + container = Activator.CreateInstance(_parameter.ParameterType); + properties = _parameter.ParameterType.GetProperties(); + } + + + foreach (var propertyInfo in properties) + { + var attribute = propertyInfo.GetCustomAttribute(); + var name = attribute?.Name ?? propertyInfo.Name; + if (!_request.Query.TryGetValue(name, out var queryValues)) { - container = Activator.CreateInstance(_parameter.ParameterType); - properties = _parameter.ParameterType.GetProperties(); + continue; } + var value = queryValues.First(); - foreach (var propertyInfo in properties) + if (!Converters.TryCreateValue(value, propertyInfo.PropertyType, out var convertedValue)) { - var attribute = propertyInfo.GetCustomAttribute(); - var name = attribute?.Name ?? propertyInfo.Name; - if (!_request.Query.TryGetValue(name, out var queryValues)) - { - continue; - } - - var value = queryValues.First(); - - if (!TryCreateValue(value, propertyInfo.PropertyType, out var convertedValue)) - { - continue; - } - - if (propertyInfo.CanWrite) - { - propertyInfo.SetValue(container, convertedValue); - } + continue; } - if (isContainerType) + if (propertyInfo.CanWrite) { - var type = typeof(QueryParamContainer<>).MakeGenericType(containerValueType); - var containerInstance = Activator.CreateInstance(type); - - if (_attribute.Validate && containerInstance is IValidatable validatable) - { - validatable.Validate(container); - } - - - var param = type.GetProperty("Params"); - if (param != null && param.CanWrite) - { - param.SetValue(containerInstance, container); - } - - return containerInstance; + propertyInfo.SetValue(container, convertedValue); } - - return container; } - - if (!_request.Query.TryGetValue(_attribute.Name, out var values)) + if (isContainerType) { - return null; - } + var type = typeof(QueryParamContainer<>).MakeGenericType(containerValueType); + var containerInstance = Activator.CreateInstance(type); - var rawValue = values.First(); + if (_attribute.Validate && containerInstance is IValidatable validatable) + { + validatable.Validate(container); + } - if (!typeof(T).IsAssignableFrom(rawValue.GetType())) - { - return null; - } - return rawValue; - } - catch (Exception ex) - { - _logger.LogCritical(ex, "Error deserializing object from body"); - throw ex; - } - } + var param = type.GetProperty("Params"); + if (param != null && param.CanWrite) + { + param.SetValue(containerInstance, container); + } - public Type Type => typeof(object); - public string ToInvokeString() => string.Empty; + return containerInstance; + } - private bool TryCreateValue(object input, Type inputType, out object value) - { - value = default; - var convertType = Nullable.GetUnderlyingType(inputType) ?? inputType; + return container; + } - if (input == null) return default; - if (convertType == typeof(string)) + if (!_request.Query.TryGetValue(_attribute.Name, out var values)) { - value = input.ToString(); - return true; + return null; } - if (convertType == typeof(Guid)) - { - if (!Guid.TryParse(input.ToString(), out Guid guid)) - { - return false; - } - - value = guid; - return true; - } + var rawValue = values.First(); - if (convertType == typeof(int)) + if (!typeof(T).IsAssignableFrom(rawValue.GetType())) { - if (!int.TryParse(input.ToString(), out int intVal)) - { - return false; - } - - value = intVal; - return true; + return null; } - return false; + return rawValue; } + + public Type Type => typeof(object); + public string ToInvokeString() => string.Empty; } } \ No newline at end of file diff --git a/src/JoachimDalen.AzureFunctions.Extensions/ValueProviders/RequestValueProvider.cs b/src/JoachimDalen.AzureFunctions.Extensions/ValueProviders/RequestValueProvider.cs new file mode 100644 index 0000000..c6c75fb --- /dev/null +++ b/src/JoachimDalen.AzureFunctions.Extensions/ValueProviders/RequestValueProvider.cs @@ -0,0 +1,134 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using JoachimDalen.AzureFunctions.Extensions.Attributes; +using JoachimDalen.AzureFunctions.Extensions.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.Azure.WebJobs.Host.Bindings; +using Microsoft.Extensions.Logging; + +namespace JoachimDalen.AzureFunctions.Extensions.ValueProviders +{ + public class RequestValueProvider : IValueProvider + { + private readonly HttpRequest _request; + private readonly RequestValueAttribute _attribute; + private readonly ParameterInfo _parameter; + private readonly bool _isUserTypeBinding; + private readonly ILogger _logger; + + public RequestValueProvider(HttpRequest request, RequestValueAttribute attribute, ParameterInfo parameter, + bool isUserTypeBinding, + ILogger logger) + { + _request = request; + _attribute = attribute; + _parameter = parameter; + _isUserTypeBinding = isUserTypeBinding; + _logger = logger; + } + + public async Task GetValueAsync() + { + object value = null; + + if (_attribute.Location.HasFlag(RequestValueLocation.Header)) + { + if (TryGetFromHeader(_attribute.Name, out var val)) + { + value = val; + } + else + { + if (_attribute.Aliases != null && TryGetFromHeader(_attribute.Aliases, out var aliasValue)) + { + value = aliasValue; + } + } + } + + if (value == null && _attribute.Location.HasFlag(RequestValueLocation.Query)) + { + if (TryGetFromQuery(_attribute.Name, out var val)) + { + value = val; + } + else + { + if (_attribute.Aliases != null && TryGetFromQuery(_attribute.Aliases, out var aliasValue)) + { + value = aliasValue; + } + } + } + + + if (Converters.TryCreateValue(value, _parameter.ParameterType, out var convertedValue)) + { + return convertedValue; + } + + return value; + } + + private bool TryGetFromHeader(string key, out object val) + { + if (_request.Headers.TryGetValue(key, out var vals)) + { + val = vals; + return true; + } + + val = null; + return false; + } + + private bool TryGetFromHeader(string[] keys, out object val) + { + if (_request.Headers.Any(x => keys.Contains(x.Key))) + { + var entry = _request.Headers.FirstOrDefault(x => keys.Contains(x.Key)); + var value = entry.Value; + val = value; + return true; + } + + val = null; + return false; + } + + private bool TryGetFromQuery(string key, out object val) + { + if (_request.Query.TryGetValue(key, out var vals)) + { + val = vals; + return true; + } + + val = null; + return false; + } + + private bool TryGetFromQuery(string[] keys, out object val) + { + if (_request.Query.Any(x => keys.Contains(x.Key))) + { + var entry = _request.Query.FirstOrDefault(x => keys.Contains(x.Key)); + var value = entry.Value; + val = value; + return true; + } + + val = null; + return false; + } + + public string ToInvokeString() + { + return string.Empty; + } + + public Type Type { get; } + } +} \ No newline at end of file