Getting OData Count in ASP.NET Core WebAPI - c#

Using the sample code from Hassan Habib's Supercharging ASP.NET Core API with OData blog post, I am able to get the record count using an OData query of $count=true:
What needs to be configured to get the response object to be wrapped in an OData context so that the #odata.count property will show?
In my own ASP.NET Core web API project, I cannot get the simple $count parameter to work and I have no idea why.
With Hassan's sample code, the response JSON is wrapped in an OData context and the payload (an IEnumerable<Student> object) is in the value property of the JSON response. In my project, the OData context wrapper does not exist; my code never returns OData context, it only returns the payload object of type IEnumerable<T>:
I've also noticed that the Content-Type in the response header is application/json; odata.metadata=minimal; odata.streaming=true; charset=utf-8 in the sample project, where as it is simply application/json; charset=utf-8 in my project. I don't see any setting that controls this in either project, so I'm assuming the Microsoft.AspNetCore.Odata NuGet package is magically changing the response when it's configured properly.
My project is also using .NET Core 2.2 (Upgraded from 2.1), all the same versions of NuGet packages as Hassan's sample projects, and all the same settings in the StartUp.cs class... although my StartUp.cs is way more complicated (hence the reason I'm not posting it's content here.)

I could reproduce your issue when i use [Route("api/[controller]")]and [ApiController] with the startup.cs like below:
app.UseMvc(routeBuilder =>
{
routeBuilder.Expand().Select().Count().OrderBy().Filter();
routeBuilder.EnableDependencyInjection();
});
To fix it,be sure you have built a private method to do a handshake between your existing data models (OData model in this case) and EDM.
Here is a simple demo:
1.Controller(comment on Route attribute and ApiController attribute):
//[Route("api/[controller]")]
//[ApiController]
public class StudentsController : ControllerBase
{
private readonly WSDbContext _context;
public StudentsController(WSDbContext context)
{
_context = context;
}
// GET: api/Students
[HttpGet]
[EnableQuery()]
public IEnumerable<Student> Get()
{
return _context.Students;
}
}
//[Route("api/[controller]")]
//[ApiController]
public class SchoolsController : ControllerBase
{
private readonly WSDbContext _context;
public SchoolsController(WSDbContext context)
{
_context = context;
}
// GET: api/Schools
[HttpGet]
[EnableQuery()]
public IEnumerable<School> Get()
{
return _context.Schools;
}
2.Startup.cs():
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvcCore(action => action.EnableEndpointRouting = false);
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
var connection = #"Server=(localdb)\mssqllocaldb;Database=WSDB;Trusted_Connection=True;ConnectRetryCount=0";
services.AddDbContext<WSDbContext>(options => options.UseSqlServer(connection));
services.AddOData();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseMvc(routeBuilder =>
{
routeBuilder.Expand().Select().Count().OrderBy().Filter();
routeBuilder.MapODataServiceRoute("api", "api", GetEdmModel());
});
}
private static IEdmModel GetEdmModel()
{
var builder = new ODataConventionModelBuilder();
builder.EntitySet<Student>("Students");
builder.EntitySet<Student>("Schools");
return builder.GetEdmModel();
}
}

Just been battling this.
I found that if I request my controller at /api/Things that most of the OData options work but $count doesn't.
However, $count does work if I request the same method via /odata/Things.

In my case I wanted to extend existing Api methods with [EnableQuery] but have it include the count metadata.
I ended up extending the EnableQuery attribute to return a different reponse, it worked perfectly.
public class EnableQueryWithMetadataAttribute : EnableQueryAttribute
{
public override void OnActionExecuted(ActionExecutedContext actionExecutedContext)
{
base.OnActionExecuted(actionExecutedContext);
if (actionExecutedContext.Result is ObjectResult obj && obj.Value is IQueryable qry)
{
obj.Value = new ODataResponse
{
Count = actionExecutedContext.HttpContext.Request.ODataFeature().TotalCount,
Value = qry
};
}
}
public class ODataResponse
{
[JsonPropertyName("#odata.count")]
public long? Count { get; set; }
[JsonPropertyName("value")]
public IQueryable Value { get; set; }
}
}

You can just set an empty preffix route when you map OData, and you will receive OData with your request your endpoint.
routeBuilder.MapODataServiceRoute("ODataEdmModel", "", GetEdmModel());

In my case I've created a special action named $count that users the OData Filter query my collection (EF Database) and return the Count;
[HttpGet("$count")]
public async Task<int> Count(ODataQueryOptions<MyBookEntity> odataQueryOptions)
{
var queryable = this.dataContext.MyBooks;
return await odataQueryOptions.Filter
.ApplyTo(queryable, new ODataQuerySettings())
.Cast<MyBookEntity>()
.CountAsync();
}

Related

Mapping dynamic odata routes with ASP.NET Core OData 8.0

I've got an application where the EDM datatypes are generated during the runtime of the application (they can even change during runtime). Based loosely on OData DynamicEDMModelCreation Sample - refactored to use the new endpoint routing. There the EDM model is dynamically generated at runtime and all requests are forwarded to the same controller.
Now I wanted to update to the newest ASP.NET Core OData 8.0 and the whole routing changed so that the current workaround does not work anymore.
I've read the two blog posts of the update Blog1Blog2 and it seems that I can't use the "old" workaround anymore as the function MapODataRoute() within the endpoints is now gone. It also seems that none of the built-in routing convention work for my use-case as all require the EDM model to be present at debug time.
Maybe I can use a custom IODataControllerActionConvention. I tried to active the convention by adding it to the Routing Convention but it seems I'm still missing a piece how to activate it.
services.TryAddEnumerable(
ServiceDescriptor.Transient<IODataControllerActionConvention, MyEntitySetRoutingConvention>());
Does this approach even work? Is it even possible to activate a dynamic model in the new odata preview? or does anybody has a slight idea how to approach a dynamic routing for the new odata 8.0?
There is an example for dynamic routing and dynamic model here:
https://github.com/OData/AspNetCoreOData/blob/master/sample/ODataDynamicModel/
See MyODataRoutingApplicationModelProvider and MyODataRoutingMatcherPolicy which will pass a custom IEdmModel to the controller.
The HandleAllController can handle different types and edm models in a dynamic way.
So after 5 days of internal OData debugging I managed to get it to work. Here are the necessary steps:
First remove all OData calls/attributes from your controller/configure services which might do funky stuff (ODataRoutingAttribute or AddOData())
Create a simple asp.net controller with the route to your liking and map it in the endpoints
[ApiController]
[Route("odata/v{version}/{Path?}")]
public class HandleAllController : ControllerBase { ... }
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHostApplicationLifetime applicationLifetime, ILoggerFactory loggerFactory)
{
...
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
}
}
Create and register your InputFormatWrapper and OutputFormatWrapper
public class ConfigureMvcOptionsFormatters : IConfigureOptions<MvcOptions>
{
private readonly IServiceProvider _services;
public ConfigureMvcOptionsFormatters(IServiceProvider services)
{
_services = services;
}
public void Configure(MvcOptions options)
{
options.InputFormatters.Insert(0, new ODataInputFormatWrapper(_services));
options.OutputFormatters.Insert(0, new OdataOutputFormatWrapper(_services));
}
}
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.ConfigureOptions<ConfigureMvcOptionsFormatters>();
...
}
public class ODataInputFormatWrapper : InputFormatter
{
private readonly IServiceProvider _serviceProvider;
private readonly ODataInputFormatter _oDataInputFormatter;
public ODataInputFormatWrapper(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
//JSON and default is first - see factory comments
_oDataInputFormatter = ODataInputFormatterFactory.Create().First();
}
public override bool CanRead(InputFormatterContext context)
{
if (!ODataWrapperHelper.IsRequestValid(context.HttpContext, _serviceProvider))
return false;
return _oDataInputFormatter.CanRead(context);
}
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
{
return _oDataInputFormatter!.ReadRequestBodyAsync(context);
}
}
// The OutputFormatWrapper looks like the InputFormatWrapper
Within the ODataWrapperHelper you can check stuff and get/set your dynamic edmModel. It is necessary in the end to set these ODataFeature()... That's not beautiful but it gets the dynamic job done...
public static bool IsRequestValid(HttpContext context, IServiceProvider serviceProvider)
{
//... Do stuff, get datasource
var edmModel = dataSource!.GetModel();
var oSegment = new EntitySetSegment(new EdmEntitySet(edmModel.EntityContainer, targetEntity, edmModel.SchemaElements.First(x => targetEntity == x.Name) as EdmEntityType));
context.ODataFeature().Services = serviceProvider.CreateScope().ServiceProvider;
context.ODataFeature().Model = edmModel;
context.ODataFeature().Path = new ODataPath(oSegment);
return true;
}
Now to the ugly stuff: We still need to register some ODataService in ConfigureServices(IServiceCollection services). I added a function there called AddCustomODataService(services) and there you can either register ~40 services yourself or do some funky reflection...
So maybe if someone from the odata team reads this, please consider opening Microsoft.AspNetCore.OData.Abstracts.ContainerBuilderExtensions
I created a
public class CustomODataServiceContainerBuilder : IContainerBuilder which is a copy of the internal Microsoft.AspNetCore.OData.Abstracts.DefaultContainerBuilder there I added the function:
public void AddServices(IServiceCollection services)
{
foreach (var service in Services)
{
services.Add(service);
}
}
and the ugly AddCustomODataServices(IServiceCollection services)
private void AddCustomODataService(IServiceCollection services)
{
var builder = new CustomODataServiceContainerBuilder();
builder.AddDefaultODataServices();
//AddDefaultWebApiServices in ContainerBuilderExtensions is internal...
var addDefaultWebApiServices = typeof(ODataFeature).Assembly.GetTypes()
.First(x => x.FullName == "Microsoft.AspNetCore.OData.Abstracts.ContainerBuilderExtensions")
.GetMethods(BindingFlags.Static|BindingFlags.Public)
.First(x => x.Name == "AddDefaultWebApiServices");
addDefaultWebApiServices.Invoke(null, new object?[]{builder});
builder.AddServices(services);
}
Now the controller should work again (with odataQueryContext and serialization in place) - Example:
[HttpGet]
public Task<IActionResult> Get(CancellationToken cancellationToken)
{
//... get model and entitytype
var queryContext = new ODataQueryContext(model, entityType, null);
var queryOptions = new ODataQueryOptions(queryContext, Request);
return (Collection<IEdmEntityObject>)myCollection;
}
[HttpPost]
public Task<IActionResult> Post([FromBody] IEdmEntityObject entityDataObject, CancellationToken cancellationToken)
{
//Do something with IEdmEntityObject
return Ok()
}

How to validate requests with FluentValdation instead of data annotations?

I created a new .Net 5 Web API with the following setup
public class MyController : ControllerBase
{
[HttpGet("{Title}")]
public ActionResult<string> Get([FromRoute] RouteModel routeModel)
{
return Ok(routeModel);
}
}
public class RouteModel
{
[MinLength(3)]
public string Title { get; set; }
}
The request validation works fine. Since I'm using FluentValidation I installed the package
FluentValidation.AspNetCore v10.3.0
and updated the Startup.ConfigureServices method to
public void ConfigureServices(IServiceCollection services)
{
services.AddValidatorsFromAssemblies(AppDomain.CurrentDomain.GetAssemblies());
services.AddControllers();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebApplication", Version = "v1" });
});
}
Instead of using the data annotations I removed the data annotations from RouteModel.Title and created this validator
public class RouteModelValidator : AbstractValidator<RouteModel>
{
public RouteModelValidator()
{
RuleFor(model => model.Title).MinimumLength(3);
}
}
I would expect a ValidationException and this would result into a 500 status code if the title has a length less than 3. But it seems there is no validation trigger anymore, the request always passes.
Does someone know what's missing?
Register FluentValidation as
services
.AddFluentValidation(x => x.RegisterValidatorsFromAssemblies(AppDomain.CurrentDomain.GetAssemblies()));
Mark your Controller with ApiController attribute
[ApiController]
public class MyController : ControllerBase
Otherwise, you should manually validate ModelState, something like:
[HttpGet("{Title}")]
public ActionResult<string> Get([FromRoute] RouteModel routeModel)
{
if (!ModelState.IsValid)
{
return BadRequest("bad");
}
return Ok(routeModel);
}
Some info:
It depends on the type of controller - if you're using the [ApiController] attribute, then ASP.NET will generate a Bad Request result automatically. If you don't use the ApiController attribute (ie for non-api controllers, such as where you return a view), then you're expected to handle the ModelState.IsValid check manually (see the asp.net doc link above). But again, this is a convention of ASP.NET, it isn't a feature of FluentValidation.

ASP.Net Core specifying multiple content-types with response compression

What is the correct way to specify that your API can return compressed responses via Swagger/SwashbuckleUI in ASP.NET Core 3.x?
The goal is to support both non-compressed and compressed responses, giving the calling user the flexibility to choose what they want via their Accept-Encoding header. This is a hard requirement as one consuming team is limited in what libraries they can use to make the API calls. The code itself is doing what is required, but I can't get Swagger/SwashbuckleUI to reflect this feature in the documentation.
Given the following example:
Startup.cs
public void ConfigureServices(IServiceConnection services)
{
services.Configure<BrotliCompressionProviderOptions>(options =>
{
options.Level = System.IO.Compression.CompressionLevel.Optimal;
});
services.AddResponseCompression(options =>
{
options.Providers.Add<BrotliCompressionProvider();
options.EnableForHttps = true;
}
}
public void Configure(IApplicationBuilder app, IWebHostEnvrironment env)
{
app.UseResponseCompression();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
MyController.cs
[ApiController]
[Route("api/dev/mycontroller")]
[Produces("application/json")]
[ApiExplorerSettings(GroupName = "My Swagger Name")]
public class MyController : Controller
{
[HttpGet("")]
public async Task<ActionResult<ResponseDTO>> GetAll()
{
var dbResults = _repository.Model.GetAll();
if (dbResults != null)
{
return Ok(dbResults.ToResponseDTOs());
}
else
{
return BadRequest();
}
}
}
I would expect that I need to add another string to the ProducesAttribute:
[Produces("application/json", "br")]
So that Swagger/SwashbuckleUI would display that, similar to examples I have seen for dual json/xml support. However, when I try to build the project after adding the "br" string to the ProducesAttribute I get:
System.FormatException "The header contains invalid values at index 0: 'br'"
from the MapControllers() call in the Configure method on startup.

I've added breeze to my webapi but it still not adding the custom $id and $type properties

I'm trying to add Breeze to my .Net Core 2.2 webapi and I can't figure out what I'm missing. To troubleshoot I've created a very simple webapi that returns 1 item. This works but breeze isn't adding it's custom properties to my entities.
I've added [BreezeQueryFilter] to my controller but the $id and $type properties are not being added to my entities.
I've create a simple repository with what I have so far.
https://github.com/wstow/SimpleBreeze
Thanks
My Controller
[Route("api/[controller]/[action]")]
[BreezeQueryFilter]
public class OrderController : Controller
{
private OrderContext _context;
private OrderManager PersistenceManager;
public OrderController(OrderContext context)
{
this._context = context;
PersistenceManager = new OrderManager(context);
}
[HttpGet]
public IActionResult Metadata()
{
return Ok(PersistenceManager.Metadata());
}
[HttpGet]
public IQueryable<ReqStatus> Status()
{
return PersistenceManager.Context.ReqStatus;
}
}
My Manager
public class OrderManager : EFPersistenceManager<OrderContext>
{
public OrderManager(OrderContext orderContext) : base(orderContext) { }
}
My Context
public class OrderContext : DbContext
{
public OrderContext()
{
//Configuration.ProxyCreationEnabled = false;
// Configuration.LazyLoadingEnabled = false;
}
public OrderContext(DbContextOptions<OrderContext> options)
: base(options)
{ }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{ }
public DbSet<ReqStatus> ReqStatus { get; set; }
}
The problem is in the JSON serialization settings. Newtonsoft.Json is highly configurable, and you need to use the right settings to communicate properly with the Breeze client.
To make this easy, Breeze has a configuration function to change the settings to good defaults. You call it from your Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
var mvcBuilder = services.AddMvc();
mvcBuilder.AddJsonOptions(opt => {
var ss = JsonSerializationFns.UpdateWithDefaults(opt.SerializerSettings);
});
mvcBuilder.AddMvcOptions(o => { o.Filters.Add(new GlobalExceptionFilter()); });
...
The documentation is lacking, but you can see what JsonSerializationFns does by looking in the Breeze code.
The last line adds an exception filter that wraps server-side validation errors so that the Breeze client can handle them. You can see what it does here.

NSwag: Generate C# Client from multiple Versions of an API

We are versioning our API and generating the Swagger specification using Swashbuckle in ASP.NET Core 1.1. We can generate two API docs based on those JSON specification files:
<!-- language: c# -->
services.AddSwaggerGen(setupAction =>
{
setupAction.SwaggerDoc("0.1", new Info { Title = "Api", Version = "0.1", Description = "API v0.1" });
setupAction.SwaggerDoc("0.2", new Info { Title = "Api", Version = "0.2", Description = "API v0.2" });
// more configuration omitted
}
We are including all actions in both spec files, unless it is mapped to a specific version using the [MapToApiVersion] and ApiExplorerSettings(GroupName ="<version>")] attributes. Methods belonging to an older version only are also decorated with the [Obsolete] attribute:
<!-- language: c# -->
[MapToApiVersion("0.1")]
[ApiExplorerSettings(GroupName = "0.1")]
[Obsolete]
However, we want to have only one C# Client generated from the Union of both spec files, where all methods are included in the Client, 0.1 as well as 0.2, but all obsolete methods marked, in fact, as obsolete.
I have looked into both NSwag (which we are using for quite some time now) as well as AutoRest. AutoRest seems to support a merging scenario, but I could not get it to work because of schema validation errors (and I am more than unsure whether our specific scenario would be actually supported).
My last idea as of now to get this sorted is to somehow JSON-merge the specs into one and then feed it to NSwag.
Do we miss anything here? Is this somehow possible to realize with NSwag?
I wrote an article about similar problem https://medium.com/dev-genius/nswag-charp-client-from-multiple-api-versions-7c79a3de4622
First of all, create a schema. As I see, there are two approaches:
one schema where multiple versions are living
own schema for each version
Next, create clients for each supported version and wrap them under the wrapper client:
public class AppApiClient
{
public IV1Client V1 { get; }
public IV2Client V2 { get; }
public AppApiClient(HttpClient httpClient)
{
V1 = new V1Client(httpClient);
V2 = new V2Client(httpClient);
}
}
Here is my idea, expanding from the comments:
With swashbuckle you can generate as many SwaggerDoc as you like, the idea on this case is to generate 3 keep the same 2 versions that you have and add one more that will have everything.
c.MultipleApiVersions(
(apiDesc, targetApiVersion) =>
targetApiVersion.Equals("default") || // Include everything by default
apiDesc.Route.RouteTemplate.StartsWith(targetApiVersion), // Only include matching routes for other versions
(vc) =>
{
vc.Version("default", "Swagger_Test");
vc.Version("v1_0", "Swagger_Test V1_0");
vc.Version("v2_0", "Swagger_Test V2_0");
});
Here is a working sample:
http://swagger-net-test-multiapiversions.azurewebsites.net/swagger/ui/index?filter=Api
And the entire code for that project is on GitHub:
https://github.com/heldersepu/Swagger-Net-Test/tree/MultiApiVersions
Packages:
Install-Package Swashbuckle.AspNetCore
Install-Package Microsoft.AspNetCore.Mvc.Versioning
ValueV1Controller.cs
[ApiVersion("1")]
[Route("api/v{version:apiVersion}/Values")]
public class ValuesV1Controller : Controller
{
// GET api/values
[HttpGet]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
}
ValueV2Controller.cs
[ApiVersion("2")]
[Route("api/v{version:apiVersion}/Values")]
public class ValuesV2Controller : Controller
{
// GET api/values
[HttpGet]
public IEnumerable<string> Get()
{
return new string[] { "value1.2", "value2.2" };
}
}
Startup.cs
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddApiVersioning();
// Register the Swagger generator, defining 1 or more Swagger documents
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Info { Title = "My API - V1", Version = "v1" });
c.SwaggerDoc("v2", new Info { Title = "My API - V2", Version = "v2" });
c.DocInclusionPredicate((docName, apiDesc) =>
{
var versions = apiDesc.ControllerAttributes()
.OfType<ApiVersionAttribute>()
.SelectMany(attr => attr.Versions);
return versions.Any(v => $"v{v.ToString()}" == docName);
});
c.OperationFilter<RemoveVersionParameters>();
c.DocumentFilter<SetVersionInPaths>();
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// Enable middleware to serve generated Swagger as a JSON endpoint.
app.UseSwagger();
// Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.),
// specifying the Swagger JSON endpoint.
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v2/swagger.json", "My API V2");
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});
app.UseMvc();
}
}
public class RemoveVersionParameters : IOperationFilter
{
public void Apply(Operation operation, OperationFilterContext context)
{
var versionParameter = operation.Parameters?.SingleOrDefault(p => p.Name == "version");
if (versionParameter != null)
operation.Parameters.Remove(versionParameter);
}
}
public class SetVersionInPaths : IDocumentFilter
{
public void Apply(SwaggerDocument swaggerDoc, DocumentFilterContext context)
{
swaggerDoc.Paths = swaggerDoc.Paths
.ToDictionary(
path => path.Key.Replace("v{version}", swaggerDoc.Info.Version),
path => path.Value
);
}
}

Categories

Resources