I'm trying to use GraphDiff (latest available version in NuGet) to handle what I consider a not terribly difficult entity model. Consider a model like so:
class A
{
public virtual ICollection<B> Bs { get; set; }
}
class B
{
public virtual ICollection<C> Cs { get; set; }
}
If I'm updating an instance of A (call it aEntity), I could do:
context.UpdateGraph(aEntity, map =>
map.OwnedCollection(a => a.Bs, withB =>
withB.OwnedCollection(b => b.Cs)))
Now I'd also, sometimes, like to update B's independently.
context.UpdateGraph(bEntity, map => map.OwnedCollection(b => b.Cs));
So I figured I could "cascade" the changes by introducing a property, like so:
class BMapper {
Expression<Func<IUpdateConfiguration<B>, object>> MappingExpression
{
get
{
return map => map.OwnedCollection(b => b.Cs);
}
}
}
... and then use that in both scenarios like so:
// Update an A and any of its B's
context.UpdateGraph(aEntity, map =>
map.OwnedCollection(a => a.Bs, (new BMapper()).MappingExpression))
// Update a B by itself
context.UpdateGraph(bEntity, (new BMapper()).MappingExpression);
Updating the B by itself works fine, but updating an A falls down in expression land:
Microsoft.CSharp.RuntimeBinder.RuntimeBinderException:
'string' does not contain a definition for 'Body'
Is there a way to share mappings in GraphDiff?
Related
In an ASP.NET Core application I have a profile class (for AutoMapper) like this:
public class CandidateProfile : AutoMapper.Profile
{
public CandidateProfile()
{
CreateMap<Job.Candidate, Job.Candidate>()
.ForMember(x => x.Id, y => y.UseDestinationValue());
}
}
In Startup.cs I registerAutoMapper with DI like this:
services.AddAutoMapper(c =>
{
c.AddProfile<JobProfile>();
c.AddProfile<CandidateProfile>();
c.AddProfile<ApplicationProfile>();
}
My aim is to not change the value of the Id property in the destination object. However the destination Id always gets set to 0 which is the value of the source object.
existingCandidate = _mapper.Map(app.Candidate, existingCandidate);
After calling this code, existingCandidate.Id is 0.
What am I doing wrong?
My aim is to not change the value of the Id property in the
destination object.
So, basically for Id, you don't want the source property to be mapped to the destination property. You want it left alone. Simply Ignore the mapping for that property then -
CreateMap<Candidate, Candidate>()
.ForMember(x => x.Id, y => y.Ignore());
Edit :
Following is the code that is working for me with above configuration (version 10.0) -
public void Test()
{
Candidate appCandidate = new Candidate { Id = 0, Name = "alice" };
Candidate existingCandidate = new Candidate { Id = 4, Name = "bob" };
existingCandidate = _Mapper.Map(appCandidate, existingCandidate);
}
We have a list page where we can enable or disable a thing™ using a <switch /> That thing™ is toggled with an IsActive flag
public class Thing
{
/* ... */
[Reactive] public bool IsActive { get; set; }
}
Given the following change listener, the idea is when the IsActive property changes (user interaction on a toggle switch), we invoke the _saveItemCommand to save the entity with the new IsActiveState.
public ObservableCollection<Thing> DataObjectList {get;} = new ObservableCollection<Thing>();
public MyClass()
{
_saveItemCommand = ReactiveCommand.CreateFromTask(SaveItemInternal);
_listWatcher = DataObjectList
.ToObservableChangeSet()
.AsObservableList()
.Connect()
.WhenPropertyChanged(x => x.IsActive)
.Throttle(TimeSpan.FromMilliseconds(250))
.ObserveOn(RxApp.MainThreadScheduler)
.Select(x => x.Sender)
.InvokeCommand(_saveItemCommand);
}
public void OnNavigatingTo()
{
var newItems = _myService.GetNewItems();
DataObjectList.AddRange(newItems);
}
public void OnDestroy()
{
_listWatcher?.Dispose();
}
The problem I'm having is that when I setup the list, The command seems to be invoked on the last item in the list immediately after AddRange is called.
I have tried using .Skip(1) without any luck, but one thing that seems to work but is ugly is .Skip(DataObjectList.Length)
How can I make it so that the command isn't invoked until the first time the user toggles the switch? What is the correct way to setup this listener?
Most likely you'll want to add a Where statement to indicate it should only be called on the IsActivated switch.
_listWatcher = DataObjectList
.ToObservableChangeSet()
.AsObservableList()
.Connect()
.WhenPropertyChanged(x => x.IsActive)
.Throttle(TimeSpan.FromMilliseconds(250))
.ToCollection()
.Where(x => x.Any(value => value.IsActive))
.ObserveOn(RxApp.MainThreadScheduler)
.Select(x => x.Sender)
.InvokeCommand(_saveItemCommand);
So the two lines I added are
.ToCollection()
.Where(x => x.Any(value => value.IsActive))
The ToCollection() will convert it into an observable list and the Where will restrict your observable to when there is change of the IsActive values.
You may wish to add a FirstAsync() call if you want it to happen only once after the Where() call.
After the comments on Glenn's answer and some additional conversations with Rodney, here's what finally works.
_listWatcher = DataObjectList
.ToObservableChangeSet()
.AsObservableList()
.Connect()
.WhenPropertyChanged(x => x.IsActive)
.Throttle(TimeSpan.FromMilliseconds(250))
.Skip(1)
.DistinctUntilChanged()
.ObserveOn(RxApp.MainThreadScheduler)
.Select(x => x.Sender)
.InvokeCommand(_createActivationsInternal);
I use AutoMapper V.6.1.1 as a mapper in my ASP.Net project.
Before I had configuration as below:
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<A, B>();
cfg.CreateMap<C, D>().ForMember(dest => dest.CityDesc, opt => opt.MapFrom(src => src.City));
});
var mapper = config.CreateMapper();
var var1= mapper.Map<B>(request);
var var2= mapper.Map<List<C>, List<D>>(result);
Now, I want to refactor the code, using Mapper.Initialize(). So I used:
Mapper.Initialize(cfg =>
{
cfg.CreateMap<A, B>();
cfg.CreateMap<C, D>().ForMember(dest => dest.CityDesc, opt => opt.MapFrom(src => src.City));
});
var var1= Mapper.Map<B>(request);
var var2= Mapper.Map<List<C>, List<D>>(result);
I have an run time error:
Missing type map configuration or unsupported mapping. Mapping types: A-> B
Is there any problem with using multiple configurations in Mapper.Initialize? There is no error in the case that has one mapping in Initialize() body. How should I fix the error?
Maybe you have more than one Mapper.Initialize in your project while you should not have multiple Mapper.Initialize in your project else it will become override and you lost previous mapping configurations that you set by Mapper.Initialize. Now It is possible to get the error (Missing type map configuration or unsupported mapping.)
I recommend you to use AutoMapper.Profile. You can warp your mapping configurations in the form of grouped (in separated Profiles) then register all of theme by Mapper.Initialize at once ;)
Look at this example:
public class AB_Profile : Profile {
protected override void Configure() {
CreateMap<A, B>();
// CreateMap<A, B1>();
// CreateMap<A, B2>();
}
}
public class CD_Profile : Profile {
protected override void Configure() {
CreateMap<C, D>()
.ForMember(dest => dest.CityDesc, opt => opt.MapFrom(src => src.City));
}
}
Then initialize the Mapper using above Profiles:
Mapper.Initialize(cfg => {
cfg.AddProfile<AB_Profile >();
cfg.AddProfile<CD_Profile >();
});
Starting version 5 use this, as mentioned on their website...
public class OrganizationProfile : Profile
{
public OrganizationProfile()
{
CreateMap<Foo, FooDto>();
// Use CreateMap... Etc.. here (Profile methods are the same as configuration methods)
}
}
// How it was done in 4.x - as of 5.0 this is obsolete:
// public class OrganizationProfile : Profile
// {
// protected override void Configure()
// {
// CreateMap<Foo, FooDto>();
// }
// }
See Doc
Then initialize the mapping as...
Mapper.Initialize(cfg => {
cfg.CreateMap<Foo, Bar>();
cfg.AddProfile<OrganizationProfile>();
});
I got some really good help to a previous question at: "TypeError.parent.context.car.getBrands is not a function": s
and that is related to my current question. As can seen in that answer to my previous error, my app won't work, unless I create an new instance of "car", but hen I call that method:
getById(id: string) {
return this.http.get('app/car.json'+id)
/*
if I log the incoming data here to the console,
the correct data from server comes, eg: 'id: id, name: name, brands: Array[2]'
*/
.map(data => data.json())
.map(car => new Car(car.id, car.name)); //<== this line causes problem!
}
receiving component:
routerOnActivate(curr: RouteSegment): void {
let id = curr.getParam('id');
this._service.getById(id)
.subscribe(car => {
// this code is executed when the response from the server arrives
this.car = car;
console.log("res: ", this.car);// <=== correct car, without the array of brands
});
// code here is executed before code from the server arrives
// event though it is written below
}
it creates a new instance "Car". Well that is all good, but the Car also contains an Array of Brands.
My service looks like this:
#Injectable()
export class Service {
constructor(private http: Http) { }
getCars(){
return this.http.get...
}
getById(id: string) {
return this.http.get...
}
}
and my Car class like:
export class Car {
private brands: Array<Brand>;
constructor(public id: string, public name: string) {
this.brands = new Array<Brand>();
}
public getBrands(): Array<Brand> {
return this.brands;
}
//some other methods.
}
So I also have some data in the brands Array, but since the getById method creates a new car, it only takes the parameter id and name, and the brands array becomes empty! I don't know how to get the data from the server side so that it includes the array of brands!
I've (desperately) tried creating an Car in my service, which does log the correct data... but obviously doesn't work.
getById(id: string) {
this.http.get('app/car.json'+id)
.map((res: Response) => res.json())
.subscribe(car => {
//this code is executed when the response from the server arrives
this.car = car;
console.log("res: ", this.car); // <==== correct data!
return this.car;
});
//return this.car placed here doesn't give void error, but returns an undefined car, since the code gets executed before subscribe!
}
and receiving component:
routerOnActivate(curr: RouteSegment){
let id = curr.getParam('id');
this.car = this._service.getById(id); //error: Type 'void' is not assignable to type 'Car'
}
Any advice to give? Thanks!
It's been ages, but I thought I would post the solution to my problem. I had to create a static method to get the app to work. As follows:
getById(id:string)
return this.http.get('app/car.json'+id)
.map(data => data.json())
.map(data => Car.carFromJSON(data))
Then in my Car class:
static carFromJSON(json) {
let id = json.id
let name = json.name
let brands: Brand[] =
json.brands.map (brand => new Brand())
return new Car(id, name, brands)
}
You're initializing your car with empty array every time:
export class Car {
private brands: Array<Brand>;
constructor(public id: string, public name: string) {
this.brands = new Array<Brand>(); <-- every new Car() will end up with empty array
}
public getBrands(): Array<Brand> {
return this.brands;
}
//some other methods.
}
You have to extend your constructor with brands:
constructor(public id: string, public name: string, public brands: Brand[]) {}
And then call:
getById(id: string) {
return this.http.get('app/car.json'+id)
.map(data => data.json())
.map(car => new Car(car.id, car.name, car.brands)); // <-- add brands
}
I'm trying to extend the UserType with a custom AccountPart.
I don't want user account id to be a content part id, i want AccountId field to be an independent auto increment 5 digit number (like 10300, 10301) or at least auto generated Guid.
I've tried number of times to achieve it, but still didn't.
Here is on of my tries. The result is 00000000-0000-0000-0000-000000000000 for the 1st user, and for the second i got an exception. The Guid didn't generated =(.
AccountPart:
public Guid AccountId
{
get { return Record.AccountId; }
set { Record.AccountId = Guid.NewGuid(); }
}
public decimal RealMoney
{
get { return Record.RealMoney; }
set { Record.RealMoney = value; }
}
public decimal VirtualMoney
{
get { return Record.VirtualMoney; }
set { Record.VirtualMoney = value; }
}
public decimal BonusPoints
{
get { return Record.BonusPoints; }
set { Record.BonusPoints = value; }
}
Migratord:
SchemaBuilder.CreateTable("AccountPartRecord",
table => table
.ContentPartRecord()
.Column<Guid>("AccountId",
x => x
.WithType(DbType.Guid)
.Unique()
.NotNull()))
ContentDefinitionManager.AlterPartDefinition("AccountPart",
builder => builder.Attachable());
ContentDefinitionManager.AlterTypeDefinition("User",
cfg => cfg.WithPart("AccountPart"));
Also i've tried to add AccountId this way:
SchemaBuilder.ExecuteSql(#"
ALTER TABLE [BetIt].[dbo].[Betit_AccountPartRecord]
ADD AccountId UNIQUEIDENTIFIER DEFAULT NEWID()");
It behaves the same - sets emty Guid to each new user. But if i open Sql Management Studio an add new record manually it'll generate a random Guid.
I'm completely confused...
Update:
I've actually acheived auto Guid generation for the Content Part by using handlers. But it looks more like workaround to me. I don't belive this is the only way to generate unique value.
Migrators:
SchemaBuilder.CreateTable("AccountPartRecord",
table => table
.ContentPartRecord()
.Column<long>("AccountId",
x => x
.WithType(DbType.Guid)
.NotNull()
.Unique())
Part:
public class AccountPart : ContentPart<AccountPartRecord>
{
public Guid AccountId
{
get { return Record.AccountId; }
set { Record.AccountId = value; }
}
}
Handler:
public class AccountPartHandler : ContentHandler
{
public AccountPartHandler(IRepository<AccountPartRecord> repository)
{
Filters.Add(new ActivatingFilter<AccountPart>("User"));
Filters.Add(StorageFilter.For(repository));
OnCreating<AccountPart>((context, accountPart) =>
accountPart.AccountId = Guid.NewGuid());
}
}
Setting up values through handler is not a workaround and it's actually the preferred way (take a look eg. at how IdentityPart unique identifier is set). All logic should be kept in code, not database (which is just a data store). This means that Guid and/or custom identifier generation should be there as well.
Please note that Orchard always creates an empty record first and then fills it with values. In this case columns with NotNull and/or Unique constraints can cause problems. Try to avoid them.