Fluent nHibernate , IDictionary Confusion - c#

Using the following classes..
public class Trait
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
}
public class Sheet
{
public virtual int Id { get; set; }
public virtual IDictionary<Trait, int> Influences { get; set; }
}
I have tried to map them using Fluent nHibernate, as such.
public class TraitMap : ClassMap<Trait>
{
public TraitMap()
{
Id(x => x.Id);
Map(x => x.Name);
Table("Traits");
}
}
public class SheetMap : ClassMap<Sheet>
{
public SheetMap()
{
Id(x => x.Id);
HasManyToMany<Trait>(x => x.Influences)
.Schema("Sheets")
.Table("Influences")
.ParentKeyColumn("Trait")
.ChildKeyColumn("Sheet")
.AsMap<int>("Rating")
.Cascade.All();
Table("Sheets");
}
}
This does not work. I get the exception.
Exception occurred getter of Trait.Id
Now, if I change the Dictionary up to look like this..
public class Sheet
{
public virtual int Id { get; set; }
public virtual IDictionary<int, Trait> Influences { get; set; }
}
Basically making the int the Key, and the Trait the value (not what I want), it does work. Can anyone explain this, and how I can more appropriately reproduce what I am trying to do?
I think the reasoning is because when I specify HasManyToMany<Trait> I am specifying the Value element of the collection. However this is not my intention.
I want to look things up by the Name of the Key, not the Name of the Value. While I realize this is technically an 'acceptable' solution, it kind of goes against the Dictionary convention. I'd prefer to take a more convention over solution approach, if at all possible - and I'd like to better understand what is actually going on under the hood.

What you want is what <composite-index> mapping in hbm would give you. I believe that instead of AsMap, you'd want AsEntityMap. However, look at this thread which talks about a rewrite (in august) of the fluent mapping for Maps that makes all of the above obsolete.
EDIT : For AsEntityMap, and other options, take a look at this SO question and answer
HasMany<Trait>(x => x.Influences)
.KeyColumn("key column name")
.AsEntityMap("referenced column name")
.Entity("dict value", v=> v.Type<int>());
Also, you say you're on the latest version - the latest release is 1.1, but trunk is 2.0, and is very different, and much further along. If you're not on 2.0+, you wouldn't see the methods posted in most of the help threads like the one linked above.

Related

Automapper flattening not behaving as expected

I'm trying to flatted out a nested object. All of the documentation and stack threads seem to suggest the solution is very simple, but it just isn't working.
Consider the following (simplified) objects:
public class CRouteHold
{
public int HoldId { get; set; }
public virtual Hold? Hold { get; set; }
public bool? fhandstart { get; set; }
public bool? ffootstart { get; set; }
public bool? rhandstart { get; set; }
public bool? rfootstart { get; set; }
}
public class Hold
{
public double? xpos { get; set; }
}
and then a flat Dto I want to map to:
public record CRouteHoldDto(
int HoldId,
bool fhandstart,
bool ffootstart,
bool rhandstart,
bool rfootstart,
double xpos
);
Now, I've tried a bunch of mapping profiles, some work and some don't. The one I want to work and which seems like it should work doesn't. Here's what I've tried.
Simple unflatted setup: If I comment out the xpos on the dto and just map the 'root' layer of the RouteHold object like so:
CreateMap<CRouteHold, CRouteHoldDto>();
This works, and gives a CRouteHoldDto.
Flattening setup using a naming convention - if I change the name of xpos on the dto to HoldXpos (camelcase of ObjectProperty) then automapper figures it out and maps the xpos from the child Hold on CRouteHold to the HoldXpos property. The mapping profile is the same as above. Nothing needs to change.
Flatting without naming convention, using ForMember - I don't want the long HoldXpos name on my Dto. I just want xpos, the same as on the child Hold. To do this, the documentation and a myriad of Stack threads seem to suggest I can use a mapping profile like this:
CreateMap<CRouteHold, CRouteHoldDto>()
.ForMember(rh => rh.xpos, m => m.MapFrom(rh => rh.Hold.xpos));
However, it just doesn't seem to work. It throws an error about needing an empty constructor or optional args.
Am I missing something here?
Edit: From comments I also tried using the IncludeMembers by creating an additional map from Hold to CRouteHoldDto:
CreateMap<CRouteHold, CRouteHoldDto>()
.IncludeMembers(s => s.Hold);
CreateMap<Hold, CRouteHoldDto>(MemberList.None);
Unfortuantely, the result is the same.
SOLUTION
Thanks to #LucianBargaoanu. For those in the future, the exact mapping profile now looks like this:
CreateMap<CRouteHold, CRouteHoldDto>()
.ForCtorParam("xpos", m => m.MapFrom(s => s.Hold.xpos))
.ForCtorParam("otherproperty", m => m.MapFrom(s => s.Hold.otherproperty))
So just keep adding on the ForCtorParam for each property you want to map.
You need ForCtorParam instead of ForMember. A record is immutable, so in fact you're doing constructor mapping.
See https://docs.automapper.org/en/latest/Construction.html.

Multiple discriminators for same type

I got the following types
internal enum IssueType
{
First,
Second,
Special
}
internal class Issue
{
public int Id { get; set; }
public string Title { get; set; }
public IssueType Type { get; set; }
}
internal class SpecialIssue : Issue
{
public string Payload { get; set; }
}
Some issue types map to certain subclasses, like Special would map to SpecialIssue in this case. Others just map to Issue i.e. there are several IssueTypes without special subclasses.
Now I would like to have an Issues table to hold all these issues so I configured Issue.Type as a discriminator value.
builder.HasDiscriminator(x => x.Type)
.HasValue<Issue>(IssueType.First)
.HasValue<Issue>(IssueType.Second)
.HasValue<SpecialIssue>(IssueType.Special);
Unfortunately it seems that I cannot set multiple discriminator values for the same type so at runtime I get errors stating that some discriminators weren't mapped although I did.
What I would like to achieve is to map all subclasses to their respective discriminators and have some kind of "fallback" that maps to Issue (or just map any remaining IssueType to the Issue type manually).
Is it possible to achieve this? I couldn't figure out any way to get EFCore to do this.
I'm currently using EFCore 5.0.10.

EF Core, include derived type property

I have a small problem with a specific include statement.
My datastructure is as follows:
[Table("item")]
public class Item
{
public int Id { get; set; }
public string ItemCode { get; set; }
...
}
public abstract class DerivedItemAbstractBase : Item
{
[ForeignKey("ItemId")]
public List<Assignment> Assignments { get; set; }
...
}
[Table("item")]
public class DerivedItemA : DerivedItemAbstractBase
{
...
}
[Table("item")]
public class DerivedItemB : DerivedItemAbstractBase
{
...
}
public class ItemContext : DbContext
{
public DbSet<Item> Items { get; set; }
...
}
Now I want to get a list of all DerivedItemA and include properties of it.
I have the following method:
public List<DerivedItemA> GetDerivedItemsA()
{
var list = _context.Items
.Include(x => (x as DerivedItemA).Assignments)
.ToList();
}
This code compiles just fine and is something I have found on stackoverflow.
However executing this results in an exception with the short message Invalid include.
I dont know how to solve this problem.
The project is a database-first approach so I have no control over the database.
All items are stored in the same table item.
I cannot create multiple DbSets because there is no discriminator column in the table and I
cannot configure a custom discriminator in code because it would need to discriminate based on multiple properties and not a single property.
Is there any other way of doing this?
Currently I am solving it by iterating through all ItemContext.Items then doing a .Select() on each and creating a new DerivedItemA. After that I manually set every Assignment by iterating from the Assignment table.
However this approach takes far too long and it would be a lot quicker if I could just include it in the initial query.

Unable to cast object of type 'System.Collections.ArrayList' to type 'System.Collections.Generic.IEnumerable'

I recently upgraded Windows SmartClient solution from nHibernate 2.2 to 4.0 and am getting an exception when writing to the db.
The exception is thrown at this code:
this.session.Save(this.Location); // NHibernate.ISession
tx.Commit(); // exception thrown here
The exception is:
'System.InvalidCastException' in NHibernate.dll
System.InvalidCastException:
Unable to cast object of type 'System.Collections.ArrayList'
to type 'System.Collections.Generic.IEnumerable`1[System.Object]'.
There are several lists in the object being saved, here is a couple representative ones:
protected System.Collections.IList locationList;
public virtual System.Collections.IList AssociatedLocationList
{
get
{
if (this.locationList == null)
{
this.locationList = new System.Collections.ArrayList();
}
return this.locationList;
}
set { this.locationList = value; }
}
protected System.Collections.Generic.IList<Inspection> inspectionList;
public virtual System.Collections.Generic.IList<Inspection> InspectionList
{
get
{
if (this.inspectionList == null)
{
this.inspectionList = new System.Collections.Generic.List<Inspection>();
}
return this.inspectionList;
}
set { this.inspectionList = value; }
}
Note that some have a type specified and some don't.
One suggestion here is set the property to an IList, but I already have it as that.
What can be done?
Support for persistent non-generic collections was removed in NHibernate 4.0. Convert to generic collection instead.
See list of breaking changes in NHibernate 4.0 release notes.
The problem here could be with latest version of NHibernate ..i.e. 4.0
you have following options.
1) Most of the time, new version support backward compatibility. If it is the case, look for overload versions of iSession.Save method. You may get non-generic as well.
2) Possibly new save method only support generic type. Yours is non-generic i.e. ArrayList. If you can change it to Ilist<>, that should help.
3) If you do not have control on Ilis, then you may write converter in between which can convert your Arraylist to Ilist<> and it would work with Save method.
Hope this helps.
I hope I'm understanding your question right and if so, I'm not sure you need to be doing your null check at all. Instead your parent class should have a list of locations that are held in another table entirely.
This is class in which your location list is.
public class Parent
{
public virtual Guid Id { get; set; }
public virtual IList<Location> Locations { get; set; }
//This is the mapping for this class.
public class ParentMapping : ClassMap<Parent>
{
Id(x => x.Id).GeneratedBy.Guid();
//This is what relates your location list to this parent.
//Notice that in the Location object below,
//there's a Owner property which will point back to here.
HasMany(x => x.Locations).Cascade.All();
}
}
And this is the class that defines your locations.
public class Location
{
public virtual Guid Id { get; set; }
public virtual Parent Owner { get; set; }
public virtual string SomeProperty { get; set; }
//This is the mapping for this class.
public class LocationMapping : ClassMap<Location>
{
Id(x => x.Id).GeneratedBy.Guid();
Map(x => x.SomeProperty);
//This will relate our property back to the parent.
References(x => x.Owner);
}
}

Using string enums in NHibernate - GetValue never gets fired

In my NHibernate driven app I have classes where some properties of the class are not full blown entities. I.e., I have a customer class that has
a customer type property. It seems a waste of resources to create a separate class for the customer type property.
What I'm trying to do is to map the customer type to an enum string in NHibernate 3.2 using mapping by code. Searching on StackOverflow,
I found a possible solution with the following code:
public class Customer
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual CustomerType CustomerType { get; set; }
}
public class CustomerMap : ClassMapping<Customer>
{
public CustomerMap()
{
Id(x => x.Id);
Property(x => x.Name, m => m.Length(100));
Property(x => x.CustomerType, m =>
{
m.Column("CustomerTypeId");
m.Type(typeof (CustomerTypeMap), null);
});
}
}
public enum CustomerType
{
Big_Customer,
Small_Customer
}
public class CustomerTypeMap : EnumStringType<CustomerType>
{
public override object GetValue(object code)
{
return code == null
? string.Empty
: code.ToString().Replace('_', ' ');
}
public override object GetInstance(object code)
{
var str = (string)code;
if (string.IsNullOrEmpty(str)) return StringToObject(str);
else return StringToObject(str.Replace(" ", "_"));
}
}
This solution works only partially. As you see from the code, I'm trying to make the enum string look nice by replacing the underscores in order to get "Big customer" instead of "Big_Customer". This doesn't work. Putting break lines into the code, I noticed that the overrriden GetInstance function gets called as it is supposed to, but the GetValue function never gets fired. Any help would be appreciated.
I agree you shouldn't create a new entity for something like CustomerType. NHibernate doesn't expect you to either. Fluent NHibernate will map enums as strings by default (I assume you're using the latest version), but apparently you want to have more fine-grained control.
It's been a while since I used Fluent NH, but maybe try this (unless the API has been changed):
Map(x => x.CustomerType).Column("CustomerTypeId").CustomType<CustomerTypeMap>();
Once again, not sure if this (still) works as the API changed/changes a lot.

Categories