Is it possible to force the JsonWriterSettings to output the ObjectID as
{ "id" : "522100a417b86c8254fd4a06" }
instead of
{ "_id" : { "$oid" : "522100a417b86c8254fd4a06" }
I know I could write my own parser, but for the sake of code maintenance, I would like to find away to possibly override the Mongo JsonWriterSettings.
If this is possible, what classes/Interfaces should I override?
If you're OK with using MongoDB C# attributes or the Mapper, then you can do something like this:
public class Order {
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; }
}
That way, you can refer to the type as a string normally (including serialization), but when MongoDB serializes it, etc., it's internally treated as an ObjectId. Here's using the class map technique:
BsonClassMap.RegisterClassMap<Order>(cm => {
cm.AutoMap();
cm.SetIdMember(cm.GetMemberMap(c => c.Id);
cm.GetMemberMap(c => c.Id)
.SetRepresentation(BsonType.ObjectId);
});
If you use JSON.NET instead it's easy to add a JsonConverter that converts ObjectId values to strings and vice-versa.
In ASP.NET WebAPI you can then add this to the default set of converters at Formatters.JsonFormatter.SerializerSettings.Converters
I am using MongoDB.Driver with version 2.15.1 and for me is working this simple solution:
public class Order {
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; }
}
You do not have to specify attribute [BsonId] if the property name is "Id" The driver uses naming conventions.
Related
I have a custom validation attribute
Lets say I have a HelloWorld class that implements ValidationAttribute. I then apply this attribute to a field within my API.
[HelloWorld]
public string FirstName { get; set; }
When I generate the Swagger UI, I get a JSON OpenAPI spec and the model displays the properties of each field like below:
If I add a Required tag, a asterisk is displayed
If I use attributes like RegularExpression/Range/StringLength, text appears to specify this.
However, I would like to make someone aware of the custom validation, with my own description.
Is it possible?
Any help would be much appreciated. I've spent all day looking at DocumentFilter/SchemaFilter/OperationFilter, but can't find any good documentation or examples.
You can do it like this:
Create custom validation attribute, for example UniqueAttribute
public class UniqueAttribute : ValidationAttribute { ... }
Apply this attribute to a model property
[Unique]
public string Name { get; set; }
Implement ISchemaFilter interface extension
public class AddUniquenessDescriptionFilter : ISchemaFilter
{
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
var attr = context.MemberInfo?.CustomAttributes.Where(x =>
x.AttributeType.Name == nameof(UniqueAttribute))
.FirstOrDefault();
if (attr is not null)
{
schema.Extensions.Add("isUnique", new OpenApiBoolean(true));
}
}
}
And use extension on startup
builder.Services.AddSwaggerGen(options =>
{
options.SchemaFilter<AddUniquenessDescriptionFilter>();
});
The results look like this:
Swagger documentation
Here is a full example: Custom ValidationAttribute
I was trying to use OperationFilter, but it wasn't supported on .Net Framework 4.1.
I would have to use the filters to amend the documentation (I believe this is the was x-extensions is utilised)
Using the C# MongoDB Driver, I'm trying to understand the main difference between the Attribute [BsonId] and the BsonClassMap.MapIdField method used in BsonClassMap.RegisterClassMap.
Less generally I'm trying to use a string field of a Model as my document's Id field. If I use the [BsonId] Attribute everything is working fine:
public class MyModel
{
[BsonId]
public string AStringId{ get; set; }
}
But if I try to instead register it as Id with MapIdField I then have a System.FormatException: Cannot deserialize a 'String' from BsonType 'ObjectId'.. whenever I'm querying that Collection 🙈
BsonClassMap.RegisterClassMap<MyModel>(cm =>
{
cm.MapIdField(m => m.AStringId);
});
I'd like to know why this problem is happening and more generally what's the difference between those two ways to use a field as custom Id?
🙏 : Don't RTFD me as the MongoDB C# driver documentation is awfully dry on that subject
Thanks
When using the PipelineStageDefinitionBuilder when creating projection stages for an aggregation pipeline it appears to be always ignoring any Id values in the dataset. I'm using the Mongo .Net driver 2.8 in a .Net Core app. Below are the steps for reproduction.
The same projection worked when using the IAggregateFluent syntax on Aggregate() however I needed to use the builders for a facet. When running the builder against Aggregate it also works, however within a facet it fails to bind any Id values.
Just empty classes with id for testing (Added Type to show normal mapping works):
public class DatabaseModel
{
public Guid Id { get; set; }
public string Type { get; set; }
}
public class ProjectionClass
{
public Guid Id { get; set; }
public string Type { get; set; }
}
When I create the projection with the below, it produces a query sucessfully, however within all models returned the Id value is set to null. The query seems to have a Id_ : 0 value but the same also seems to be produced in normal aggregation so I don't think this is related?
var typeFilter = Builders<DatabaseModel>.Filter.Eq(x => x.Type, "Full");
var aggregationPipeline = new EmptyPipelineDefinition<DatabaseModel>()
.AppendStage(PipelineStageDefinitionBuilder.Match(typeFilter))
.AppendStage(PipelineStageDefinitionBuilder.Project<DatabaseModel, ProjectionClass>(x => new ProjectionClass
{
Id = x.Id,
Type = x.Type,
}));
var normalAggregationResult = await db.Aggregate(aggregationPipeline).ToListAsync();//The id's appear here
var databaseModelsFacet = AggregateFacet.Create("DatabaseModels", aggregationPipeline);
var faucetResult = db.Aggregate().Facet(databaseModelsFacet).SingleOrDefault().Facets;
var projectionModels = faucetResult.
Single(x => x.Name == "DatabaseModels")
.Output<ProjectionClass>();// This results in missing Id's (Including in nested objects with anything named Id)
Resulting mongo query
{[{
"$match" : { "Type" : "Full" } },
{ "$project" : { "Id" : "$_id", "Type" : "$Type", "_id" : 0 }
}]}
Is there any way to be able to run a projection using the pipeline builders with a facet while not ignoring the Id? I have seen examples using similar queries but haven't seen this as an issue. It could be an issue with facet as it only appears to happen when using this.
Thanks!
UPDATE 6/1/2020: Updated question after finding it only seems to occur with facet
It seems to be an Driver issue, (or other issue is when the structure does not match the fields), as Id cant simply be serialized to Id , but if you choose any other value it will work for example
[BsonNoId]
public class DatabaseModel
{
[BsonRepresentation(BsonType.ObjectId)]
public string Identifier { get; set; }
public string Type { get; set; }
}
I'm having a hard time figuring something out that seems as a "easy" problem.
I'm working with Microsoft Azure mobile apps .Net backend, a MSSQL database, Entity Framework code-first and AutoMapper.
So i have the following objects:
public class Route
{
public string Id { get; set; }
[...] //some other properties
public string SerializedGoogleRoute { get; set; }
}
public class DtoRoute
{
public string Id { get; set; }
[...]
public DtoGoogleRoute GoogleRoute { get; set; }
}
public class DtoGoogleRoute
{
[...] //only strings, ints,...
}
So what I want to do is: In the database save the GoogleRoute as a serialized string because it consists of many properties and I don't need them in different columns - I just want it as a serialized string in one column on the route entity.
When the Route object is projected to the DtoRoute object I want the GoogleRoute to be serialized and vice versa.
Because I'm working with LINQ / queryables I am limited to a few AutoMapper mapping options (see AutoMapper wiki). And with none of these I can't get it to work.
The problems I'm facing/what I tried:
I can't serialize/deserialize the string to the DtoGoogleRoute on mapping (with MapFrom or ConstructProjectionUsing) because LINQ obviously cannot transform the JsonConvert.Serialize/Deserialize methods to SQL statements.
I tried having a DtoGoogleRoute property in the Route object and a string property in the DtoRoute object with getters/setters doing the (de)serialization. This works almost perfectly in a custom API controller but because of the OData query filter the azure mobile app .Net backend uses in the tablecontrollers again only the serialized string property gets returned to the client (because OData/LINQ does not know of the other property).
Another option was making a complex type out of DtoGoogleRoute with Entity Framework - this works fine but not with AutoMapper because AutoMapper can't handle complex types.
For now I'm working with a custom API controller and this works. But it would be better to use the tablecontrollers because they support offline sync.
I can't imagine such a simple thing (at least I thought it was a simple thing) can't be done or is so hard to do. But maybe the problem is all the components (tablecontroller, OData, LINQ, EF, AutoMapper) involved.
I would really be thankful if someone could help.
[EDIT]: I think the fact that it works with a normal api controller and not with a tablecontroller has something to do with OData. I tried putting the same code in a tablecontroller method and in an API controller method. when calling the API controller method I can see on the server that it just calls this function and returns all the right properties to the client (checked with fiddler). But when calling the tablecontroller method the tablecontroller method "rewrites" the URL to a OData URL --> I think this is because of some of the EnableQuery or other OData attributes. Because here (although not AutoMapper but it seems like a similar project from Microsoft) it says that the EnableQuery attribute is called twice - also when the request leaves the server. And I think it cuts of the GoogleRoute property because it does not know about this property in the OData metadata or something like that.
You can achieve it like this -
internal class RouteToDtoConverter : TypeConverter<Route, DtoRoute>
{
protected override DtoRoute ConvertCore(Route source)
{
return new DtoRoute
{
Id = source.Id,
GoogleRoute = JsonConvert.DeserializeObject<DtoGoogleRoute>(source.SerializedGoogleRoute)
};
}
}
internal class DtoToRouteConverter : TypeConverter<DtoRoute, Route>
{
protected override Route ConvertCore(DtoRoute source)
{
return new Route
{
Id = source.Id,
SerializedGoogleRoute = JsonConvert.SerializeObject(source.GoogleRoute)
};
}
}
public class Route
{
public string Id { get; set; }
public string SerializedGoogleRoute { get; set; }
}
public class DtoRoute
{
public string Id { get; set; }
public DtoGoogleRoute GoogleRoute { get; set; }
}
public class DtoGoogleRoute
{
public int MyProperty { get; set; }
public int MyProperty2 { get; set; }
}
AutoMapper.Mapper.CreateMap<Route, DtoRoute>()
.ConvertUsing(new RouteToDtoConverter());
AutoMapper.Mapper.CreateMap<DtoRoute, Route>()
.ConvertUsing(new DtoToRouteConverter());
var res = Mapper.Map<DtoRoute>(new Route
{
Id = "101",
SerializedGoogleRoute = "{'MyProperty':'90','MyProperty2':'09'}"
});
var org = Mapper.Map<Route>(res); //pass
When using a FindOne() using MongoDB and C#, is there a way to ignore fields not found in the object?
EG, example model.
public class UserModel
{
public ObjectId id { get; set; }
public string Email { get; set; }
}
Now we also store a password in the MongoDB collection, but do not want to bind it to out object above. When we do a Get like so,
var query = Query<UserModel>.EQ(e => e.Email, model.Email);
var entity = usersCollection.FindOne(query);
We get the following error
Element 'Password' does not match any field or property of class
Is there anyway to tell Mongo to ignore fields it cant match with the models?
Yes. Just decorate your UserModel class with the BsonIgnoreExtraElements attribute:
[BsonIgnoreExtraElements]
public class UserModel
{
public ObjectId id { get; set; }
public string Email { get; set; }
}
As the name suggests, the driver would ignore any extra fields instead of throwing an exception. More information here - Ignoring Extra Elements.
Yet Another possible solution, is to register a convention for this.
This way, we do not have to annotate all classes with [BsonIgnoreExtraElements].
Somewhere when creating the mongo client, setup the following:
var pack = new ConventionPack();
pack.Add(new IgnoreExtraElementsConvention(true));
ConventionRegistry.Register("My Solution Conventions", pack, t => true);
Yes. Another way (instead of editing you model class) is to use RegisterClassMap with SetIgnoreExtraElements.
In your case just add this code when you initialize your driver:
BsonClassMap.RegisterClassMap<UserModel>(cm =>
{
cm.AutoMap();
cm.SetIgnoreExtraElements(true);
});
You can read more about ignoring extra elements using class mapping here - Ignoring Extra Elements.