ASP.Net core correct way of implementing http methods for related models - asp.net-core

I have tow models Context and Connection as following:
public class Context
{
[Key]
public long ContextId { get; set; }
[Required]
public string Role { get; set; }
public ICollection<Connection> Connections { get; set; }
public Context()
{
}
}
And
public class Connection
{
[Key]
public long ConnectionId { get; set; }
[Required]
public string Name { get; set; }
public long ContextId { get; set; }
public Context Context { get; set; }
public Connection()
{
}
}
So far, I did not create any controller or repository for Connection. ContextRepositiry looks like following:
public class ContextRepository: IContextRepository
{
private readonly WebAPIDataContext _db;
public ContextRepository(WebAPIDataContext db)
{
_db = db;
}
public Context CreateContext(Context context)
{
_db.Contexts.Add(context);
_db.SaveChanges();
return context;
}
public void DeleteContext(long id)
{
Context context = GetContext(id);
if (context != null)
{
_db.Contexts.Remove(context);
_db.SaveChanges();
}
}
public List<Context> GetAllContexts()
{
return _db.Contexts.AsNoTracking().ToList();
}
public Context GetContext(long id)
{
return _db.Contexts.FirstOrDefault(o => o.ContextId == id);
}
public void UpdateContext(long id, Context context)
{
}
}
public interface IContextRepository
{
List<Context> GetAllContexts();
Context GetContext(long id);
Context CreateContext(Context context);
void UpdateContext(long id, Context context);
void DeleteContext(long id);
}
And it's controller:
[Route("api/[controller]")]
public class ContextController : Controller
{
private readonly IContextRepository _contexts;
public ContextController(IContextRepository contexts)
{
_contexts = contexts;
}
[HttpGet("")]
public IActionResult GetAllContexts()
{
try
{
List<Context> contexts = _contexts.GetAllContexts();
return Ok(contexts);
}
catch (EntityNotFoundException<Context>)
{
return NotFound();
}
}
[HttpGet("{id}")]
public IActionResult GetContext(long id)
{
Context context= _contexts.GetContext(id);
if (context == null)
{
return NotFound();
}
return Ok(context);
}
[HttpPost]
public IActionResult CreateContext([FromBody] Context context)
{
if (ModelState.IsValid == false)
{
return BadRequest(ModelState);
}
Context createdContext= _contexts.CreateContext(context);
if (createdContext== null)
{
return NotFound();
}
return CreatedAtAction(
nameof(GetContext), new { id = createdContext.ContextId}, createdContext);
}
[HttpPut("{id}")]
public IActionResult UpdateContext(long id, [FromBody] Context context)
{
if (ModelState.IsValid == false)
{
return BadRequest(ModelState);
}
try
{
_contexts.UpdateContext(id, context);
return Ok();
}
catch (EntityNotFoundException<Context>)
{
return NotFound();
}
}
[HttpDelete("{id}")]
public IActionResult DeleteCOntext(long id)
{
_contexts.DeleteContext(id);
return Ok();
}
}
Question: While creating a context I shouldn't have to enter any connection data i.e. it should be optional (look ta the swagger request bellow). However, on updating a specific context there could be connection data, and corresponding context should be updated accordingly.
Right now, in Swagger for POST if I enter something like:
{
"contextId": 0,
"role": "Employee",
"connections": [
{
"connectionId": 0,
"name": "",
"contextId": 0,
"context": {}
}
]
}
then it says, The Name field is required and The Role field is required (I am trying to send just context data like role and leaving blank connection data- which should be possible). If I remove "connections":[] then it posts with connections set to null, but don't want to remove it from there.

Related

Invalid Include Path Error in Asp.net 6 Web API

After Adding a Include property on the parent model controller I am facing the following error:
'Microsoft.EntityFrameworkCore.Query.InvalidIncludePathError': Unable to find navigation ' CurrencyList' specified in string based include path ' CurrencyList'. This exception can be suppressed or logged by passing event ID 'CoreEventId.InvalidIncludePathError' to the 'ConfigureWarnings' method in 'DbContext.OnConfiguring' or 'AddDbContext'.'
I am sharing all the related class & model.
CurrencyList (Model) : This is the child class which I want to include into the parent class for populating the dropdown.
public class CurrencyList
{
[Key]
public int Id { get; set; }
[DisplayName("CURRENCY")]
[Required(ErrorMessage = "CURRENCY is a required field")]
public string? CurrencyName { get; set; }
[DisplayName("EXCHANGE RATE")]
[Required(ErrorMessage = "EXCHANGE RATE is a required field")]
public double Rate { get; set; }
}
CurrencyList Controller:
public class CurrencyListController : ControllerBase
{
private readonly IUnitOfWork _unitOfWork;
public CurrencyListController(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
[HttpGet]
public IEnumerable<CurrencyList> Get()
{
IEnumerable<CurrencyList> objCurrencyDropDownList = _unitOfWork.Currency.GetAll();
return _unitOfWork.Currency.GetAll();
}
[HttpGet("{id:int}")]
public CurrencyList GetDetails(int id)
{
return _unitOfWork.Currency.GetDetails(id);
}
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public IActionResult Create(CurrencyList obj)
{
if (ModelState.IsValid)
{
_unitOfWork.Currency.Add(obj);
_unitOfWork.Save();
return CreatedAtAction("GetDetails", new { id = obj.Id }, obj);
}
return BadRequest();
}
[HttpPut]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public IActionResult Edit(CurrencyList obj)
{
if (ModelState.IsValid)
{
_unitOfWork.Currency.Update(obj);
_unitOfWork.Save();
return NoContent();
}
return BadRequest();
}
[HttpDelete]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public IActionResult Delete(int? id)
{
var obj = _unitOfWork.Currency.GetFirstOrDefault(c => c.Id == id);
if (obj == null)
{
return NotFound();
}
_unitOfWork.Currency.Remove(obj);
_unitOfWork.Save();
return NoContent();
}
}
BTBNewLienOpening (Model) : This is the parent class where I created FK retalion with CurrencyList Model.
public class BTBNewLienOpening
{
[Key]
public int ExpLCNoId { get; set; }
[DisplayName("EXPORT L/C NO")]
[ValidateNever]
public string? ExpLCNo { get; set; }
[DisplayName("ORDER NO")]
[ValidateNever]
public string? OrderNo { get; set; }
[DisplayName("STYLE")]
public int? StyleListId { get; set; }
[ForeignKey("StyleListId")]
[ValidateNever]
public StyleList? StyleList { get; set; }
[DisplayName("VALUE")]
[ValidateNever]
[DisplayFormat(DataFormatString = "{0:C}", ApplyFormatInEditMode = false)]
public decimal? Value { get; set; }
[DisplayName("NET VALUE")]
[ValidateNever]
[DisplayFormat(DataFormatString = "{0:C}", ApplyFormatInEditMode = false)]
public decimal? NetValue { get; set; }
[DisplayName("CURRENCY")]
public int? CurrencyListId { get; set; }
[ForeignKey("CurrencyListId")]
[ValidateNever]
public CurrencyList? CurrencyList { get; set; }
[DisplayName("QTY PCS")]
[ValidateNever]
public string? QtyPcs { get; set; }
[DisplayName("SHIPMENT")]
[ValidateNever]
public string? Shipment { get; set; }
[DisplayName("EXPIRY DATE")]
[ValidateNever]
public string? Expiry { get; set; }
[DisplayName("COUNTRY")]
public int? CountryListId { get; set; }
[ForeignKey("CountryListId")]
[ValidateNever]
public CountryList? CountryList { get; set; }
}
BTBNewLienOpeningController : This is the controller where I use the include properties for including all the dropdown properties. In the GetAll() method I use the include properties and include the dropdown properties. If I run the program without CurrencyList the app works fine. But after adding CurrencyList it shows the Microsoft.EntityFrameworkCore.Query.InvalidIncludePathError'
public class BTBNewLienOpeningController : ControllerBase
{
private readonly IUnitOfWork _unitOfWork;
private readonly ILogger<BTBNewLienOpeningController> _logger;
public BTBNewLienOpeningController(IUnitOfWork unitOfWork, ILogger<BTBNewLienOpeningController> logger)
{
_unitOfWork = unitOfWork;
_logger = logger;
}
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult GetNewLienOpening()
{
try
{
IEnumerable<BTBNewLienOpening> objBTBNewLienOpeningList = _unitOfWork.BTBNewLienOpening.GetAll(includeProperties: "StyleList, CurrencyList, CountryList");
return Ok(objBTBNewLienOpeningList);
}
catch (Exception ex)
{
_logger.LogError(ex, $"Something went wrong in the {nameof(GetNewLienOpening)}");
return StatusCode(500, "Internal Server Error, Please Try Again Leter!");
}
}
[HttpGet("{id:int}")]
public BTBNewLienOpening GetDetails(int id)
{
return _unitOfWork.BTBNewLienOpening.GetDetails(id);
}
//[Authorize(Roles = "Administrator")]
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult Create(BTBNewLienOpening obj)
{
if (ModelState.IsValid)
{
_unitOfWork.BTBNewLienOpening.Add(obj);
_unitOfWork.Save();
return CreatedAtAction("GetDetails", new { id = obj.ExpLCNoId }, obj);
}
_logger.LogError($"Something went wrong in the {nameof(Create)}");
return StatusCode(500, "Internal Server Error, Please Try Again Leter!");
}
[HttpPut]
[ProducesResponseType(StatusCodes.Status202Accepted)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult Update(BTBNewLienOpening obj)
{
if (ModelState.IsValid)
{
_unitOfWork.BTBNewLienOpening.Update(obj);
_unitOfWork.Save();
return StatusCode(202, "Successfully Updated!");
}
_logger.LogError($"Something went wrong in the {nameof(Update)}");
return StatusCode(500, "Internal Server Error, Please Try Again Leter!");
}
[HttpDelete]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult Delete(int? id)
{
if (id < 1)
{
_logger.LogError($"Invalid Delete Attempt In {nameof(Delete)}");
return BadRequest();
}
try
{
var obj = _unitOfWork.BTBNewLienOpening.GetFirstOrDefault(c => c.ExpLCNoId == id);
if (obj == null)
{
_logger.LogError($"Invalid Delete Attempt In {nameof(Delete)}");
return BadRequest("Submitted Data Is Invalid");
}
_unitOfWork.BTBNewLienOpening.Remove(obj);
_unitOfWork.Save();
return NoContent();
}
catch (Exception ex)
{
_logger.LogError(ex, $"Something Went Wrong In The {nameof(Delete)}");
return StatusCode(500, "Internal Server Error, Please Try Again Leter!");
}
}
}
Repository (Class) : Here I use the GetAll method and added the include functionality.
public class Repository<T> : IRepository<T> where T : class
{
private readonly ApplicationDbContext _db;
internal DbSet<T> dbSet;
public Repository(ApplicationDbContext db)
{
_db = db;
this.dbSet = _db.Set<T>();
}
public void Add(T entity)
{
dbSet.Add(entity);
}
public IEnumerable<T> GetAll(string? includeProperties = null)
{
IQueryable<T> query = dbSet;
if (includeProperties != null)
{
foreach (var includeProp in includeProperties.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
query = query.Include(includeProp);
}
}
return query.ToList();
}
public T GetFirstOrDefault(Expression<Func<T, bool>> filter, string? includeProperties = null)
{
IQueryable<T> query = dbSet;
query = query.Where(filter);
if (includeProperties != null)
{
foreach (var includeProp in includeProperties.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
query = query.Include(includeProp);
}
}
return query.FirstOrDefault();
}
public T GetDetails(int id)
{
return dbSet.Find(id);
}
public void Remove(T entity)
{
dbSet.Remove(entity);
}
public void RemoveRange(IEnumerable<T> entity)
{
dbSet.RemoveRange(entity);
}
public IEnumerable<T> GetProp(string? includeProperties = null)
{
IQueryable<T> query = dbSet;
if (includeProperties != null)
{
foreach (var includeProp in includeProperties.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
query = query.Include(includeProp);
}
}
return query.ToList();
}
}

system.outofmemoryexception swashbuckle.aspnetcore

I am having this issue when I am dealing with Geometry datatypes when I change the property to string everything works like a charm. Below you may see that I used schema filter to remove Ignored data member , and document filter to remove anything related to nettopology.
Property Name = GeoPoly
Swagger Config Class
public static IServiceCollection AddSwaggerModule(this IServiceCollection services)
{
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v2", new OpenApiInfo { Title = "Test API", Version = "0.0.1" });
c.SchemaFilter<MySwaggerSchemaFilter>();
c.DocumentFilter<RemoveBogusDefinitionsDocumentFilter>();
c.ResolveConflictingActions(x => x.First());
});
return services;
}
public static IApplicationBuilder UseApplicationSwagger(this IApplicationBuilder app)
{
app.UseSwagger(c =>
{
c.RouteTemplate = "{documentName}/api-docs";
});
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/v2/api-docs", "Test API");
});
return app;
}
}
public class MySwaggerSchemaFilter : Swashbuckle.AspNetCore.SwaggerGen.ISchemaFilter
{
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
if (schema?.Properties == null)
{
return;
}
var ignoreDataMemberProperties = context.Type.GetProperties()
.Where(t => t.GetCustomAttribute<IgnoreDataMemberAttribute>() != null);
foreach (var ignoreDataMemberProperty in ignoreDataMemberProperties)
{
var propertyToHide = schema.Properties.Keys
.SingleOrDefault(x => x.ToLower() == ignoreDataMemberProperty.Name.ToLower());
if (propertyToHide != null)
{
schema.Properties.Remove(propertyToHide);
}
}
}
}
public class RemoveBogusDefinitionsDocumentFilter : Swashbuckle.AspNetCore.SwaggerGen.IDocumentFilter
{
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
swaggerDoc.Components.Schemas.Remove("Districts");
swaggerDoc.Components.Schemas.Remove("Geometry");
swaggerDoc.Components.Schemas.Remove("CoordinateSequenceFactory");
swaggerDoc.Components.Schemas.Remove("GeometryOverlay");
swaggerDoc.Components.Schemas.Remove("NtsGeometryServices");
swaggerDoc.Components.Schemas.Remove("CoordinateEqualityComparer");
swaggerDoc.Components.Schemas.Remove("NtsGeometryServices");
swaggerDoc.Components.Schemas.Remove("GeometryFactory");
swaggerDoc.Components.Schemas.Remove("OgcGeometryType");
swaggerDoc.Components.Schemas.Remove("Coordinate");
swaggerDoc.Components.Schemas.Remove("Point");
}
}
Entity Class
public class Districts : BaseEntity<long>
{
public string DistrictsDesc { get; set; }
public string DistrictsDescAr { get; set; }
[IgnoreDataMember]
[Column(TypeName = "geometry")]
public Geometry GeoPoly { get; set; }
public IList<Records> Records { get; set; } = new List<Records>();
public long? RegionsId { get; set; }
public Regions Regions { get; set; }
public long? CitiesId { get; set; }
public Cities Cities { get; set; }
}
Is there a way to stop swashbuckle gen from dealing with datatypes other than documents filter ?

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.

MVC 6 Authorize only owner or admin

I have a user that can have multiple projects.
I want everybody to see the projects, but only the user that created it, or the admins, should be able to edit it.
I changed the ApplicationUser class that the template provided.
public class ApplicationUser : IdentityUser
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Address { get; set; }
}
In the Project class I save the user that created it.
public class Project
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public virtual ApplicationUser ApplicationUser { get; set; }
}
Then I check in every method if the current user is the owner of the project that he wants to change.
// GET: Projects/Edit/5
[Authorize]
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return HttpNotFound();
}
ApplicationUser user = await GetCurrentUserAsync();
Project project = _context.Project.Single(m => m.Id == id);
if (project == null)
{
return HttpNotFound();
}else if(project.ApplicationUser != user)
{
return HttpNotFound();
}
return View(project);
}
Is there a better way to do this ?
I saw that in MVC 5 you had AuthorizationAttribute, but it's no longer available.
Something like this would be great, but I don't even know where to start:
[OwnerAuthorize]
public IActionResult Edit(int? id)
EDIT:
I managed to find a way of doing this after reading https://docs.asp.net/en/latest/security/authorization/resourcebased.html
public class ProjectAuthorizationHandler : AuthorizationHandler<OperationAuthorizationRequirement, Project>
{
private readonly UserManager<ApplicationUser> _userManager;
public ProjectAuthorizationHandler(UserManager<ApplicationUser> userManager)
{
_userManager = userManager;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, OperationAuthorizationRequirement requirement, Project resource)
{
var userId = _userManager.GetUserId(context.User);
if(userId != null && resource.OwnerId == userId)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
And in the controller
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}
var project = await _context.Project.SingleOrDefaultAsync(m => m.Id == id);
if (project == null)
{
return NotFound();
}
if (!await _authorizationService.AuthorizeAsync(User, project, Operations.Update))
{
return Forbid();
}
return View(project);
}
It works, but the only thing that I don't understand is what should I do with the requirement in the HandleRequirementAsync method.
Is this the way to do this in MVC 6 ?

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
}
]
}