Asked  7 Months ago    Answers:  5   Viewed   133 times

I'm working on a web service using ASP.NET MVC's new WebAPI that will serve up binary files, mostly .cab and .exe files.

The following controller method seems to work, meaning that it returns a file, but it's setting the content type to application/json:

public HttpResponseMessage<Stream> Post(string version, string environment, string filetype)
{
    var path = @"C:Temptest.exe";
    var stream = new FileStream(path, FileMode.Open);
    return new HttpResponseMessage<Stream>(stream, new MediaTypeHeaderValue("application/octet-stream"));
}

Is there a better way to do this?

 Answers

36

Try using a simple HttpResponseMessage with its Content property set to a StreamContent:

// using System.IO;
// using System.Net.Http;
// using System.Net.Http.Headers;

public HttpResponseMessage Post(string version, string environment,
    string filetype)
{
    var path = @"C:Temptest.exe";
    HttpResponseMessage result = new HttpResponseMessage(HttpStatusCode.OK);
    var stream = new FileStream(path, FileMode.Open, FileAccess.Read);
    result.Content = new StreamContent(stream);
    result.Content.Headers.ContentType = 
        new MediaTypeHeaderValue("application/octet-stream");
    return result;
}

A few things to note about the stream used:

  • You must not call stream.Dispose(), since Web API still needs to be able to access it when it processes the controller method's result to send data back to the client. Therefore, do not use a using (var stream = …) block. Web API will dispose the stream for you.

  • Make sure that the stream has its current position set to 0 (i.e. the beginning of the stream's data). In the above example, this is a given since you've only just opened the file. However, in other scenarios (such as when you first write some binary data to a MemoryStream), make sure to stream.Seek(0, SeekOrigin.Begin); or set stream.Position = 0;

  • With file streams, explicitly specifying FileAccess.Read permission can help prevent access rights issues on web servers; IIS application pool accounts are often given only read / list / execute access rights to the wwwroot.

Tuesday, June 1, 2021
 
SkyNet
answered 7 Months ago
91

You could use BundleTransformer to compile your LESS server side.

It can depend on how you want to serve the file. If you know all the tenants then just add add a bundle url for each tenant application to the bundle config.

 var themeStyles = new CustomStyleBundle("~bundles/theme/tenant").Include("~/Content/theme.less");
 themeStyles.Builder = new ThemeBuilder();
 BundleTable.Bundles.Add(themeStyles);

If you don't and the tenants are flexible as was the case in our situation then add the following controller action for your themes.

    [Route("bundles/theme/{id}")]
    public ContentResult Theme(string id)
    {
        var tenantThemePath = string.Format("~/bundles/theme/{0}", id);

        // Check that bundle has not already been added.
        if (BundleTable.Bundles.All(x => x.Path != tenantThemePath))
        {
            var themeStyles = new CustomStyleBundle(tenantThemePath ).Include("~/Content/theme.less");

            themeStyles.Builder = new ThemeBuilder();

            BundleTable.Bundles.Add(themeStyles);
        }

        var context = new BundleContext(HttpContext, BundleTable.Bundles, institutionPath);

        var response = BundleTable.Bundles.GetBundleFor(tenantThemePath).GenerateBundleResponse(context);

        Response.Cache.SetCacheability(response.Cacheability);

        return Content(response.Content, response.ContentType);
    }

The ThemeBuilder implementation for BundleTransformer

public class ThemeBuilder : IBundleBuilder
{
    public string BuildBundleContent(Bundle bundle, BundleContext context, IEnumerable<BundleFile> files)
    {
        var lessTranslator = bundle.Transforms.OfType<StyleTransformer>()
            .Where(x => x != null)
            .Select(x => x.Translators.OfType<LessTranslator>().FirstOrDefault())
            .FirstOrDefault();

        if (lessTranslator == null)
        {
            return string.Empty;
        }

        lessTranslator.GlobalVariables = GetThemeVariables();

        return string.Empty;
    }

    private string GetThemeVariables()
    {
        // Simplified for brevity
        // This will be translated to less variables by the BundleTransformer
        // themeColour should correspond to a variable name in your less file.  
        return string.Format("themeColour={0}", themeColour);
    }

}

You will need away of getting the theme colours out we stashed those variables in HttpContext stores so that we could pull them out using an extension method in the GetThemeVariables method.

I hope this helps.

UPDATE I've expanded on my original answer and created a more reusable way of including themes.

Demo site here: http://bundletransformer-theme-builder.azurewebsites.net/

GitHub repo here: https://github.com/benembery/bundle-transformer-theme-builder

Friday, August 6, 2021
 
CAMason
answered 4 Months ago
36

Found the problem.

ApiControllers class names need to be suffixed with "Controller", and mine was not. Changing it to SampleSlashBaseController solved the problem.

NOTE: It is possible to suffix it with "Service" as I did, but then you have to implement a custom IHttpControllerSelector like described here: http://netmvc.blogspot.no/2012/06/aspnet-mvc-4-webapi-support-areas-in.html

Friday, August 6, 2021
 
Matt
answered 4 Months ago
74

For resource based authorization, I'd suggest to use claim based identity and embed user id as a claim. Write an extension method to read the claim from identity. So the sample code will look like:

public Resource GetResource(int id)
{
     var resource = resourceRepository.Find(id);
    if (resource.UserId != User.Identity.GetUserId())
    {
        throw new HttpResponseException(HttpStatusCode.Unauthorized);
    }

    return resource;
}

If you want to simplify the code further more, you may write a UserRepository which knows user data and resource repository to centralize the code. The code will look like:

public Resource GetResource(int id)
{
    return User.Identity.GetUserRepository().FindResource(id);
}

For role based authorization, AuthorizeAttribute will be the best place to handle it and you'd better use separate action or controller for that.

[Authorize(Roles = "admin")]
public Resource GetResourceByAdmin(int id)
{
    return resourceRepository.Find(id);
}

[Edit] If OP do want to use one single action to handle different types of users, I personally prefer to use a user repository factory. The action code will be:

public Resource GetResource(int id)
{
    return User.GetUserRepository().FindResource(id);
}

The extension method will be:

public static IUserRepository GetUserRepository(this IPrincipal principal)
{
    var resourceRepository = new ResourceRepository();
    bool isAdmin = principal.IsInRole("Admin");
    if (isAdmin)
    {
        return new AdminRespository(resourceRepository);
    }
    else
    {
       return new UserRepository(principal.Identity, resourceRepository);
    }
}

The reason that I don't want to use AuthorizeAttribute to do per resource authentication is that different resources may have different code to check ownership, it's hard to centralized the code in one attribute and it requires extra DB operations which is not really necessary. Another concern is that AuthroizeAttribute happens before parameter binding, so you need to make sure the parameter of the action is coming from route data. Otherwise, for example, from a post body, you won't be able to get the parameter value.

Tuesday, August 10, 2021
 
timothy5216
answered 4 Months ago
28

Is there a Web API controller method equivalent to the MVC controller method RedirectToAction?

You could set the Location header:

public HttpResponseMessage Get()
{
    var response = Request.CreateResponse(HttpStatusCode.Found);
    response.Headers.Location = new Uri("http://www.google.com");
    return response;
}
Wednesday, August 11, 2021
 
Gilko
answered 4 Months ago
Only authorized users can answer the question. Please sign in first, or register a free account.
Not the answer you're looking for? Browse other questions tagged :  
Share