ASP.NET Web API 2 - ReadAsMultipartAsync alternative/how to - asp.net

I'm using Web API 2.2 to receive an HTTP post with form data and files, I'm doing the following:
Synchronous version:
[HttpPost]
public IHttpActionResult CreateTicket()
{
string root = HttpContext.Current.Server.MapPath("~/UserFiles/Temp/");
var provider = new MultipartFormDataStreamProvider(root);
// Timeouts
Request.Content.ReadAsMultipartAsync(provider).Wait(TimeSpan.FromSeconds(20));
...
}
Async version:
[HttpPost]
public async Task<HttpResponseMessage> CreateTicket()
{
string root = HttpContext.Current.Server.MapPath("~/UserFiles/Temp/");
var provider = new MultipartFormDataStreamProvider(root);
// Works
await Request.Content.ReadAsMultipartAsync(provider);
...
// Error because: HttpContext.Current == null
}
Both versions of this code work in IIS 10, but fail on production (IIS 8.5). The first version timeouts and the second works but HttpContext.Current is not available which makes other parts of my code fail.
How can I make either the upload synchronous (preferred) or make the async code have HttpContext.Current available?
UPDATE:
Just discovered that the problem (in the second code fragment) only happens if the application does not have the following in the web.config:
<httpRuntime targetFramework="4.6.1" />
Still investigating the side effects to see if I can apply that safely or need to change the code.

I've finally figured it out by creating a small app and trying to reproduce it. As Dmitry and Paulo have pointed out, it should work. However, it should work for any new project and in my case the project is 10 years old and has lots of legacy configurations.
TL;DR: The async/await keywords do not work very well (the HttpContext.Current will be null after calling await) if this setting is not present in the web.config:
<httpRuntime targetFramework="4.6.1" />
That is a shortcut for a bunch of settings, including this one (which is the one I care here):
<configuration>
<appSettings>
<add key="aspnet:UseTaskFriendlySynchronizationContext" value="true" />
</appSettings>
</configuration>
Everything is explained in detail here: https://blogs.msdn.microsoft.com/webdev/2012/11/19/all-about-httpruntime-targetframework/
For reference, it says:
<add key="aspnet:UseTaskFriendlySynchronizationContext" value="true" />
Enables the new await-friendly asynchronous pipeline that was
introduced in 4.5. Many of our synchronization primitives in earlier
versions of ASP.NET had bad behaviors, such as taking locks on public
objects or violating API contracts. In fact, ASP.NET 4’s
implementation of SynchronizationContext.Post is a blocking
synchronous call! The new asynchronous pipeline strives to be more
efficient while also following the expected contracts for its APIs.
The new pipeline also performs a small amount of error checking on
behalf of the developer, such as detecting unanticipated calls to
async void methods.
Certain features like WebSockets require that this switch be set.
Importantly, the behavior of async / await is undefined in ASP.NET
unless this switch has been set. (Remember: setting <httpRuntime
targetFramework="4.5" /> is also sufficient.)
If that settings is not present at all, then version 4.0 is assumed and it works in 'quirks'-mode:
If there is no <httpRuntime targetFramework> attribute present in Web.config, we assume that the application wanted 4.0 quirks behavior.

For retrieving files in ASP.NET Core try using IFileProvider instead of HttpContext - see File Providers in ASP.NET Core documentation for more details about configuring and injecting it via DI.
If that is the POST controller action to upload multiple files and receive other data - you can do it this way. Below for demo purposes I use View but data can just go from anywhere as API POST request.
View
#model MyNamespace.Models.UploadModel
<form asp-controller="MyController" asp-action="Upload" enctype="multipart/form-data" method="post">
<input asp-for="OtherProperty">
<input name="Files" multiple type="file">
<button type="submit" class="btn btn-success">Upload</button>
</form>
Model - note that files are passed as IFormFile objects
public class UploadModel
{
public List<IFormFile> Files { get; set; }
public string OtherProperty { get; set; }
}
Controller
[HttpGet]
public IActionResult Upload()
{
return View(new UploadModel());
}
[HttpPost]
public async Task<IActionResult> Index(UploadModel model)
{
var otherProperty = model.OtherProperty;
var files = new Dictionary<string, string>();
foreach (IFormFile file in model.Files)
{
using (var reader = new StreamReader(file.OpenReadStream()))
{
string content = await reader.ReadToEndAsync();
files.Add(file.Name, content);
// Available file properties:
// file.FileName
// file.ContentDisposition
// file.ContentType
// file.Headers
// file.Length
// file.Name
// You can copy file to other stream if needed:
// file.CopyTo(new MemoryStream()...);
}
}
}

Related

How can I manage robots.txt in Azure app service with a staging slot

I have an ASP.NET MVC app running in an Azure app service with one staging slot, and a build and release pipeline in VSTS.
I want the production instance to have Allow / in robots.txt and Disallow / in the staging slot at all times.
Currently we are changing robots.txt manually every time we do a swap but this is error prone
How can I automate this process?
To solve this problem I did consider creating the robots.txt file dynamically based on app settings set in the Azure portal (set to stay with the slot), however this won't work since after the swap happens prod will have the staging Disallow rule.
Can anyone advise the best way to manage this?
Robots are mainly used by search engines to crawl and check pages on the public websites. Staging and other deployment slots are not public (and should not be public — unless you have a good reason for that), and thus it doesn't make much sense to configure and manage it. Secondly, in most cases I would recommend to redirect any public request to your production slot and keep staging offline and active for internal use cases only. This would also help you to manage the analytics and logs coming from the public only, and not being polluted with internal and deployment slots stuff.
Anyways, if you are still inclined to do this, then there is one way that you can manage this. Write your own routing to control the robots file, and then render a content-type: text/plain page, which would be dynamic based on whether it is a staging or production request. Something like this,
// Create the robots.txt file dynamically, by controlling the URL handler
[Route("robots.txt")]
public ContentResult DynamicRobotsFile()
{
StringBuilder content = new StringBuilder();
content.AppendLine("user-agent: *");
// Check the condition by URL or Environment variable
if(allow) {
content.AppendLine("Allow: /");
else {
content.AppendLine("Disallow: /");
}
return this.Content(stringBuilder.ToString(), "text/plain", Encoding.UTF8);
}
This way you can manage how the robots.txt is created and you would be able to control the allow disallow for the robots. You can create a separate controller or an action only in the home controller of your app.
Now that you know how to do, you can setup the environment variables for the production/staging slots to check other requirements.
I use below code and It works for me
[Route("robots.txt")]
public ContentResult DynamicRobotsFile()
{
StringBuilder content = new StringBuilder();
if (System.Configuration.ConfigurationManager.AppSettings["production"] != "true")
{
content.AppendLine("user-agent: *");
content.AppendLine("Disallow: /");
}
return this.Content(content.ToString(), "text/plain", Encoding.UTF8);
}
web.config
<appSettings>
<add key="production" value="false" />
</appSettings>
<system.webServer>
<handlers>
<add name="RobotsTxt" path="robots.txt" verb="GET" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
</handlers>
</system.webServer>
EDITED
I use this version now.
[Route("/robots.txt")]
public ContentResult RobotsTxt()
{
var sb = new StringBuilder().AppendLine("User-Agent: *");
if (_env.IsProduction())
{
sb.AppendLine("Allow: /");
sb.AppendLine("Disallow: /admin");
}
else
{
sb.AppendLine("Disallow: /");
}
sb.AppendLine(string.Empty);
sb.AppendLine($"Sitemap: {this.Request.Scheme}://{this.Request.Host}/sitemap.xml");
return this.Content(sb.ToString(), "text/plain", Encoding.UTF8);
}
and I use IWebHostEnvironment to detect prod or not
public class SeoController : Controller
{
private readonly IWebHostEnvironment _env;
public SeoController(IWebHostEnvironment env)
{
_env = env;
}
}

Hangfire.io Dashboard mapped to IIS Virtual Directory

I'm having trouble getting the Hangfire (1.5.8) dashboard to work inside of an IIS Virtual Directoy. Everything works beautifully in my dev environment where my application is simply mapped to the root of localhost. Our beta server, on the other hand, uses Virtual Directories to separate apps and app pools.
It's an ASP.Net MVC site using Hangfire with an OWIN Startup class. It gets deployed to http://beta-server/app-name/. When I attempt to access either http://beta-server/app-name/hangfire or http//beta-server/hangfire I get a 404 from IIS.
For the purposes of troubleshooting this, my IAuthenticationFilter simply returns true.
Here is my Startup.cs, pretty basic:
public class Startup
{
public void Configuration(IAppBuilder app)
{
// For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=316888
GlobalConfiguration.Configuration
.UseSqlServerStorage(new DetectsEnvironment().GetEnvironment());
app.UseHangfireDashboard("/hangfire", new DashboardOptions
{
AuthorizationFilters = new[] {new AuthenticationFilter()}
});
app.UseHangfireServer();
}
}
Does anyone have a working implementation that gets deployed to a Virtual Directory? Are there any OWIN middleware admin/management tools I can use to dig into what URL is getting registered within IIS?
I ended up fixing this simply by adding the HTTPHandler to the section in web.config.
<system.webServer>
<handlers>
<add name="hangfireDashboard" path="hangfire" type="System.Web.DefaultHttpHandler" verb="*" />
</handlers>
</system.webServer>
I had a similar issue in ASP.NET Core 2.0 and it required proper authorization setup (I use a middleware to protect the route, so I did not rely on authorization in my example):
app.UseHangfireDashboard("/hangfire", new DashboardOptions
{
Authorization = new [] {new HangfireDashboardAuthorizationFilter()}
});
/// <summary>
/// authorization required when deployed
/// </summary>
public class HangfireDashboardAuthorizationFilter : IDashboardAuthorizationFilter
{
///<inheritdoc/>
public bool Authorize(DashboardContext context)
{
// var httpContext = context.GetHttpContext();
// Allow all authenticated users to see the Dashboard (potentially dangerous).
// handled through middleware
return true; // httpContext.User.Identity.IsAuthenticated;
}
}
There is not need to change anything in web.config.
For more information check Hangfire documentation about this topic.
I had the exact same problem. In my case, this was because of bad configuration - the Startup class was not called. So try to add the following to your config file:
<add key="owin:appStartup" value="YourProject.YourNamespace.Startup, YourProject" />
<add key="owin:AutomaticAppStartup" value="true" />
Hope this helps.
Martin

ASP.NET MVC5 Customised Inbound Routing

I'm "playing" around with custom inbound URL routing and have came across a problem.
When I pass my custom route a URL to examine, that ends in *.+, my class is not fired when i submit the request.
An example URL would be "~/old/windows.html"
When I step through this in the debugger, my RouteBase implementation doesn't fire. If i edit the url that i pass to the constructor of my route to try to match against "~/old/windows", my implemetation is fired as expected.
Again, If i change the url ro examine to "~/old/windows." the problem reoccurs.
My Route Implementation is below :-
public class LegacyRoute : RouteBase
{
private string[] _urls;
public LegacyRoute(string[] targetUrls)
{
_urls = targetUrls;
}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
RouteData result = null;
string requestedURL = httpContext.Request.AppRelativeCurrentExecutionFilePath;
if (_urls.Contains(requestedURL, StringComparer.OrdinalIgnoreCase))
{
result = new RouteData(this, new MvcRouteHandler());
result.Values.Add("controller", "Legacy");
result.Values.Add("action","GetLegacyURL");
result.Values.Add("legacyURL", requestedURL);
}
return result;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
return null;
}
}
In the RoutesConfig file I have registered my route like so :-
routes.MapMvcAttributeRoutes();
routes.Add(new LegacyRoute(new[]{"~/articles/windows.html","~/old/.Net_1.0_Class_Library"}));
Can anyone point out why there is a problem?
By default, the .html extension is not handled by .NET, it is handled by IIS directly. You can override by adding the following section in Web.config under <system.webServer> -
<handlers>
<add name="HtmlFileHandler" path="*.html" verb="GET" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
</handlers>
As pointed out here. The above will route EVERY .html file request to .NET, you might want to be more specific by providing a more complete path if you don't want your routing to handle every .html file.
I've found the problem, and I'm sure this will help out a lot of fellow developers.
The problem is with IIS Express that is running via Visual Studio.
There is a module configured in the applicationhost.config called :-
UrlRoutingModule-4.0
This is how it looks in file :-
<add name="UrlRoutingModule-4.0" type="System.Web.Routing.UrlRoutingModule" preCondition="managedHandler,runtimeVersionv4.0" />
You need to set the preCondition Parameter to "".
To do this :-
Run you app via Visual Studio
Right click on IIS Express in your system tray, select "Show All Applications"
Click on the project you wish to edit, then click the config URL.
Open the file with Visual Studio, Locate the module and ammend.
Hope this helps anyone else, who ran into a similar problem.

Orchard/MVC WCF Service Url with Area

Bertrand created a blog post to specify how to use IoC in WCF Modules for Orchard.
In 1.1, you can create a SVC file using the new Orchard host factory:
<%# ServiceHost Language="C#" Debug="true"
Service="MyModule.IMyService, MyAssembly"
Factory="Orchard.Wcf.OrchardServiceHostFactory, Orchard.Framework" %>
Then register your service normally as an IDependency but with service and operation contract attributes:
using System.ServiceModel;
namespace MyModule {
[ServiceContract]
public interface IMyService : IDependency {
[OperationContract]
string GetUserEmail(string username);
}
}
My question is that all of Orchard's modules are really area's. So how can you build a route that hits the svc file created in the area/module?
Should you use the full physical path to get to the svc file (tried that and it caused a web.config issue since it was bridging a site and area).
http://localhost/modules/WebServices/MyService.svc
Or do you create a ServiceRoute with WebServiceHostFactory/OrchardServiceHostFactory?
new ServiceRoute("WebServices/MyService", new OrchardServiceHostFactory(), typeof(MyService))
Whatever I try I get a 404 when trying to hit the resource. I was able to get this working using a wcf Application project and setting WCF as a stand alone application, my issues started when trying to bring it into Orchard/MVC.
UPDATE
Thanks for the help Piotr,
This is the steps I took to implement the service.
Routes.cs
new RouteDescriptor { Priority = 20,
Route = new ServiceRoute(
"Services",
new WebServiceHostFactory(),
typeof(MyService)) }
If I use OrchardServiceHostFactory() instead of WebServiceHostFactory() I get the following error.
Operation is not valid due to the current state of the object.
Orchard Root Web.Config
<system.serviceModel>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true"/>
<standardEndpoints>
<webHttpEndpoint>
<!--
Configure the WCF REST service base address via the global.asax.cs file and the default endpoint
via the attributes on the <standardEndpoint> element below
-->
<standardEndpoint name="" helpEnabled="true" automaticFormatSelectionEnabled="true"/>
</webHttpEndpoint>
</standardEndpoints>
</system.serviceModel>
MyService
[ServiceContract]
public interface IMyService : IDependency
{
[OperationContract]
string GetTest();
}
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
class MyService : IMyService
{
public string GetTest()
{
return "test";
}
}
I couldn't get the service working by just modifying the module's web.config. I get the following error
ASP.NET routing integration feature requires ASP.NET compatibility.
UPDATE 2
Orchard Root Web.Config
<system.serviceModel>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true" />
<!-- ... -->
</system.serviceModel>
Routes.cs
public IEnumerable<RouteDescriptor> GetRoutes() {
return new[] {
new RouteDescriptor { Priority = 20,
Route = new ServiceRoute(
"Services",
new OrchardServiceHostFactory(),
typeof(IMyService))
}
};
}
This works, the key here is that you must call typeof on the object that is referencing IDependency, WorkContextModule.IsClosingTypeOf cant handle the object that consumes the dependancy, it must take the Interface that it is directly called by.
As you stated, Orchard modules are areas in ASP.NET MVC terms, so the URL you provided is incorrect and should be:
http://localhost/Your.Orchard.Module/WebServices/MyService.svc
Where localhost is the virtual directory under which your app runs and /WebServices is a folder in the root of your module.
You can also create a service route programatically without problem. This article tells how to add new routes in Orchard. You can just assign a ServiceRoute to the Route property of a RouteDescriptor instead of a default MVC route (as shown in docs).
The question about adding ServiceRoute in area-enabled ASP.NET MVC app was asked before, check it out as it may help you out.
Btw - You may also check this SO question about prefixed service routes.
HTH

Response.Write JSONP using JQuery and WCF

UPDATE:
The service cannot be activated because it does not support ASP.NET compatibility. ASP.NET compatibility is enabled for this application. Turn off ASP.NET compatibility mode in the web.config or add the AspNetCompatibilityRequirements attribute to the service type with RequirementsMode setting as 'Allowed' or 'Required'.
when i try to access wcf service i get this error: the reason is HttpContext.Current is null, what should i do in this case? any help?
Object reference not set to an instance of an object.
System.Web.Script.Serialization.JavaScriptSerializer s = new System.Web.Script.Serialization.JavaScriptSerializer();
Person p = new Person() { FirstName = "First name", LastName= "last name" };
string json = s.Serialize(p);
System.Web.HttpContext.Current.Response.Write("jsoncallback" + json);} //error
HttpContext is an ASP.Net construct. If you want to be able to access it in your service, then you need to enable ASP.Net Compatibility for your service.
Either through the web.config:
<system.serviceModel>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true" />
</system.serviceModel>
Or declaratively:
[AspNetCompatibilityRequirements(RequirementsMode=AspNetCompatibilityRequirementsMode.Required)]
public class MyService : IMyService { ... }
If simple response write is the thing for you, then consider using a simple HttpHandler (by implementing the IHttpHandler interface). The Response object is not meant be used in a WCF Service...
If WCF however is the thing for you (maybe the stack of tech it offers is something you need), then consider using the plumming already there to output json:
[ServiceContract]
public interface IService
{
[OperationContract]
[WebGet(ResponseFormat = WebMessageFormat.**Json**)]
String DoStuff();
}

Resources