I'm new in .NET and Web API; I create a signup user API with .NET 5.
This is my controller:
namespace SignupUsers.Controllers
{
[Route("api/signup")]
[ApiController]
public class MySignupController : ControllerBase
{
private readonly IDataRepository<UserSignup> _dataRepository;
public MySignupController(IDataRepository<UserSignup> dataRepository)
{
_dataRepository = dataRepository;
}
[HttpGet]
public IActionResult Get()
{
IEnumerable<UserSignup> userSignups = _dataRepository.GetAll();
return Ok(userSignups);
}
[HttpGet("{id}", Name = "Get")]
public IActionResult Get(int id)
{
UserSignup userSignup = _dataRepository.Get(id);
if (userSignup == null)
{
return NotFound("User Not Found!!!");
}
return Ok(userSignup);
}
[HttpPost]
public IActionResult Post([FromBody] UserSignup userSignup)
{
if (userSignup == null)
{
return BadRequest("User is null!!!");
}
_dataRepository.Add(userSignup);
return CreatedAtRoute(
"Get",
new { Id = userSignup.Id },
userSignup);
}
[HttpPut("{id}")]
public IActionResult Put(int id, [FromBody] UserSignup userSignup)
{
if (userSignup == null)
{
return BadRequest("User is null!!!");
}
UserSignup userSignupToUpdate = _dataRepository.Get(id);
if (userSignupToUpdate == null)
{
return NotFound("The User record couldn't be found");
}
_dataRepository.Update(userSignupToUpdate, userSignup);
return NoContent();
}
[HttpDelete("{id}")]
public IActionResult Delete(int id)
{
UserSignup userSignup = _dataRepository.Get(id);
if (userSignup == null)
return NotFound("The User record couldn't be found");
_dataRepository.Delete(userSignup);
return NoContent();
}
}
}
I added the DbContext to another folder:
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using SignupUsers.Models;
namespace SignupUsers.Data
{
public class UserSignupDbContext : DbContext
{
public UserSignupDbContext(DbContextOptions<UserSignupDbContext> options)
: base(options)
{
}
public DbSet<Models.UserSignup> SignupUsers { get; set; }
}
}
For using code first I use EF Core.
UserSignup model class:
namespace SignupUsers.Models
{
public class UserSignup
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public int Password { get; set; }
}
}
For managing input and output, I use an interface and implemented that in datamanager.cs:
namespace SignupUsers.Models
{
public class DataManager : IDataRepository<UserSignup>
{
readonly UserSignupDbContext _userSignupDbContext;
public DataManager(UserSignupDbContext context)
{
_userSignupDbContext = context;
}
public void Add(UserSignup entity)
{
_userSignupDbContext.SignupUsers.Add(entity);
_userSignupDbContext.SaveChanges();
}
public void Delete(UserSignup entity)
{
_userSignupDbContext.SignupUsers.Remove(entity);
_userSignupDbContext.SaveChanges();
}
public UserSignup Get(int id)
{
return _userSignupDbContext.SignupUsers.
FirstOrDefault(s => s.Id == id);
}
public IEnumerable<UserSignup> GetAll()
{
return _userSignupDbContext.SignupUsers.ToList();
}
public void Update(UserSignup dbEntity, UserSignup entity)
{
dbEntity.Name = entity.Name;
dbEntity.Email = entity.Email;
dbEntity.Password = entity.Password;
_userSignupDbContext.SaveChanges();
}
}
}
This is my startup:
namespace SignupUsers
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<UserSignupDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("UserSignup")));
services.AddScoped<IDataRepository<UserSignup>, DataManager>();
//services.AddCors();
services.AddControllers();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}
When I post data to that (with Postman), I get back an error 415 (unsupported media type).
What can I do?
Thank you in advance for your help.
in postman, set the "content-type" as JSON (application/json)
Related
I have REST API in .net5.0.
My controller inherits ControllerBaseand has the ApiController attribute.
My simple model and I need to bind value from Claim to a single property.
My model looks like this:
public class CreatePlugin
{
[Required]
[JsonProperty("name"), JsonPropertyName("name")]
public string Name { get; set; }
[Required]
[JsonProperty("description"), JsonPropertyName("description")]
public string Description { get; set; }
[Required]
[JsonProperty("version"), JsonPropertyName("version")]
public string Version { get; set; }
[Required]
[JsonProperty("public"), JsonPropertyName("public")]
public bool Public { get; set; }
[BindProperty(BinderType = typeof(CreatedByModelBinder))]
[ModelBinder(BinderType = typeof(CreatedByModelBinder))]
[Newtonsoft.Json.JsonConverter(typeof(CreatedByConverter))]
[SwaggerIgnore]
[JsonProperty("created_by"), JsonPropertyName("created_by")]
public int CreatedBy { get; set; }
}
and a simple controller method:
[HttpPost]
[Produces("application/json")]
[ProducesResponseType(typeof(Plugin), (int)HttpStatusCode.OK)]
public async Task<IActionResult> Add(CreatePlugin plugin)
{
//some logic
return Ok();
}
My request looks like this:
{
"name": "test",
"description": "sample description",
"version": "1.0",
"public": true
}
Because of ApiController attribute, my model binder isn't working. The same happens when I remove ApiController attribute from my controller and add FromBody to Add method parameter (ref: Custom model binder not firing for a single property in ASP.NET Core 2).
When I remove both attributes, the binder works fine, but the rest of the dto is empty.
I've tried using a custom JsonConverter, but it only works if I pass something in the created_by field.
How can I bind a single property of the model to something that isn't coming from the request data (in my case to claims)?
My workaround for now is to manually assign property value in each method like so:
plugin.CreatedBy = _identityContext.UserId;
Most of my models have CreatedBy and EditedBy properties, and I'd like to automatically fill them with a specific value (in this case with a value from a Claim)
You can custom the model binding like below:
public class CreatedByModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext));
var claim = int.Parse(bindingContext.HttpContext.User.FindFirst("KeyName").Value);
bindingContext.Result = ModelBindingResult.Success(claim);
return Task.CompletedTask;
}
}
Model:
public class CreatePlugin
{
[Required]
[JsonProperty("name"), JsonPropertyName("name")]
public string Name { get; set; }
//...
[NSwag.Annotations.SwaggerIgnore]
[ModelBinder(BinderType = typeof(CreatedByModelBinder))]
[JsonProperty("created_by"), JsonPropertyName("created_by")]
public int CreatedBy { get; set; }
}
Controller:
Not sure how do you add the claims, I just add the claim in cookie authentication.
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
[HttpGet]
public async Task<IActionResult> GetAsync()
{
var claims = new List<Claim>
{
new Claim("KeyName","4")
};
var authProperties = new AuthenticationProperties
{
IssuedUtc = DateTimeOffset.UtcNow,
ExpiresUtc = DateTimeOffset.UtcNow.AddHours(1),
IsPersistent = false
};
const string authenticationType = "Cookies";
var claimsIdentity = new ClaimsIdentity(claims, authenticationType);
await HttpContext.SignInAsync(authenticationType, new ClaimsPrincipal(claimsIdentity), authProperties);
return Ok();
}
[HttpPost]
[Produces("application/json")]
public async Task<IActionResult> Add([FromForm]CreatePlugin plugin) //add FromForm....
{
//some logic
return Ok();
}
}
Result:
UPDATE:
If you must using FromBody, you can custom JsonConverter for the model:
public class CreatedByConverter : Newtonsoft.Json.JsonConverter
{
private readonly IHttpContextAccessor httpContextAccessor;
public CreatedByConverter(IHttpContextAccessor httpContextAccessor)
{
this.httpContextAccessor = httpContextAccessor;
}
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(CreatePlugin));
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer)
{
var claim = int.Parse(httpContextAccessor.HttpContext.User.FindFirst("KeyName").Value);
JObject obj = JObject.Load(reader);
CreatePlugin root = new CreatePlugin();
root.Name = (string)obj["name"];
root.Description = (string)obj["description"];
root.Version = (string)obj["version"];
root.Public = (bool)obj["public"];
root.CreatedBy = claim;
return root;
}
public override void WriteJson(JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer)
{
JToken t = JToken.FromObject(value);
if (t.Type != JTokenType.Object)
{
t.WriteTo(writer);
}
else
{
JObject o = (JObject)t;
o.WriteTo(writer);
}
}
}
Model:
Note: If you get the claim from HttpContext, you need inject the service and cannot use JsonConverter attribute any more. You need register it in Startup.cs
public class CreatePlugin
{
[Required]
[JsonProperty("name"), JsonPropertyName("name")]
public string Name { get; set; }
//more property...
[NSwag.Annotations.SwaggerIgnore]
//[Newtonsoft.Json.JsonConverter(typeof(CreatedByConverter))]
[JsonProperty("created_by"), JsonPropertyName("created_by")]
public int CreatedBy { get; set; }
}
Controller:
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
[HttpGet]
public async Task<IActionResult> GetAsync()
{
//add claims like the first answer..
return Ok();
}
[HttpPost]
[Produces("application/json")]
public async Task<IActionResult> Add(CreatePlugin plugin)
{
//some logic
return Ok(plugin);
}
}
Startup.cs:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
var httpContextAccessor = new HttpContextAccessor();
services.AddControllers()
.AddNewtonsoftJson(options =>
{
options.SerializerSettings.Converters.Add(new CreatedByConverter(httpContextAccessor));
});
services.AddSingleton<IHttpContextAccessor>(httpContextAccessor);
services.AddSwaggerDocument();
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(o => o.LoginPath = new PathString("/account/login"));
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseHttpsRedirection();
app.UseOpenApi();
app.UseSwaggerUi3();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
In my ASP.NER Core Web API Project, I have this code:
public interface IUriPaginationService
{
public Uri GetPaginationUri(int page, string actionUrl);
}
public class UriPaginationService : IUriPaginationService
{
private readonly string _baseUri;
public UriPaginationService(string baseUri)
{
_baseUri = baseUri;
}
public Uri GetPaginationUri(int page, string actionUrl)
{
string baseUrl = $"{_baseUri}{actionUrl}";
return new Uri(baseUrl);
}
}
public abstract class AuditableBaseEntity
{
public AuditableBaseEntity()
{
CreatedDate = DateTime.Now;
}
[Key]
public virtual long Id { get; set; }
[JsonIgnore]
public bool IsDeleted { get; set; }
[DataType(DataType.DateTime)]
public DateTime? CreatedDate { get; set; }
public long? LastUpdatedBy { get; set; }
}
Repositories:
public interface IBaseRepository<T> where T : AuditableBaseEntity
{
Task<IEnumerable<T>> GetAll();
Task<T> GetById(long id);
bool EntityExists(long id);
}
public class BaseRepository<T> : IBaseRepository<T> where T : AuditableBaseEntity
{
private readonly DDMDbContext _context;
private DbSet<T> _entities;
public BaseRepository(DDMDbContext context)
{
_context = context;
_entities = context.Set<T>();
}
public async Task<IEnumerable<T>> GetAll()
{
var list = await _entities.Where(x => x.IsDeleted == false).ToListAsync();
return list;
}
public async Task<T> GetById(long id)
{
return await _entities.FindAsync(id);
}
public bool EntityExists(long id)
{
return _entities.Any(x => x.Id == id && x.IsDeleted == false);
}
}
UnitOfWork:
public interface IUnitOfWork : IDisposable
{
IBaseRepository<Merchant> MerchantRepository { get; }
}
public class UnitOfWork : IUnitOfWork
{
private readonly DDMDbContext _context;
public UnitOfWork(DDMDbContext context)
{
_context = context;
}
private readonly IBaseRepository<Merchant> _merchantRepository;
public IBaseRepository<Merchant> MerchantRepository => _merchantRepository ?? new BaseRepository<Merchant>(_context);
public void Dispose()
{
if (_context != null)
{
_context.Dispose();
}
}
}
Services:
public interface IMerchantService
{
public Task<IEnumerable<Merchant>> GetMerchants();
public bool EntityExists(long id);
public Task<ResponsePagination<GenericPagination<MerchantGetDto>>> GetAll(int page, int sizeByPage);
}
public class MerchantService : IMerchantService
{
private readonly IUnitOfWork _unitOfWork;
private readonly IUriPaginationService _uriPaginationService;
private readonly DDMDbContext _context;
public MerchantService(IUnitOfWork unitOfWork, IUriPaginationService uriPaginationService, DDMDbContext context)
{
_unitOfWork = unitOfWork;
_uriPaginationService = uriPaginationService;
_context = context;
}
public async Task<IEnumerable<Merchant>> GetMerchants()
{
return await _unitOfWork.MerchantRepository.GetAll();
}
public async Task<ResponsePagination<GenericPagination<MerchantGetDto>>> GetAll(int page, int sizeByPage)
{
string nextRoute = null, previousRoute = null;
IEnumerable<Merchant> data = await _unitOfWork.MerchantRepository.GetAll();
var mapper = new EntityMapper();
var merchantsDto = data.Select(m => mapper.FromMerchantToMerchantGetDto(m)).ToList();
GenericPagination<MerchantGetDto> objGenericPagination = GenericPagination<MerchantGetDto>.Create(merchantsDto, page, sizeByPage);
ResponsePagination<GenericPagination<MerchantGetDto>> response = new ResponsePagination<GenericPagination<MerchantGetDto>>(objGenericPagination);
response.CurrentPage = objGenericPagination.CurrentPage;
response.HasNextPage = objGenericPagination.HasNextPage;
response.HasPreviousPage = objGenericPagination.HasPreviousPage;
response.PageSize = objGenericPagination.PageSize;
response.TotalPages = objGenericPagination.TotalPages;
response.TotalRecords = objGenericPagination.TotalRecords;
response.Data = objGenericPagination;
if (response.HasNextPage)
{
nextRoute = $"/merchants?page={(page + 1)}";
response.NextPageUrl = _uriPaginationService.GetPaginationUri(page, nextRoute).ToString();
}
else
{
response.NextPageUrl = null;
}
if (response.HasPreviousPage)
{
previousRoute = $"/merchants?page={(page - 1)}";
response.PreviousPageUrl = _uriPaginationService.GetPaginationUri(page, previousRoute).ToString();
}
else
{
response.PreviousPageUrl = null;
}
return response;
}
public bool EntityExists(long id)
{
return _unitOfWork.MerchantRepository.EntityExists(id);
}
public async Task<Merchant> GetById(long id)
{
return await _unitOfWork.MerchantRepository.GetById(id);
}
}
Controller:
[Produces("application/json")]
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/[controller]")]
public class AdminController : ControllerBase
{
private readonly IMerchantService _merchantService;
public AdminController(MerchantService merchantService)
{
_merchantService = merchantService;
}
[HttpGet("merchants")]
public async Task<ResponsePagination<GenericPagination<MerchantGetDto>>> GetAll(int page, int sizeByPage)
{
return await _merchantService.GetAll(page, sizeByPage);
}
}
startup:
public void ConfigureServices(IServiceCollection services)
{
// services.AddMvc();
services.AddControllers();
services.AddDb(Configuration);
services.AddJwtAuthentication(Configuration);
services.AddMvcCoreFramework(Configuration);
services.AddAppAuthorization(Configuration);
services.AddControllersWithViews();
services.AddRazorPages();
services.AddVersioning();
services.AddSwagger();
services.AddRouting(options => options.LowercaseUrls = true);
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddSingleton<IUriPaginationService>(provider =>
{
var accesor = provider.GetRequiredService<IHttpContextAccessor>();
var request = accesor.HttpContext.Request;
var absoluteUri = string.Concat(request.Scheme, "://", request.Host.ToUriComponent());
return new UriPaginationService(absoluteUri);
});
services.AddScoped(typeof(IBaseRepository<>), typeof(BaseRepository<>));
services.AddScoped<IUnitOfWork, UnitOfWork>();
services.AddTransient<IMerchantService, MerchantService>();
}
When I use this on postman get request:
https://localhost:44341/api/v1/merchant/mandates
I got this error:
System.InvalidOperationException: Unable to resolve service for type 'API.Infrastructure.Services.Concrete.MerchantService' while attempting to activate 'API.Web.Controllers.v1.MerchantController'.
at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.GetService(IServiceProvider sp, Type type, Type requiredBy, Boolean isDefaultParameterRequired)
How do I resolve this:
Thanks
The controller is dependent on the implementation (class)
public AdminController(MerchantService merchantService)
{
_merchantService = merchantService;
}
while you registered the abstraction (interface).
services.AddTransient<IMerchantService, MerchantService>();
Based on the registration it is most likely the controller was meant to be dependent on the interface (abstraction)
public AdminController(IMerchantService merchantService) //<-- CHANGED TO INTERFACE
{
_merchantService = merchantService;
}
Updating the controller constructor to be dependent on the registered abstraction will fix the shown exception.
in addition to Nkosi answer, the three injection service are listed below and their scope. three methods define the lifetime of the services
AddTransient
Transient lifetime services are created each time they are requested. This lifetime works best for lightweight, stateless services.
AddScoped
Scoped lifetime services are created once per request.
AddSingleton
Singleton lifetime services are created the first time they are requested (or when ConfigureServices is run if you specify an instance there) and then every subsequent request will use the same instance.
Brief explation can be found here
I'm struggling to find a way to catch an exception thrown by a model property (actually by its type struct), which must be bound to a POST request body data.
I have a general scenario where I need to treat very specific data types, so I'm using structs to validate them accordingly each case.
Despite of the following codes are just drafts, all suggestions are very welcome!
So the following is an example of a Controller:
[ApiController]
[TypeFilter(typeof(CustomExceptionFilter))]
public class OrdersController : ControllerBase
{
public OrdersController(ILogger<OrdersController> logger, IDataAccess dataAccess)
{
_dataAccess = dataAccess;
_logger = logger;
}
private readonly IDataAccess _dataAccess;
private readonly ILogger<OrdersController> _logger;
[EnableCors]
[Route("api/[controller]/Sales")]
[HttpPost]
public async Task<ActionResult> PostSaleAsync(
[FromBody] SaleOrder saleOrder)
{
try
{
Guid saleOrderId = Guid.NewGuid();
saleOrder.SaleOrderId = saleOrderId;
foreach (SaleOrderItem item in saleOrder.items)
item.SaleOrderId = saleOrderId;
OrderQuery query = new OrderQuery(_dataAccess);
await query.SaveAsync(saleOrder);
_dataAccess.Commit();
var response = new
{
Error = false,
Message = "OK",
Data = new
{
SaleOrderId = saleOrderId
}
};
return Ok(response);
}
catch (DataAccessException)
{
_dataAccess.Rollback();
//[...]
}
//[...]
}
}
and an example of a model, Order, and a struct, StockItemSerialNumber:
public class SaleOrder : Order
{
public Guid SaleOrderId { get => OrderId; set => OrderId = value; }
public Guid CustomerId { get => StakeholderId; set => StakeholderId = value; }
public Guid? SellerId { get; set; }
public SaleModelType SaleModelType { get; set; }
public SaleOrderItem[] items { get; set; }
}
public class SaleOrderItem : OrderItem
{
public Guid SaleOrderId { get; set; }
public StockItemSerialNumber StockItemSerialNumber { get; set; }
//[JsonConverter(typeof(StockItemSerialNumberJsonConverter))]
//public StockItemSerialNumber? StockItemSerialNumber { get; set; }
}
public struct StockItemSerialNumber
{
public StockItemSerialNumber(string value)
{
try
{
if ((value.Length != 68) || Regex.IsMatch(value, #"[^\w]"))
throw new ArgumentOutOfRangeException("StockItemSerialNumber");
_value = value;
}
catch(RegexMatchTimeoutException)
{
throw new ArgumentOutOfRangeException("StockItemSerialNumber");
}
}
private string _value;
public static implicit operator string(StockItemSerialNumber value) => value._value;
public override string ToString() => _value;
}
I would like to catch ArgumentOutOfRangeException thrown by StockItemSerialNumber struct and then return a response message informing a custom error accordingly.
Since this exception is not catch by the try...catch block from Controller, I've tried to build a class that extends IExceptionFilter and add as a filter:
public class CustomExceptionFilter : IExceptionFilter
{
private readonly IWebHostEnvironment _hostingEnvironment;
private readonly IModelMetadataProvider _modelMetadataProvider;
public CustomExceptionFilter(
IWebHostEnvironment hostingEnvironment,
IModelMetadataProvider modelMetadataProvider)
{
_hostingEnvironment = hostingEnvironment;
_modelMetadataProvider = modelMetadataProvider;
}
public void OnException(ExceptionContext context)
{
context.Result = new BadRequestObjectResult(new {
Error = false,
Message = $"OPS! Something bad happened, Harry :( [{context.Exception}]."
});
}
}
Startup.cs :
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(options =>
// [...]
});
services.AddTransient<IDataAccess>(_ => new DataAccess(Config.DBCredentials));
services.AddControllers(options => options.Filters.Add(typeof(CustomExceptionFilter)));
}
But that approach also doesn't work, I mean, the ArgumentOutOfRangeException remains not being catch and crashes the execution during a POST request.
Finally, here's an example of a JSON with request data:
{
"CustomerId":"fb2b0555-6d32-404b-b2f0-50032a7e0f59",
"SellerId":null,
"items": [
{
"StockItemSerialNumber":"22B6E75510AB459B8DB2874F20C722B6F3DC19C6E474337D5F73BB87699E9A1001"
}, // Invalid
{
"StockItemSerialNumber":"022B6E755122B659B8DB2874F20C780030F3DC19C6E47465AS1673BB87699E9A1001"
} // Valid
]
}
So I appreciate any help or suggestion! Thanks!
*Update:
Was able to follow this tutorial, and now the error I'm getting states:
"message": "No HTTP resource was found that matches the request URI 'http://localhost:0000/api/v1/Pets('dog')/Color'."
"message": "No routing convention was found to select an action for the OData path with template '~/entityset/key/unresolved'."
Any ideas?*
I am getting an error when trying to retrieve the Color in my Pet query using OData V4.
I'm having quite a bit of trouble, ideally I would use an expand on color (e.g. localhost:0000/api/v1/Pets('dog')?$expand=Colors)
The JSON I need returned is something like:
[
{
"_Key": "1",
"animalname": "dog",
"furcolorname": "black,white",
"color": {
"_Key": "1",
"colorname": "black"
},
{
"_Key": "2",
"colorname": "white"
}
}
]
Maybe I'm on the completely wrong path, either way any input is appreciated!
If I query localhost:0000/api/v1/Pets('dog') :
…
"Message\": \"Invalid column name 'Pet__Key'.\"
…
If I query localhost:0000/api/v1/Pets('dog')?$exand=Colors :
…
"The query specified in the URI is not valid. Could not find a property named 'Colors' on type 'PetShop.Odata.Models.Pet'."
…
Pet.cs model:
namespace PetShop.Odata.Models
{
[Table("pet")]
public class Pet
{
[Key]
public string _Key { get; set; }
public string AnimalName { get; set; }
[ForeignKey("Color")]
public string FurColorName { get; set; }
public virtual Color Color { get; set; }
public virtual ICollection<Color> Colors { get; set; }
}
}
Color.cs model:
namespace PetShop.Odata.Models
{
[Table("color")]
public class Color
{
public Color()
{
new HashSet<Pet>();
}
[Key]
public string _Key { get; set; }
public string ColorName { get; set; }
}
}
PetsController.cs:
namespace PetShop.Odata.Controllers
{
[ApiVersion("1.0")]
[ODataRoutePrefix("Pets")]
[ApiControllerMetricReport]
public class PetsController : ODataController
{
private readonly MyContext context = new MyContext ();
[HttpGet]
[ODataRoute]
[EnableQuery]
[ResponseType(typeof(List<Pet>))]
public IHttpActionResult Get()
{
return Ok(context.Pets.AsQueryable());
}
[EnableQuery]
public IQueryable<Color> Get ([FromODataUri] string key)
{
return context.Pets.Where(m => m._Key == key).SelectMany(a => a.Colors);
}
protected override void Dispose(bool disposing)
{
context.Dispose();
base.Dispose(disposing);
}
}
}
ColorsController.cs:
namespace PetShop.Odata.Controllers
{
[ApiVersion("1.0")]
[ODataRoutePrefix("Colors")]
[ApiControllerMetricReport]
public class ColorsController : ODataController
{
private readonly MyContext context = new MyContext ();
[HttpGet]
[ODataRoute]
[EnableQuery]
[ResponseType(typeof(List<Color>))]
public IHttpActionResult Get()
{
return Ok(context.Colors.AsQueryable());
}
protected override void Dispose(bool disposing)
{
context.Dispose();
base.Dispose(disposing);
}
}
}
PetConfig.cs:
namespace PetShop.Odata.Configuration
{ public class PetModelConfiguration : IModelConfiguration
{
public void Apply(ODataModelBuilder builder, ApiVersion apiVersion)
{
builder.EntitySet<Pet>("Pets");
}
}
}
ColorConfig.cs:
namespace PetShop.Odata.Configuration
{ public class ColorModelConfiguration : IModelConfiguration
{
public void Apply(ODataModelBuilder builder, ApiVersion apiVersion)
{
builder.EntitySet<Color>("Colors");
}
}
}
MyContext.cs:
…
public DbSet<Pet> Pets { get; set; }
public DbSet<Color> Colors { get; set; }
…
Setup.cs:
…
public static HttpServer CreateHttpServer()
{
var httpConfig = new HttpConfiguration();
var webApiServer = new HttpServer(httpConfig);
httpConfig.AddApiVersioning(options => options.ReportApiVersions = true);
httpConfig.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
var modelBuilder = new VersionedODataModelBuilder(httpConfig)
{
ModelBuilderFactory = () => new ODataConventionModelBuilder().EnableLowerCamelCase(),
ModelConfigurations =
{
new PetConfig(),
new ColorConfig()
},
};
var models = modelBuilder.GetEdmModels();
httpConfig.Count().Filter().OrderBy().Expand().Select().MaxTop(null);
…
Assuming you are using default routing convention, your Get colors method in PetsController.cs does not match the expected OData route format.
Instead of the following:
[EnableQuery]
public IQueryable<Color> Get ([FromODataUri] string key)
{
return context.Pets.Where(m => m._Key == key).SelectMany(a => a.Colors);
}
You should try:
[EnableQuery]
public IQueryable<Color> GetColors ([FromODataUri] string key)
{
return context.Pets.Where(m => m._Key == key).SelectMany(a => a.Colors);
}
This definition would make the OData route: http://localhost:0000/api/v1/Pets('dog')/Colors
For more information on routing conventions see https://learn.microsoft.com/en-us/odata/webapi/built-in-routing-conventions.
A couple alternative approaches exist as well:
Register a different name to a custom nav property
Define a custom OData Function
I am using ASP CORE 2.1 and EF CORE to create Web API with 2 data table with 1 ForeignKey
Model:
public partial class AuditInfo
{
public int Id { get; set; }
public string Level { get; set; }
public string Period { get; set; }
public string Auditor { get; set; }
public virtual ICollection<Item> Items { get; set; }
}
public partial class Item
{
public int Id { get; set; }
public string Ponumber { get; set; }
public bool IsComplete { get; set; }
public AuditInfo AuditInfo { get; set; }
}
public partial class VEDHOMEContext : DbContext
{
public VEDHOMEContext()
{
}
public VEDHOMEContext(DbContextOptions<VEDHOMEContext> options)
: base(options)
{
}
public virtual DbSet<AuditInfo> AuditInfo { get; set; }
public virtual DbSet<Item> Item { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.HasAnnotation("ProductVersion", "2.1.1-rtm-30846")
.HasAnnotation("Relational:MaxIdentifierLength", 128)
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
modelBuilder.Entity("auditAPI.Models.AuditInfo", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("Auditor");
b.Property<string>("Level");
b.Property<string>("Period");
b.HasKey("Id");
b.ToTable("AuditInfo");
});
modelBuilder.Entity("auditAPI.Models.Item", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<int?>("AuditInfoId");
b.Property<bool>("IsComplete");
b.Property<string>("Ponumber");
b.HasKey("Id");
b.HasIndex("AuditInfoId");
b.ToTable("Item");
});
modelBuilder.Entity("auditAPI.Models.Item", b =>
{
b.HasOne("auditAPI.Models.AuditInfo", "AuditInfo")
.WithMany("Items")
.HasForeignKey("AuditInfoId");
});
}
}
Controller:
[Route("api/[controller]")]
[ApiController]
public class AuditInfoesController : ControllerBase
{
private readonly VEDHOMEContext _context;
public AuditInfoesController(VEDHOMEContext context)
{
_context = context;
}
// GET: api/AuditInfoes
[HttpGet]
public IEnumerable<AuditInfo> GetAuditInfo()
{
return _context.AuditInfo;
}
// GET: api/AuditInfoes/5
[HttpGet("{id}")]
public async Task<IActionResult> GetAuditInfo([FromRoute] int id)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var auditInfo = await _context.AuditInfo.FindAsync(id);
if (auditInfo == null)
{
return NotFound();
}
return Ok(auditInfo);
}
// PUT: api/AuditInfoes/5
[HttpPut("{id}")]
public async Task<IActionResult> PutAuditInfo([FromRoute] int id, [FromBody] AuditInfo auditInfo)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
if (id != auditInfo.Id)
{
return BadRequest();
}
_context.Entry(auditInfo).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!AuditInfoExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return NoContent();
}
// POST: api/AuditInfoes
[HttpPost]
public async Task<IActionResult> PostAuditInfo([FromBody] AuditInfo auditInfo)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
_context.AuditInfo.Add(auditInfo);
await _context.SaveChangesAsync();
return CreatedAtAction("GetAuditInfo", new { id = auditInfo.Id }, auditInfo);
}
// DELETE: api/AuditInfoes/5
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteAuditInfo([FromRoute] int id)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var auditInfo = await _context.AuditInfo.FindAsync(id);
if (auditInfo == null)
{
return NotFound();
}
_context.AuditInfo.Remove(auditInfo);
await _context.SaveChangesAsync();
return Ok(auditInfo);
}
private bool AuditInfoExists(int id)
{
return _context.AuditInfo.Any(e => e.Id == id);
}
}
However i got return null for the item related data, how can i fix it?
i am new to this framework, any help would appreciate, thanks.
[{"id":1,"level":"level1","period":"jan","auditor":"A","items":null},{"id":2,"level":"level2","period":"feb","auditor":"B","items":null}]
expected output:
[{"id":1,"level":"level1","period":"jan","auditor":"A","items":{"Id":1,"Ponumber":"0001","IsComplete":"True","AuditInfoId":1},{"id":2,"Ponumber":"0002","IsComplete":"True","AuditInfoId":1}}]
To who have similar problem, I solve it by adding
services.AddMvc()
.AddJsonOptions(options => {
options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
});
and edit controller
public async Task<List<AuditInfo>> GetAuditInfo()
{
//return _context.AuditInfo;
var infoes = await _context.AuditInfo.Include(a => a.Items).ToListAsync();
return infoes;
}
I'm not sure if you've seen the accepted answer to this question, but the problem is to do with how the JSON Serializer deals with circular references. Full details and links to more references can be found at the above link, and I'd suggest digging into those, but in short, adding the following to startup.cs will configure the serializer to ignore circular references:
services.AddMvc()
.AddJsonOptions(options => {
options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
});