I'm writing a service to handle bans for a game and I'm currently a bit stuck trying to write a MongoDB query. Currently I have a collection of "User" objects, and the objects look like this:
public class User
{
public List<Ban> Bans { get; set; }
// some irrelevant additional fields
}
public class Ban
{
public HardwareId HWID { get; set; }
public DateTime Expires { get; set; }
// some irrelevant additional fields
}
public class HardwareId : IEquatable<HardwareId>
{
public string Field1 { get; set; }
public string Field2 { get; set; }
public string Field3 { get; set; }
public string Field4 { get; set; }
public bool Equals([AllowNull] HardwareId other)
{
if (ReferenceEquals(other, null)) return false;
if (ReferenceEquals(this, other)) return true;
return Field1 == other.Field1 &&
Field2 == other.Field2 &&
Field3 == other.Field3 &&
Field4 == other.Field4;
}
}
What I want to do is have a query that finds all users with a ban where HWID has say 3 out of 4 fields matching. Currently I have a query that only finds users where the HWID match exactly (due to the Equals() implementation), but I'd like to change it. My current code looks like this:
public Ban FindBan(HardwareId hwid)
{
var banBuilder = Builders<Ban>.Filter;
var hwidFilter = banBuilder.Eq(b => b.HWID, hwid);
var expFilter = banBuilder.Gt(b => b.Expires, DateTime.UtcNow);
var banFilter = banBuilder.And(hwidFilter, expFilter);
var user = _users.Find(Builders<User>.Filter.ElemMatch(p => p.Bans, banFilter)).FirstOrDefault();
if (user != null)
{
return user.Bans[0];
}
return null;
}
The only way of solving it I can think of is to write "spaghetti if-statements" in the Equals() function, but I'd like a dynamic solution where I could add multiple "Fields" to the HardwareId class later down the line. Another issue with the FindBan() function currently is the fact that it returns "user.Bans[0]" instead of the actual ban that was found, but I imagine I could solve that by sorting by expiry.
here's one approach to match by minimum of 3 fields of HWID:
var numFieldsToMatch = 3;
var hwid = {
Field1: "one",
Field2: "two",
Field3: "three",
Field4: "four"
};
db.User.find({
$expr: {
$anyElementTrue: {
$map: {
input: "$Bans",
in: {
$gte: [{
$size: {
$filter: {
input: { $objectToArray: "$$this.HWID" },
cond: {
$or: [
{ $and: [{ $eq: ["$$this.k", "Field1"] }, { $eq: ["$$this.v", hwid.Field1] }] },
{ $and: [{ $eq: ["$$this.k", "Field2"] }, { $eq: ["$$this.v", hwid.Field2] }] },
{ $and: [{ $eq: ["$$this.k", "Field3"] }, { $eq: ["$$this.v", hwid.Field3] }] },
{ $and: [{ $eq: ["$$this.k", "Field4"] }, { $eq: ["$$this.v", hwid.Field4] }] }
]
}
}
}
}, numFieldsToMatch]
}
}
}
}
})
https://mongoplayground.net/p/MPwB14o6kuO
it's not possible to translate this mongo query to c# driver code afaik.
see here for an easy way to run this with c#.
You can write a method similar to Equals (or make changes to it), so that you have each condition match in to an array
FldArr[0] = (Field1 == other.Field1);
FldArr[1] = (Field2 == other.Field2);
FldArr[2] = (Field3 == other.Field3);
FldArr[3] = (Field4 == other.Field4);
Then by using LINQ for FldArr, you can find 3/4 true conditions.
Related
Having this strange-looking json response:
{
"invoices":{
"0":{
"invoice":{
"id":"420",
"invoicecontents":{
"0":{
"invoicecontent":{
"name":"Here's the name of the content 0"
}
},
"1":{
"invoicecontent":{
"name":"Here's the name of the content 1"
}
}
}
}
},
"1":{
"invoice":{
"id":"420",
"invoicecontents":{
"0":{
"invoicecontent":{
"name":"Here's the name of the content 0"
}
}
}
}
},
"parameters":{
"limit":"3",
"page":"1",
"total":"420"
}
},
"status":{
"code":"OK"
}
}
How do I change the structure into this easy-to-deserialize one?
{
"invoices":[
{
"id":"420",
"invoicecontents":[
{
"name":"Here's the name of the content 0"
},
{
"name":"Here's the name of the content 1"
}
]
},
{
"id":"420",
"invoicecontents":[
{
"name":"Here's the name of the content 0"
}
]
}
]
}
I'd like to deserialize into List of Invoices as below
class Invoice {
public string Id { get; set; }
[JsonProperty("invoicecontents")]
public InvoiceContent[] Contents { get; set; }
class InvoiceContent {
public string Name { get; set; }
}
}
There's no problem with getting the status code or parameters, I simply do this:
var parsed = JObject.Parse(jsonInvoices);
var statusCode = parsed?["status"]?["code"]?.ToString();
var parameters = parsed?["invoices"]?["parameters"]?;
The real problem starts when I'm trying to achieve easy-to-deserialize json structure I've mentioned before. I've tried something like this:
var testInvoices = parsed?["invoices"]?
.SkipLast(1)
.Select(x => x.First?["invoice"]);
But I can't manage to "repair" invoicecontents/invoicecontent parts.
My goal is to deserialize and store the data.
This isn't JSON.
JavaScript Object Notation literally describes Objects. What you have here is the punchline of a joke that starts out with: "A SQL JOIN, a StringBuilder, and a couple for loops walk into a bar..."
As others have demonstrated, JObject is great for working with JSON that would be impractical to define as classes. You can use Linq to navigate through it, but JSONPath Expressions can be much simpler.
Example 1: Let's get the status code.
var status = parsed.SelectToken(".status.code").Value<string>();
Example 2: Let's 'deserialize' our invoices.
public string Invoice
{
public string Id { get; set; }
public List<string> Content { get; set; }
}
public List<Invoice> GetInvoices(string badJson)
{
var invoices = JObject.Parse(badJson).SelectTokens(".invoices.*.invoice");
var results = new List<Invoice>();
foreach (var invoice in invoices)
{
results.Add(new Invoice()
{
Id = invoice.Value<string>("id"),
Contents = invoice.SelectTokens(".invoicecontents.*.invoicecontent.name")
.Values<string>().ToList()
// Note: JToken.Value<T> & .Values<T>() return nullable types
});
}
return results;
}
try this
var jsonParsed = JObject.Parse(json);
var invoices = ((JObject) jsonParsed["invoices"]).Properties()
.Select(x => (JObject) x.Value["invoice"] ).Where(x => x != null)
.Select(s => new Invoice
{
Id = s.Properties().First(x => x.Name == "id").Value.ToString(),
Contents = ((JObject)s.Properties().First(x => x.Name == "invoicecontents").Value).Properties()
.Select(x => (string) x.Value["invoicecontent"]["name"]).ToList()
}).ToList();
and I simplified your class too, I don't see any sense to keep array of classes with one string, I think it should be just array of strings
public class Invoice
{
public string Id { get; set; }
public List<string> Contents { get; set; }
}
result
[
{
"Id": "420",
"Contents": [
"Here's the name of the content 0",
"Here's the name of the content 1"
]
},
{
"Id": "420",
"Contents": [
"Here's the name of the content 0"
]
}
]
I have 2 entities parent/child want select them all using include.
When adding condition on child it return parents which have this condition only not all parents
My code:
Parent entity:
public class SecRole
{
public string Name { get; set; }
public virtual ICollection<SecRolePageAction> SecRole_SecRolePageAction { get; set; }
}
Child entity:
public class SecRolePageAction
{
public virtual SecRole SecRole { get; set; }
public long SecRoleID { get; set; }
public bool IsDeleted { get; set; } = false;
}
Code:
var Q = Context.Set<SecRole>().AsNoTracking().AsQueryable();
Q = Q.Include(O => O.SecRole_SecRolePageAction)
// Child condition below
.Where(O => O.SecRole_SecRolePageAction.Any(P => P.IsDeleted == false)
.ToList();
Result: it returns just parent contain child has IsDeleted = false but any parent has no child it does not return
I need to return all parents any help
It looks like you are selecting and including correctly. You may just need to modify your Where clause to check and see if your child collection is null/empty OR it has any children which have not been deleted.
Something like:
.Where(O => O.SecRole_SecRolePageAction.Count() == 0 || O.SecRole_SecRolePageAction.Any(P => P.IsDeleted == false))
#Keith.Abramo alreay gave the right answer for this question.
Here is the test result with this solution.
public List<SecRole> getSecRole()
{
var SecRoles = _context.SecRoles.Include(h => h.SecRole_SecRolePageAction)
.Where(O => O.SecRole_SecRolePageAction.Count() == 0 || O.SecRole_SecRolePageAction.Any(P => P.IsDeleted == false)).ToList(); // include address table
return SecRoles;
}
Date Source of SecRoles:
[
{
"Name": "1",
"SecRole_SecRolePageAction": [
{
"SecRoleID": "101",
"IsDeleted": "true"
}
]
},
{
"Name": "2",
"SecRole_SecRolePageAction": [
]
},
{
"Name": "3",
"SecRole_SecRolePageAction": [
{
"SecRoleID": "301",
"IsDeleted": "false"
},
{
"SecRoleID": "302",
"IsDeleted": "true"
}
]
}
]
Screenshot of test result with Where
In a WebAPI project, i have a controller that checks a status of a product, based on a value the user enters.
Lets say they enter "123" and the response should be "status": 1, AND a list of products. If they enter "321" the "status" is 0, AND a list of products.
My question is, how do i build such a string correct in a WebAPI controller.
[Route("{value:int}")]
public string GetProducts(int value)
{
var json = "";
var products = db.Products;
if (products.Any())
{
foreach (var s in products)
{
ProductApi product = new ProductApi();
product.Name = s.Name;
json += JsonConvert.SerializeObject(supplier);
}
}
var status = db.Status;
if (status.Any())
{
json += "{status:1}";
}
else
{
json += "{status:0}";
}
return json;
}
public class ProductApi
{
public string Name { get; set; }
}
Also, is this output/response considered valid?
[
{
"id":1,
"name":"product name"
},
{
"id":2,
"name":"product name 2"
},
{
"id":3,
"name":"product name 3"
}
]
{
"status": 0
}
So here are the changes for your post:
First, you should make your api return Json by default when you pass a text/html request (is this you are looking for?), adding this line to your WebApiConfig class:
config.Formatters.JsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/html"));
Second, I changed the code to return a real object, impersonating your response:
public class ProductApiCollection
{
public ProductApi[] Products { get; set; }
public byte Status { get; set; }
}
public class ProductApi
{
public string Name { get; set; }
}
Method body:
public ProductApiCollection Get()
{
var result = new ProductApiCollection();
var dbProducts = db.Products;
var apiModels = dbProducts.Select(x => new ProductApi { Name = x.Name } ).ToArray();
result.Products = apiModels;
var status = db.Status.Any() ? 1 : 0;
result.Status = status;
return result;
}
This will results in the following example json:
{
"Products": [
{
"Name": "Pork"
},
{
"Name": "Beef"
},
{
"Name": "Chicken"
},
{
"Name": "Salad"
}
],
"Status": 1
}
I strongly advise you not to do manual formatting for such things, and rely on built-in and 3rd party libraries. Otherwise, you will be reinventing the things already available, tested and ready to work.
Just as raderick mentioned, you don't need to create your own custom JSON infrastructure.
public class ProductApi
{
public int Id {get;set;}
public string Name { get; set; }
}
public class ResponseDTO
{
public int Status {get;set;}
public List<ProductApi> { get; set; }
}
And in your API action, return like this:
[Route("{value:int}")]
public ResponseDTO GetProducts(int value)
{
ResponseDTO result = ...// construct response here
return result;
}
One of the Mongo objects I use (referred to as 'Admin' here) uses GUID as the primary key and has a list of 'DailyActivities' which contains datevalue and another list called 'Subactivities'. The Admin object looks like something below. I am struggling to find resources in C# that will help extract DailyActivities that only corresponds to a particular date with Subactivities that has a category of 'Power Consumption'.
{
"_id" : ObjectId("5a2b7b887df7ce464404dc7d"),
"DailyActivities" : [
{
"datetime" : ISODate("2017-12-09T16:29:00.916Z"),
"Subactivities" : [
{
"entryDate" : ISODate("2017-12-09T06:30:26.658Z"),
"category" : "Power Consumption"
},
{
"entryDate" : ISODate("2017-12-09T06:30:26.658Z"),
"category" : "Machinery"
}
]
},
{
"datetime" : ISODate("2017-12-13T00:00:00.916Z"),
"Subactivities" : [
{
"entryDate" : ISODate("2017-12-13T06:30:26.658Z"),
"category" : "Lamination"
}
]
}
]
}
The result I would like to receive should be:
{
"_id" : ObjectId("5c7044f07ef75175b2b8efd6"),
"entryDate" : ISODate("2017-12-09T06:30:26.658Z"),
"category" : "Power Consumption"
}
Right now, I don't have the time to complete this exercise but here is something to get you going. I shall edit and improve this (as in convert it into some c# a lot of which won't be possible using a typed approach...) next week.
db.collection.aggregate([{
$project: {
"DailyActivities": {
$filter: {
input: "$DailyActivities",
cond: {
$eq: [ "$$this.datetime", ISODate("2017-12-09T16:29:00.916Z") ]
}
}
}
}
}, {
$unwind: "$DailyActivities"
}, {
$unwind: "$DailyActivities.Subactivities"
}, {
$replaceRoot: {
"newRoot": "$DailyActivities.Subactivities"
}
}, {
$match: {
"category": "Power Consumption"
}
}])
let me offer you a solution which uses MongoDAL as the data access layer. it's a wrapper around the c# driver so you get all the features of the driver plus a highly typed api.
using System;
using System.Linq;
using MongoDAL;
namespace AdminActs
{
class Admin : Entity
{
public DailyActivity[] DailyActivities { get; set; }
}
class DailyActivity
{
public DateTime Time { get; set; }
public SubActivity[] SubActivities { get; set; }
}
class SubActivity
{
public DateTime EntryDate { get; set; }
public string Category { get; set; }
}
class Program
{
static void Main(string[] args)
{
new DB("activities");
var now = DateTime.Now;
var admin = new Admin
{
DailyActivities = new DailyActivity[]
{
new DailyActivity{
Time = now,
SubActivities = new SubActivity[]
{
new SubActivity{
Category ="Power Consumption",
EntryDate = DateTime.Now}
}
}
}
};
admin.Save();
var subActivities = admin.Collection()
.SelectMany(a => a.DailyActivities)
.Where(da => da.Time == now)
.SelectMany(da => da.SubActivities)
.Where(sa => sa.Category == "Power Consumption");
var res = subActivities.ToArray();
Console.ReadKey();
}
}
}
In a WebAPI project, i have a controller that checks a status of a product, based on a value the user enters.
Lets say they enter "123" and the response should be "status": 1, AND a list of products. If they enter "321" the "status" is 0, AND a list of products.
My question is, how do i build such a string correct in a WebAPI controller.
[Route("{value:int}")]
public string GetProducts(int value)
{
var json = "";
var products = db.Products;
if (products.Any())
{
foreach (var s in products)
{
ProductApi product = new ProductApi();
product.Name = s.Name;
json += JsonConvert.SerializeObject(supplier);
}
}
var status = db.Status;
if (status.Any())
{
json += "{status:1}";
}
else
{
json += "{status:0}";
}
return json;
}
public class ProductApi
{
public string Name { get; set; }
}
Also, is this output/response considered valid?
[
{
"id":1,
"name":"product name"
},
{
"id":2,
"name":"product name 2"
},
{
"id":3,
"name":"product name 3"
}
]
{
"status": 0
}
So here are the changes for your post:
First, you should make your api return Json by default when you pass a text/html request (is this you are looking for?), adding this line to your WebApiConfig class:
config.Formatters.JsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/html"));
Second, I changed the code to return a real object, impersonating your response:
public class ProductApiCollection
{
public ProductApi[] Products { get; set; }
public byte Status { get; set; }
}
public class ProductApi
{
public string Name { get; set; }
}
Method body:
public ProductApiCollection Get()
{
var result = new ProductApiCollection();
var dbProducts = db.Products;
var apiModels = dbProducts.Select(x => new ProductApi { Name = x.Name } ).ToArray();
result.Products = apiModels;
var status = db.Status.Any() ? 1 : 0;
result.Status = status;
return result;
}
This will results in the following example json:
{
"Products": [
{
"Name": "Pork"
},
{
"Name": "Beef"
},
{
"Name": "Chicken"
},
{
"Name": "Salad"
}
],
"Status": 1
}
I strongly advise you not to do manual formatting for such things, and rely on built-in and 3rd party libraries. Otherwise, you will be reinventing the things already available, tested and ready to work.
Just as raderick mentioned, you don't need to create your own custom JSON infrastructure.
public class ProductApi
{
public int Id {get;set;}
public string Name { get; set; }
}
public class ResponseDTO
{
public int Status {get;set;}
public List<ProductApi> { get; set; }
}
And in your API action, return like this:
[Route("{value:int}")]
public ResponseDTO GetProducts(int value)
{
ResponseDTO result = ...// construct response here
return result;
}