With minimalistic API how does MapGet automatically fill parameters from querystring?
With minimalistic API the following is possible:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("api/Students/Grades", StudentsDataContext.GetGradesAsync).RequireAuthorization("Admin");
//...
public class Grade
{
public string? Subject { get; set; }
public int GradePercentage { get; set; } = 0;
}
public class StudentsDataContext
{
public static async Task<List<Grade>> GetGradesAsync(int? studentId, ClaimsPrincipal user, CancellationToken ct))
{
// Gets grades from database...
return new List<Grade>() {
new () { Subject = "Algebra", GradePercentage=95 },
new () { Subject = "English", GradePercentage=90 }
};
}
}
When you call: /api/Students/Grades?studentId=5
magically, studentId is passed to the GetGradesAsync, as well as ClaimsPrinicipal, and CancellationToken.
How does this witchcraft work? Is it possible to learn this power of the darkside?
The link you have provided describes the rules of the parameter binding in Minimal APIs. In the nutshell it is pretty simple - the request handler delegate is analyzed (via some reflection+runtime code generation or source generation at build time I suppose) and actual handler is created were all parameters are processed accordingly and passed to user defined one.
I have not spend much time so the following can be not entirely correct, but the starting point of the investigation how does it actually work would be EndpointRouteBuilderExtensions which leads to RequestDelegateFactory (see AddRouteHandler call which fills List<RouteEntry> _routeEntries which is later processed with CreateRouteEndpointBuilder method, which calls CreateHandlerRequestDelegate) which should contain all the actual "magic".
Related
As part of an integration test, I am using the WebApplication class to simulate the response of an external api call. I have a lot of setup code that ensures that when my code under test makes an http request, it is routed towards my WebApplication class instead of towards the internet.
To illustrate with an example: the following code snippet is setup such that I can control the response to any request made to https:api.example.com/products:
public class Product
{
public int Id { get; set; }
public string? Name { get; set; }
}
private void ConfigureProductsEndpoint(WebApplication testWebApi)
{
var products = new []
{
new Product { Id = 1, Name = "A Product" },
new Product { Id = 2, Name = "Another Product" }
};
testWebApi.MapGet("/products", () =>
{
return Results.Content(JsonSerializer.Serialize(products));
});
}
This works well, I can put a breakpoint on the return statement and it gets hit every time the endpoint is called. Http calls to the endpoint return the response with the 2 products as expected.
I can further customise the setup to take into account any query string parameters that have been used in the http request like so:
private void ConfigureProductsEndpoint(WebApplication testWebApi)
{
testWebApi.MapGet("/products", (int page, int size) =>
{
var products = new[]
{
new Product { Id = 1, Name = $"{page}" },
new Product { Id = 2, Name = $"{size}" }
};
return Results.Content(JsonSerializer.Serialize(products));
});
}
This also works in that when my code makes api calls to
https:api.example.com/products?page=2&size=10
the response contains products with name "2" and "10" as expected
What I'm actually trying to do is test requests to an api that follows the filtering syntax defined in the Microsoft REST API guidelines, so the endpoint I'm requesting actually contains query parameters that like this:
https:api.example.com/products?$filter=something
The C# language specification does not allow for variable names (or anything else for that matter) to contain the dollar symbol so that setup code like this does not compile
private void ConfigureProductsEndpoint(WebApplication testWebApi)
{
testWebApi.MapGet("/products", (string $filter) =>
{
var products = new[] { new Product { Id = 1, Name = "A Product" } };
return Results.Content(JsonSerializer.Serialize(products));
});
}
Dropping the dollar symbol, unsurprisingly, results in the WebApplication instance returning a 400 response as it has only been setup for a request without the dollar symbol.
I'm not aware of any way I can decorate the lambda expression's input parameters to add a dollar symbol at run time (like you could with the [JsonProperty] attribute). I have tried changing the type in the lambda expression. This also doesn't work as this is obviously designed to bind to the query parameters' field names, not value type.
The MS documentation page I have linked at the start of my question is light on example usages of this functionality, as is the rest of the internet
I am working on an Azure Mobile Apps project. Where I have to define a Table Controller with that can accept two parameters and give a list of values. I have a DataObject for ProductItem, which is
public class ProductItem : EntityData
{
public string Name { get; set; }
public string Details { get; set; }
public double Price { get; set; }
public string Image { get; set; }
public Merchant Merchant { get; set; }
}
I need to get a specific Product item, filter by its Price and Merchant. Already in the ProductItemContoller, I have scaffolded
// GET tables/ProductItem
public IQueryable<ProductItem> GetAllProductItems()
{
return Query();
}
// GET tables/ProductItem/48D68C86-6EA6-4C25-AA33-223FC9A27959
public SingleResult<ProductItem> GetProductItem(string id)
{
return Lookup(id);
}
by looking at existing examples. But in examples, we have not called any of the given methods from Client. Rather, IEnumerable<ProductItem> items = await productTable.ToEnumerableAsync(); was called.
My question is why can't we call GetAllProductItems() which was already defined in the controller to the client. If we can call, how to do it.
And also, I need to have a controller method, I need to have a GetAllProductByMerchat(string merchantId). How can I make this possible.
The Table controllers are called automatically by the client SDKs on your behalf, allowing you to work with LINQ queries on the client. You can use something like:
var items = productTable.Where(p => p.Price < 100).ToListAsync();
This gets translated into an OData query across the wire, then translated back into a LINQ query on the server, where it then gets translated into SQL and executed on the SQL Azure instance.
For more information, see chapter 3 of http://aka.ms/zumobook
Did you mean this?
// Server method:
[HttpGet]
[Route("GetAllProductItems")]
public IQueryable<ProductItem> GetAllProductItems()
{
return Query();
}
// Client call
var result = await MobileService.InvokeApiAsync<IQueryable<ProductItem>>("ProductItem/GetAllProductItems", HttpMethod.Get, null);
Remember to add these attribute before the ProductItemController:
[MobileAppController]
[RoutePrefix("api/ProductItem")]
You can do the same thing to your GetAllProductByMerchat(string merchantId) method.
I currently have an C# WebAPI that uses a version of OData that we wrote. We want to start using Microsoft's OData4 which can do more then our custom implementation.
Creating a controller that extends the ODataController I can create a controller that automatically queries based on the query string. (Shown below)
The problem is that it returns the results of the query when I want it to return the Result object which includes additional data. When I set the return type to Result though it will no longer apply the query string.
How can I use the automatic queryable implementation and still return my own object? I've tried making a public method that returns the correct object and calls a private method returning the queryable but it doesn't filter the queryable correctly.
Am I on the right track, or are there other options?
public class CallController : ODataController
{
[EnableQuery]
public IQueryable<Call> GetCall()
{
var list = new List<Call>
{
new Call
{
Id = 4
},
new Call
{
Id = 9
},
new Call
{
Id = 1
}
};
return list.AsQueryable();
}
}
public class Call
{
public int Id { get; set; }
}
public class Result
{
public Call[] Calls { get; set; }
public string NewToken { get; set; }
public string Warning { get; set; }
}
Use ODataQuertOptions instead of [EnableQuery] attribute. Check https://learn.microsoft.com/en-us/aspnet/web-api/overview/odata-support-in-aspnet-web-api/supporting-odata-query-options#invoking-query-options-directly
You would need to intercept the response with an action filter attribute in the onactionexecuted and convert the value to whatever you want. It wouldn't be pretty since it wouldn't be clear what the method was truly returning. But I don't see any other option with odata as the result must be iquerable.
Under the hood, the EnableQuery attribute is executing the action it decorates to get an IQueryable<T>, converting the odata query into something that can be applied to the IQueryable<T>, applying it and returning the results.
In order to work, it needs an IQueryable<T>.
The ODataQueryOptions and example in Ihar's answer may give you what you want, but for various reasons it wasn't as useful to me as EnableQuery and so I ended up with an output formatter.
You can inspect the first output formatters in services.AddMvc(options => { options.OutputFormatters } in Startup.ConfigureServices and see that the first one has a bunch of different payload kinds.
I have been able to insert a custom error handler to handle ODataPayloadKind.Error payloads - re-writing the content returned from the server to remove stack traces etc if not in dev mode. I haven't looked into non-error cases, but you may be able to use what I have here as a starting point.
I am building a Service Layer in Web API OData that exposes a file management API. I have a problem with composable functions. Consider the following scenario. Particular files can be accessed in two ways: through an ID or through a complex Path. My original design concept was to have two URLS:
/File({IdAsGuid})
/Repositories({RepositoryName})/Libraries({libName})/Path({path})/api.getFileByName(name={fileName})
This worked pretty well using the ODataRoute attributes. The next step was to support versions, which would use URL's like:
/File({IdAsGuid})/Versions({versionNumber})
/Repositories({RepositoryName})/Libraries({libName})/Path({path})/api.getFileByName(name={fileName})/Versions({versionNumber})
Using an EntitySet "Versions" as a path segment was no problem or the first URL. However, OData refused to validate the EntitySet used after the function call. The error:
The segment 'eBesNg.getContentByName' must be the last segment in the
URI because it is one of the following: $ref, $batch, $count, $value,
$metadata, a named media resource, an action, a noncomposable
function, an action import, a noncomposable function import, an
operation with void return type, or an operation import with void
return type.
After some research, I realized that the function is defined as follows:
builder.Namespace = "api";
var function = builder.EntityType<Path>().Function("getFileByName");
function.Parameter<string>("name");
function.ReturnsFromEntitySet<File>("Files");
And may additionally require:
function.IsComposable = true;
However, this created a different issue. Now, during the OData validation, I receive a NullReferenceException:
[NullReferenceException: Object reference not set to an instance of an object.]
Microsoft.OData.Core.UriParser.Parsers.ODataPathParser.CreatePropertySegment(ODataPathSegment
previous, IEdmProperty property, String queryPortion) +205
Microsoft.OData.Core.UriParser.Parsers.ODataPathParser.CreateNextSegment(String
text) +405
Microsoft.OData.Core.UriParser.Parsers.ODataPathParser.ParsePath(ICollection'1
segments) +244
Microsoft.OData.Core.UriParser.Parsers.ODataPathFactory.BindPath(ICollection'1
segments, ODataUriParserConfiguration configuration) +96
Microsoft.OData.Core.UriParser.ODataUriParser.ParsePathImplementation()
+205
What am I missing? Is it not possible to use functions for navigation and continue to navigate on results in OData?
You should properly set the EntitySetPath of your function. That is, replace:
function.ReturnsFromEntitySet<File>("Files");
With
function.ReturnsEntityViaEntitySetPath<File>("bindingParameter/xxx");
Here is a complete sample:
class Path
{
public string Id { get; set; }
public File File { get; set; }
}
class File
{
public Guid Id { get; set; }
public ICollection<Version> Versions { get; set; }
}
class Version
{
public string Id { get; set; }
}
class Program
{
static void Main(string[] args)
{
var builder = new ODataConventionModelBuilder();
builder.Namespace = "api";
builder.EntityType<File>();
var function = builder.EntityType<Path>().Function("getFileByName");
function.Parameter<string>("name");
//function.ReturnsFromEntitySet<File>("Files");
function.ReturnsEntityViaEntitySetPath<File>("bindingParameter/File");
function.IsComposable = true;
builder.EntitySet<Path>("Paths");
builder.EntitySet<Version>("Versions");
var model = builder.GetEdmModel();
string path = "Paths('1')/api.getFileByName(name='sd')/Versions('s')";
var parser = new ODataUriParser(model, new Uri(path, UriKind.Relative));
var pa = parser.ParsePath();
Console.WriteLine(pa);
}
}
I am evaluating how to add hypermedia links to DTO responses. Although there is no standard, add List to the response DTOs seems to be the suggested approach.
Do you know of any example or reference of implementation using ServiceStack framework?
Adding List is ok for me, but my doubts are about where to put the logic of the following links (Within the service or a specialized class that holds the state machine?) and where to resolve the routes (A filter?)
Thanks.
[Update] From ServiceStack version v3.9.62 it is posible to access Routes configuration via EndpointHost.Config.Metadata.Routes.RestPath, so the solution provided by tgmdbm can be improved withouth the need of "IReturn + Routes attributes", just using Metadata.Routes information.
In fact all service metadata can be queried and used to cross-cutting concerns. Servicestack rocks.
The way I do this currently is I pass back a response dto which implements an interface
public interface IHaveLinks
{
[IgnoreDataMember]
IEnumerable<Link> Links { get; }
}
public class Link
{
public string Name { get; set; }
public IReturn Request { get; set; }
public string Method { get; set; }
}
Then I use a response filter to generate the urls and populate the response headers with the links.
this.ResponseFilters.Add((req, res, dto) =>
{
if (!(dto is IHaveLinks))
return;
var links = (dto as IHaveLinks).Links
if(links == null || !links.Any())
return;
var linksText = links
.Select(x => string.Format("<{0}>; rel={1}"), x.Request.ToUrl(x.Method), x.Name));
var linkHeader = string.Join(", ", linksText);
res.AddHeader("Link", linkHeader);
});
This seems the cleanest way. The Link object above effectively says "If you make this request with this method you will get back the named resource". The only HTTP thing that bleeds up to the BLL is Method. But you could get rid of that and only pass back GET urls. Or map it to some generalised "operation"?
As an example:
public class ExampleService : Service
{
public ExamplesResponse Get(ExamplesRequest request)
{
var page = request.Page;
var data = // get data;
return new ExamplesResponse
{
Examples = data,
Links = new []
{
new Link { Name = "next", Request = request.AddPage(1), Method = "GET" },
new Link { Name = "previous", Request = request.AddPage(-1), Method = "GET" },
}
}
}
}
[Route("/examples/{Page}")]
public class ExamplesRequest : IReturn<ExamplesResponse>
{
public int Page { get; set; }
// ...
}
(The AddPage method returns a clone of the request and sets the Page property appropriately.)
Hope that helps.