I'm building an ASP.NET MVC 2 application in C# and i am successfully using Automapper to map values back and forth between ViewModels and business objects.
In addition to several explicit properties, my business objects wrap a dictionary as a catch all for properties that aren't explicitly defined. Something similar to:
public class MyBusinessObject {
public void SetExtraPropertyValue<T>(string key, T value) {
// ...
}
public T GetExtraPropertyValue<T>(string key, T defaultValue) {
// ...
}
}
In my ViewModel, I have the freedom to create any properties I want, but I cannot modify the business objects.
So let's say I create a view model like this:
class MyViewModel {
public string CustomProp { get; set; }
}
and the value I want to store and retrieve will need to use
businessModelInstance.SetExtraPropertyValue("CustomProp", newVal);
and
businessModelInstance.GetExtraPropertyValue("CustomProp");
I have problems going both directions.
First, when going from the MyBusinessObject to the MyViewModel, I thought it should be simple to do in my custom Automapper profile:
CreateMap<MyBusinessObject, MyViewModel>()
.ForMember(dest => dest.CustomProp,
opt => opt.MapFrom(s => s.GetExtraPropertyValue("CustomProp", "")));
However, MyBusinessObject.CustomProp is never populated, though other properties are.
Secondly, I don't know how to configure getting a value from MyViewModel.CustomProp to calling MyBusinessObject.SetExtraPropertyValue.
Is there a way to accomplish this
mapping with Automapper?
Is there a completely different attack that I
should be trying?
Do I have to resort to manual mapping in my controller? For example, MyBusinessObject.SetExtraPropertyValue("CustomProp",
MyViewModel.CustomProp)
UPDATE: Here is my solution based on #Patrick Steele's suggestions:
I added a custom attribute to the view model properties that i wanted to map to extra property keys. A custom TypeConverter uses reflection to find these attributes and map properties appropriately.
public class ItemExtraPropertyConverter : ITypeConverter<MyViewModel, MyBusinessObject>
{
public MyBusinessObject Convert(ResolutionContext context)
{
var destination = context.DestinationValue as MyBusinessObject;
if (destination == null )
throw new InvalidOperationException("Destination type is not of type MyBusinessObject");
foreach (var property in context.SourceType.GetProperties())
foreach (var attribute in property.GetCustomAttributes(true).OfType<ExtraPropertyAttribute>())
{
var key = attribute.Key;
if (string.IsNullOrEmpty(key))
key = property.Name;
destination.SetExtraPropertyValue(key, property.GetValue(context.SourceValue, null));
}
return destination;
}
}
public class ExtraPropertyAttribute : Attribute
{
public ExtraPropertyAttribute()
{
}
public ExtraPropertyAttribute(string key)
{
Key = key;
}
public string Key { get; set; }
}
public class MyViewModel
{
[ExtraProperty]
public string CustomProp { get; set; }
[ExtraProperty("OtherPropertyValue")]
public string CustomProp2 { get; set; }
}
In the custom profile class's configure method:
CreateMap<MyViewModel, MyBusinessObject>()
.ConvertUsing<ItemExtraPropertyConverter>();
My guess is that something is wrong with your GetExtraPropertyValue and SetExtraPropertyValue implementations. I threw together a quick test and the mapping you provided above worked as expected. Here's the implementation I used for the test:
public class MyBusinessObject
{
private readonly Dictionary<string, object> extraProperties = new Dictionary<string, object>();
public int Id { get; set; }
public string Name { get; set; }
public void SetExtraPropertyValue<T>(string key, T value)
{
extraProperties.Add(key, value);
}
public T GetExtraPropertyValue<T>(string key, T defaultValue)
{
if (extraProperties.ContainsKey(key))
{
return (T)extraProperties[key];
}
return defaultValue;
}
}
Related
I have a model that's being used by one of the other models, that is being accepted as a parameter to one of my controllers. So as a result, this model is being displayed in Swagger UI. This model is a nullable type and is optional and I want to hide it from my documentation.
public class A
{
public string SomeProperty { get; set; }
public B? ClassB {get; set; }
}
public class B
{
public int SomeIntProperty { get; set; }
public bool SomeBooleanProperty { get; set; }
}
in the controller method:
public async Task<ActionResult<SomeType>> GetSomeType(A modelA, CancellationToken token)
As is, this endpoint will accept a JSON document like:
{
"SomeProperty": "SomeValue"
}
And won't need B to be present. So as a result, I want to hide B from my Swagger schemas. How can I do that? I found some related questions/answers but all of them are about hiding properties, https://stackoverflow.com/a/48454933/16749442
Hiding all properties of a model results in empty model:
The only working and clean solution I found is, unfortunately, using reflection again.
SwaggerExcludeAttribute.cs
[AttributeUsage(AttributeTargets.Class)]
public class SwaggerExcludeAttribute : Attribute
{
}
SwaggerIgnoreModelFilter.cs
public class SwaggerIgnoreModelFilter : IDocumentFilter
{
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
// Get all models that are decorated with SwaggerExcludeAttribute
// This will only work for models that are under current Assembly
var excludedTypes = GetTypesWithHelpAttribute(Assembly.GetExecutingAssembly());
// Loop through them
foreach (var _type in excludedTypes)
{
// Check if that type exists in SchemaRepository
if (context.SchemaRepository.TryLookupByType(_type, out _))
{
// If the type exists in SchemaRepository, check if name exists in the dictionary
if (swaggerDoc.Components.Schemas.ContainsKey(_type.Name))
{
// Remove the schema
swaggerDoc.Components.Schemas.Remove(_type.Name);
}
}
}
}
// Get all types in assembly that contains SwaggerExcludeAttribute
public static IEnumerable<Type> GetTypesWithHelpAttribute(Assembly assembly)
{
return assembly.GetTypes().Where(type => type.GetCustomAttributes(typeof(SwaggerExcludeAttribute), true).Length > 0);
}
}
Startup.cs
services.AddSwaggerGen(c =>
{
.....
c.DocumentFilter<SwaggerIgnoreModelFilter>();
......
});
I am trying to use a custom attribute on a Entity class generated automatically by the Entity Framework.
The problem is how to add an property attribute on an existing field?
Here the point where I am right now:
// the custom attribute class
public class MyCustomAttribute : Attribute
{
public String Key { get; set; }
}
// Entity Framework class generated automatically
public partial class EntityClass
{
public String Existent { get; set; }
//...
}
// set a metadata class for my entity
[MetadataType(typeof(EntityClassMetaData))]
public partial class EntityClass
{
// if I add a new property to the entity, it works. This attribute will be read
[MyCustomAttribute(Key = "KeyOne" )]
public int newProp { get; set; }
}
public class EntityClassMetaData
{
// adding the custom attribute to the existing property
[MyCustomAttribute(Key = "keyMeta") ]
public String Existent { get; set; }
}
Running this test:
[TestMethod]
public void test1()
{
foreach (var prop in typeof(EntityClass).GetProperties())
{
var att = prop.GetCustomAttribute<MyCustomAttribute>();
if (att != null)
{
Console.WriteLine($"Found {att.Key}");
}
}
}
will produce:
Found KeyOne
Or the Metadata class store the attribute in a different way or only works for data annotations.
I am stuck here, how can I set and read custom attributes of the generated class without having to edit the generated file?
I came across this same problem today. I figured EF magic would do the trick and map the attribute across to each model property. Turns out it does, but only for EF data annotations and I couldn't find an answered solution to pull out custom attributes so made this function. Hope it helps dude.
private object[] GetMetadataCustomAttributes(Type T, string propName)
{
if (Attribute.IsDefined(T, typeof(MetadataTypeAttribute)))
{
var metadataClassType =
(T.GetCustomAttributes(typeof(MetadataTypeAttribute), true).FirstOrDefault() as
MetadataTypeAttribute).MetadataClassType;
var metaDataClassProperty = metadataClassType.GetProperty(propName);
if (metaDataClassProperty != null)
{
return metaDataClassProperty.GetCustomAttributes(true);
}
}
return null;
}
I believe if you want to set an attribute in the metadata class, you have to use this syntax:
public class EntityClassMetaData
{
// adding the custom attribute to the existing property
[MyCustomAttribute(Key = "keyMeta") ]
public String Existent;
}
You must not have { get; set; } on your pre-existing property - just the property with the correct name and datatype.
I have a simple pair of classes which for I've set up a mapping at initialization time.
public class Order {
public int ID { get; set; }
public string Foo { get; set; }
}
public class OrderDTO {
public int ID { get; set; }
public string Foo { get; set; }
}
...
Mapper.CreateMap<Order, OrderDTO>();
Now at a certain point I need to map an Order to an OrderDTO. BUT depending on some circumstances, I might need to ignore Foo during mapping. Let's also assume that I cannot "store" the condition in the source or destination object.
I know how I can configure the ignored properties at initialization time, but I have no idea how I could achieve such a dynamic runtime behavior.
Any help would be appreciated.
UPDATE
My use case for this behaviour is something like this. I have an ASP.NET MVC web grid view which displays a list of OrderDTOs. The users can edit the cell values individually. The grid view sends the edited data back to the server like a collection of OrderDTOs, BUT only the edited field values are set, the others are left as default. It also sends data about which fields are edited for each primary key. Now from this special scenario I need to map these "half-empty" objects to Orders, but of course, skip those properties which were not edited for each object.
The other way would be to do the manual mapping, or use Reflection somehow, but I was just thinking about if I could use AutoMapper in some way.
I've digged into the AutoMapper source code and samples, and found that there is a way to pass runtime parameters at mapping time.
A quick example setup and usage looks like this.
public class Order {
public int ID { get; set; }
public string Foo { get; set; }
}
public class OrderDTO {
public int ID { get; set; }
public string Foo { get; set; }
}
...
Mapper.CreateMap<Order, OrderDTO>()
.ForMember(e => e.Foo, o => o.Condition((ResolutionContext c) => !c.Options.Items.ContainsKey("IWantToSkipFoo")));
...
var target = new Order();
target.ID = 2;
target.Foo = "This should not change";
var source = new OrderDTO();
source.ID = 10;
source.Foo = "This won't be mapped";
Mapper.Map(source, target, opts => { opts.Items["IWantToSkipFoo"] = true; });
Assert.AreEqual(target.ID, 10);
Assert.AreEqual(target.Foo, "This should not change");
In fact this looks quite "technical", but I still think there are quite many use cases when this is really helpful. If this logic is generalized according to application needs, and wrapped into some extension methods for example, then it could be much cleaner.
Expanding on BlackjacketMack's comment for others:
In your MappingProfile, add a ForAllMaps(...) call to your constructor.
using AutoMapper;
using System.Collections.Generic;
using System.Linq;
public class MappingProfile : Profile
{
public MappingProfile()
{
ForAllMaps((typeMap, mappingExpression) =>
{
mappingExpression.ForAllMembers(memberOptions =>
{
memberOptions.Condition((o1, o2, o3, o4, resolutionContext) =>
{
var name = memberOptions.DestinationMember.Name;
if (resolutionContext.Items.TryGetValue(MemberExclusionKey, out object exclusions))
{
if (((IEnumerable<string>)exclusions).Contains(name))
{
return false;
}
}
return true;
});
});
});
}
public static string MemberExclusionKey { get; } = "exclude";
}
Then, for ease of use, add the following class to create an extension method for yourself.
public static class IMappingOperationOptionsExtensions
{
public static void ExcludeMembers(this AutoMapper.IMappingOperationOptions options, params string[] members)
{
options.Items[MappingProfile.MemberExclusionKey] = members;
}
}
Finally, tie it all together: var target = mapper.Map<Order>(source, opts => opts.ExcludeMembers("Foo"));
I'm trying to post a JSON to NancyFx. The JSON is the following:
{
"prop1": 1,
"entries":{
"Entry1": 1,
"entry2": 2
}
}
On server side I created a corresponding model:
public class Model
{
public int Prop1 { get; set; }
public IDictionary<string, object> Entries { get; set; }
}
entries field in JSON has a dynamic structure, and because of that IDictionary<string, object> is used in the model.
And then I bind the model:
this.Bind<Model>();
Model is created successfully but problem is that in Entries dictionary both keys are in capital case. For me case is very important and I expect second key to be entry2, not Entry2.
I also tried to use JavaScriptConverter and JavaScriptPrimitiveConverter but in Deserialize method I get already capitalized data.
Any ideas oh how to fix that?
For me this was solved by configuring JavascriptSerializer to retain casing.
Unfortunately I couldn't figure out a clean way to do this, but here's the hack I'm using for now.
public class Model
{
public IDictionary<string, object> Entries { get; set; }
}
public class CustomModelBinder : IModelBinder
{
public bool CanBind(Type modelType)
{
return modelType == typeof(Model);
}
public object Bind(NancyContext context, Type modelType, object instance, BindingConfig configuration, params string[] blackList)
{
using (var sr = new StreamReader(context.Request.Body))
{
return (new JavaScriptSerializer() { RetainCasing = true }).Deserialize<Model>(sr.ReadToEnd());
}
}
}
Nancy will pick up this binder at runtime, no need to explicitly register anything.
This solution is not ideal because it ignores some Nancy features like blacklists and possibly other binding configuration settings.
A better option is to set JsonSettings from Bootstrapper
public class MyBootstrapper : DefaultNancyBootstrapper
{
public MyBootstrapper () : base()
{
JsonSettings.RetainCasing = true;
}
}
Implementing IModelBinder works, but it messes up other default binding settings.
Is it possible to add some additional attributes to my components which are then set/hydrated using some custom logic/perhaps from a data store? Similar to adding some custom builder strategy in cab/unity ?
UPDATE
e.g.
assuming a class has these properties
[MyImport] string name1 { get; set }
[MyImport] MyType name2 { get; set }
[MyGuid] Guid { get; set; }
with custom attributes MyImport and MyGuid which are resolved by an "extension" to MEF ( which gets executed after the [imports] are resolved ) and has code along these lines
// property SET
var valu = myDBStore.GetValue( instanceGUID, propertyInfo.Name);
propertyInfo.SetValue( instance, TypeDescripter.GetConverter(valu).ConvertTo(propertyType), null);
// property GET - for example only, used during dehydration outside of MEF !
var valu = propertyInfo.GetValue( instance, null);
myDBStore.SetValue( instanceGUID, propertyInfo.Name, TypeDescripter.GetConverter(valu).ConvertTo(typeof(string));
// the above is pseudo code only, pls no comments on correct args/syntax :)
EDIT
components which are then set/hydrated using some custom logic/perhaps from a data store
One can do this via an "ExportFactory".
// "ExportFactory"
public sealed class DataStoreProvider
{
[Export(typeof(Model))]
public Model Item
{
get
{
return [custom logic];
}
}
}
public class NeedsModel
{
[Import(typeof(Model))]
public Model Item { get; set; }
}
Initial Answer
This is possible through MEF's Lazy<T, TMetadata>.
public interface ISomeMetadata
{
string UsefulInfo { get; }
}
[MetadataAttribute]
[AttributeUsage(AttributeTargets.Class, AllowMultiple=false)]
public class ExportBaseAttribute : ExportAttribute, ISomeMetadata
{
public ExportBaseAttribute(string usefulInfo)
:base(typeof(BaseExport))
{
UsefulInfo = usefulInfo;
}
public string UsefulInfo { get; private set; }
}
// BaseExport class is not needed.. just showing advanced attribute usage.
public abstract class BaseExport { }
[ExportBase("Useful Filter Information")]
public class SomeExport : BaseExport
{
}
Then, in your host (composer), you can
[ImportMany(typeof(BaseExport))]
Lazy<BaseExport, ISomeMetadata>[] _baseExports
After you compose, you can run a LINQ filter using .Metadata
var goodExports = from export in _baseExports
where export.Metadata.UsefulInfo ...
select export;