How to use string keys in (fluent) NHibernate - c#

I am working with brownfield database that uses strings as primary keys.
Using Fluent NHibernate with Sqlite (in-memory provider for unit testing) and SQL Server 2005.
I have the following entity:
public class Entity
{
public virtual DateTime TimeStamp { get; set; }
public virtual string Name { get; set; }
}
With this mapping:
public class EntityMap : ClassMap<Entity>
{
public EntityMap()
{
Map(_ => _.TimeStamp);
Id(_ => _.Name).CustomType("AnsiString");
}
}
However it does not work saying NHibernate.TypeMismatchException : Provided id of the wrong type. Expected: System.Int32, got System.String
How make this work? Also, is there any good documentation about fluent nhibernate available?
Thanks in advance.

If you are using strings as your primary keys you'll probably have to do something like this:
public class EntityMap : ClassMap<Entity>
{
public EntityMap()
{
Id(x => x.Name).GeneratedBy.Assigned();
Map(x => x.TimeStamp);
}
}
From the nhibernate documentation:
5.1.4.7. Assigned Identifiers
If you want the application to assign identifiers (as opposed to
having NHibernate generate them), you may use the assigned generator.
This special generator will use the identifier value already assigned
to the object's identifier property. Be very careful when using this
feature to assign keys with business meaning (almost always a terrible
design decision).
Due to its inherent nature, entities that use this generator cannot be
saved via the ISession's SaveOrUpdate() method. Instead you have to
explicitly specify to NHibernate if the object should be saved or
updated by calling either the Save() or Update() method of the
ISession.
Also here is a related article. It is a bit dated but still applies to your situation:
http://groups.google.com/group/fluent-nhibernate/browse_thread/thread/6c9620b7c5bb7ca8

Related

How to map destination properties with destination class name as prefix using AutoMapper 12.0

TL;DR: Is there a way to create a sort of convention or prefix rule that would use the source or destination class names as prefix for some field mappings using AutoMapper 12.0?
I'm experimenting with AutoMapper 12.0 in a dotnet core 6 project and the entity classes use the entity name as prefix for their keys (eg.: Person class has a PersonId property, which is mapped to the primary key in DB), but the API side has DTOs that don't include those prefixes:
// Entity definition
public class Person
{
public int PersonId { get; set; }
public string Name { get; set; }
// ...
}
// API response definition
public class PersonDetails
{
public int Id { get; set; }
public string Name { get; set; }
// ...
}
The way I configured my mappings to make it work was to manually define the mapping for the IDs using .ForMember(...), but that requires me to do that for every single entity and DTO combination:
public class EntityToViewModelMappingProfile : Profile
{
public EntityToViewModelMappingProfile() : base(nameof(EntityToViewModelMappingProfile))
{
CreateMap<Person, PersonDetails>()
.ForMember(p => p.Id, o => o.MapFrom(p => p.PersonId))
.ReverseMap();
// ...
// the same code for all of my entities :(
}
}
I'm looking for a way to avoid a bunch of boilerplate code and have a generic rule that would work for all of my entities without having to write custom mappings for all of them.
Even though I have custom mappings for other fields, that case is the one that seems to be a fit for a custom convention, but I couldn't find a way to implement one that would give me access to the destination and source type names to be able to use them as prefixes.
As Lucian Bargaoanu pointed out in the comments, there is a similar question for an earlier version of AutoMapper here and it suggests using:
Mapper.Initialize(exp =>
{
exp.CreateMap<User, UserDto>();
exp.ForAllMaps((typeMap, mappingExpression) =>
mappingExpression.ForMember("Id", o=>o.MapFrom(typeMap.SourceType.Name + "Id"))
);
});
AutoMapper 12.0 seems to not support ForAllMaps, from what I researched in their docs. Is there a way to achieve the same with 12.0?
Lucian Bargaoanu's comments lead me to a similar question for a previous version of AutoMapper, but the current version (12.0) doesn't expose that API through the regular public interface anymore. At least it gave me a hint on possible ways of achieving my problem, so I started looking for how to apply the same on newer versions.
By digging through the upgrade docs from past versions, I found that ForAllMaps was moved to the Internal API on version 11.0 and now requires importing AutoMapper.Internal namespace to have access to the .Internal() extension methods on config objects to access that method.
Here is my solution for the problem in AutoMapper 12.0 (possibly compatible with 11.0 as well):
using AutoMapper;
using AutoMapper.Internal; // <== this is important
namespace MyOwn.Solution.AutoMapper
{
public class DomainToAPIModelMappingProfile : Profile
{
public DomainToAPIModelMappingProfile() : base(nameof(DomainToAPIModelMappingProfile))
{
CreateMap<Domain.Entities.Person, APIModels.PersonDetails>();
CreateMap<Domain.Entities.Foo, APIModels.FooData>();
CreateMap<Domain.Entities.Bar, APIModels.BarData>();
// ...
// this is the solution
this.Internal().ForAllMaps((typeMap, mappingExpression) =>
mappingExpression.ForMember("Id", o => o.MapFrom(typeMap.SourceType.Name + "Id"))
);
}
}
}

An Entity with identical table data

Before I elaborate the problem, I'm well aware the database isn't designed conventionally. Sadly, I can't change this particular database due to how it is integrated, so I've got a potential solution but that won't be implemented for several months. In the mean time I need to work around the following:
The problem is I need to build an Entity, this would represent our Accounts. But the problem, our database implements the following structure:
Invoiced Table
Non-Invoiced Table
My Entity, represents the exact same data on those tables, same column names, duplicate under all conditions, except one is invoiced while the other represents non-invoiced customers. But since it isn't one table, with a Flag to indicate invoiced versus non-invoiced, how can my Entity link to both of those tables?
Since both tables represent separate names, I can't use the [Table("...")] or the auto mapping capabilities. I hate asking such a question, but I can't find any documentation on how to handle such an issue.
You could use table-per-concrete class inheritance then define the table names on the derived types:
public abstract class Account
{
// common entity code here
...
}
public class InvoicedAccount : Account {}
public class NonInvoicedAccount: Account {}
public YourContext : DbContext
{
public DbSet<InvoicedAccount> InvoicedAccounts { get; set; }
public DbSet<NonInvoicedAccount> NonInvoicedAccounts { get; set; }
protected override void OnModelCreating( DbModelBuilder modelBuilder )
{
modelBuilder.Entity<InvoicedAccounts>().Map( m =>
{
m.MapInheritedProperties();
m.ToTable( "InvoicedAccountTable" );
} );
modelBuilder.Entity<NonInvoicedAccounts>().Map( m =>
{
m.MapInheritedProperties();
m.ToTable( "NonInvoicedAccountTable" );
} );
}
}

Table Per Class Inheritance with FluentNHibernate - Identity generation

I have a set of payment objects that come from different sources that I'd like to use an TPC inheritance in NHibernate for. The base for all these object is the payment processor (so all have a consistent format), however there is no representation of the base class in the database. I think I have my mapping worked out, except for the fact that I get an exception thrown when trying to insert - "Cannot use identity column key generation with <union-subclass> mapping for: <EntityName>"
Classes:
public class BasePayment
{
public virtual int Id { get; set; }
public virtual PaymentSource Source { get; set; }
public virtual DateTime Date { get; set; }
public virtual decimal Amount { get; set; }
}
public class SubPayment : BasePayment
{
}
Mappings:
public class BasePaymentMap : ClassMap<BasePayment>
{
public BasePaymentMap()
{
UseUnionSubclassForInheritanceMapping();
DiscriminateSubClassesOnColumn("Source");
Id(m => m.Id);
Map(m => m.Source);
Map(m => m.Amount);
Map(m => m.Date);
}
}
public class SubPaymentMap : SubclassMap<SubPayment>
{
public SubPaymentMap()
{
DiscriminatorValue(PaymentSource.SourceX);
Abstract();
Table("SourceXPayments");
}
}
And that's as far as I've got. On a SaveOrUpdate, I'm getting the error above, and a google search is not proving helpful thus far - I have found this question: Cannot use identity column key generation with <union-subclass> ( TABLE_PER_CLASS ) which appears to cover the issue in Java/Hibernate, but I'm having a problem with converting the #GeneratedValue(strategy = GenerationType.TABLE) into a Fluent syntax, as Fluent NHibernate's GeneratedBy() method doesn't seem to have a table, or a lot of documentation (that I have found) as to what it does under the covers.
Doing some more reading around it seems that this might not be possible, so if anyone can confirm this or offer a work around for the situation, it'd be greatly appreciated.
If I've not given enough detail, please let me know what would be more use.
Thanks in advance
UseUnionSubclassForInheritanceMapping();
DiscriminateSubClassesOnColumn("Source");
is conflicting and FNH will ignore one.
UseUnionSubclassForInheritanceMapping means each class has it's own table with all columns and the table is used to discriminate the class, no column needed
DiscriminateSubClassesOnColumn means each class of the inheritance hierarchy live in the same table and a column is used to discriminate the classes.
the error message means that when using UseUnionSubclassForInheritanceMapping identity generation is not allowed since the id's would be unique for one table only but NHibernate threats all subclasses as one set where the id must be unique.
e.g. session.Get<BaseClass>(5); won't work relyably anymore since there could be more than one subclass with the same id.
#GeneratedValue(strategy = GenerationType.TABLE) just tells "use another strategy than identity". GeneratedBy.HiLow(); would be a good alternative to identity.

Fluent NHibernate automap list of strings with nvarchar(max)

I am using Fluent NHibernate 1.2 for NHibernate 3.1. I have a class:
public class Marks
{
public virtual int Id { get; set; }
public virtual IList<string> Answers { get; set; }
}
In the mapping for the Marks class, I have:
HasMany(m => m.Answers).Element("Value");
When the tables are created, an "Answers" table get created with the following columns:
Marks_id (FK, int, not null)
Value (nvarchar(255), null)
What I would like to do is have the Value be nvarchar(max). I'd prefer not doing this for every string in every class, just for this one class.
I have looked at these posts: first, second, third, but haven't found anything yet that helps.
Thanks in advance for any help you can offer. If you need additional information, please let me know.
Edit:
This is the code that resolves the issue:
HasMany(x => x.Answers).Element("Value", x => x.Columns.Single().Length = 4001);
You can force mapping string to longer column at each column level in mapping either by using CustomSqlType("nvarchar(max)") or, bit more universally, by setting Length(4001) (SQL Server magic number, above which it creates nvarchar(max) automatically).
To apply it automatically for all string columns in your entities, you can write your own FluentNHibernate convention:
public class LongStringConvention : IPropertyConvention, IPropertyConventionAcceptance
{
public void Accept(IAcceptanceCriteria<IPropertyInspector> criteria)
{
criteria.Expect(x => x.Type == typeof(string));
}
public void Apply(IPropertyInstance instance)
{
instance.Length(4001);
}
}
And register it in mapping container i.e. like that:
Fluently.Configure()
.Mappings(...)
.Conventions.Add<LongStringConvention>()
To apply it for collection of strings, you can use custom element mappings:
HasMany(x => x.Answers).Element("Value", x => x.Columns.Single().Length = 4001);
Came across this issue myself and the above answers were most helpful in pointing me in the right direction...but I'm using a slightly newer version of FluentNH.
For Fluent NHibernate 1.3 and NH 3.3.1, the correct code is:
HasMany(x => x.Answers).Element("Value", x => x.Length(4001));
The answers above only work for older version of nhibernate.
If you try HasMany(x => x.Answers).Element("Value", x => x.Length(4001)); you will get the following:
Error Property or indexer
'FluentNHibernate.MappingModel.ColumnMapping.Length' cannot be
assigned to -- it is read only
The correct way to do this now is (NHibernate 4.0.2.4, FluentNHibernate 2.0.1.0):
HasMany(m => m.Answers).Element("Value", e => e.Length(4001))

How to set discriminate column type for subclass with FNH?

What is the new SetAttribute() in FNH mapping? I need to set my discriminator value on subclass because String is not preferred - old post
with NH 2.1.2.4000, FNH 1.1.0.689
public class BaseBuildingMap : ClassMap<BaseBuilding>
{
public BaseBuildingMap()
{
Id(x => x.Id);
DiscriminateSubClassesOnColumn<int>("BuildingType", -1);
}
}
public class PowerStationMap : SubclassMap<PowerStation>
{
public PowerStationMap()
{
Map(x => x.ElectricityProduction);
}
}
NHibernate.MappingException: Could not format discriminator value to SQL string of entity Model.Test.PowerStation ---> System.FormatException: Input string was not in a correct format.
I need to set SetAttribute("discriminator-value", "-1"); but there is no such method.
EDIT 1
Question: How to set discriminate column type for subclass with FNH?
public class PowerStationMap : SubclassMap<PowerStation>
{
public PowerStationMap()
{
DiscriminatorValue((int)1);
Map(x => x.ElectricityProduction);
}
}
I've finally found my answer, it's
SubclassMap<T>::DiscriminatorValue(object discriminatorValue);
From Fluent NHibernate 1.0 Release Notes
Removed SetAttribute - SetAttribute was a stop-gap measure to allow people to use Fluent NHibernate when we didn't support the attributes they needed. We've now gone to a great length to support all of the main attributes in the fluent interface, so you shouldn't need this anymore. If there are any attributes you need that we've missed, let us know (or even better, send us a pull request/patch)

Categories