Can't get Odata 7.1 working in web api (.net core 2.2) - asp.net-core

I'm having trouble implementing OData in my Web API project.
I'm using .NET Core 2.2 and OData (Microsoft.AspNetCore.OData v7.1.0).
(I also have AutoMapper installed, but I don't think this is relevant)
The problem is that the response is never enriched with the extra properties (like odata.context and odata.count). Also, adding ?count=true doesn't do anything either.
Does anyone have any idea why this is happening?
This is my Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
/* AutoMapper config */
var mappingConfig = new MapperConfiguration(mc =>
{
mc.AddProfile(new MappingProfile());
});
IMapper mapper = mappingConfig.CreateMapper();
services.AddSingleton(mapper);
services.AddMvc(opt => opt.EnableEndpointRouting = false).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddOData();
var connection = #"snipped-connection-string";
services.AddDbContext<AllOnBoardContext>
(options => options.UseSqlServer(connection));
/* CORS for local development, not ideal, I know */
services.AddCors(setup =>
{
setup.AddPolicy(AllowCorsPolicyName, builder =>
{
builder.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod();
});
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseCors(AllowCorsPolicyName);
app.UseMvc(routeBuilder =>
{
routeBuilder.Select().Filter().OrderBy().Expand().Count().MaxTop(10);
routeBuilder.MapODataServiceRoute("api", "api", GetEdmModel());
});
}
private static IEdmModel GetEdmModel()
{
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Tenant>("Tenants");
return builder.GetEdmModel();
}
And this is my `Controller´ class:
public class TenantController : ControllerBase
{
private ITenantService tenantService;
public TenantController(ITenantService tenantService, IMapper mapper) : base(mapper, tenantService)
{
this.tenantService = tenantService;
}
[HttpGet]
[EnableQuery()]
public ActionResult<IEnumerable<TDto>> Get()
{
return Service.GetAll().ToList();
}
}

First of all, OData query parameters always start with a $. So in your case, it is ?$count=true.
Next, make sure that every Controller inherits from ODataController:
[ODataRoutePrefix("Tenants")]
public class TenantController : ODataController
{
private ITenantService tenantService;
public TenantController(ITenantService tenantService, IMapper mapper) : base(mapper, tenantService)
{
this.tenantService = tenantService;
}
[HttpGet]
[ODataRoute]
[EnableQuery]
public ActionResult<IEnumerable<TDto>> Get()
{
return Service.GetAll().ToList();
}
}
Annotations will help you to control OData. The prefix string of the ODataRoutePrefixAttribute specifies the OData URL path template that this action handles (in your case http://<host:port>/api/Tenants) and with the ODataRouteAttribute it is possible to specify the OData URL path template that this action handles.

Related

.net core 5.0.2 and jwt => response 401 Unauthorized

I am following an video tutorial for identity server 4 with web api's.
And Im not sure when I went wrong.
Im getting 401 Unauthorized when I try to call api with bearer token.
In previos step, without authorization, my api worked.
This is my api controller in my TablesReach.API project:
...
namespace TablesReach.Controllers
{
[Authorize]
[Route("api/[controller]")]
[ApiController]
public class UsersController : ControllerBase
{
private readonly DataContext _context;
public UsersController(DataContext context)
{
_context = context;
}
// GET: api/Users
[HttpGet]
public async Task<ActionResult<IEnumerable<User>>> GetUsers()
{
return await _context.Users.ToListAsync();
}
...
this is my Startup.cs of my api project:
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.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(opts =>
{
opts.Authority = "http://localhost:5000";
opts.RequireHttpsMetadata = false;
opts.ApiName = "TablesReachApi";
});
services.AddDbContext<DataContext>(opts => opts.UseInMemoryDatabase("UNWDb"));
services.AddControllers();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
app.UseAuthentication();
}
}
My other project TablesReach.IdentityServer is host on localhost:5000
and Im being able to get bearer token, so I assume that this project is quite OK.
identityServer startup.cs class:
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.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryApiScopes(Config.GetAllApiResources())
.AddInMemoryClients(Config.GetClients());
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
//else
//{
// app.UseExceptionHandler("/Home/Error");
// // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
// app.UseHsts();
//}
//app.UseHttpsRedirection();
//app.UseStaticFiles();
//app.UseRouting();
//app.UseAuthorization();
//app.UseEndpoints(endpoints =>
//{
// endpoints.MapControllerRoute(
// name: "default",
// pattern: "{controller=Home}/{action=Index}/{id?}");
//});
app.UseIdentityServer();
}
}
and Config.cs:
public class Config
{
public static IEnumerable<ApiScope> GetAllApiResources()
{
return new List<ApiScope>
{
new ApiScope("TablesReachApi", "Api for solution")
};
}
public static IEnumerable<Client> GetClients()
{
return new List<Client>
{
new Client
{
ClientId = "client",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = { "TablesReachApi" }
}
};
}
}
Note: When I remove annotation [Authorize] from my api controller I can reach my method.
For some middleware, order matters. Authentication and authorization, for example, can't go in the order that you have put them in the API. Microsoft has some clear documentation on this for you to read here..

How do I reflect a dotnet web api endpoint that uses query string parameters in SwagggerUI?

I'm trying to implement a dotnet web api with API versioning that uses query strings and headers. Here im using swagger to document and test the endpoints. I successfully used path versioning and reflected the endpoints in swagger. But im struggling to understand how to reflect query string & header versioning in swagger. I tried to find a solution from this article https://swagger.io/docs/specification/describing-parameters/#query-parameters but was still confused how to implement this in my dotnet web api.
My project contains 2 main controller classes with the following API versions.
WeatherForecastController.cs
namespace QueryStringVersioning.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[ApiVersion("1.1", Deprecated = true)]
[ApiVersion("3.0")]
[Route ("api")] //support query string & header versioning
// [Route("api/v{version:apiVersion}/[controller]")] //support path versioning
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering",
"Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
var rng = new Random();
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
})
.ToArray();
}
[HttpGet, MapToApiVersion("3.0")]
public IActionResult GetV3_0() => Ok(new string[] { "MapToApiVersion value 3.0" });
[HttpGet, MapToApiVersion("1.1")]
public IActionResult GetV1_1() => Ok(new string[] { "Depreceated MapToApiVersion value" });
}}
WeatherForecastController2.cs
namespace QueryStringVersioning.Controllers2
{
[ApiController]
[ApiVersion("2.0")]
[ApiVersion("2.1")]
[Route ("api")] //support query string & header versioning
// [Route("api/v{version:apiVersion}/[controller]")] //support path versioning
public class WeatherForecastController : ControllerBase
{
public IActionResult GetV2_0() => Ok(new string[] { "This is API Version 2.0" });
[HttpGet, MapToApiVersion("2.1")]
public IActionResult GetV2_1() => Ok(new string[] { "This is API Version 2.1" });
}}
And the startup.cs file
namespace QueryStringVersioning
{
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.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo
{
Version = "v1",
Title = "API_Versioning V1",
});
c.SwaggerDoc("v1.1", new Microsoft.OpenApi.Models.OpenApiInfo
{
Version = "v1.1",
Title = "API_Versioning V1.1",
});
c.SwaggerDoc("v2", new Microsoft.OpenApi.Models.OpenApiInfo
{
Version = "v2",
Title = "API_Versioning V2"
});
c.SwaggerDoc("v2.1", new Microsoft.OpenApi.Models.OpenApiInfo
{
Version = "v2.1",
Title = "API_Versioning V2.1"
});
c.SwaggerDoc("v3", new Microsoft.OpenApi.Models.OpenApiInfo
{
Version = "v3",
Title = "API_Versioning V3"
});
c.ResolveConflictingActions (apiDescriptions => apiDescriptions.First ());
// c.OperationFilter<RemoveVersionFromParameter>();
// c.DocumentFilter<ReplaceVersionWithExactValueInPath>();
});
services.AddControllers();
services.AddMvc();
services.AddApiVersioning(option =>
{
option.ReportApiVersions = true;
option.AssumeDefaultVersionWhenUnspecified = true;
option.DefaultApiVersion = new ApiVersion(1, 0);
// Supporting multiple versioning scheme
option.ApiVersionReader = ApiVersionReader.Combine(new HeaderApiVersionReader("X-version"), new QueryStringApiVersionReader("api-version"));
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
// 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/v1/swagger.json", "API_Versioning V1.0");
c.SwaggerEndpoint("/swagger/v1.1/swagger.json", "API_Versioning V1.1");
c.SwaggerEndpoint("/swagger/v2/swagger.json", "API_Versioning V2.0");
c.SwaggerEndpoint("/swagger/v2.1/swagger.json", "API_Versioning V2.1");
c.SwaggerEndpoint("/swagger/v3/swagger.json", "API_Versioning V3.0");
});
}
}
}
#michael-wang is correct. You need to also include the API Versioning API Explorer extensions. This extensions make API discovery API version aware. One of the many possible uses for this information is OpenAPI/Swagger integration.
Links to all of the applicable NuGet packages are listed on the API Versioning landing page. There is also an end-to-end example provided using Swashbuckle.

ASP.NET Core Odata Service Post not working

I implemented a ODATA Service in my ASP.NET Core application. The GET function is working fine, but I have some problems with the POST function.
If I excecute a POST the programm is excecuting the right method but I don't receive any data.
Is there anything missing in my code?
Controller:
[EnableCors]
[ODataRoutePrefix("documents")]
public class DocumentController : ODataController
{
[ODataRoute]
[EnableQuery]
public Document PushDocument([FromBody]Document doc)
{
System.Diagnostics.Debug.WriteLine("DomentID: " + doc.Id);
System.Diagnostics.Debug.WriteLine("Dokument: " + doc.RawDocument);
return doc;
}
}
Since you use [FromBody], you need to send data as Content-Type: application/json,in postman:
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddOData();
services.AddMvc(options =>
{
options.EnableEndpointRouting = false;
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
// 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
{
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseMvc(b =>
{
b.MapODataServiceRoute("odata", "odata", GetEdmModel());
});
}
private static IEdmModel GetEdmModel()
{
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Document>("Documents");
builder.EntitySet<Press>("Presses");
return builder.GetEdmModel();
}

How to return indented json content from an OData controller in asp core web api?

I can retrieve intended json result from normal WebApi using following way.
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
.AddJsonOptions(x=>
{
x.SerializerSettings.Formatting = Newtonsoft.Json.Formatting.Indented;
});
But I cannot find a way to output json like this when using ODataController as opposed to ControllerBase when web api is used. ODataController always sends a minified json.
public class EmployeeController : ODataController
{
[EnableQuery()]
public IActionResult Get()
{
return Ok(new BOContext().Employees.ToList());
}
}
Also, startup.cs
public class Startup
{
private static IEdmModel GetModel()
{
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Employee>("Employee");
return builder.GetEdmModel();
}
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.AddOData();
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
.AddJsonOptions(x=>
{
x.SerializerSettings.Formatting = Newtonsoft.Json.Formatting.None;
});
}
// 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.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMvc(routes =>
{
routes.MapODataServiceRoute("odata", "odata", GetModel());
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
The route is working and I am receiving correct data.
Is there a way to control and output indented json from an OData controller?
I suggest you to make the transfer with minified jsonm, but use json beutifier to show formatted json. dont do this on the data flow phase.
If you are using javascript on the front-end side. You can simple use
JSON.stringify(jsObj, null, "\t"); // stringify with tabs inserted at each level
JSON.stringify(jsObj, null, 2); // stringify with 2 spaces at each level
Not sure if this is still actual, but you can specify formatter when returning the data
// [...]
public IActionResult Get()
{
var res = Ok(_db.Employees);
res.Formatters.Add(new Microsoft.AspNetCore.Mvc.Formatters.JsonOutputFormatter(
new Newtonsoft.Json.JsonSerializerSettings() { Formatting = Newtonsoft.Json.Formatting.Indented },
System.Buffers.ArrayPool<char>.Create()));
return res;
}
And of course, if you want more generalized solution (or you just have a lot of code that is already written), you can create interim abstract class and inherit from that class instead of just ODataController:
public abstract class AbstractFormattedOdataController : ODataController
{
public override OkObjectResult Ok(object value)
{
var res = base.Ok(value);
res.Formatters.Add(new Microsoft.AspNetCore.Mvc.Formatters.JsonOutputFormatter(
new Newtonsoft.Json.JsonSerializerSettings() { Formatting = Newtonsoft.Json.Formatting.Indented },
System.Buffers.ArrayPool<char>.Create()));
return res;
}
}
// [...]
public class EmployeesController : AbstractFormattedOdataController
{
[EnableQuery()]
public IActionResult Get()
{
return Ok(new BOContext().Employees.ToList());
}
}

asp.net core custom IRouter Dependency Injection

I'm creating a custom routing in asp.net core 2, where I check the path in a DB and update action and controller to the desired one.
I have this custom IRouter defined like this
public interface IRouteCustom : IRouter
{
}
public class RouteCustom : IRouteCustom
{
private readonly IRouter _innerRouter;
private readonly IMemoryCache _memoryCache;
private readonly IUnitOfWork _unitOfWork;
public RouteCustom(IRouter innerRouter, IMemoryCache memoryCache, IUnitOfWork unitOfWork)
{
_innerRouter = innerRouter ?? throw new ArgumentNullException(nameof(innerRouter));
_memoryCache = memoryCache;
_unitOfWork = unitOfWork;
}
public async Task RouteAsync(RouteContext context)
{
I check the routes in the DB using the _unitOfWork
}
public VirtualPathData GetVirtualPath(VirtualPathContext context)
{
Also I do the same here...
}
}
I have no problem with those functions and I'm able to select controller and action.
My problem is how to access the database, since I can't inject the IUnitOfWork dependency into de custom router.
I'm getting this error message:
'Cannot resolve 'IUnitOfWork' from root provider because it requires scoped service 'DbContext'.'
I have my ConfigureServices like this
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<DbContext>(options => options.UseMySQL(configuration.GetConnectionString("DefaultClient")));
services.AddIdentity<Domain.EntitiesClient.Entities.ApplicationUser, Domain.EntitiesClient.Entities.ApplicationRole>().AddEntityFrameworkStores<DbContext>().AddDefaultTokenProviders();
services.AddScoped<IDbContext, DbContext>();
services.AddTransient<IUnitOfWork, UnitOfWork>();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddMemoryCache();
services.AddMvc();
/*route custom*/
var supportedCultures = new[] { new CultureInfo("en-US"), new CultureInfo("es-ES"), new CultureInfo("it-IT") };
var optionsCulture = new RequestLocalizationOptions { DefaultRequestCulture = new RequestCulture("en-US", "en-US"), SupportedCultures = supportedCultures, SupportedUICultures = supportedCultures };
optionsCulture.RequestCultureProviders = new IRequestCultureProvider[] { new RouteDataRequestCultureProvider() { RouteDataStringKey = "culture", Options = optionsCulture } };
services.AddSingleton(optionsCulture);
}
And the Configure
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseBrowserLink();
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.Routes.Add(new RouteCustom(routes.DefaultHandler
, routes.ServiceProvider.GetRequiredService<IMemoryCache>()
, app.ApplicationServices.GetService<IUnitOfWork>()
));
routes.MapRoute(name: "areas", template: "{area:exists}/{controller=Home}/{action=Index}/{id?}");
routes.MapRoute(name: "default", template: "{controller=Home}/{action=Index}/{id?}");
});
}
The problem is here
app.ApplicationServices.GetService<IUnitOfWork>()
I need to inject the IUnitOfWork in order to check the database, but I don't know how to do it. In other Middlewares I could inject the IUnitOfWork directly in the function, but in this case I can't do it in the
public async Task RouteAsync(RouteContext context)
How can I achieve this? I'm sure I'm doing something wrong here, but I have been reading a lot of articles and can't figure out the way.
Thanks.
UPDATE: POSSIBLE SOLUTION
The only solution I can think is remove the injection into the IRouter and get the service "manually" inside the RouteAsync method.
public async Task RouteAsync(RouteContext context)
{
var unitOfWork = context.HttpContext.RequestServices.GetRequiredService<IUnitOfWork>()
var routes = unitOfWork.Router.GetAll();
...
}
This way we have access to the database and it works good.
Is it a good approach?