asp.net core 2 Access httpcontext from scoped DI service accessed through hangfire task - asp.net-core

I have an asp.net core2 application with hangfire. Configuration works fine for basic tasks, however now I need to enqueue a background job which accesses the dependency injected httpcontext and dbcontext and I am getting null reference exceptions for httpcontext - I understand why this would be, but...
Can I configure hangfire's enqueue such that the httpcontext and dbcontext from which the task is fired are included with the job? The job is always originally fired from within a controller where the contexts are available. The job is a method on ApprovalService which has _userservice injected into its constructor. _Userservice has the httpContext injected in its constructor.
As I understand it, the graph should be able to resolve this, it is just a question of how...
I dont want to refactor to pass these as arguments as the services are used elsewhere where they do have access to the contexts.
The
My startup is as follows (a lot of things removed for clarity)
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
private IHostingEnvironment _env;
public static string connection;
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddCors();
services.AddMvc(config => {
config.Filters.Add(new AuthorizeFilter(authorizePolicy));
config.OutputFormatters.OfType<StringOutputFormatter>().Single().SupportedMediaTypes.Add("text/html");
})
.AddJsonOptions(options =>
{
options.SerializerSettings.ContractResolver = new Newtonsoft.Json.Serialization.DefaultContractResolver();
options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
});
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
connection = Configuration.GetConnectionString("(default)");
services.AddDbContext<CpContext>(options =>
{
options.UseSqlServer(connection);
});
services.AddHangfire(configuration => configuration
.UseSqlServerStorage(connection));
services.AddScoped<IApprovalService, ApprovalService>();
services.AddScoped<IUserService, UserService>();
services.AddScoped<SystemControlService>();
services.AddScoped<ProjectControlService>();
services.AddIdentity<CpIdentityUser, IdentityRole>().AddUserManager<cpUserManager>();
services.AddScoped<ApprovalService>();
services.AddTransient<IEmailService, EmailService>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
Log.Information("In configure");
_env = env;
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
//app.UseBrowserLink();
}
app.UseCors(x => x
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials());
app.UseAuthentication();
app.UseMvc();
app.UseStaticFiles();
app.UseHangfireDashboard("/hangfire", new DashboardOptions
{
Authorization = new[] { new CustomAuthorizeFilter() }
});
app.UseHangfireServer();
}
public class CustomAuthorizeFilter : IDashboardAuthorizationFilter
{
public bool Authorize([NotNull] DashboardContext context)
{
var httpcontext = context.GetHttpContext();
return httpcontext.User.Identity.IsAuthenticated;
}
}
}
I fire the job like this (non job version commented and works - hangfire job hangs on nullreference when trying to get userID from the context);
[Produces("application/json")]
[Route("api/Approvals")]
public class ApprovalsController : Controller
{
private readonly CpContext _context;
private IUserService _userService;
private IBackgroundJobClient _backgroundJobClient;
private ApprovalService _approvalService;
public ApprovalsController(CpContext context, IUserService userService, ApprovalService approvalService, IBackgroundJobClient backgroundJobClient)
{
_context = context;
_userService = userService;
_approvalService = approvalService;
_backgroundJobClient = backgroundJobClient;
}
public class approvalWrapper
{
public int ApprovalId { get; set; }
public List<string> emailTo { get; set; }
public List<string> ccTo { get; set; }
public string ManualApprCode { get; set; }
public int RequestToId { get; set; }
public DateTime RequestDate { get; set; }
public DateTime RequiredDate { get; set; }
public DateTime ResponseDate { get; set; }
public string RequestText { get; set; }
public string ResponseText { get; set; }
public int ApprovalStatusTypeId { get; set; }
public int ApprovalItemTypeId { get; set; }
public int? NcrLinkId { get; set; }
public int? LotItpDetailLinkId { get; set; }
public int? LotQtyLinkId { get; set; }
}
// POST: api/Approvals/sendRequest
[HttpPost("sendRequest")]
public async Task<IActionResult> sendRequest([FromBody] approvalWrapper approvalInfo)
{
if (!ModelState.IsValid) return BadRequest(ModelState);
Approval approval = new Approval()
{
RequestById = _userService.User_ID,
RequestToId = approvalInfo.RequestToId,
RequestDate = approvalInfo.RequestDate,
RequiredDate = approvalInfo.RequiredDate,
RequestText = approvalInfo.RequestText,
NcrLinkId = approvalInfo.NcrLinkId,
LotItpDetailLinkId = approvalInfo.LotItpDetailLinkId,
LotQtyLinkId = approvalInfo.LotQtyLinkId,
ApprovalItemTypeId = approvalInfo.ApprovalItemTypeId,
ApprovalStatusTypeId = 5,
};
try
{
_context.Approval.Add(approval);
await _context.SaveChangesAsync();
}
catch (Exception ex)
{
Log.Error(ex, "Error trying to create approval.");
return StatusCode(422);
}
_backgroundJobClient.Enqueue<IApprovalService>(serv => serv.sendRequestEmailAsync(approval.ApprovalId, approvalInfo.emailTo, approvalInfo.ccTo));
//await _approvalService.sendRequestEmailAsync(approval.ApprovalId, approvalInfo.emailTo, approvalInfo.ccTo);
return Ok(1);
}
}
interface IApprovalService
{
Task<string> getApprovalRequestTextForChecklistItem(int checklistItemId);
Task<string> getApprovalRequestTextForNCR(int NCRId);
Task<bool> sendRequestEmailAsync(int apprToRequestID, List<string> emailTo = null, List<string> ccTo = null);
Task<bool> sendResponseEmailAsync(int apprToRequestID, List<string> emailTo = null, List<string> ccTo = null);
Task<bool> isApprovalCodeValidAsync(string qryString, int apprToRequestID);
}
public class ApprovalService: IApprovalService
{
CpContext _context;
IEmailService _emailService;
private ProjectControlService _projectControlService;
private SystemControlService _systemControlService;
private IUserService _userService;
public ApprovalService(CpContext context, IEmailService emailService, SystemControlService systemControlService,
ProjectControlService projectControlService, IUserService userService)
{
_context = context;
_emailService = emailService;
_userService = userService;
_systemControlService = systemControlService;
_projectControlService = projectControlService;
}
public interface IUserService
{
int Project_ID { get; }
int User_ID { get; }
Task<UserCredDto> AuthenticateAsync(string username, string password);
HashSet<string> getUserPermsForProject(int userID, int ProjectID);
IEnumerable<User> GetAll();
Task<User> GetByIdAsync(int id);
Task<User> GetUserAsync();
Task<User> CreateUserAsync(User user, string password);
Task UpdateAsync(User user, string password = null);
Task<User> DeleteAsync(int id);
bool Exists(int id);
string checkRefreshToken(string refreshToken, UserCredDto tokenOwner, int refreshLifeTime);
Task<string> getNewRefreshTokenAsync(UserCredDto tokenOwner, int refreshLifeTime = 60);
string GetUserName();
Task<UserDto> GetUser();
ClaimsPrincipal GetClaimsPrincipal();
}
public class UserService : IUserService
{
private CpContext _context;
private readonly IHttpContextAccessor _httpcontext;
public UserService(CpContext context, IHttpContextAccessor httpcontext)
{
_context = context;
_httpcontext = httpcontext;
}
}

Related

Custom role based auth .NET CORE

I have a project in which users can have multiple roles, such as cashier and stock clerk. These roles have the same rights, however someone can also have the roles admin and cashier. In this case he can access more features than a admin/cashier on its own.
I've searched far and wide but I don't get any wiser from the documentation, as I first thought policies were the way to go, but now I think we need claim-based authorization.
After searching and playing around I found no answers on the following questions:
What tables/entities do I need?
Can this be done without scaffolding tools?
How does this whole process work, how does .NET CORE know what roles to look at? How can I use custom roles?
If someone could help me out with this I would appreciate it.
Cheers.
One way is to use Identity and authorize the user by using [Authorize(Roles ="Admin")].
If you want to do without scaffolding tools,you could use jwt token authentication or cookie authentication.
Here is a simple demo about how to use cookie authentication:
Model:
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Password { get; set; }
public List<UserRole> UserRoles { get; set; }
}
public class Role
{
public int Id { get; set; }
public string RoleName { get; set; }
public List<UserRole> UserRoles { get; set; }
}
public class UserRole
{
public int UserId { get; set; }
public User User { get; set; }
public int RoleId { get; set; }
public Role Role { get; set; }
}
public class LoginModel
{
public string Name { get; set; }
public string Password { get; set; }
}
Controller:
public class HomeController : Controller
{
private readonly YouDbContext _context;
public HomeController(YouDbContext context)
{
_context = context;
}
public IActionResult Login()
{
return View();
}
[HttpPost]
public async Task<IActionResult> Login(LoginModel model)
{
var claims = new List<Claim>{};
var user = _context.User
.Include(u=>u.UserRoles)
.ThenInclude(ur=>ur.Role)
.Where(m => m.Name == model.Name).FirstOrDefault();
if(user.Password==model.Password)
{
foreach(var role in user.UserRoles.Select(a=>a.Role.RoleName))
{
var claim = new Claim(ClaimTypes.Role, role);
claims.Add(claim);
}
var claimsIdentity = new ClaimsIdentity(
claims, CookieAuthenticationDefaults.AuthenticationScheme);
var authProperties = new AuthenticationProperties{};
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity),
authProperties);
}
return View("Index");
}
public IActionResult Index()
{
return View();
}
//allow Cashier
[Authorize(Roles = "Cashier")]
public IActionResult Privacy()
{
return View();
}
//allow Admin
[Authorize(Roles = "Admin")]
public IActionResult AllowAdmin()
{
return View();
}
//allow both of the Admin and Cashier
[Authorize(Roles = "Admin,Cashier")]
public IActionResult AllowBoth()
{
return View();
}
//user has no rights to access the page
public IActionResult AccessDenied()
{
return View();
}
//log out
public async Task<IActionResult> Logout()
{
await HttpContext.SignOutAsync(
CookieAuthenticationDefaults.AuthenticationScheme);
return RedirectToAction("Index");
}
}
DbContext:
public class YouDbContext: DbContext
{
public YouDbContext(DbContextOptions<YouDbContext> options)
: base(options)
{
}
public DbSet<User> User { get; set; }
public DbSet<Role> Role { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<UserRole>()
.HasKey(bc => new { bc.UserId, bc.RoleId });
modelBuilder.Entity<UserRole>()
.HasOne(bc => bc.User)
.WithMany(b => b.UserRoles)
.HasForeignKey(bc => bc.UserId);
modelBuilder.Entity<UserRole>()
.HasOne(bc => bc.Role)
.WithMany(c => c.UserRoles)
.HasForeignKey(bc => bc.RoleId);
}
}
Startup.cs:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/Home/Login";
options.AccessDeniedPath = "/Home/AccessDenied";
});
services.AddDbContext<WebApplication1Context>(options =>
options.UseSqlServer(Configuration.GetConnectionString("WebApplication1Context")));
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
Result:

.net Core 3 / API / DI / Repository pattern

I'm trying to create a web API in .net Core. I would like this API calls a DLL with repository design pattern implemented in it. In this whole thing I tried to use dependency injection but I have some issues to manage context for database in repository.
I would like than the context one new context per lifetime of a call to the API.
When I try to execute my code I have an exception at line CreateHostBuilder(args).Build().Run(); in Main.
This Exception is :
'Some services are not able to be constructed'
InvalidOperationException : Unable to resolve service for type 'BX_Security_AccessBase.Context.SecurityContext' while attempting to activate 'BX_Security_AccessBase.Repository.UsersRepository'.
I know the code is incomplete and won't work completely but at least it should break way later than actually. I think I made a mistake in the architecture.
There is a lot of code below but I couldn't isolate my problem.
Thank you everybody.
In my API I have :
Startup.cs
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddScoped<IUsersRepository, UsersRepository>();
services.AddScoped<IUserService, UserService>();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
User.cs
public class User
{
public User() {}
public int UserId { get; set; }
public string LastName { get; set; }
public string FirstName { get; set; }
public DateTime Birthdate { get; set; }
}
UserService.cs
public class UserService : IUserService
{
private readonly AppSettings _appSettings;
private readonly IUsersRepository _userRepository;
public UserService(IOptions<AppSettings> appSettings, IUsersRepository userRepository)
{
_appSettings = appSettings.Value;
_userRepository = userRepository;
}
public IEnumerable<User> GetAll()
{
return _userRepository.GetAllUsers().Select(u=> new User());
}
}
IUserService.cs
public interface IUserService
{
public IEnumerable<User> GetAll();
}
AppSettings.cs
public class AppSettings
{
public string Secret { get; set; }
}
UsersController.cs
[Route("api/[controller]")]
[ApiController]
public class UsersController : ControllerBase
{
private IUserService _userService { get; }
public UsersController(IUserService userService)
{
_userService = userService;
}
[HttpGet]
public IActionResult GetAll()
{
var users = _userService.GetAll();
return Ok(users);
}
}
In the DLL I have :
SecurityContext.cs
public partial class SecurityContext : DbContext
{
public SecurityContext(DbContextOptions<SecurityContext> options) : base(options) { }
public DbSet<Users> Users { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
optionsBuilder.UseSqlServer("Data Source=; Database=BXDB; User Id=sa; Password=;");
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Users>(entity =>
{
entity.HasKey(e => e.UserId).HasName("PK_User_UserId");
entity.ToTable("Users", "sec");
entity.Property(e => e.Birthdate).HasColumnType("date");
entity.Property(e => e.FirstName)
.HasMaxLength(50)
.IsUnicode(false);
entity.Property(e => e.LastName)
.HasMaxLength(50)
.IsUnicode(false);
});
OnModelCreatingPartial(modelBuilder);
}
partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}
Users.cs
public class Users
{
public Users() { }
public int UserId { get; set; }
public string LastName { get; set; }
public string FirstName { get; set; }
public DateTime Birthdate { get; set; }
}
IUsersRepository.cs
public interface IUsersRepository
{
public IQueryable<Users> GetAllUsers();
}
UsersRepository.cs
public class UsersRepository : IUsersRepository
{
public readonly SecurityContext _dbContext;
public UsersRepository(SecurityContext dbContext)
{
_dbContext = dbContext;
}
public IQueryable<Users> GetAllUsers()
{
return _dbContext.Users;
}
}
'Some services are not able to be constructed' InvalidOperationException : Unable to resolve service for type 'BX_Security_AccessBase.Context.SecurityContext' while attempting to activate 'BX_Security_AccessBase.Repository.UsersRepository'.
From the error , you should register the DbContext as a service as follows:
public void ConfigureServices(IServiceCollection services)
{
var connection = #"Server=(localdb)\mssqllocaldb;Database=BXDB;Trusted_Connection=True;ConnectRetryCount=0";
services.AddDbContext<DLL.Models.SecurityContext>(options => options.UseSqlServer(connection, x => x.MigrationsAssembly("DLL")));
services.AddControllers();
services.AddScoped<IUsersRepository, UsersRepository>();
services.AddScoped<IUserService, UserService>();
}

Invalid column name 'EmailAddress' when using generic repository, but works fine with context

Getting the mentioned error when trying to do a GetAll on accounts. It works fine if I go directly to the dbcontext, but gives me the error if I try to work with the repo. I have about 20 others that use just the generic repo and are working great. Because I have additional actions for Accounts, I have created its own repository that implements the generic. I also have several others that work like this and have no problem. The problem is specific to the accounts.
Database of course does have the EmailAddress column, since I can return it if I use dbcontext from the controller instead of the repo.
Any help would be much appreciated.
AccountsController:
public class AccountsController : ControllerBase
{
private readonly AccountRepository _repo;
public AccountsController(DatabaseContext context)
{
_repo = new AccountRepository(context);
}
[HttpGet]
public async Task<ActionResult<IEnumerable<Account>>> GetAccount()
{
// return _context.Account.ToListAsync(); works fine if _context is defined
var accounts = await _repo.GetAll();
if (accounts == null)
return NoContent();
return Ok(accounts); // Gives invalid column error
}
[HttpGet("getaccount")]
public async Task<ActionResult<Account>> GetCurrentAccount()
{
var account = await _repo.GetCurrentAccount(HttpContext.User.Identity.Name);
if (account == null)
{
return NotFound();
}
return account; // Works fine
}
}
Account:
public partial class Account
{
public string Name { get; set; }
public string RefId { get; set; }
public string Position { get; set; }
public bool IsActive { get; set; }
public string EmailAddress { get; set; }
[Key]
public string UserId { get; set; }
}
IAccountRepository:
public interface IAccountRepository : IRepository<Account>
{
Task<Account> GetCurrentAccount(string emailAddress);
}
AccountRepository:
public class AccountRepository : Repository<Account>, IAccountRepository
{
private DatabaseContext _context;
public AccountRepository(DatabaseContext context)
{
_context = context;
}
public async Task<Account> GetCurrentAccount(string emailAddress)
{
var account = await _context.Account
.Where(a => a.EmailAddress == emailAddress)
.FirstOrDefaultAsync();
return account; // this works just fine, and returns with EmailAddress
}
}
IRepository (generic):
public interface IRepository<T>
{
Task<IEnumerable<T>> GetAll();
Task<T> GetById(object id);
void Add(T entity);
void Update(T entity);
void Delete(T entity);
Task<bool> Save();
}
Repository (generic):
public class Repository<T> : IRepository<T> where T : class
{
private DatabaseContext _context;
public Repository()
{
_context = new DatabaseContext();
}
public Repository(DatabaseContext context)
{
_context = context;
}
public void Add(T obj)
{
_context.Set<T>().Add(obj);
}
public void Delete(T entity)
{
_context.Set<T>().Remove(entity);
}
public async Task<IEnumerable<T>> GetAll()
{
return await _context.Set<T>().ToListAsync();
}
public async Task<T> GetById(object id)
{
return await _context.Set<T>().FindAsync(id);
}
public void Update(T obj)
{
_context.Set<T>().Update(obj);
}
public async Task<bool> Save()
{
try
{
await _context.SaveChangesAsync();
}
catch (Exception)
{
return false;
}
return true;
}
}
EDIT
I should mention that EmailAddress was added to the database via EF migration.

ASP.Net Core storing and retrieving an entity against identity user

I am building a Web API and have implemented registration and login. I have a model called Task which is as following:
public class User_Task
{
[Key]
public long TaskId { get; set; }
public string What { get; set; }
public string How_often { get; set; }
public string How_important { get; set; }
[ForeignKey("FeatureId")]
public long? FeatureId { get; set; }
public virtual ICollection<Step> Steps { get; set; }
public User_Task()
{
}
}
It's repository:
public class User_TaskRepository : IUser_TaskRepository
{
private readonly WebAPIDataContext _context;
public User_TaskRepository(WebAPIDataContext context)
{
_context = context;
}
public IEnumerable<User_Task> GetAll()
{
return _context.User_Tasks.Include(task => task.Steps).ToList();
}
public void Add(User_Task item)
{
_context.User_Tasks.Add(item);
_context.SaveChanges();
}
public User_Task Find(long key)
{
return _context.User_Tasks.Include(task => task.Steps).FirstOrDefault(t => t.TaskId == key);
}
public void Remove(long key)
{
var entity = _context.User_Tasks.First(t => t.TaskId == key);
_context.User_Tasks.Remove(entity);
_context.SaveChanges();
}
public void Update(User_Task item)
{
_context.User_Tasks.Update(item);
_context.SaveChanges();
}
}
public interface IUser_TaskRepository
{
void Add(User_Task item);
IEnumerable<User_Task> GetAll();
User_Task Find(long key);
void Remove(long key);
void Update(User_Task item);
}
And it's controller:
[Route("api/[controller]")]
public class User_TaskController : Controller
{
private readonly IUser_TaskRepository _taskRepository;
//Controller
public User_TaskController(IUser_TaskRepository taskRepository)
{
_taskRepository = taskRepository;
}
//Get methods
[HttpGet]
public IEnumerable<User_Task> GetAll()
{
return _taskRepository.GetAll();
}
[HttpGet("{id}", Name = "GetTask")]
public IActionResult GetById(long id)
{
var item = _taskRepository.Find(id);
if (item == null)
{
return NotFound();
}
return new ObjectResult(item);
}
//Create
[HttpPost]
public IActionResult Create([FromBody] User_Task item)
{
if (item == null)
{
return BadRequest();
}
_taskRepository.Add(item);
return CreatedAtRoute("GetTask", new { id = item.TaskId }, item);
}
//Update
[HttpPut("{id}")]
public IActionResult Update(long id, [FromBody] User_Task item)
{
if (item == null)
{
return BadRequest();
}
var task = _taskRepository.Find(id);
if (task == null)
{
return NotFound();
}
task.What = item.What;
task.How_often = item.How_often;
task.How_important = item.How_important;
UpdateTaskSteps(item.Steps, task.Steps);
_taskRepository.Update(task);
return new NoContentResult();
}
private void UpdateTaskSteps(ICollection<Step> steps, ICollection<Step> taskSteps)
{
foreach (var step in steps)
{
Step taskStep = taskSteps.FirstOrDefault(x => x.StepId == step.StepId);
if (taskStep != null)
{
// Update
taskStep.What = step.What;
}
else
{
// Create
taskSteps.Add(new Step
{
What = step.What,
TaskId = step.TaskId
});
}
}
}
//Delete
[HttpDelete("{id}")]
public IActionResult Delete(long id)
{
var task = _taskRepository.Find(id);
if (task == null)
{
return NotFound();
}
_taskRepository.Remove(id);
return new NoContentResult();
}
}
Now I have ApplicationUser model as following:
public class ApplicationUser : IdentityUser
{
// Extended Properties
public string FirstName { get; set; }
public string LastName { get; set; }
public ApplicationUser()
{
}
}
And yet another Stakeholder model:
public class Stakeholder
{
public int Id { get; set; }
public string IdentityId { get; set; }
public ApplicationUser Identity { get; set; } // navigation property
public Stakeholder()
{
}
}
How can I make sure that each Task is created against the logged in user i.e. Stakeholder? I will have to update my Task model with a foreign key to Stakeholder? How can I do that, and how can update my controller methods so that I can send back Tasks belonging to the user/Stakeholder making the request?
UPDATE: startup.cs
public class Startup
{
private const string SecretKey = "iNivDmHLpUA223sqsfhqGbMRdRj1PVkH"; // todo: get this from somewhere secure
private readonly SymmetricSecurityKey _signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(SecretKey));
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
}
public IConfigurationRoot Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddDbContext<WebAPIDataContext>(options =>
{
options.UseMySql(Configuration.GetConnectionString("MysqlConnection"),
b => b.MigrationsAssembly("Vision_backlog_backend"));
});
services.AddSingleton<IJwtFactory, JwtFactory>();
// jwt wire up
// Get options from app settings
var jwtAppSettingOptions = Configuration.GetSection(nameof(JwtIssuerOptions));
// Configure JwtIssuerOptions
services.Configure<JwtIssuerOptions>(options =>
{
options.Issuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)];
options.Audience = jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)];
options.SigningCredentials = new SigningCredentials(_signingKey, SecurityAlgorithms.HmacSha256);
});
services.AddScoped<IProfileRepository, ProfileRepository>();
services.AddScoped<IUser_TaskRepository, User_TaskRepository>();
services.AddScoped<IFeatureRepository, FeatureRepository>();
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy",
builder => builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader());
});
// api user claim policy
services.AddAuthorization(options =>
{
options.AddPolicy("ApiUser", policy => policy.RequireClaim(Constants.Strings.JwtClaimIdentifiers.Rol, Constants.Strings.JwtClaims.ApiAccess));
});
services.AddIdentity<ApplicationUser, IdentityRole>
(o =>
{
// configure identity options
o.Password.RequireDigit = false;
o.Password.RequireLowercase = false;
o.Password.RequireUppercase = false;
o.Password.RequireNonAlphanumeric = false;
o.Password.RequiredLength = 6;
})
.AddEntityFrameworkStores<WebAPIDataContext>()
.AddDefaultTokenProviders();
services.AddMvc().AddFluentValidation(fv => fv.RegisterValidatorsFromAssemblyContaining<Startup>());
services.AddAutoMapper();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Info { Title = "My API", Version = "v1" });
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
// global policy - assign here or on each controller
app.UseCors("CorsPolicy");
var jwtAppSettingOptions = Configuration.GetSection(nameof(JwtIssuerOptions));
var tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)],
ValidateAudience = true,
ValidAudience = jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)],
ValidateIssuerSigningKey = true,
IssuerSigningKey = _signingKey,
RequireExpirationTime = false,
ValidateLifetime = false,
ClockSkew = TimeSpan.Zero
};
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
TokenValidationParameters = tokenValidationParameters
});
app.UseMvc();
// Enable middleware to serve generated Swagger as a JSON endpoint.
app.UseSwagger();
// Enable middleware to serve swagger-ui (HTML, JS, CSS etc.), specifying the Swagger JSON endpoint.
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});
}
}
From what I've understood, you want each Stakeholder to have a list of User_Task.
I suggest you add a foreign key to your User_Task class which references the Stakeholder Id, then add navigation properties to your User_Task and Stakeholder classes.
The following should work:
User_Task class:
public class User_Task
{
[Key]
public long TaskId { get; set; }
public string What { get; set; }
public string How_often { get; set; }
public string How_important { get; set; }
[ForeignKey("FeatureId")]
public long? FeatureId { get; set; }
public virtual ICollection<Step> Steps { get; set; }
// EF should detect a reference to another table if your property name follows the {className}{idName} format
// so the ForeignKey attribute isn't really needed
[ForeignKey("StakeholderId")]
[Required]
public int StakeholderId { get; set; }
public Stakeholder Stakeholder { get; set; }
public User_Task()
{
}
}
Stakeholder class:
public class Stakeholder
{
public int Id { get; set; }
public string IdentityId { get; set; }
public ApplicationUser Identity { get; set; }
// navigation property for User_Tasks
public ICollection<User_Task> User_Tasks { get; set; }
public Stakeholder()
{
}
}
For your repository class, you could have a method that returns all Tasks that belong to a certain Stakeholder based on the logged in user's Id:
public ICollection<User_Task> GetUserTasks(string userId){
Stakeholder currentStakeholder = _context.Stakeholders
.FirstOrDefault(sh => sh.IdentityId == userId);
var userTasks = _context.User_Tasks
.Where(task => task.StakeholderId == currentStakeholder.Id).ToList();
return userTasks;
}
Now to get the logged in user's Id, you have to use the UserManager class, which should be injected into your DI Container by IdentityServer if you've set it up correctly. So you just have to add a UserManager to your controller's constructor.
The Controller class has a property called "User", which you can pass to the GetUserId() method of the UserManager class:
[Route("api/[controller]")]
public class User_TaskController : Controller
{
private readonly IUser_TaskRepository _taskRepository;
private readonly UserManager<ApplicationUser> _userManager;
//Controller
public User_TaskController(IUser_TaskRepository taskRepository, UserManager<ApplicationUser> userManager)
{
_taskRepository = taskRepository;
_userManager = userManager;
}
// The Authorize header means that this method cannot be accessed if the requester is not authenticated
[Authorize]
[HttpGet("current")]
public IActionResult GetCurrentUserTasks()
{
string currentUserId = _userManager.GetUserId(User);
var userTasks = _taskRepository.GetUserTasks(userId);
return userTasks;
}
}
Some additional things to consider:
You might want to adopt RESTful style when it comes to your APIs. Consider making the logged in user access his own tasks through another controller that follows a pattern like: /Account/Tasks
Since EF Core does not support Lazy Loading yet, you don't need to add the "virtual" keyword before navigation properties
You can also setup foreign keys in your DbContext's OnModelCreating method as follows:
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<User_Task>().HasOne(t => t.Stakeholder).WithMany(sh => sh.User_Tasks).HasForeignKey(t => t.StakeholderId);
}
Update
Adding a Task to a specific user in your repository class:
public void Add(string userId, User_Task item)
{
Stakeholder currentStakeholder = _context.Stakeholders
.FirstOrDefault(sh => sh.IdentityId == userId);
item.StakeholderId = currentStakeholder.Id;
_context.User_Tasks.Add(item);
_context.SaveChanges();
}
You could also add a Task to a Stakeholder by calling "Add()" to a Stakeholder object's User_Tasks ICollection.
Another thing to keep in mind: You should probably use DTOs when dealing with input for creating your entities. Users shouldn't have the possibility of setting the primary keys of entries, unless that's something you want because of some use case.

RavenDB UniqueConstraint doesn't seem to work

I've been trying for a day to get UniqueConstraint working, but it doesn't seem the are. I have a simple MVC6 site that creates a User on a POST. I'm expecting that on the second POST an exception should be thrown as a user will have already been created with the same properties. I'm wanting to ensure that the email address is unique.
using Raven.Client;
using Raven.Client.Document;
using Raven.Client.UniqueConstraints;
namespace MVC6Test.DomainModel
{
public class User
{
public string Id { get; private set; }
[UniqueConstraint]
public string Email { get; set; }
public string Password { get; set; }
public string Name { get; set; }
}
}
namespace MVC6Test.Web.Controllers
{
public class AdminController : Microsoft.AspNet.Mvc.Controller
{
private IDocumentStore _documentStore { get; set; }
public IDocumentSession Session { get; set; }
[HttpPost]
[AllowAnonymous]
[Route("login")]
public async Task<IActionResult> Login(string userName, string password)
{
User user = new User() {
Email = "test#gmail.com"
};
Session.Store(user);
}
public override void OnActionExecuting(ActionExecutingContext context)
{
if (_documentStore.IsDefault()) {
_documentStore = context.HttpContext.RequestServices.GetRequiredService<IDocumentStore>();
}
Session = _documentStore.OpenSession();
base.OnActionExecuting(context);
}
public override void OnActionExecuted(ActionExecutedContext context)
{
using (Session) {
if (Session != null && context.Exception == null) {
Session.SaveChanges();
}
}
base.OnActionExecuted(context);
}
}
}
namespace MVC6Test.Web
{
public class Startup
{
private IDocumentStore DocumentStore;
public void ConfigureServices(IServiceCollection services)
{
DocumentStore = new DocumentStore {
DefaultDatabase = "MVC6Test",
Url = "http://localhost:3366"
};
DocumentStore.Listeners.RegisterListener(new UniqueConstraintsStoreListener());
DocumentStore.Initialize();
services.TryAddSingleton(typeof(IDocumentStore), (provider) => {
return DocumentStore;
});
}
public void Configure(IApplicationBuilder app, IApplicationLifetime lifetime)
{
lifetime.ApplicationStopped.Register(() => {
DocumentStore.Dispose();
});
}
}
}
I do get this metadata on the items that are created:
{
"Raven-Entity-Name": "Users",
"Raven-Clr-Type": "MVC6Test.DomainModel.User, MVC6Test",
"Ensure-Unique-Constraints": [
{
"Name": "Email",
"CaseInsensitive": false
}
]
}