I been developing a C# ASP.NET Core Application and I am currently doing databases using SQL Server Object Explorer. I want to display certain hardcoded data as a testing, however when I try to run the page, I got this error
System.InvalidOperationException: 'The entity type 'recent_health_issue' requires a primary key to be defined. If you intended to use a keyless entity type, call 'HasNoKey' in 'OnModelCreating'
My primary key is already defined and the display page works normally without that line of error code, so I am not sure here.
My database:
Model class:
public class recent_health_issue
{
[Required, MinLength(3, ErrorMessage = "Enter at least 3 characters"),
MaxLength(5)]
public string recent_id { get; set; }
[Required, MaxLength(25)]
public string recent_name { get; set; }
[Required, EmailAddress]
public string recent_email { get; set; }
[Required]
public string recent_description { get; set; }
}
My DbContext:
public class RecentHealthIssueDBContext : DbContext
{
private readonly IConfiguration _config;
public RecentHealthIssueDBContext(IConfiguration configuration)
{
_config = configuration;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
string connectionString = _config.GetConnectionString("MyConn");
optionsBuilder.UseSqlServer(connectionString);
}
public DbSet<recent_health_issue> HealthIssues { get; set; }
}
My controller (where the error happens):
public class RecentHealthController
{
private RecentHealthIssueDBContext _context;
public RecentHealthController(RecentHealthIssueDBContext context)
{
_context = context;
}
public List<recent_health_issue> getAllRecentHealthIssues()
{
List<recent_health_issue> AllRecentHealth = new List<recent_health_issue>();
// error occured here
AllRecentHealth = _context.HealthIssues.ToList();
return AllRecentHealth;
}
public recent_health_issue getRecentHealthIssueById(String id)
{
recent_health_issue RecentHealthIssue = new recent_health_issue();
return RecentHealthIssue;
}
}
The error message says you need to define a primary key for the table:
[Key]
[Required, MinLength(3, ErrorMessage = "Enter at least 3 characters"),
MaxLength(5)]
public string recent_id { get; set; }
Also try to rename the DbSet property in your DbContext to match the DB table name, which is RecentHealthIssues instead of HealthIssues:
public DbSet<recent_health_issue> RecentHealthIssues { get; set; }
To clarify, from your question, you're doing database first development.
This means that your model and your database need to match exactly, so if there is any discrepancy then in most cases won't work.
Entity Framework expects Pascal Case for properties, and if it finds a single occurrence of property either named or containing "Id" then it will assume that is the key.
If you don't follow pascal case (which is the C# standard), or don't use a property containing the letters "Id", then you must use the [Key] data annotation.
As you're doing database first, you've chosen snake case for your SQL table, which means that you need to align the names with the C# without breaking coding conventions and allowing EF to do it's thing.
Also, looking at your code and what you're doing, as you're using snake_case to define the names in the database, then you should be using data annotations for tables/column names, not changing your property names to align, as defined here: https://learn.microsoft.com/en-us/ef/ef6/modeling/code-first/data-annotations#table-and-column
For your information, EF expects Pascal casing in C#, so it can do it's automation (which is why you should be using it)
public long Id {get;set;} //ef will recognise this as a Key
public long HelloCheekyThisIsMyId {get;set;} //ef will recognise this as a Key
[Key] //ef will be told this is a key
public long product_identity { get; set; }
So your model should look like this:
[Table("recent_health_issue")]
public class RecentHealthIssue
{
[Key] //this shouldn't be needed but it's good for readability
[Required, MinLength(3, ErrorMessage = "Enter at least 3 characters"), MaxLength(5)]
[Column("recent_id")]
public string RecentId { get; set; }
[Required, MaxLength(25)]
[Column("recent_name")]
public string RecentName { get; set; }
[Required, EmailAddress]
[Column("recent_email")]
public string RecentEmail { get; set; }
[Required]
[Column("recent_description")]
public string RecentDescription { get; set; }
}
as defined in the documentation: "Don’t confuse Column’s TypeName attribute with the DataType DataAnnotation. DataType is an annotation used for the UI and is ignored by Code First."
Your context like this:
public class RecentHealthIssueDBContext : DbContext
{
private readonly IConfiguration _config;
public RecentHealthIssueDBContext(IConfiguration configuration)
{
_config = configuration;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
string connectionString = _config.GetConnectionString("MyConn");
optionsBuilder.UseSqlServer(connectionString);
}
public DbSet<RecentHealthIssue> HealthIssues { get; set; }
}
and your controller should look like this:
public class RecentHealthController
{
private RecentHealthIssueDBContext _context;
public RecentHealthController(RecentHealthIssueDBContext context)
{
_context = context;
}
//This should be PascalCase (GetAllRecentHealthIssues)
public List<recent_health_issue> getAllRecentHealthIssues()
{
List<RecentHealthIssue> allRecentHealth = new List<RecentHealthIssue>();
allRecentHealth = _context.HealthIssues.ToList(); //error occured here
return allRecentHealth;
}
//This should be PascalCase (GetRecentHealthIssuesById)
public recent_health_issue getRecentHealthIssueById(String id)
{
//return recent health issues using an Id
}
}
Also I would reccomend some defensive programming, it will save you headaches in the future. (Null checks, try/catch blocks etc.)
This is the C# coding convention:
https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions
You can find out more about the available annotations here:
https://learn.microsoft.com/en-us/ef/ef6/modeling/code-first/data-annotations#key
Related
is there "elegant" way to give specific property a default value ?
Maybe by DataAnnotations, something like :
[DefaultValue("true")]
public bool Active { get; set; }
Thank you.
You can do it by manually edit code first migration:
public override void Up()
{
AddColumn("dbo.Events", "Active", c => c.Boolean(nullable: false, defaultValue: true));
}
It's been a while, but leaving a note for others.
I achieved what is needed with an attribute and I decorated my model class fields with that attribute as I want.
[SqlDefaultValue(DefaultValue = "getutcdate()")]
public DateTime CreatedDateUtc { get; set; }
Got the help of these 2 articles:
EF on CodePlex
Andy Mehalick blog
What I did:
Define Attribute
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class SqlDefaultValueAttribute : Attribute
{
public string DefaultValue { get; set; }
}
In the "OnModelCreating" of the context
modelBuilder.Conventions.Add( new AttributeToColumnAnnotationConvention<SqlDefaultValueAttribute, string>("SqlDefaultValue", (p, attributes) => attributes.Single().DefaultValue));
In the custom SqlGenerator
private void SetAnnotatedColumn(ColumnModel col)
{
AnnotationValues values;
if (col.Annotations.TryGetValue("SqlDefaultValue", out values))
{
col.DefaultValueSql = (string)values.NewValue;
}
}
Then in the Migration Configuration constructor, register the custom SQL generator.
SetSqlGenerator("System.Data.SqlClient", new CustomMigrationSqlGenerator());
The above answers really helped, but only delivered part of the solution.
The major issue is that as soon as you remove the Default value attribute, the constraint on the column in database won't be removed. So previous default value will still stay in the database.
Here is a full solution to the problem, including removal of SQL constraints on attribute removal.
I am also re-using .NET Framework's native DefaultValue attribute.
Usage
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
[DefaultValue("getutcdate()")]
public DateTime CreatedOn { get; set; }
For this to work you need to update IdentityModels.cs and Configuration.cs files
IdentityModels.cs file
Add/update this method in your ApplicationDbContext class
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
var convention = new AttributeToColumnAnnotationConvention<DefaultValueAttribute, string>("SqlDefaultValue", (p, attributes) => attributes.SingleOrDefault().Value.ToString());
modelBuilder.Conventions.Add(convention);
}
Configuration.cs file
Update your Configuration class constructor by registering custom Sql generator, like this:
internal sealed class Configuration : DbMigrationsConfiguration<ApplicationDbContext>
{
public Configuration()
{
// DefaultValue Sql Generator
SetSqlGenerator("System.Data.SqlClient", new DefaultValueSqlServerMigrationSqlGenerator());
}
}
Next, add custom Sql generator class (you can add it to the Configuration.cs file or a separate file)
internal class DefaultValueSqlServerMigrationSqlGenerator : SqlServerMigrationSqlGenerator
{
private int dropConstraintCount;
protected override void Generate(AddColumnOperation addColumnOperation)
{
SetAnnotatedColumn(addColumnOperation.Column, addColumnOperation.Table);
base.Generate(addColumnOperation);
}
protected override void Generate(AlterColumnOperation alterColumnOperation)
{
SetAnnotatedColumn(alterColumnOperation.Column, alterColumnOperation.Table);
base.Generate(alterColumnOperation);
}
protected override void Generate(CreateTableOperation createTableOperation)
{
SetAnnotatedColumns(createTableOperation.Columns, createTableOperation.Name);
base.Generate(createTableOperation);
}
protected override void Generate(AlterTableOperation alterTableOperation)
{
SetAnnotatedColumns(alterTableOperation.Columns, alterTableOperation.Name);
base.Generate(alterTableOperation);
}
private void SetAnnotatedColumn(ColumnModel column, string tableName)
{
if (column.Annotations.TryGetValue("SqlDefaultValue", out var values))
{
if (values.NewValue == null)
{
column.DefaultValueSql = null;
using var writer = Writer();
// Drop Constraint
writer.WriteLine(GetSqlDropConstraintQuery(tableName, column.Name));
Statement(writer);
}
else
{
column.DefaultValueSql = (string)values.NewValue;
}
}
}
private void SetAnnotatedColumns(IEnumerable<ColumnModel> columns, string tableName)
{
foreach (var column in columns)
{
SetAnnotatedColumn(column, tableName);
}
}
private string GetSqlDropConstraintQuery(string tableName, string columnName)
{
var tableNameSplitByDot = tableName.Split('.');
var tableSchema = tableNameSplitByDot[0];
var tablePureName = tableNameSplitByDot[1];
var str = $#"DECLARE #var{dropConstraintCount} nvarchar(128)
SELECT #var{dropConstraintCount} = name
FROM sys.default_constraints
WHERE parent_object_id = object_id(N'{tableSchema}.[{tablePureName}]')
AND col_name(parent_object_id, parent_column_id) = '{columnName}';
IF #var{dropConstraintCount} IS NOT NULL
EXECUTE('ALTER TABLE {tableSchema}.[{tablePureName}] DROP CONSTRAINT [' + #var{dropConstraintCount} + ']')";
dropConstraintCount++;
return str;
}
}
Your model properties don't have to be 'auto properties' Even though that is easier. And the DefaultValue attribute is really only informative metadata
The answer accepted here is one alternative to the constructor approach.
public class Track
{
private const int DEFAULT_LENGTH = 400;
private int _length = DEFAULT_LENGTH;
[DefaultValue(DEFAULT_LENGTH)]
public int LengthInMeters {
get { return _length; }
set { _length = value; }
}
}
vs.
public class Track
{
public Track()
{
LengthInMeters = 400;
}
public int LengthInMeters { get; set; }
}
This will only work for applications creating and consuming data using this specific class. Usually this isn't a problem if data access code is centralized. To update the value across all applications you need to configure the datasource to set a default value. Devi's answer shows how it can be done using migrations, sql, or whatever language your data source speaks.
What I did, I initialized values in the constructor of the entity
Note: DefaultValue attributes won't set the values of your properties automatically, you have to do it yourself
I admit that my approach escapes the whole "Code First" concept. But if you have the ability to just change the default value in the table itself... it's much simpler than the lengths that you have to go through above... I'm just too lazy to do all that work!
It almost seems as if the posters original idea would work:
[DefaultValue(true)]
public bool IsAdmin { get; set; }
I thought they just made the mistake of adding quotes... but alas no such intuitiveness. The other suggestions were just too much for me (granted I have the privileges needed to go into the table and make the changes... where not every developer will in every situation). In the end I just did it the old fashioned way. I set the default value in the SQL Server table... I mean really, enough already! NOTE: I further tested doing an add-migration and update-database and the changes stuck.
After #SedatKapanoglu comment, I am adding all my approach that works, because he was right, just using the fluent API does not work.
1- Create custom code generator and override Generate for a ColumnModel.
public class ExtendedMigrationCodeGenerator : CSharpMigrationCodeGenerator
{
protected override void Generate(ColumnModel column, IndentedTextWriter writer, bool emitName = false)
{
if (column.Annotations.Keys.Contains("Default"))
{
var value = Convert.ChangeType(column.Annotations["Default"].NewValue, column.ClrDefaultValue.GetType());
column.DefaultValue = value;
}
base.Generate(column, writer, emitName);
}
}
2- Assign the new code generator:
public sealed class Configuration : DbMigrationsConfiguration<Data.Context.EfSqlDbContext>
{
public Configuration()
{
CodeGenerator = new ExtendedMigrationCodeGenerator();
AutomaticMigrationsEnabled = false;
}
}
3- Use fluent api to created the Annotation:
public static void Configure(DbModelBuilder builder){
builder.Entity<Company>().Property(c => c.Status).HasColumnAnnotation("Default", 0);
}
It's simple! Just annotate with required.
[Required]
public bool MyField { get; set; }
the resultant migration will be:
migrationBuilder.AddColumn<bool>(
name: "MyField",
table: "MyTable",
nullable: false,
defaultValue: false);
If you want true, change the defaultValue to true in the migration before updating the database
In .NET Core 3.1 you can do the following in the model class:
public bool? Active { get; set; }
In the DbContext OnModelCreating you add the default value.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Foundation>()
.Property(b => b.Active)
.HasDefaultValueSql("1");
base.OnModelCreating(modelBuilder);
}
Resulting in the following in the database
Note:
If you don't have nullable (bool?) for you property you will get the following warning
The 'bool' property 'Active' on entity type 'Foundation' is configured with a database-generated default. This default will always be used for inserts when the property has the value 'false', since this is the CLR default for the 'bool' type. Consider using the nullable 'bool?' type instead so that the default will only be used for inserts when the property value is 'null'.
I found that just using Auto-Property Initializer on entity property is enough to get the job done.
For example:
public class Thing {
public bool IsBigThing{ get; set; } = false;
}
using System.ComponentModel;
[DefaultValue(true)]
public bool Active { get; set; }
In EF core released 27th June 2016 you can use fluent API for setting default value. Go to ApplicationDbContext class, find/create the method name OnModelCreating and add the following fluent API.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<YourTableName>()
.Property(b => b.Active)
.HasDefaultValue(true);
}
Just Overload the default constructor of Model class and pass any relevant parameter which you may or may not use. By this you can easily supply default values for attributes. Below is an example.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Aim.Data.Domain
{
[MetadataType(typeof(LoginModel))]
public partial class Login
{
public Login(bool status)
{
this.CreatedDate = DateTime.Now;
this.ModifiedDate = DateTime.Now;
this.Culture = "EN-US";
this.IsDefaultPassword = status;
this.IsActive = status;
this.LoginLogs = new HashSet<LoginLog>();
this.LoginLogHistories = new HashSet<LoginLogHistory>();
}
}
public class LoginModel
{
[Key]
[ScaffoldColumn(false)]
public int Id { get; set; }
[Required]
public string LoginCode { get; set; }
[Required]
public string Password { get; set; }
public string LastPassword { get; set; }
public int UserGroupId { get; set; }
public int FalseAttempt { get; set; }
public bool IsLocked { get; set; }
public int CreatedBy { get; set; }
public System.DateTime CreatedDate { get; set; }
public Nullable<int> ModifiedBy { get; set; }
public Nullable<System.DateTime> ModifiedDate { get; set; }
public string Culture { get; set; }
public virtual ICollection<LoginLog> LoginLogs { get; set; }
public virtual ICollection<LoginLogHistory> LoginLogHistories { get; set; }
}
}
Even from .NET Core 1.0, It is possible to set default values when you are using the code first approach. See the following code snippet.
using System.ComponentModel;
[DefaultValue(true)]
public bool Active { get; set; }
Read for more: Microsoft official docs
Lets consider you have a class name named Products and you have a IsActive field. just you need a create constructor :
Public class Products
{
public Products()
{
IsActive = true;
}
public string Field1 { get; set; }
public string Field2 { get; set; }
public bool IsActive { get; set; }
}
Then your IsActive default value is True!
Edite :
if you want to do this with SQL use this command :
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.IsActive)
.HasDefaultValueSql("true");
}
The Entity Framework Core Fluent API HasDefaultValue method is used to specify the default value for a database column mapped to a property. The value must be a constant.
public class Contact
{
public int ContactId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public bool IsActive { get; set; }
public DateTime DateCreated { get; set; }
}
public clas SampleContext : DbContext
{
public DbSet<Contact> Contacts { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Context>()
.Propery(p => p.IsActive)
.HasDefaultValue(true);
}
}
Or
like it!
You can also specify a SQL fragment that is used to calculate the default value:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Created)
.HasDefaultValueSql("getdate()");
}
Hmm... I do DB first, and in that case, this is actually a lot easier. EF6 right? Just open your model, right click on the column you want to set a default for, choose properties, and you will see a "DefaultValue" field. Just fill that out and save. It will set up the code for you.
Your mileage may vary on code first though, I haven't worked with that.
The problem with a lot of other solutions, is that while they may work initially, as soon as you rebuild the model, it will throw out any custom code you inserted into the machine-generated file.
This method works by adding an extra property to the edmx file:
<EntityType Name="Thingy">
<Property Name="Iteration" Type="Int32" Nullable="false" **DefaultValue="1"** />
And by adding the necessary code to the constructor:
public Thingy()
{
this.Iteration = 1;
Set the default value for the column in table in MSSQL Server, and in class code add attribute, like this:
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
for the same property.
I have two sets of objects: Coupon and DiscountScheme.
Each have a connected object of {Type}Action, and identical configurations.
When making a request for Coupon, I do not get anything back,
but the same query for DiscountScheme works as expected
A condensed version of the classes (The full code and sql for the tables can be found here):
public class CouponAction
{
public int Id { get; set; }
public virtual Coupon Coupon { get; set; }
}
public class Coupon
{
public int Id { get; set; }
public virtual CouponAction Action { get; set; }
}
public class DiscountSchemeAction
{
public int Id { get; set; }
public virtual DiscountScheme DiscountScheme { get; set; }
}
public class DiscountScheme
{
public int Id { get; set; }
public virtual DiscountSchemeAction Action { get; set; }
}
The configuration:
public class CouponActionMap : EntityTypeConfiguration<CouponAction>
{
public CouponActionMap()
{
ToTable("CouponAction");
}
}
public class CouponMap : EntityTypeConfiguration<Coupon>
{
public CouponMap()
{
ToTable("Coupon");
HasRequired(c => c.Action);
}
}
public class DiscountSchemeActionMap : EntityTypeConfiguration<DiscountSchemeAction>
{
public DiscountSchemeActionMap()
{
ToTable("DiscountSchemeAction");
}
}
public class DiscountSchemeMap : EntityTypeConfiguration<DiscountScheme>
{
public DiscountSchemeMap()
{
ToTable("DiscountScheme");
HasRequired(ds => ds.Action);
}
}
The query I am trying to make:
using(var context = new Context()/* My database context, using a custom wrapper framework*/)
{
Console.WriteLine(context.Coupons.ToList()); // nothing
Console.WriteLine(context.DiscountSchemes.ToList()); // the contents of the table
}
If I query the actions table, I do get the contents, but again for CouponAction I do not get the connected Coupon, and for DiscountScheme it works as expected.
The issue is with your 1-to-1 relationship. By default EF expects a 1-to-1 to be using the PKs on both tables. By putting a CouponID on your CouponAction you are not setting a 1-to-1 relationship, you are setting a 1-to-many/many-to-1. Nothing stops several CouponAction records from having the same CouponId. You could put a unique constraint on CouponID, but if that were the case then you may as well have the CouponAction's PK to be the CouponID. Hence, this is why I don't advise using "Id" as a PK name, but rather CouponId vs. DiscountId, etc.
If the relationship between coupon and action is truly 1-to-1 then get rid of the CouponId on the Action table, and ensure you're using the same ID value across both tables for the related records. You can test this by changing your mapping to configure EF to use CouponId on the CouponAction as it's PK. Once you do that, you should see your related records coming up.
Alternatively you can establish a many to 1 relationship (HasOne.WithMany()) from Action to Coupon, but no return reference without a CouponActionId on Coupon. Or you can set up a 1-to-many where Coupon contains an ICollection<CouponAction> CouponActions even though you intend to only have one action per coupon. But if it is 1-to-1 then I would highly recommend using the same PK value across both tables.
I have the following model:
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string Category { get; set; }
public decimal Price { get; set; }
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
[ScaffoldColumn(false)]
public DateTime CreatedAt { get; set; } = DateTime.Now;
}
And the following application context:
public class ApplicationContext : DbContext
{
private readonly string _connectionString;
public DbSet<Product> Products { get; set; }
public ApplicationContext(IConfiguration configuration)
{
_connectionString = configuration.GetConnectionString("Sportshop");
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseNpgsql(_connectionString, b => b.MigrationsAssembly("Web"));
}
}
Next I will show you a screen of my PostgreSQL data base schema:
I'm trying to save a new entity with the help of the next code:
public ViewResult Index()
{
Product product = new Product
{
Name = "P1",
Category = "test",
Price = 1000,
Description = "Hello !!!"
};
_applicationContext.Products.Add(product);
_applicationContext.SaveChanges();
return View(// ... some view data);
}
But as the result I see this error. I don't understand why.
I absolutely sure that CreatedAtfield is not null during the saving. See a screen of debug:
Where is the mistake?
Your property indeed has a value, but it is also decorated with [DatabaseGenerated(DatabaseGeneratedOption.Computed)].
By using Computed, you are telling EF Core that it must leave the property's value upto the database. In other words, the database is made fully responsible for generating or computing a value during every query. As can be read here, "The practical effect of this is that Entity Framework will not include the property in INSERT or UPDATE statements".
The generated insert statement will then look like
INSERT INTO Products (Id, Name, Description, Category, Price) ...
Which will fail because CreatedAt has a NOT NULL setting, apparently without a default value.
You can try giving the field a default value using the Fluent API:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>()
.Property(b => b.CreatedAt)
.HasDefaultValueSql("getdate()");
}
This should work as long as it is supported for Npgsql, which I don't know for sure. If it does not work then you may have no other option but to remove the attribute and take value creation in your own hands.
I get this error when trying to add something on my db through EF Core.
System.InvalidOperationException: 'No suitable constructor found for
entity type 'HealthCheck'. The following constructors had parameters
that could not be bound to properties of the entity type: cannot bind
'hctype' in 'HealthCheck(string title, string hctype, string link)'.'
This is my HealthCheck class:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Application.Models
{
public class HealthCheck
{
public HealthCheck(string title, string hctype, string link)
{
Title = title;
HCType = hctype;
Link = link;
}
public int Id { get; set; }
public string Title { get; set; }
public string HCType { get; set; }
public string Link { get; set; }
}
}
My RepositoryContext
using Microsoft.EntityFrameworkCore;
using Application.Models;
namespace Application.Repository
{
public class RepositoryContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(
#"Server=(localdb)\mssqllocaldb;Database=healthcheck;Integrated Security=True");
}
//public DbSet<HealthCheck> HealthChecks { get; set; }
//public DbSet<UserHealthCheck> UserHealthChecks { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<HealthCheck>().ToTable("HealthCheck");
modelBuilder.Entity<UserHealthCheck>().ToTable("UserHealthCheck");
}
}
}
My Repository
using Application.Models;
namespace Application.Repository
{
public class Repository
{
public void InsertHealthCheck(HealthCheck healthCheck)
{
using (var db = new RepositoryContext())
{
db.Add(healthCheck);
db.SaveChanges();
}
}
}
}
And this is where "InsertHealthCheck()" is being called from
[Route("/api/HealthCheck/Website")]
[HttpPost]
public ActionResult WebsiteStatus([FromBody] WebsiteDataModel websiteData)
{
HealthCheck data = new HealthCheck(websiteData.Title, "Website", websiteData.Url);
try
{
HttpWebRequest myHttpWebRequest = (HttpWebRequest)WebRequest.Create(websiteData.Url);
HttpWebResponse myHttpWebResponse = (HttpWebResponse)myHttpWebRequest.GetResponse();
HttpStatusCode HealthCheckStatusCode = myHttpWebResponse.StatusCode;
myHttpWebResponse.Close();
return Ok(HealthCheckStatusCode);
}
catch(UriFormatException)
{
return Ok("Check url.");
}
catch (Exception)
{
return Ok("400");
}
finally
{
repository.InsertHealthCheck(data);
}
}
If you can give me a hand I would appreciate it, if you need for me to post any other part of the code just ask.
Also, I literally just started learning EF Core, so if I did something really stupid, point it out
Providing a parameterless constructor avoids the problem, but it was not the true cause of the error in the OP's case. EF Core 2.1 and higher uses a strict convention to map constructor parameters to property names of the entity. It expects a constructor parameter's name to be a true camel-case representation of a property name in Pascal-case. If you change the parameter name from "hctype" to "hCType", you should not get the error and not have to provide a parameterless constructor if your approach to domain-driven design dictates that it would be problematic to do so.
If, however, you were simply providing the parameterized constructor as a convenience but it is not improper for a caller to be able to instantiate a HealthCheck with the "new" operator, then simply adding the parameterless constructor is acceptable.
You are missing empty constructor:
public class HealthCheck
{
// here
public HealthCheck()
{
}
public HealthCheck(string title, string hctype, string link)
{
Title = title;
HCType = hctype;
Link = link;
}
public int Id { get; set; }
public string Title { get; set; }
public string HCType { get; set; }
public string Link { get; set; }
}
try it like that
I got this exception.
No suitable constructor was found for entity type 'Blog'. The following constructors had parameters that could not be bound to properties of the entity type: cannot bind 'name' in 'Blog(string name)'; cannot bind 'name' in 'Blog(int id, string name)'.'
I mitigated this in two ways.
First take a look at my Blog class.
public class Blog
{
public Blog(string name)
{
Name = name;
}
private Blog(int id, string name)
{
Id = id;
Name = name;
}
public int Id { get; private set; }
public string Name { get; } // Note: No setter, it's a readonly prop.
public List<Post> Posts { get; } = new();
}
Note there is not setter for Name.
So the first approach was to add private setter. Now its no more a readonly prop. It can be set from within the class.
public string Name { get; private set; }
The app ran without exception.
But if you insist on keeping the Name prop readonly here is another way. We need to add and explicit configuration to the context as follows. Here we tell Ef Core to explicitly map it. Ef Core does not map readonly properties by default.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Ef does not map readonly properties by default, we have to
// ask ef to map the Name even though Name is readonly
modelBuilder.Entity<Blog>(b => b.Property(e => e.Name));
}
As the HealthCheck class is representing a Table in your database, so you have to provide an empty constructor.
Try the following implementation and let me know if you still receive the same error:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Application.Models
{
public class HealthCheck
{
public HealthCheck()
{
}
public HealthCheck(string title, string hctype, string link)
{
Title = title;
HCType = hctype;
Link = link;
}
public int Id { get; set; }
public string Title { get; set; }
public string HCType { get; set; }
public string Link { get; set; }
}
}
So many ways of getting this error :-( Here's yet another one:
read-only properties are not mapped by convention [... even if the PropertyName / constructorParameterNames accurately follow the PascalCase/camelCase matching convention ...], and hence there are no mapped properties that match the constructor. Mapping the properties like this should result in the constructor being used without error:
modelBuilder.Entity<A>(b =>
{
b.Property(e => e.Id).ValueGeneratedNever();
b.Property(e => e.Name);
});
https://github.com/dotnet/efcore/issues/14336
But here is a summary of the rules for ef core as at 2021, which should cover most cans and can'ts:
https://learn.microsoft.com/en-us/ef/core/modeling/constructors
I have two versions of an almost identical database. Below I have created an Example table to demonstrate the basic differences, namely the ID column has changed from an Integer Identity to a GUID and various properties have been updated, in the Example archived has been replaced with readOnly and hidden:
Legacy version:
CREATE TABLE Example
(
--Data Identity (maps to DbId in the example code)
Id int IDENTITY PRIMARY KEY,
--Example columns
SomeValue varchar(50),
AnotherValue int,
--Data Properties
Archived bit
)
New version:
CREATE TABLE Example
(
--Data Identity (maps to DbId in the example code)
Id uniqueidentifier PRIMARY KEY,
--Example columns
SomeValue varchar(50),
AnotherValue int,
--Data Properties
ReadOnly bit,
Hidden bit
)
I need to be able to use an O/R mapper such as NHibernate to connect to one or other of these database versions. I would like to be able to tell the application which version to use through settings in a configuration file.
My initial plan was to create a common interface for the business logic and use an IoC container such as Unity to swap between the relevant concrete classes in the configuration file.
Below is an example of the code I created to test this theory:
public interface IDataIdentity
{
object Id { get; }
}
public class LegacyDataIdentity : IDataIdentity
{
public virtual long DbId { get; set; }
public object Id
{
get { return DbId; }
}
}
public class NewDataIdentity : IDataIdentity
{
public virtual Guid DbId { get; set; }
public object Id
{
get { return DbId; }
}
}
public interface IDataProperties
{
bool ReadOnly { get; set; }
bool Hidden { get; set; }
}
public class LegacyDataProperties : IDataProperties
{
public virtual bool Archived { get; set; }
public bool ReadOnly
{
get { return Archived; }
set { Archived = value; }
}
public bool Hidden
{
get { return Archived; }
set { Archived = value; }
}
}
public class NewDataProperties : IDataProperties
{
public virtual bool ReadOnly { get; set; }
public virtual bool Hidden { get; set; }
}
public class DataItem
{
public DataItem(IDataIdentity dataIdentity, IDataProperties dataProperties)
{
DataIdentity = dataIdentity;
DataProperties = dataProperties;
}
public IDataIdentity DataIdentity { get; set; }
public IDataProperties DataProperties { get; set; }
}
public class Example : DataItem
{
public Example(IDataIdentity dataIdentity, IDataProperties dataProperties)
: base(dataIdentity, dataProperties)
{
}
public virtual string SomeValue { get; set; }
public virtual int AnotherValue { get; set; }
}
Can anyone advise if this is possible (specifically with Unity and NHibernate) and if so how to create the relevant NHibernate mapping files?
Alternatively, can anyone suggest any solution to the problem using any other methods or other IoC and O/R mapping tools (commercial or open source)?
Many thanks,
Paul
Why not abstract your data provider, implementing 2 versions (one with nhibernate mappings for the legacy data item, and one for the new data item).
To paraphrase your code slightly (simplified for clarity):
public abstract class AbstractData
{
public abstract string SomeValue { get; set; }
public abstract bool ReadOnly { get; set; }
//etc.
}
public interface IDataProvider
{
AbstractData Get(object id);
}
public class LegacyData : AbstractData
{
// Implement AbstractData, and
public virtual long Id { get { return m_Id; } set { m_Id = value; };
private long m_Id;
}
public class LegacyDataNHibernateProvider : IDataProvider
{
public LegacyDataProvider()
{
// Set up fluent nhibernate mapping
}
public AbstractData Get(object id)
{
// Interpret id as legacy identifier, retrieve LegacyData item, and return
}
};
// Same again for new data provider
This way, you aren't tied to nhibernate (or a database, for that matter), and you can specify concrete classes with correctly typed identifiers (which nhibernate can handle). This is the approach I'm taking, where I currently have to map against an existing database's SPs, but will later migrate to a new ORM-based system.
Maybe I'm not understanding your question correctly, but it sounds to me like you need to implement something like the "Factory Pattern".
I recently used the factory pattern to code (c#) to two data layers when the company I work for was switching from JDE to SAP. I was able to flip a config switch to switch between the two data layers, and the GUI wouldn't know any different.
Here's a couple links I found:
http://msdn.microsoft.com/en-us/library/ms954600.aspx
http://www.allapplabs.com/java_design_patterns/factory_pattern.htm
http://en.wikipedia.org/wiki/Abstract_factory_pattern
As far as NHibernate goes, I'm not familiar with it... sorry. Hope this helps.
Hard to recommend without knowing the full picture, but.... You can create an abstruction in the SPs by having SP return identical dataset for both table structures.
Another abstruction I am thinking of, is you can actually specify different hybernate mapping files, and initialize Hibernate with a different file depending on what database you connect to.