Testing a Multipart file upload Azure Function - azure

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.

Related

Use OWIN middleware or a delegating MessgaeHandler to log api requests/responses?

In my old non-OWIN APIs, I use a MessageHanlder to log all HttpRequests and HttpResponses. Here is the MessageHandler:
public class MessageHandler : DelegatingHandler
{
private static readonly ILog RequestApiLogger = LogManager.GetLogger("RequestApiPacketLogger");
private static readonly ILog ResponseApiLogger = LogManager.GetLogger("ResponseApiPacketLogger");
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var correlationId = Guid.NewGuid();
RequestApiLogger.LogHttpRequest(request, correlationId);
return await base.SendAsync(request, cancellationToken).ContinueWith(
task =>
{
var response = task.Result;
response.Headers.Add("http-tracking-id", correlationId.ToString("D"));
ResponseApiLogger.LogHttpResponse(response, correlationId);
return response;
}, cancellationToken);
}
}
However, in my newer projects I could write custom OWIN middleware to do something similar using the OwinContext like this:
//use an alias for the OWIN AppFunc
using AppFunc = Func<IDictionary<string, object>, Task>;
public class LoggingMiddleware
{
private readonly AppFunc _next;
public LoggingMiddleware(AppFunc next)
{
_next = next;
}
public async Task Invoke(IDictionary<string, object> environment)
{
IOwinContext context = new OwinContext(environment);
// Get the identity
var identity = (context.Request.User != null && context.Request.User.Identity.IsAuthenticated)
? context.Request.User.Identity.Name
: "(anonymous)";
// Buffer the request (body is a string, we can use this to log the request later
var requestBody = new StreamReader(context.Request.Body).ReadToEnd();
var requestData = Encoding.UTF8.GetBytes(requestBody);
context.Request.Body = new MemoryStream(requestData);
var apiPacket = new ApiPacket
{
CallerIdentity = identity,
Request = requestBody,
RequestLength = context.Request.Body.Length
};
// Buffer the response
var responseBuffer = new MemoryStream();
var responseStream = context.Response.Body;
context.Response.Body = responseBuffer;
// add the "http-tracking-id" response header so the user can correlate back to this entry
var responseHeaders = (IDictionary<string, string[]>)environment["owin.ResponseHeaders"];
responseHeaders["http-tracking-id"] = new[] { apiPacket.TrackingId.ToString("d") };
await _next.Invoke(environment);
responseBuffer.Seek(0, SeekOrigin.Begin);
var reader = new StreamReader(responseBuffer);
apiPacket.Response = await reader.ReadToEndAsync();
apiPacket.ResponseLength = context.Response.ContentLength ?? 0;
WriteRequestHeaders(context.Request, apiPacket);
WriteResponseHeaders(context.Response, apiPacket);
// You need to do this so that the response we buffered is flushed out to the client application.
responseBuffer.Seek(0, SeekOrigin.Begin);
await responseBuffer.CopyToAsync(responseStream);
//TODO: persist the ApiPacket in the database
}
private static void WriteRequestHeaders(IOwinRequest request, ApiPacket packet)
{
packet.Verb = request.Method;
packet.RequestUri = request.Uri;
packet.RequestHeaders = "{\r\n" + string.Join(Environment.NewLine, request.Headers.Select(kv => "\t" + kv.Key + "=" + string.Join(",", kv.Value))) + "\r\n}";
}
private static void WriteResponseHeaders(IOwinResponse response, ApiPacket packet)
{
packet.StatusCode = response.StatusCode;
packet.ReasonPhrase = response.ReasonPhrase;
packet.ResponseHeaders = "{\r\n" + string.Join(Environment.NewLine, response.Headers.Select(kv => "\t" + kv.Key + "=" + string.Join(",", kv.Value))) + "\r\n}";
}
}
I'm using log4net to write the information to a SQL2012 database. Both ways accomplish my goal. However, I'm looking for a reason to use one method over the other. Should I use custom OWIN middleware OR a MessageHandler, and why? Thanks in advance.
Since you already have the MessageHandler implementations, I would recommend using that until you have a reason otherwise.
However, off the top of my head one valid reason to move logging to an OwinMiddleware would be if you have other OwinMiddleware components that require (or would benefit from) that logging functionality (assuming that you are using WebApi whereby the MessageHandlers will run after all of the OwinMiddleware in the request-pipeline).
Looks like I will be using OWIN middleware. I found that inside the MessageHandler the Principal.IIdentity has not yet been resolved. For example, if I put breakpoints in my message handler, an API controller's constructor, and in the API method, this is what I see (in order).
Using Message Handler
In MessageHandler > Principal.IIdentity not yet resolved.
In API controller's constructor > Principal.IIDentity not yet resolved.
In API controller's GET method, the Principal.IIdentity is finally resolved.
Thus, I can't pull out and log the authorized user's id in the MessageHandler.
However, when using the OWIN middleware, the Principal.IIdentity IS resolved there, so I can write the userId to my log table at that point. This is why I've decided to use the middleware.
Maybe someone can provide some clarity as to when the IIDentity is set in an API project though.

How to log ServiceStack.Messaging.Message to a database with OrmLite?

Given the following code:
public class AppHost : BasicAppHost
{
public AppHost()
: base(typeof(LeadService).Assembly){}
public override void Configure(Container container)
{
SetConfig(new HostConfig
{
DebugMode = ConfigUtils.GetAppSetting<bool>("DebugMode:Enabled", false)
});
//DataAccess
//Set ORMLite to work with columns like ColumnLikeThis
PostgreSqlDialect.Provider.NamingStrategy = new OrmLiteNamingStrategyBase();
//Set ORMLite to use ServiceStack.Text for JSON serialization
PostgreSqlDialect.Provider.StringSerializer = new JsonStringSerializer();
var dbFactory = new OrmLiteConnectionFactory(ConfigUtils.GetConnectionString("Lead:Default"), PostgreSQLDialectProvider.Instance);
container.Register<IDbConnectionFactory>(dbFactory);
//RabbitMQ
container.Register<IMessageService>(c => new RabbitMqServer()
{
AutoReconnect = true,
DisablePriorityQueues = true,
});
var mqServer = container.Resolve<IMessageService>();
//Handlers
container.Register<IMessageHandlers>(c => new MessageHandlers(c.Resolve<IDbConnectionFactory>()));
var handlers = container.Resolve<IMessageHandlers>();
mqServer.RegisterHandler<LeadInformation>(handlers.OnProcessLeadInformation, handlers.OnExceptionLeadInformation);
mqServer.Start();
}
}
public class MessageHandlers : IMessageHandlers
{
private readonly ILog _log = LogManager.GetLogger(typeof(MessageHandlers));
private readonly IDbConnectionFactory _connectionFactory;
public MessageHandlers(IDbConnectionFactory connectionFactory)
{
_connectionFactory = connectionFactory;
}
public object OnProcessLeadInformation(IMessage<LeadInformation> request)
{
var sw = Stopwatch.StartNew();
try
{
// Log to the database
using (var db = _connectionFactory.OpenDbConnection())
{
db.CreateTableIfNotExists<Message>();
var msg = request as Message<LeadInformation>; // Anyway not to have to cast it?
db.Save(msg); // Does not work
}
// Run rules against lead
// Log response to database
// return response
}
catch (Exception exception)
{
_log.Error(request, exception);
}
return new LeadInformationResponse
{
TimeTakenMs = sw.ElapsedMilliseconds,
Result = "Processed lead {0}".Fmt(request.GetBody().LeadApplication.LastName)
};
}
public void OnExceptionLeadInformation(IMessage<LeadInformation> request, Exception exception)
{
_log.Error(request, exception);
}
}
Is it possible to persist the whole message? The table gets created, and I was able to save one message, and that's it no more saves with different messages.
Update
Turns out I'm getting an exception during the save operation
Npgsql.NpgsqlException was caught
_HResult=-2147467259
_message=ERROR: 42P01: relation "Message1" does not exist
HResult=-2147467259
IsTransient=false
Message=ERROR: 42P01: relation "Message1" does not exist
Source=Npgsql
ErrorCode=-2147467259
BaseMessage=relation "Message1" does not exist
Code=42P01
ColumnName=""
ConstraintName=""
DataTypeName=""
Detail=""
ErrorSql=SELECT "Id", "CreatedDate", "Priority", "RetryAttempts", "ReplyId", "ReplyTo", "Options", "Error", "Tag", "Body" FROM "Message1" WHERE "Id" = (('ab297bca-5aea-4886-b09b-5a606b0764d5')::uuid)
File=src\backend\parser\parse_relation.c
Hint=""
Line=986
Position=119
Routine=parserOpenTable
SchemaName=""
Severity=ERROR
TableName=""
Where=""
StackTrace:
at Npgsql.NpgsqlState.d__0.MoveNext()
at Npgsql.ForwardsOnlyDataReader.GetNextResponseObject(Boolean cleanup)
at Npgsql.ForwardsOnlyDataReader.GetNextRowDescription()
at Npgsql.ForwardsOnlyDataReader.NextResultInternal()
at Npgsql.ForwardsOnlyDataReader..ctor(IEnumerable1 dataEnumeration, CommandBehavior behavior, NpgsqlCommand command, NotificationThreadBlock threadBlock, Boolean preparedStatement, NpgsqlRowDescription rowDescription)
at Npgsql.NpgsqlCommand.GetReader(CommandBehavior cb)
at Npgsql.NpgsqlCommand.ExecuteReader(CommandBehavior cb)
at Npgsql.NpgsqlCommand.ExecuteDbDataReader(CommandBehavior behavior)
at System.Data.Common.DbCommand.System.Data.IDbCommand.ExecuteReader()
at ServiceStack.OrmLite.OrmLiteReadExtensions.ExecReader(IDbCommand dbCmd, String sql)
at ServiceStack.OrmLite.OrmLiteResultsFilterExtensions.ConvertTo[T](IDbCommand dbCmd, String sql)
at ServiceStack.OrmLite.OrmLiteReadExtensions.SingleById[T](IDbCommand dbCmd, Object value)
at ServiceStack.OrmLite.OrmLiteWriteExtensions.Save[T](IDbCommand dbCmd, T obj)
at ServiceStack.OrmLite.OrmLiteWriteConnectionExtensions.<>c__DisplayClass5a1.b__58(IDbCommand dbCmd)
at ServiceStack.OrmLite.OrmLiteExecFilter.Exec[T](IDbConnection dbConn, Func2 filter)
at ServiceStack.OrmLite.ReadConnectionExtensions.Exec[T](IDbConnection dbConn, Func2 filter)
at ServiceStack.OrmLite.OrmLiteWriteConnectionExtensions.Save[T](IDbConnection dbConn, T obj, Boolean references)
at LO.Leads.Processor.ServiceInterface.MessageHandlers.OnProcessLeadInformation(IMessage`1 request) in e:\Lead\src\LO.Leads.Processor\LO.Leads.Processor.ServiceInterface\MessageHandlers.cs:line 41
Update 2
Turns out my cast was wrong, this now works
using (var db = _connectionFactory.OpenDbConnection())
{
db.CreateTableIfNotExists<Message>();
db.Save(request as Message);
}
Thank you,
Stephen
You have to cast the IMessage back to a Message DTO in order for it to work. e.g.
using (var db = _connectionFactory.OpenDbConnection())
{
db.CreateTableIfNotExists<Message>();
db.Save(request as Message);
}

Custom error pages in servicestack

How do I configure ServiceStack to serve specific error pages (404, 500, etc.) depending on the type of error being returned?
Currently, I'm using the RawHttpHandler below code to ensure that a request for a HTML file is authenticated. However, if the user specifies a non-existent file or endpoint, how can I have it return my 404.html page.
this.RawHttpHandlers.Add(httpReq =>
{
var session = httpReq.GetSession();
if(!session.IsAuthenticated) {
var isHtmlFileRequest = httpReq.PathInfo.EndsWith(".html");
if(isHtmlFileRequest && !files.Any(s => httpReq.PathInfo.ToLower().Contains(s))) {
return new RedirectHttpHandler {
AbsoluteUrl = "/Login.html"
};
}
}
return null;
});
The Error Handling wiki shows different ways to Customize Handling of Exceptions in ServiceStack, e.g you can redirect 404 errors to /404.cshtml with:
public override void Configure(Container container)
{
this.CustomHttpHandlers[HttpStatusCode.NotFound] =
new RazorHandler("/404");
}
CustomHttpHandlers can be any IServiceStackHandler which is just a HttpHandler that supports both ASP.NET and HttpListener requests. The easiest way to create one is to just inherit from IServiceStackHandler. Here's an example of a Custom Static File Handler similar to StaticFileHandler except it only writes the specified filePath instead of using the HTTP Request path:
public class CustomStaticFileHandler : HttpAsyncTaskHandler
{
string filePath;
public CustomStaticFileHandler(string filePath)
{
this.filePath = filePath;
}
public override void ProcessRequest(HttpContextBase context)
{
var httpReq = context.ToRequest(GetType().GetOperationName());
ProcessRequest(httpReq, httpReq.Response, httpReq.OperationName);
}
public override void ProcessRequest(IRequest request, IResponse response,
string operationName)
{
response.EndHttpHandlerRequest(skipClose: true, afterHeaders: r =>
{
var file = HostContext.VirtualPathProvider.GetFile(filePath);
if (file == null)
throw new HttpException(404, "Not Found");
r.SetContentLength(file.Length);
var outputStream = r.OutputStream;
using (var fs = file.OpenRead())
{
fs.CopyTo(outputStream, BufferSize);
outputStream.Flush();
}
}
}
}
This can then be registered as normal, i.e:
public override void Configure(Container container)
{
this.CustomHttpHandlers[HttpStatusCode.NotFound] =
new CustomStaticFileHandler("/404.html");
}

WebAPI can't call Put method

I have the following code on server:
public class UploadController : ApiController
{
public void Put(string filename, string description)
{
...
}
public void Put()
{
...
}
and try to call it from client:
var clientDescr = new HttpClient();
var postData = new List<KeyValuePair<string, string>>();
postData.Add(new KeyValuePair<string, string>("filename", "test"));
postData.Add(new KeyValuePair<string, string>("description", "100"));
HttpContent contentDescr = new FormUrlEncodedContent(postData);
clientDescr.PutAsync("http://localhost:8758/api/upload", contentDescr).ContinueWith(
(postTask) =>
{
postTask.Result.EnsureSuccessStatusCode();
});
but this code calls second put method (without parameters). Why and how to call first put method correctly?
You have several options here:
You can either choose to pass the parameters in the query string, by just changing the URI to:
http://localhost:8758/api/upload?filename=test&description=100
or you can have Web API parse the form data for you by changing your action to look like this:
public void Put(FormDataCollection formData)
{
string fileName = formData.Get("fileName");
string description = formData.Get("description");
}
You can also choose to create a class that has a fileName and a description property and use that as your parameter and Web API should be able to bind it correctly for you.

Render an MVC3 action to a string from a WCF REST service method

I have a WCF REST service that takes some parameters and sends an email. The template for the email is an MVC3 action. Essentially I want to render that action to a string.
If it were an ASP.NET WebForm, I could simply use Server.Execute(path, stringWriter, false). However when I plug in the path to my action, I get Error executing child request.
I have full access to HttpContext from my service (AspNetCompatibilityRequirementsMode.Allowed).
I know there are other answers out there for rendering actions to strings from within the context of a controller. How do I do this when I'm outside that world, but still on the same server (and, for that matter, in the same app)?
I cobbled together an answer based on several different google searches. It works, but I'm not 100% sure it's as lean as it could be. I'll paste the code for others to try.
string GetEmailText(TemplateParameters parameters) {
// Get the HttpContext
HttpContextBase httpContextBase =
new HttpContextWrapper(HttpContext.Current);
// Build the route data
var routeData = new RouteData();
routeData.Values.Add("controller", "EmailTemplate");
routeData.Values.Add("action", "Create");
// Create the controller context
var controllerContext = new ControllerContext(
new RequestContext(httpContextBase, routeData),
new EmailTemplateController());
var body = ((EmailTemplateController)controllerContext.Controller)
.Create(parameters).Capture(controllerContext);
return body;
}
// Using code from here:
// http://blog.approache.com/2010/11/render-any-aspnet-mvc-actionresult-to.html
public class ResponseCapture : IDisposable
{
private readonly HttpResponseBase response;
private readonly TextWriter originalWriter;
private StringWriter localWriter;
public ResponseCapture(HttpResponseBase response)
{
this.response = response;
originalWriter = response.Output;
localWriter = new StringWriter();
response.Output = localWriter;
}
public override string ToString()
{
localWriter.Flush();
return localWriter.ToString();
}
public void Dispose()
{
if (localWriter != null)
{
localWriter.Dispose();
localWriter = null;
response.Output = originalWriter;
}
}
}
public static class ActionResultExtensions
{
public static string Capture(this ActionResult result, ControllerContext controllerContext)
{
using (var it = new ResponseCapture(controllerContext.RequestContext.HttpContext.Response))
{
result.ExecuteResult(controllerContext);
return it.ToString();
}
}
}

Resources