Skip to content

Commit

Permalink
Merge pull request #61 from VahidFarahmandian/60-send-multipartform-d…
Browse files Browse the repository at this point in the history
…ata-using-httpclientfactory

60 send multipartform data using httpclientfactory
  • Loading branch information
VahidFarahmandian committed Aug 12, 2023
2 parents 8c2676d + 17ddcfc commit f213bd2
Show file tree
Hide file tree
Showing 11 changed files with 1,076 additions and 30 deletions.
69 changes: 69 additions & 0 deletions 01-Core/Jinget.Core/Utilities/Http/HeaderUtility.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Mime;

namespace Jinget.Core.Utilities.Http
{
public class HeaderUtility
{
/// <summary>
/// check if content type header presents in the given collection or not.
/// This method does serch for the key using <seealso cref="StringComparison.OrdinalIgnoreCase"/>
/// </summary>
public static bool HasContentType(Dictionary<string, string> headers)
=>
headers != null &&
headers.Keys.Any(x => string.Equals(x, "Content-Type", StringComparison.OrdinalIgnoreCase));

/// <summary>
/// searches for content-type header and if presents, return its value.
/// This method searches for the content-type header using <seealso cref="HasContentType(Dictionary{string, string})"/>
/// </summary>
public static string GetContentTypeValue(Dictionary<string, string> headers)
{
if (headers != null && HasContentType(headers))
{
return headers.FirstOrDefault(x => string.Equals(x.Key, "Content-Type", StringComparison.OrdinalIgnoreCase)).Value;
}
return null;
}

/// <summary>
/// searches for content-type header and if presents, return its key.
/// This method searches for the content-type header using <seealso cref="HasContentType(Dictionary{string, string})"/>
/// </summary>
public static string GetContentTypeHeaderName(Dictionary<string, string> headers)
{
if (headers != null && HasContentType(headers))
return headers.FirstOrDefault(x => string.Equals(x.Key, "Content-Type", StringComparison.OrdinalIgnoreCase)).Key;
return null;
}

/// <summary>
/// check if <seealso cref="MediaTypeNames.Application.Xml"/>
/// or <seealso cref="MediaTypeNames.Text.Xml"/> exists in the given header collection
/// </summary>
public static bool IsXmlContentType(Dictionary<string, string> headers)
=>
HasContentType(headers) &&
(string.Equals(GetContentTypeValue(headers), MediaTypeNames.Text.Xml, StringComparison.OrdinalIgnoreCase) ||
GetContentTypeValue(headers).StartsWith(MediaTypeNames.Application.Xml, StringComparison.OrdinalIgnoreCase));

/// <summary>
/// check if <seealso cref="MediaTypeNames.Application.Json"/> exists in the given header collection
/// </summary>
public static bool IsJsonContentType(Dictionary<string, string> headers)
=>
HasContentType(headers) &&
GetContentTypeValue(headers).StartsWith(MediaTypeNames.Application.Json, StringComparison.OrdinalIgnoreCase);

/// <summary>
/// check if <seealso cref="MediaTypeNames.Application.mu"/> exists in the given header collection
/// </summary>
public static bool IsMultiPartFormDataContentType(Dictionary<string, string> headers)
=>
HasContentType(headers) &&
GetContentTypeValue(headers).StartsWith(MediaTypeNames.Application.Json, StringComparison.OrdinalIgnoreCase);
}
}
811 changes: 811 additions & 0 deletions 01-Core/Jinget.Core/Utilities/Http/MimeTypeMap.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Net.Mime;
using System.Threading.Tasks;
Expand Down Expand Up @@ -56,6 +57,12 @@ public async Task<TResponseModel> PostAsync(object content = null, Dictionary<st
public async Task<TResponseModel> PostAsync(string url, object content = null, Dictionary<string, string> headers = null)
=> await ProcessTask(async () => await HttpClientFactory.PostAsync(url, content, headers));

public async Task<TResponseModel> UploadFileAsync(string url, List<FileInfo> files = null, Dictionary<string, string> headers = null)
=> await ProcessTask(async () => await HttpClientFactory.UploadFileAsync(url, files, headers));

public async Task<TResponseModel> UploadFileAsync(string url, MultipartFormDataContent multipartFormData = null, Dictionary<string, string> headers = null)
=> await ProcessTask(async () => await HttpClientFactory.UploadFileAsync(url, multipartFormData, headers));

public async Task<TResponseModel> SendAsync(HttpRequestMessage message) => await ProcessTask(async () => await HttpClientFactory.SendAsync(message));
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Mime;
using System.Reflection.Metadata;
using System.Text;
using System.Threading.Tasks;
using Jinget.Core.Utilities;
using Jinget.Core.Utilities.Http;
using Newtonsoft.Json;

namespace Jinget.Handlers.ExternalServiceHandlers.ServiceHandler.Factory
Expand All @@ -23,25 +25,16 @@ internal HttpClientFactory(string baseUri, bool ignoreSslErrors = false)
ServerCertificateCustomValidationCallback = (message, cert, chain, sslPolicyErrors) => true
};

client = new HttpClient(httpClientHandler)
{
BaseAddress = new Uri(baseUri)
};
client = new HttpClient(httpClientHandler);
}
else
{
client = new HttpClient
{
BaseAddress = new Uri(baseUri)
};
client = new HttpClient();
}
client.BaseAddress = new Uri(baseUri);
}

private Uri GetUrl(string url)
{
string baseUrl = client.BaseAddress.ToString().EndsWith("/") ? client.BaseAddress.ToString() : $"{client.BaseAddress}/";
return new Uri($"{baseUrl}{url}".TrimEnd('/'));
}
private Uri GetUrl(string url) => new Uri($"{client.BaseAddress.ToString().TrimEnd('/')}/{url}".TrimEnd('/'));

#nullable enable
private void SetHeaders(Dictionary<string, string>? headers)
Expand All @@ -67,30 +60,50 @@ private void SetHeaders(Dictionary<string, string>? headers)
}
}

public async Task<HttpResponseMessage> UploadFileAsync(string url, List<FileInfo> files = null, Dictionary<string, string> headers = null)
{
using var multipartFormContent = new MultipartFormDataContent();
foreach (var item in files)
{
//bool isMimeTypeAvailable = MimeTypeMap.TryGetMimeType(item.Name, out var mimeType);

var st = new ByteArrayContent(await File.ReadAllBytesAsync(item.FullName));
//st.Headers.ContentType = new MediaTypeHeaderValue(isMimeTypeAvailable ? mimeType : MediaTypeNames.Application.Octet);
multipartFormContent.Add(st, "file", item.Name);
}
return await UploadFileAsync(url, multipartFormContent, headers);
}
public async Task<HttpResponseMessage> UploadFileAsync(string url, MultipartFormDataContent multipartFormData, Dictionary<string, string> headers = null)
{
SetHeaders(headers);
return await client.PostAsync(GetUrl(url), multipartFormData);
}

public async Task<HttpResponseMessage> PostAsync(string url, object content = null, Dictionary<string, string> headers = null)
{
if (url != "" && url.StartsWith("/"))
throw new Core.Exceptions.JingetException($"{nameof(url)} should not start with '/'");
if (!string.IsNullOrWhiteSpace(url) && url.StartsWith('/'))
url = url.TrimStart('/');
if (content is MultipartFormDataContent)
{
return await UploadFileAsync(url, content as MultipartFormDataContent, headers);
}

StringContent bodyContent = null;
if (content != null)
{
if (headers != null && headers.Keys.Any(x => x == "Content-Type") && headers["Content-Type"].ToLower() != MediaTypeNames.Application.Json)
if (HeaderUtility.IsXmlContentType(headers))
{
if (headers["Content-Type"] == MediaTypeNames.Text.Xml || headers["Content-Type"] == MediaTypeNames.Application.Xml)
{
bodyContent = new StringContent(content is string ? content.ToString() : XmlUtility.SerializeToXml(content), Encoding.UTF8, headers["Content-Type"]);
}
else
{
bodyContent = new StringContent(content.ToString(), Encoding.UTF8, headers["Content-Type"]);
}
bodyContent = new StringContent(content is string ? content.ToString() : XmlUtility.SerializeToXml(content), Encoding.UTF8, headers["Content-Type"]);
}
else
else if (HeaderUtility.IsJsonContentType(headers))
{
bodyContent = new StringContent(JsonConvert.SerializeObject(content), Encoding.UTF8, MediaTypeNames.Application.Json);
}
headers?.Remove("Content-Type");
else
{
bodyContent = new StringContent(content.ToString(), Encoding.UTF8, HeaderUtility.GetContentTypeValue(headers));
}
headers?.Remove(HeaderUtility.GetContentTypeHeaderName(headers));
}

SetHeaders(headers);
Expand All @@ -99,9 +112,9 @@ public async Task<HttpResponseMessage> PostAsync(string url, object content = nu

public async Task<HttpResponseMessage> GetAsync(string url, Dictionary<string, string> headers = null)
{
if (url.StartsWith("/"))
throw new Core.Exceptions.JingetException($"{nameof(url)} should not start with '/'");
SetHeaders(headers);
if (url.StartsWith('/'))
url = url.TrimStart('/'); SetHeaders(headers);

return await client.GetAsync(GetUrl(url));
}

Expand Down
76 changes: 76 additions & 0 deletions Tests/Jinget.Core.Tests/Utilities/Http/HeaderUtilityTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Collections.Generic;

namespace Jinget.Core.Utilities.Http.Tests
{
[TestClass()]
public class HeaderUtilityTests
{
[TestMethod()]
public void should_return_true_when_contenttype_exists()
{
Dictionary<string, string> headers = new()
{
{ "content-type","some thing..." }
};
var result = HeaderUtility.HasContentType(headers);
Assert.IsTrue(result);
}

[TestMethod()]
public void should_return_true_for_text_xml_contenttype_header()
{
Dictionary<string, string> headers = new()
{
{ "content-type","text/xml" }
};
var result = HeaderUtility.IsXmlContentType(headers);
Assert.IsTrue(result);
}

[TestMethod()]
public void should_return_true_for_application_xml_contenttype_header()
{
Dictionary<string, string> headers = new()
{
{ "content-type","application/xml" }
};
var result = HeaderUtility.IsXmlContentType(headers);
Assert.IsTrue(result);
}

[TestMethod()]
public void should_return_true_for_application_json_contenttype_header()
{
Dictionary<string, string> headers = new()
{
{ "content-type","application/json" }
};
var result = HeaderUtility.IsJsonContentType(headers);
Assert.IsTrue(result);
}

[TestMethod()]
public void should_return_false_for_non_xml_json_contenttype_header()
{
Dictionary<string, string> headers = new()
{
{ "content-type","some thing..." }
};
var result = HeaderUtility.IsXmlContentType(headers);
Assert.IsFalse(result);
}

[TestMethod()]
public void should_return_contenttype_header_name()
{
string expectedResult = "content-type";
Dictionary<string, string> headers = new()
{
{ "content-type","some thing..." }
};
var result = HeaderUtility.GetContentTypeHeaderName(headers);
Assert.AreEqual(result, expectedResult);
}
}
}
30 changes: 30 additions & 0 deletions Tests/Jinget.Core.Tests/Utilities/Http/MimeTypeMapTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Jinget.Core.Utilities.Http.Tests
{
[TestClass()]
public class MimeTypeMapTests
{
[TestMethod()]
public void should_try_return_valid_mimetype_for_givent_filename()
{
bool result = MimeTypeMap.TryGetMimeType("sample.txt", out string mimeType);
Assert.IsTrue(result);
Assert.AreEqual("text/plain", mimeType);
}

[TestMethod()]
public void should_return_valid_mimetype_for_givent_filename()
{
string result = MimeTypeMap.GetMimeType("sample.txt");
Assert.AreEqual("text/plain", result);
}

[TestMethod()]
public void should_return_file_extension_for_given_mimetype()
{
string result = MimeTypeMap.GetExtension("text/plain");
Assert.AreEqual(".txt", result);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using Jinget.Handlers.ExternalServiceHandlers.Tests.DefaultServiceHandler.SampleType;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
using System.Net.Http.Headers;
using System.Net.Mime;

#pragma warning disable CS8622 // Nullability of reference types in type of parameter doesn't match the target delegate (possibly because of nullability attributes).
namespace Jinget.Handlers.ExternalServiceHandlers.DefaultServiceHandler.Tests
Expand Down Expand Up @@ -143,6 +145,29 @@ public async Task should_call_get_soap()
Assert.IsFalse(result is null);
Assert.AreEqual(3, result.AddResult);
}

//[TestMethod]
public async Task should_post_multipart_formdata()
{
var jingetServiceHandler = new JingetServiceHandler<SamplePostResponse>("https://localhost:7027/api/upload", true);
jingetServiceHandler.Events.ServiceCalled += (object sender, HttpResponseMessage e) =>
{
Assert.IsTrue(e.IsSuccessStatusCode);
};
jingetServiceHandler.Events.ResponseDeserialized += (object sender, SamplePostResponse e) =>
{
Assert.IsFalse(e is null);
};

List<FileInfo> files = new() {
new FileInfo("Sample Upload File1.txt") ,
new FileInfo("Sample Upload File2.txt")
};

var response = await jingetServiceHandler.UploadFileAsync("something", files);

Assert.IsFalse(string.IsNullOrWhiteSpace(response.Status));
}
}
}
#pragma warning restore CS8622 // Nullability of reference types in type of parameter doesn't match the target delegate (possibly because of nullability attributes).
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ public class SamplePostResponse
public string? Body { get; set; }
public int UserId { get; set; }
public int Id { get; set; }
public string? Status{ get; set; }
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,16 @@
<ProjectReference Include="..\..\02-Handlers\Jinget.Handlers.ExternalServiceHandlers\Jinget.Handlers.ExternalServiceHandlers.csproj" />
</ItemGroup>

<ItemGroup>
<None Update="Sample Upload File.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Sample Upload File1.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Sample Upload File2.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>

Large diffs are not rendered by default.

Large diffs are not rendered by default.

0 comments on commit f213bd2

Please sign in to comment.