nhibernate mapping.JoinedSubClass(keyColumn) does not respect keyColumn - c#

I'm using Fluent-NHibernate and attempting to persist an object hierarchy using the table per subclass method:
public class AbstractProduct
{
public int ProductId { get; set; }
public string Name { get; set; }
}
public class SingleProduct : AbstractProduct
{
public int SingleProductId { get; set; }
public string SomeField { get; set; }
}
when saving an object
var singleProduct = new SingleProduct();
session.SaveOrUpdate(singleProduct);
I get this error:
NHibernate.Exceptions.GenericADOException: could not insert: [FluentNHibernateSubClassTest.SingleProduct#3][SQL: INSERT INTO SingleProductData (Field1, AbstractProduct_id) VALUES (?, ?)] ---> System.Data.SqlClient.SqlException: Invalid column name 'AbstractProduct_id'.
despite having the following overrides:
public class AbstractProductOverrides : IAutoMappingOverride<AbstractProduct>
{
public void Override(AutoMapping<AbstractProduct> mapping)
{
mapping.Id(x => x.ProductId).Column("ProductId");
//this mapping provided to illustrate the overrides are picked up
mapping.Table("ProductsData");
mapping.JoinedSubClass<SingleProduct>("ProductId");//ignored??
}
}
public class SingleProductOverrides : IAutoMappingOverride<SingleProduct>
{
public void Override(AutoMapping<SingleProduct> mapping)
{
mapping.Id(x => x.SingleProductId);
mapping.Table("SingleProductData");
mapping.Map(x => x.SomeField).Column("Field1");
}
}
It doesn't appear to matter what column name I supply to JoinedSubClass it ignores it and uses AbstractProduct_id instead.
How can I tell nhibernate the key column is ProductId and not AbstractProduct_id?
I have a test project demonstrating the issue available here (you need to create the db)
UPDATE
I've got around this by providing the following convention:
public class JoinedSubclassConvention : IJoinedSubclassConvention
{
public void Apply(IJoinedSubclassInstance instance)
{
if (instance.EntityType == typeof(SingleProduct))
instance.Key.Column(("ProductId"));
}
}
which works but feels like its the wrong way or a hack.

mapping.Id in SingleProductOverrides is flawed. Subclasses don't have their own id, they inherit the Id from their base classes. Even mapping.JoinedSubClass<SingleProduct>("ProductId"); is redundant (probably ignored) if SingleProduct is automapped as well (it is as seen from the Override for it). JoinedSubclassConvention is the right way to do this.

Related

How to perform strict mapping on Dapper

I am using dapper to map SQL result set directly to my C# object, everything works nicely.
I am using statements like this to do the mapping
var result = connection.Query< MyClass >( "sp_select", );
but this statement doesn't seem to enforce exact mapping between the class fields and the columns returned from the database. Meaning, it won't fail when the field on the POCO doesn't exist on the result set.
I do enjoy the fact that the implementation is loose and doesn't enforce any restriction right of the bat, but is there any feature of dapper that would allow me to demand certain fields from the result set before deeming the mapping successful?
You can also try Dapper-Extensions
Here is an example:
public class Person
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Address { get; set; }
}
[TestFixture]
public class DapperExtensions
{
private SqlConnection _connection;
[SetUp]
public void Init()
{
_connection = new SqlConnection(#"Data Source=.\sqlexpress;Integrated Security=true; Initial Catalog=mydb");
_connection.Open();
_connection.Execute("create table Person(Id int not null, FirstName varchar(100) not null, LastName varchar(100) not null)");
_connection.Execute("insert into Person(Id, FirstName, LastName) values (1, 'Bill', 'Gates')");
}
[TearDown]
public void Teardown()
{
_connection.Execute("drop table Person");
_connection.Close();
}
[Test]
public void Test()
{
var result = _connection.Get<Person>(1);
}
}
The test will fail due to a missing Address column in the Person table.
You can also ignore columns with Custom Maps:
public class PersonMapper : ClassMapper<Person>
{
public PersonMapper()
{
Map(x => x.Address).Ignore();
AutoMap();
}
}
There is no way for you to enforce this "automagically" with an attribute or a flag. You can follow this open Github issue for more background.
This could be accomplished by you manually by mapping each property yourself in a select clause, although at that point you've lost a lot of the power and ease of use of Dapper.
var result = connection.Query<MyClass>("sp_select")
.Select(x =>
{
// manually map each property and verify
// that the data is returned
});

Can these models be represented in EF7?

I'm trying to use some classes from another assembly in my own project as entities that I can persist using EF7, rather than writing a series of very similar classes that are more database-friendly.
Simplified versions look like this:
interface IMediaFile
{
string Uri { get; }
string Title { get; set; }
}
class CMediaFile : IMediaFile
{
public CMediaFile() { }
public string Uri { get; set; }
public string Title { get; set; }
}
//The following types are in my project and have full control over.
interface IPlaylistEntry
{
IMediaFile MediaFile { get; }
}
class CPlaylistEntry<T> : IPlaylistEntry where T : IMediaFile
{
public CPlaylistEntry() { }
public T MediaFile { get; set; }
}
There are multiple implementations of IMediaFile, I am showing only one. My PlaylistEntry class takes a generic argument to enable different traits for those various implementations, and I just work with the IPlaylistEntry.
So I've started to model it like so:
var mediaFile = _modelBuilder.Entity<CMediaFile>();
mediaFile.Key(e => e.Uri);
mediaFile.Index(e => e.Uri);
mediaFile.Property(e => e.Title).MaxLength(256).Required();
var mediaFilePlaylistEntry = _modelBuilder.Entity<CPlaylistEntry<CMediaFile>>();
mediaFilePlaylistEntry.Key(e => e.MediaFile);
mediaFilePlaylistEntry.Reference(e => e.MediaFile).InverseReference();
As a simple test, I ignore the CPlaylistEntry<> and just do:
dbContext.Set<CMediaFile>().Add(new CMediaFile() { Uri = "irrelevant", Title = "" });
dbContext.SaveChanges()
This throws:
NotSupportedException: The 'MediaFile' on entity type 'CPlaylistEntry' does not have a value set and no value generator is available for properties of type 'CMediaFile'. Either set a value for the property before adding the entity or configure a value generator for properties of type 'CMediaFile'`
I don't even understand this exception, and I don't see why CPlaylistEntry is appearing when I'm only trying to store a CMediaFile entity. I'm guessing this is related to my model definition - specifically defining the primary key of the CPlaylistEntry as not a simple type, but a complex type - another entity. However I would expect EF to be smart enough to work out that it all boils down to a string Uri, because that complex type has its own primary key declared already, and I have declared the property as a foreign key to that type.
Is it possible to model these classes in EF without radically redesigning them to look closer to what corresponding database tables might be? I've worked with EF6 database-first in the past, so this is my first attempt into a code-first pattern, and I'm really hoping that I can isolate the mess that a database might look like to just my model definition, and keep "clean" classes that I interact with in .NET.
If more explanation of these types and their relationship is required, just ask - I'm attempting to keep this brief.
Doubt this is currently supported (unsure if it eventually will or not).| I've tried to recreate your model with slight changes and when trying to create the database I get:
System.NotSupportedException: The property 'PlaylistEntry`1MediaFile'
cannot be mapped because it is of type 'MediaFile' which is currently
not supported.
Update 1
I think that the fact that you are putting MediaFile as a key is creating problems. I've done a few changes to your model. I hope this will not break anything negative on your end:
public interface IPlaylistEntry<T>
where T : IMediaFile
{
T MediaFile { get; set; }
}
public class PlaylistEntry<T> : IPlaylistEntry<T>
where T : IMediaFile
{
public int Id { get; set; }
public string PlaylistInfo { get; set; } //added for testing purposes
public T MediaFile { get; set; }
}
Mappings:
protected override void OnModelCreating(ModelBuilder builder)
{
builder.ForSqlServer().UseIdentity();
builder.Entity<MediaFile>().ForRelational().Table("MediaFiles");
builder.Entity<MediaFile>().Key(e => e.Uri);
builder.Entity<MediaFile>().Index(e => e.Uri);
builder.Entity<MediaFile>().Property(e => e.Title).MaxLength(256).Required();
builder.Entity<PlaylistEntry<MediaFile>>().ForRelational().Table("MediaFileEntries");
builder.Entity<PlaylistEntry<MediaFile>>().Key(e => e.Id);
builder.Entity<PlaylistEntry<MediaFile>>().Reference(e => e.MediaFile).InverseReference();
}
Usage:
var mediaFile = new MediaFile() {Uri = "irrelevant", Title = ""};
context.Set<MediaFile>().Add(mediaFile);
context.SaveChanges();
context.Set<PlaylistEntry<MediaFile>>().Add(new PlaylistEntry<MediaFile>
{
MediaFile = mediaFile,
PlaylistInfo = "test"
});
context.SaveChanges();
This works and saves the correct data to the database.
You can retrieve the data using:
var playlistEntryFromDb = context.Set<PlaylistEntry<MediaFile>>()
.Include(plemf => plemf.MediaFile).ToList();
Update 2
Since you do not want to have an identity as key, you can add a Uri property to your playlistentry class that will be used for the relationship between PlaylistEntry and MediaFile.
public class PlaylistEntry<T> : IPlaylistEntry<T>
where T : IMediaFile
{
public string Uri { get; set; }
public string PlaylistInfo { get; set; }
public T MediaFile { get; set; }
}
Here is what the mapping in this case would look like:
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<MediaFile>().ForRelational().Table("MediaFiles");
builder.Entity<MediaFile>().Key(e => e.Uri);
builder.Entity<MediaFile>().Index(e => e.Uri);
builder.Entity<MediaFile>().Property(e => e.Title).MaxLength(256).Required();
builder.Entity<PlaylistEntry<MediaFile>>().ForRelational().Table("MediaFileEntries");
builder.Entity<PlaylistEntry<MediaFile>>().Key(e => e.Uri);
builder.Entity<PlaylistEntry<MediaFile>>().Reference(e => e.MediaFile).InverseReference().ForeignKey<PlaylistEntry<MediaFile>>(e => e.Uri);
}
Usage to insert data stays the same:
var mediaFile = new MediaFile() { Uri = "irrelevant", Title = "" };
context.Set<MediaFile>().Add(mediaFile);
context.SaveChanges();
context.Set<PlaylistEntry<MediaFile>>().Add(new PlaylistEntry<MediaFile>
{
MediaFile = mediaFile,
PlaylistInfo = "test"
});
context.SaveChanges();
This code above will put "irrelevant" in the PlaylistEntry Uri property since it is used as the foreign key.
And to retrieve data:
var mediaFiles = context.Set<PlaylistEntry<MediaFile>>().Include(x => x.MediaFile).ToList();
The join will occur on the Uri field in both tables.

Fluent NHibernate automap PostGIS geometry type

Given the following model:
using NetTopologySuite.Geometries;
public class bounding_box
{
public virtual int id { get; protected set; }
public virtual Polygon area { get; set; }
}
How do I automap the area property to a area geometry(Polygon) column when generating the DB schema using Fluent Nhibernate? Note that I do not care about being able to read / update the geometry column using NHibernate since I will be using GDAL in my code.
I know I can do it by implementing a manual override, i.e.:
public class bounding_boxMappingOverrride : IAutoMappingOverride<bounding_box>
{
public void Override(AutoMapping<bounding_box> mapping)
{
mapping.Map(x => x.area)
.CustomSqlType("geometry(Polygon)");
}
}
However, I have many tables with geometry columns so I would much prefer to be able to specify a custom type mapping.
For some reason, the area property is never intercepted by the following property convention:
public class PostgisTypesConvention : IPropertyConvention
{
public void Apply(IPropertyInstance instance)
{
if (instance.Type == typeof(Polygon))
{
instance.CustomSqlType("geometry(Polygon)"); // Never reached
}
}
}
I have the same problem if I use GeoAPI.Geometries.IPolygon instead of NetTopologySuite.Geometries.Polygon...
I was finally able to resolve this by defining a custom UserTypeConvention, i.e.:
using NetTopologySuite.Geometries;
using NHibernate.Spatial.Type;
public class PostGisPolygonUserTypeConvention : UserTypeConvention<PostGisGeometryType>
{
public override void Accept(IAcceptanceCriteria<IPropertyInspector> criteria)
{
criteria.Expect(c => c.Type == typeof(Polygon));
}
public override void Apply(IPropertyInstance instance)
{
// Have to set CustomType to be able to read/write rows using NHibernate
instance.CustomType<PostGisGeometryType>();
// Have to set CustomSqlType to generate correct SQL schema
instance.CustomSqlType("geometry(Polygon)");
}
}
The same principle can also be used to create UserTypeConventions for other geometries, such as Point, LineString, MultiPoint, etc.

Is it possible to use a PK column as a discriminator in Entity Framework code first?

I'm trying to use Entity Framework code first to try to map a simple lookup table to a heirarchy of types and want to use the primary key of the table as the discriminator column for a "table per heirarchy" entity. More acturately, I'm trying to make this work against an existing database.
Here is a contrived sample app I put together trying to figure out if I can make it work or not:
using System;
using System.ComponentModel.DataAnnotations;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
namespace EfTest
{
public abstract class Base
{
public int LookupId { get; set; }
public String Name { get; set; }
public abstract String GetTest();
}
public class Derived1 : Base
{
public override string GetTest() { return Name + "1"; }
}
public class Derived2 : Base
{
public override string GetTest() { return Name + "2"; }
}
public class Derived3 : Base
{
public override string GetTest() { return Name + "3"; }
}
class Program
{
static void Main(string[] args)
{
Database.SetInitializer<MyContext>(new DropCreateDatabaseIfModelChanges<MyContext>());
using(var context = new MyContext())
{
foreach (var item in context.Lookups)
{
Console.WriteLine("{0} {1} {2}", item.LookupId, item.Name, item.GetType().FullName);
}
}
Console.ReadKey();
}
}
public class MyContext : DbContext
{
public DbSet<Base> Lookups { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
var config = modelBuilder.Entity<Base>();
config.HasKey(e => e.LookupId).ToTable("dbo.Lookup");
config.Property(e => e.LookupId)
.HasColumnName("LookupId")
.IsRequired()
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
config.Property(e => e.Name)
.IsRequired()
.HasColumnName("Name")
.HasMaxLength(32)
.HasColumnType("varchar");
config.Map<Derived1>(e => e.Requires("LookupId").HasValue(1).IsRequired());
config.Map<Derived2>(e => e.Requires("LookupId").HasValue(2).IsRequired());
config.Map<Derived3>(e => e.Requires("LookupId").HasValue(3).IsRequired());
//config.Map<Derived1>(e => e.Requires("Name").HasValue("Item1").IsRequired());
//config.Map<Derived2>(e => e.Requires("Name").HasValue("Item2").IsRequired());
//config.Map<Derived3>(e => e.Requires("Name").HasValue("Item3").IsRequired());
}
}
}
However, this raises an exception stating:
Problem in mapping fragments starting at line 24:Condition member
'Base.LookupId' with a condition other than 'IsNull=False' is mapped.
Either remove the condition on Base.LookupId or remove it from the
mapping.
I've also tried discrimiating with the "Name" column with similar results.
The errors look like it complaining that I'm trying to map to nullable column in the database, however, the table that actually gets created has both columns marked not null, as I would expect.
Is what I'm trying to do simply not supported by EF?
This is not possible. Each column can have only single special purpose in the mapping. Having a column as a key and discriminator are two special purposes. Discriminator column has special meaning of selecting the correct type to be materialized and because if that it must not be present in mapped entity (you cannot set it from your application). Also having discriminator on column which must be unique is incorrect.

Extending a EF Entity for business logic throws a mapping exception

I have a model in our Database ImportError. I have a class the extends this model, AssessmentImportError. AssessmentImportError does not map to a table. It has a [NotMapped] attribute on it. When I try to select or insert a ImportError (not an AssessmentImportError...) I get the following exception:
Exception:
An exception of type 'System.Exception' occurred in EntityFramework.MappingAPI.dll but was not handled in user code
Additional information: Type 'DAL.SharedModels.AssessmentImportError' is not found in context 'DAL.HTTrans.Model.HTTransDB'
The models:
[NotMapped]
public class AssessmentImportError : ImportError
{
public string APN { get; set; }
}
public partial class ImportError : Frameworks.Interfaces.IImportError
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long ErrorId { get; set; }
[Required]
[MaxLength(200)]
[Index("IX_TableName_RecordId", 1)]
public string TableName { get; set; }
// ... deleted code
}
Bulk Insert:
private static void SaveErrors(List<IImportError> errors, int batchID)
{
// Casting from IImportError to ImportError. They are already an ImportError though.
List<ImportError> castedErrors = errors.Select(e => (ImportError)e).ToList();
using (var db = new HTTransDB())
{
foreach (var e in castedErrors)
{
e.BatchId = batchID;
}
db.BulkInsert(castedErrors);
errors.Clear();
}
}
EDIT:
If I change db.BulkInsert(castedErrors) to db.ImportErrors.AddRange(castedErors) I no longer have an issue. This appears to be a bug within the BulkInsert Nuget package's Mapping API.
This cast:
errors.Select(e => (ImportError)e)
will still return a list of AssessmentImportError which are not mapped to the DB model.
What you should do is maybe an extension method (or a service) which maps from AssessmentImportError to the desired ImportError like this:
List<ImportError> castedErrors = errors.Select(e => e.To<ImportError>()).ToList();
where
To< ImportError >() ===> returns new ImportError object
is the extension method.

Categories