C# - Fluent NHibernate mapping question - c#

Is there a way to avoid the explicit Id mapping in Fluent NHibernate?
I want it to somehow generate the entry id automatically so that I wouldn't have to introduce it as a part of the class.
public class HeyMapping
{
public String Name { get; set; }
public DateTime Timestamp { get; set; }
}
public class HeyMapping : ClassMap<HeyMapping>
{
public HeyMapping()
{
Not.LazyLoad();
// I'm not particularly sure how this line works, but
// it fails the mapping unit test.
CompositeId().KeyProperty(x => x.Name);
Map(x => x.Name).Not.Nullable().Length(64);
Map(x => x.Timestamp).Not.Nullable();
}
}

If you want to have no id in your entity, you still have to map an Id so NHibernate knows the database column to use.
You can call
Id<TColumnDataType>("column_name");
Please note that you will give up some NHibernate functionality (specifically cascading updates and the ability to call SaveOrUpdate()) and incur a performance penalty on the database side for having database-only identity (I believe NHibernate will have to make extra queries for comparison).
I usually concede this point and allow the Id as the one persistence concern in my domain classes, so I would do this:
public class HeyMapping
{
protected internal int Id { get; set; } // persistence concern
public virtual String Name { get; set; }
public virtual DateTime Timestamp { get; set; }
}
I realize you might not want to do this; I'm just letting you know that there is a tradeoff.

Create a base class from which all of your mapped entities inherit, then add an Id property to your base class.

Related

Is it possible in EF Core to make a one-way navigation property required?

I am working on a basic group chat system, for which I created these classes:
public class Role
{
public Guid Id { get; set; };
public string Username { get; set; }
}
public class Message
{
public Guid Id { get; set; };
public Role Author { get; set; }
public Conversation Conversation { get; set; }
public DateTime Date { get; set; }
public string Text { get; set; }
}
public class Conversation
{
public Guid Id { get; set; };
public IList<ConversationParticipant> ConversationParticipants { get; set; };
public IList<Message> Messages { get; set; };
}
public class ConversationParticipant
{
public Conversation Conversation { get; set; }
public Role Role { get; set; }
}
We are using EF Core 3.1 Code-First with migrations.
I am looking for a way to make Message.Author a required property, which should lead to a column in table Message that is created as AuthorId NOT NULL.
I tried:
public static void Map(this EntityTypeBuilder<Message> builder)
{
builder.HasOne(m => m.Author);
}
As this is applied using Add-Migration and Update-Database, the database column AuthorId is created, but with NULLs allowed.
There does not seem to be a method IsRequired() that I can add after HasOne().
I also tried:
public static void Map(this EntityTypeBuilder<Message> builder)
{
builder.Property(m => m.Author).IsRequired();
}
but that fails saying
The property 'Message.Author' is of type 'Role' which is not supported by current database provider. Either change the property CLR type or ignore the property using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'.
Doing .HasOne(...) followed by .Property(...).IsRequired() also does not work:
'Author' cannot be used as a property on entity type 'Message' because it is configured as a navigation.
I managed to make Message.Conversation required through this:
public static void Map(this EntityTypeBuilder<Conversation> builder)
{
builder.HasMany(c => c.Messages) // A conversation can have many messages
.WithOne(e => e.Conversation) // Each message belongs to at most 1 conversation
.IsRequired(); // A message always has a conversation
}
However I'd rather not make Role aware of Messages, as I will never want to retrieve Messages directly from a Role (this will happen through Conversations and Participants).
My ultimate question is: Is there a way to make Message.Author required (NOT NULL), without linking Message and Role together in a full 1-to-many relationship with a Messages property in Role?
What about adding Role's foreign key to Message and then requiring that property to not be null? Something like:
// MessageConfiguration.cs
builder.Property(b => b.RoleId).IsRequired()
While the answer by #Ben Sampica was helpful and got me where I needed to be, the comments by #Ivan Stoev provided details and clarity that made me think that a more comprehensive answer would be useful.
There are multiple ways to make a foreign key column required (NOT NULL) in the generated table.
The simplest is to put [Required] on the navigation property:
public class Message
{
// ...
[Required] public Role Author { get; set; }
// ...
}
This will cause EF to create a shadow property AuthorId of type Guid because Message.Author is a Role and Role.Id is of type Guid. This leads to UNIQUEIDENTIFIER NOT NULL in case of SQL Server.
If you omit [Required] then EF will use Guid?, which leads to UNIQUEIDENTIFIER NULL, unless you apply one of the other options.
You can use an explicit Id property with a type that can't be null:
public class Message
{
// ...
public Guid AuthorId { get; set; }
public Role Author { get; set; }
// ...
}
Note (i) - This only works if you follow EF Core shadow property naming rules, which in this case means you must name the Id property nameof(Author) + nameof(Role.Id) == AuthorId.
Note (ii) - This will break if one day you decide to rename Author or Role.Id but forget to rename AuthorId accordingly.
If you can't or don't want to change the Model class, then you can tell EF Core that it needs to treat the shadow property as required:
builder.Property("AuthorId").IsRequired();
The same Notes apply as listed at 2, with the addition that you could now use nameof() to reduce the effort and the risks.
In the end I decided to use the [Required] approach, because
It is simple and descriptive,
No effort needed to think of which shadow property name to use,
No risk of breaking the shadow property name later on.
This may apply sometimes, not always:
Input forms may use the Model class attribute to check if a property is required. However it may be a better approach to build your forms around DTO classes, and then an attribute on an EF Model class may provide no worth for your forms.

Merge properties from mapping table to single class

I have a website that is using EF Core 3.1 to access its data. The primary table it uses is [Story] Each user can store some metadata about each story [StoryUserMapping]. What I would like to do is when I read in a Story object, for EF to automatically load in the metadata (if it exists) for that story.
Classes:
public class Story
{
[Key]
public int StoryId { get; set; }
public long Words { get; set; }
...
}
public class StoryUserMapping
{
public string UserId { get; set; }
public int StoryId { get; set; }
public bool ToRead { get; set; }
public bool Read { get; set; }
public bool WontRead { get; set; }
public bool NotInterested { get; set; }
public byte Rating { get; set; }
}
public class User
{
[Key]
public string UserId { get; set; }
...
}
StoryUserMapping has composite key ([UserId], [StoryId]).
What I would like to see is:
public class Story
{
[Key]
public int StoryId { get; set; }
public bool ToRead { get; set; } //From user mapping table for currently logged in user
public bool Read { get; set; } //From user mapping table for currently logged in user
public bool WontRead { get; set; } //From user mapping table for currently logged in user
public bool NotInterested { get; set; } //From user mapping table for currently logged in user
public byte Rating { get; set; } //From user mapping table for currently logged in user
...
}
Is there a way to do this in EF Core? My current system is to load the StoryUserMapping object as a property of the Story object, then have Non-Mapped property accessors on the Story object that read into the StoryUserMapping object if it exists. This generally feels like something EF probably handles more elegantly.
Use Cases
Setup: I have 1 million stories, 1000 users, Worst-case scenario I have a StoryUserMapping for each: 1 billion records.
Use case 1: I want to see all of the stories that I (logged in user) have marked as "to read" with more than 100,000 words
Use case 2: I want to see all stories where I have NOT marked them NotInterested or WontRead
I am not concerned with querying multiple StoryUserMappings per story, e.g. I will not be asking the question: What stories have been marked as read by more than n users. I would rather not restrict against this if that changes in future, but if I need to that would be fine.
Create yourself an aggregate view model object that you can use to display the data in your view, similar to what you've ended up with under the Story entity at the moment:
public class UserStoryViewModel
{
public int StoryId { get; set; }
public bool ToRead { get; set; }
public bool Read { get; set; }
public bool WontRead { get; set; }
public bool NotInterested { get; set; }
public byte Rating { get; set; }
...
}
This view model is concerned only about aggregating the data to display in the view. This way, you don't need to skew your existing entities to fit how you would display the data elsewhere.
Your database entity models should be as close to "dumb" objects as possible (apart from navigation properties) - they look very sensible as they are the moment.
In this case, remove the unnecessary [NotMapped] properties from your existing Story that you'd added previously.
In your controller/service, you can then query your data as per your use cases you mentioned. Once you've got the results of the query, you can then map your result(s) to your aggregate view model to use in the view.
Here's an example for the use case of getting all Storys for the current user:
public class UserStoryService
{
private readonly YourDbContext _dbContext;
public UserStoryService(YourDbContext dbContext)
{
_dbContext = dbContext;
}
public Task<IEnumerable<UserStoryViewModel>> GetAllForUser(string currentUserId)
{
// at this point you're not executing any queries, you're just creating a query to execute later
var allUserStoriesForUser = _dbContext.StoryUserMappings
.Where(mapping => mapping.UserId == currentUserId)
.Select(mapping => new
{
story = _dbContext.Stories.Single(story => story.StoryId == mapping.StoryId),
mapping
})
.Select(x => new UserStoryViewModel
{
// use the projected properties from previous to map to your UserStoryViewModel aggregate
...
});
// calling .ToList()/.ToListAsync() will then execute the query and return the results
return allUserStoriesForUser.ToListAsync();
}
}
You can then create a similar method to get only the current user's Storys that aren't marked NotInterested or WontRead.
It's virtually the same as before, but with the filter in the Where to ensure you don't retrieve the ones that are NotInterested or WontRead:
public Task<IEnumerable<UserStoryViewModel>> GetForUserThatMightRead(string currentUserId)
{
var storiesUserMightRead = _dbContext.StoryUserMappings
.Where(mapping => mapping.UserId == currentUserId && !mapping.NotInterested && !mapping.WontRead)
.Select(mapping => new
{
story = _dbContext.Stories.Single(story => story.StoryId == mapping.StoryId),
mapping
})
.Select(x => new UserStoryViewModel
{
// use the projected properties from previous to map to your UserStoryViewModel aggregate
...
});
return storiesUserMightRead.ToListAsync();
}
Then all you will need to do is to update your View's #model to use your new aggregate UserStoryViewModel instead of your entity.
It's always good practice to keep a good level of separation between what is "domain" or database code/entities from what will be used in your view.
I would recommend on having a good read up on this and keep practicing so you can get into the right habits and thinking as you go forward.
NOTE:
Whilst the above suggestions should work absolutely fine (I haven't tested locally, so you may need to improvise/fix, but you get the general gist) - I would also recommend a couple of other things to supplement the approach above.
I would look at introducing a navigation property on the UserStoryMapping entity (unless you already have this in; can't tell from your question's code). This will eliminate the step from above where we're .Selecting into an anonymous object and adding to the query to get the Storys from the database, by the mapping's StoryId. You'd be able to reference the stories belonging to the mapping simply by it being a child navigation property.
Then, you should also be able to look into some kind of mapping library, rather than mapping each individual property yourself for every call. Something like AutoMapper will do the trick (I'm sure other mappers are available). You could set up the mappings to do all the heavy lifting between your database entities and view models. There's a nifty .ProjectTo<T>() which will project your queried results to the desired type using those mappings you've specified.

Dapper Extension: class with composite fields, mapped to an individual table

I have the following classes:
public class Publication
{
public int Id { get; set; }
public string Title { get; set; }
public string Headline { get; set; }
public DateTime Published { get; set; }
public ProductContact Contact { get; set; }
}
public class ProductContact
{
public string FullName { get; set; }
public string JobTitle { get; set; }
public string Email { get; set; }
}
And the table associated with this structure, "Publications", has all these fields (included the properties of ProductContact).
If I try to insert a Publication row (with the ProductContact information included) the program throws an exception:
System.NotSupportedException: The member Contact of type ProductContact cannot be used as a parameter value
So, I added a mapper to map out the ProductContact properties to fields in the Properties table:
public PublicationMapper ()
{
TableName = "Publications";
Map(x => x.Contact.FullName).Column("ContactFullName");
Map(x => x.Contact.JobTitle).Column("ContactJobTitle");
Map(x => x.Contact.Email).Column("ContactEmail");
AutoMap();
}
With this mapper I get the same exception.
Then, I added the ignore statement for the Contact field, to tell Dapper to not include this element in the insert statement
Map(x => x.Contact).Ignore();
In this case, I get another exception:
System.Data.SqlClient.SqlException (0x80131904): Must declare the scalar variable "#FullName".
It indicates that Dapper is ignoring completely this property, and the mapping added in the previous step does not have effect.
Is there a way to map out the ProductContact properties to the table fields?
Thank you.
I don't think that this is possible with DapperExtensions.
In one of their issues, they say
Currently, we aren't planning to support nested objects. However, you
can create your own Mapper that will allow for skipping over the
nested object
I have tried a variety of approaches and different mapping classes and can't get anywhere - I don't think that they support any way to map nested property values and to ignore the property itself (which, if not done, will result in "cannot be used as a parameter value" error).
One approach would be to manually flatten your object into an anonymous class (as suggested by #juharr in a comment on your question), another approach would be to use something like AutoMapper to flatten your complex object into a flattened insert model.

Update Entity to expose existing foreign key property in EF code first?

I tried this question: How to expose Foreign Key property to existing entity having navigational property using EF6 Code First and it doesn't work. I get the following error:
The index 'IX_FormEntry_Id' is dependent on column 'FormEntry_Id'.
ALTER TABLE ALTER COLUMN FormEntry_Id failed because one or more objects
access this column.
I was just trying to expose the FormEntryId on the FormReport POCO:
public class FormReport : Entity
{
public Guid? FormEntryId { get; set; } //I added this
public virtual FormEntry FormEntry { get; set; }
//other props
}
And I used this mapping as outlined in the above linked answer:
public class FormReportMapping : EntityTypeConfiguration<FormReport>
{
public FormReportMapping()
{
HasRequired(x => x.FormEntry)
.WithOptional()
.Map(p => p.MapKey("FormEntry_Id"));
new EntityMap().MapInheritedProperties(this);
}
}
I was hoping it would recognize hey that's exactly how it is, no change needed, but that's not what's happening, how can I do this?
Edit: I'd like to keep my naming conventions, which doesn't match the auto generated ones by EF. Not a single other of my FK properties use an underscore in my POCOs. But that's what the column name in the DB is.
It can easily be done with data annotations:
public class FormReport : Entity
{
[Column("FormEntry_Id")]) // Map to the existing column name
[ForeignKey("FormEntry")] // Associate with the navigation property
public Guid? FormEntryId { get; set; }
public virtual FormEntry FormEntry { get; set; }
//other props
}
What about fluent API, looks like the only way to achieve the goal is to emulate the above:
public class FormReportMapping : EntityTypeConfiguration<FormReport>
{
public FormReportMapping()
{
Property(x => x.FormEntryId)
.HasColumnName("FormEntry_Id")
.HasColumnAnnotation("ForeignKey", "FormEntry");
// ...
}
}

Does anyone know why I'm getting this warning from NHibernate/NH Profiler?

"Disabled lazy properies fetching for fully_qualified_type_name beacuse it does not support lazy at the entity level".
This warning was reported by NH Profiler, and as a result, I'm experiencing the dreaded SELECT N + 1 side affect. So if 2200 Subgroup entities are returned, an additional query is being executed to retrieve each InvoicePreference entity (2201 queries total). Something about that relationship seems to be causing the issue.
Here are the entities in question and their respective mappings.
Entity 1
public class Subgroup : Entity
{
public virtual string GroupNumber { get; set; }
public virtual string RUSNumber { get; set; }
public virtual string REANumber { get; set; }
public virtual string CustomerType { get; set; }
public virtual string Name { get; set; }
public virtual IList<IndividualEmployment> Employees { get; set; }
public virtual IList<BenefitsAdministrator> Administrators { get; set; }
public virtual InvoicePreference InvoicePreference { get; set; }
}
Entity 2
public class InvoicePreference : IEntity
{
public virtual Guid Id { get; set; }
public virtual Guid SubgroupId { get; set; }
public virtual bool PaperlessNotifications { get; set; }
}
Mapping 1
public static AutoPersistenceModel ConfigureSubGroup(this AutoPersistenceModel
autoPersistenceModel)
{
return autoPersistenceModel.Override<Subgroup>(map =>
{
map.Table("SubgroupV");
map.Id(s => s.Id).Column(SubGroupPrimaryKeyColumn);
map.Map(s => s.CustomerType, "BAS_Customer_Type");
map.Map(s => s.RUSNumber, "BAS_RUS_Number");
map.Map(s => s.GroupNumber, "BAS_Group_Number");
map.Map(s => s.REANumber, "BAS_REA_Number");
map.HasMany(s => s.Administrators).KeyColumn(SubGroupPrimaryKeyColumn);
map.HasMany(s => s.Employees).KeyColumn(SubGroupPrimaryKeyColumn);
map.HasOne(s => s.InvoicePreference).PropertyRef(i => i.SubgroupId);
});
}
Mapping 2
public static AutoPersistenceModel ConfigureInvoicePreference(this AutoPersistenceModel autoPersistenceModel)
{
return autoPersistenceModel.Override<InvoicePreference>(map =>
{
map.Table("SubgroupInvoicePreference");
map.Schema(RetirementStatementsSchemaName);
});
}
InvoicePreference is referenced as hasone. Since it is lazyloaded by default NHibernate will create a proxy to populate the property InvoicePreference and to do that it needs the identity from InvoicePreference which is not present in the Subgroup. Therefor it has to query for it using the property in the propertyref.
To remedy that do .Not.LazyLoad() and/or .Fetch.Join()
I guess that there is some reason why NH disabled lazy loading "on entity level", which I understand as not creating proxies. There may be several reasons for that. Did you get another warning before? I don't really understand why it disabled "lazy properies", which means that some properties are lazy loaded. This is a feature that is used in the mapping explicitly, but I can't see something like this in your mapping definitions.
To overcome the N+1, you may use Fetch.Join. I had bad experience with that, because the queries get really large. In a complex model, you could hit some database server limits (like max. number of columns of a query). It is mostly better to use batch size, which reduces the number of queries notably. Take a look at my answer to "Nhinerbate lazy loading of reference entity".

Categories