I've the following set of classes to map (only in one direction, from Data* to Api*):
// Top level
public class DataEntity
{
public NestedDataEntity Nested { get; set; }
// ... other primitive/complex properties
}
public class ApiEntity
{
public NestedApiEntity Nested { get; set; }
// ... other primitive/complex properties
}
// Nested level
public class NestedDataEntity
{
public string Items { get; set; }
}
public class NestedApiEntity
{
public IEnumerable<ApiSubItem> Items { get; set; }
}
public class ApiSubItem
{
// there are properties here. Not needed for the sake of example
}
Mapping is configured within a Profile as in the following bit of code:
// mapping profile
public class MyCustomProfile : Profile
{
public MyCustomProfile()
{
CreateMap<DataEntity, ApiEntity>();
CreateMap<NestedDataEntity, NestedApiEntity>();
CreateMap<string, IEnumerable<ApiSubItem>>()
.ConvertUsing<TextToSubItemsConverter>();
}
}
// type converter definition
public class TextToSubItemsConverter :
ITypeConverter<string, IEnumerable<ApiSubItem>>
{
public IEnumerable<ApiSubItem> Convert(
string dataItems, IEnumerable<ApiSubItem> apiItems, ResolutionContext context)
{
// actually, deserialize & return an ApiSubItem[]
// here just return some fixed array
return new ApiSubItem[]
{
new ApiSubItem(),
new ApiSubItem(),
new ApiSubItem(),
};
}
}
// Main
public class Program
{
public static void Main(string[] args)
{
Mapper.Initialize(cfg =>
{
cfg.AddProfile<MyCustomProfile>();
});
Mapper.AssertConfigurationIsValid();
DataEntity dataEntity = new DataEntity()
{
Nested = new NestedDataEntity()
{
Items = "Ignored text",
},
};
// This maps ok, no issues
ApiEntity apiEntity = Mapper.Map<DataEntity, ApiEntity>(dataEntity);
IQueryable<DataEntity> dataEntities = new[] { dataEntity }.AsQueryable();
// This exposes the System.Char to ApiSubItem issue
ApiEntity apiEntityProjected = dataEntities.ProjectTo<ApiEntity>().First();
}
}
Mapping configuration passes initial validation at startup, but then when an actual mapping is required I get exception:
System.InvalidOperationException: Missing map from System.Char to my.whatever.namespace.ApiSubItem. Create using Mapper.CreateMap.
If I omit completely the configuration between string and IEnumerable<ApiSubItem>, initial validation complains about the same context:
Context:
Mapping from type System.Char to my.whatever.namespace.ApiSubItem
although when added, it seems like it's not picked up by AutoMapper.
The mapping happens in a static context, through a ProjectTo<ApiEntity>() call on top of LINQ query over DbSet<DataEntity>. I've checked that converter does not require any dependency, just in case.
AutoMapper is 5.0.2, running in an MVC API web application under ASP.Net Core 1 RTM.
I've checked similar issues here on SO, but with no luck. Does anyone know how to make this type of scenario work? I guess I'm not the first one trying to (Auto)map from a string to a collection. TA
EDIT Added both failing and non-failing case to example
EDIT 2 After further search, somewhere someone suggested to ignore the field and perform mapping in AfterMap(). With this profile, exception is not thrown but resulting Items field is null:
CreateMap<DataEntity, ApiEntity>();
CreateMap<NestedDataEntity, NestedApiEntity>()
.ForMember(api => api.Items, options => options.Ignore())
.AfterMap((data, api) =>
{
api.Items = Mapper.Map<IEnumerable<ApiSubItem>>(data.Items);
});
CreateMap<string, IEnumerable<ApiSubItem>>()
.ConvertUsing<TextToSubItemsConverter>();
EDIT 3 Reworded question title to make it more specific for projection
I guess the actual answer is along the lines of "projections and converter/resolvers don't play nicely together". From AutoMapper Queryable.Extensions wiki:
Not all mapping options can be supported, as the expression generated must be interpreted by a LINQ provider. [...]
Not supported:
[...]
Before/AfterMap,
Custom resolvers,
Custom type converters
EDIT Workaround:
In this particular scenario, I've later found to be useful having an intermediate DTO entity that could grab some more fields, not needed for the final API entity but still needed for business logic.
In such DTO entity I left the string field as in DataEntity, so I could use projection and let AutoMapper/LinqToSQL grab only needed fields from DB.
Then, between DTO entity and API entity already in memory, I could apply a second mapping which also took advantage of custom converter for string ==> IEnumerable<ApiSubItem> mapping. HTH
Related
I am having trouble getting a complex object to serialize. I have found few examples on this & need some help. I have a POCO class that implements an Interface for a Class Property, as below...
The problem is...OData cannot serialize the IObjectState Property
HOW DO I MAKE ODATA AWARE of COMPLEX TYPES IN PROPERTIES?
Please keep in mind the IObjectState is a non-entity class & has no key
POCO CLASS:
public class ShakeoutDocument : Document, IDocument, IStateful
{
public IObjectState ObjectState { get; set; } //<-- This property contains the instance (below)
public int ShakeoutId { get; set; }
public string SchedulingBatch { get; set; }
[...] //<-- Other properties are omitted for brevity
}
IObjectState Property CLASS:
Here is an example of a concrete IObjectState class...
// Example of an IObjectState Instance
public class New : IObjectState
{
public List<IObjectStateEvent> Events { get; set; }
public string Name { get; set; }
}
EDM CONFIGURATION: As a BOUND Function:
Using this EDM Model configuration & Api...
modelBuilder.EntitySet<ShakeoutDocument>("ShakeoutDocument");
[HttpGet]
public ShakeoutDocument Get([FromODataUri] int id)
{
var provider = Application.ShakeoutDocumentProvider as ShakeoutDocumentProvider;
var entity = provider.Get(id);
return entity;
}
Generates the following exception...
"The given model does not contain the type 'New'."
EDM CONFIGURATION: As a UNBOUND Function:
Using this EDM Model configuration & Api...
var getShakeoutDocument = modelBuilder.Function("GetShakeoutDocument").ReturnsFromEntitySet<ShakeoutDocument>("ShakeoutDocument");
getShakeoutDocument.Parameter<int>("id");
[ODataRoute("GetShakeoutDocument(id={id})")]
public IHttpActionResult GetShakeoutDocument([FromODataUri] int id)
{
var provider = Application.ShakeoutDocumentProvider as ShakeoutDocumentProvider;
var entity = provider.Get(id);
return Ok(entity);
}
Generates the following exception...
"The given model does not contain the type 'New'."
UPDATING THE EDM CONFIGURATION: with concrete New:
Updating the EDM Model configuration with...
modelBuilder.AddComplexType(typeof(New));
Generates the following exception...
"A value was encountered that has a type name that is incompatible
with the metadata. The value specified its type as 'New', but the type
specified in the metadata is 'IObjectState'."
Any help is appreciated.
Okay, after much searching, I found the answer here. Basically, you have to describe to OData all the possible implementations of any given Complex Property...even for UNBOUND FUNCTIONS.
Unbound really SHOULD imply standard WebApi is being used...but this obviously isn't the case....as this object serializes FINE using standard WebApi, but fails using OData.
private static IEdmModel GetEdmModel()
{
var modelBuilder = new ODataConventionModelBuilder();
// ENTITY SETS: Normal
modelBuilder.EntitySet<tblDbcCommunications>("Communication");
modelBuilder.EntitySet<tblDbcItems>("Item");
modelBuilder.EntitySet<Meter>("Meter");
modelBuilder.EntitySet<Product>("Product");
modelBuilder.EntitySet<WarehouseMeter>("WarehouseMeter");
modelBuilder.EntitySet<Shakeout>("Shakeout");
modelBuilder.EntitySet<tblDbcCircuitOwner>("tblDbcCircuitOwner");
// ENTITY SETS: Complex
ConfigureComplexEntitySet(modelBuilder, modelBuilder.EntitySet<ShakeoutDocument>("ShakeoutDocument"));
// KEY BINDINGS
modelBuilder.EntityType<WarehouseMeter>().HasKey(x => x.METER_ID);
// UNBOUND FUNCTIONS
// - Action = Post
// - Function = Get
// WarehouseMeter
var findPulseMeter = modelBuilder.Function("FindWarehouseMeter").ReturnsCollectionFromEntitySet<WarehouseMeter>("WarehouseMeter");
findPulseMeter.Parameter<string>("search");
// ShakeoutDocument
var getShakeoutDocument = modelBuilder.Function("GetShakeoutDocument").ReturnsFromEntitySet<ShakeoutDocument>("ShakeoutDocument");
getShakeoutDocument.Parameter<int>("id");
// Product
var listProduct = modelBuilder.Function("ListProducts").ReturnsCollectionFromEntitySet<Product>("Product");
return modelBuilder.GetEdmModel();
}
private static EntitySetConfiguration<ShakeoutDocument> ConfigureComplexEntitySet(ODataConventionModelBuilder modelBuilder, EntitySetConfiguration<ShakeoutDocument> configuration)
{
// Collection Properties
configuration.EntityType.CollectionProperty(x => x.Seals);
configuration.EntityType.CollectionProperty(x => x.Details);
// Complex Properties
configuration.EntityType.ComplexProperty(x => x.ObjectState);
// Object State
var newObjectState = modelBuilder.ComplexType<New>();
newObjectState.DerivesFrom<IObjectState>();
var submittedObjectState = modelBuilder.ComplexType<Submitted>();
submittedObjectState.DerivesFrom<IObjectState>();
// Object State Event
var isNewObjectStateEvent = modelBuilder.ComplexType<IsNew>();
isNewObjectStateEvent.DerivesFrom<IObjectStateEvent>();
var isSubmittedObjectStateEvent = modelBuilder.ComplexType<IsSubmitted>();
isSubmittedObjectStateEvent.DerivesFrom<IObjectStateEvent>();
return configuration;
}
Suppose I have a model with 20 fields, and in my index page, I want to list all models that are stored in my database.
In index page, instead of listing all fields of the model, I only to list 3 fields.
So, I make two class:
class CompleteModel {
public int Id { get; set; }
public string Field01 { get; set; }
public string Field02 { get; set; }
public string Field03 { get; set; }
public string Field04 { get; set; }
public string Field05 { get; set; }
...
public string Field20 { get; set; }
}
now, in my Controller, I can use:
await _context.CompleteModel.ToListAsync();
but I feel that it does not seem to be the right way to do it, because I'm getting all fields and using only 3 fields.
So, I made this code:
class ViewModel {
public string Field02 { get; set; }
public string Field04 { get; set; }
public string Field08 { get; set; }
}
var result = _context.CompleteModel.Select(
x => new {
x.Field02,
x.Field04,
x.Field08
}).ToListAsync();
var listResults = new List<IndexViewModel>();
if (result != null)
{
listResults.AddRange(results.Select(x => new IndexViewModel
{
Field02 = x.Field02,
Field04 = x.Field04,
Field08 = x.Field08
}));
}
I think this is a lot of code to do this.
First, I selected all the fields that I want, then, copied everything to another object.
There's a "more directly" way to do the same thing?
Like:
_context.CompleteModel.Select(x => new IndexViewModel { Field02, Field04, Field08 });
You could use AutoMapper to reduce the boiler plate so you're not manually copying field values over.
If you include the AutoMapper NuGet package then you'd need to have the following in your startup somewhere to configure it for your classes:
Mapper.Initialize(cfg => cfg.CreateMap<CompleteModel, ViewModel>());
You could then do something like the following:
var results = await _context.CompleteModel.ToListAsync();
var viewModelResults = results.Select(Mapper.Map<ViewModel>).ToList();
There are a lot of configuration options for the package so do take a look at the documentation to see if it suits your needs and determine the best way to use it if it does.
In my view this is one of the weaknesses of over abstraction and layering. The VM contains the data that is valuable to your application within the context of use (screen, process etc). The data model contains all the data that could be stored that might be relevant. At some point you need to match the two.
Use EF Projection to fetch only the data you need from the database into projected data model classes (using the EF POCO layer to define the query, but not to store the resultant data).
Map the projected classes onto your VM, if there is a naieve mapping, using Automapper or similar. However unless you are just writing CRUD screens a simple field by field mapping is of little value; the data you fetch from your data store via EF is in its raw, probably relational form. The data required by your VM is probably not going to fit that form very neatly (again, unless you are doing a simple CRUD form), so you are going to need to add some value by coding the relationship between the data store and the View Model.
I think concentrating on the count of lines of code would lead to the wrong approach. I think you can look at that code and ask "is it adding any value". If you can delegate the task to Automapper, then great; but your VM isn't really pulling its weight other than adding some validation annotation if you can consistently delegate the task of data model to VM data copying.
I'm having some problems working out how to get Automapper 4.2.1 to allow for a type mapping where the destination value might be null depending on the source value.
Older versions of Automapper allowed an AllowNullDestination flag to be set via the Mapper configuration but I can't find the equivalent recipe for the new version and the old mechanism of configuring via the static Mapper object seems to have been obsoleted.
I have tried the following without success:
Mapper.Configuration.AllowNullDestinationValues = true;
Mapper.AllowNullDestinationValues = true;
Mapper.Initialize(c=>c.AllowNullDestinationValues=true);
Here's a simple test case demonstrating the problem. This fails on the final line with an AutoMapperMappingException since the Substitute method is returning null. I would like both mappings to succeed.
I would prefer to avoid the use of .ForMember in the solution since in the real scenario I'm trying to address, the mapping between bool and 'object' (actually a custom class) should apply across the entire object tree.
Although there are several similar questions here on StackOverflow, I haven't found one that refers to a recent version of Automapper.
Thanks in advance for any suggestions
using AutoMapper;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace AutoMapperTest
{
[TestClass]
public class ExampleTest
{
[TestMethod]
public void NullDestinationCanBeMapped()
{
var mapper = new MapperConfiguration(configuration =>
{
configuration.CreateMap<Source, Target>();
//How should the following mapping be modified to pass the test?
configuration.CreateMap<bool, object>()
.Substitute(i => i ? null : new object());
}).CreateMapper();
var target1 = mapper.Map<Source, Target>(new Source {Member = false}); //succeeds
Assert.IsNotNull(target1.Member); //pass
var target2 = mapper.Map<Source, Target>(new Source {Member = true}); //fails to map with exception
Assert.IsNull(target2.Member); //not reached
}
}
public class Source
{
public bool Member { get; set; }
}
public class Target
{
public object Member { get; set; }
}
}
Instead of using Substitute, use ConvertUsing...
configuration.CreateMap<bool, MyClass>()
.ConvertUsing(i => i ? null : new object());
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
When using a FindOne() using MongoDB and C#, is there a way to ignore fields not found in the object?
EG, example model.
public class UserModel
{
public ObjectId id { get; set; }
public string Email { get; set; }
}
Now we also store a password in the MongoDB collection, but do not want to bind it to out object above. When we do a Get like so,
var query = Query<UserModel>.EQ(e => e.Email, model.Email);
var entity = usersCollection.FindOne(query);
We get the following error
Element 'Password' does not match any field or property of class
Is there anyway to tell Mongo to ignore fields it cant match with the models?
Yes. Just decorate your UserModel class with the BsonIgnoreExtraElements attribute:
[BsonIgnoreExtraElements]
public class UserModel
{
public ObjectId id { get; set; }
public string Email { get; set; }
}
As the name suggests, the driver would ignore any extra fields instead of throwing an exception. More information here - Ignoring Extra Elements.
Yet Another possible solution, is to register a convention for this.
This way, we do not have to annotate all classes with [BsonIgnoreExtraElements].
Somewhere when creating the mongo client, setup the following:
var pack = new ConventionPack();
pack.Add(new IgnoreExtraElementsConvention(true));
ConventionRegistry.Register("My Solution Conventions", pack, t => true);
Yes. Another way (instead of editing you model class) is to use RegisterClassMap with SetIgnoreExtraElements.
In your case just add this code when you initialize your driver:
BsonClassMap.RegisterClassMap<UserModel>(cm =>
{
cm.AutoMap();
cm.SetIgnoreExtraElements(true);
});
You can read more about ignoring extra elements using class mapping here - Ignoring Extra Elements.