Entity Framework 7 - single dbcontext, different schemas - asp.net-mvc

I have an ASP.NET 5 web api, and I was hoping to use a single dbcontext for multiple models. These models point to tables with different schemas in my database.
The models below contain multiple classes
Auth
Study
Simplified a little: Auth.cs
public class MyContext : DbContext
{
public DbSet<Auth.App> Apps { get; set; }
public DbSet<Auth.Permission> Permissions { get; set; }
public DbSet<Study.StudyLink> StudyLinks { get; set; }
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
foreach (var entity in modelBuilder.Model.GetEntityTypes())
{
if (entity.Name.Contains("Auth+"))
{
modelBuilder.HasDefaultSchema("auth"); // Switch to the auth schema
modelBuilder.Entity(entity.Name).ToTable(entity.Name.Replace("myproject.Models.Auth+", string.Empty));
}
if (entity.Name.Contains("Study+"))
{
modelBuilder.HasDefaultSchema("study"); // Switch to the study schema
modelBuilder.Entity(entity.Name).ToTable(entity.Name.Replace("myproject.Models.Study+", string.Empty));
}
}
}
Using just the Auth model, I was able to change the default schema and can access the tables no problem.. when I add the Study model, the modelBuilder.Model.GetEntityTypes() foreach brings up both the Auth and Study models, so is switching the default schema, meaning I can't access the Auth schema because it switches to study.
Is there some way I can apply the schema without using HasDefaultSchema() or do I need to create a new context for each schema I use in my database?
Thanks

You can add your schema to the .ToTable("tableName", "schemaName"):
if (entity.Name.Contains("Auth+"))
{
modelBuilder.Entity(entity.Name).ToTable(entity.Name.Replace("myproject.Models.Auth+", string.Empty),"auth");
}
https://msdn.microsoft.com/en-us/data/jj591617.aspx?f=255&MSPPError=-2147217396#2.3

Related

Best way to check if record exists and use site wide

I am fairly new to c# and MVC but I am building an intranet app. Being on the internal network there is no need to sign in to use the app but I do have it connected to a database which has an 'Administration' table. In this table are the administrator's email addresses. I am also using System.DirectoryServices.AccountManagement and then UserPrincipal.Current.EmailAddress to get the users email address. What I would like to do is compare the UserPrincipal.Current.EmailAddress to the database table and if there is a match then set a boolean to TRUE that I can reference/call upon within my entire site.
I have a model matching the database tables and I can also query the database using a where statement to the value of UserPrincipal.Current.EmailAddress but only within a set method (ActionResult) and return the boolean value within a viewbag to that particular controller that is accessed by the related view only.
I would like to know what is best practice for setting up my site so that whichever page a users visits their email is compaired to the database and a boolean is set to true/false if they are/aren't in the database administrator table.
Edit: Would this be to create a base controller and then inherit it in all other controllers and within the base controller perform the database query - if so a little guidance would be greatly appricated
My current set up is an EmailEntityModel:
using System;
using System.Data.Entity;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
public partial class EmailEntities : DbContext
{
public EmailEntities()
: base("name=EmailEntities")
{
}
public virtual DbSet<Audience> Audiences { get; set; }
public virtual DbSet<CallToAction> CallToActions { get; set; }
public virtual DbSet<ColourScheme> ColourSchemes { get; set; }
public virtual DbSet<Email> Emails { get; set; }
public virtual DbSet<EmailType> EmailTypes { get; set; }
public virtual DbSet<Administrator> Administrators { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
}
}
Then I have an email Controller:
public class EmailsController : Controller
{
private EmailEntities db = new EmailEntities();
public ActionResult Index()
{
return View(db.Emails.ToList());
}
Can I use the EmailEntities to query the Administator DBset within my controller but can I use this elsewhere?
If I understood your question correctly, you want to query the DB on every request and compare the current user's email against the admin email. If that's the case, then you have many options.
If it was me, I would keep the Admin email in a constant/static variable (so I don't have to make the trip to the DB on every request):
public static class StaticCache
{
// static constructor would run only once, the first it is used
// this value is maintained for the entire life-time of the application
static StaticCache()
{
using (var context = MyApplicationDbContext.Create())
{
// get your admin email for the DB
AdminEmail = context.Email.Where(/*some admin flag == true*/).FirstOrDefault();
}
}
public static string AdminEmail;
public static bool IsAdminUser(string curEmail)
{
return string.Equal(curEmail, AdminEmail, StringComparison.InvariantCultureIgnoreCase);
}
}
That's it. Now you can call StaticCache.IsAminUser() anywhere in your program (even in your view). All you need is to pass the current email to the method.

Setting up for selective auditing in same DbContext

I just stumbled onto Audit.Net and I'm hooked. I went through the Audit.Net Entity Framework (6) documentation and am a bit lost in the output part.
My solution is a bit many-layers design:
Mvc 5 website
Wcf Client
Using WcfClientProxyGenerator
Wcf Service
Separate IService Contracts Library
All interfaces expose BDOs
Business Logic
Seperate BDO Library
Data Access
Reference EF6
Reference Audit.Net
DTOs
Entity Framwework Library
Has EDMX only
Reference Audit.Net
Reference: My EDMX is named Focus
Usage
I manage to modify the FocusModel.Context.tt from:
partial class <#=code.Escape(container)#> : DbContext
To:
partial class <#=code.Escape(container)#> : Audit.EntityFramework.AuditDbContext
Configuration
I found the default setting for Mode, IncludeEntityObjects, & AuditEventType were to my liking. the attribute for Include/Ignore entities/properties were straightforward as well.
Output
This is where I'm confused. I need to audit to the same database preferably to Audit tables for selected entities. Every entity in my database has composite PKs. How do I set the output mode in this scenario? Also, in my solution setup, the starting point for all projects that are behind the WCF Service is the WCF service itself. Does this mean that the point to Fluent-API-configure Audit.Net is here?
Have you seen the main Audit.NET documentation, specifically the output data providers?
I need to audit to the same database preferably to Audit tables for selected entities. Every entity in my database has composite PKs.
So you can use the EF data provider. It works with any kind of primary key.
How do I set the output mode in this scenario?
I'm not sure what do you mean by output mode, but I'm guessing you ask about OptIn/OptOut to ignore your audit entities to be audited. If that's the case you have multiple options, like using AuditIgnore attribute on your audit POCO classes, or via the fluent-api OptIn()/OptOut() methods. See example below.
The starting point for all projects that are behind the WCF Service is the WCF service itself. Does this mean that the point to Fluent-API-configure Audit.Net is here?
You can configure the Audit.NET library in any place, but you must do it before any audit event creation, so it is recommended to be on your startup code, as soon as your app or service starts.
Sample code
The following is a minimal example showing how you can configure the Audit.NET and Audit.EntityFramework libraries.
Suppose you have the following schema:
public class Student
{
public int PK_1 { get; set; }
public string PK_2 { get; set; }
public string Name { get; set; }
}
public class Student_Audit
{
public int PK_1 { get; set; }
public string PK_2 { get; set; }
public string Name { get; set; }
public DateTime AuditDate { get; set; }
public string AuditAction { get; set; }
}
public class SchoolContext : AuditDbContext
{
protected override void OnModelCreating(ModelBuilder modelBuilder) //<--Tip: its not DbModelBuilder, its Microsoft.EntityFrameworkCore.ModelBuilder
{
modelBuilder.Entity<Student>().HasKey(c => new { c.PK_1, c.PK_2 });
modelBuilder.Entity<Student_Audit>().HasKey(c => new { c.PK_1, c.PK_2, c.AuditDate });
}
public DbSet<Student> Students { get; set; }
public DbSet<Student_Audit> Students_Audit { get; set; }
}
You can configure the library on your startup code as follows:
// Setup audit to use the EF data provider
Audit.Core.Configuration.Setup()
.UseEntityFramework(_ => _
.AuditTypeExplicitMapper(m => m
// Map Student to Student_Audit
.Map<Student, Student_Audit>((ev, ent, studentAudit) =>
{
//add the action name and the date to the audit entity
studentAudit.AuditAction = ent.Action;
studentAudit.AuditDate = DateTime.UtcNow;
})));
// Configure the EF audit behavior
Audit.EntityFramework.Configuration.Setup()
.ForContext<SchoolContext>(_ => _.IncludeEntityObjects())
.UseOptOut()
.Ignore<Student_Audit>(); // Do not audit the audit tables
And a test case:
using (var db = new SchoolContext())
{
db.Database.EnsureCreated();
var st = new Student() { PK_1 = 1, PK_2 = "one", Name = "John" };
db.Students.Add(st);
db.SaveChanges();
}
Will generate the following:

Why must I access the related fields of a user before I can delete them in ASP.NET MVC EF app?

There is a ApplicationUser. They can have multiple TfsAccounts and one TfsToken. A TfsAccount can have multiple TrackedTasks.
public class ApplicationUser : IdentityUser
{
public virtual ICollection<TfsAccount> TfsAccounts { get; set; }
public virtual TfsToken TfsToken { get; set; }
}
public class TfsAccount
{
[ForeignKey("ApplicationUserId")]
public virtual ApplicationUser ApplicationUser { get; set; }
public string ApplicationUserId { get; set; }
public virtual ICollection<TrackedTask> TrackedTasks { get; set; }
}
public class TrackedTask
{
[ForeignKey("TfsAccountId")]
public virtual TfsAccount TfsAccount { get; set; }
public int TfsAccountId { get; set; }
}
Now, I have a method to cancel a subscription for a user and delete them:
[HttpGet]
public async Task<ActionResult> CancelSubscription()
{
var currentUser = UserManager.FindById(User.Identity.GetUserId());
var tfsAccounts = currentUser.TfsAccounts; <---REMOVE THESE
var tfsToken = currentUser.TfsToken; <---TWO LINES AND IT BREAKS
var result = await UserManager.DeleteAsync(currentUser); <---ON THIS LINE
AuthenticationManager.SignOut();
return RedirectToAction("CancellationConfirmed");
}
Here is the error I get in the browser:
The DELETE statement conflicted with the REFERENCE constraint "FK_dbo.TfsAccounts_dbo.AspNetUsers_ApplicationUserId". The conflict occurred in database "TfsTeamStatus", table "dbo.TfsAccounts", column 'ApplicationUserId'.
The statement has been terminated.
Why do I have to access the related fields on the user before I can delete the user? This works but feels super hacky.
That's super hacky. Correct.
You need to set on delete cascade option to set for that.
If you are using EF Designer, then you can do that using EF Designer as per these steps
Set Cascade on relation in EF designer. This instruct context that all
loaded related entities must be deleted prior to deletion of the
parent entity. If this doesn't happen EF will throw exception because
internal state will detect that loaded childs are not related to any
existing parent entity even the relation is required. I'm not sure if
this happens before execution of delete statement of the parent entity
or after but there is no difference. EF doesn't reload related
entities after executing modifications so it simply doesn't know about
cascade deletes triggered in the database.
Set ON CASCADE DELETE on
relation in database. This will instruct SQL to delete all related
records which were not loaded to context in the time of deleting the
parent.
If you are doing code first approach, you can do that using Fluent API as per [Entity Framework (EF) Code First Cascade Delete for One-to-Zero-or-One relationship
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<User>()
.HasOptional(a => a.UserDetail)
.WithOptionalDependent()
.WillCascadeOnDelete(true);
}
Now, coming to your point, I am not sure why it works with the two lines when you access TfsAccount and TfsToken, but I guess EF might be setting Modified flag when you accessed it. So while doing SaveChanges it might be deleting those entities. - It's just a guess.

Parameterize Schema in Entity Framework 6

I have multiple object and some of them go to one schema "NewObjects" and the other go to "OldObjects" I want to be able to make what the NewObjects Schema is configurable from config file. Is there a way? Here is what I have.
namespace IDJC.Domain
{
[Table("Agency", Schema = "NewObjects")]
public class Agency
{
public int AgencyId { get; set; }
public string AgencyName { get; set; }
}
}
Yes, through fluent api. Check the part: Mapping an Entity Type to a Specific Table in the Database:
For example, if you create a setting in your project (through Properties->Settings) name MySchemaName, you will be able to access it in your DbContext derived class. So when you override OnModelCreating you will be able to do something like:
modelBuilder.Entity<Agency>()
.ToTable("Agency", Properties.Settings.Default.MySchemaName);

Unable to Add Controller (with scaffolding) ASP.NET MVC

This has been addressed in multiple other questions, but alas, I have tried all the solutions posted there with no success. I'm developing an ASP.NET MVC application, using Code-First EF. I am trying to take advantage of the scaffolding built in so that it can automatically create a Controller for me based off my Model and DbContext. However, I am getting the following error when I try to create a Controller in this way:
'Unable to retrieve metadata for Employer.' Using the same DbCompiledModel to create contexts against different types of database servers is not supported. Instead, create a separate DbCompiledModel for each type of server being used.
The code for my model, Employer, my DbContext, MyDataContext, and my web.config file follow:
//Employer.cs
public class Employer : Organization, IEmployer
{
public virtual PhysicalAddress Address { get; set; }
public virtual ICollection<ContactMethod> ContactMethods { get; set; }
public int FederalTaxID { get; set; }
public virtual Client Client { get; set; }
}
-
//MyDataContext.cs
[DbConfigurationType(typeof(MySqlEFConfiguration))]
public class MyDataContext : IdentityDbContext<ApplicationUser>
{
public MyDataContext()
: base("DataConnection")
{
}
static MyDataContext()
{
Database.SetInitializer(new DataInitializer());
}
public DbSet<Client> Clients { get; set; }
public DbSet<Organization> Organizations { get; set; } // Store Employer and OrgEntity
/// <summary>
/// Sets up unclear relationships between entities before the models are constructued in a database.
/// For example, models which extend other models must have their Ids mapped (because it is an
/// inherited member, and so is not found by Entity Framework by default)
/// </summary>
/// <param name="modelBuilder">the object responsible for constructing the database from code-first</param>
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
//set up parent organization relationship
modelBuilder.Entity<Organization>()
.HasOptional(n => n.ParentOrganization)
.WithMany(o => o.ChildOrganizations)
.Map(m => m.MapKey("ParentOrganization_Id"));
//set up the organization-employee assignment
modelBuilder.Entity<Employee>()
.HasOptional(a => a.OrganizationAssignment)
.WithMany(a => a.Employees);
//used to integrate Identity stuff into same db as Clients, etc.
modelBuilder.Entity<IdentityUserLogin>().HasKey<string>(l => l.UserId);
modelBuilder.Entity<IdentityRole>().HasKey<string>(r => r.Id);
modelBuilder.Entity<IdentityUserRole>().HasKey(r => new { r.RoleId, r.UserId });
}
}
-
<!--web.config-->
.
.
.
<connectionStrings>
<add name="DataConnection"
providerName="Mysql.Data.MysqlClient"
connectionString="server=localhost;user id=myid;password=mypass;persistsecurityinfo=True;database=app_db"/>
</connectionStrings>
.
.
.
From various other posts like this one , this one , and this one . I have tried many different things to get it to work. This includes commenting out my constructor for MyDbContext, changing the providerName attribute of my connection string to be "System.Data.SqlClient", changing the connection string name to be "DefaultConnection", removing my connection strings altogether, and combinations of all of these things. I make sure to rebuild between trying to add the Controller. However, after performing these different changes, I still receive the same error when I try to add a new Controller.
Generally, I can find all my questions already answered, but the answers don't seem to be working for me on this one. I think what might separate my case from the ones linked is that my DbContext is actually an instance of IdentityDbContext. I believe the constructor for IdentityDbContext just calls the base constructor for it, anyway, so I don't see how this could be much of an issue.
Any help is much appreciated. Thank you!
You didn't map your Employer entity to the model. That's why compiler is unable to retrieve metadata for Employer. Firstly, add DbSet as below and let me know, if does it work.
public DbSet<Client> Clients { get; set; }
public DbSet<Organization> Organizations { get; set; }
public DbSet<Employer> Employers { get; set; }

Resources