OData .NET Core API - Conditional AutoExpand ($expand) - .net-core

I am developing a .NET Core Web API with OData capabilities.
For some entities, I would like to $expand some properties provided the user has the appropriate permissions.
Therefore, I need to handle this $expand part in the API, without the user having to specifically ask for the nested entities he has access to.
Is there a way to apply the AutoExpand attribute conditionally?
Thank you for your help!
EDIT (Possible solution)
I created a new class, inherited from EnableQueryAttribute, then overrode public override IQueryable ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions).
In this method, I forge a new query string based on the permissions:
public override IQueryable ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions)
{
// Keep the original query string
string originalQueryString = queryOptions.Request.QueryString.Value;
// Suppose you can get the permissions as followed
var lPermissions = this.GetPermissions();
// Then create a query string based on it
string forgedQueryString = this.GenerateQueryString(lPermissions);
// Forge a new ODataQueryOptions
queryOptions.Request.QueryString = new QueryString(forgedQueryString);
var oQueryOptions = new ODataQueryOptions(queryOptions.Context, queryOptions.Request);
var queryEntities = oQueryOptions.ApplyTo(queryable, new ODataQuerySettings()
{
PageSize = this.PageSize
});
// Reset the initial URL (to handle "#odata.nextLink")
queryOptions.Request.QueryString = new QueryString(originalQueryString);
return queryEntities;
}

Related

How do I create a ClaimsPrincipal in my Blazor/.NetCore "Session"?

Background: I have an old MVC app that I'm experimenting with migrating to a shiny new Blazor app. Blazor seems to tick a lot of boxes for me here. Wunderbar. For clarity this is the solution template in VS2022 where there's a WASM, a .Net Core host, and a shared project. I will have plenty of api calls that need to be secured as well as UI that will be affected by various authorization policies (eg show/hide admin features).
I have a table of users with an ID and hashed password.
I can't get Blazor to use its native authentication/authorization processes with my existing store.
My latest attempt was to create an AccountController on the server app (inherits ControllerBase) and put in a Login method that gets the username and password from a json body for the moment. I have successfully ported the old authentication mechanism and I have my user that I have verified the password for. I now want to use Claims and a ClaimsPrincipal to store some of the things about the user, nothing too complex.
How do I put my ClaimsPrincipal into the app such that the WASM UI can see it AND future calls to api controllers (or ControllerBase controllers) will see it?
I have found hundreds of examples that use built-in scaffolding that lets it use EF to create tables and things but I need to use my existing stores and I can't find anything that joins the dots on how to connect the WASM and the server side.
I have read about and implemented and around the place, and tried some #authorize around the place but my WASM just doesn't know about the authenticated user.
In my login controller I have attempted a bunch of different approaches:
I implemented a custom AuthenticationStateProvider, got it into the controller via DI, called the AuthenticationStateChanged() and for the lifecycle of that one controller call I can see my HttpContext.User gets the new identity. But the WASM doesn't, and if I hit the same method again the User is null again
I tried to implement a SignInManager. This never worked well and my reading suggests that it's not compatible
I discovered ControllerBase.SignIn() which hasn't helped either
HttpContext.SignInAsync() with Cookie authentication (because that was the example I found)
I tried setting HttpContext.User directly (and tried combining that one call with the AuthenticationStateProvider implementation simultaneously)
I tried creating a fresh solution from template to pick through it, but it would appear to be reliant on hacking up my EF DataContext. I just want to find how I tell the whole contraption "Here's a ClaimsPrincipal" and have that work in both the WASM and api controllers.
I'm also not excited to have a dependency on the Duende stuff - I don't see what it brings to the table. I don't really need a whole identity provider, I already have my own code for authorizing against the database I just need to get my very boring ClaimsPrincipal into my app.
Am I going at this all wrong? Has my many years of "old school" experience stopped me from seeing a modern way of doing this? Am I trying to force cool new stuff to behave like clunky old stuff? Yes I'd love to switch to Google/Facebook/Twitter/MS authorization but that's not an option, I have passwords in a database.
You need to build a custom AuthenticationHandler.
Here's the relevant bits of one of mine (see credits at bottom for where I lifted some of the code). You'll need to pick out the pieces from the code to make your work. Ask if you have any specific problems.
The custom AuthenticationHandler looks up your user in your database and if authenticated, builds a standard ClaimsPrincipal object and adds it to the security header. You can then use the standard Authorization and AuthenticationStateProvider.
public class AppAuthenticationHandler : AuthenticationHandler<AppAuthOptions>
{
private const string AuthorizationHeaderName = "Authorization";
private const string BasicSchemeName = "BlazrAuth";
//this is my custom identity database
private IIdentityService _identityService;
public AppAuthenticationHandler(IOptionsMonitor<AppAuthOptions> options, IIdentityService identityService, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{
_identityService = identityService;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
await Task.Yield();
// Check the Headers and make sure we have a valid set
if (!Request.Headers.ContainsKey(AuthorizationHeaderName))
return AuthenticateResult.Fail("No Authorization Header detected");
if (!AuthenticationHeaderValue.TryParse(Request.Headers[AuthorizationHeaderName], out AuthenticationHeaderValue? headerValue))
return AuthenticateResult.Fail("No Authorization Header detected");
if (!BasicSchemeName.Equals(headerValue.Scheme, StringComparison.OrdinalIgnoreCase))
return AuthenticateResult.Fail("No Authorization Header detected");
if (headerValue is null || headerValue.Parameter is null)
return AuthenticateResult.Fail("No Token detected");
// Get the User Guid from the security token
var headerValueBytes = Convert.FromBase64String(headerValue.Parameter);
var userpasswordstring = Encoding.UTF8.GetString(headerValueBytes);
// This will give you a string like this "me#you.com:password"
if (youcantdecodethestring ))
return AuthenticateResult.Fail("Invalid Token submitted");
// Get the user data from your database
var principal = await this.GetUserAsync(userId);
if (principal is null)
return AuthenticateResult.Fail("User does not Exist");
// Create and return an AuthenticationTicket
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
// method to get the user from the database and retuen a ClaimsPrincipal
public async Task<ClaimsPrincipal?> GetUserAsync(Guid Id)
{
// Get the user object from the database
var result = await _identityService.GetIdentityAsync(Id);
// Construct a ClaimsPrincipal object if the have a valid user
if (result.Success && result.Identity is not null)
return new ClaimsPrincipal(result.Identity);
// No user so return null
return null;
}
}
You can construct a ClaimsIdentity like this:
var identity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Sid, record.Id.ToString()),
new Claim(ClaimTypes.Name, record.Name),
new Claim(ClaimTypes.Role, record.Role)
}, "MyIdentityProvider");
public class AppAuthOptions : AuthenticationSchemeOptions
{
public string Realm = "BlazrAuth";
}
The service registration:
public static class AuthServicesCollection
{
public static void AddAppAuthServerServices(this IServiceCollection services)
{
services.AddAuthentication("BlazrAuth").AddScheme<AppAuthOptions, AppAuthenticationHandler>("BlazrAuth", null);
services.AddScoped<IIdentityService, IdentityService>();
}
}
Credits: Some of this code was derived from: https://harrison-technology.net/

CosmosDb not using the ContractResolver provided when generating select queries

I have a project where I'm using CosmosDb (SQL API) as my database. It's a .Net Core project and I'm using the latest stable NuGet packages.
The document client is created as follows and use a custom contract resolver.
new DocumentClient(new Uri(settings.DatabaseUri), settings.DatabaseKey,
new JsonSerializerSettings
{
ContractResolver = new PrivateSetterCamelCasePropertyNamesContractResolver(),
Converters = new List<JsonConverter>
{
new EmailJsonConverter()
}
});
I have a collection called EmailAccount
public class EmailAccount : Entity
{
public string Email { get; private set; }
public string DisplayName { get; private set; }
public EmailAccount(DDD.Core.ValueObjects.Email email,
string displayName)
{
Email = email ?? throw new ArgumentNullException(nameof(email));
DisplayName = string.IsNullOrWhiteSpace(displayName) ? throw new ArgumentNullException(nameof(displayName)) : displayName;
}
}
All the properties are converted into camel-case when serialized which all works fine. But the problem is when I try to filter the documents. The SQL query that's generated looks something like this when I try to filter by the Email.
SELECT * FROM root WHERE (root["Email"] = "amila#iagto.com")
The problem is with the case of the property (Email). The property in the database is email but the query generator doesn't seem to be adhering to the ContractResolver provided and generates the above sql query which doesn't return any result.
If I put [JsonProperty("email")] above the Email property, the query is generated properly. Anyway to get the query generated properly without using attributes in the Entity class?
Any help much appreciated.
You need to set the JsonSerializerSettings at the CreateDocumentQuery level for the LINQ to SQL to pick it up.
This property was added in the SDK on 2.0.0+ versions.

ITempDataProvider in MVC 6 to use cookies for tempdata

I'm migrating a site over to use MVC 6. Currently I have tempdata store in cookies, but I can't find the set up of how to do this in the new MVC framework.
First, implement your ITempDataProvider. I did it this way, using JSON.Net.
public class CookieTempDataProvider : ITempDataProvider
{
readonly string CookieKey = "_tempdata";
public IDictionary<string,object> LoadTempData(HttpContext context)
{
var cookieValue = context.Request.Cookies[this.CookieKey];
if(string.IsNullOrWhiteSpace(cookieValue))
{
return new Dictionary<string, object>();
}
var decoded = Convert.FromBase64String(cookieValue);
var jsonAsString = Encoding.UTF8.GetString(decoded);
var dictionary = JsonConvert.DeserializeObject<IDictionary<string,object>>(jsonAsString, new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All, TypeNameAssemblyFormat = FormatterAssemblyStyle.Full });
// The cookie really should be deleted when the SaveTempData() method is called with an empty dictionary
// but that does not seem to be working for some reason. Added this delete for now (maybe this is a beta issue)
// TODO: Revisit at next release
context.Response.Cookies.Delete(this.CookieKey);
return dictionary;
}
public void SaveTempData(HttpContext context, IDictionary<string,object> values)
{
if (values == null || values.Count == 0)
{
context.Response.OnStarting(() => Task.Run(() =>
{
context.Response.Cookies.Delete(this.CookieKey);
}));
return;
}
var jsonAsString = JsonConvert.SerializeObject(values, new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All, TypeNameAssemblyFormat = FormatterAssemblyStyle.Full });
var bytes = Encoding.UTF8.GetBytes(jsonAsString);
var encoded = Convert.ToBase64String(bytes);
context.Response.Cookies.Append(this.CookieKey, encoded);
}
}
Next, in Startup.cs, where services are wired up, replace the default ITempDataProvider with your custom version, like so:
public void ConfigureServices(IServiceCollection services)
{
// Replace Temp Data Provider
var existing = services.FirstOrDefault(x => x.ServiceType == typeof(ITempDataProvider));
services.Remove(existing);
services.AddSingleton<ITempDataProvider, CookieTempDataProvider>();
}
EDIT
Since RC2 the original answer doesn't work any longer due to what seems like timing changes in the MVC request lifecycle...you'll receive an error about not being able to modify headers. I've updated the SaveTempData() method above to account for this.
I also had this need, so I've implemented a cookie-based TempData provider for ASP.NET Core MVC and published it on NuGet. It is available here.
If you think about TempData class for storing data for next request, there is some changes in MVC 6. You need to add additional package and configure it. Here are steps:
Remove "dnxcore50" from frameworks section in [project.json]. Session hasn't implementd yet in dnxcore50.
In the [project.json] add:
"Microsoft.AspNet.Session": "1.0.0-rc1-final"
Enable Caching and Session in class Startup.cs, method ConfigureServices, by adding next lines after services.AddMvc():
services.AddCaching();
services.AddSession();
Cinfigure it on class Startup.cs, method Configure, adding next line before app.UseMvc(...):
app.UseSession();
And that's it. But remember, you can store only primitive or serializable data types. If you need to store user defined data type, you need to serialized it. For that purpose we use "Newtonsoft.Json" lib. Here is example:
string json = JsonConvert.SerializeObject(myObject);
TempData["myKey"] = json;

Web API httpget with many parameters

I am trying to create my first REST service using WEB API to replace some of my postbacks in a web forms asp.net project. In the web forms project, when I browse to a new web page, I always get an ASP.net Application variable and a querystring value that helps me determine which database to connect to. In this old app, it connects to several different databases that all have the same schema and database objects but the data is different in each database
I am not sure the best way to pass these variables to a REST Service or if they should be part of the route or some other method.
So in a REST method like the one below
// GET api/<controller>/5
public string GetCategoryByID(int id)
{
return "value";
}
I can get the category id and pass that to my database layer, but I also need the two variables mentioned above. I will need to obtain these variables in every call to my REST api in order to access the appropriate database. Should I use something like the following:
// GET api/<controller>/5
public string GetCategoryByID(int id, string applicationEnvironment, string organization)
{
return "value";
}
Or should they be part of the route with something like this:
api/{appEnvironment}/{organization}/{controller}/{id}
This seems like a simple problem, but I am having trouble figuring out a solution.
I ended up passing extra parameters with my httpget call. I will probably follow this pattern unless I get some additional feedback.
[HttpGet]
public Company[] GetProgramCompanies(int id, [FromUri] string org, [FromUri] string appEnvir)
{
DataLayer dataAccess = new DataLayer(Utilities.GetConnectionString(org, appEnvir));
IEnumerable<BudgetProgramCompanyListing> companies = dataAccess.GetProgramCompaniesListing(id).OrderBy(o => o.Company_Name);
Company[] returnComps = new Company[companies.Count()];
int count = 0;
foreach (BudgetProgramCompanyListing bpc in companies)
{
returnComps[count] = new Company
{
id = bpc.Company_ID,
name = bpc.Company_Name
};
count++;
}
return returnComps;
}
Calling the above service with this url:
api/programcompanies/6?org=SDSRT&appEnvir=GGGQWRT
In .Net core 1.1 you can specify more parameters in HttGet attribute like this:
[HttpGet("{appEnvironment}/{organization}/{controller}/{id}")]
It may work in other .Net versions too.
I used to follow the below two method to pass multiple parameter in HttpGet
public HttpResponseMessage Get(int id,[FromUri]int DeptID)
{
EmpEntity = new EmpDBEntities();
var entity = EmpEntity.USP_GET_EMPINFO(id, DeptID).ToList();
if(entity.Count()!=0)
{
return Request.CreateResponse(HttpStatusCode.OK, entity);
}
else
{
return Request.CreateErrorResponse(HttpStatusCode.NotFound, "Employee With ID=" + id.ToString() + " Notfound");
}
}
and the webapi url will be http://localhost:1384/api/emps?id=1&DeptID=1
in the above methode USP_GET_EMPINFO is the stored procedure with two parameters.
in second method we can use the class with [FromUri] to pass multiple parameter.
the code snippet is as below
public HttpResponseMessage Get(int id,[FromUri]Employee emp)
{
EmpEntity = new EmpDBEntities();
var entity = EmpEntity.USP_GET_EMPINFO(id,emp.DEPTID).ToList();
if(entity.Count()!=0)
{
return Request.CreateResponse(HttpStatusCode.OK, entity);
}
else
{
return Request.CreateErrorResponse(HttpStatusCode.NotFound, "Employee With ID=" + id.ToString() + " Notfound");
}
}
and the webapi url will be http://localhost:1384/api/emps?id=1&DEPTID=1
here the DEPTID is one of the property of the class. we can add multiple parameters separated with & in the url
You could also define a model and send that with the request and bind it to a variable in your api function using [FromBody].
Something like:
[HttpGet]
public Company[] GetProgramCompanies([FromBody] YourModel model) { ... }
As explained here Model binding in Asp.Net Core

JsonResult in services layer

In my MVC3 solution I'm wondering how to move the logic that returns Json out of the controller and into the service layer. Say I have the following action in my controller to get the Json needed for a JQueryUI autocomplete control:
public JsonResult ClientAutocompleteJSON(string term)
{
NorthwindEntities db = new NorthwindEntities();
var customers = db.Customers
.Where(c => c.ContactName.Contains(term))
.Take(25)
.Select(c => new
{
id = c.CustomerID,
label = c.ContactName,
value = c.ContactName
});
return Json(customers, JsonRequestBehavior.AllowGet);
}
How would I move this into the service layer? I would prefer not to reference System.Web.MVC in my service layer. I've also thought of returning the customers but I'm not sure how to return the anonymous type - would I have to create a class?
I would not couple your service implementation to a specific (UI) format. It would be better to return a strongly typed customer object and then format this how you want within your Action method.
// Service method
public IEnumerable<Customer> FindCustomers(string term) {
NorthwindEntities db = new NorthwindEntities();
return db.Customers
.Where(c => c.ContactName.Contains(term))
.Take(25)
.ToList();
}
// Action method
public JsonResult ClientAutocompleteJSON(string term) {
var customers = customerService.FindCustomers(term)
.Select(c => new
{
id = c.CustomerID,
label = c.ContactName,
value = c.ContactName
});
return Json(customers, JsonRequestBehavior.AllowGet);
}
This code is much more reusable - for example, you could use the same service method to provide a simple HTML search form.
Create a DTO object: http://martinfowler.com/eaaCatalog/dataTransferObject.html
I know about a feature in Ruby on Rails, there you can define that your method is capable of returning JSON or XML or HTML based on client preference, it will be a good feature if you can find a library that can do this for you. It could be an aspect which by dynamic proxifying your services can do.

Resources