I'm struggling to find a way to catch an exception thrown by a model property (actually by its type struct), which must be bound to a POST request body data.
I have a general scenario where I need to treat very specific data types, so I'm using structs to validate them accordingly each case.
Despite of the following codes are just drafts, all suggestions are very welcome!
So the following is an example of a Controller:
[ApiController]
[TypeFilter(typeof(CustomExceptionFilter))]
public class OrdersController : ControllerBase
{
public OrdersController(ILogger<OrdersController> logger, IDataAccess dataAccess)
{
_dataAccess = dataAccess;
_logger = logger;
}
private readonly IDataAccess _dataAccess;
private readonly ILogger<OrdersController> _logger;
[EnableCors]
[Route("api/[controller]/Sales")]
[HttpPost]
public async Task<ActionResult> PostSaleAsync(
[FromBody] SaleOrder saleOrder)
{
try
{
Guid saleOrderId = Guid.NewGuid();
saleOrder.SaleOrderId = saleOrderId;
foreach (SaleOrderItem item in saleOrder.items)
item.SaleOrderId = saleOrderId;
OrderQuery query = new OrderQuery(_dataAccess);
await query.SaveAsync(saleOrder);
_dataAccess.Commit();
var response = new
{
Error = false,
Message = "OK",
Data = new
{
SaleOrderId = saleOrderId
}
};
return Ok(response);
}
catch (DataAccessException)
{
_dataAccess.Rollback();
//[...]
}
//[...]
}
}
and an example of a model, Order, and a struct, StockItemSerialNumber:
public class SaleOrder : Order
{
public Guid SaleOrderId { get => OrderId; set => OrderId = value; }
public Guid CustomerId { get => StakeholderId; set => StakeholderId = value; }
public Guid? SellerId { get; set; }
public SaleModelType SaleModelType { get; set; }
public SaleOrderItem[] items { get; set; }
}
public class SaleOrderItem : OrderItem
{
public Guid SaleOrderId { get; set; }
public StockItemSerialNumber StockItemSerialNumber { get; set; }
//[JsonConverter(typeof(StockItemSerialNumberJsonConverter))]
//public StockItemSerialNumber? StockItemSerialNumber { get; set; }
}
public struct StockItemSerialNumber
{
public StockItemSerialNumber(string value)
{
try
{
if ((value.Length != 68) || Regex.IsMatch(value, #"[^\w]"))
throw new ArgumentOutOfRangeException("StockItemSerialNumber");
_value = value;
}
catch(RegexMatchTimeoutException)
{
throw new ArgumentOutOfRangeException("StockItemSerialNumber");
}
}
private string _value;
public static implicit operator string(StockItemSerialNumber value) => value._value;
public override string ToString() => _value;
}
I would like to catch ArgumentOutOfRangeException thrown by StockItemSerialNumber struct and then return a response message informing a custom error accordingly.
Since this exception is not catch by the try...catch block from Controller, I've tried to build a class that extends IExceptionFilter and add as a filter:
public class CustomExceptionFilter : IExceptionFilter
{
private readonly IWebHostEnvironment _hostingEnvironment;
private readonly IModelMetadataProvider _modelMetadataProvider;
public CustomExceptionFilter(
IWebHostEnvironment hostingEnvironment,
IModelMetadataProvider modelMetadataProvider)
{
_hostingEnvironment = hostingEnvironment;
_modelMetadataProvider = modelMetadataProvider;
}
public void OnException(ExceptionContext context)
{
context.Result = new BadRequestObjectResult(new {
Error = false,
Message = $"OPS! Something bad happened, Harry :( [{context.Exception}]."
});
}
}
Startup.cs :
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(options =>
// [...]
});
services.AddTransient<IDataAccess>(_ => new DataAccess(Config.DBCredentials));
services.AddControllers(options => options.Filters.Add(typeof(CustomExceptionFilter)));
}
But that approach also doesn't work, I mean, the ArgumentOutOfRangeException remains not being catch and crashes the execution during a POST request.
Finally, here's an example of a JSON with request data:
{
"CustomerId":"fb2b0555-6d32-404b-b2f0-50032a7e0f59",
"SellerId":null,
"items": [
{
"StockItemSerialNumber":"22B6E75510AB459B8DB2874F20C722B6F3DC19C6E474337D5F73BB87699E9A1001"
}, // Invalid
{
"StockItemSerialNumber":"022B6E755122B659B8DB2874F20C780030F3DC19C6E47465AS1673BB87699E9A1001"
} // Valid
]
}
So I appreciate any help or suggestion! Thanks!
Related
I am using ASP.NET Core Web API. I have these models:
public abstract class EntityBase
{
[Key]
public int Id { get; set; }
}
public class Mandate : EntityBase
{
public int? MerchantId { get; set; }
public DateTime DueDate { get; set; }
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
[ForeignKey("MerchantId")]
public virtual Merchant Merchant { get; set; }
}
Model: Mandate
ViewModel (Dto):
public class MandateGetDto
{
public int? MerchantId { get; set; }
public DateTime DueDate { get; set; }
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
}
IBaseRepository:
public interface IBaseRepository<T> where T : BaseEntity
{
Task<IEnumerable<T>> GetAll();
bool EntityExists(long id);
}
BaseRepository:
public class BaseRepository<T> : IBaseRepository<T> where T : AuditableBaseEntity
{
private readonly DDMDbContext _context;
private DbSet<T> _entities;
public BaseRepository(DDMDbContext context)
{
_context = context;
_entities = context.Set<T>();
}
public async Task<IEnumerable<T>> GetAll()
{
var list = await _entities.ToListAsync();
return list;
}
public bool EntityExists(long id)
{
return _entities.Any(x => x.Id == id);
}
}
EntityMapper:
public MandateGetDto FromMandateToMandateGetDto(Mandate mandate)
{
MandateGetDto mandateDto = new MandateGetDto();
mandateDto.MerchantId = mandate.MerchantId;
mandateDto.DueDate = mandate.DueDate;
mandateDto.StartDate = mandate.StartDate;
mandateDto.EndDate = mandate.EndDate;
return mandateDto;
}
UnitOfWork:
public class UnitOfWork : IUnitOfWork
{
private readonly DContext _context;
public UnitOfWork(DContext context)
{
_context = context;
}
#region Mandate
private readonly IBaseRepository<Mandate> _mandateRepository;
public IBaseRepository<Mandate> MandateRepository => _mandateRepository ?? new BaseRepository<Mandate>(_context);
# endregion
public void Dispose()
{
if (_context != null)
{
_context.Dispose();
}
}
}
Below is the code I have written for the Mandate service which retrieves all the records for the mandates.
MandateService:
public async Task<ResponsePagination<GenericPagination<MandateGetDto>>> GetAll(int page, int sizeByPage)
{
string nextRoute = null, previousRoute = null;
IEnumerable<Mandate> data = await _unitOfWork.MandateRepository.GetAll();
var mapper = new EntityMapper();
var mandatesDto = data.Select(m => mapper.FromMandateToMandateGetDto(m)).ToList();
GenericPagination<MandateGetDto> objGenericPagination = GenericPagination<MandateGetDto>.Create(mandatesDto, page, sizeByPage);
ResponsePagination<GenericPagination<MandateGetDto>> response = new ResponsePagination<GenericPagination<MandateGetDto>>(objGenericPagination);
response.CurrentPage = objGenericPagination.CurrentPage;
response.HasNextPage = objGenericPagination.HasNextPage;
response.HasPreviousPage = objGenericPagination.HasPreviousPage;
response.PageSize = objGenericPagination.PageSize;
response.TotalPages = objGenericPagination.TotalPages;
response.TotalRecords = objGenericPagination.TotalRecords;
response.Data = objGenericPagination;
if (response.HasNextPage)
{
nextRoute = $"/mandates?page={(page + 1)}";
response.NextPageUrl = _uriPaginationService.GetPaginationUri(page, nextRoute).ToString();
}
else
{
response.NextPageUrl = null;
}
if (response.HasPreviousPage)
{
previousRoute = $"/mandates?page={(page - 1)}";
response.PreviousPageUrl = _uriPaginationService.GetPaginationUri(page, previousRoute).ToString();
}
else
{
response.PreviousPageUrl = null;
}
return response;
}
public async Task<IEnumerable<Mandate>> GetMandates()
{
return await _unitOfWork.MandateRepository.GetAll();
}
startup.cs:
services.AddTransient<IMandateService, MandateService>();
Controller:
[Produces("application/json")]
[ApiController]
[ApiVersion("1.0")]
public class AdminController : ControllerBase
{
private readonly IMerchantService _merchantService;
private readonly DDMDbContext _context;
public AdminController(DDMDbContext context, IMerchantService merchantService)
{
_merchantService = merchantService;
_context = context;
}
[HttpGet("mandates")]
[Authorize]
public async Task<ResponsePagination<GenericPagination<MandateGetDto>>> GetAllMyMandates(int page, int sizeByPage)
{
return await _mandateService.GetAll(page, sizeByPage);
}
}
When I used POSTMAN for the Get Request, I got this response:
{
"current_page": 0,
"page_size": 0,
"total_pages": -2147483648,
"total_records": 1,
"has_next_page": false,
"has_previous_page": false,
"data": []
}
Data is blank while total record is 1.
How do I resolve this?
Thanks
You have not shown us the principal part of your code, which is the controller
Controller is the reason why your web API even works. So, if you have misconfigured something, it should be present in the controller first or then somewhere else.
Show us the code for the controller.
Edit
I am not sure if DbSet is appropriate for the usage, but using the context object works for me.
You should use
_context.MandateOrWhateverElseItIs.ToListAsync();
instead of using
_entities.ToListAsync();
Using the DbSet can be an issue, I recommend using the context. My application works fruitfully with context.
Edit
This is your code in BaseRepository
public class BaseRepository<T> : IBaseRepository<T> where T : AuditableBaseEntity
{
private readonly DDMDbContext _context;
private DbSet<T> _entities;
public BaseRepository(DDMDbContext context)
{
_context = context;
_entities = context.Set<T>();
}
public async Task<IEnumerable<T>> GetAll()
{
var list = await _entities.ToListAsync();
return list;
}
public bool EntityExists(long id)
{
return _entities.Any(x => x.Id == id);
}
}
What you should change it to is
public class BaseRepository<T> : IBaseRepository<T> where T : AuditableBaseEntity
{
private readonly DDMDbContext _context;
private DbSet<T> _entities;
public BaseRepository(DDMDbContext context)
{
_context = context;
_entities = context.Set<T>();
}
public async Task<IEnumerable<T>> GetAll()
{
//Changes are here
var list = await _context.EntitiesOrWhateverElseItIs.ToListAsync(); //I don't know what the option is, you can look it inside the autocomplete. Be sure to change EntitiesOrWhateverElseItIs with the option you see in autocomplete menu
return list;
}
public bool EntityExists(long id)
{
return _entities.Any(x => x.Id == id);
}
}
In my ASP.NER Core Web API Project, I have this code:
public interface IUriPaginationService
{
public Uri GetPaginationUri(int page, string actionUrl);
}
public class UriPaginationService : IUriPaginationService
{
private readonly string _baseUri;
public UriPaginationService(string baseUri)
{
_baseUri = baseUri;
}
public Uri GetPaginationUri(int page, string actionUrl)
{
string baseUrl = $"{_baseUri}{actionUrl}";
return new Uri(baseUrl);
}
}
public abstract class AuditableBaseEntity
{
public AuditableBaseEntity()
{
CreatedDate = DateTime.Now;
}
[Key]
public virtual long Id { get; set; }
[JsonIgnore]
public bool IsDeleted { get; set; }
[DataType(DataType.DateTime)]
public DateTime? CreatedDate { get; set; }
public long? LastUpdatedBy { get; set; }
}
Repositories:
public interface IBaseRepository<T> where T : AuditableBaseEntity
{
Task<IEnumerable<T>> GetAll();
Task<T> GetById(long id);
bool EntityExists(long id);
}
public class BaseRepository<T> : IBaseRepository<T> where T : AuditableBaseEntity
{
private readonly DDMDbContext _context;
private DbSet<T> _entities;
public BaseRepository(DDMDbContext context)
{
_context = context;
_entities = context.Set<T>();
}
public async Task<IEnumerable<T>> GetAll()
{
var list = await _entities.Where(x => x.IsDeleted == false).ToListAsync();
return list;
}
public async Task<T> GetById(long id)
{
return await _entities.FindAsync(id);
}
public bool EntityExists(long id)
{
return _entities.Any(x => x.Id == id && x.IsDeleted == false);
}
}
UnitOfWork:
public interface IUnitOfWork : IDisposable
{
IBaseRepository<Merchant> MerchantRepository { get; }
}
public class UnitOfWork : IUnitOfWork
{
private readonly DDMDbContext _context;
public UnitOfWork(DDMDbContext context)
{
_context = context;
}
private readonly IBaseRepository<Merchant> _merchantRepository;
public IBaseRepository<Merchant> MerchantRepository => _merchantRepository ?? new BaseRepository<Merchant>(_context);
public void Dispose()
{
if (_context != null)
{
_context.Dispose();
}
}
}
Services:
public interface IMerchantService
{
public Task<IEnumerable<Merchant>> GetMerchants();
public bool EntityExists(long id);
public Task<ResponsePagination<GenericPagination<MerchantGetDto>>> GetAll(int page, int sizeByPage);
}
public class MerchantService : IMerchantService
{
private readonly IUnitOfWork _unitOfWork;
private readonly IUriPaginationService _uriPaginationService;
private readonly DDMDbContext _context;
public MerchantService(IUnitOfWork unitOfWork, IUriPaginationService uriPaginationService, DDMDbContext context)
{
_unitOfWork = unitOfWork;
_uriPaginationService = uriPaginationService;
_context = context;
}
public async Task<IEnumerable<Merchant>> GetMerchants()
{
return await _unitOfWork.MerchantRepository.GetAll();
}
public async Task<ResponsePagination<GenericPagination<MerchantGetDto>>> GetAll(int page, int sizeByPage)
{
string nextRoute = null, previousRoute = null;
IEnumerable<Merchant> data = await _unitOfWork.MerchantRepository.GetAll();
var mapper = new EntityMapper();
var merchantsDto = data.Select(m => mapper.FromMerchantToMerchantGetDto(m)).ToList();
GenericPagination<MerchantGetDto> objGenericPagination = GenericPagination<MerchantGetDto>.Create(merchantsDto, page, sizeByPage);
ResponsePagination<GenericPagination<MerchantGetDto>> response = new ResponsePagination<GenericPagination<MerchantGetDto>>(objGenericPagination);
response.CurrentPage = objGenericPagination.CurrentPage;
response.HasNextPage = objGenericPagination.HasNextPage;
response.HasPreviousPage = objGenericPagination.HasPreviousPage;
response.PageSize = objGenericPagination.PageSize;
response.TotalPages = objGenericPagination.TotalPages;
response.TotalRecords = objGenericPagination.TotalRecords;
response.Data = objGenericPagination;
if (response.HasNextPage)
{
nextRoute = $"/merchants?page={(page + 1)}";
response.NextPageUrl = _uriPaginationService.GetPaginationUri(page, nextRoute).ToString();
}
else
{
response.NextPageUrl = null;
}
if (response.HasPreviousPage)
{
previousRoute = $"/merchants?page={(page - 1)}";
response.PreviousPageUrl = _uriPaginationService.GetPaginationUri(page, previousRoute).ToString();
}
else
{
response.PreviousPageUrl = null;
}
return response;
}
public bool EntityExists(long id)
{
return _unitOfWork.MerchantRepository.EntityExists(id);
}
public async Task<Merchant> GetById(long id)
{
return await _unitOfWork.MerchantRepository.GetById(id);
}
}
Controller:
[Produces("application/json")]
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/[controller]")]
public class AdminController : ControllerBase
{
private readonly IMerchantService _merchantService;
public AdminController(MerchantService merchantService)
{
_merchantService = merchantService;
}
[HttpGet("merchants")]
public async Task<ResponsePagination<GenericPagination<MerchantGetDto>>> GetAll(int page, int sizeByPage)
{
return await _merchantService.GetAll(page, sizeByPage);
}
}
startup:
public void ConfigureServices(IServiceCollection services)
{
// services.AddMvc();
services.AddControllers();
services.AddDb(Configuration);
services.AddJwtAuthentication(Configuration);
services.AddMvcCoreFramework(Configuration);
services.AddAppAuthorization(Configuration);
services.AddControllersWithViews();
services.AddRazorPages();
services.AddVersioning();
services.AddSwagger();
services.AddRouting(options => options.LowercaseUrls = true);
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddSingleton<IUriPaginationService>(provider =>
{
var accesor = provider.GetRequiredService<IHttpContextAccessor>();
var request = accesor.HttpContext.Request;
var absoluteUri = string.Concat(request.Scheme, "://", request.Host.ToUriComponent());
return new UriPaginationService(absoluteUri);
});
services.AddScoped(typeof(IBaseRepository<>), typeof(BaseRepository<>));
services.AddScoped<IUnitOfWork, UnitOfWork>();
services.AddTransient<IMerchantService, MerchantService>();
}
When I use this on postman get request:
https://localhost:44341/api/v1/merchant/mandates
I got this error:
System.InvalidOperationException: Unable to resolve service for type 'API.Infrastructure.Services.Concrete.MerchantService' while attempting to activate 'API.Web.Controllers.v1.MerchantController'.
at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.GetService(IServiceProvider sp, Type type, Type requiredBy, Boolean isDefaultParameterRequired)
How do I resolve this:
Thanks
The controller is dependent on the implementation (class)
public AdminController(MerchantService merchantService)
{
_merchantService = merchantService;
}
while you registered the abstraction (interface).
services.AddTransient<IMerchantService, MerchantService>();
Based on the registration it is most likely the controller was meant to be dependent on the interface (abstraction)
public AdminController(IMerchantService merchantService) //<-- CHANGED TO INTERFACE
{
_merchantService = merchantService;
}
Updating the controller constructor to be dependent on the registered abstraction will fix the shown exception.
in addition to Nkosi answer, the three injection service are listed below and their scope. three methods define the lifetime of the services
AddTransient
Transient lifetime services are created each time they are requested. This lifetime works best for lightweight, stateless services.
AddScoped
Scoped lifetime services are created once per request.
AddSingleton
Singleton lifetime services are created the first time they are requested (or when ConfigureServices is run if you specify an instance there) and then every subsequent request will use the same instance.
Brief explation can be found here
*Update:
Was able to follow this tutorial, and now the error I'm getting states:
"message": "No HTTP resource was found that matches the request URI 'http://localhost:0000/api/v1/Pets('dog')/Color'."
"message": "No routing convention was found to select an action for the OData path with template '~/entityset/key/unresolved'."
Any ideas?*
I am getting an error when trying to retrieve the Color in my Pet query using OData V4.
I'm having quite a bit of trouble, ideally I would use an expand on color (e.g. localhost:0000/api/v1/Pets('dog')?$expand=Colors)
The JSON I need returned is something like:
[
{
"_Key": "1",
"animalname": "dog",
"furcolorname": "black,white",
"color": {
"_Key": "1",
"colorname": "black"
},
{
"_Key": "2",
"colorname": "white"
}
}
]
Maybe I'm on the completely wrong path, either way any input is appreciated!
If I query localhost:0000/api/v1/Pets('dog') :
…
"Message\": \"Invalid column name 'Pet__Key'.\"
…
If I query localhost:0000/api/v1/Pets('dog')?$exand=Colors :
…
"The query specified in the URI is not valid. Could not find a property named 'Colors' on type 'PetShop.Odata.Models.Pet'."
…
Pet.cs model:
namespace PetShop.Odata.Models
{
[Table("pet")]
public class Pet
{
[Key]
public string _Key { get; set; }
public string AnimalName { get; set; }
[ForeignKey("Color")]
public string FurColorName { get; set; }
public virtual Color Color { get; set; }
public virtual ICollection<Color> Colors { get; set; }
}
}
Color.cs model:
namespace PetShop.Odata.Models
{
[Table("color")]
public class Color
{
public Color()
{
new HashSet<Pet>();
}
[Key]
public string _Key { get; set; }
public string ColorName { get; set; }
}
}
PetsController.cs:
namespace PetShop.Odata.Controllers
{
[ApiVersion("1.0")]
[ODataRoutePrefix("Pets")]
[ApiControllerMetricReport]
public class PetsController : ODataController
{
private readonly MyContext context = new MyContext ();
[HttpGet]
[ODataRoute]
[EnableQuery]
[ResponseType(typeof(List<Pet>))]
public IHttpActionResult Get()
{
return Ok(context.Pets.AsQueryable());
}
[EnableQuery]
public IQueryable<Color> Get ([FromODataUri] string key)
{
return context.Pets.Where(m => m._Key == key).SelectMany(a => a.Colors);
}
protected override void Dispose(bool disposing)
{
context.Dispose();
base.Dispose(disposing);
}
}
}
ColorsController.cs:
namespace PetShop.Odata.Controllers
{
[ApiVersion("1.0")]
[ODataRoutePrefix("Colors")]
[ApiControllerMetricReport]
public class ColorsController : ODataController
{
private readonly MyContext context = new MyContext ();
[HttpGet]
[ODataRoute]
[EnableQuery]
[ResponseType(typeof(List<Color>))]
public IHttpActionResult Get()
{
return Ok(context.Colors.AsQueryable());
}
protected override void Dispose(bool disposing)
{
context.Dispose();
base.Dispose(disposing);
}
}
}
PetConfig.cs:
namespace PetShop.Odata.Configuration
{ public class PetModelConfiguration : IModelConfiguration
{
public void Apply(ODataModelBuilder builder, ApiVersion apiVersion)
{
builder.EntitySet<Pet>("Pets");
}
}
}
ColorConfig.cs:
namespace PetShop.Odata.Configuration
{ public class ColorModelConfiguration : IModelConfiguration
{
public void Apply(ODataModelBuilder builder, ApiVersion apiVersion)
{
builder.EntitySet<Color>("Colors");
}
}
}
MyContext.cs:
…
public DbSet<Pet> Pets { get; set; }
public DbSet<Color> Colors { get; set; }
…
Setup.cs:
…
public static HttpServer CreateHttpServer()
{
var httpConfig = new HttpConfiguration();
var webApiServer = new HttpServer(httpConfig);
httpConfig.AddApiVersioning(options => options.ReportApiVersions = true);
httpConfig.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
var modelBuilder = new VersionedODataModelBuilder(httpConfig)
{
ModelBuilderFactory = () => new ODataConventionModelBuilder().EnableLowerCamelCase(),
ModelConfigurations =
{
new PetConfig(),
new ColorConfig()
},
};
var models = modelBuilder.GetEdmModels();
httpConfig.Count().Filter().OrderBy().Expand().Select().MaxTop(null);
…
Assuming you are using default routing convention, your Get colors method in PetsController.cs does not match the expected OData route format.
Instead of the following:
[EnableQuery]
public IQueryable<Color> Get ([FromODataUri] string key)
{
return context.Pets.Where(m => m._Key == key).SelectMany(a => a.Colors);
}
You should try:
[EnableQuery]
public IQueryable<Color> GetColors ([FromODataUri] string key)
{
return context.Pets.Where(m => m._Key == key).SelectMany(a => a.Colors);
}
This definition would make the OData route: http://localhost:0000/api/v1/Pets('dog')/Colors
For more information on routing conventions see https://learn.microsoft.com/en-us/odata/webapi/built-in-routing-conventions.
A couple alternative approaches exist as well:
Register a different name to a custom nav property
Define a custom OData Function
I have a controller that expects a list of integers as input like:
[ValidateModel]
[HttpGet("get")]
public IActionResult GetController(int[] ints)
{
// Method body.
}
So, someone can call this route like "http://localhost:8080/get?ints=1,2,3" and Asp.Net Core automatically parses the string "1,2,3" into an array of integers.
However, the error message returned when say I call this route like "http://localhost:8080/get?ints=foo" is just the generic error "Invalid model". How can I set my own error message for this?
My ValidateModel is:
public class ValidateModelAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
context.Result = new ValidationFailureResult(context.ModelState);
}
}
}
And the ValidationFailureResult is:
public class ValidationFailureResult : ObjectResult
{
public ValidationFailureResult(ModelStateDictionary modelState)
: base(new ValidateModelResult(modelState))
{
this.StatusCode = StatusCodes.Status422UnprocessableEntity;
}
}
public class ValidateModelResult
{
private const string ValidationMessage = "Model state validation failed";
public ValidateModelResult(ModelStateDictionary modelState)
{
this.Message = ValidationMessage;
this.Errors = modelState.Keys
.SelectMany(key => modelState[key].Errors.Select(x =>
new ValidationError(key, x.ErrorMessage)))
.ToList();
}
[JsonConstructor]
private ValidateModelResult(
string message,
List<ValidationError> errors)
{
this.Message = message;
this.Errors = errors;
}
public string Message { get; }
public List<ValidationError> Errors { get; }
}
Using C# Mongo DB Driver version, 2.4.4 and I try to deserialize a complex class. It has been stored correctly in the MongoDB, but when being deserialized it is null.
This is my datamodel:
public abstract class Document
{
public DocumentId Id { get; private set; }
public DocumentProperty TestProperty { get; private set; }
protected Document() { }
public void SetId(string value)
{
if (string.IsNullOrEmpty(value)) throw new ArgumentNullException("value");
Id = new DocumentId(value);
}
public void AddProperty(DocumentProperty property)
{
if (property == null) throw new ArgumentNullException("property");
TestProperty = property;
}
}
public class DocumentId
{
public string Value { get; }
public DocumentId(string value)
{
Value = value;
}
}
public abstract class DocumentProperty
{
public string Value { get; }
protected DocumentProperty()
{
}
protected DocumentProperty(string value)
{
Value = value;
}
}
public class TitleDocumentProperty : DocumentProperty
{
public TitleDocumentProperty() { }
public TitleDocumentProperty(string value) : base(value)
{
}
}
This is my mapping code:
public class MongoClassMapper
{
public static void InitializeMap()
{
BsonClassMap.RegisterClassMap<DocumentProperty>(map =>
{
map.MapProperty(property => property.Value).SetDefaultValue("123");
map.SetIsRootClass(true);
});
BsonClassMap.RegisterClassMap<TitleDocumentProperty>(map =>
{
});
BsonClassMap.RegisterClassMap<Document>(map =>
{
map.MapProperty(document => document.Id);
map.MapProperty(document => document.TestProperty);
map.SetIsRootClass(true);
});
}
}
This is my methods for adding and retrieving data from Mongo:
public async Task<string> Add(Document document)
{
await _documents.InsertOneAsync(document);
return document.Id.Value;
}
public async Task<Document> Get(DocumentId id)
{
var mongoDocuments = await _documents.Find(document => document.Id == id)
.ToListAsync();
return mongoDocuments.SingleOrDefault();
}
This is the code that I use for testing:
private static MongoCache _cache;
static void Main(string[] args)
{
MongoClassMapper.InitializeMap();
_cache = new MongoCache(ConfigurationManager.ConnectionStrings["connName"].ConnectionString, "dbName");
string id = ItShouldCreateRecord().Result;
var doc = ItShouldGetRecord(id).Result;
}
private static Task<string> ItShouldCreateRecord()
{
//Arrange
Document document = new FakeDocument();
document.SetId(Guid.NewGuid().ToString());
document.AddProperty(new TitleDocumentProperty("this is a title"));
//Act
string id = _cache.Add(document).Result;
//Assert
//Assert.NotEmpty(id);
return Task.FromResult(id);
}
private static Task<Document> ItShouldGetRecord(string id)
{
//Arrange
//Act
return _cache.Get(new DocumentId(id));
//Assert
//Assert.NotNull(doc);
}
}
[BsonIgnoreExtraElements]
public class FakeDocument : Document
{
}
I expected that when I retrieve the document (using _cache.Get()) from the DB, that the property TestProperty would have an actual value. Currently, it is NULL.
The problem is the absence of a setter on your Value property which will cause MongoDB to not deserialize this property:
public abstract class DocumentProperty
{
public string Value { get; /* no setter*/ }
}
You can fix this by simply adding the setter (using any accessibility setting, even private works):
public abstract class DocumentProperty
{
public string Value { get; /* private if you want */ set; }
}
I have captured a JIRA issue and a pull request to get that fixed.