Is it possible to render View based on OData query? - asp.net-core

I've got Asp.Net Core application and there is an abstract controller.
I want to create a method, which will allow me to render list of entities as PartialView.
I've made it like this:
Should return PartialView with list of entities
[HttpGet]
[EnableQuery()]
public async Task<IActionResult> _List()
{
var result = _context.GetQueryByType<T>(); //returns DbSet<T> of whole table
return PartialView(await result.ToListAsync());
}
Example PartialView
#model IEnumerable<SomeClass>
<table class="table table-sm table-striped table-hover">
...
</table>
I want to call my method like this:
http://localhost:32769/SomeController/_List?$filter=id%20eq%2009515a38-2a1a-4a53-a4f8-e91e4dbd870b
And get filtered List view.
But anyway I get only whole table data.
The only solution for me is split this logic into 2 methods:
Get filtered Json data via standard Odata methods like:
http://localhost:32769/odata/SomeClass?$filter=ReleaseId%20eq%2011f28258-48cb-4c82-85e0-822850fd1f5c
Pass this data to method:
[HttpPost]
public IActionResult _List([FromBody] IEnumerable<T> entities)
{
return PartialView(entities);
}
I don't like this solution. Is there any possibility to filter my view data using OData queries?

Thx to ChristophLütjen.
.ApplyTo() is the solution.
Finally, working method looks like:
[HttpGet]
[EnableQuery]
public async Task<IActionResult> _List(ODataQueryOptions<T> queryOptions)
{
var result= (IQueryable<T>)queryOptions.ApplyTo(_context.GetQueryByType<T>());
return PartialView(await result.ToListAsync());
}
Also, it's very important to use ODataQueryOptions<T>, not ODataQueryOptions.
If you will use not common class, you will get an error, that method should return IEnumerable<T>, but not IActionResult.
Here is some documentation. Just want to pin it to the answer.
https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnet.odata.query.odataqueryoptions?view=odata-aspnetcore-7.0
Hope, that this info will be usefull for someone else.
upd:
Also I've found out, that it's not perfect soulution, if you want to use $expand method in your OData queries.
If you'll try to get type T of expanded query, you'll face the problem of SelectAllAndExpand type.
In this case this is the solution, I think it's not very beatiful and perfect, but it works:
[HttpGet]
[EnableQuery]
public IActionResult _List(ODataQueryOptions<T> queryOptions)
{
var validationSettings = new ODataValidationSettings
{
AllowedQueryOptions = AllowedQueryOptions.All,
AllowedFunctions = AllowedFunctions.All,
};
queryOptions.Validate(validationSettings);
IQueryable resultSet = queryOptions.ApplyTo(_context.GetQueryByType<T>(), new ODataQuerySettings());
List<T> resultList = new List<T>();
foreach (var item in resultSet)
{
if (item is T)
{
resultList.Add((T)item);
}
else if (item.GetType().Name == "SelectAllAndExpand`1")
{
var entityProperty = item.GetType().GetProperty("Instance");
resultList.Add((T)entityProperty.GetValue(item));
}
}
return PartialView(resultList as IEnumerable<T>);
}
Found it here: https://github.com/OData/WebApi/issues/1441

Related

Can I change my response data in OutputFormatter in ASP.NET Core 3.1

I'm trying to create a simple feature to make the first action act like the second one.
public IActionResult GetMessage()
{
return "message";
}
public IActionResult GetMessageDataModel()
{
return new MessageDataModel("message");
}
First idea came to my mind was to extend SystemTextJsonOutputFormater, and wrap context.Object with my data model in WriteResponseBodyAsync, but the action is marked sealed.
Then I tried to override WriteAsync but context.Object doesn't have protected setter, either.
Is there anyway I can achieve this by manipulating OutputFormatter?
Or I have another option instead of a custom OutputFormatter?
for some reason they prefer every response in a same format like {"return":"some message I write.","code":1}, hence I want this feature to achieve this instead of creating MessageDataModel every time.
Based on your description and requirement, it seems that you'd like to generate unified-format data globally instead of achieving it in each action's code logic. To achieve it, you can try to implement it in action filter, like below.
public class MyCustomFilter : Attribute, IActionFilter
{
public void OnActionExecuted(ActionExecutedContext context)
{
// implement code logic here
// based on your actual scenario
// get original message
// generate new instance of MessageDataModel
//example:
var mes = context.Result as JsonResult;
var model = new MessageDataModel
{
Code = 1,
Return = mes.Value.ToString()
};
context.Result = new JsonResult(model);
}
Apply it on specific action(s)
[MyCustomFilter]
public IActionResult GetMessage()
{
return Json("message");
}

Asp.net core Custom routing

I am trying to implement custom routing on an asp.net core application.
The desired result is the following:
http://Site_URL/MyController/Action/{Entity_SEO_Name}/
Entity_SEO_Name parameter will be a unique value saved into the database that it is going to help me identify the id of the entity that I am trying to display.
In order to achieve that I have implemented a custom route:
routes.MapMyCustomRoute(
name: "DoctorDetails",
template: " {controller=MyController}/{action=TestRoute}/{name?}");
public class MyTemplateRoute : TemplateRoute
{
public override async Task RouteAsync(RouteContext context)
{
//context.RouteData.Values are always empty. Here is the problem.
var seo_name = context.RouteData.Values["Entity_SEO_Name"];
int entityId = 0;
if (seo_name != null)
{
entityId = GetEntityIdFromDB(seo_name);
}
//Here i need to have the id and pass it to controller
context.RouteData.Values["id"] = entityId;
await base.RouteAsync(context);
}
}
My controller actionresult:
public ActionResult TestRoute(int id)
{
var entity = GetEntityById(id);
return Content("");
}
The problem with this approach is that the context.RouteData.Values are always empty.
Any ideas on how to move forward with this one ?
Your solution too complicated. You can have route template like
template: "{controller=Home}/{action=Index}/{seo?}"
and controller action just like
public ActionResult TestRoute(string seo)
{
var entity = GetEntityBySeo(seo);
return Content("");
}
It is enough, asp.net mvc is smart enough to bind seo variable to the parameter from url path.

Looking for best practice to handle conditional logic inside controller actions in asp.net mvc

Currently I am looking for best practice in handling conditions inside the controller actions in asp.net mvc. For example -
public ActionResult Edit(int Id = 0)
{
var Item = _todoListItemsRepository.Find(Id);
**if (Item == null)
return View("NotFound");
if (!Item.IsAuthorized())
return View("NotValidOwner");**
return View("Edit", Item);
}
The above two conditions marked in bold is used in other actions inside the controller. So, in order not to repeat these conditions in all the actions. I have used the below approach.
[HttpGet]
[Authorize]
[ModelStatusActionFilter]
public ActionResult Edit(int Id = 0)
{
var Item = _todoListItemsRepository.Find(Id);
return View("Edit", Item);
}
public class ModelStatusActionFilterAttribute : ActionFilterAttribute
{
private readonly ITodoListItemsRepository _todoListItemsRepository;
public ModelStatusActionFilterAttribute()
: this(new TodoListItemsRepository())
{
}
public ModelStatusActionFilterAttribute(ITodoListItemsRepository todoListItemsRepository)
{
_todoListItemsRepository = todoListItemsRepository;
}
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
try
{
var Id = Convert.ToInt32(filterContext.RouteData.Values["Id"]);
var Item = _todoListItemsRepository.Find(Id);
if (Item == null)
{
filterContext.Result = new ViewResult() { ViewName = "NotFound" };
}
else if (!Item.IsAuthorized())
{
filterContext.Result = new ViewResult() { ViewName = "NotValidOwner" };
}
}
catch
{
}
}
}
I am unsure if this is the best practice in handling such scenarios. So, could someone please advise ?
Regards,
Ram
usually you don't use action filter for so-called business logic of your web application - this is what the controllers are for. Action filter are rather for the whole stuff which is external to the actual logic - common case is logging, performance measurement, checking if user is authenticated / authorized (I don't think this is your case, although you call IsAuthorized method on the "Item").
Reducing code is generally good thing but in this case, I don't think putting the logic to action is a good way, because you;ve actually made it a bit unreadable, and unreadable code is in my opinon much worse than repeated code.
Also, specifically in your case, for all valid items you actually call the _todoListItemsRepository.Find() twice (for each valid item), which might be costly if this is some webservice call or db lookup.
If the code is just repeated throughout the actions, make a method out of it like:
private View ValidateItem(Item) {
if (Item == null)
return View("NotFound");
if (!Item.IsAuthorized())
return View("NotValidOwner");
return null; }

Passing viewModel when redirecting in ASP.NET MVC 4

I have below statement:
return Redirect(this.Request.UrlReferrer.AbsolutePath);
this redirects to the caller view. It is working ok, but now I need to return a view model when redirecting, something like this (it's wrong):
return Redirect(this.Request.UrlReferrer.AbsolutePath(item));
So how can I achieve this?
I want to do this because I have a jqrid in which one of its columns offers some actions, edit and delete the row. So if user clicks on edit, i retrieve some data from the id passed to the database. Then once I get this data, I populate a view model in order to update some textboxes in the view, so I need to pass the view model when redirecting.
Below my code in the controller:
public ActionResult Edit(int id)
{
ItemViewModel item = new ItemViewModel();
using (DBContext context = new DBContext())
{
Items itemToModify = context.Items.Single(i=> i.ItemId == id);
item.Desc = itemToModify.Desc;
item.Name = itemToModify.Name;
}
return Redirect(this.Request.UrlReferrer.AbsolutePath, item); <-- how to do this
}
You can use TempData like
In your controller
public ActionResult Action1()
{
ItemViewModel item = new ItemViewModel();
TempData["item"] = item;
return Redirect("Action2");
}
public ActionResult Action2()
{
ItemViewModel item = (ItemViewModel)TempData["item"];
//Your Code
}

ASP.NET MVC , proper way to persist dynamic selectList?

I am learning on MVC4+EF 5.0 project, i using VS2012 default template to create blank project and scaffolding the database to *.edmx model, and a edit view which use for edit a staff working on which company.
I experience a problem is maintenance the SelectList in edit view(Dropdown) when user fail input and return to it.
The DropDownList bind the ListItem from controller:
Edit.cshtml
#Html.DropDownListFor(model => model.CompanyID, (SelectList)ViewData["CompanySelectList"])
MemberController.cs
[HttpGet]
public ActionResult Edit(int SelectedCompanyID = 0, int StaffID = 0)
{
IQueryable<company_benefit> companys = from c in db.company where c.ID.Equals(CompanyID) select c ;
ViewData["CompanySelectList"] = new SelectList(companys, "ID", "Name", SelectedCompanyID);
staff s = db.staff.Find(StaffID);
if (s == null)
{
return HttpNotFound();
}
return View(s);
}
[HttpPost]
public ActionResult Edit(staff s)
{
if (ModelState.IsValid)
{
db.Entry(s).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index"); //Edit Success
}
return View(s); //Edit Fail
}
If someone submit the form with invalid data resulting fail input, It will return the view.
However, the SelectList is bind from ViewData, so the ViewData will gone when the page is loaded, and it is behavior of viewdata ,and i change to TempData is not help too.
So do i need build the SelectList again when Post to Edit Action?
I concern to use session to store that, but afriad to break to MVC design pattern.
My english is not good, sorry for confusion.
Thanks you.
A quick solution is In your http post method for edit again create your view data
[HttpPost]
public ActionResult Edit(staff s)
{
if (ModelState.IsValid)
{
db.Entry(s).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index"); //Edit Success
}
IQueryable<company_benefit> companys = from c in db.company where c.ID.Equals(CompanyID) select c ;
ViewData["CompanySelectList"] = new SelectList(companys, "ID", "Name", SelectedCompanyID);
return View(s); //Edit Fail
}
What you are doing is basically saying that when you return from your edit view to the server then server should rebuild view data and call the same view so it can populate the list.
There is a better way where you can create a model which includes both your current model and a list<companys> companies = new list<companys>(); and then populate it again from database. Again the concept is same just using strongly typed model.