Attributes added by custom ModelMetadataProvider are not being considered in model-binding - c#

Disclaimer: this is a long one. Unreasonable knowledge of ASP.NET Core MVC internals is almost certainly required. Here be dragons.
Background
I am trying to implement a method for augmenting type metadata in ASP.NET MVC Core.
The reason I want to do this is that my data models are used by multiple projects, so I've shared them by placing them in a NuGet package:
// defined in NuGet package
public class MyModel
{
public int MyProperty { get; set; }
public MyNestedModel NestedModel { get; set; }
}
public class MyNestedModel
{
public bool NestedProperty { get; set; }
}
^ Listing 1
However, some of the projects will need to apply additional metadata to the model types - for example, in the case of an ASP.NET Core project, these models will be used as inputs and therefore participate in model-binding, so they require FromQuery, FromHeader etc. attributes applied. I obviously cannot do this in the package, as different consumer projects will need to apply different attributes depending on their use cases.
The simplest and mostly guaranteed-to-work way is to modify all model properties to be virtual and then have subclasses that override those properties as necessary to add attributes:
// defined in NuGet package
public class MyModel
{
public virtual int MyProperty { get; set; }
public virtual MyNestedModel NestedModel { get; set; }
}
public class MyNestedModel
{
public virtual bool NestedProperty { get; set; }
}
// defined in ASP.NET MVC project consuming above package
public class MyViewModel : MyModel
{
[FromQuery(Name = "foo")]
public override int MyProperty { get; set; }
public new // can't use override, as type is different
MyNestedViewModel NestedModel { get; set; }
}
[Bind(Prefix = "")]
public class MyNestedViewModel : MyNestedModel
{
[FromQuery(Name = "bar")]
public override bool NestedProperty { get; set; }
}
^ Listing 2
I don't want to do this, because in every model that has a child model property, that child model has to be subclassed, and then it cannot be used as an override but has to be re-declared with new - and the semantics of new won't work for me here. Also, I don't really intend for the model types to be subclassed.
I'm aware of ModelMetadataTypeAttribute but the intended usage of that attribute is on the model type to be augmented, not the type doing the augmenting. Since in my case there's no way to know ahead of time what the actual metadata types will be (as they're defined in the consuming projects), I cannot use ModelMetadataTypeAttribute. Subclasses and partial classes will also not work - ModelMetadataTypeAttribute only applies to the class specified, partial cannot span across assemblies.
Solution
Essentially, a "reverse" version of ModelMetadataTypeAttribute that is applied on the type doing the augmenting, pointing back to the model type to be augmented:
// defined in ASP.NET MVC project consuming package from Listing 1
[ReverseModelMetadataType(typeof(MyModel))]
public class MyModelMetadata
{
[FromQuery(Name = "foo")]
public int MyProperty { get; set; }
public MyNestedModel NestedModel { get; set; }
}
[ReverseModelMetadataType(typeof(NestedModel))]
[Bind(Prefix = "")]
public class NestedModelMetadata
{
[FromQuery(Name = "bar")]
public bool NestedProperty { get; set; }
}
^ Listing 3
The intention is that at runtime, the model-binding infrastructure will pick up that MyModel.MyProperty should be bound using [FromQuery(Name = "foo")], i.e. from the query string as a variable named foo. Similarly, MyModel.NestedModel.NestedProperty should be simply bound as bar from the query string.
In order to make this work I've implemented a custom ModelMetadataProvider that inherits DefaultModelMetadataProvider and overrides the CreatePropertyDetails method:
// defined in same ASP.NET Core MVC project as Listing 3
public class MyModelMetadataProvider : DefaultModelMetadataProvider
{
protected override DefaultMetadataDetails[] CreatePropertyDetails(ModelMetadataIdentity key)
{
var defaultPropertyDetails = base.CreatePropertyDetails(key);
// check if the key.ModelType should be augmented
// if so, mutate the relevant element(s) of the defaultPropertyDetails array to do the augmentation
return defaultPropertyDetails;
}
}
The mutation part effectively changes the relevant DefaultMetadataDetails.ModelAttributes member to have the additional attributes applied to the properties of the type(s) decorated with ReverseModelMetadataTypeAttribute. I then get ASP.NET MVC Core to use my provider instead of its DefaultModelMetadataProvider via the following in Startup.cs#ConfigureServices:
services.AddSingleton<IModelMetadataProvider, MyModelMetadataProvider>();
I have verified that provider is registered and the mutation code works correctly: MyModelMetadataProvider.CreatePropertyDetails is hit and the defaultPropertyDetails returned do contain the extra attributes applied by my *ModelMetadata classes from Listing 3.
Problem
Model binding does not work: ASP.NET Core MVC essentially behaves as if the attributes added via Listing 3 do not exist, and I don't know why. As far as I'm aware, the results of the calls to the various IModelMetadataProvider methods are cached then used for all subsequent lookup of metadata that's required, including for the purposes of binding models.
I'm hoping that somebody can provide some advice on how to get this to work, before I have to go through the pain of stepping through the ASP.NET Core source myself.

Related

Controller accepting different models

I want to accept different model type from body based on query param value.
Example:
[HttpGet]
[Route("GetSystemdetails")]
public string Getdeatils([FromBody] SystemDetail sysdetails, string type)
{
//some code here
string details = getdetails(sysdetails);
}
// abc model
public class abc
{
public int UserID { get; set; }
public string Email { get; set; }
}
//xyz model
public class xyz
{
public int xyzid { get; set; }
public string systemval { get; set; }
public string snum { get; set; }
}
type abc and xyz will have it's own model. So based on type I receive in query param I wanted to pick the model and proceed.
Sample url:
localhost/GetSystemdetails/type=abc
localhost/GetSystemdetails/type=xyz
I thought of creating a new model SystemDetail which holds these two models(xyz and abc) and based on system pick them.
I wanted to know what are possible ways to achieve this kind of requirements without creating multiple methods in controller(I don't want to change the format of the URL).
That's not something that's supported out of the box. Your linked solution is probably the closest you'll get to that.
ASP.NET Core is not supposed to take values of the parameters into account when routing, except for validation.
There are several possible ways to do so
Having multiple model objects
As in the link you provided, you can declare multiple model objects. The site has given the example of
public class PostUserGCM
{
public User User { get; set; }
public GCM GCM { get; set; }
}
but you can use your own examples.
Base model
Your models can inherit from some base model. If you only need a single model at a time and they share some similarities, then you could just create a base model which the other two are inheriting from, be agnostic at implementation time and your use cases will mainly differ on instantiation inside the controller, while some service methods could handle other differences.

serialized property to complex type in dto with linq and automapper

I'm having a hard time figuring something out that seems as a "easy" problem.
I'm working with Microsoft Azure mobile apps .Net backend, a MSSQL database, Entity Framework code-first and AutoMapper.
So i have the following objects:
public class Route
{
public string Id { get; set; }
[...] //some other properties
public string SerializedGoogleRoute { get; set; }
}
public class DtoRoute
{
public string Id { get; set; }
[...]
public DtoGoogleRoute GoogleRoute { get; set; }
}
public class DtoGoogleRoute
{
[...] //only strings, ints,...
}
So what I want to do is: In the database save the GoogleRoute as a serialized string because it consists of many properties and I don't need them in different columns - I just want it as a serialized string in one column on the route entity.
When the Route object is projected to the DtoRoute object I want the GoogleRoute to be serialized and vice versa.
Because I'm working with LINQ / queryables I am limited to a few AutoMapper mapping options (see AutoMapper wiki). And with none of these I can't get it to work.
The problems I'm facing/what I tried:
I can't serialize/deserialize the string to the DtoGoogleRoute on mapping (with MapFrom or ConstructProjectionUsing) because LINQ obviously cannot transform the JsonConvert.Serialize/Deserialize methods to SQL statements.
I tried having a DtoGoogleRoute property in the Route object and a string property in the DtoRoute object with getters/setters doing the (de)serialization. This works almost perfectly in a custom API controller but because of the OData query filter the azure mobile app .Net backend uses in the tablecontrollers again only the serialized string property gets returned to the client (because OData/LINQ does not know of the other property).
Another option was making a complex type out of DtoGoogleRoute with Entity Framework - this works fine but not with AutoMapper because AutoMapper can't handle complex types.
For now I'm working with a custom API controller and this works. But it would be better to use the tablecontrollers because they support offline sync.
I can't imagine such a simple thing (at least I thought it was a simple thing) can't be done or is so hard to do. But maybe the problem is all the components (tablecontroller, OData, LINQ, EF, AutoMapper) involved.
I would really be thankful if someone could help.
[EDIT]: I think the fact that it works with a normal api controller and not with a tablecontroller has something to do with OData. I tried putting the same code in a tablecontroller method and in an API controller method. when calling the API controller method I can see on the server that it just calls this function and returns all the right properties to the client (checked with fiddler). But when calling the tablecontroller method the tablecontroller method "rewrites" the URL to a OData URL --> I think this is because of some of the EnableQuery or other OData attributes. Because here (although not AutoMapper but it seems like a similar project from Microsoft) it says that the EnableQuery attribute is called twice - also when the request leaves the server. And I think it cuts of the GoogleRoute property because it does not know about this property in the OData metadata or something like that.
You can achieve it like this -
internal class RouteToDtoConverter : TypeConverter<Route, DtoRoute>
{
protected override DtoRoute ConvertCore(Route source)
{
return new DtoRoute
{
Id = source.Id,
GoogleRoute = JsonConvert.DeserializeObject<DtoGoogleRoute>(source.SerializedGoogleRoute)
};
}
}
internal class DtoToRouteConverter : TypeConverter<DtoRoute, Route>
{
protected override Route ConvertCore(DtoRoute source)
{
return new Route
{
Id = source.Id,
SerializedGoogleRoute = JsonConvert.SerializeObject(source.GoogleRoute)
};
}
}
public class Route
{
public string Id { get; set; }
public string SerializedGoogleRoute { get; set; }
}
public class DtoRoute
{
public string Id { get; set; }
public DtoGoogleRoute GoogleRoute { get; set; }
}
public class DtoGoogleRoute
{
public int MyProperty { get; set; }
public int MyProperty2 { get; set; }
}
AutoMapper.Mapper.CreateMap<Route, DtoRoute>()
.ConvertUsing(new RouteToDtoConverter());
AutoMapper.Mapper.CreateMap<DtoRoute, Route>()
.ConvertUsing(new DtoToRouteConverter());
var res = Mapper.Map<DtoRoute>(new Route
{
Id = "101",
SerializedGoogleRoute = "{'MyProperty':'90','MyProperty2':'09'}"
});
var org = Mapper.Map<Route>(res); //pass

ODataConventionModelBuilder with inherited entities

I've an WebAPI OData v3 interface using ODataConventionModelBuilder. It contains some entities which are inherited, and also a model which has a collection of the abstract object:
public abstract class BaseObject
{
[Key]
public int Id { get; set; }
[ForeignKey("Object3")]
public int? ParentId { get; set; }
public virtual Object3 Parent { get; set; }
}
public class Object1: BaseObject
{
}
public class Object2: BaseObject
{
}
public class Object3
{
[Key]
public int Id { get; set; }
public ICollection<BaseObject> MyObjects { get; set; }
}
I'm calling the interface using Breeze with client side metadata, using expand:
http://example.com/api/Object3?$expand=MyObjects
The server response looks like this:
{
"odata.type":"MyNamespace.Object1",
"odata.id":"http://example.com/api/BaseObject(1)",
"Parent#odata.navigationLinkUrl":"http://example.com/api/BaseObject(1)/Parent",
"Id":1,
"ParentId":1
}
Breeze now recognizes this as an entity of type Object1. But if I modify the entity and save the changes it makes a POST request to http://example.com/api/BaseObject(1). To being able to handle the different concrete types I need the POST request to go to the specific controller http://example.com/api/Object(1).
What do I need to change so that Breeze makes to update POST calls to the concrete controller and not the controller of the base object?
UPDATE: After inspecting the Breeze source code, it seems like Breeze uses the odata.id as URI for the POST request. Is it somehow possible to have the OData API return the URI for the concrete object as odata.id instead of the base object?
I got this working with a nasty hack by removing extraMetadata from all entities before saving with breeze:
var entities = manager.getEntities(null, breeze.EntityState.Modified);
for (var i = 0; i < entities.length; i++) {
delete entities[i].entityAspect.extraMetadata;
}
It there are no extraMetadata (which contains the odata.id) are available, breeze calculates the URI to the controller of the concrete model.
I don't know if there's a better solution available, that the OData API sends the correct odata.id in the first place.

Entity Framework Code First, nonempty setter or getter?

I am working with an EF Code First project, and all is well. I have a simple Class, Customer. In my Customer Class I have a field I want to encrypt (Yes, I know I can encrypt at the DB level but requirements dictate I encrypt at the Domain/Code level), so I am hoping that I can do something like the following:
public class Customer
{
public int CustomerID { get; set; }
public string FieldToEncrypt { get; set { _FieldToEncrypt = MyEncryptionFunction.Encrypt(); } }
}
However, I assume that if the setter has a definition, entity framework code first may ignore that property when generating the schema. So my question is, is there a way to do EF Code First with provided getters/setters, or should I move this functionality into a constructor? Should I override one of the methods/events that happens when the Context is saving, instead?
EDIT ********************
As a note, I am using DataService to transmit the data over an OData protocol service. This automatically generates insert/update/select methods. Some of the suggestions require creating a second property, but the DataService class does not seem to pass through NotMapped properties. This throws a bit of a kink into my earlier question.
public class Customer
{
public int CustomerID { get; set; }
public string EncryptedField { get; private set; }
[NotMapped]
public string Field
{
get { return MyEncryptionFunction.Decrypt(EncryptedField); }
set { EncryptedField = MyEncryptionFunction.Encrypt(value); }
}
}

Passing DTO to my ViewModels constructor to map properties

In my solution I have two projects.
Project 1 (Core)
Mapping SQL to DTO using Dapper
Project 2 (WebUI - ASP.NET MVC 4)
Here I use a ViewModel per View.
Examples of a Controller
[HttpGet]
public ActionResult Edit(int id)
{
// Get my ProductDto in Core
var product = Using<ProductService>().Single(id);
var vm = new ProductFormModel(product);
return View(vm);
}
Examples of a ViewModel
public class ProductFormModel : BaseViewModel, ICreateProductCommand
{
public int ProductId { get; set; }
public int ProductGroupId { get; set; }
public string ArtNo { get; set; }
public bool IsDefault { get; set; }
public string Description { get; set; }
public string Specification { get; set; }
public string Unit { get; set; }
public string Account { get; set; }
public decimal NetPrice { get; set; }
public ProductFormModel(int productGroupId)
{
this.ProductGroupId = productGroupId;
}
public ProductFormModel(ProductDto dto)
{
this.ProductId = dto.ProductId;
this.ProductGroupId = dto.ProductGroupId;
this.ArtNo = dto.ArtNo;
this.IsDefault = dto.IsDefault;
this.Description = dto.Description;
this.Specification = dto.Specification;
this.Unit = dto.Unit;
this.Account = dto.Account;
this.NetPrice = dto.NetPrice;
}
public ProductFormModel()
{
}
}
Explanation:
I'll get my DTOs in my controller using a service class in the project (Core).
Then i create my ViewModel and pass the DTO to the constructor in ViewModel.
I can also use this view to add a new Product because my ViewModel can take a empty constructor.
Does anyone have experience of this. I wonder if I am in this way will have problems in the future as the project gets bigger?
I know this has nothing to do with Dapper. But I would still like a good way to explain my solution.
I think you will be fine using your current approach. More importantly, start out like this and refactor if you start to encounter problems related to your object mapping code (instead of thinking too much about it beforehand).
Another way to organize mapping logic that I use sometimes is to employ extension methods. That way, the mapping code is kept separate from the view model itself. Something like:
public static class ProductMappingExtensions
{
public static ProductFormModel ToViewModel(this ProductDto dto)
{
// Mapping code goes here
}
}
// Usage:
var viewModel = dto.ToViewModel();
Yet another approach would be to use a mapping framework like AutoMapper - this is a good fit in particular if your mapping logic is simple (lots of 1:1 mappings between properties).
But again, start simple and refactor when you need to.
I realize that this is a little bit late answer, but maybe it will help someone in the future.
This way of doing mapping between objects breaks the 'S' of the SOLID principles, because the responsibility of the ViewModel is to prepare data in its properties to be ready to use by the view and nothing else, therefore, mapping objects should not be on it's responsibilities.
Another drawback of this way is that it also breaks the 'Loose Coupling' OO principle as you ViewModel is strongly coupled with your DTO.
I think, even when we are in the very first step of the project, there are some importants OO principles that we should never break, so using mapper classes, either auto (AutoMapper, ValueInjecter ...) or manual, is definitely better.

Categories