Store all dates as BsonDocuments - c#

I have a feeling this is possible but I can't seem to find it. I'd like to configure my mongo driver to make any DateTime object stored as a BsonDocument.
The mongo c# driver lets you set certain conventions globally so you don't need to annotate everything, is this also possible for date time options?
For example, I'd like to remove the following annotation:
[BsonDateTimeOptions(Representation = BsonType.Document)]
From all of my DateTime properties. Can anyone point me in the right direction?

When I tried to verify that the answer provided by devshorts worked I got a compile time error (because the Add method of ConventionPack, which is being called by the collection initializer syntax, expects an IConvention).
The suggested solution was almost right, and only a slight modification was needed:
ConventionRegistry.Register(
"dates as documents",
new ConventionPack
{
new DelegateMemberMapConvention("dates as documents", memberMap =>
{
if (memberMap .MemberType == typeof(DateTime))
{
memberMap .SetSerializationOptions(new DateTimeSerializationOptions(DateTimeKind.Utc, BsonType.Document));
}
}),
},
t => true);
If we needed to use this convention in more than one place we could package it up in a class, as so:
public class DateTimeSerializationOptionsConvention : ConventionBase, IMemberMapConvention
{
private readonly DateTimeKind _kind;
private readonly BsonType _representation;
public DateTimeSerializationOptionsConvention(DateTimeKind kind, BsonType representation)
{
_kind = kind;
_representation = representation;
}
public void Apply(BsonMemberMap memberMap)
{
if (memberMap.MemberType == typeof(DateTime))
{
memberMap.SetSerializationOptions(new DateTimeSerializationOptions(_kind, _representation));
}
}
}
And then use it like this:
ConventionRegistry.Register(
"dates as documents",
new ConventionPack
{
new DateTimeSerializationOptionsConvention(DateTimeKind.Utc, BsonType.Document)
},
t => true);

Long overdue, but the answer is to use a convention pack and set
ConventionRegistry.Register(
"Dates as utc documents",
new ConventionPack
{
new MemberSerializationOptionsConvention(typeof(DateTime), new DateTimeSerializationOptions(DateTimeKind.Utc, BsonType.Document)),
},
t => true);

Related

FluentAssertions Equivalency Comparison Behavior and IMemberInfo

I am using FluentAssertions (v6.2.0) to test API's that return table-like data. I want to change comparison behavior for one of the field, and tried to use method described in documentation.
orderDto.Should().BeEquivalentTo(order, options => options
.Using<DateTime>(ctx => ctx.Subject.Should().BeCloseTo(ctx.Expectation, 1.Seconds()))
.When(info => info.Name == "Date"));
The issue is that IMemberInfo class that When extension method is expecting doesn't have Name property, it has property called Path. Was Name replaced by Path and this is a typo in documentation, or do I need to import another namespace to use Name property?
From a quick look at the FluentAssertions source code, I'm seeing that the info argument is of type IObjectInfo and it has a Path property. A quick test with this code shows the Path property working as you would expect:
void Main()
{
var orderDto = new OrderDto { Date = DateTime.Now };
var order = new Order { Date = DateTime.Now };
//var order = new Order { Date = DateTime.Now.Subtract(TimeSpan.FromSeconds(2)) };
orderDto.Should().BeEquivalentTo(order, options => options
.Using<DateTime>(ctx => ctx.Subject.Should().BeCloseTo(ctx.Expectation, 1.Seconds()))
.When(info => info.Path == "Date"));
}
class OrderDto {
public DateTime Date { get; set; }
}
class Order
{
public DateTime Date { get; set; }
}
In fact, the FluentAssertions test for that code also uses path. See https://github.com/fluentassertions/fluentassertions/blob/master/Tests/FluentAssertions.Specs/Equivalency/ExtensibilityRelatedEquivalencySpecs.cs#L394
There is also an IMethodInfo interface with both Name and Path properties. However, that is used by the Include* and Exclude* methods.
So it appears to be a documentation bug.

Get version when using IPageRouteModelConvention

I sometime ago asked how to add some kind of localized url's, were IPageRouteModelConvention came into play in a, for me, perfect way.
With that I'm able to have routes in different languages/names.
If I use www.domain.com/nyheter (swedish) or www.domain.com/sistenytt (norwegian) I still only find, in RouteData, that the News route were used (RouteData.Values["page"]).
How do I get which version?
I know I can check/parse the context.Request.Path but am wondering if there is a built-in property that will give me it instead.
In startup
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2).AddRazorPagesOptions(options =>
{
options.Conventions.Add(new LocalizedPageRouteModelConvention(new LocalizationService(appsettings.Routes)));
});
appsettings.Routes is read from appsettings.json
"Routes": [
{
"Page": "/Pages/News.cshtml",
"Versions": [ "nyheter", "sistenytt" ]
},
and so on....
]
The class
public class LocalizedPageRouteModelConvention : IPageRouteModelConvention
{
private ILocalizationService _localizationService;
public LocalizedPageRouteModelConvention(ILocalizationService localizationService)
{
_localizationService = localizationService;
}
public void Apply(PageRouteModel model)
{
var route = _localizationService.LocalRoutes().FirstOrDefault(p => p.Page == model.RelativePath);
if (route != null)
{
foreach (var option in route.Versions)
{
model.Selectors.Add(new SelectorModel()
{
AttributeRouteModel = new AttributeRouteModel
{
Template = option
}
});
}
}
}
}
To retrieve a RouteData value, you can specify a token within the template for a route. For example, the route {version} would add a RouteData value of version that would be taken from the URL's first segment. In your example, you don't specify a token for version and so there will be no RouteData value for it, as you've described.
The solution for your specific problem is two-part:
Instead of using specific values when creating new SelectorModels, use a token as described above.
With this in place, you will now be able to access a version value from RouteData, but the new problem is that any value can be provided, whether or not it was specified in your configuration.
To solve the second problem, you can turn to IActionConstraint. Here's an implementation:
public class VersionConstraint : IActionConstraint
{
private readonly IEnumerable<string> allowedValues;
public VersionConstraint(IEnumerable<string> allowedValues)
{
this.allowedValues = allowedValues;
}
public int Order => 0;
public bool Accept(ActionConstraintContext ctx)
{
if (!ctx.RouteContext.RouteData.Values.TryGetValue("version", out var routeVersion))
return false;
return allowedValues.Contains((string)routeVersion);
}
}
VersionConstraint takes a list of allowed values (e.g. nyheter, sistenytt) and checks whether or not the version RouteData value matches. If it doesn't match, the "action" (it's really a page at this point) won't be a match and will end up with a 404.
With that implementation in place, you can update your implementation of LocalizedPageRouteModelConvention's Apply to look like this:
var route = _localizationService.LocalRoutes().FirstOrDefault(p => p.Page == model.RelativePath);
if (route != null)
{
model.Selectors.Add(new SelectorModel
{
AttributeRouteModel = new AttributeRouteModel
{
Template = "{version}"
},
ActionConstraints =
{
new VersionConstraint(route.Versions)
}
});
}
This implementation adds a single new SelectorModel that's set up with a Version RouteData value and is constrained to only allow the values specified in configuration.

How can I configure Automapper 4 to allow a null destination value

I'm having some problems working out how to get Automapper 4.2.1 to allow for a type mapping where the destination value might be null depending on the source value.
Older versions of Automapper allowed an AllowNullDestination flag to be set via the Mapper configuration but I can't find the equivalent recipe for the new version and the old mechanism of configuring via the static Mapper object seems to have been obsoleted.
I have tried the following without success:
Mapper.Configuration.AllowNullDestinationValues = true;
Mapper.AllowNullDestinationValues = true;
Mapper.Initialize(c=>c.AllowNullDestinationValues=true);
Here's a simple test case demonstrating the problem. This fails on the final line with an AutoMapperMappingException since the Substitute method is returning null. I would like both mappings to succeed.
I would prefer to avoid the use of .ForMember in the solution since in the real scenario I'm trying to address, the mapping between bool and 'object' (actually a custom class) should apply across the entire object tree.
Although there are several similar questions here on StackOverflow, I haven't found one that refers to a recent version of Automapper.
Thanks in advance for any suggestions
using AutoMapper;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace AutoMapperTest
{
[TestClass]
public class ExampleTest
{
[TestMethod]
public void NullDestinationCanBeMapped()
{
var mapper = new MapperConfiguration(configuration =>
{
configuration.CreateMap<Source, Target>();
//How should the following mapping be modified to pass the test?
configuration.CreateMap<bool, object>()
.Substitute(i => i ? null : new object());
}).CreateMapper();
var target1 = mapper.Map<Source, Target>(new Source {Member = false}); //succeeds
Assert.IsNotNull(target1.Member); //pass
var target2 = mapper.Map<Source, Target>(new Source {Member = true}); //fails to map with exception
Assert.IsNull(target2.Member); //not reached
}
}
public class Source
{
public bool Member { get; set; }
}
public class Target
{
public object Member { get; set; }
}
}
Instead of using Substitute, use ConvertUsing...
configuration.CreateMap<bool, MyClass>()
.ConvertUsing(i => i ? null : new object());

How can I specify the constructor to use for MongoDB deserialization without using attributes (C#)

Note: I'm using the MongoDB C# Driver 2.0
I would like to replicate the behaviour of the BsonConstructor attribute but using the BsonClassMap API.
Something like this:
BsonClassMap.RegisterClassMap<Person>(cm =>
{
cm.AutoMap();
cm.MapCreator(p => new Person(p.FirstName, p.LastName));
});
but without having to specify each argument.
The reason I want to do it this way is that I don't want to "pollute" my domain model with implementation concerns.
I have found this (SetCreator)
BsonClassMap.RegisterClassMap<Person>(cm =>
{
cm.AutoMap();
cm.SetCreator(what goes here?);
});
but I don't know how to use the SetCreator function, and if it does what I think it does...
I achived the same result using the conventions instead of BsonClassMap
Here is an example (reading (serialization) from read only public properties and writing (deserialization) to the constructor)
public class MongoMappingConvention : IClassMapConvention
{
public string Name
{
get { return "No use for a name"; }
}
public void Apply(BsonClassMap classMap)
{
var nonPublicCtors = classMap.ClassType.GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance);
var longestCtor = nonPublicCtors.OrderByDescending(ctor => ctor.GetParameters().Length).FirstOrDefault();
classMap.MapConstructor(longestCtor);
var publicProperties = classMap.ClassType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.CanRead);
foreach (var publicProperty in publicProperties)
{
classMap.MapMember(publicProperty);
}
}
}
Old question, I know, but something like this worked for me with MongoDB driver 2.10.4:
var mapper = new BsonClassMap(type);
mapper.AutoMap();
var constructorInfo = type.GetConstructor(...); // Find the constructor you want to use
mapper.MapConstructor(constructorInfo, new[] {"FirstName", "LastName"});
Note that the array passed to MapConstructor has the property names, not the constructor argument names. As I understand, it goes by the order of constructor arguments, but they may have different names, e.g.
public Person(string givenName, string surname)
{
FirstName = givenName;
LastName = surname;
}

How to force mongo to store members in lowercase?

I have a collection of BsonDocuments, for example:
MongoCollection<BsonDocument> products;
When I do inserts into the collection, I want the member name to always be lowercase. After reading the documentation, it appears that ConventionPack is the way to go. So, I've defined one like this:
public class LowerCaseElementNameConvention : IMemberMapConvention
{
public void Apply(BsonMemberMap memberMap)
{
memberMap.SetElementName(memberMap.MemberName.ToLower());
}
public string Name
{
get { throw new NotImplementedException(); }
}
}
And right after I get my collection instance I register the convention like this:
var pack = new ConventionPack();
pack.Add(new LowerCaseElementNameConvention());
ConventionRegistry.Register(
"Product Catalog Conventions",
pack,
t => true);
Unfortunately, this has zero effect on what is stored in my collection. I debugged it and found that the Apply method is never called.
What do I need to do differently to get my convention to work?
In order to use IMemeberMapConvention, you must make sure to declare your conventions before the mapping process takes place. Or optionally drop existing mappings and create new ones.
For example, the following is the correct order to apply a convention:
// first: create the conventions
var myConventions = new ConventionPack();
myConventions.Add(new FooConvention());
ConventionRegistry.Register(
"My Custom Conventions",
myConventions,
t => true);
// only then apply the mapping
BsonClassMap.RegisterClassMap<Foo>(cm =>
{
cm.AutoMap();
});
// finally save
collection.RemoveAll();
collection.InsertBatch(new Foo[]
{
new Foo() {Text = "Hello world!"},
new Foo() {Text = "Hello world!"},
new Foo() {Text = "Hello world!"},
});
Here's how this sample convention was defined:
public class FooConvention : IMemberMapConvention
private string _name = "FooConvention";
#region Implementation of IConvention
public string Name
{
get { return _name; }
private set { _name = value; }
}
public void Apply(BsonMemberMap memberMap)
{
if (memberMap.MemberName == "Text")
{
memberMap.SetElementName("NotText");
}
}
#endregion
}
These are the results that came out when I ran this sample. You could see the Text property ended up being saved as "NotText":
If I understand correctly, conventions are only applied when auto-mapping. If you have a classmap, you need to explicitly call AutoMap() to use conventions. Then you can modify the automapping, e.g.:
public class MyThingyMap : BsonClassMap<MyThingy>
{
public MyThingyMap()
{
// Use conventions to auto-map
AutoMap();
// Customize automapping for specific cases
GetMemberMap(x => x.SomeProperty).SetElementName("sp");
UnmapMember(x => x.SomePropertyToIgnore);
}
}
If you don't include a class map, I think the default is to just use automapping, in which case your convention should apply. Make sure you're registering the convention before calling GetCollection<T>.
You can define ConventionPack which is also part of their official document on Serialization. Like below which stores are property names as camel case. You can place while Configuring services/repositories
Official link
https://mongodb.github.io/mongo-csharp-driver/1.11/serialization/[Mongo Db Serialization C#]1
// For MongoDb Conventions
var pack = new ConventionPack
{
new CamelCaseElementNameConvention()
};
ConventionRegistry.Register(nameof(CamelCaseElementNameConvention), pack, _ => true);

Categories