.net core custom model binding - asp.net-core-mvc

I have a model such as
public class MyModel
{
public MyObject myObject {get;set;}
}
public class MyObject
{
public string FirstName {get;set;}
public string LastName {get;set;}
}
With out using a custom model binder everything works great. I am trying to implement a model binder and not getting anywhere -- the resources that I have come from are
https://www.youtube.com/watch?v=qDRORgoZxZU (returns null model to the controller)
http://intellitect.com/custom-model-binding-in-asp-net-core-1-0/ (controller dies on the constructor)
http://hotzblog.com/asp-net-vnext-defaultmodelbinder-and-automatic-viewmodel-string-trim/ (can not even find MutableObjectModelBinder in the .net-core namespace)
Ideally what I want is to track which properties where set by the ModelBinder.
public class MyObject
{
public string FirstName {get;set;}
public string LastName {get;set;}
public List<String> ModifiedProperties {get;set;}
}
when the object is created by the ModelBinder for each property that is being set it adds it to the ModifiedProperties list.

This is solution. You need to implement IModelBinderProvider and IModelBinder
public class EntityFrameworkModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
//We only want to invoke the CustomeBinder on IBaseEntity classes
if (context.Metadata.ContainerType != null && context.Metadata.ContainerType.GetInterfaces().Contains(typeof(SurgeOne.Core.IBaseEntity)))
{
//We only create the custom binder on value types. E.g. string, guid, etc
if (context.Metadata.ModelType.GetTypeInfo().IsValueType ||
context.Metadata.ModelType == typeof(System.String))
{
return new EntityFrameworkModelBinder();
}
}
return null;
}
}
And IModelBinder
public class EntityFrameworkModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
//Get the value
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (valueProviderResult == ValueProviderResult.None)
{
// no entry
return Task.CompletedTask;
}
//Set the value -- not sure what this does
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
//Set the value -- this has to match the property type.
System.ComponentModel.TypeConverter typeConverter = System.ComponentModel.TypeDescriptor.GetConverter(bindingContext.ModelType);
object propValue = typeConverter.ConvertFromString(valueProviderResult.FirstValue);
bindingContext.Result = ModelBindingResult.Success(propValue);
//Code to track changes.
return Task.CompletedTask;
} //BindModelAsync
}

Related

ASPNET Boilerplate, extending audit log

I am trying to extend the AuditLog entity in ASPNETBOILETPLATE framework in order to add some new properties to it. I have tried to extend the AuditLog class (ExtendedAuditInfo) and implement a customised version of AuditStore Class (ExtendedAuditStore). However, I am not able to inject my new ExtendedAuditInfo in the constructor and receive two error messages regarding unmatching input parameters in the Constructor and SaveAsync method.
Class ExtendedAuditInfo:
public class ExtendedAuditInfo : AuditInfo
{
// Some properties
}
Class ExtendedAuditStore:
public class ExtendedAuditStore : AuditingStore
{
public ExtendedAuditStore(IRepository<ExtendedAuditInfo, long> auditLogRepository)
: base(auditLogRepository)
{
}
public override Task SaveAsync(ExtendedAuditInfo auditInfo)
{
if (!string.IsNullOrEmpty(auditInfo.Parameters) && auditInfo.Parameters != "{}")
{
var parameters = JsonConvert.DeserializeObject<AuditParameterInput>(auditInfo.Parameters);
if (parameters != null)
auditInfo.CustomData = parameters.Input.Id.ToString();
}
return base.SaveAsync(auditInfo);
}
}
The errors are:
cannot convert from 'Abp.Domain.Repositories.IRepository<SixB.Serafina.Auditing.ExtendedAuditInfo, long>' to 'Abp.Domain.Repositories.IRepository<Abp.Auditing.AuditLog, long>'
and
no suitable method found to override
The procedure above is based on the idea that I found Here
I found the solution based on the official document of How To Extend Existing Entities.
In order to extend the AuditLog class, inheritance must be used. Therefore a new class, let's say ExtendedAuditInfo needs to be inherited from AuditLog.
public class ExtendedAuditLog : AuditLog
{
public ExtendedAuditLog()
{
}
public ExtendedAuditLog(AuditInfo auditInfo)
{
this.BrowserInfo = auditInfo.BrowserInfo;
this.ClientIpAddress = auditInfo.ClientIpAddress;
this.ClientName = auditInfo.ClientName;
this.CustomData = auditInfo.CustomData;
this.Exception = auditInfo.Exception?.Message.ToString() + "";
this.ExecutionDuration = auditInfo.ExecutionDuration;
this.ExecutionTime = auditInfo.ExecutionTime;
this.ImpersonatorTenantId = auditInfo.ImpersonatorTenantId;
this.ImpersonatorUserId = auditInfo.ImpersonatorUserId;
this.MethodName = auditInfo.MethodName;
this.Parameters = auditInfo.Parameters;
this.ReturnValue = auditInfo.ReturnValue;
this.ServiceName = auditInfo.ServiceName;
this.TenantId = auditInfo.TenantId;
this.UserId = auditInfo.UserId;
}
//new properties
}
This class has to be added to the context and obviously, a new migration needs to be run in order to add the new properties.
public class ProjectDbContext : AbpZeroDbContext<Tenant, Role, User, ProjectDbContext >
{
/* Define a DbSet for each entity of the application */
public SerafinaDbContext(DbContextOptions<SerafinaDbContext> options)
: base(options)
{
}
public virtual DbSet<County> Counties { get; set; }
public virtual DbSet<Country> Countries { get; set; }
public virtual DbSet<Currency> Currencies { get; set; }
public virtual DbSet<OrganisationType> OrganisationTypes { get; set; }
public virtual DbSet<ExtendedAuditLog> ExtendedAuditLogs { get; set; }
}
Finally, in the ExtendedAuditStore class, IRepository<ExtendedAuditLog, long> _extendedAuditLogRepository has to be injected as a second parameter of the constructor and can be used to insert the extended entity.
public class ExtendedAuditStore : AuditingStore
{
IRepository<ExtendedAuditLog, long> _extendedAuditLogRepository;
public ExtendedAuditStore(
IRepository<AuditLog, long> auditLogRepository,
IRepository<ExtendedAuditLog, long> extendedAuditLogRepository
)
: base(auditLogRepository)
{
_extendedAuditLogRepository = extendedAuditLogRepository;
}
public override async Task SaveAsync(AuditInfo auditInfo)
{
if (auditInfo.Exception != null)
await base.SaveAsync(auditInfo);
var auditLog = new ExtendedAuditLog(auditInfo);
//new properties can be set here
await _extendedAuditLogRepository.InsertAsync(auditLog);
}
}
Also, instead of inheriting from AuditingStore, a new implementation for IAuditingStore can be created and injected into application services.
UPDATE:
Finally, all you need is to replace the default AuditingStore in StartUp class:
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddTransient<IAuditingStore, ExtendedAuditStore>();
}

ModelState.Isvalid invalidating field even though default value existed

public abstract class Base : IBase
{
[Required]
public int key {get;set;}
}
public class Entity: Base
{
public string Name {get;set;}
}
public class child : Entity
{
[Required]
public string Park {get;set;}
}
ActionFilter
public class ValidateViewModelAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (actionContext.ModelState.IsValid == false) {
actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState);
}
}
}
Now, when value is posted to API then, not setting "Key" field as it is the request for SAVE. Problem is, above attribute says, MODEL IS INVALID for field "key" . Its already there as 0 value for Id field (as default int).
I expect, it should validate true as 0 is default value.
NOTE: I could not remove or make any change in BASEENTITY and PARENT entity above.
I have only control in CHILD entity and this attribute class.
To ignore a property that is marked as [Required] you can use ModelState.Remove("propertyName");
Also, your property has a value of 0 because an int cannot have a value of NULL so the 0 is automatically attributed. But if you did not pass this value in the form data, the model validation will "consider" that it is NULL and thus will make the model invalid. If you do not want to use the call to Remove as shown above, you will have to explicitly give a value to the Key property :-)
source: The first comment on this page - credit for this explanation #Stephen Muecke
Use something like
public class ValidateViewModelAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
ModelState.Remove("key");
if (actionContext.ModelState.IsValid == false) {
actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState);
}
}
}
Note: By default, MVC6 model validation will simplicity tag all non-nullable value types as required (god knows why).
call
DataAnnotationsModelValidatorProvider.AddImplicitRequiredAttributeForValueTypes = false;
disable this behaviour.
I have the same problem and solved it by this way :
[AcceptVerbs("Post")]
public ActionResult EditingInline_Create([DataSourceRequest] DataSourceRequest request, Model.Server server)
{
if (server != null && ModelState.IsValid) //Invalid!!
{
_exchangeService.Create(server);
}
return Json(new[] { server }.ToDataSourceResult(request, ModelState));
}
and replace model with viewModel ,because in viewModel we don`t have ID (refer to Use ViewModel)
then we will have :
[AcceptVerbs("Post")]
public ActionResult EditingInline_Create([DataSourceRequest] DataSourceRequest request, ViewModel.ServerViewModel server)
{
if (server != null && ModelState.IsValid)
{
_exchangeService.Create(server);
}
return Json(new[] { server }.ToDataSourceResult(request, ModelState));
}

Retrieve model name in custom display name attribute

Here is my development requirement,
My label values are stored in the database, and I still want to use the data annotation in a declarative way, this is to make my model more readable.
And here is my approach,
I decided to write custom DisplayNameAttribute, where the default value provided by my model will be overwritten by the value retrieved from the database.
Here is the property defined in the model,
[CustomDisplay(Name: "First Name")]
[CustomRequired(ErrorMessage: "{0} is required")]
public String FirstName { get; set; }
Here is the custom display name attribute class,
public class CustomDisplayAttribute : DisplayNameAttribute
{
private string _defaultName;
private string _displayName;
public CustomDisplayAttribute(string Name)
{
_defaultName = Name;
}
public override string DisplayName
{
get
{
if (String.IsNullOrEmpty(_displayName))
{
_displayName = DAO.RetrieveValue(**ModelName**, _defaultName);
}
return _displayName;
}
}
}
Now, you can see in the above code, ModelName is something I need, but I don't have!!
While debugging, I dig into ModelMetadataProviders.Current and can see the availability of the current model in action. But, as it is part of non-public static members I am unable to access it through my code.
I have written the below method to retrieve the model name through reflection,
private static string GetModelName()
{
var modelName = String.Empty;
FieldInfo info = typeof(CachedAssociatedMetadataProvider<CachedDataAnnotationsModelMetadata>)
.GetField("_typeIds", BindingFlags.NonPublic | BindingFlags.Static);
var types = (ConcurrentDictionary<Type, string>)info.GetValue(null);
modelName = types.FirstOrDefault().Key.Name;
return modelName;
}
But the problem is, the types collection provides me entries for all the models (visited at least once by the user). And there is no clue to know, which is currently in action!!
IMHO Attributes should not be used to make database calls. Attributes should be used to add metadata to Classes/Properties etc...
So If you're willing to change your code to be more like the Microsoft architecture for MVC then you'd have your custom Attribute and a custom ModelMetadataProvider:
public class CustomDisplayAttribute : Attribute
{
public CustomDisplayAttribute(string name)
{
Name = name;
}
public string Name { get; private set; }
}
Then a new ModelMetadataProvider:
public class DatabaseModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
public DatabaseModelMetadataProvider()
{
}
protected override ModelMetadata CreateMetadata(
IEnumerable<Attribute> attributes,
Type containerType,
Func<object> modelAccessor,
Type modelType,
string propertyName)
{
var metadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
var displayAttribute = containerType == null
? null as CustomDisplayAttribute
: containerType.GetProperty(propertyName)
.GetCustomAttributes(false)
.OfType<CustomDisplayAttribute>()
.FirstOrDefault();
if (displayAttribute != null)
{
var displayValue = DAO.RetrieveValue(containerType.ToString(), displayAttribute.Name)
metadata.DisplayName = displayValue;
}
return metadata;
}
}
Where
public class MyViewModel
{
public MyPropertyType PropertyName { get; set; }
}
containerType = MyViewModel
modelType = MyPropertyType
propertyName = PropertyName
Then register the provider (global.asax or whatever):
ModelMetadataProviders.Current = new LocalizedModelMetadataProvider();
Also you can take a look at the ModelMetadata it has a few other things you might want to change in the future.

MVC 6 EF7 RC1 creating multiple dbcontexts

I am trying to figure out how to create a second DB context in EF7 RC1. In the past I could use a constructor with :base("connectionName") but that no longer seems an option since it says cannot convert string to System.IServiceProvider.
My second context code is as follows:
public class DecAppContext : DbContext
{
public DecAppContext()
// :base("DefaultConnection")
{
}
public DbSet<VignetteModels> VignetteModels { get; set; }
public DbSet<VignetteResult> Result { get; set; }
}
}
In my config.json I have the connection specified:
"Data": {
"DefaultConnection": {
"ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=aspnet5-xxxxx...;Trusted_Connection=True;MultipleActiveResultSets=true"
}
}
In my configure services section of my startup I have both contexts added:
services.AddEntityFramework()
.AddSqlServer()
.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration["Data:DefaultConnection:ConnectionString"]))
.AddDbContext<DecAppContext>(options => options.UseSqlServer(Configuration["Data:DefaultConnection:ConnectionString"]));
The applicationDB context works fine since I can create a user and login without issue
However when I try to access the other context as in my controller via:
private DecAppContext db = new DecAppContext();
var vignette = db.VignetteModels.SingleOrDefault(v => v.CaseId == vid);
I get the error:
No database providers are configured. Configure a database provider by
overriding OnConfiguring in your DbContext class or in the
AddDbContext method when setting up services.
Any working examples in EF7 RC1 with multiple db contexts and accessing them would be much appreciated.
First of all I would recommend you the article from the wiki of EntityFramework on GitHub. The article describes many ways to define DbContext, which references to a section of appsettings.json. I personally prefer the way with the usage of [FromServices] attribute.
The code could be about the following:
First of all you defined appsettings.json with the following content
{
"Data": {
"ApplicationDbConnectionString": "Server=(localdb)\\mssqllocaldb;Database=ApplicationDb;Trusted_Connection=True;MultipleActiveResultSets=true",
"DecAppDbConnectionString": "Server=Server=(localdb)\\mssqllocaldb;Database=DecAppDb;Trusted_Connection=True;MultipleActiveResultSets=true"
}
}
where you define two connection strings.
Seconds you declare the classes DecAppContext and ApplicationDbContext which have DbContext as the base class. The simplest form will be just
public class ApplicationDbContext : DbContext
{
}
public class DecAppContext : DbContext
{
}
without any DbSet properties.
Third Step. You use Microsoft.Extensions.DependencyInjection to inject the database contexts. To do this you need just include in Startup.cs something like
public class Startup
{
// property for holding configuration
public IConfigurationRoot Configuration { get; set; }
public Startup(IHostingEnvironment env)
{
// Set up configuration sources.
var builder = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.AddEnvironmentVariables();
// save the configuration in Configuration property
Configuration = builder.Build();
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddMvc()
.AddJsonOptions(options => {
options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
});
services.AddEntityFramework()
.AddSqlServer()
.AddDbContext<ApplicationDbContext>(options => {
options.UseSqlServer(Configuration["Data:ApplicationDbConnectionString"]);
})
.AddDbContext<DecAppContext>(options => {
options.UseSqlServer(Configuration["Data:DecAppDbConnectionString"]);
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
...
}
}
Se create two DbContext (DecAppContext and ApplicationDbContext) using the configuration "Data:DecAppDbConnectionString" and "Data:ApplicationDbConnectionString".
Now we can just use the context in the controller. For example
[Route("api/[controller]")]
public class UsersController : Controller
{
[FromServices]
public ApplicationDbContext ApplicationDbContext { get; set; }
[FromServices]
public DecAppContext DecAppContext { get; set; }
[HttpGet]
public IEnumerable<object> Get() {
var returnObject = new List<dynamic>();
using (var cmd = ApplicationDbContext.Database.GetDbConnection().CreateCommand()) {
cmd.CommandText = "SELECT Id, FirstName FROM dbo.Users";
if (cmd.Connection.State != ConnectionState.Open)
cmd.Connection.Open();
var retObject = new List<dynamic>();
using (var dataReader = cmd.ExecuteReader())
{
while (dataReader.Read())
{
var dataRow = new ExpandoObject() as IDictionary<string, object>;
for (var iFiled = 0; iFiled < dataReader.FieldCount; iFiled++)
dataRow.Add(
dataReader.GetName(iFiled),
dataReader.IsDBNull(iFiled) ? null : dataReader[iFiled] // use null instead of {}
);
retObject.Add((ExpandoObject)dataRow);
}
}
return retObject;
}
}
}
or the same using async/await:
[Route("api/[controller]")]
public class UsersController : Controller
{
[FromServices]
public ApplicationDbContext ApplicationDbContext { get; set; }
[FromServices]
public DecAppContext DecAppContext { get; set; }
[HttpGet]
public async IEnumerable<object> Get() {
var returnObject = new List<dynamic>();
using (var cmd = ApplicationDbContext.Database.GetDbConnection().CreateCommand()) {
cmd.CommandText = "SELECT Id, FirstName FROM dbo.Users";
if (cmd.Connection.State != ConnectionState.Open)
cmd.Connection.Open();
var retObject = new List<dynamic>();
using (var dataReader = await cmd.ExecuteReaderAsync())
{
while (await dataReader.ReadAsync())
{
var dataRow = new ExpandoObject() as IDictionary<string, object>;
for (var iFiled = 0; iFiled < dataReader.FieldCount; iFiled++)
dataRow.Add(dataReader.GetName(iFiled), dataReader[iFiled]);
retObject.Add((ExpandoObject)dataRow);
}
}
return retObject;
}
}
}
One can just declare the property public ApplicationDbContext ApplicationDbContext { get; set; } with the attribute [FromServices] and ASP.NET initialize it from the context injected in ConfigureServices. In the same way one can use the second context DecAppContext whenever you need it.
The above code example will execute SELECT Id, FirstName From dbo.Users in the database context and return JSON data in the form [{"id":123, "firstName":"Oleg"},{"id":456, "firstName":"Xaxum"}]. The conversion of property names from Id and FirstName to id and firstName will be done automatically during serialization because of usage AddJsonOptions in ConfigureServices.
UPDATE: I have to reference the announcement. The next version of MVC (RC2) will require to change the above code to use [FromServices] as additional parameter (of method Get() for example) instead of usage public property [FromServices] public ApplicationDbContext ApplicationDbContext { get; set; }. One will need to remove the property ApplicationDbContext and to add additional parameter to Get() method: public async IEnumerable<object> Get([FromServices] ApplicationDbContext applicationDbContext) {...}. Such changes can be easy done. See here and example of the changes in the demo example of MVC:
[Route("api/[controller]")]
public class UsersController : Controller
{
[HttpGet]
public async IEnumerable<object> Get(
[FromServices] ApplicationDbContext applicationDbContext,
[FromServices] DecAppContext decAppContext)
{
var returnObject = new List<dynamic>();
// ... the same code as before, but using applicationDbContext
// and decAppContext parameters instead of ApplicationDbContext
// and DecAppContext properties
}

How to indexing and searching nested properties of Interface type in NEST

I have a following document index entity:
[ElasticType(Name = "Document", IdProperty = "Id")]
public class Document
{
[ElasticProperty(Index = FieldIndexOption.NotAnalyzed)]
public string Id { get; set; }
[ElasticProperty(Type = FieldType.Nested)]
public ICustomer Customer { get; set; }
}
where ICustomer can be different types:
public interface ICustomer
{
}
public class Supplier : ICustomer
{
public string Name { get; set; }
//another properties
}
public class Vendor : ICustomer
{
public string Name { get; set; }
//another properties
}
My mapping is:
Client.CreateIndex("Document", c => c
.AddMapping<Document>(m => m
.SearchAnalyzer("standard")
.IndexAnalyzer("standard")
.MapFromAttributes()
.NumericDetection()
.DateDetection();
When I save document to index it saves also nested objects (Supplier or Vendor) serialized correctly.
But I have problem when I'm searching data. I'm getting following exception from newtonsoft:
Type is an interface or abstract class and cannot be instantiated.
I was trying to create custom json converter
public class CustomJsonConvertor : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
serializer.Serialize(writer, value);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (objectType == typeof(Supplier))
{
return serializer.Deserialize(reader, typeof (Supplier));
}
if (objectType == typeof(Vendor))
{
return serializer.Deserialize(reader, typeof(Vendor));
}
throw new NotSupportedException(string.Format("Type {0} unexpected.", objectType));
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof (Supplier) || objectType == typeof (Vendor);
}
}
and register it as:
settings.AddContractJsonConverters(t => typeof(ICustomer).IsAssignableFrom(t) ? new CustomJsonConvertor() : null);
But then I'm receiving exception in ReadJson method, because objectType is of type ICustomer and the conditions if (objectType == typeof(Supplier)) are never true. Parameter existingValue is null in this method, so I have no option how to determine
the correct type.
NOTE: my entities (Vendor, Supplier) are in separated dll (plugin) and I have no direct access to the them while defining Document.
Can you advise what I'm doing wrong or give me some best practice advice how to deal with interface or abstract class inside document index and how to deal with polymorphism?
thanks a lot!
That information is lost, you'll need to inspect the JSON to provide a differentiator.
For hits themselves NEST is able to use _type as a differentiator. For collections inside your document you will have to either write your jsonconverter to instantiate the right type based on a property or instruct Json.NET to write that information for you when serializing automatically.

Resources