Just some ideas for making this library better
See original GitHub issueFirst off all, i really like this library, the intent-based handling is really intuitive, i really liked it! š I woud like to help to make this library better and i had a few ideas:
- Use endpoint routing
- Controller pattern
- System.IO.Pipelines support
Endpoint routing
I noticed that since asp net 3.0, there is a endpoint middleware which simplifies routing configuration and looks like this:
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapControllers();
endpoints.MapHealthChecks("/health")
endpoints.MapHub<ChatHub>("/chathub");
endpoints.MapGet("/helloworld", async context => await context.Response.WriteAsync("Hello World!"));
});
and i would propose switching to this, combined with controller pattern, for example:
app.UseEndpoints(endpoints =>
{
// new approach while also using controller pattern:
endpoints.MapTusController<MyTusController>("/myfiles");
endpoints.MapTusController<VideoFileTusController>("/videofiles");
// alternative new approach (more similar to old one):
endpoints.MapTus("/files", httpContext => new DefaultTusConfiguration());
});
//old approach:
app.UseTus(httpContext => new DefaultTusConfiguration())
this allows registering multiple controllers at different endpoints
Controller pattern
The current pattern uses TusDiskStore and events, which works, but things like DI accessing contexts and authorization are more difficult, for example you have to use the OnAuthorizeAsync event instead of doing authorization together with ITusStore logic. I would propose to combine the authorization and request logic and TusDisk Store to something like a āTusControllerā that works similar to the asp net mvc controlers, but is tus-compliant. I wrote some pseudo code of how i imagined it would look like:
public class MyTusController : TusController, ITusCreationController // keep the interface based extension support, just like with TusDiskStore
{
// dependency injection is easily possible
private readonly DbContext _dbContext;
private readonly FileStorageService _fileStorage;
private readonly IAuthorizationService _authService;
public MyTusController(DbContext dbContext, FileStorageService fileStorage, IAuthorizationService authService)
{
_dbContext = dbContext;
_fileStorage = fileStorage;
_authService = authService;
}
public override async Task<ITusActionResult> GetFileAsync(string fileId)
{
// access to httpcontext, tuscontext and user properties
this.HttpContext;
this.TusContext; // this could store file info, metadata etc.
this.User;
// authorize
var authResult = _authService.AuthorizeAsync(this.User, "MyAuthPolicy");
if (!authResult.Success) return TusUnauthorized();
// ... do some logic
ITusFile file = await _fileStorage.GetFile(fileId);
// return result
return TusFile(file);
// other result types may be:
return TusSuccess();
return TusNotFound();
return TusUnauthorized();
return TusNotFound();
// etc...
}
public override async Task<ITusActionResult> CreateFileAsync()
{
// ... do some logic
var filename = this.TusContext.Metadata["filename"];
ITusFile file = _fileStorage.CreateFile(filename);
_dbContext.Add(file);
// in this case we return success with the file id
return TusSuccess(file.Id);
}
//...
// methods that arent on a per-request basis stay the same as in TusDiskStore
public async Task<DateTimeOffset?> GetExpirationAsync(string fileId, CancellationToken _)
{
//...
}
}
System.IO.Pipelines support
this is basically a better alternative to system.io.streams. The problem with system.io.streams is that if i want to write to a memorystream, a copy will be created in the internal buffer, and during reading, a copy of the internal copy will be returned to the consumer. Thats just unneccrssary copying steps and buffer allocaions, and system.io.pipelines solves this by allowing to write and read directly to the raw memory using Memory<byte> GetMemory(int sizeHint)
. The asp net core webserver engine kestrel already swithed to system.io.pipelines and i would reccomend TusDotNet to do the same.
What are your thoughts on this? š
Issue Analytics
- State:
- Created 3 years ago
- Reactions:10
- Comments:18 (4 by maintainers)
Not the OP but Iāll provide some feedback as I recommend this library on a regular basis:
Endpoint routing support would be great!
Maybe itās worth considered how valuable the extra targets are at this point. NET Core 1.x has been End-of-Life for years now. .NET Core 2.1 LTS is EoL as of august this year. At the bare minimum there should be a .NET Standard 2.0 target. David Fowler has been asking library developers to target .NET Standard 2.0 for ages because 1.x brings in a bucketload of nonsense for .NET Core consumers and results in security false-positives for those of us using vulnerability scanners like Snyk.
I also have to question if anyone is going out of their way to integrate tus into .NET Framework projects at this point in the lifecycle.
I think this a simple solution that allows for proper DI without overcooking it. Works for me.
Microsoftās library guidance for Endpoint integration has some thoughts on this topic: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-5.0#guidance-for-library-authors
If the extension simply returns
IEndpointRouteBuilder
, then the consumer can manage configuration of authorization themselves without any additional work being required on your behalf. Correct me if Iām wrong, but Iām pretty sure the modern authorization policy configuration covers just about everything theOnAuthorizeAsync
event can.Hi!
Thank you for your kind words, your engagement and, frankly, your patience with this issue! I wholeheartedly like that you bring new points of view to things that I take for granted. š
There are things in your post that I like and some that I think need some more understanding from my part. Iāll split my answer into each of your headlines. Feel free to correct me if I misunderstood something!
Endpoint routing
This has been discussed earlier and while it would give us some perks, like using variables in our route, it would give us two ways of doing the same thing, which Iām not really a fan of. As background, endpoint routing support hasnāt been added due to that we need to support multiple pipelines (Owin, ASP.NET Core 1.x, 2.x etc) while endpoint routing is only available for ASP.NET Core 3.x+, i.e. we would still need to keep our old code. Iām open for adding this to newer versions of tusdotnet, e.g. netstandard2.1/net5. This in turn would require new build targets etc which is a bit of a pain, but totally doable. As pointed out in #118 itās not super intuitive why it sometimes does not work and since endpoint routing is the new defacto standard I think it would be good to integrate with it.
Controller pattern
I like the idea but I think that the example is oversimplifying things. The tus protocol is not as straight forward as the pseudo code makes it look. Itās not just a matter of āCreateFileAsyncā and return āTusSuccessā and be done with it. There are actually lots of ifs and buts. The current architecture of tusdotnet separates the handling of the protocol (
TusProtocolHandlerIntentBased
) and the storage of file (e.gITusStore
implemented byTusDiskStore
). This is an important separation as it allows for different storing backends that are themselves responsible for how to store files, how ids are generated etc. As an example we might need to generate different types of IDs depending on if the store stores files locally (e.g.TusDiskStore
) or is using some database with primary key constraints etc.Pros:
Cons:
app.UseTus
(subjective I agree)When it comes to DI I think you have several valid points which makes tusdotnet harder than necessary to use:
DefaultTusConfiguration
and use your implementation or use theHttpContext
provided for each event. I can see how this is not intuitive for people when they are used to doing controllers and constructor arguments.I could see a compromise of the two patterns becoming a reality where the events are part of the controllerās methods and could be overriden. Iām not sure if this brings anything new to the table though, except for constructor injection that people are used to? Example:
Possible solutions for these issues
Note that these would ony be available on newer pipelines, e.g. Core 3.1 or .NET 5.
Store implementation
Implement a kind of āwrapperā or āproxyā that one can use to inject the store into services/controllers. Something like this (name stolen from your example):
Authorization
The problem with using the controller pattern and using an
Authorize
attribute is that itās not a 1:1 match between what the client wishes to do and a method in a controller. For example you cannot do the below as there is no way of determining if the client is going to concatenate multiple files or create a new file (maybe we donāt care?). We could limit ourselves to the http method but that would not cover all currently supported extensions of the protocol (and more might be added in the future)One solution that I can think of to help with this issue is to create some kind of builder that can āautomaticallyā create the
OnAuthorize
event handler. Something like this:What do you think? Itās not as easy as just using the
Authorize
attribute but it gives the developer both easier access and full customizability.System.IO.Pipelines support
This has been discussed earlier aswell but I think that your statement is a bit of an oversimplification. Pipelines and Streams are just two different types of abstraction around data and work in different ways. I wouldnāt say that Pipelines is necessarily ābetterā than streams in all cases. It all depends on what you are going to use it for. For instance, streams can be seekable (i.e. you can go back and forth in the data) while pipelines is āone wayā.
Since pipelines can be used as a buffer manager around data (e.g. a http requests) it makes sense that Kestrel uses it as the memory needed for parsing incoming http requests can be optimized in a way so that data no longer needed can be discarded (or rather returned to the pool). In tusdotnet there is really no parsing of incoming data (except for some headers where we already get a full string). All we do is take the data from the http request, load it into a buffer, and when the buffer is full we write the data to disk. We could definitely start using pipelines instead but I think the performance implications of this would be minimal, except maybe for slightly lower memory usage for slightly slower uploads. Since changing the
ITusStore
contract is a breaking change for external stores, Iām hestiant to make that change. That being said, there are probably many places in the tusdotnet code that can be optimized. I know for a fact that we do some unnecessary memory copies inTusDiskStore
. Feel free to provide a POC for changing to pipelines where a performance gain can be observed and Iāll gladly consider the change. Maybe one could change the implementation of the stream provided to the stores so that one can get the ārawā pipereader instead?