I am coding a value object for my domain object identifier, similar to what is done here. Basically I am just wrapping a guid.
By following this article's advice I was able to create a custom model binder which can convert a string to the value object (for input to the controller), but I don't seem to be able to get the correct value as output from the controller.
My hope is to get something similar to this back from my controller action:
{
id: "string representation of the guid goes here",
someOtherProperty: "foo"
}
Has anyone done something similar, and if so, what am I missing?
TenantId.cs
public class TenantId : IEquatable<TenantId>
{
public TenantId(Guid id)
{
if (id == Guid.Empty)
{
throw new InvalidOperationException("TenantId must not be an empty GUID.");
}
_id = id;
}
private readonly Guid _id;
public bool Equals(TenantId other)
{
if (null == other)
{
return false;
}
return _id.Equals(other._id);
}
public override int GetHashCode()
{
return _id.GetHashCode();
}
public override bool Equals(object other)
{
return Equals(other as TenantId);
}
public override string ToString()
{
return _id.ToString("B");
}
}
}
TenantIdModelBinder.cs
public class TenantIdModelBinder : IModelBinder
{
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
var key = bindingContext.ModelName;
var val = bindingContext.ValueProvider.GetValue(key);
if (val == null)
{
return false;
}
var s = val.AttemptedValue;
if (s == null)
{
return false;
}
Guid g;
if (!Guid.TryParse(s, out g))
{
return false;
}
bindingContext.Model = new TenantId(g);
return true;
}
}
TenantController.cs
public class TenantController : ApiController
{
public IHttpActionResult Get([ModelBinder(typeof(TenantIdModelBinder))]TenantId id)
{
return Ok(new
{
id
});
}
}
I can step through and see model is bound properly to the correct guid I pass in, however, this code simply returns: {"id":{}}
Related
How to prevent execution of next ValidationAttribute, if the first validation is failed?
For example, if (category "id" <= 0), then do not try to check if it exists.
Because now, when I do "PUT /api/categories/-1", I get this:
{
"id": [
"id must be greater or equal to 0",
"Entity with such Id was not found!"
]
}
Method where I want to prevent further validation:
[HttpPut("{id}")]
public ActionResult UpdateCategory([Min(0)][CategoryExists] int id, [FromBody] Category category)
{
return new OkResult();
//if (category.Id == 0) {
// return new BadRequestObjectResult("Id property is required!");
//}
//_context.Category.Update(category);
//_context.SaveChanges();
}
Min attribute
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property)]
public class MinAttribute : ValidationAttribute
{
private int _minVal;
public MinAttribute(int minVal)
{
_minVal = minVal;
}
public override bool IsValid(object value)
{
if ((int)value >= _minVal)
{
return true;
}
else
{
return false;
}
}
public override string FormatErrorMessage(string name)
{
return $"{name} must be greater or equal to {_minVal}";
}
}
CategoryExists attribute
public class CategoryExistsAttribute : ValidationAttribute
{
public CategoryExistsAttribute()
{
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var context = (TechDbContext)validationContext.GetService(typeof(TechDbContext));
var result = from i in context.Category
where i.Id == (int)value
select i;
Category category = result.SingleOrDefault();
if (category == null)
{
return new ValidationResult("Entity with such Id was not found!");
}
return ValidationResult.Success;
}
public override string FormatErrorMessage(string name)
{
return base.FormatErrorMessage(name);
}
}
Write an extension method for the Category object, which validates all your needs.
Eg
internal static bool Validate(this Category category){
if (category.Id <= 0) {
return false;
}
// Add more validations or throw exceptions if needed.
return true;
}
So from your controller you can do this.
if(category.Validate()){
//proceed to other business logic
}
For more detailed information about data validation read this
This is a tough one. I have an issue with binding a model from JSON. I am attempting to resolve polymorphic-ally the record supplied with the type of record that it will resolve to (I want to be able to add many record types in the future). I have attempted to use the following example to resolve my model when calling the endpoint however this example only works for MVC and not Web API applications.
I have attempted to write it using IModelBinder and BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext). However I can't find the equivalent of ModelMetadataProviders in the System.Web.Http namespace.
Appreciate any help anyone can give.
I have a Web API 2 application which has the following object structure.
public abstract class ResourceRecord
{
public abstract string Type { get; }
}
public class ARecord : ResourceRecord
{
public override string Type
{
get { return "A"; }
}
public string AVal { get; set; }
}
public class BRecord : ResourceRecord
{
public override string Type
{
get { return "B"; }
}
public string BVal { get; set; }
}
public class RecordCollection
{
public string Id { get; set; }
public string Name { get; set; }
public List<ResourceRecord> Records { get; }
public RecordCollection()
{
Records = new List<ResourceRecord>();
}
}
JSON Structure
{
"Id": "1",
"Name": "myName",
"Records": [
{
"Type": "A",
"AValue": "AVal"
},
{
"Type": "B",
"BValue": "BVal"
}
]
}
After some research I discovered that metadata providers don't exist within WebAPI and in order to bind to complex abstract objects you have to write your own.
I started by writing a new model binding method, with the use of a custom type name JSon serializer and finally I updated my endpoint to use the custom binder. It's worth noting the following will only work with requests in the body, you will have to write something else for requests in the header. I would suggest a read of chapter 16 of Adam Freeman's Expert ASP.NET Web API 2 for MVC Developers and complex object binding.
I was able to serialize my object from the body of the request using the following code.
WebAPI configuration
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Services.Insert(typeof(ModelBinderProvider), 0,
new SimpleModelBinderProvider(typeof(RecordCollection), new JsonBodyModelBinder<RecordCollection>()));
}
}
Custom model binder
public class JsonBodyModelBinder<T> : IModelBinder
{
public bool BindModel(HttpActionContext actionContext,
ModelBindingContext bindingContext)
{
if (bindingContext.ModelType != typeof(T))
{
return false;
}
try
{
var json = ExtractRequestJson(actionContext);
bindingContext.Model = DeserializeObjectFromJson(json);
return true;
}
catch (JsonException exception)
{
bindingContext.ModelState.AddModelError("JsonDeserializationException", exception);
return false;
}
return false;
}
private static T DeserializeObjectFromJson(string json)
{
var binder = new TypeNameSerializationBinder("");
var obj = JsonConvert.DeserializeObject<T>(json, new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.Auto,
Binder = binder
});
return obj;
}
private static string ExtractRequestJson(HttpActionContext actionContext)
{
var content = actionContext.Request.Content;
string json = content.ReadAsStringAsync().Result;
return json;
}
}
Custom Serialization binding
public class TypeNameSerializationBinder : SerializationBinder
{
public string TypeFormat { get; private set; }
public TypeNameSerializationBinder(string typeFormat)
{
TypeFormat = typeFormat;
}
public override void BindToName(Type serializedType, out string assemblyName, out string typeName)
{
assemblyName = null;
typeName = serializedType.Name;
}
public override Type BindToType(string assemblyName, string typeName)
{
string resolvedTypeName = string.Format(TypeFormat, typeName);
return Type.GetType(resolvedTypeName, true);
}
}
End point definition
[HttpPost]
public void Post([ModelBinder(BinderType = typeof(JsonBodyModelBinder<RecordCollection>))]RecordCollection recordCollection)
{
}
The TypeNameSerializationBinder class is not necessary anymore as well as the WebApiConfig configuration.
First, you need to create enum for record type:
public enum ResourceRecordTypeEnum
{
a,
b
}
Then, change your "Type" field in ResourceRecord to be the enum we just created:
public abstract class ResourceRecord
{
public abstract ResourceRecordTypeEnum Type { get; }
}
Now you should create these 2 classes:
Model Binder
public class ResourceRecordModelBinder<T> : IModelBinder
{
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType != typeof(T))
return false;
try
{
var json = ExtractRequestJson(actionContext);
bindingContext.Model = DeserializeObjectFromJson(json);
return true;
}
catch (JsonException exception)
{
bindingContext.ModelState.AddModelError("JsonDeserializationException", exception);
return false;
}
}
private static T DeserializeObjectFromJson(string json)
{
// This is the main part of the conversion
var obj = JsonConvert.DeserializeObject<T>(json, new ResourceRecordConverter());
return obj;
}
private string ExtractRequestJson(HttpActionContext actionContext)
{
var content = actionContext.Request.Content;
string json = content.ReadAsStringAsync().Result;
return json;
}
}
Converter class
public class ResourceRecordConverter : CustomCreationConverter<ResourceRecord>
{
private ResourceRecordTypeEnum _currentObjectType;
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var jobj = JObject.ReadFrom(reader);
// jobj is the serialized json of the reuquest
// It pulls from each record the "type" field as it is in requested json,
// in order to identify which object to create in "Create" method
_currentObjectType = jobj["type"].ToObject<ResourceRecordTypeEnum>();
return base.ReadJson(jobj.CreateReader(), objectType, existingValue, serializer);
}
public override ResourceRecord Create(Type objectType)
{
switch (_currentObjectType)
{
case ResourceRecordTypeEnum.a:
return new ARecord();
case ResourceRecordTypeEnum.b:
return new BRecord();
default:
throw new NotImplementedException();
}
}
}
Controller
[HttpPost]
public void Post([ModelBinder(BinderType = typeof(ResourceRecordModelBinder<RecordCollection>))] RecordCollection recordCollection)
{
}
Another option if you don't want to create a generic binder.
Custom Binder
public class RecordCollectionModelBinder : IModelBinder
{
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType != typeof(RecordCollection))
{
return false;
}
try
{
var json = ExtractRequestJson(actionContext);
bindingContext.Model = DeserializeObjectFromJson(json);
return true;
}
catch (JsonException exception)
{
bindingContext.ModelState.AddModelError("JsonDeserializationException", exception);
return false;
}
}
private string ExtractRequestJson(HttpActionContext actionContext)
{
var content = actionContext.Request.Content;
string json = content.ReadAsStringAsync().Result;
return json;
}
private RecordCollection DeserializeObjectFromJson(string json)
{
var jObject = JObject.Parse(json);
var result = jObject.ToObject<RecordCollection>();
if (result.Restrictions == null)
{
return result;
}
int index = 0;
foreach (var record in result.Records.ToList())
{
switch (record.Type)
{
case "A":
result.Restrictions[index] = jObject["Records"][index].ToObject<ARecord>();
break;
case "B":
result.Restrictions[index] = jObject["Records"][index].ToObject<BRecord>();
break;
}
index++;
}
return result;
}
}
Controller
[HttpPost]
public void Post([ModelBinder(BinderType = typeof(RecordCollectionModelBinder))] RecordCollection recordCollection)
{
}
I have a class that contains the following:
HashSet<CookieSetItem> _set = new HashSet<CookieSetItem>();
public IEnumerable<CookieSetItem> Set
{
get { return _set; }
}
public void Add(int id)
{
id.ThrowDefault("id");
var item = new CookieSetItem(id);
if (_set.Add(item))
{
// this only happens for the first call
base.Add();
}
}
When I call the add method multiple times, say with ID's 1,2,3 etc, only the first item is added.
Obviously I'm confused as a new CookieSetItem is being created each time with a unique element (the ID), so why is it not being added?.
For completeness, here's the cookie set class:
public sealed class CookieSetItem
{
readonly DateTime _added;
readonly int _id;
public DateTime Added
{
get { return _added; }
}
public int ID
{
get { return _id; }
}
public CookieSetItem(int id)
: this(id, DateTime.Now)
{
}
public CookieSetItem(int id, DateTime added)
{
id.ThrowDefault("id");
added.ThrowDefault("added");
_id = id;
_added = added;
}
}
Got to the bottom of it - more than one error, which clouded the overall view.
Firstly I updated my class with IEquatable, which fixed the adding problem. Secondly, I found that the end result which was to update a cookie with a string version of the hashset also failed due to the fact that it was not encrypted. Here's the amended class that fixed the original problem.
public sealed class DatedSet : IEquatable<DatedSet>
{
readonly DateTime _added;
readonly int _id;
public DateTime Added
{
get { return _added; }
}
public int ID
{
get { return _id; }
}
public DatedSet(int id)
: this(id, DateTime.Now)
{
}
public DatedSet(int id, DateTime added)
{
id.ThrowDefault("id");
added.ThrowDefault("added");
_id = id;
_added = added;
}
public bool Equals(DatedSet other)
{
if (other == null) return false;
return this.ID == other.ID;
}
public override bool Equals(Object obj)
{
if (obj == null) return false;
var ds = obj as DatedSet;
return ds == null ? false : Equals(ds);
}
public override int GetHashCode()
{
return ID.GetHashCode();
}
}
Thanks for the advice.
I have a view model that looks like this:
public class EventVm
{
public int Id { get; set; }
public int GroupId { get; set; }
public string Title { get; set; }
public EventLayout EventLayout { get; set; }
}
EventLayout is a custom object that looks like this:
public class EventLayout
{
private const string SingleColumnLayoutLocalKey = "MyOrg.SingleColumnLayout";
...
//Removed for breviety
public static EventLayout SingleColumnLayout = new EventLayout(SingleColumnLayoutLocalKey);
...
//Removed for breviety
public string Value
{
get { return _layoutLocalKey; }
}
private readonly string _layoutLocalKey;
private EventLayout(string layoutLocalKey)
{
_layoutLocalKey = layoutLocalKey;
}
public static EventLayout LayoutFromLocalString(string localString)
{
...
}
public override string ToString()
{
return _layoutLocalKey;
}
public override bool Equals(object obj)
{
if (obj.GetType() != this.GetType())
{
return false;
}
if (this._layoutLocalKey == obj.ToString())
{
return true;
}
return false;
}
public override int GetHashCode()
{
return this._layoutLocalKey.GetHashCode();
}
}
Basically EventLayout is just a custom enumeration that provides string backing store. EventLayout is bound to a Select form control (T() is just a localization extension method):
<select id="eventLayoutSelect" name="EventVm.EventLayout">
#foreach (var option in Model.EventLayoutOptions)
{
<option value="#option.Value" #((Model.EventLayout != null && Model.EventLayout.Equals(option))
? "selected=selected"
: string.Empty)>#T(option.Value)</option>
}
</select>
When I POST this form to the server, EventVm.EventLayout property is null when the action attempts to bind. However I can see that an EventLayout instance is POSTed in the form data:
My action looks like this:
[HttpPost]
public ActionResult Update(EventVm eventVm)
{
_eventService.UpdateEvent(eventVm);
return RedirectToAction("Index", new { groupId = eventVm.GroupId });
}
Can someone tell me what I've done wrong please?
A custom binding solved the problem. Thank you all for the helpful comments.
Brad Christie pointed out that because the object that my action is attempting to bind to only has a private constructor a custom binding would be needed.
Custom binding:
public class EventVmBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
HttpRequestBase request = controllerContext.HttpContext.Request;
var eventVm = new EventVm()
{
Id = Int32.Parse(request.Form.Get("Id")),
GroupId = Int32.Parse(request.Form.Get("GroupId")),
Title = request.Form.Get("Title"),
HeaderMarkup = request.Form.Get("HeaderMarkup"),
LeftNavigationMarkup = request.Form.Get("LeftNavigationMarkup"),
CenterContentMarkup = request.Form.Get("CenterContentMarkup"),
RightNavigationMarkup = request.Form.Get("RightNavigationMarkup"),
EventLayout = EventLayout.LayoutFromLocalString(request.Form.Get("EventLayout")),
DisplayOrder = Int32.Parse(request.Form.Get("DisplayOrder")),
Active = request.Form.Get("Active").As<bool>(),
CanEdit = request.Form.Get("CanEdit").As<bool>()
};
return eventVm;
}
}
Wiring it up at the action:
[HttpPost]
public ActionResult Create([ModelBinder(typeof(EventVmBinder))]EventVm eventVm)
{
_groupService.AddEventToGroup(eventVm);
return RedirectToAction("Index", new {groupId = eventVm.GroupId});
}
Replace your foreach loop with for loop like below
#foreach (var option in Model.EventLayoutOptions)
{
<option value="#option.Value" #((Model.EventLayout != null && Model.EventLayout.Equals(option))
? "selected=selected"
: string.Empty)>#T(option.Value)</option>
}
with
for(int i=0; i<Model.EventLayoutOptions.Count; i++ )
{
<option value="#Model.EventLayoutOptions[i].Value" #((Model.EventLayout != null && Model.EventLayout.Equals(Model.EventLayoutOptions[i]))
? "selected=selected"
: string.Empty)>#T(Model.EventLayoutOptions[i].Value)</option>
}
I have an entity called Feature which contains a value identity called FeatureIdentity.
I have a list of these entities, and i want to quickly determine if the identity already exists.
The kicker is i need to be able to compare by the FeatureIdentity and not be the Feature, the Contains procedure on lists is checking against a provided T parameter.
So I am currently doing the code:
public class SomeClass
{
HashSet<Feature> features = new HashSet<Feature>();
public void SetRequirement(FeatureIdentity feature, FeatureIdentity requires)
{
if (ContainsFeature(feature) == false || ContainsFeature(requires) == false)
{
// throw
}
this.requirements.Add(feature, requires);
}
bool ContainsFeature(FeatureIdentity identity)
{
return this.features.Where(x => x.Id.Equals(identity)).Count() > 0;
}
}
Does Linq optimize this, or is this there a correct optimal way of checking if the item exists?
public class Feature
{
public Feature(FeatureIdentity id, string name)
{
this.id = id;
this.name = name;
}
FeatureIdentity id;
string name;
FeatureIdentity Id
{
get { return this.id; }
}
}
public class FeatureIdentity : IEquatable<FeatureIdentity>
{
private readonly string sku;
public FeatureIdentity(string sku)
{
this.sku = sku;
}
public bool Equals(FeatureIdentity other)
{
return this.sku == other.sku;
}
public string Sku
{
get { return this.sku; }
}
public override int GetHashCode()
{
return this.sku.GetHashCode();
}
}
with ctor public HashSet(), HashSet<Feature> is using EqualityComparer<Feature>.Default as default Comparer.
if you use HashSet<Feature>, you should implement IEquatable<Feature> and override GetHashCode.
public class Feature: IEquatable<Feature>
{
public bool Equals(Feature other)
{
return this.id.Equals(other.id);
}
public override int GetHashCode()
{
return this.id.GetHashCode();
}
}
then you can try following workaround which waster a temp object from heap.
bool ContainsFeature(FeatureIdentity identity)
{
return this.features.Contain(new Feature(identity, null));
}