Sorry for the wall of text please don't TLDR.
I have a very simple object model, basically it's
public class Colony
{
public virtual IList<Ant> Ants { get; set; }
}
public class Ant
{
public bool Dead { get; set }
public virtual IList<Egg> Eggs { get; set; }
}
public class Egg
{
public bool Dead { get; set }
public virtual int IncubationPeriod { get; set; }
}
You get the idea. So I have declared two mapping overrides.
public class ColonyMappingOverride : IAutoMappingOverride<Colony>
{
public void Override(AutoMapping<Colony> mapping)
{
mapping.HasMany(c => c.Ants).Where(x => !x.Dead);
}
}
public class AntMappingOverride : IAutoMappingOverride<Ant>
{
public void Override(AutoMapping<Ant> mapping)
{
mapping.HasMany(c => c.Eggs).Where(x => !x.Dead);
}
}
So when I grab data out of the DB I end up with inconsistent data.
For example:
Colony.Ants doesn't contain any dead ants (as expected), however Colony.Ants[0].Eggs contains all Eggs... dead or not.
If I call a Session.Refresh(Colony.Ants[0]) the dead eggs get removed.
Anyone know why lazy loading is ignoring the Where clause on the Ants mapping override?
it works for me using FNH 2.0.1.0 and NH 4.0.0.4000 and following fixed code
public class Colony : Entity
{
public Colony()
{
Ants = new List<Ant>();
}
public virtual IList<Ant> Ants { get; set; }
}
public class Ant : Entity
{
public Ant()
{
Eggs = new List<Egg>();
}
public virtual bool Dead { get; set; }
public virtual IList<Egg> Eggs { get; set; }
}
public class Egg : Entity
{
public virtual bool Dead { get; set; }
public virtual int IncubationPeriod { get; set; }
}
public class ColonyMappingOverride : IAutoMappingOverride<Colony>
{
public void Override(AutoMapping<Colony> mapping)
{
mapping.HasMany(c => c.Ants).Where(x => !x.Dead);
}
}
public class AntMappingOverride : IAutoMappingOverride<Ant>
{
public void Override(AutoMapping<Ant> mapping)
{
mapping.HasMany(c => c.Eggs).Where(x => !x.Dead);
}
}
code
var config = Fluently.Configure()
.Database(SQLiteConfiguration.Standard.InMemory().ShowSql().FormatSql())
.Mappings(m => m
.AutoMappings.Add(AutoMap.AssemblyOf<Ant>()
.Conventions.Add(DefaultCascade.All())
.Override<Colony>(new ColonyMappingOverride().Override)
.Override<Ant>(new AntMappingOverride().Override)
)
)
.BuildConfiguration();
using (var sf = config.BuildSessionFactory())
using (var session = sf.OpenSession())
{
new SchemaExport(config).Execute(true, true, false, session.Connection, null);
using (var tx = session.BeginTransaction())
{
session.Save(new Colony
{
Ants =
{
new Ant
{
Dead = true,
Eggs =
{
new Egg { Dead = true, IncubationPeriod = 1 },
new Egg { Dead = false, IncubationPeriod = 2 },
}
},
new Ant
{
Dead = false,
Eggs =
{
new Egg { Dead = true, IncubationPeriod = 1 },
new Egg { Dead = false, IncubationPeriod = 2 },
}
},
}
});
tx.Commit();
}
session.Clear();
var result = session.QueryOver<Colony>()
.Fetch(c => c.Ants).Eager
.SingleOrDefault();
Console.WriteLine(result.Ants[0].Eggs.All(e => !e.Dead));
}
Related
There are a lot of examples how to use Fluent API in the internet, but mostly shows how configure one relationship between two models. In my case I need 3 relationships between 2 models. How to configure relationships between models below with Fluent API?
public class Company
{
public int Id { get; set; }
public string Name { get; set; }
public int FinanceEstimateId { get; set; }
public Estimate FinanceEstimate { get; set; }
public int EnvironmentEstimateId { get; set; }
public Estimate EnvironmentEstimate { get; set; }
public int SelfEstimateId { get; set; }
public Estimate SelfEstimate { get; set; }
}
public class Estimate
{
public int Id { get; set; }
public string Name { get; set; } // like: bad, good, excellent
public float Value { get; set; } // like: 1,2,3
}
Maybe this points you in the right direction.
I would go for 2 configurations like:
public class CompanyConfiguration : IEntityTypeConfiguration<Company>
{
public void Configure(EntityTypeBuilder<Company> builder)
{
builder.ToTable("Companies");
builder
.HasOne(x => x.EnvironmentEstimate)
.WithMany()
.HasForeignKey(x => x.EnvironmentEstimateId)
.OnDelete(DeleteBehavior.NoAction);
builder
.HasOne(x => x.FinanceEstimate)
.WithMany()
.HasForeignKey(x => x.FinanceEstimateId)
.OnDelete(DeleteBehavior.NoAction);
builder
.HasOne(x => x.SelfEstimate)
.WithMany()
.HasForeignKey(x => x.SelfEstimateId)
.OnDelete(DeleteBehavior.NoAction);
}
}
public class EstimateConfiguration : IEntityTypeConfiguration<Estimate>
{
public void Configure(EntityTypeBuilder<Estimate> builder)
{
builder.ToTable("Estimates");
}
}
You need a DbContext:
public class MyDbContext : DbContext
{
public DbSet<Company> Companies { get; set; } = null!;
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(#"CONNECTIONSTRING");
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// applies the configuration (those IEntityTypeConfiguration<T> things)
modelBuilder.ApplyConfigurationsFromAssembly(typeof(MyDbContext).Assembly);
}
}
I created a console application that demonstrates the usage
using var ctx = new MyDbContext();
await ctx.Database.EnsureDeletedAsync();
await ctx.Database.EnsureCreatedAsync();
var company1 = new Company
{
Name = "Name1",
EnvironmentEstimate = new Estimate { Name = "EnvironmentEstimate1", Value = 1 },
FinanceEstimate = new Estimate { Name = "FinanceEstimate1", Value = 2 },
SelfEstimate = new Estimate { Name = "SelfEstimate1", Value = 3 }
};
var company2 = new Company
{
Name = "Name2",
EnvironmentEstimate = new Estimate { Name = "EnvironmentEstimate2", Value = 4 },
FinanceEstimate = new Estimate { Name = "FinanceEstimate2", Value = 5 },
SelfEstimate = new Estimate { Name = "SelfEstimate2", Value = 6 }
};
await ctx.Companies.AddAsync(company1);
await ctx.Companies.AddAsync(company2);
await ctx.SaveChangesAsync();
var result = await ctx.Companies.ToListAsync();
Console.WriteLine("Done");
I am having this issue when I am dealing with Geometry datatypes when I change the property to string everything works like a charm. Below you may see that I used schema filter to remove Ignored data member , and document filter to remove anything related to nettopology.
Property Name = GeoPoly
Swagger Config Class
public static IServiceCollection AddSwaggerModule(this IServiceCollection services)
{
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v2", new OpenApiInfo { Title = "Test API", Version = "0.0.1" });
c.SchemaFilter<MySwaggerSchemaFilter>();
c.DocumentFilter<RemoveBogusDefinitionsDocumentFilter>();
c.ResolveConflictingActions(x => x.First());
});
return services;
}
public static IApplicationBuilder UseApplicationSwagger(this IApplicationBuilder app)
{
app.UseSwagger(c =>
{
c.RouteTemplate = "{documentName}/api-docs";
});
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/v2/api-docs", "Test API");
});
return app;
}
}
public class MySwaggerSchemaFilter : Swashbuckle.AspNetCore.SwaggerGen.ISchemaFilter
{
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
if (schema?.Properties == null)
{
return;
}
var ignoreDataMemberProperties = context.Type.GetProperties()
.Where(t => t.GetCustomAttribute<IgnoreDataMemberAttribute>() != null);
foreach (var ignoreDataMemberProperty in ignoreDataMemberProperties)
{
var propertyToHide = schema.Properties.Keys
.SingleOrDefault(x => x.ToLower() == ignoreDataMemberProperty.Name.ToLower());
if (propertyToHide != null)
{
schema.Properties.Remove(propertyToHide);
}
}
}
}
public class RemoveBogusDefinitionsDocumentFilter : Swashbuckle.AspNetCore.SwaggerGen.IDocumentFilter
{
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
swaggerDoc.Components.Schemas.Remove("Districts");
swaggerDoc.Components.Schemas.Remove("Geometry");
swaggerDoc.Components.Schemas.Remove("CoordinateSequenceFactory");
swaggerDoc.Components.Schemas.Remove("GeometryOverlay");
swaggerDoc.Components.Schemas.Remove("NtsGeometryServices");
swaggerDoc.Components.Schemas.Remove("CoordinateEqualityComparer");
swaggerDoc.Components.Schemas.Remove("NtsGeometryServices");
swaggerDoc.Components.Schemas.Remove("GeometryFactory");
swaggerDoc.Components.Schemas.Remove("OgcGeometryType");
swaggerDoc.Components.Schemas.Remove("Coordinate");
swaggerDoc.Components.Schemas.Remove("Point");
}
}
Entity Class
public class Districts : BaseEntity<long>
{
public string DistrictsDesc { get; set; }
public string DistrictsDescAr { get; set; }
[IgnoreDataMember]
[Column(TypeName = "geometry")]
public Geometry GeoPoly { get; set; }
public IList<Records> Records { get; set; } = new List<Records>();
public long? RegionsId { get; set; }
public Regions Regions { get; set; }
public long? CitiesId { get; set; }
public Cities Cities { get; set; }
}
Is there a way to stop swashbuckle gen from dealing with datatypes other than documents filter ?
Currently experiencing issues with Session.Save() not inserting records and producing the following exception:
null id in NHModels.Domain.Activity entry (don't flush the Session after an exception occurs)
at NHibernate.Event.Default.DefaultFlushEntityEventListener.CheckId(Object obj, IEntityPersister persister, Object id, EntityMode entityMode)
at NHibernate.Event.Default.DefaultFlushEntityEventListener.GetValues(Object entity, EntityEntry entry, EntityMode entityMode, Boolean mightBeDirty, ISessionImplementor session)
at NHibernate.Event.Default.DefaultFlushEntityEventListener.OnFlushEntity(FlushEntityEvent event)
at NHibernate.Event.Default.AbstractFlushingEventListener.FlushEntities(FlushEvent event)
at NHibernate.Event.Default.AbstractFlushingEventListener.FlushEverythingToExecutions(FlushEvent event)
at NHibernate.Event.Default.DefaultFlushEventListener.OnFlush(FlushEvent event)
at NHibernate.Impl.SessionImpl.Flush()
at NHibernate.Transaction.AdoTransaction.Commit()
at NHUnitOfWork.Dispose() in NHUnitOfWork.cs:line
at DatabaseActivityOperations.<WriteActivity>d__6.MoveNext() in DatabaseActivityOperations.cs:line 240
My class and mapping look like this.
Activity (For simplicity sake, I've removed several ILists related to this class)
public class Activity {
public Activity() {
Activityschema = new List<ActivitySchema>();
}
public virtual int ActivityKey { get; set; }
public virtual string Activityname { get; set; }
public virtual string Activitydescription { get; set; }
public virtual DateTime Averageactivitytime { get; set; }
public virtual int Averagenumberpeople { get; set; }
public virtual string Worktype { get; set; }
public virtual bool? Canautocomplete { get; set; }
public virtual IList<ActivitySchema> Activityschema { get; set; }
}
ActivityMap
public class ActivityMap : ClassMapping<Activity> {
public ActivityMap() {
Schema("dbo");
Lazy(true);
Id(x => x.ActivityKey, map => { map.Generator(Generators.Identity); });
Property(x => x.Activityname, map => { map.NotNullable(true); map.Length(50); });
Property(x => x.Activitydescription, map => { map.NotNullable(true); map.Length(100); });
Property(x => x.Averageactivitytime, map =>
{
map.NotNullable(true);
map.Type(NHibernateUtil.Time);
});
Property(x => x.Averagenumberpeople, map => { map.NotNullable(true); map.Precision(10); });
Property(x => x.Worktype, map => { map.NotNullable(true); map.Length(50); });
Property(x => x.Canautocomplete);
Bag(x => x.Activityschema, colmap => { colmap.Key(x => x.Column("ActivityKey")); colmap.Inverse(true); }, map => { map.OneToMany(); });
}
}
Finally, here's the Unit of Work class I have:
public class NHUnitOfWork : IDisposable
{
public static string ConnectingString { get; private set; } = #"data source=nh;initial catalog=db;MultipleActiveResultSets=True;";
protected static Configuration _config;
protected static NHibernate.ISessionFactory _sessionFactory;
public NHibernate.ISession Session { get; private set; }
protected NHibernate.ITransaction Transaction { get; set; }
private const System.Data.IsolationLevel ISOLATION_LEVEL = System.Data.IsolationLevel.ReadUncommitted;
private bool RollBack { get; set; } = false;
public NHUnitOfWork(string databaseConnectionString)
{
if (_config == null)
{
var cfg = new Configuration();
cfg.DataBaseIntegration(db =>
{
db.Driver<NHibernate.Driver.SqlClientDriver>();
db.ConnectionString = #"data source=nh;initial catalog=db;MultipleActiveResultSets=True;";
//db.ConnectionString = databaseConnectionString;
db.Dialect<MsSql2012Dialect>();
db.BatchSize = 500;
})
.AddAssembly(typeof(Activity).Assembly)
.SessionFactory()
.GenerateStatistics();
var mapper = new ModelMapper();
mapper.AddMappings(typeof(ActivityMap).Assembly.GetTypes());
cfg.AddMapping(mapper.CompileMappingForAllExplicitlyAddedEntities());
_config = cfg;
_sessionFactory = _config.BuildSessionFactory();
}
Session = _sessionFactory.OpenSession();
Transaction = Session.BeginTransaction(ISOLATION_LEVEL);
RollBack = false;
}
public void Commit()
{
Transaction.Commit();
}
public void Rollback()
{
if (Transaction.IsActive) Transaction.Rollback();
}
public void Dispose()
{
if (RollBack)
{
Transaction.Rollback();
}
else
{
Transaction.Commit();
}
Session.Close();
}
}
By this point, I believe my configuration for this is correct. I'm successfully used Session.Query to read data and that doesn't present issues. The problem comes when I write something like this to add a new record:
var activity = new Activity
{
Activityname = "TestActivity",
Activitydescription = "This is a test",
Averagenumberpeople = 1,
Worktype = "Test",
Canautocomplete = false,
Averageactivitytime = new DateTime(1, 1, 1, 0, 55, 55)
};
using (var uow = new NHUnitOfWork(NHUnitOfWork.ConnectingString))
{
uow.Session.Save(activity); // Produces exception here
//This also produces an exception
//uow.Session.Save(activity, Generators.Identity);
}
I think this has something to do with how I'm mapping the ID in ActivityMap and the generator isn't working as expected. I've tried to change it to several other types and get the same exception, or one stating that it's not able to convert to SystemInt32. I've also tried changing the ID to long and specifiying a data type, but no luck. What do I seem to be doing wrong here?
Literally minutes after I posted this, I figured out that the issue was actually with how I was setting the date time.
new DateTime(1, 1, 1, 0, 55, 55)
It didn't like the "1/1/0001" part, so that seemed to be causing the issue, I'm only concerned with the time piece. Changing the year to something like 2001 fixed the insert and it's working fine, the message was just not very descriptive.
After updating to nHibernate (4.0.2.4000 via nuget), the many to many mappings that previously worked now cause mapping exception "Could not determine type for: nHibernateManyToMany.IRole, nHibernateManyToMany, for columns: NHibernate.Mapping.Column(id)"
Seems to be only for many to many, and when the List is a interface (i.e. List<Role> vs List<IRole>).
Example code that now fails:
class Program
{
static void Main(string[] args)
{
var configuration = new Configuration().SetProperty(Environment.ReleaseConnections, "on_close")
.SetProperty(Environment.Dialect, typeof(SQLiteDialect).AssemblyQualifiedName)
.SetProperty(Environment.ConnectionDriver, typeof(SQLite20Driver).AssemblyQualifiedName)
.SetProperty(Environment.CollectionTypeFactoryClass, typeof(DefaultCollectionTypeFactory).AssemblyQualifiedName)
.SetProperty(Environment.CommandTimeout, "0");
var mapper = new ModelMapper();
mapper.AddMappings(new[] { typeof(EmployeeMapping), typeof(RoleMapping) });
var hbmMapping = mapper.CompileMappingForAllExplicitlyAddedEntities();
hbmMapping.autoimport = false;
configuration.AddMapping(hbmMapping);
// this line will fail
var factory = configuration.BuildSessionFactory();
}
}
public class Employee
{
public virtual int Id { get; set; }
public virtual List<IRole> Roles { get; set; }
}
public interface IRole
{
int Id { get; set; }
string Description { get; set; }
}
public class Role : IRole
{
public virtual int Id { get; set; }
public virtual string Description { get; set; }
}
public class EmployeeMapping : ClassMapping<Employee>
{
public EmployeeMapping()
{
Id(c => c.Id, x =>
{
x.Type(NHibernateUtil.Int32);
x.Generator(Generators.Identity);
x.Column("EmployeeId");
});
Bag(x => x.Roles, m =>
{
m.Table("EmployeeRole");
m.Key(km =>
{
km.Column("EmployeeId");
km.NotNullable(true);
km.ForeignKey("FK_Role_Employee");
});
m.Lazy(CollectionLazy.Lazy);
}, er => er.ManyToMany(m =>
{
m.Class(typeof(Role));
m.Column("RoleId");
}));
}
}
public class RoleMapping : ClassMapping<Role>
{
public RoleMapping()
{
Id(c => c.Id, x =>
{
x.Type(NHibernateUtil.Int32);
x.Generator(Generators.Identity);
x.Column("RoleId");
});
Property(x => x.Description, c =>
{
c.Length(50);
c.NotNullable(true);
});
}
}
Any help or suggestions about where we could look for details on how this has changed since v3 would be appreciated.
Turned out to be a bug in nHibernate.
A pull request has been submitted here https://github.com/nhibernate/nhibernate-core/pull/385
I have two classes, each having a domain and a repo version.
DOMAIN:
public class MusicInfo
{
public string Id { get; set; }
public MusicImage Image { get; set; }
public MusicInfo(byte[] image)
{
this.Image = new MusicImage(this, image);
}
}
public class MusicImage
{
public byte[] Blob { get; set; }
public MusicInfo MusicInfo { get; set; }
public string Id { get; set; }
public MusicImage(MusicInfo musicInfo, byte[] blob)
{
if (musicInfo == null)
throw new ArgumentNullException("musicInfo");
if (blob == null)
throw new ArgumentNullException("blob");
this.MusicInfo = musiscInfo;
this.Blob = blob;
}
}
REPO:
public class MusicInfoRepo
{
public virtual long Id { get; set; }
public virtual MusicImageRepo Image { get; set; }
}
public class MusicImageRepo
{
public virtual byte[] Blob { get; set; }
public virtual MusicInfoRepo MusicInfo { get; set; }
public virtual long Id { get; set; }
}
And here are their mappings:
public class MusicInfoRepoMap : HighLowClassMapping<MusicInfoRepo>
{
public MusicInfoRepoMap()
{
Table("MusicInfo");
Id(f => f.Id, m => m.Generator(Generators.HighLow, HighLowMapper));
OneToOne(f => f.Image, m => m.Cascade(Cascade.All));
}
}
public class MusicImageRepoMap : ClassMapping<MusicImageRepo>
{
public MusicImageRepoMap()
{
Table("MusicImage");
Id(f => f.Id, m => m.Generator(Generators.Foreign<MusicImageRepo>(f => f.MusicInfo)));
Property(f => f.Blob, m =>
{
m.NotNullable(true);
m.Column(c => c.SqlType("VARBINARY(MAX)"));
m.Length(Int32.MaxValue);
m.Update(false);
});
OneToOne(f => f.MusicInfo,
m =>
{
m.Cascade(Cascade.None);
m.Constrained(true);
m.Lazy(LazyRelation.NoLazy);
});
}
}
When I am trying to query for a ClassA that has a one to one relationship with ClassB which also has a one to one relationship with MusicInfo, an error occurs that says:
Missing type map configuration or unsupported mapping.
Mapping types:
MusicImageRepo -> Byte[]
Blah.MusicImageRepo -> System.Byte[]
Destination path:
List`1[0]
Source value:
Blah.MusicImageRepo
but here is how i map them:
Mapper.CreateMap<MusicInfo, MusicInfoRepo>();
Mapper.CreateMap<MusicInfoRepo, MusicInfo>();
Mapper.CreateMap<MusicImage, MusicImageRepo>();
Mapper.CreateMap<MusicImageRepo, MusicImage>();
There are no problems when saving these classes.
I really dont get why the error happens.
Will really appreciate your help.
OneToOne says that the other entity/table has the Reference(column). It should not work when both sides have a onetoone mapping since they bounce the responsiblity back and forth.
Also the classes look a bit overcomplicated when all you need is to store the bytes somewhere else.
public class MusicInfoRepo
{
public virtual long Id { get; private set; }
public virtual MusicImageRepo Image { get; private set; }
public MusicInfo(byte[] image)
{
this.Image = new MusicImageRepo(this, image);
}
}
public class MusicImageRepo
{
public virtual MusicInfoRepo MusicInfo { get; private set; }
public virtual byte[] Blob { get; set; }
}
public class MusicInfoRepoMap : HighLowClassMapping<MusicInfoRepo>
{
public MusicInfoRepoMap()
{
Table("MusicInfo");
Id(f => f.Id, m => m.Generator(Generators.HighLow, HighLowMapper));
OneToOne(f => f.Image, m => m.Cascade(Cascade.All));
}
}
public class MusicImageRepoMap : ClassMapping<MusicImageRepo>
{
public MusicImageRepoMap()
{
Table("MusicImage");
ComposedId(m => m.ManyToOne(x => x.MusicInfo));
Property(f => f.Blob, m =>
{
m.NotNullable(true);
m.Column(c => c.SqlType("VARBINARY(MAX)"));
m.Length(Int32.MaxValue);
m.Update(false);
});
}
}
Note: think about it if the seperation between DomainModel and MappedModel really makes sense. NHibernate goes to great length supporting mapping of DomainModels.