Fluent NHibernate mapping a dictionary with a key that is a component - c#

I've created the following two classes with their respective mappings:
public class ParentClass
{
public int Id { get; set; }
public IDictionary<YearMonth, ChildClass> SomeMap { get; set; }
}
public class ParentClassMap : ClassMap<ParentClass>
{
public ParentClassMap()
{
Table("ParentClass");
Id(x => x.Id);
HasMany(x => x.SomeMap)
.Table("ChildClass")
.KeyColumn("ParentId")
.AsMap(x => x.YearMonth);
}
}
public class ChildClass
{
public int Id { get; set; }
public YearMonth YearMonth { get; set; }
}
public class ChildClaassMap : ClassMap<ChildClass>
{
public ChildClaassMap()
{
Table("ChildClass");
Id(x => x.Id);
Component(x => x.YearMonth, x =>
{
x.Map(Reveal.Member<YearMonth>("_dateTime"))
.Access.Field()
.Column("`Date`");
});
}
}
When testing the mapping for the ParentClass, though, the following error is thrown:
[TestFixture]
public class ParentClassMapTests : IntegrationTestBase
{
[Test]
public void Should_be_retrievable()
{
new PersistenceSpecification<ParentClass>(GetSession(), new PersistentObjectEqualityComparer())
.CheckProperty(Reveal.Member<ParentClass>("_dataDictionary"), new Dictionary<YearMonth, BenchmarkPieceData> { { new YearMonth(2010, 1), new BenchmarkPieceData(null, new YearMonth(2010, 1), 0) } })
.VerifyTheMappings();
}
}
NHibernate.MappingException : Could not determine type for: BenchmarkPlus.CommonDomain.ValueObjects.YearMonth, BenchmarkPlus.CommonDomain, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null, for columns: NHibernate.Mapping.Column(YearMonth)
at NHibernate.Mapping.SimpleValue.get_Type()
at NHibernate.Mapping.SimpleValue.IsValid(IMapping mapping)
at NHibernate.Mapping.IndexedCollection.Validate(IMapping mapping)
at NHibernate.Cfg.Configuration.ValidateCollections()
at NHibernate.Cfg.Configuration.Validate()
at NHibernate.Cfg.Configuration.BuildSessionFactory()
NHibernate\SessionFactoryBuilder.cs(24,0): at BenchmarkPlus.RMS.Infrastructure.NHibernate.SessionFactoryBuilder.BuildFactory()
NHibernate\SessionFactoryBuilder.cs(13,0): at BenchmarkPlus.RMS.Infrastructure.NHibernate.SessionFactoryBuilder..ctor(IConfigurationFactory configurationFactory)
If the type is changed from a YearMonth to a DateTime, the mapping works fine. Am I suppose to be able to map to an IDictionary with my YearMonth type as the key? If so, what am I doing wrong?

Related

Prevent EF Core from generating foreign key

I'd like to create an entity that keeps a reference to a clone of itself, and that clone would actually be serialised to json before saving to database.
public class Foo
{
public string StringProperty { get; set; }
public int IntProperty { get; set; }
public Foo Snapshot { get; set; }
}
public class FooConfiguration : IEntityTypeConfiguration< Foo >
{
public virtual void Configure( EntityTypeBuilder< Foo > builder )
{
builder.Property( e => e.StringProperty );
builder.Property( e => e.IntProperty )
.IsRequired();
builder.Property( e => e.Snapshot )
.HasConversion( new FooToJsonConverter() );
}
}
The problem is that because EF knows about Foo (it is referenced in the context and there is a fluent configuration file for it), it creates a foreign key.
Even when I try to ignore it with
builder.Ignore( e => e.Snapshot )
I have successfully serialised another type with a custom converter, but that other type is unknown to EF (no reference in the context and no fluent configuration file).
Is there a way to achieve this?
The problem is that because EF knows about Foo (it is referenced in the context and there is a fluent configuration file for it), it creates a foreign key.
You can create special type - wrapper - EF will not know about.
public class SnapshotWrapper<T>
{
public T? Snapshot { get; set; }
public string Serialize() => JsonSerializer.Serialize(Snapshot);
public static SnapshotWrapper<T> CreateFromJson(string json)
{
if (json == null)
throw new ArgumentNullException(nameof(json));
return new SnapshotWrapper<T>
{
Snapshot = JsonSerializer.Deserialize<T>(json)
};
}
}
Then define interface to identify entities with snapshots:
public interface IHasSnapshot<T>
{
SnapshotWrapper<T> Snapshot { get; }
}
Example for Foo:
public class Foo : IHasSnapshot<Foo>
{
public Foo(int id, string name, int age) : this()
{
FooId = id;
FooName = name ?? throw new ArgumentNullException(nameof(name));
FooAge = age;
}
//to follow DRY principle
//you can specify some SnapshotBase base type for doing this
public Foo()
{
Snapshot = new SnapshotWrapper<Foo>
{
Snapshot = this
};
}
public int FooId { get; set; }
public string? FooName { get; set; }
public int FooAge { get; set; }
[JsonIgnore]
public SnapshotWrapper<Foo> Snapshot { get; set; }
}
Moreover, you can else specify some base EntityTypeConfiguration for such entities:
public abstract class WithSnapshotEntityTypeConfigurationBase<T> : IEntityTypeConfiguration<T>
where T : class, IHasSnapshot<T>
{
public virtual void Configure(EntityTypeBuilder<T> builder)
{
builder.Property(x => x.Snapshot).HasConversion(
x => x.Serialize(),
str => SnapshotWrapper<T>.CreateFromJson(str));
}
}
public class FooConfiguration : WithSnapshotEntityTypeConfigurationBase<Foo>
{
public override void Configure(EntityTypeBuilder<Foo> builder)
{
base.Configure(builder);
builder.HasKey(x => x.FooId);
builder.Property(x => x.FooName).IsRequired().HasMaxLength(200);
builder.HasData(
new Foo(1, "John Doe", 30),
new Foo(2, "Jane Smith", 20),
new Foo(3, "Billy The Drunken", 40),
new Foo(4, "James Webb", 60),
new Foo(5, "Old president", 40));
}
}
This works. A couple of downsides:
Need to mark Snapshot property with [JsonIgnore]
No any constraints for T in IHasSnapshot<T>, so you can write other than Foo (class Foo : IHasSnapshot<Bar>), but it is not critical.

AutoMapper - Mapping to Nested Destination List

I am new to AutoMapper and I'm looking to map a Nested List to my Parent DTO, here is my DTO:
public class ParentDTO
{
//Other Members
public List<HostInfo> HostInfo { get; set; }
}
public class HostInfo
{
//Other Members
public List<PluginInfo> Plugins { get; set; }
}
public class PluginInfo
{
//More Members
}
Here is my Profile:
public ParentProfile()
{
//Other Mappings
CreateMap<Plugin, PluginInfo>().ReverseMap();
CreateMap<Source, ParentDTO>()
.ForMember(dto => dto.HostInfo, opt => opt.MapFrom(x => SourceLookUp.Select(x => x.Data))))
.ForPath(dto => dto.HostInfo.Select(x=> x.Plugins), opt => opt.MapFrom(x => x.SourceLookUp.Select(x => x.Plugin)))
}
However the following line:
.ForPath(dto => dto.HostInfo.Select(x=> x.Plugins), opt => opt.MapFrom(x => x.Source.SourceLookUp(x => x.Plugin)))
Creates this error:
System.ArgumentOutOfRangeException: 'Only member accesses are allowed. dto => dto.HostInfo.Select(x => x.Plugins) Arg_ParamName_Name
Here is my desired output:
#ParentDTO
{
//other data,
"hostInfo": [
{
//Other Data
"Plugin": [
{
//Other Data
},
{
//Other Data
}
]
}
]
}
Here is my current output when removing the troubled code stated above:
#ParentDTO
{
//other data,
"hostInfo": [
{
//Other Data
"Plugin": Null
]
}
UPDATE:
Here is my Source Class and other relevant classes/members, they are generated by Entity Framework using db-scaffold:
public partial class Source
{
public Source()
{
SourceLookUps = new HashSet<SourceLookUp>();
}
//Other Members
public virtual ICollection<SourceLookUp> SourceLookUps { get; set; }
}
Plugin Class:
public partial class Plugin
{
public Plugin()
{
SourceLookUps = new HashSet<SourceLookUp>();
}
//Other Members
public virtual ICollection<SourceLookUps > SourceLookUps { get; set; }
}
SourceLookUps Class
public partial class ScanLookUp
{
//Other Members
public virtual Plugin Plugin { get; set; }
public virtual Source Source{ get; set; }
}
Finally, I interact with Auto Mapper using a .Net 6.0 WebAPI:
[HttpGet("{id}")]
public ParentDTO Get(int id)
{
var data= _context.Source.First(x => x.Id == id);
return _mapper.Map<ParentDTO>(data);
}

Fluent NHibernate: ISet of base class

In my project I have a base class (not mapped):
public abstract class BaseEntity
{
public virtual string Name { get; set; }
public virtual string Description { get; set; }
}
Also I have a few inherited classes (they look all almost the same, so here is a code and map for only one)
public class User : BaseEntity
{
public virtual int UserId { get; set; }
public virtual string Login { get; set; }
public virtual string PasswordHash { get; set; }
public virtual ISet<BaseEntity> Entities { get; set; }
}
public class UserMap : ClassMap<User>
{
public UserMap()
{
this.Id(x => x.UserId);
this.Map(x => x.Login);
this.Map(x => x.PasswordHash);
this.HasManyToMany<BaseEntity>(x => x.Entities);
}
}
Next, I have a NHibernateHelper:
public class NHibernateHelper
{
public static ISession OpenSession()
{
ISessionFactory sessionFactory = Fluently.Configure()
.Database(MsSqlConfiguration.MsSql2008.ConnectionString(#"someconstring")
.ShowSql()
)
.Mappings(m => m.FluentMappings.AddFromAssemblyOf<User>())
.ExposeConfiguration(cfg => new SchemaUpdate(cfg).Execute(false, true))
.BuildSessionFactory();
return sessionFactory.OpenSession();
}
}
And here is a question:
How can I exclude BaseEntity class from mapping, if I need table like EnitiyToEntity in my Database for many-to-many relationship?
Take a look to this:
https://www.codeproject.com/Articles/232034/Inheritance-mapping-strategies-in-Fluent-Nhibernat
If I understand your question the solution should be to implement TPC (Table per concrete class).
By the way, in your mapping you have to use the concrete type for HasManyToMany.
For example (I supposed your user is referenced to many groups):
HasManyToMany<Group>(x => x.Entities).Table("UsersGroups");
where the Group class is something like this:
public class Group : BaseEntity
{
public virtual int GroupId { get; set; }
public virtual string PasswordHash { get; set; }
public virtual ISet<BaseEntity> Members { get; set; }
}
And in the GroupMap class you can reference the users like this:
HasManyToMany<User>(x => x.Members).Table("UsersGroups");
If you reference a class you have to map it. So map Entity as ClassMap and all the others as SubclassMap. They will end up as union subclass which is one table per class. Unfortunatly you can not map a hasmanytoany with FNH.
You can map it as hasmanytomany and work around it:
var config = new Configuration();
config.BeforeBindMapping += BeforeBindMapping;
_config = Fluently
.Configure(config)
...
private void BeforeBindMapping(object sender, NHCfg.BindMappingEventArgs e)
{
var userclass = e.Mapping.RootClasses.FirstOrDefault(rc => rc.name.StartsWith(typeof(User).FullName));
if (userclass != null)
{
HbmSet prop = (HbmSet)paymentclass.Properties.FirstOrDefault(rc => rc.Name == "Entities");
prop.Item = new HbmManyToAny // == prop.ElementRelationship
{
column = new[]
{
new HbmColumn { name = "entityType", notnull = true, notnullSpecified = true },
new HbmColumn { name = "entity_id", notnull = true, notnullSpecified = true }
},
idtype = "Int64",
metatype = "String",
metavalue = typeof(Entity).Assembly.GetTypes()
.Where(t => !t.IsInterface && !t.IsAbstract && typeof(Entity).IsAssignableFrom(t))
.Select(t => new HbmMetaValue { #class = t.AssemblyQualifiedName, value = t.Name })
.ToArray()
};
}
}

nhibernate mapping An association from the table Image refers to an unmapped class

I have two classes MyArticle and Image.
public class MyArticle : IArticle{
....
public virtual List<Image> Images {get; set;}
}
public class Image{
...
public IArticle Article {get; set;}
}
using nhibernate mapping by code I mapped those
public class ImageMap : ClassMapping<Image>
{
public ImageMap()
{
ManyToOne(x => x.Article, m =>
{
m.NotNullable(true);
});
}
}
public class MyArticleMap: ClassMapping<MyArticle>
{
public MyArticleMap()
{
Bag(x => x.Images,
c => { },
r => { r.OneToMany(); }
);
}
}
When tried to unit test mapping it failes with error
{"An association from the table Image refers to an unmapped class: MyApp.Model.IArticle"}
Try this:
public class ImageMap : ClassMapping<Image>
{
public ImageMap()
{
ManyToOne(x => x.Article, m =>
{
m.NotNullable(true);
m.Class(typeof(MyArticle));
});
}
}
This mapping is wrong
public class MyArticleMap: ClassMapping<MyArticleMap>{...}
It should be
public class MyArticleMap: ClassMapping<MyArticle>{...}
And also the list of Images should be of Interface type like this.
public virtual IList<Image> Images {get; set;}

Fluent NHibernate child class mapping using DiscriminateSubClassesOnColumn issue

I have an issue with mapping, simplified my relationship looks like this.
I have parent class:
public abstract class DocumentType
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
}
and two subclasses:
public class UploadedFileDocument : DocumentType
{
}
public class ApplicationFormDocument : DocumentType
{
}
mapped like this:
public DocumentTypeMap()
{
Schema("Core");
Id(x => x.Id);
Map(x => x.Name).Length(128).Not.Nullable();
DiscriminateSubClassesOnColumn("Type");
}
public class UploadedFileDocumentMap : SubclassMap<UploadedFileDocument>
{
}
public class ApplicationFormDocumentMap : SubclassMap<ApplicationFormDocument>
{
}
Then I have another entity with a FK to DocumentType, mapped like this:
public FileConversionMap()
{
Schema("Core");
Id(x => x.Id);
References(x => x.Application).Not.Nullable();
References(x => x.DocumentType).Not.Nullable().Fetch.Select();
}
my issue is, when I retrieve rows from the DB like this:
Session.Query<FileConversion>().AsQueryable();
all the rows come back with the DocumentType being of type DocumentType, not of the child type (ie the actual type of that property, ie. when i do .GetType(), either UploadedFileDocument or ApplicationFormDocument)
Apologies if this is just me being dim. But how can I determine which type of DocumentType I have ... is my mapping wrong?
When you look at your generated SQL (adding .ShowSQL() to your .Database method), do you see the Type being entered? You should see something similar to:
INSERT
INTO
"Core_DocumentType"
(Name, Type)
VALUES
(#p0, 'ApplicationFormDocument');
select
last_insert_rowid();
#p0 = 'afd' [Type: String (0)]
Using the mappings you provided, it looks fine and I could return the DocumentType (using SQLite) just fine.
Here's the code I used to reproduce it. I didn't have your FileConversion object, so please verify that it matches what you need.
DocumentType
public class DocumentType
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
}
public class DocumentTypeMap : ClassMap<DocumentType>
{
public DocumentTypeMap()
{
GenerateMap();
}
void GenerateMap()
{
Schema("Core");
Id(x => x.Id).GeneratedBy.Identity();
Map(x => x.Name).Length(128).Not.Nullable();
DiscriminateSubClassesOnColumn("Type");
}
}
UploadFileDocument/ApplicationFormDocument
public class UploadedFileDocument : DocumentType
{
public virtual string ContentType { get; set; }
}
public class ApplicationFormDocument : DocumentType
{
}
public class UploadFileDocumentMap :
SubclassMap<UploadedFileDocument>
{
public UploadFileDocumentMap()
{
GenerateMap();
}
void GenerateMap()
{
Map(x => x.ContentType);
}
}
public class ApplicationFormDocumentMap :
SubclassMap<ApplicationFormDocument>
{
}
FileConversion
public class FileConversion
{
public virtual int Id { get; set; }
public virtual DocumentType DocumentType { get; set; }
}
public class FileConversionMap : ClassMap<FileConversion>
{
public FileConversionMap()
{
GenerateMap();
}
void GenerateMap()
{
Schema("Core");
Id(x => x.Id).GeneratedBy.Identity();
References(x => x.DocumentType).Not.Nullable().Fetch.Select();
}
}
The tests I used (using machine.specifications):
Context
public class when_discriminating_on_subclass
{
static IList<FileConversion> results;
Establish context = () =>
{
using (var session = DataConfiguration.CreateSession())
{
using (var transaction = session.BeginTransaction())
{
var upload = new UploadedFileDocument
{ Name = "uploaded", ContentType = "test" };
var form = new ApplicationFormDocument
{ Name = "afd" };
session.Save(form);
session.Save(upload);
var formConversion =
new FileConversion { DocumentType = form };
var uploadConversion =
new FileConversion { DocumentType = upload };
session.Save(formConversion);
session.Save(uploadConversion);
transaction.Commit();
}
using (var transaction = session.BeginTransaction())
{
results = session.Query<FileConversion>().AsQueryable().ToList();
transaction.Commit();
}
}
};
Specifications
It should_return_two_results = () =>
results.Count.ShouldEqual(2);
It should_contain_one_of_type_uploaded_file = () =>
results
.Count(x => x.DocumentType.GetType() == typeof(UploadedFileDocument))
.ShouldEqual(1);
It should_contain_one_of_type_application_form = () =>
results
.Count(x => x.DocumentType.GetType() == typeof(ApplicationFormDocument))
.ShouldEqual(1);
}
Debugging through the assertions, I can see that the collection comes back with the two types:
Are you casting them back to the base type anywhere in your mappings or classes?

Categories