Pagination with AspNetCore.OData - missing #odata properties in response - c#

I'm trying to implement pagination with asp.net core 2.2 and Microsoft.AspNetCore.OData 7.1.0 with following configuration:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddOData();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseMvc(b =>
{
b.EnableDependencyInjection();
});
}
}
For this I have a testing controller:
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
[HttpGet]
[EnableQuery(PageSize = 5)]
public IQueryable<int> Get()
{
return new int[] { 1,2,3,4,5,6,7,8,9,10 }.AsQueryable();
}
}
When invoking an endpoint, I would expect a response like:
{
"#odata.context":...,
"value":[1,2,3,4,5],
"#odata.nextLink":...
}
but instead I get only:
[1,2,3,4,5]
So how do I get those extra #odata properties?

Finally I figured out how to do it.
First it doesn't work with primitive types so I had to create strong one with Id property:
public class Value
{
public Value(int id)
{
Id = id;
}
public int Id { get; set; }
}
Second I had to drop ApiController and Route attributes from controller .
public class ValuesController : ControllerBase
{
[HttpGet]
[EnableQuery(PageSize = 5)]
public IQueryable<Value> Get()
{
return Enumerable.Range(1, 10).Select(i => new Value(i)).AsQueryable();
}
}
And finally register odata endpoint:
ODataModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Value>("Values");
app.UseOData("odata", "api", builder.GetEdmModel());

Related

.Net Core API - oData Prefix is lost when passing arguments

This call works correctly and is mapped to /oData/Projects.
[HttpGet(Name = "GetProjects")]
[EnableQuery]
public IEnumerable<ProjectEntity> Get()
{
return _db.Projects;
}
How can I make pass an argument with out losing the oData prefix in the URL?
This loses the oData Prefix:
[HttpGet("{id}", Name = "GetProjectById")]
[EnableQuery]
public ProjectEntity GetProjectById(int id)
{
return _db.Projects.Where(p => p.Id == id).FirstOrDefault();
}
I am using .net core web api and oData 8.0
Here is the full controller:
[ApiController]
[Route("[controller]")]
public class ProjectsController : ControllerBase
{
private readonly ILogger<ProjectsController> _logger;
private readonly ApplicationDbContext _db;
public ProjectsController(ILogger<ProjectsController> logger, ApplicationDbContext db)
{
_logger = logger;
_db = db;
}
[HttpGet("{id}", Name = "GetProjectById")]
[EnableQuery]
public ProjectEntity GetProjectById(int id)
{
return _db.Projects.Where(p => p.Id == id).FirstOrDefault();
}
[HttpGet(Name = "GetProjects")]
[EnableQuery]
public IEnumerable<ProjectEntity> Get()
{
return _db.Projects;
}
}
To enable OData attribute routing to work, either in controller or
action should decorate an attribute named ODataRoutingAttribute.
It’s renamed as ODataAttributeRoutingAttribute in 8.0. since as you
are using asp.net core controller so your controller action should
be like below:
Model:
public class ProjectInfo
{
public int Id { get; set; }
public string ApiVersion { get; set; }
public string Name { get; set; }
public string PhoneNumber { get; set; }
}
Controller:
public class ProjectsController : ControllerBase
{
private ProjectInfo[] project= new ProjectInfo[]
{
new ProjectInfo
{
Id = 1,
ApiVersion = "v1.0",
Name = "Kiron",
PhoneNumber = "111-222-3333"
},
new ProjectInfo
{
Id = 2,
ApiVersion = "v1.0",
Name = "Farid",
PhoneNumber = "456-ABC-8888"
}
};
[EnableQuery]
[HttpGet("odata/Projects")]
public IActionResult Get()
{
return Ok(project);
}
[ODataAttributeRoutingAttribute]
[HttpGet("odata/Projects/{key}")]
public IActionResult Get(int key)
{
var project = project.FirstOrDefault(c => c.Id == key);
if (project == null)
{
return NotFound($"Cannot find project with Id={key}.");
}
return Ok(project);
}
}
Note: You can also set [ODataAttributeRoutingAttribute] on global controller so that you don't need to set on each action. in
that case just get rid of [ApiController] and
[Route("[controller]")]
Reference Required:
using Microsoft.AspNetCore.OData.Routing.Attributes;
Startup.cs:
services.AddControllers().AddOData(opt => opt.Select());
Output:
Note: As you are using oData 8.0 you have to use ODataAttributeRoutingAttribute annotations either on your
controller or action as you can see the above code and screenshot.
It will resolve your problem.
In addition you could have a look more details on our official blog document here also here is the sample project

WebApi: mistmatch between ids

Given the following route
/api/Person/15
And we do a PUT to this route with the body:
{
id: 8,
name: 'Joosh'
}
The route segment value is 15 but the [FromBody] id is 8.
Right now there is something like the following in our controllers:
public Model Put(string id, [FromBody] Model model)
{
if (id != model.Id)
throw new Exception("Id mismatch!");
// ... Do normal stuff
}
Is there a "default" or DRY-ish method for doing this without assuming that it will always be as simple as parameter ID and Model.Id property?
You can achieve via custom model validation
[HttpPut("api/Person/{id}")]
public IActionResult Put(string id, [FromBody]Person person)
{
// ... Do normal stuff
return Ok();
}
public class Person
{
[ValidateId]
public string Id { get; set; }
public string Name { get; set; }
}
public sealed class ValidateId : ValidationAttribute
{
protected override ValidationResult IsValid(object id, ValidationContext validationContext)
{
var httpContextAccessor = (IHttpContextAccessor)validationContext.GetService(typeof(IHttpContextAccessor));
var routeData = httpContextAccessor.HttpContext.GetRouteData();
var idFromUrl = routeData.Values["id"];
if (id.Equals(idFromUrl))
{
return ValidationResult.Success;
}
else
{
return new ValidationResult("Id mismatch!");
}
}
}
// In the Startup class add the IHttpContextAccessor
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddHttpContextAccessor();
// ...
}
Is there a "default" or DRY-ish method for doing this without assuming that it will always be as simple as parameter ID and Model.Id property?
Custom validation logic can be implemented in an ActionFilter. Because the ActionFilter is processed after the model binding in the action execution, the model and action parameters can be used in an ActionFilter without having to read from the Request Body, or the URL. You could refer to the below working demo:
Customize ValidationFilter
public class ValidationFilter: ActionFilterAttribute
{
private readonly ILogger _logger;
public ValidationFilter(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger("ValidatePayloadTypeFilter");
}
public override void OnActionExecuting(ActionExecutingContext context)
{
var carDto = context.ActionArguments["car"] as Car;
var id = context.ActionArguments["id"];
if (Convert.ToInt32(id)!=carDto.Id)
{
context.HttpContext.Response.StatusCode = 400;
context.Result = new ContentResult()
{
Content = "Id mismatch!"
};
return;
}
base.OnActionExecuting(context);
}
}
Register this action filter in the ConfigureServices method
services.AddScoped<ValidationFilter>();
Call this action filter as a service
public class Car
{
public int Id { get; set; }
public string CarName { get; set; }
}
[ServiceFilter(typeof(ValidationFilter))]
[HttpPut("{id}")]
public Car Put(int id, [FromBody] Car car)
{
// the stuff you want
}
Reference:
https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-2.2#action-filters
https://code-maze.com/action-filters-aspnetcore/#comments
You can create your own CustomValidation and compare the values of id and model.id.
Check this link
example of custom model validation.

ASP.net Core OData String Key Is Null

I have a strange one here. I've been porting some controllers over from asp.net odata to asp.net core odata and ran into a bit of a snag with the primary keys.
In my .net framework 4.6.2 app I have a GUID as a primary key and in the .net core app I have a string as a primary key. I've been able to get most everything working EXCEPT a Get(key) method. This is my method signature:
[HttpGet]
[EnableQuery]
public async Task<IActionResult> Get([FromODataUri] string key)
{
// key is null!
}
Please Below Follow step
Install Microsoft.AspNetCore.OData from NuGet Package
StartUp.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddOData(); //This Is added for OData
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseMvc(routeBuilder =>
{
routeBuilder.EnableDependencyInjection(); //This Is added for OData
routeBuilder.Expand().Select().Count().OrderBy().Filter(); //This Is added for OData
});
}
In Api Conteroller
[HttpGet]
[EnableQuery] //This Is added for OData
public ActionResult<List<TestModel>> Get()
{
var model = new List<TestModel>();
for (int i = 1; i <= 10; i++)
{
var res = new TestModel()
{
ID = i,
Name="Test"+i,
Mobile="Test"+i,
City="Test_"+i
};
model.Add(res);
}
return model;
}
public class TestModel
{
public int ID { get; set; }
public string Name { get; set; }
public string Mobile { get; set; }
public string City { get; set; }
}
After Run Api And Check Like This

ASP.NET Core Logging With ActionFilter

I am trying to add a logging function to save to my SQL server database. I tried to create ActionFilter class and apply on one of my controller but not working.
i want to capture userid, IP address, controller and action visit and timestamp. What am i missing?
AuditAttribute Action Filter class
public class AuditAttribute : ActionFilterAttribute
{
private readonly ApplicationDbContext _db;
//Inject ApplicationDBContext
public AuditAttribute(ApplicationDbContext db)
{
_db = db;
}
IPHostEntry ipHostInfo = Dns.GetHostEntry(Dns.GetHostName());
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
//Stores the Request in an Accessible object
var request = filterContext.HttpContext.Request;
//Generate an audit
Portal_Logger audit = new Portal_Logger()
{
teacherNRIC = filterContext.HttpContext.User.Identity.Name,
IPAddress = Convert.ToString(ipHostInfo.AddressList.FirstOrDefault(address => address.AddressFamily == AddressFamily.InterNetwork)),
ControllerAccess = (string)filterContext.RouteData.Values["controller"],
Timestamp = DateTime.Now,
};
public DbSet<Portal_Logger> Portal_LoggerDBSet { get; set; }
//Store objects to database
_db.Portal_LoggerDBSet.Add(audit);
_db.SaveChanges();
base.OnActionExecuting(filterContext);
}
}
}
Home controller class
[AuditAttribute]
public class HomeController : Controller
{ ..content of controller
}
Portal_logger model class
public class Portal_Logger
{
[Key]
public int LoggerId { get; set; }
[StringLength(10)]
public string userid{ get; set; }
[StringLength(50)]
public string IPAddress { get; set; }
[StringLength(50)]
public string ControllerAccess { get; set; }
public DateTime? Timestamp { get; set; }
}
Please see the error message as attached image
Screenshot of error message
I tried and have a new issue.
enter image description here
I tried this method also not working.
[AuditActionFilter] -> Error message (AuditActionFilter is not an attribute class)
public class HomeController : Controller
{
}
I tried this method but not working still.
[AuditActionFilter] - error message -> "AuditActionFilter" is not an
public class HomeController : Controller
{
}
I have added services.AddMVC to startup.cs
services.AddMvc(options => options.Filters.Add(typeof(AuditActionFilter)));
This is the AuditActionFilter.cs
public class AuditActionFilter : IActionFilter
{
private readonly ApplicationDbContext _db;
public AuditActionFilter(ApplicationDbContext db)
{
_db = db;
}
IPHostEntry ipHostInfo = Dns.GetHostEntry(Dns.GetHostName());
public void OnActionExecuting(ActionExecutingContext filterContext)
{
var request = filterContext.HttpContext.Request;
Portal_Logger audit = new Portal_Logger()
{
teacherNRIC = filterContext.HttpContext.User.Identity.Name,
IPAddress = Convert.ToString(ipHostInfo.AddressList.FirstOrDefault(address => address.AddressFamily == AddressFamily.InterNetwork)),
ControllerAccess = (string)filterContext.RouteData.Values["controller"],
Timestamp = DateTime.Now,
};
//Store objects to database
_db.Portal_LoggerDBSet.Add(audit);
_db.SaveChanges();
}
public void OnActionExecuted(ActionExecutedContext context)
{
// do something after the action executes or leave it empty
}
}
For using AuditAttribute on the specific method or controller, you could try ServiceFilterAttribute or TypeFilterAttribute.
Here are the available two options:
Option1
Use TypeFilterAttribute.
[TypeFilter(typeof(AuditAttribute))]
public class HomeController : Controller
Option2
Use ServiceFilterAttribute
Register AuditAttribute in Startup.cs
services.AddScoped<AuditAttribute>();
Use AuditAttribute
[ServiceFilter(typeof(AuditAttribute))]
public class HomeController : Controller
The error is occurring because you have a parameter of "DbContext" on your attribute, but you are not supplying it when applying it to the controller?
public AuditAttribute(ApplicationDbContext db)
{
_db = db;
}
How to use DI in Action filters is described here:
https://www.c-sharpcorner.com/blogs/custom-filter-in-mvc-or-logging-using-mvc-filter
The article in short:
Enable for all controllers:
starup.cs:
services.AddMvc(options =>
{
options.Filters.Add(typeof(AuditAttribute));
});
Enable for only one controller:
starup.cs:
services.AddScoped<AuditAttribute>();
Next you place on the controller or action method:
[ServiceFilter(typeof(AuditAttribute))]

There is no argument given that corresponds to the required formal parameter 'options'

I'm working on my first application in .Net Core.
I'm getting this build error for some reason:
Error CS7036 There is no argument given that corresponds to the required formal parameter 'options' of 'LakeViewContext.LakeViewContext(DbContextOptions<LakeViewContext>)' LakeView
I wasn't able to find a solution through Google Search or MS documentation.
My Context class:
using LakeView.Models;
using Microsoft.EntityFrameworkCore;
namespace LakeView
{
public class LakeViewContext : DbContext
{
public LakeViewContext(DbContextOptions<LakeViewContext> options) : base(options)
{
}
public DbSet<HTMLElement> HTMLElements { get; set; }
public DbSet<CustomizedElement> CustomizedElements { get; set; }
public DbSet<TemplateFileType> TemplateFileTypes { get; set; }
public DbSet<StyleFile> StyleFiles { get; set; }
public DbSet<Template> Templates { get; set; }
public DbSet<Course> Courses { get; set; }
public DbSet<Page> Pages { get; set; }
public DbSet<HTML> HTMLs { get; set; }
public DbSet<Comment> Comments { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<CustomizedElementTemplate>()
.HasKey(s => new { s.CustomizedElementId, s.TemplateId });
base.OnModelCreating(modelBuilder);
}
}
}
Controller class:
using LakeView.Models;
using LakeView.Models.ViewModels;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;
namespace LakeView.Controllers
{
public class CoursesController : Controller
{
private LakeViewContext db = new LakeViewContext();
public IActionResult Index()
{
ICollection<Course> courses = db.Courses.ToList();
return View(courses);
}
[HttpGet]
public IActionResult CreateCourse()
{
return View("CreateCourse");
}
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult CreateCourse(CreateCourseViewModel courseVM)
{
if (ModelState.IsValid)
{
Course newCourse = new Course()
{
CourseCode = courseVM.CourseCode,
CourseTitle = courseVM.CourseTitle,
MasterOU = int.Parse(courseVM.MasterOU)
};
db.Courses.Add(newCourse);
db.SaveChanges();
return RedirectToAction("Index");
}
return View("CreateCourse", courseVM);
}
}
}
(bold text is underlined in Visual Studio with the same error
"private LakeViewContext db = new LakeViewContext();"
Startup class:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using LakeView.Models;
namespace LakeView
{
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
var connection = #"Data Source = (localdb)\MSSQLLocalDB; Database = LakeViewData; Trusted_Connection = True;";
services.AddDbContext<LakeViewContext>(options => options.UseSqlServer(connection));
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
//loggerFactory.AddConsole();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
}
}
}
LakeViewContext expects a DbContextOptions<LakeViewContext> to be passed into its constructor. However, you are calling the constructor without providing anything:
private LakeViewContext db = new LakeViewContext();
To fix the issue, you can just plug into the Dependency Injection system that you've set up. To do this, change your controller as follows:
public class CoursesController : Controller
{
private readonly LakeViewContext db;
public CoursesController(LakeVieContext db)
{
this.db = db;
}
...
The ASP.NET Core Dependency Injection system will provide you with a LakeViewContext in the constructor - Just use that.
you are trying to new up the dbcontext in your controller without passing in the options.
You should instead add a constructor to your controller and add the dbContext to your constructor so it will get injected, ie
public CoursesController(LakeViewContext dbContext)
{
db = dbContext;
}
private LakeViewContext db;
... the rest of your code
dependency injection will pass it in to you
When observing at how the classes are generated via the database first approach, I was able to fix this error with an empty constructor.
public class MyDBContext : DbContext
{
public MyDBContext()
{
}
//Rest of your code
}

Categories