I have a web api 2 application, and in my controller , I have this code :
[Queryable]
public IQueryable<Title> GetTitles()
{
return db.Titles;
}
and here is the Title entity :
public partial class Title
{
public Title()
{
this.People = new HashSet<Person>();
}
public short Id { get; set; }
public string NameT { get; set; }
public virtual ICollection<Person> People { get; set; }
}
When people query the Titles, they must get only "NameT" property. but now they get all of the properties. and yes, I know about $select, but I want another way. means even they use $select, they should not able to get "Id" property for example. if I have to bring more information, please tell me. thanks.
There are two ways to solve your problem when you use ODataController. However, they won't affect ApiController non-query part.
In that condition, you can try what Zoe suggested.
1.Ignore those properties while building your model with model builder.
builder.EntityType<Title>().Ignore(title => title.Id);
2.Add ignore member attributes on those properties.
[IgnoreDataMember]
public short Id { get; set; }
More than these, we provide support for limiting the set of allowed queries in Web API 2.2 for OData v4.0.
http://blogs.msdn.com/b/webdev/archive/2014/03/13/getting-started-with-asp-net-web-api-2-2-for-odata-v4-0.aspx
We can use attributes like Unsortable, NonFilterable, NotExpandable or NotNavigable on the properties of the types in our model, or we can configure this explicitly in the model builder.
Maybe you can have filter in your action GetTitles(), like:
[Queryable]
public IQueryable<Title> GetTitles()
{
return db.Titles.Select(t=>t.Name);
}
Use the ODataModelBuilder class as opposed to the ODataConventionModelBuilder class.
var modelBuilder = new ODataModelBuilder();
var titles = modelBuilder.EntitySet<Title>("titles");
var title = titles.EntityType;
title.HasKey(x => x.Id);
title.Ignore(x => x.Id);
title.Property(x => x.TName);
titles.HasIdLink(x => { return x.GenerateSelfLink(true); }, true);
config.Routes.MapODataRoute("odata", "odata", modelBuilder.GetEdmModel());
Related
I was looking at the demo:
Telerik's Demo
and this
Telerik's Example
I still cannot figure out how, using this technique, to pass a parameter
to the Read method, so instead of reading ALL games or products, it reads ONLY a
subset of games or products, by category ID lest say. If I change the Read method in
the server-side code to take a parameter, it never gets hit anymore,
and I cannot figure out how to pass a parameter from the transport
client-side definition definition... Any help would be highly
appreciated!
I posted the question to the Telerik's forums and got this which should work!
Telerik's Answer
In case anyone else comes along in the future and the link in the other answer is gone, or for those who don't want to download a project file and sift through it themselves, here's more detail from the answer given on the Telerik forums:
They use Kendo.DynamicLinq to "bind the request parameters."
They created a custom class that mirrors the structure of the existing DataSourceRequest class typically used in AJAX grid actions.
using System.Collections.Generic;
using Kendo.DynamicLinq;
namespace SignalRLocalHub.Models
{
public class MyDataSourceRequest
{
public int Take { get; set; }
public int Skip { get; set; }
public int Page { get; set; }
public int PageSize { get; set; }
public Filter Filter { get; set; }
public IEnumerable<Sort> Sort { get; set; }
public IEnumerable<Aggregator> Aggregates { get; set; }
}
}
In the ProductHub (SignalR hub) class, this is the Read method:
public DataSourceResult Read(MyDataSourceRequest request)
{
return productService.Read()
.ToDataSourceResult(
request.Take,
request.Skip,
request.Sort,
request.Filter,
request.Aggregates);
}
The productService.Read method it calls, for reference, looks like this:
//int take, int skip, IEnumerable<Sort> sort, Filter filter, IEnumerable<Aggregator> aggregates
public IQueryable<ProductViewModel> Read()
{
return entities.Products
.OrderBy(p => p.ProductID)
.Select(product => new ProductViewModel
{
ProductID = product.ProductID,
ProductName = product.ProductName,
UnitPrice = product.UnitPrice.HasValue ? product.UnitPrice.Value : default(decimal),
UnitsInStock = product.UnitsInStock.HasValue ? product.UnitsInStock.Value : default(short),
Discontinued = product.Discontinued
});
}
And finally, here is the grid's DataSource configuration:
.DataSource(d => d
.SignalR()
.AutoSync(true)
.Transport(tr => tr
.Promise("hubStart")
.Hub("hub")
.Client(c => c.Read("read").Create("create").Update("update").Destroy("destroy"))
.Server(s => s.Read("read").Create("create").Update("update").Destroy("destroy"))
)
.ServerFiltering(true)
.Filter(f => f.Add(m => m.UnitPrice).IsEqualTo(10))
.ServerPaging(true)
.PageSize(10)
.Schema(s => s
.Data("Data")
.Total("Total")
.Aggregates("Aggregates")
.Model(m =>
{
m.Id(e => e.ProductID);
m.Field(e => e.ProductID).Editable(false);
})
)
)
This allows sorting, paging, and filtering by any field that is part of the grid view model. So as long as your "category ID" is a property of the grid view model and the grid is configured to be able to filter by that field, this method should work.
I've been fiddling about and trying multiple things, but I'm going wrong somewhere. I tried to make my first attempt using AutoMapper as simple as possible. I'm trying to create a new Brand and save it to the database, using a CreateBrandViewModel. Some of this might look a bit fruity, but I was trying to get it to work in the simplest way possible.
Domain:
public class Brand : EntityBase
{
public virtual string Name { get; set; } //Not Nullable
public virtual bool IsActive { get; set; } // Not Nullable
public virtual Product DefaultProduct { get; set; } // Nullable
public virtual IList<Product> Products { get; set; } // Nullable
}
ViewModel:
public class CreateBrandViewModel
{
public string Name { get; set; }
public bool IsActive { get; set; }
}
Controller
this is where I've been playing about the most for a while, so it looks a bit strange now. The commented out code hasn't resolved my problem.
[HttpPost]
public ActionResult Create(CreateBrandViewModel createBrandViewModel)
{
if(ModelState.IsValid)
{
Mapper.CreateMap<Brand, CreateBrandViewModel>();
//.ForMember(
// dest => dest.Name,
// opt => opt.MapFrom(src => src.Name)
//)
//.ForMember(
// dest => dest.IsActive,
// opt => opt.MapFrom(src => src.IsActive)
//);
Mapper.Map<Brand, CreateBrandViewModel>(createBrandViewModel)
Session.SaveOrUpdate(createBrandViewModel);
return RedirectToAction("Index");
}
else
{
return View(createBrandViewModel);
}
}
Just for the record, BrandController inherits from SessionController (Ayendes way), and transactions are managed through an ActionFilter. Though thats is a bit irrelevant I think. I've tried various different ways so I have different error messages - if you can take a look at whats happening and tell me how you might expect to use it that would be great.
For reference, my fluent nhibernate mapping for Brand:
public class BrandMap : ClassMap<Brand>
{
public BrandMap()
{
Id(x => x.Id);
Map(x => x.Name)
.Not.Nullable()
.Length(50);
Map(x => x.IsActive)
.Not.Nullable();
References(x => x.DefaultProduct);
HasMany(x => x.Products);
}
}
Edit 1
I just tried the following code, but putting a breakpoint on Session.SaveOrUpdate(updatedModel) the fields are null and false, when they shouldn't be:
var brand = new Brand();
var updatedBrand = Mapper.Map<Brand, CreateBrandViewModel>(brand, createBrandViewModel);
Session.SaveOrUpdate(updatedBrand);
return RedirectToAction("Index");
}
You appear to be doing your mapping the wrong way around on your return trip from the post. The alternative syntax may help out here, try:
// setup the viewmodel -> domain model map
// (this should ideally be done at initialisation time, rather than per request)
Mapper.CreateMap<CreateBrandViewModel, Brand>();
// create our new domain object
var domainModel = new Brand();
// map the domain type to the viewmodel
Mapper.Map(createBrandViewModel, domainModel);
// now saving the correct type to the db
Session.SaveOrUpdate(domainModel);
let me know if that cracks the egg... or just egg on yer face again :-)
I am trying to extend ProDinner by adding phone numbers to Chef.
ChefInput view model:
public class ChefInput :Input
{
public string Name { get; set; }
public ChefInput()
{
PhoneNumberInputs = new List<PhoneNumberInput>(){
new PhoneNumberInput()
};}
public IList<PhoneNumberInput> PhoneNumberInputs { get; set; }
}
PhoneInput view model:
public class PhoneNumberInput :Input
{
public string Number { get; set; }
public PhoneType PhoneType { get; set; } <-- an enum in Core project
}
Chef Create.cshtml file:
#using (Html.BeginForm())
{
#Html.TextBoxFor(o => o.Name)
#Html.EditorFor(o => o.PhoneNumberInputs)
}
PhoneNumberInput.cshtml in EditorTemplate folder:
#using (Html.BeginCollectionItem("PhoneNumberInputs"))
{
#Html.DropDownListFor(m => m, new SelectList(Enum.GetNames(typeof(PreDefPhoneType))))
#Html.TextBoxFor(m => m.Number)
}
When debugging and I stop it at Create in Crudere file, the Phone collection is null.
Anyone have any ideas?
Thanks in Advance.
Joe,
You don't show your controller logic but I've got a feeling you're getting null because you're not populating the PhoneNumberInputs ViewModel. From what I can see, all you're doing is newing up the list in the model. Ensure that you fill this 'list' in your controller from the database (with the appropriate values) and i'm certain all will work as planned.
[edit] - in answer to comment. don't know what the prodinner controllers etc look like but something alsong these lines:
public ActionResult Edit(int id)
{
var viewModel = new ChefInput();
viewModel.ChefInput = _context.GetById<ChefModel>(id);
viewModel.PhoneNumberInputs = _context.All<PhoneNumberInput>();
return View(viewModel);
}
as i said, not sure of the prodinner setup, but this is what i meant.
MVC 3, EntityFramework 4.1, Database First, Razor customization:
I have an old database that sometimes uses Int16 or Char types for a field that must appear as a CheckBox in the MVC _CreateOrEdit.cshtml View. If it is an Int, 1=true and 0=false. If it is a Char, "Y"=true and "N"=false. This is too much for the Entity Framework to convert automatically. For the Details View, I can use:
#Html.CheckBox("SampleChkInt", Model.SampleChkInt==1?true:false)
But this won't work in place of EditorFor in the _CreateOrEdit.cshtml View.
How to do this? I was thinking of a custom HtmlHelper, but the examples I've found don't show me how to tell EntityFramework to update the database properly. There are still other such customizations that I might like to do, where the MVC View does not match the database cleanly enough for EntityFramework to do an update. Answering this question would be a good example. I am working on a sample project, using the following automatically generated (so I can't make changes to it) model class:
namespace AaWeb.Models
{
using System;
using System.Collections.Generic;
public partial class Sample
{
public int SampleId { get; set; }
public Nullable<bool> SampleChkBit { get; set; }
public Nullable<short> SampleChkInt { get; set; }
public Nullable<System.DateTime> SampleDate { get; set; }
public string SampleHtml { get; set; }
public Nullable<int> SampleInt { get; set; }
public Nullable<short> SampleYesNo { get; set; }
public string Title { get; set; }
public byte[] ConcurrencyToken { get; set; }
}
}
I figured it out. Do not need a model binder or Html Helper extension:
In _CreateOrEdit.cshtml, I made up a new name SampleChkIntBool for the checkbox, and set it according to the value of the model SampleChkInt:
#Html.CheckBox("SampleChkIntBool", Model == null ? false : ( Model.SampleChkInt == 1 ? true : false ), new { #value = "true" })
Then, in the [HttpPost] Create and Edit methods of the Sample.Controller, I use Request["SampleChkIntBool"] to get the value of SampleChkIntBool and use it to set the model SampleChkInt before saving:
string value = Request["SampleChkIntBool"];
// #Html.CheckBox always generates a hidden field of same name and value false after checkbox,
// so that something is always returned, even if the checkbox is not checked.
// Because of this, the returned string is "true,false" if checked, and I only look at the first value.
if (value.Substring(0, 4) == "true") { sample.SampleChkInt = 1; } else { sample.SampleChkInt = 0; }
I believe a custom model binder would be in order here to handle the various mappings to your model.
ASP.NET MVC Model Binder for Generic Type
etc
etc
Here is the way to go from checkbox to database, without the special code in the controller:
// The following statement added to the Application_Start method of Global.asax.cs is what makes this class apply to a specific entity:
// ModelBinders.Binders.Add(typeof(AaWeb.Models.Sample), new AaWeb.Models.SampleBinder());
// There are two ways to do this, choose one:
// 1. Declare a class that extends IModelBinder, and supply all values of the entity (a big bother).
// 2. Declare a class extending DefaultModelBinder, and check for and supply only the exceptions (much better).
// This must supply all values of the entity:
//public class SampleBinder : IModelBinder
//{
// public object BindModel(ControllerContext cc, ModelBindingContext mbc)
// {
// Sample samp = new Sample();
// samp.SampleId = System.Convert.ToInt32(cc.HttpContext.Request.Form["SampleId"]);
// // Continue to specify all of the rest of the values of the Sample entity from the form, as done in the above statement.
// // ...
// return samp;
// }
//}
// This must check the property names and supply appropriate values from the FormCollection.
// The base.BindProperty must be executed at the end, to make sure everything not specified is take care of.
public class SampleBinder : DefaultModelBinder
{
protected override void BindProperty( ControllerContext cc, ModelBindingContext mbc, System.ComponentModel.PropertyDescriptor pd)
{
if (pd.Name == "SampleChkInt")
{
// This converts the "true" or "false" of a checkbox to an integer 1 or 0 for the database.
pd.SetValue(mbc.Model, (Nullable<Int16>)(cc.HttpContext.Request.Form["SampleChkIntBool"].Substring(0, 4) == "true" ? 1 : 0));
// To do the same in the reverse direction, from database to view, use pd.GetValue(Sample object).
return;
}
// Need the following to get all of the values not specified in this BindProperty method:
base.BindProperty(cc, mbc, pd);
}
}
This might be a stupid question! (n00b to AutoMapper and time-short!)
I want to use AutoMapper to map from EF4 entities to ViewModel classes.
1) If I call
CreateMap<ModelClass, ViewModelClass>()
then do I also need to call
CreateMap<ViewModelClass, ModelClass>()
to perform the reverse?
2) If two classes have the same property names, then do I need a CreateMap statement at all, or is this just for "specific/custom" mappings?
For the info of the people who stumble upon this question. There appears to be now a built-in way to achieve a reverse mapping by adding a .ReverseMap() call at the end of your CreateMap() configuration chain.
In AutoMapper you have a Source type and a Destination type. So you will be able to map between this Source type and Destination type only if you have a corresponding CreateMap. So to answer your questions:
You don't need to define the reverse mapping. You have to do it only if you intend to map back.
Yes, you need to call CreateMap to indicate that those types are mappable otherwise an exception will be thrown when you call Map<TSource, TDest> telling you that a mapping doesn't exist between the source and destination type.
I've used an extension method do mapping both ways
public static IMappingExpression<TDestination, TSource> BothWays<TSource, TDestination>
(this IMappingExpression<TSource, TDestination> mappingExpression)
{
return Mapper.CreateMap<TDestination, TSource>();
}
usage:
CreateMap<Source, Dest>().BothWays();
Yes, or you can call CreateMap<ModelClass, ViewModelClass>().ReverseMap().
If two classes have same Member(Property,Field,GetMethod()), you needn't call CreateMap<TSrc,TDest>. Actually, if every member in TDest are all exist in TSrc, you needn't call CreateMap<TSrc,TDest>. The following code works.
class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
class Person2
{
public string Name { get; set; }
public int? Age { get; set; }
public DateTime BirthTime { get; set; }
}
public class NormalProfile : Profile
{
public NormalProfile()
{
//CreateMap<Person2, Person>();//
}
}
var cfg = new MapperConfiguration(c =>
{
c.AddProfile<NormalProfile>();
});
//cfg.AssertConfigurationIsValid();
var mapper = cfg.CreateMapper();
var s3 = mapper.Map<Person>(new Person2 { Name = "Person2" });