Odata $expand won't work on specific queries - c#

We're making an ASP.NET Core API for a web app that should get a list of users (and expand a specific field) from a SQL Server database with Entity Framework. It works until we specify which user Id we want in the URL.
Here is the Startup class:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
private static IEdmModel GetEdmModel()
{
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<User>("Users");
builder.EntitySet<Character>("Characters");
return builder.GetEdmModel();
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<GaiumContext>(opts => opts.UseSqlServer(Configuration["ConnectionString:Gaium"]));
services.AddOData();
services.AddMvc(options =>
{
options.EnableEndpointRouting = false;
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseMvc(b =>
{
b.EnableDependencyInjection();
b.Select().Expand().Filter().OrderBy().MaxTop(100).Count();
b.MapODataServiceRoute("api", "api", GetEdmModel());
});
}
}
Here is the DbContext:
public class GaiumContext : DbContext
{
public GaiumContext(DbContextOptions<GaiumContext> options) : base(options)
{
}
public DbSet<User> Users { get; set; }
public DbSet<Character> Characters { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>().HasMany(c => c.Characters);
}
}
Finally, the controller UsersController :
public class UsersController : ODataController
{
private GaiumContext _context;
public UsersController(GaiumContext context)
{
_context = context;
}
[EnableQuery]
public IActionResult Get()
{
return Ok(_context.Users);
}
[EnableQuery]
public IActionResult Get(long key)
{
return Ok(_context.Users.Find(key));
}
}
The user object looks like this:
Users {
id: int,
name: string,
Characters: [{
id: int,
name: String
}]
}
Here's a query for all the users:
GET: https://localhost:44323/api/users?$expand=Characters
In this case the query works fine and we do receive the list of users, as well as their Characters field.
{"#odata.context":"https://localhost:44323/api/$metadata#Users","value":[{"Id":1,"Username":"Ok","Characters":[{"Id":1,"Name":"bigusernamesmoke"}]}]}
But when we try to get the result for one specific user, using their ID, the Characters list is empty:
GET: https://localhost:44323/api/users/1?$expand=Characters
{"#odata.context":"https://localhost:44323/api/$metadata#Users/$entity","Id":1,"Username":"Ok","Characters":[]}

Note the Find(key) method returns a single Book instead of a queryable object :
return Ok(_context.Users.Find(key));
If I change your code to return a SingleResult as below:
[EnableQuery]
public SingleResult<User> Get(long key)
{
var query = _context.Users.Where(p => p.Id == key);
return SingleResult.Create(query);
}
it works fine for me.

Related

EF Core 6 derived DbContext doesn't get connection string from Startup

I have a windows service that listens for status messages from server using socket. This works for sometime when the service is started. After that, it stops receiving new messages.
I have a very basic Web API to track time for the several hourly workers at my company. In my Startup.cs I have an AddDbContext method
services.AddDbContext<TimeCardsContext>(options =>
options.UseSqlServer(connectionString));
I get the configuration string through
Configuration.GetConnectionString("TimeConnection").
From the documentation, and hours googling, it seems that my derived DbContext simply has to have constructor that takes a DbContextOptions, like so:
public TimeCardsContext (DbContextOptions options) : base (options)
{
}
As I understand it, that should be enough to inject the DbContext created in Startup. This, however, simply doesn't work. When I debug, I get a null reference to the connection string.
I must really not understand how this is supposed to work.
Some google searches suggest I use the OnConfiguring method in my DbContext class, but in every example they hard code the connection string in the method. That just ain't right!
Suggestions appreciated!
EDITED to add:
Here is the entire relevant part of my Startup.cs
namespace TimeCardsNg2
{
public class Startup
{
public IConfiguration Configuration { get; set; }
public static string connectionString { get; set; }
public Startup (IConfiguration configuration)
{
Configuration = configuration;
}
public Startup () {}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
connectionString = Configuration.GetConnectionString ("TimeConnect");
services.AddDbContext<TimeCardsContext>(options =>
options.UseSqlServer(connectionString)
.LogTo(Console.WriteLine, LogLevel.Information));
services.AddControllers().AddNewtonsoftJson();
services.AddMvc();
services.AddScoped<DbContext, TimeCardsContext>();
services.AddScoped<IWorkerService, WorkerService>();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1"
, new OpenApiInfo
{
Title = "TimeCardsNg2 API",
Version = "v1"
,
Description = "Testing Swagger"
});
});
}
// 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();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseStatusCodePages();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(name: default, "{controller=Home}/{action=Index}/{id?}");
});
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json"
, "Zomato API V1");
// To serve SwaggerUI at application's root page, set the RoutePrefix property to an empty string.
c.RoutePrefix = string.Empty;
});
}
}
}
And, here's the relevant portion of my context class:
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
#nullable disable
namespace TimeCardsNg2.Models
{
public partial class TimeCardsContext : DbContext
{
private IConfiguration configuration { get; set; }
protected string connectionString { get; set; }
public TimeCardsContext()
{
}
public TimeCardsContext(DbContextOptions<TimeCardsContext> options) : base(options)
{ }
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
}
public virtual DbSet<TimeCard> TimeCards { get; set; }
public virtual DbSet<Worker> Workers { get; set; }
I hope this will make my situation a little clearer.
Forgot to add a nice picture:
Please notice the connectionString is null.

Aspnetcore healthchecks degraded status ignored

I was learing about HealthChecks following the information posted on the MSDN site: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/health-checks?view=aspnetcore-5.0#register-health-check-services
The following code snippet tells that if an unhealthy checks returns I can overwrite the value by saying it's degraded instead.
services.AddHealthChecks()
.AddCheck<ExampleHealthCheck>(
"example_health_check",
failureStatus: HealthStatus.Degraded,
tags: new[] { "example" });
So I tried this with this implementation, assuming that I would get a degraded result instead of an unhealthy one:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddHealthChecks().AddCheck<ExampleHealthCheck>("ExampleHealthCheck", failureStatus: HealthStatus.Degraded);
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapHealthChecks("/health");
});
}
}
internal class ExampleHealthCheck : IHealthCheck
{
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken))
{
return Task.FromResult(HealthCheckResult.Unhealthy("An unhealthy result."));
}
}
Can someone shed some light on why this isn't working or where I got it wrong?
It is quite confusing, because you expect that the HealthCheck gives you back automatically the result you defined on the failureStatus property, but your HealthCheck explicitly returns Unhealthy.
If you take a look at the AddHealthCheck<T> method on GitHub, you will see in only creates an instance of HealthCheckRegistration.
public static IHealthChecksBuilder AddCheck(
this IHealthChecksBuilder builder,
string name,
IHealthCheck instance,
HealthStatus? failureStatus = null,
IEnumerable<string>? tags = null,
TimeSpan? timeout = null)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
if (instance == null)
{
throw new ArgumentNullException(nameof(instance));
}
return builder.Add(new HealthCheckRegistration(name, instance, failureStatus, tags, timeout));
}
This object is afterwards passed inside the context of your health check. You can then read the failureStatus which is expected in case of failure and then return an HealthCheckResult from it. In your case, this would return Degraded :
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddHealthChecks().AddCheck<ExampleHealthCheck>("ExampleHealthCheck", failureStatus: HealthStatus.Degraded);
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapHealthChecks("/health");
});
}
}
internal class ExampleHealthCheck : IHealthCheck
{
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken))
{
return Task.FromResult(new HealthCheckResult(context.Registration.FailureStatus));
}
}

ASP.NET CORE Custom Action Filter Attribute Calling on Controller where it is not applied (IIS)

I am using an Custom Action Filter attribute to check if session is null or not..
Authenticate.cs
public class Authenticate : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
var data = context.HttpContext.Session.GetString("UserSession");
if (data == null)
{
bool isAjax = context.HttpContext.Request.Headers["X-Requested-With"] == "XMLHttpRequest";
if (isAjax)
{
context.Result = new JsonResult("Session Expired!");
}
else
{
context.Result = new RedirectResult("/Account/Login");
}
}
}
public override void OnActionExecuted(ActionExecutedContext context)
{
// our code after action executes
}
}
I am calling this attribute on all controllers except Account Controller
AccountController.cs
public class AccountController : Controller
{
UserFunctions userFunctions;
public AccountController(ARSContext context)
{
userFunctions = new UserFunctions(context);
}
public IActionResult Index()
{
return View();
}
public IActionResult Login()
{
return View();
}
[HttpPost]
public IActionResult Login(AccountViewModel viewModel)
{
User user = userFunctions.Login(viewModel.Username, viewModel.Password);
if (user != null)
{
HttpContext.Session.SetString("UserSession", JsonConvert.SerializeObject(user));
return Json(true);
}
return Json(false);
}
}
My Startup.cs
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.AddControllersWithViews().AddRazorRuntimeCompilation();
services.AddDbContext<MyDBContext>(item => item.UseSqlServer(Configuration.GetConnectionString("MyDBEntities")));
services.AddScoped<CountryFunctions>();
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromHours(1);
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseSession();
app.UseDeveloperExceptionPage();
if (env.IsDevelopment())
{
}
else
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
Now the main problem is that all of this is working perfectly on my local computer when I run it through Visual Studio. But when I deploy it on IIS what happens is that when I call '/Account/Login' without 'HttpPOST' it shows you view.
But when I submit the form and call '/Account/Login' 'HttpPost' action then My Authenticate filter is called which redirects me back to '/Account/Login' View Action. I also tried to use ServiceFilters but same problem persists.

ASP.NET Core WebApi with SQL Server and EFCore 2

I'm trying to create my first .NET Core Web Api with SQL Server and Entity Framework Core 2, but when I call the endpoint from my browser while project build, I get nothing. It always stays at the startup page that says "Hello World".
How to get data from the Rest Api correctly?
First, my model class:
public class Project
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
}
With a DbContext:
public class TaskManagerDbContext : DbContext
{
public TaskManagerDbContext(DbContextOptions<TaskManagerDbContext> options) : base(options) { }
public DbSet<Project> Projects { get; set; }
}
Then I hardcoded two records into database:
I generated ProjectsController by scaffolding an API with R/W based on Entity Framework. The get function look like this (I think there is no sense to show all controller because its generated by the scaffold):
[Route("api/[controller]")]
[ApiController]
public class ProjectsController : ControllerBase
{
private readonly TaskManagerDbContext _context;
public ProjectsController(TaskManagerDbContext context)
{
_context = context;
}
// GET: api/Projects
[HttpGet]
public IEnumerable<Project> GetProjects()
{
return _context.Projects;
}
// GET: api/Projects/5
[HttpGet("{id}")]
public async Task<IActionResult> GetProject([FromRoute] int id)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var project = await _context.Projects.FindAsync(id);
if (project == null)
{
return NotFound();
}
return Ok(project);
}
}
Finally, my Startup.cs:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddDbContext<TaskManagerDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.Run(async (context) =>
{
await context.Response.WriteAsync("Hello World!");
});
app.UseHttpsRedirection();
app.UseMvc();
}
}
Question is, what change is needed to get data from my database by API endpoints? For now, when I try to call the endpoint, it always stays on the "hello world" page.
My connection string is in appsettings.json file in project directory.
Thanks for any advice!
You've got some remnant left from the template for an empty asp core project.
Just remove this entire block, because you don't need it:
app.Run(async (context) =>
{
await context.Response.WriteAsync("Hello World!");
});

Middleware DI Error

I am attempting to implement the API Key Validator mentioned in this post. I am running into an issue where the injected service I am using to do validation in the middleware class is returning:
InvalidOperationException: Cannot resolve 'FoosballKeepr.Services.Interfaces.ILeagueService' from root provider because it requires scoped service 'FoosballKeepr.Data.FoosballKeeprContext'.
I believe I am registering my dbContext, services, and repositories correctly in Startup.cs.
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)
{
//MVC
services.AddMvc();
//Database
var connection = #"Server=localhost\SQLEXPRESS;Database=FoosballKeepr;Trusted_Connection=True;";
services.AddDbContext<FoosballKeeprContext>(options => options.UseSqlServer(connection));
//Services
services.AddTransient<IPlayerService, PlayerService>();
services.AddTransient<ILeagueService, LeagueService>();
//Repositories
services.AddTransient<IPlayerRepository, PlayerRepository>();
services.AddTransient<ILeagueRepository, LeagueRepository>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseMiddleware<ApiKeyValidatorMiddleware>();
app.UseMvc();
}
}
Custom middleware validator:
public class ApiKeyValidatorMiddleware
{
private readonly RequestDelegate _next;
private ILeagueService _leagueService;
public ApiKeyValidatorMiddleware(RequestDelegate next, ILeagueService leagueService)
{
_next = next;
_leagueService = leagueService;
}
public async Task Invoke(HttpContext context)
{
if (!context.Request.Headers.Keys.Contains("x-api-key"))
{
context.Response.StatusCode = 400;
await context.Response.WriteAsync("API Key Missing.");
return;
}
else
{
int leagueId = _leagueService.ValidateApiKey(context.Request.Headers["x-api-key"]);
if (leagueId == 0)
{
context.Response.StatusCode = 401;
await context.Response.WriteAsync("Invalid API Key");
return;
}
else
{
context.Items["LeagueId"] = leagueId;
}
}
await _next.Invoke(context);
}
}
Service
public class LeagueService : ILeagueService
{
private readonly ILeagueRepository _leagueRepository;
public LeagueService(ILeagueRepository leagueRepository)
{
_leagueRepository = leagueRepository;
}
public int ValidateApiKey(string apiKey)
{
return _leagueRepository.ValidateApiKey(apiKey);
}
}
Repository
public class LeagueRepository : ILeagueRepository
{
private readonly FoosballKeeprContext _context;
public LeagueRepository(FoosballKeeprContext context)
{
_context = context;
}
public int ValidateApiKey(string apiKey)
{
var query = from l in _context.League
where l.ApiKey == apiKey
select l.LeagueId;
return query.FirstOrDefault();
}
}
This is my first time implementing custom middleware functionality so I feel like my issue is not correctly setting something up in the correct context, but nothing is popping up as obvious. Does this look familiar to anyone??
The problem is that middlewares don't have a scope, given that:
Middleware is constructed once per application lifetime
So, when you need to inject scoped services, you do it at the Invoke operation (what's known as method injection):
public async Task Invoke(HttpContext context, ILeagueService service)
{
//...
}

Categories

Resources