Giter Site home page Giter Site logo

cartercommunity / carter Goto Github PK

View Code? Open in Web Editor NEW
2.0K 54.0 162.0 23.66 MB

Carter is framework that is a thin layer of extension methods and functionality over ASP.NET Core allowing code to be more explicit and most importantly more enjoyable.

License: MIT License

C# 96.92% Shell 0.95% PowerShell 2.14%
nancy asp-net-core csharp asp-net mit-license dotnet-core middleware

carter's Introduction

Carter

Carter is a framework that is a thin layer of extension methods and functionality over ASP.NET Core allowing the code to be more explicit and most importantly more enjoyable.

For a better understanding, take a good look at the samples inside this repo. The samples demonstrate usages of elegant extensions around common ASP.NET Core types as shown below.

Other extensions include:

  • Validate<T> - FluentValidation extensions to validate incoming HTTP requests which is not available with ASP.NET Core Minimal APIs.
  • BindFile/BindFiles/BindFileAndSave/BindFilesAndSave - Allows you to easily get access to a file/files that has been uploaded. Alternatively you can call BindFilesAndSave and this will save it to a path you specify.
  • Routes to use in common ASP.NET Core middleware e.g., app.UseExceptionHandler("/errorhandler");.
  • IResponseNegotiators allow you to define how the response should look on a certain Accept header(content negotiation). Handling JSON is built in the default response but implementing an interface allows the user to choose how they want to represent resources.
  • All interface implementations for Carter components are registered into ASP.NET Core DI automatically. Implement the interface and off you go.

Releases

  • Latest NuGet Release NuGet Version
  • Latest NuGet Pre-Release NuGet Version
  • Lateset CI Release feedz.io
  • Build Status Build Status

Join our Slack Channel

Routing

Carter uses IEndpointRouteBuilder routing and all the extensions IEndpointConventionBuilder offers also known as Minimal APIs. For example you can define a route with authorization required like so:

app.MapGet("/", () => "There's no place like 127.0.0.1").RequireAuthorization();

Where does the name "Carter" come from?

I have been a huge fan of, and core contributor to Nancy, the best .NET web framework, for many years, and the name "Nancy" came about due to it being inspired from Sinatra the Ruby web framework. Frank Sinatra had a daughter called Nancy and so that's where it came from.

I was also trying to think of a derivative name, and I had recently listened to the song Empire State of Mind where Jay-Z declares he is the new Sinatra. His real name is Shaun Carter so I took Carter and here we are!

CI Builds

If you'd like to try the latest builds from the master branch add https://f.feedz.io/carter/carter/nuget/index.json to your NuGet.config and pick up the latest and greatest version of Carter.

Getting Started

You can get started using either the template or by adding the package manually to a new or existing application.

Template

https://www.nuget.org/packages/CarterTemplate/

  1. Install the template - dotnet new install CarterTemplate

  2. Create a new application using template - dotnet new carter -n MyCarterApp -o MyCarterApp

  3. Go into the new directory created for the application cd MyCarterApp

  4. Run the application - dotnet run

Package

https://www.nuget.org/packages/Carter

  1. Create a new empty ASP.NET Core application - dotnet new web -n MyCarterApp

  2. Change into the new project location - cd ./MyCarterApp

  3. Add Carter package - dotnet add package carter

  4. Modify your Program.cs to use Carter

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCarter();

var app = builder.Build();

app.MapCarter();
app.Run();
  1. Create a new Module
    public class HomeModule : ICarterModule
    {
        public void AddRoutes(IEndpointRouteBuilder app)
        {
            app.MapGet("/", () => "Hello from Carter!");
        }
    }
  1. Run the application - dotnet run

Sample

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IActorProvider, ActorProvider>();
builder.Services.AddCarter();

var app = builder.Build();

app.MapCarter();
app.Run();

public class HomeModule : ICarterModule
{
    public void AddRoutes(IEndpointRouteBuilder app)
    {
        app.MapGet("/", () => "Hello from Carter!");
        app.MapGet("/qs", (HttpRequest req) =>
        {
            var ids = req.Query.AsMultiple<int>("ids");
            return $"It's {string.Join(",", ids)}";
        });
        app.MapGet("/conneg", (HttpResponse res) => res.Negotiate(new { Name = "Dave" }));
        app.MapPost("/validation", HandlePost);
    }

    private IResult HandlePost(HttpContext ctx, Person person, IDatabase database)
    {
        var result = ctx.Request.Validate(person);

        if (!result.IsValid)
        {
            return Results.UnprocessableEntity(result.GetFormattedErrors());
        }

        var id = database.StorePerson(person);

        ctx.Response.Headers.Location = $"/{id}";
        return Results.StatusCode(201);
    }
}

public record Person(string Name);

public interface IDatabase
{
    int StorePerson(Person person);
}

public class Database : IDatabase
{
    public int StorePerson(Person person)
    {
        //db stuff
    }
}

More samples

Configuration

As mentioned earlier Carter will scan for implementations in your app and register them for DI. However, if you want a more controlled app, Carter comes with a CarterConfigurator that allows you to register modules, validators and response negotiators manually.

Carter will use a response negotiator based on System.Text.Json, though it provides for custom implementations via the IResponseNegotiator interface. To use your own implementation of IResponseNegotiator (say, CustomResponseNegotiator), add the following line to the initial Carter configuration, in this case as part of Program.cs:

    builder.Services.AddCarter(configurator: c =>
    {
        c.WithResponseNegotiator<CustomResponseNegotiator>();
        c.WithModule<MyModule>();
        c.WithValidator<TestModelValidator>()
    });

Here again, Carter already ships with a response negotiator using Newtonsoft.Json, so you can wire up the Newtonsoft implementation with the following line:

    builder.Services.AddCarter(configurator: c =>
    {
        c.WithResponseNegotiator<NewtonsoftJsonResponseNegotiator>();
    });

carter's People

Contributors

azure-pipelines[bot] avatar bugagain avatar dreamwalker666 avatar ggmueller avatar gkinsman avatar grandchamp avatar henrikrxn avatar j0nnyhughes avatar jaxelr avatar jchannon avatar joestead avatar josephwoodward avatar jussimattila avatar khalidabuhakmeh avatar klym1 avatar martincostello avatar mderriey avatar modernist avatar niklashansen avatar pauliusnorkus avatar pdwetz avatar petedishman avatar poke avatar ragingkore avatar scardetto avatar sebastienros avatar simoncropp avatar sphiecoh avatar tomzo avatar williamhbell avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

carter's Issues

CarterModule.Get() automatically registers route for both GET and HEAD verbs

I noticed this when trying to debug why I couldn't register a different handler for HEAD when GET was already registered for the same route. It's not something that I can't work around, of course, but I'm still wondering: is there a good reason why CarterModule.Get() would want to implicitly cover HEAD as well?

Remove Travis

Remove travis.yml integration as we now have appveyor

Use C#7 tuples in BotwinModule

It currently uses Tuple.Create and therefore the extension class refers to the route handlers as Item3. We can tidy that up!

Binding from Form data can throw if there is a mismatch in fields.

When binding from a FormCollection you can get a null reference exception if the fields do not match up properly.

Given the type Document:

public class Document
{
   public string Type { get; set; }
}

and the form data:

name="type"[...]

you will get a null reference exception because of the erroneous quotes.

Instead of throwing, it should not bind that data - and should fail validation if it is present

Add support for camelCasing in DefaultJsonResponseNegotiator

The default Json.Net casing strategy is PascalCasing, which is counter to what a lot of json-based API consumers expect. It'd be nice to easily switch to camelCasing instead.

https://www.newtonsoft.com/json/help/html/NamingStrategyCamelCase.htm

I have a local implementation within my project, but I could pull it out into a Botwin pull request if desired. It's pretty simple and just accepts IContractResolver in the contructor of the negotiator, which I then use in the SerializeObject call.

Profile Carter

After the lovely @benaadams added Carter to the TechEmpower perf tests https://github.com/TechEmpower/FrameworkBenchmarks MVC was fractionally faster on plain text.

After reading the results at 9am they sent me down a nasty path of drug and alcohol abuse. I had an existential crisis and questioned my sanity. I didn't listen to friends and ignored their advice. Luckily I passed out the other side a better man. By 9.30am I was back ready to take on the beast.

So we need to run some profiling to see if we can see any hot spots in the code because in theory Carter should be faster, it's just a wrapper over RoutingMiddleware.

LET'S DO THIS!

Add XML docs to public methods

  • BindExtensions - done via #46
  • ValidationExtensions - done via #47
  • QueryStringExtensions - done via #47
  • RouteDataExtensions - done via #49
  • StreamExtensions - done via #48
  • ResponseExtensions - done via #49
  • BotwinExtensions - done via #50
  • BotwinModule - done via #55
  • BotwinOptions - done via #56
  • IValidatorLocator - done via #57
  • IStatusCodeHandler - done via #58
  • IResponseNegotiator - done via #49

Sample code does not work

I am hitting a rather strange issue :

 var assemblyProvider = provider.GetService<IAssemblyProvider>();
   if (assemblyProvider == null)
            {
                services.AddSingleton<IAssemblyProvider, AssemblyProvider>();
               
            }
 assemblyProvider = provider.GetService<IAssemblyProvider>();

returns a null always. I have tried running the sample and blows up with a NullReferenceException in services.AddBotwin();
It only work if you do this :

  services.AddSingleton<IAssemblyProvider, AssemblyProvider>();
  services.AddBotwin();

I will update the sample and shoot a PR

Accept RouteMetaData instance

Thanks for the OpenAPI work, it would be nice if the route could accept an instance of RouteMetaData as an alternative to a type name. I'm not too concerned about how that instance would get passed but I'd like to avoid the creation of new classes for each route just to document it. Kind of defeats the purpose of using Carter in the first place in my opinion.

Route clash with parametrized routes -> 405 Method not allowed

Consider the following:

public class ThingsModule : CarterModule
{
  public ThingsModule() : base("things")
  {
    Get("/", GetAllThings);
    Get("{type}", GetThing);
    Post("horse", PostHorse); //will not work. GET {type} will be selected, and return 405 Method not allowed
  }

  private Task GetAllThings(HttpRequest req, HttpResponse res, RouteData routeData)
  {
    return res.WriteAsync("Horse, anvil, cucumber");
  }

  private Task PostHorse(HttpRequest req, HttpResponse res, RouteData routeData)
  {
    return res.WriteAsync("Horse saved!");
  }

  private Task GetThing(HttpRequest req, HttpResponse res, RouteData routeData)
  {
    var thing = routeData.As<string>("type");
    return res.WriteAsync($"You asked for {WebUtility.HtmlEncode(thing)}");
  }
}

POST /horse will return 405 Method not allowed, because the GET {type} route is selected.
Switching the order of the two routes will result in GET /horse not working, because the POST /horse route returns 405.

It would be nice if Carter could find the first valid route (both correct method and route pattern match) to execute.

BindAndSave file to add optional fileName argument

This is so the filename can be saved as a user requested filename rather than use the filename as sent in the request

https://github.com/jchannon/Botwin/blob/master/src/ModelBinding/BindExtensions.cs#L99

public static async Task BindAndSaveFile(this HttpRequest request, string saveLocation, string fileName = "")
        {
            var file = await request.BindFile();
            fileName = string.IsNullOrWhiteSpace(fileName) ? file.FileName : fileName;
            using (var fileToSave = File.Create(Path.Combine(saveLocation, fileName)))
            {
                await file.CopyToAsync(fileToSave);
            }
        }

As<T> throws InvalidCastException on Nullable types

When using As<T> and AsMultiple<T> extension methods on IQueryCollection or RouteData with Nullable data-types as T (e.g. int?), an InvalidCastException is thrown.

This is due the framework unable to cast int to int? (same for other Nullable<T>) directly when using Convert.ChangeType.

routeData.As<Guid>("id") thrown Invalid cast exception

this.Get("/stock/{id:guid}", async (req, res, routeData) =>
{
     var myid = routeData.As<Guid>("id");  //thrown Invalid cast exception
     await res.Negotiate(myid);
});

There has been an error
System.InvalidCastException: Invalid cast from 'System.String' to 'System.Guid'.
at System.Convert.DefaultToType(IConvertible value, Type targetType, IFormatProvider provider)
at coster.api.ExRouteDataExtensions.AsGuid(RouteData routeData, String key) in
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at Carter.CarterExtensions.<>c__DisplayClass1_0.<b__0>d.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at Microsoft.AspNetCore.Builder.RouterMiddleware.d__4.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.d__6.MoveNext()

Generating URIs for Routes

Ability to generate a URI for another Botwin route? Similar to Nancy.Linker. This would likely need to give name to the route so you can resolve it.

My primary usage for this is hypermedia and returning URIs for relevant routes.

Obtaining Request CancellationToken

I'm not sure how to obtain a CancellationToken in my request handler. For example:

        public ActionsModule(ILinnApiActions linnApiProxy)
        {
            this.Post(
                "/ifttt/v1/actions/turn_off_all_devices",
                async (req, res, routeData) =>
                    {
                        var id = await linnApiProxy.TurnOfAllDevices(req.GetAccessToken(), CancellationToken.None);
                        var resource = new[] { new ActionResponseResource { Id = id } };
                        await res.AsJson(new DataResource<ActionResponseResource[]>(resource));
                    });
        }

I'm having to pass in a CancellationToken.None to my proxy rather than one attached to the request.

Ideally, I'd like to be able to use a CancellationToken handed in along with the req, res and routeData, is there any way I can get hold of the token?

Unable to respond with a Stream

I'm unable to work out how to reply with a streamed response using Botwin. Reading up on ASP.NET Core it looks liked I'd be able to do the following:

[HttpGet]
public FileStreamResult GetTest()
{
  var stream = new MemoryStream(Encoding.ASCII.GetBytes("Hello World"));
  return new FileStreamResult(stream, new MediaTypeHeaderValue("text/plain"))
  {
    FileDownloadName = "test.txt"
  };
}

(https://stackoverflow.com/questions/42771409/how-to-stream-with-asp-net-core#42772150)

However, I can't work out how I can do this using the HttpResponse I have from the BotwinModule handler?

Here's a snippet of my module, but it doesn't return any data:

var models = this.journalReader.Read(start, end, type, ct);

var fileStream =
    new FileStreamResult(CreateExportStream(models, requestLog, ct), "application/csv")
        {
            FileDownloadName
                = $"journal-{type}.csv"
        };

res.Body = fileStream.FileStream;

Use the ILogger stuff to add debug level tracing

As we're using the IServiceProvider provided by AspNetCore, we should hook up some of the other features, including enabling logging.

It's a pain to try and diagnose issues with things without any logging, we should implement trace/debug calls around the code base that can be configured with the ILoggerFactory

Test dotnet new botwin

Run

dotnet new -i BotwinTemplate

dotnet new botwin -n MyBotwinApp

Currently seeing object reference error but others have reported all is ok.

Route Maps

When Botwin loads it has to construct all of the BotwinModules, which in turn has to construct all of its dependencies. I assume it does this so it can determine all the relevant routes so it knows which Module to construct when an associated request comes in.

This can be problematic if you have (like I often do) a lot of BotwinModules. Each module generally only defines one or two routes. This can slow down startup because of DI basically.

I've been thinking if it would make sense to have some sort of interface that Botwin could look for at startup/load to get routes and mappings instead of constructing all Modules.

https://gist.github.com/dcomartin/81f09f24aeea004b831d96b0d49c3d90

Thoughts?

Register Validators as scoped instances rather than singleton

I had a setup a validation as below wherein RoleManager via constructor injection is a scoped instance and this resulted in an error Cannot consume scoped service in Singleton (validators are registered as Singleton within CarterExtensions.cs).

public class CommandValidator : AbstractValidator<Command>
{
    public CommandValidator(RoleManager<UserRole> roleManager)
    {
        RuleFor(c => c.Name).CustomAsync(async (name, context, cancel) =>
        {
            if (string.IsNullOrEmpty(name))
            {
                context.AddFailure("Role name is required");
            }
            else
            {
                var role = await roleManager.FindByNameAsync(name);

                if (role != null)
                {
                    context.AddFailure("Role name already exists");
                }
            }
        });
    }
}

It would be good to register validators as scoped instances.

@jchannon if you could confirm on this change, I would be happy to send my first PR to Carter :-)

Update sample/documentation regarding authentication

It would be useful to update the sample app and documentation, detailing how to use Botwin along with authentication middleware.

Although it's relatively easy to implement such functionality using the Before / After handlers (as in Nancy), it would be helpful to describe the equivalent of the Authorize attribute and perhaps add attributes or extension methods to ease the process.

Renaming to Carter

Thinking of renaming Botwin to Carter.

Consensus seems to be that Botwin, whilst I think is genius, and as a benevolent dictator you cannot disagree with me, is confusing some folks as they think it's a Bot framework for Windows so after too many hours thinking about it I've come up with Carter.

Carter comes from the surname of Jay-Z (Shawn Carter) and in his song Empire State of Mind he sings "I'm the new Sinatra". Sinatra is a web framework which inspired Nancy which heavily inspired Botwin.

Unless there is any real objection like people think this is a library for giving instructions on how to cart their mother in law away ("cart - her") we'll rename this in the coming few weeks.

Let me know what you think

Separate BindAndValidate so Validate can be called on its own

Currently, the BindAndValidate method does exactly what it says, but there is no way to validate the payload without binding it.

This should just be a case of introducing a public Validate method, can calling that method from BindAndValidate

Route Level security

Was thinking of something like this.

Thoughts?

this.Get("/", async ctx => await this.RequiresAuthentication(async innerctx => await innerctx.WriteAsync("authed")));

Base path on module

Currently have to do:

this.Get("/api/runs", this.GetUITestRuns);
this.Post("/api/runs", this.CreateTestRun);

If /api/ was a base path it would clean things up a bit

Question about services lifecycles

I´m trying to use the Botwin library with EF Core. I registered the DbContext using services.AddDbContext (By default sets ServiceLifetime.Scoped) but I noticed that my application uses the same DbContext in each request. Then I noticed that the modules are created once, regardless of whether they are registered at IoC container as transient. So each dependency injected don't respect its configured Lifetime. Is this a desing decision?

Thanks in advance.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.