How can I get independent JSON reference resolution between requests in ASP.NET Core? - asp.net-core

I am attempting to add a custom IReferenceResolver implementation to an ASP.NET Core 2.2 MVC API application to reduce data in a JSON payload. However the reference resolutions are being shared between different requests.
It appears that a single instance of the ReferenceResolver is shared between requests. I want the references to be resolved independent of other requests, as different users of my won't have this shared reference context.
This is my ConfigureServices method in Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.AddJsonOptions(opts =>
{
opts.SerializerSettings.ReferenceResolverProvider = () => new ThingReferenceResolver();
});
}
This is my controller implementation along with my custom IReferenceResolver
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
[HttpGet("")]
public ActionResult<ThingsResponse> Get()
{
return new ThingsResponse
{
MainThing = new Thing { Id = "foo" },
Things = new List<Thing>
{
new Thing { Id = "foo" },
new Thing { Id = "bar" }
}
};
}
}
public class ThingsResponse
{
[JsonProperty(IsReference = true)]
public Thing MainThing { get; set; }
[JsonProperty(ItemIsReference = true)]
public List<Thing> Things { get; set; }
}
public class Thing
{
public string Id { get; set; }
}
public class ThingReferenceResolver : IReferenceResolver
{
private readonly IDictionary<string, Thing> _idReference = new Dictionary<string, Thing>();
public void AddReference(object context, string reference, object value)
{
_idReference[reference] = (Thing)value;
}
public string GetReference(object context, object value)
{
var thing = (Thing)value;
_idReference[thing.Id] = thing;
return thing.Id.ToString();
}
public bool IsReferenced(object context, object value)
{
var thing = (Thing)value;
return _idReference.ContainsKey(thing.Id);
}
public object ResolveReference(object context, string reference)
{
_idReference.TryGetValue(reference, out Thing thing);
return thing;
}
}
On my first request I get the following response:
{
"mainThing": {
"$id": "foo",
"id": "foo"
},
"things": [
{
"$ref": "foo"
},
{
"$id": "bar",
"id": "bar"
}
]
}
On my second request I get the following response:
{
"mainThing": {
"$ref": "foo"
},
"things": [
{
"$ref": "foo"
},
{
"$ref": "bar"
}
]
}
I want my second request to look like my first request i.e. repeatable outputs.

You get different results for the second request because MVC creates one serializer and caches it, which then caches references if you have reference tracking on like you do.
I think if you return a JsonResult with new serializer settings in each result then you won't have this problem:
new JsonResult(yourData, new JsonSerializerSettings { ... })

One option I have come up with is to bypass configuring the JSON serializer that MVC provides and create my own for the request in question.
[HttpGet("")]
public ActionResult<ThingsResponse> Get()
{
var serializerSettings = JsonSerializerSettingsProvider.CreateSerializerSettings();
serializerSettings.ReferenceResolverProvider = () => new ThingReferenceResolver();
return new JsonResult(
new ThingsResponse
{
MainThing = new Thing { Id = "foo" },
Things = new List<Thing>
{
new Thing { Id = "foo" },
new Thing { Id = "bar" }
}
},
serializerSettings
);
}
In my specific scenario this is OK, because I do not have many endpoints that this would need to be configured for.
This means the following code from the example Startup.cs is not needed to solve my problem (as I define it per request)
.AddJsonOptions(opts =>
{
opts.SerializerSettings.ReferenceResolverProvider = () => new ThingReferenceResolver();
});
I think I will settle on this option for my circumstances, but would love to know if there are better ways to implement it.

Related

How to send Collection of abstract types with ActionResult

I am trying to send back a collection of abstract types in a Controller using ActionResult.I do not know how to tell the serializer to also include derived type(s) specific properties:
public abstract class Base
{
public int Id{get;set;}
}
public class D1:Base
{
public string D1Value{get;set;}
}
public class D2:Base
{
public bool IsD2Value{get;set;}
}
public async Task<ActionResult<IEnumerable<Base>>> GetAll()
{
var collection=new []{ new D1 { Id=1, D1Value="hi"} ,new D2 {Id=2, IsD2Value=true}};
return StatusCode(200,collection);
}
How can i reach this result in a easy and elegant way.I have checked the JsonSerializer options but in my case i am not the one that is doing the serialization.
What i get
[{ "Id":1} , { "Id":2 }]
What i want
[{ "Id":1,"D1Value":"hi" } , { "Id":2 , "IsD2Value":true }]
Try the following code:
public async Task<ActionResult<IEnumerable<Base>>> GetAll()
{
var collection = new List<object>()
{
new D1 { Id = 1, D1Value = "hi" },
new D2 { Id = 2, IsD2Value = true }
};
return StatusCode(200, collection);
}
Here is the test result:
Use new ArrayList() instead of List.

How to make "One or more validation errors occurred" raise an exception?

I'm running a WebAPI on Core 3.1 and one of my endpoints excepts JSON with a model that has fields with [Required] attribute like so:
public class Vendor
{
public int ID { get; set; }
[Required(ErrorMessage = "UID is required")] <<<------ required attribute
public string UID { get; set; }
}
When i call this endpoint without setting UID I get the following output as expected:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "|7ced8b82-4aa34d65daa99a12.",
"errors": {
"Vendor.UID": [
"UID is required"
]
}
}
Altho this output is pretty informative and clear it is not consistent with other error outputs that my API produces by means of ExceptionFilter. Is there any way this errors can be routed to the exception filter as well?
Here is my Startup.cs:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
Common.Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_3_0);
services.AddMvc().AddXmlDataContractSerializerFormatters();
services.AddMvc().AddMvcOptions(options =>
{
options.EnableEndpointRouting = false;
});
services.AddMvcCore(options => options.OutputFormatters.Add(new XmlSerializerOutputFormatter()));
services.AddOptions();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger<Startup> logger)
{
//Middleware for exception filtering
app.UseMiddleware<ErrorHandlingMiddleware>(new ErrorHandlingMiddlewareOptions { logger = logger });
app.UseStaticFiles();
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
app.UseMvc(routes =>
{
routes.MapRoute("EndpointNotFound", "{*url}", new { controller = "Error", action = "EndpointNotFound" });
});
}
}
You can add filter in your mvc service or controller service
this filter return badrequest
services.AddControllers(option =>
{
option.Filters.Add<ValidationFilter>();
});
to create your filter you can add this class
also you can customize this filter to what ever you want
public class ValidationFilter : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
//before contrller
if(!context.ModelState.IsValid)
{
var errorsInModelState = context.ModelState
.Where(x => x.Value.Errors.Count > 0)
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Errors.Select(x => x.ErrorMessage).ToArray());
var errorResponse = new ErrorResponse();
foreach (var error in errorsInModelState)
{
foreach (var subError in error.Value)
{
var errorModel = new ErrorModel
{
FieldName = error.Key,
Message = subError
};
errorResponse.Error.Add(errorModel);
}
context.Result = new BadRequestObjectResult(errorResponse);
return;
}
await next();
//after controller
}
}
}
I have created error model just like this
public class ErrorModel
{
public string FieldName { get; set; }
public string Message { get; set; }
}
and error response like below
public class ErrorResponse
{
public List<ErrorModel> Error { get; set; } = new List<ErrorModel>();
public bool Successful { get; set; }
}
In order to achive this functionality you need to implement your own model validator described in this question: Model validation in Web API - Exception is thrown with out a throw statement?
You might want to take a look at the great library FluentValidation!
Sample:
Build a validator module binding your DTO and create a set of rules.
public class CustomerValidator : AbstractValidator<Customer> {
public CustomerValidator() {
RuleFor(x => x.Surname).NotEmpty();
RuleFor(x => x.Forename).NotEmpty().WithMessage("Please specify a first name");
RuleFor(x => x.Discount).NotEqual(0).When(x => x.HasDiscount);
RuleFor(x => x.Address).Length(20, 250);
RuleFor(x => x.Postcode).Must(BeAValidPostcode).WithMessage("Please specify a valid postcode");
}
private bool BeAValidPostcode(string postcode) {
// custom postcode validating logic goes here
}
}
Inject it in your controllers through DI:
services.AddControllers()
.AddFluentValidation(s =>
{
s.ValidatorOptions.CascadeMode = CascadeMode.Stop;
s.RunDefaultMvcValidationAfterFluentValidationExecutes = false;
s.ValidatorOptions.LanguageManager.Culture = new CultureInfo("en-US");
s.RegisterValidatorsFromAssemblyContaining<Customer>();
...
// more validators
});
That way your code looks well organized;
You get rid of Data Annotations spread all over your code.
Personalize error messages and validations.
You also may want to check why implementing ControllerBase on your controllers might be the way to go while using web APIs.
You want to output the JSON like this:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "|7ced8b82-4aa34d65daa99a12.",
"errors": {
"Vendor.UID": [
"UID is required"
]
}
}
the type and the status field, are from here https://github.com/dotnet/aspnetcore/blob/v3.1.17/src/Mvc/Mvc.Core/src/DependencyInjection/ApiBehaviorOptionsSetup.cs#L54-L108, the ClientErrorMapping will be configured when the dotnet core project setup.
the JSON was a ValidationProblemDetails which created by the DefaultProblemDetailsFactory, https://github.com/dotnet/aspnetcore/blob/v3.1.17/src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsFactory.cs#L45
we can use this ProblemDetailsFactory to create the ValidationProblemDetails https://github.com/dotnet/aspnetcore/blob/v3.1.17/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs#L261
I have just change in http methos and work fine for the same issue
[HttpPost("authenticate")]
public IActionResult Authenticate([FromBody]AuthenticateModel model)

Configuring Swashbuckle to use Inline Schema

With SwashBuckle we have a project that is creating inline schema's for Array/lists:
Right it returns something like this:
"200": {
"description": "Success",
"schema": {
"uniqueItems": false,
"type": "array",
"items": {
"$ref": "#/definitions/SwaggerGenerationSample.Models.Response.Employee"
}
}
}
We would like to have something like this:
"200": {
"description": "Success",
"schema": {
"$ref": "#/definitions/EmployeeArray"
}
}
...
"definitions": {
"EmployeeArray": {
"uniqueItems": false,
"type": "array",
"items": {
"$ref": "#/definitions/SwaggerGenerationSample.Models.Response.Employee"
}
}
}
What should we configure in SwashBuckle to get the result above? I created an example project to reproduce the issue: https://github.com/mvdiemen/SwaggerArrayGenerationExample
Take the general guideline of Override Schema for Specific Types from Swashbuckle, here the ISchemaFilter.
What you want is to provide your own explicit schema. With your example project, for any array we can use:
/**
* code dependencies:
* netcoreapp2.1
* Microsoft.AspNetCore.App;2.1.*
* Swashbuckle.AspNetCore;4.0.1
*/
using Swashbuckle.AspNetCore.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace SwaggerGenerationSample
{
public class ArraySchemaFilter : ISchemaFilter
{
public void Apply(Schema schema, SchemaFilterContext context)
{
// has to be changed to enumerable later on...
if (context.SystemType.IsArray && context.SystemType.HasElementType)
{
// could be generalized to any T[], for now ...
var elemType = context.SystemType.GetElementType();
if (elemType != typeof(SwaggerGenerationSample.Models.Response.Employee)) return;
var name = elemType.Name + "Array";
// add if not done already
if (!context.SchemaRegistry.Definitions.ContainsKey(name))
{
context.SchemaRegistry.Definitions.Add(
name,
new Schema
{
UniqueItems = schema.UniqueItems,
Type = schema.Type,
Items = schema.Items,
});
}
// and clear the schema for the endpoint...
schema.UniqueItems = null;
schema.Type = null;
schema.Items = null;
schema.Ref = "#/definitions/" + name;
}
}
}
}
We need to add the filter in your startup.
public void ConfigureServices(IServiceCollection services)
{
services.AddSwaggerGen(config =>
{
/* your config omitted for brevity */
config.SchemaFilter<ArraySchemaFilter>();
}
}
For this to work we need to "adjust" the endpoint from IEnumerable<T> to T[].
public class EmployeeController : ControllerBase
{
[SwaggerResponse((int)HttpStatusCode.OK, Type = typeof(Models.Response.Employee[]))]
/* other attributes omitted for brevity */
public async Task<IActionResult> GetEmployees(string companyId)
{
return Ok(GetEmployees());
}
}
Run the app and the swagger.json should contain the schema as requested.
The homework for you would be to replace the array restriction with a IEnumerable<> but the code above should be enough as a proof of concept.
friendly reminder
With an upgrade to .Net Core 3 (or newer) the code has to be updated - very likely. And with Swashbuckle.AspNetCore
5 upcoming breaking have already been announced [^1].
[^1]: See release notes here: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/releases/tag/v5.0.0-rc4
Following the same approach of ckerth answer of using ISchemaFilter, I was able to get the same results using Swashbuckle.AspNetCore 5. See the code below:
public class ArraySchemaFilter : ISchemaFilter
{
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
if (context.Type.IsArray && context.Type.HasElementType)
{
var elemType = context.Type.GetElementType();
var name = elemType.Name + "Array";
if (!context.SchemaRepository.Schemas.ContainsKey(name))
{
context.SchemaRepository.Schemas.Add(
name,
new OpenApiSchema
{
UniqueItems = schema.UniqueItems,
Type = schema.Type,
Items = schema.Items
});
}
schema.UniqueItems = null;
schema.Type = null;
schema.Items = null;
schema.Reference = new OpenApiReference
{
Id = name,
Type = ReferenceType.Schema
};
}
}
}
To add the filter in startup, the code is the same:
public void ConfigureServices(IServiceCollection services)
{
services.AddSwaggerGen(config =>
{
/* your config omitted for brevity */
config.SchemaFilter<ArraySchemaFilter>();
}
}
And to register the response type I've used a different approach:
public class EmployeeController : ControllerBase
{
[ProducesResponseType(typeof(Employee[]), StatusCodes.Status200OK)]
public async Task<IActionResult> GetEmployees(string companyId)
{
return Ok(GetEmployees());
}
}
Now the swagger.json will have the requested result.

Wiring up validation in MediatR and ASP.NET Core using autofac

I've just started to use MediatR in an asp.net core project and am struggling to wire up validation ...
Here's my controller:
public class PersonController : Controller
{
IMediator mediator;
public PersonController(IMediator mediator)
{
this.mediator = mediator;
}
[HttpPost]
public async Task<ActionResult> Post([FromBody]CreatePerson model)
{
var success = await mediator.Send(model);
if (success)
{
return Ok();
}
else
{
return BadRequest();
}
}
}
... and the CreatePerson command, validation (via FluentValidation) and request handler:
public class CreatePerson : IRequest<bool>
{
public string Title { get; set; }
public string FirstName { get; set; }
public string Surname { get; set; }
}
public class CreatePersonValidator : AbstractValidator<CreatePerson>
{
public CreatePersonValidator()
{
RuleFor(m => m.FirstName).NotEmpty().Length(1, 50);
RuleFor(m => m.Surname).NotEmpty().Length(3, 50);
}
}
public class CreatePersonHandler : IRequestHandler<CreatePerson, bool>
{
public CreatePersonHandler()
{
}
public bool Handle(CreatePerson message)
{
// do some stuff
return true;
}
}
I have this generic validation handler:
public class ValidatorHandler<TRequest, TResponse> : IRequestHandler<TRequest, TResponse> where TRequest : IRequest<TResponse>
{
private readonly IRequestHandler<TRequest, TResponse> inner;
private readonly IValidator<TRequest>[] validators;
public ValidatorHandler(IRequestHandler<TRequest, TResponse> inner, IValidator<TRequest>[] validators)
{
this.inner = inner;
this.validators = validators;
}
public TResponse Handle(TRequest message)
{
var context = new ValidationContext(message);
var failures = validators
.Select(v => v.Validate(context))
.SelectMany(result => result.Errors)
.Where(f => f != null)
.ToList();
if (failures.Any())
throw new ValidationException(failures);
return inner.Handle(message);
}
}
... but I'm struggling to wire the validation up correctly in Startup.ConfigureServices using autofac:
public IServiceProvider ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddMvc();
var builder = new ContainerBuilder();
builder.Register<SingleInstanceFactory>(ctx =>
{
var c = ctx.Resolve<IComponentContext>();
return t => c.Resolve(t);
});
builder.Register<MultiInstanceFactory>(ctx =>
{
var c = ctx.Resolve<IComponentContext>();
return t => (IEnumerable<object>)c.Resolve(typeof(IEnumerable<>).MakeGenericType(t));
});
builder.RegisterAssemblyTypes(typeof(IMediator).GetTypeInfo().Assembly).AsImplementedInterfaces();
builder.RegisterAssemblyTypes(typeof(CreatePersonHandler).GetTypeInfo().Assembly).AsClosedTypesOf(typeof(IRequestHandler<,>));
builder.RegisterGenericDecorator(typeof(ValidatorHandler<,>), typeof(IRequestHandler<,>), "Validator").InstancePerLifetimeScope();
builder.Populate(services);
var container = builder.Build();
return container.Resolve<IServiceProvider>();
}
When I run the app and POST /api/person
{
"title": "Mr",
"firstName": "Paul",
"surname": ""
}
I get a 200.
CreatePersonHandler.Handle() was called but CreatePersonValidator() is never called.
Am i missing something in Startup.ConfigureServices()?
I suggest that you read the official documentation on how to wire up decorators in Autofac.
Decorators use named services to resolve the decorated services.
For example, in your piece of code:
builder.RegisterGenericDecorator(
typeof(ValidatorHandler<,>),
typeof(IRequestHandler<,>),
"Validator").InstancePerLifetimeScope();
you're instructing Autofac to use ValidationHandler<,> as a decorator to IRequestHandler<,> services that have been registered with the Validator name, which is probably not what you want.
Here's how you could get it working:
// Register the request handlers as named services
builder
.RegisterAssemblyTypes(typeof(CreatePersonHandler).GetTypeInfo().Assembly)
.AsClosedTypesOf(typeof(IRequestHandler<,>))
.Named("BaseImplementation");
// Register the decorators on top of your request handlers
builder.RegisterGenericDecorator(
typeof(ValidatorHandler<,>),
typeof(IRequestHandler<,>),
fromKey: "BaseImplementation").InstancePerLifetimeScope();
I find specifying the name of the fromKey parameter helps in understanding how decorators work with Autofac.

Elastisearch.net property opt in?

I'm using Elastisearch.NET with NEST 2.3. I want to use attribute mapping but I only want to index certain properties. As I understand it all properties are indexed unless you ignore them using for example [String(Ignore = true)] Is it possible to ignore all properties by default and only index the ones that have a nest attribute attached to them? Like JSON.NETs MemberSerialization.OptIn
You could do this using a custom serializer to ignore any properties not marked with a NEST ElasticsearchPropertyAttributeBase derived attribute.
void Main()
{
var pool = new SingleNodeConnectionPool(new Uri("http://localhost:9200"));
var connectionSettings = new ConnectionSettings(
pool,
new HttpConnection(),
new SerializerFactory(s => new CustomSerializer(s)));
var client = new ElasticClient(connectionSettings);
client.CreateIndex("demo", c => c
.Mappings(m => m
.Map<Document>(mm => mm
.AutoMap()
)
)
);
}
public class Document
{
[String]
public string Field1 { get; set;}
public string Field2 { get; set; }
[Number(NumberType.Integer)]
public int Field3 { get; set; }
public int Field4 { get; set; }
}
public class CustomSerializer : JsonNetSerializer
{
public CustomSerializer(IConnectionSettingsValues settings, Action<JsonSerializerSettings, IConnectionSettingsValues> settingsModifier) : base(settings, settingsModifier) { }
public CustomSerializer(IConnectionSettingsValues settings) : base(settings) { }
public override IPropertyMapping CreatePropertyMapping(MemberInfo memberInfo)
{
// if cached before, return it
IPropertyMapping mapping;
if (Properties.TryGetValue(memberInfo.GetHashCode(), out mapping))
return mapping;
// let the base method handle any types from NEST
// or Elasticsearch.Net
if (memberInfo.DeclaringType.FullName.StartsWith("Nest.") ||
memberInfo.DeclaringType.FullName.StartsWith("Elasticsearch.Net."))
return base.CreatePropertyMapping(memberInfo);
// Determine if the member has an attribute
var attributes = memberInfo.GetCustomAttributes(true);
if (attributes == null || !attributes.Any(a => typeof(ElasticsearchPropertyAttributeBase).IsAssignableFrom(a.GetType())))
{
// set an ignore mapping
mapping = new PropertyMapping { Ignore = true };
Properties.TryAdd(memberInfo.GetHashCode(), mapping);
return mapping;
}
// Let base method handle remaining
return base.CreatePropertyMapping(memberInfo);
}
}
which produces the following request
PUT http://localhost:9200/demo?pretty=true
{
"mappings": {
"document": {
"properties": {
"field1": {
"type": "string"
},
"field3": {
"type": "integer"
}
}
}
}
}