In my Web API, The POST action method uploads a file on server.
For unit testing this method, I need to create a HttpContext and put a file inside its request:
HttpContext.Current.Request.Files
So far I am faking HttpContext with this Code which works perfectly:
HttpRequest request = new HttpRequest("", "http://localhost/", "");
HttpResponse response = new HttpResponse(new StringWriter());
HttpContext.Current = new HttpContext(request, response);
Note that I DON'T want to use Moq or any other Mocking libraries.
How can I accomplish this? (MultipartContent maybe?)
Thanks
This is useful when you have a common service that is used by your controllers. You can then access the current HTTP context in a safe way: var context = _httpContextAccessor. HttpContext; // Do something with the current HTTP context...
When an HTTP request arrives at the server, the server processes the request and builds an HttpContext object. This object represents the request which your application code can use to create the response.
HTTPContext. Current is a static property. This property is a static property of the HttpContext class. The property stores the HttpContext instance that applies to the current request. The properties of this instance are the non-static properties of the HttpContext class.
I was eventually able to add fake files to HttpContext
for WebApi unit tests by making heavy use of reflection, given that most of the Request.Files
infrastructure lies hidden away in sealed or internal classes.
Once you've added the code below, files can be added relatively easily to HttpContext.Current
:
var request = new HttpRequest(null, "http://tempuri.org", null);
AddFileToRequest(request, "File", "img/jpg", new byte[] {1,2,3,4,5});
HttpContext.Current = new HttpContext(
request,
new HttpResponse(new StringWriter());
With the heavy lifting done by:
static void AddFileToRequest(
HttpRequest request, string fileName, string contentType, byte[] bytes)
{
var fileSize = bytes.Length;
// Because these are internal classes, we can't even reference their types here
var uploadedContent = ReflectionHelpers.Construct(typeof (HttpPostedFile).Assembly,
"System.Web.HttpRawUploadedContent", fileSize, fileSize);
uploadedContent.InvokeMethod("AddBytes", bytes, 0, fileSize);
uploadedContent.InvokeMethod("DoneAddingBytes");
var inputStream = Construct(typeof (HttpPostedFile).Assembly,
"System.Web.HttpInputStream", uploadedContent, 0, fileSize);
var postedFile = Construct<HttpPostedFile>(fileName,
contentType, inputStream);
// Accessing request.Files creates an empty collection
request.Files.InvokeMethod("AddFile", fileName, postedFile);
}
public static object Construct(Assembly assembly, string typeFqn, params object[] args)
{
var theType = assembly.GetType(typeFqn);
return theType
.GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null,
args.Select(a => a.GetType()).ToArray(), null)
.Invoke(args);
}
public static T Construct<T>(params object[] args) where T : class
{
return Activator.CreateInstance(
typeof(T),
BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance,
null, args, null) as T;
}
public static object InvokeMethod(this object o, string methodName,
params object[] args)
{
var mi = o.GetType().GetMethod(methodName,
BindingFlags.NonPublic | BindingFlags.Instance);
if (mi == null) throw new ArgumentOutOfRangeException("methodName",
string.Format("Method {0} not found", methodName));
return mi.Invoke(o, args);
}
Usually it's a bad practice to use objects that hard to mock in controllers (objects like HttpContext, HttpRequest, HttpResponse
etc). For example in MVC applications we have ModelBinder
and HttpPostedFileBase
object that we can use in controller to avoid working with HttpContext
(for Web Api
application we need to write our own logic).
public ActionResult SaveUser(RegisteredUser data, HttpPostedFileBase file)
{
// some code here
}
So you don't need to work with HttpContext.Current.Request.Files
. It's hard to test. That type of work must be done in another level of your application (not in the controller). In Web Api
we can write MediaTypeFormatter for that purposes.
public class FileFormatter : MediaTypeFormatter
{
public FileFormatter()
{
SupportedMediaTypes.Add(new MediaTypeHeaderValue("multipart/form-data"));
}
public override bool CanReadType(Type type)
{
return typeof(ImageContentList).IsAssignableFrom(type);
}
public override bool CanWriteType(Type type)
{
return false;
}
public async override Task<object> ReadFromStreamAsync(Type type, Stream stream, HttpContent content, IFormatterLogger logger)
{
if (!content.IsMimeMultipartContent())
{
throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
}
var provider = new MultipartMemoryStreamProvider();
var formData = await content.ReadAsMultipartAsync(provider);
var imageContent = formData.Contents
.Where(c => SupportedMediaTypes.Contains(c.Headers.ContentType))
.Select(i => ReadContent(i).Result)
.ToList();
var jsonContent = formData.Contents
.Where(c => !SupportedMediaTypes.Contains(c.Headers.ContentType))
.Select(j => ReadJson(j).Result)
.ToDictionary(x => x.Key, x => x.Value);
var json = JsonConvert.SerializeObject(jsonContent);
var model = JsonConvert.DeserializeObject(json, type) as ImageContentList;
if (model == null)
{
throw new HttpResponseException(HttpStatusCode.NoContent);
}
model.Images = imageContent;
return model;
}
private async Task<ImageContent> ReadContent(HttpContent content)
{
var data = await content.ReadAsByteArrayAsync();
return new ImageContent
{
Content = data,
ContentType = content.Headers.ContentType.MediaType,
Name = content.Headers.ContentDisposition.FileName
};
}
private async Task<KeyValuePair<string, object>> ReadJson(HttpContent content)
{
var name = content.Headers.ContentDisposition.Name.Replace("\"", string.Empty);
var value = await content.ReadAsStringAsync();
if (value.ToLower() == "null")
value = null;
return new KeyValuePair<string, object>(name, value);
}
}
So any content that will be posted with multipart/form-data
content type (and files must be posted with that content-type) will be parsed to the child class of ImageContentList
(so with files you can post any other information). If you want to post 2 or 3 files - it will be working too.
public class ImageContent: IModel
{
public byte[] Content { get; set; }
public string ContentType { get; set; }
public string Name { get; set; }
}
public class ImageContentList
{
public ImageContentList()
{
Images = new List<ImageContent>();
}
public List<ImageContent> Images { get; set; }
}
public class CategoryPostModel : ImageContentList
{
public int? ParentId { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}
Then you can use it in any controller in your application. And it's easy to test because the code of your controller is not depend on HttpContext anymore.
public ImagePostResultModel Post(CategoryPostModel model)
{
// some code here
}
Also you need to register MediaTypeFormatter
for Web Api
configuration
configuration.Formatters.Add(new ImageFormatter());
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With