Web API HTTPGet for multiple attributes? - c#

We have a Web API written in DotNet Core 3.1.402 (I am new to DotNet Core and WebAPI).
We use SqlKata for Database processing.
We have an Account model that has AccountID, AccountName, AccountNumber, etc.
We would like to get an Account by different attributes, for ex: by AccountID, by AccountName, by AccountNumber.
How can we do that so that we don't need a separate HttpGet for each attribute (so we don't have to repeat the same code for different attributes) ?
This is our HttpGet in the AccountsController to get the account by AccountID
public class AccountsController : ControllerBase
{
private readonly IAccountRepository _accountRepository;
[HttpGet("{AccountID}")]
public Account GetAccount(int AccountID)
{
var result = _accountRepository.GetAccount(AccountID);
return result;
}
This is the code in the AccountRepository.cs
public Account GetAccount(int accountID)
{
var result = _db.Query("MyAccountTable").Where("AccountID", accountID).FirstOrDefault<Account>();
return result;
}
This is the Account class
namespace MyApi.Models
{
public class Account
{
public string AccountID { get; set; }
public string AccountName { get; set; }
public string AccountNumber { get; set; }
// other attributes
}
}
Thank you.

Doing it with GET can be a pain, there are ways to pass on the path/query arrays and complex objects but are ugly, the best you can do is to use POST instead of GET and pass an object with the filters that you want.
//In the controller...
[HttpPost]
public Account GetAccount([FromBody]Filter[] DesiredFilters)
{
var result = _accountRepository.GetAccount(DesiredFilters);
return result;
}
//Somewhere else, in a shared model...
public class Filter
{
public string PropertyName { get; set; }
public string Value { get; set; }
}
//In the repository...
public Account GetAccount(Filter[] Filters)
{
var query = _db.Query("MyAccountTable");
foreach(var filter in Filters)
query = query.Where(filter.PropertyName, filter.Value);
return query.FirstOrDefault<Account>();
}
Now you can send a JSON array on the request body with any filters that you want, per example:
[
{ "PropertyName": "AccountID", "Value": "3" },
{ "PropertyName": "AccountName", "Value": "Whatever" }
]

Related

How to partial update mongodb document in Web Api Patch request

I want to partial update fileds of my document that changed in the patch request and in every request the field probably changed for example one time Balance will be charged another time AccountHolder and others.
I want to update each specific field in single method not create a method for the number of fields .
Note: there is nothing in IDto interface, I use it just for separating Dtos from other classes.
UpdateAccountDto.cs
public class UpdateAccountDto : IDto
{
public string Id { get; set; } = string.Empty;
public string AccountId { get; set; } = string.Empty;
public string AccountHolder { get; set; } = string.Empty;
public string AccountType { get; set; } = string.Empty;
public decimal Balance { get; set; }
}
Account.cs my entity
public class Account
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; } = string.Empty;
[BsonElement("account_id")]
public string AccountId { get; set; } = string.Empty;
[BsonElement("account_holder")]
public string AccountHolder { get; set; } = string.Empty;
[BsonElement("account_type")]
public string AccountType { get; set; } = string.Empty;
[BsonRepresentation(BsonType.Decimal128)]
[BsonElement("balance")]
public decimal Balance { get; set; }
}
My endpoint
[HttpPatch("UpdatePartialAccount")]
public async Task<ActionResult> UpdatePartialAccount([FromQuery]string id,[FromBody] JsonPatchDocument<UpdateAccountDto>? document)
{
if (document is null)
return BadRequest();
var updateAccountDto = document.ToDto();
document.ApplyTo(updateAccountDto, ModelState);
if (!ModelState.IsValid)
return BadRequest();
var entity = updateAccountDto.ToEntity<Account>();
entity.Id = id;
await _accountRepository.PartialUpdateAsync(entity);
return NoContent();
}
PartialUpdateAsync method
public async Task<UpdateResult> PartialUpdateAsync(Account account)
{
//var filter = Builders<Account>.Filter.Eq(a => a.Id, account.Id);
//var update = Builders<Account>.Update.Set()
//Partial update
}
Your question are actually not very clear, but I guess you may want to know the field settings corresponding to JsonPatchDocument and the operation of updating the Mongodb database.
Regarding the field setting of JsonPatchDocument, you can refer to the official document to use it. For example:
[HttpPatch("UpdatePartialAccount")]
public async Task<ActionResult> UpdatePartialAccount([FromQuery] string id, [FromBody] JsonPatchDocument<UpdateAccountDto>? document)
{
if (document is null)
return BadRequest();
var dto = new UpdateAccountDto() { AccountId = "DAccount1" };
document.ApplyTo(dto, ModelState);
//.....
}
Suppose you do a Replace operation on it:
[
{
"operationType": 2,
"path": "/accountHolder",
"op": "replace",
"value": "TestHolder"
}
]
At this point your dto will become:
Please confirm the content of dto and then match with Account to update the database(I'm not sure how you map to Account, but I used AutoMapper). For example:
public async Task UpdateAsync(Account account)
{
await _account.UpdateOneAsync(c => c.AccountId == account.AccountId, account.AccountHolder);
}
For more operations on updating the database, you can check this link.
Hope this can help you.

C# How to Properly Consume Yelp GraphQL api

I'm trying to call the business endpoint of Yelp's GraphQL api with my asp.net core mvc application using GraphQLHttpClient. I have the api and bearer token configured in my client instance. I followed the query structure here using business as the endpoint and I just wanted Id and Name fields from the data. When I call SendQueryAsync(query), I get a GraphQL Error from the response. I'm not sure if I'm making an improper httprequest and/or my query is written wrong. I couldn't find any YouTube videos, stackoverflow questions, or github projects regarding consuming Yelp's GraphQL api using C#. Any help is appreciated. Thank you! Below is my source code and attached response.
[Update: Resolved Issued]
There were a collection of issues. Added additional required fields with variables to YelpGraphQL query for GraphQL request. More about query structure and variable declaration is explained in this thread. Overrided the casing of the fields (ty Neil). Fixed the responsetype class and added the missing classes (ty Neil). Added searchconsumer class to controller via dependency injection. Also I will post copied text of exceptions next time.
Classes
public class Business
{
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
}
public class Search
{
[JsonPropertyName("business")]
public List<Business> business { get; set; }
}
public class SearchResponseType
{
[JsonPropertyName("search")]
public Search Search { get; set; }
}
public interface ISearchConsumer
{
public Task<List<Business>> GetAllBusinesses();
}
public class SearchConsumer : ISearchConsumer
{
private readonly ApplicationDbContext _dbContext;
public SearchConsumer(ApplicationDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<List<Business>> GetAllBusinesses()
{
var authorization = _dbContext.Authorizations.FirstOrDefault().Token;
var _client = new GraphQLHttpClient("https://api.yelp.com/v3/graphql", new NewtonsoftJsonSerializer());
_client.HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authorization);
var query = new GraphQLRequest
{
Query = #"
query($termId: String $locationId: String){
search(term:$termId location:$locationId) {
business {
id
name
}
}
}",
Variables = new
{
termId = "burrito",
locationId = "san francisco"
}
};
var response = await _client.SendQueryAsync<SearchResponseType>(query);
var businesses = response.Data.Search.business;
return businesses;
}
}
Controllers
public class YelpGraphQLController : Controller
{
private readonly ISearchConsumer _consumer;
public YelpGraphQLController(ISearchConsumer consumer)
{
_consumer = consumer;
}
public IActionResult Index()
{
return View();
}
[HttpGet]
public async Task<IActionResult> Get()
{
var businesses = await _consumer.GetAllBusinesses();
return Ok(businesses);
}
}
Program
ConfigureServices(builder.Services);
void ConfigureServices(IServiceCollection services)
{
services.AddTransient<ISearchConsumer, SearchConsumer>();
}
YelpGraphQL Json Data Example
{
"data": {
"search": {
"business": [
{
"id": "wGl_DyNxSv8KUtYgiuLhmA",
"name": "Bi-Rite Creamery"
},
{
"id": "lJAGnYzku5zSaLnQ_T6_GQ",
"name": "Brenda's French Soul Food"
}
]
}
}
}
Debug GraphQL Error
I'm guessing that the deserialization isn't working because of the casing of the fields vs your class, which you can override like so:
public class Business
{
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
}
public class ResponseBusinessCollectionType
{
[JsonPropertyName("businesses")]
public List<Business> Businesses { get; set; }
}

How to get the summary of the controller and action

I am implementing IApiDescriptionGroupCollectionProvider to get the API description.
I have implemented in this way
private readonly IApiDescriptionGroupCollectionProvider _apiExplorer;
public RouteController(
IApiDescriptionGroupCollectionProvider apiExplorer)
{
_apiExplorer = apiExplorer;
}
[HttpGet("all")]
public IActionResult GetRoute()
{
var paths = GetApiDescriptionsFor("v1");
return Ok();
}
I want to bind all the details of the controller to an ApiRouteDocument custom model with a description of the action too. But the interface I have implemented doesn't give a summary of the action, and the controller. Is there any built-in interface to extract the summary from the actions?
I wanted to avoid the Reflection.
[ApiController]
[Route("api/contact")]
[ApiExplorerSettings(GroupName = "Contact")]
public class ContactController : ControllerBase
{
/// <summary>
/// Get by Name Contact
/// </summary>
[HttpGet("getbyname/{name}")]
public async Task<IActionResult> GetByName(string name)
{
return Ok();
}
}
public class ApiRouteDocument
{
//controllername tag
public string ControllerName { get; set; }
public string ControllerDescription { get; set; }
public IList<RoutePath> Paths;
}
public class RoutePath
{
//get
public string Method { get; set; }
//operationid
public string Name { get; set; }
//summary
public string Description { get; set; }
//path
public string Path { get; set; }
}
You have to run this code after the middleware pipeline has been established (and app.UseEndpoints() has been executed), so it should run inside a controller, or an endpoint, for example.
Because documentation comments aren't included in the assembly, you need to use attributes to annotate your classes & actions. You have the option to use the ones provided by Microsoft, such as [DisplayName], [Description], etc. under System.ComponentModel.Primitives namespace.
Or you can create your own attribute:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
internal class SummaryAttribute : Attribute
{
public string Summary { get; }
public SummaryAttribute(string summary)
{
Summary = summary;
}
}
Then inject IEnumerable<EndpointDataSource> in a controller, which will give you the list of discovered endpoints.
You can get reflected runtime type info from ControllerActionDescriptor, then extract the attributes you've added to controllers & actions.
[Description("Provides info")]
[Route("info")]
public class ThingsController : ControllerBase
{
private IEnumerable<EndpointDataSource> _endpointDataSources;
public ThingsController(IEnumerable<EndpointDataSource> endpointDataSources)
{
_endpointDataSources = endpointDataSources;
}
[Description("Returns a list of endpoints")]
[HttpGet("endpoints")]
public ActionResult Endpoints()
{
var actions = _endpointDataSources.SelectMany(it => it.Endpoints)
.OfType<RouteEndpoint>()
.Where(
it => it.Metadata
.OfType<ControllerActionDescriptor>()
.Any()
)
.Select(
e => {
var actionDescriptor = e.Metadata
.OfType<ControllerActionDescriptor>()
.First();
var isControllerIgnored = actionDescriptor.ControllerTypeInfo.GetCustomAttribute<ApiExplorerSettingsAttribute>()?.IgnoreApi ?? false;
var isActionIgnored = actionDescriptor.MethodInfo .GetCustomAttribute<ApiExplorerSettingsAttribute>()?.IgnoreApi ?? false;
return new
{
ControllerName = actionDescriptor.ControllerName,
ControllerType = actionDescriptor.ControllerTypeInfo.FullName,
ActionDescription = actionDescriptor.MethodInfo.GetCustomAttribute<DescriptionAttribute>()?.Description,
ControllerDescription = actionDescriptor.ControllerTypeInfo
.GetCustomAttribute<DescriptionAttribute>()
?.Description,
Method = actionDescriptor.EndpointMetadata.OfType<HttpMethodMetadata>()
.FirstOrDefault()
?.HttpMethods?[0],
Path = $"/{e.RoutePattern!.RawText!.TrimStart('/')}",
};
}
)
.ToList();
return Ok(actions);
}
}
when you visit /info/endpoints, you'll get a list of endpoints:
[
{
"controllerName": "Things",
"controllerType": "ApiPlayground.ThingsController",
"actionDescription": "Returns a list of endpoints",
"controllerDescription": "Provides info",
"method": "GET",
"path": "/info/endpoints"
}
]

WebApi field filtering without using EF

I am building WebApi2 project to expose some RESTful service. Let's say I have following model objects.
public class Person
{
public string Name { get; set; }
public DateTime? Birthdate { get; set; }
public int Status { get; set; }
public List<Account> Accounts { get; set; }
}
public class Account
{
public decimal Amount { get; set; }
public string Code { get; set; }
public DateTime Expiry { get; set; }
}
In my service I have to go to 2 different systems to retrieve data for Person and the account info of the Person. Obviously the service implementation looks like
[HttpGet]
[Route("Person/{id:int}")]
public IHttpActionResult Get(string id)
{
var person = new Person();
person = GetPersonFromSystemA(id);
if (person.Status == 2)
{
person.Accounts = GetPersonAccountsFromSystemB(id);
}
return this.Ok(person);
}
I cannot use EF at all in this case, so OData is very tricky.
I have some requirement that I need to provide the filtering capability to the service client. The client can decide which fields of the objects to return, it also means that if the client does not like to include Accounts info of the person I should skip the second call to system B to avoid entire child object.
I did some quick search but I could not find some similar solution yet. I know I can implement my own filtering syntax, and have all custom codes to use the filtering (by having lots of if/else).
I am looking for some ideas of more elegant solution.
Entity Framework is not required for building an OData Service. If you do not use OData, you will probably have to implement your own IQueryable which is what OData does out of the box.
Some sample code.
Model classes with some added properties
public class Person
{
[Key]
public String Id { get; set; }
[Required]
public string Name { get; set; }
public DateTime? Birthdate { get; set; }
public int Status { get; set; }
public List<Account> Accounts { get; set; }
}
public class Account
{
[Key]
public String Id { get; set; }
[Required]
public decimal Amount { get; set; }
public string Code { get; set; }
public DateTime Expiry { get; set; }
}
WebApiConfig.cs
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.MapODataServiceRoute("odata", null, GetEdmModel(), new DefaultODataBatchHandler(GlobalConfiguration.DefaultServer));
config.EnsureInitialized();
}
private static IEdmModel GetEdmModel()
{
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.Namespace = "YourNamespace";
builder.ContainerName = "DefaultContainer";
builder.EntitySet<Person>("People");
builder.EntitySet<Account>("Accounts");
var edmModel = builder.GetEdmModel();
return edmModel;
}
}
Controller method
[EnableQuery]
public class PeopleController : ODataController
{
public IHttpActionResult Get()
{
return Ok(SomeDataSource.Instance.People.AsQueryable());
}
}
You will need to include the Microsoft.AspNet.OData Nuget package.
Refer to the following for more guidance. It uses an in memory data source, but the concept is the same regardless.
http://www.odata.org/blog/how-to-use-web-api-odata-to-build-an-odata-v4-service-without-entity-framework/
When building a web api you would often want to filter your response and get only certain fields. You could do it in many ways, one of which as suggested above. Another way, you could approach it is using data shaping in your web api.
If you had a controller action as such:
public IHttpActionResult Get(string fields="all")
{
try
{
var results = _tripRepository.Get();
if (results == null)
return NotFound();
// Getting the fields is an expensive operation, so the default is all,
// in which case we will just return the results
if (!string.Equals(fields, "all", StringComparison.OrdinalIgnoreCase))
{
var shapedResults = results.Select(x => GetShapedObject(x, fields));
return Ok(shapedResults);
}
return Ok(results);
}
catch (Exception)
{
return InternalServerError();
}
}
And then your GetShapedData method can do the filtering as such:
public object GetShapedObject<TParameter>(TParameter entity, string fields)
{
if (string.IsNullOrEmpty(fields))
return entity;
Regex regex = new Regex(#"[^,()]+(\([^()]*\))?");
var requestedFields = regex.Matches(fields).Cast<Match>().Select(m => m.Value).Distinct();
ExpandoObject expando = new ExpandoObject();
foreach (var field in requestedFields)
{
if (field.Contains("("))
{
var navField = field.Substring(0, field.IndexOf('('));
IList navFieldValue = entity.GetType()
?.GetProperty(navField, BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Public)
?.GetValue(entity, null) as IList;
var regexMatch = Regex.Matches(field, #"\((.+?)\)");
if (regexMatch?.Count > 0)
{
var propertiesString = regexMatch[0].Value?.Replace("(", string.Empty).Replace(")", string.Empty);
if (!string.IsNullOrEmpty(propertiesString))
{
string[] navigationObjectProperties = propertiesString.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
List<object> list = new List<object>();
foreach (var item in navFieldValue)
{
list.Add(GetShapedObject(item, navigationObjectProperties));
}
((IDictionary<string, object>)expando).Add(navField, list);
}
}
}
else
{
var value = entity.GetType()
?.GetProperty(field, BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Public)
?.GetValue(entity, null);
((IDictionary<string, object>)expando).Add(field, value);
}
}
return expando;
}
Check my blog for a detailed post: https://jinishbhardwaj.wordpress.com/2016/12/03/web-api-supporting-data-shaping/

Getting null value from Web api

I am trying to store a List of strings that are members of a conversation.
[DataContract]
public class Conversation
{
[Key]
[DataMember]
public string Key { get; set; }
[DataMember]
public string ConversationName { get; set; }
[DataMember]
public string Administrator { get; set; }
[DataMember]
public List<string> Members { get; set; }
public Conversation(string key, string name, string admin, List<string> members)
{
Key = key;
ConversationName = name;
Administrator = admin;
Members = members;
}
public Conversation()
{
}
}
I am using Postman to make a Post request to the URI which seems to work fine, returning 201 Created and giving a json object containing the correct info.
This is the json I am posting:
{
"Key": "123",
"ConversationName": "Test",
"Administrator": "Ken",
"Members": ["y#12.com", "f#78.com"]
}
However, when I try to get the conversation in a GET method this is the result:
{
"Key": "123",
"ConversationName": "Test",
"Administrator": "Ken",
"Members": null
}
These are my functions in my controller:
Post:
[HttpPost]
//[ResponseType(typeof(Conversation))]
public async Task<IHttpActionResult> PostConversation(Conversation convo)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
db.Conversations.Add(convo);
await db.SaveChangesAsync();
return CreatedAtRoute("DefaultApi", new { name = convo.Key }, convo);
}
GET:
[HttpGet]
public IQueryable<Conversation> GetConversations()
{
return db.Conversations;
}
Any help would be much appreciated. Thanks!
The post basically returns the entity as you offer it, which is with Members, apparently.
The get loads a Conversation from the database. It can't have any Members at that moment, because Members is a List<string>, which can't possibly be mapped to a database column.
You probably expect the post to store the Members into the database. This doesn't happen, because you should use Member entities in stead of strings. Member can be a simple class having an Id and a string property.

Categories