How to use Expression in Expression for single element - c#

How can I use an Expression in another Expression. For a set I can use blog.Posts.Select(postMapper.ProjectPost). But How can I use it for a single object? I don't want to call compile, I need to use that in EF sql translator. I try some hacks like new List<Blog>{post.Blog}.Select(blogMapper.ProjectBlog).First() but it's not working.
public class BloggingContext : DbContext
{
private readonly ILoggerFactory loggerFactory;
public BloggingContext(ILoggerFactory loggerFactory)
{
this.loggerFactory = loggerFactory;
}
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseLoggerFactory(loggerFactory).UseSqlServer(#"Server=(LocalDB)\MSSqlLocalDb;Database=EFExpressionMapper;Trusted_Connection=True");
}
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
public List<Post> Posts { get; } = new List<Post>();
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int BlogId { get; set; }
public Blog Blog { get; set; }
}
class Program
{
static async Task Main(string[] args)
{
IServiceCollection serviceCollection = new ServiceCollection();
serviceCollection.AddLogging(builder => builder
.AddConsole()
.AddFilter(level => level >= LogLevel.Information)
);
var loggerFactory = serviceCollection.BuildServiceProvider().GetService<ILoggerFactory>();
await using var dbContext = new BloggingContext(loggerFactory);
dbContext.Add(new Blog
{
Url = "http://blogs.msdn.com/sample-blog",
Posts =
{
new Post {Title = "Post 1", Content = "Post 1 content"},
new Post {Title = "Post 2", Content = "Post 2 content"},
new Post {Title = "Post 3", Content = "Post 3 content"},
}
});
await dbContext.SaveChangesAsync();
var postMapper = new PostMapper(new BlogMapper());
var posts = await dbContext.Posts.Select(postMapper.ProjectPost).ToArrayAsync();
foreach (var post in posts)
{
Console.WriteLine($"{post.Title} {post.Blog.Url}");
}
}
}
public class PostMapper
{
public Expression<Func<Post, PostDto>> ProjectPost { get; }
public PostMapper(BlogMapper blogMapper)
{
//TODO USE blogMapper.ProjectBlogList WITHOUT COMPILE
ProjectPost = post => new PostDto(post.PostId, post.Title, post.Content, blogMapper.ProjectBlogList.Compile()(post.Blog));
}
}
public class BlogMapper
{
public Expression<Func<Blog, BlogListDto>> ProjectBlogList { get; } = blog => new BlogListDto(blog.BlogId, blog.Url);
}
public class BlogListDto
{
public int BlogId { get; }
public string Url { get; }
public BlogListDto(int blogId, string url)
{
BlogId = blogId;
Url = url;
}
}
public class PostDto
{
public int PostId { get; }
public string Title { get; }
public string Content { get; }
public BlogListDto Blog { get; }
public PostDto(int postId, string title, string content, BlogListDto blog)
{
PostId = postId;
Title = title;
Content = content;
Blog = blog;
}
}
Look into PostMapper constructor. I'm used a Compile method there. But it's not good for EF

Actually LINQKit may correct your query and make EF happy when using Compile. Just add AsExpandable() to your query.
But I suggest to do not create mapping class for each DTO but collect them in logical one:
public static class DtoMapper
{
[Expandable(nameof(AsDtoPost))]
public static PostDto AsDto(this Post post)
=> throw new NotImplementedException();
[Expandable(nameof(AsDtoBlogList))]
public static BlogListDto AsDto(this Blog blog)
=> throw new NotImplementedException();
static Expression<Func<Post, PostDto>> AsDtoPost()
=> post => new PostDto(post.PostId, post.Title, post.Content, post.Blog.AsDto()));
static Expression<Func<Blog, BlogListDto>> AsDtoBlogList()
=> blog => new BlogListDto(blog.BlogId, blog.Url);
}
So your sample can be rewritten
var posts = await dbContext.Posts
.AsExpandable()
.Select(p => p.AsDto()).ToArrayAsync();
Similar but answer already created before, which covers other libraries which do the same lambda expression expanding. https://stackoverflow.com/a/66386142/10646316
This answer is focused on LINQKit realisation.

Related

How to set up Automapper global options to allow destination values to be null when source is null

I have next classes where I want to use Automapper and if source property is null, then set destination property as null as well:
public class CompanyTest
{
public Guid? Id { get; set; }
public string CompanyName { get; set; } = string.Empty;
public PersonTest? CEO { get; set; }
public List<PersonTest> People { get; set; } = new();
}
public class CompanyPatchTest
{
public string? CompanyName { get; set; }
public PersonPatchTest? CEO { get; set; }
public List<PersonPatchTest>? People { get; set; }
}
public class PersonTest
{
public Guid? Id { get; set; }
public string Name { get; set; }
public List<PersonTest> Children { get; set; } = new();
}
public class PersonPatchTest
{
public string? Name { get; set; }
public List<PersonPatchTest>? Children { get; set; }
}
and I have created next xunit test that is failing:
using Xunit;
using AutoMapper;
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
namespace UnitTests.Tests;
public class AutomapperTests
{
private readonly IMapper _mapper;
public AutomapperTests()
{
var services = new ServiceCollection();
services.AddAutoMapper(x =>
{
x.AddProfile(new ProfileTest());
});
var serviceProvider = services.BuildServiceProvider();
_mapper = serviceProvider.GetRequiredService<IMapper>();
}
[Fact]
public void ShouldSetNullValue()
{
var company = new CompanyTest
{
Id = Guid.NewGuid(),
CompanyName = "OriginalName",
CEO = new()
{
Id = Guid.NewGuid(),
Name= "Name",
Children = new()
{
new PersonTest
{
Id = Guid.NewGuid(),
Name= "Name",
}
}
},
};
var source = new CompanyPatchTest
{
CompanyName = "CompanyName",
CEO = null,
};
company = _mapper.Map<CompanyPatchTest, CompanyTest>(source, company);
company.CEO.Should().BeNull();
}
}
My profile is the next one:
public class ProfileTest : Profile
{
public ProfileTest()
{
CreateMap<CompanyTest, CompanyPatchTest>();
CreateMap<PersonTest, PersonPatchTest>();
CreateMap<CompanyPatchTest, CompanyTest>(MemberList.Source)
.ForMember(d => d.CEO, op => op.AllowNull());
CreateMap<PersonPatchTest, PersonTest>(MemberList.Source);
}
}
If I remove CreateMap<PersonPatchTest, PersonTest>(MemberList.Source); line then it works. But I won't be able to customize that mapping.
Also, it'd be nice if this could be set up as a global setting for all properties.
I have tried using AllowNullDestinationValues but it is not working
...
public AutomapperTests()
{
var services = new ServiceCollection();
services.AddAutoMapper(x =>
{
x.AddProfile(new ProfileTest());
x.AllowNullCollections = true;
x.AllowNullDestinationValues = true;
});
var serviceProvider = services.BuildServiceProvider();
_mapper = serviceProvider.GetRequiredService<IMapper>();
}
...
If this cannot be done using Automapper, do you know any other tool that ?could achieve this.
Autmapper version: 12.0.0
This is a bug that was solved in version 12.0.1. You don't need any settings for that test to pass. The default for AllowNullDestinationValues is true.
https://github.com/AutoMapper/AutoMapper/pull/4083

Automapper setting property to null when it should ignore

I was trying to implement a code to ignore a property (therefore mantaining the source value). I used the ignore method, which works most of the time. For some reason I noticed that sometimes the ignore sets the property value to null.
Do you know what could be problem?
I created the following code to reproduce the issue. I was expecting client.ContactDetails.First().Address to have the value "Old".
using AutoMapper;
class Program
{
static void Main(string[] args)
{
ClientMapperProfile clientMapperProfile = new ClientMapperProfile();
var configurationProvider = new MapperConfiguration(c => c.AddProfile(clientMapperProfile));
Mapper mapper = new Mapper(configurationProvider);
var client = new Client()
{
ContactDetails = new []
{
new ContactDetails()
{
Address= "Old"
}
}
};
var clientDto = new ClientDto()
{
ContactDetails = new []
{
new ContactDetailsDto()
{
Address = "New"
}
}
};
mapper.Map(clientDto,client);
Console.WriteLine(client.ContactDetails.First().Address);
}
}
public class Client
{
public ContactDetails[] ContactDetails { get; set; }
}
public class ContactDetails
{
public string Address { get; set; }
}
public class ClientDto
{
public ContactDetailsDto[] ContactDetails { get; set; }
}
public class ContactDetailsDto
{
public string Address { get; set; }
}
public class ClientMapperProfile : Profile
{
public ClientMapperProfile()
{
CreateMap<ClientDto, Client>();
CreateMap<ContactDetailsDto, ContactDetails>()
.ForMember(c => c.Address, opt => opt.Ignore());
}
}

Mapping the external api request based on a pattern from a general request

I have an application with a general search request which has an identification number that could be anything like product number or customer number along with additional search criteria. I want to display all the results. I am writing a middleware to call the search api end points.
public class GeneralRequest
{
public string IdentificationNumber { get; set; }
public string Location { get; set; }
public bool IsActive { get; set; }
public DateTime CreatedDate { get; set; }
public DateTime Start { get; set; }
public DateTime Stop { get; set; }
}
public class AdditionalSearch
{
public RangeSearch Range { get; set; }
public string Location { get; set; }
public bool IsActive { get; set; }
}
public class RangeSearch
{
public DateTime Start { get; set; }
public DateTime Stop { get; set; }
}
public class GetProductRequest : AdditionalSearch, ISearchRequest
{
public string ProductId { get; set; }
}
public class GetCustomerRequst : AdditionalSearch, ISearchRequest
{
public string CustomerNumber { get; set; }
}
public class GetManufacturerRequest : AdditionalSearch, ISearchRequest
{
public string ManufacturerNumber { get; set; }
}
// this is a dummy interface to make all the requests general
public interface ISearchRequest
{
}
This is the searchprocessor where I am creating the correct request based on the identification number pattern. But I am failing to assign the AdditonalSearch to the request that I get after invoking the func. Of course it is an interface that has nothing in it. How can I achieve this by not repeating(I mean I don't want to repeat the initialization logic in the dictionary)
Please suggest me what is the best practice here.
public class SearchProcessor
{
private readonly Dictionary<Regex, Func<GeneralRequest, ISearchRequest>> _pattern;
private readonly IAppClient _appClient;
public SearchProcessor(IAppClient appClient)
{
_appClient = appClient;
_pattern = new Dictionary<Regex, Func<GeneralRequest, ISearchRequest>>
{
{new Regex("/\b([0-9]|10)\b /"), p=> new GetProductRequest(){ProductId = p.IdentificationNumber} },
{new Regex("^\\d{}1,9}$"), p=> new GetCustomerRequst(){CustomerNumber = p.IdentificationNumber} },
{new Regex("^\\d{}1,11}$"), p=> new GetManufacturerRequest(){ManufacturerNumber = p.IdentificationNumber} }
};
}
public List<SearchResult> GetAllSearchResults(GeneralRequest request)
{
var requests = _pattern.Where(r => r.Key.IsMatch(request.IdentificationNumber)).Select(v => v.Value);
var responses = new List<SearchResult>();
foreach (var req in requests)
{
var appRequest = req.Invoke(request);
appRequest.AdditionalSearch = new AdditionalSearch // this is where I am not able to assign the additional seach from the general request
{
Range = new RangeSearch { Start = request.Start, Stop = request.Stop}
IsActive = request.IsActive,
Location = request.Location
};
//This calls another api to get the response.
responses.Add(_appclient.FindResult(appRequest));
}
return responses;
}
}
---UPDATE--
Here is the appclient that calls the external api..
sample request for the getproduct route is
public class AppClient : IAppClient
{
private readonly string _baseurl;
private readonly string _getProductRoute;
private readonly string _getCustomerRoute;
private readonly string _getManufacturerRoute;
public AppClient()
{
_getProductRoute = $"{_baseurl}/api/get-product";
_getCustomerRoute = $"{_baseurl}/api/get-customer";
_getManufacturerRoute = $"{_baseurl}/api/get-Manufacturer";
}
public SearchResult FindResult(ISearchRequest searchRequest)
{
var routes = new Dictionary<Type, string>
{
{typeof(GetProductRequest), _getProductRoute },
{typeof(GetCustomerRequst), _getCustomerRoute },
{typeof(GetManufacturerRequest), _getManufacturerRoute }
};
// Here it is going to be http implementation to call above routes.
return new SearchResult();
}
}
The request for get product route is
{
"ProductId":"",
"RangeSearch":{
"Start":"",
"Stop":""
},
"Location":"",
"IsActive":true
}
For get-customer request is
{
"CustomerNumber":"",
"RangeSearch":{
"Start":"",
"Stop":""
},
"Location":"",
"IsActive":true
}
You can simplify your work by doing this :
public class SearchRequest
{
public string IdentificationNumber { get; set; }
public string Location { get; set; }
public bool IsActive { get; set; }
public SearchRequestRange Range { get; set; }
public SearchRequest(string identificationNumber)
{
IdentificationNumber = identificationNumber;
}
}
public class SearchRequestRange
{
public DateTime Start { get; set; }
public DateTime Stop { get; set; }
}
now the process you are doing is not needed, you only need to adjust your ApiClient to something like this :
public class AppClient : IAppClient
{
private static readonly IReadOnlyDictionary<string, string> _endPointsSearchPatterns = new Dictionary<string, string>
{
{"get-product", "/\b([0-9]|10)\b /"},
{"get-customer", "^\\d{}1,9}$"},
{"get-Manufacturer", "^\\d{}1,11}$"}
};
private readonly string _baseUrl;
public AppClient(string baseUrl)
{
if(string.IsNotNullOrWhiteSpace(baseUrl))
throw new ArgumentNullException(nameof(baseUrl));
_baseurl = baseUrl;
}
public IEnumerable<SearchResult> FindResult(SearchRequest searchRequest)
{
var endPoints = _endPointsSearchPatterns.Where(x=> Regex.IsMatch(request.IdentificationNumber , x.Value))?.ToList();
if(endPoints?.Count == 0)
{
yield break;
}
var responses = new List<SearchResult>();
foreach(var endpoint in endPoints)
{
ISearchBy search = null;
switch(endPoint.Key)
{
case "get-product":
action = new ProductApiClient(this);
break;
case "get-customer":
action = new ProductApiClient(this);
break;
case "get-Manufacturer":
action = new ProductApiClient(this);
break;
}
yield return action.SearchBy(searchRequest);
}
return searchResult;
}
}
Regarding GetProductRequest, GetCustomerRequst, and GetManufacturerRequest these should be refactored, and instead you can create a class for each entity like this :
public interface ISearchBy
{
SearchResult ISearchBy(SearchRequest request);
}
public class ProductApiClient : ISearchBy
{
private readonly IAppClient _appClient;
public ProductApiClient(IAppClient appClient)
{
_appClient = appClient;
}
public SearchResult SearchBy(SearchRequest request)
{
// do stuff
}
// other related endpoints
}
public class CustomerApiClient : ISearchBy
{
private readonly IAppClient _appClient;
public CustomerApiClient(IAppClient appClient)
{
_appClient = appClient;
}
public SearchResult SearchBy(SearchRequest request)
{
// do stuff
}
// other related endpoints
}
public class ManufacturerApiClient : ISearchBy
{
private readonly IAppClient _appClient;
public ManufacturerApiClient(IAppClient appClient)
{
_appClient = appClient;
}
public SearchResult SearchBy(SearchRequest request)
{
// do stuff
}
// other related endpoints
}

C# - Model is always getting its default values

I have an ASP.Net Core 2.1 application.
Below is my DTO
public class Movie
{
public int Id { get; set;}
public bool IsSpecial {get; set;}
public IEnumerable<Ticket> Tickets { get; set; }
public Movie()
{
if(IsSpecial)
{
this.Tickets = new List<TicketSpecial>();
}
else
{
this.Tickets = new List<Ticket>();
}
}}}
Tickets (Base Class)
public class Ticket
{
public int Id { get; set;}
public string Name { get; set;}
public decimal price { get; set;}
}
TicketsSpecial (Child/Derived Class)
public class TicketsSpecial : Ticket
{
public string SpecialProp1 { get; set;}
public string SpecialProp2 { get; set;}
}
WebAPI Controller
public class MovieController : ControllerBase
{
public IActionResult Post([FromBody]Movie movie)
{
}
}
Postman (HTTPPost Req payload Content-Type = application/json)
{
"IsSpecial": true,
"SpecialProp1": "Mumbai Test",
}
When I call the above API via Postman & debug at Movie ctor, it always catches the value of IsSpecial = false & all fields default value (ex. for string type null)
Thanks!
There are two issues in your current implementation. First, your request json is invalid for nested properties and json deserializer would not deserialize for you with TicketsSpecial.
Follow steps below for a workaround:
Movie
public class Movie
{
public int Id { get; set; }
public bool IsSpecial { get; set; }
public IEnumerable<Ticket> Tickets { get; set; }
}
MyJsonInputFormatter
public class MyJsonInputFormatter : JsonInputFormatter
{
public MyJsonInputFormatter(ILogger logger, JsonSerializerSettings serializerSettings, ArrayPool<char> charPool, ObjectPoolProvider objectPoolProvider, MvcOptions options, MvcJsonOptions jsonOptions) : base(logger, serializerSettings, charPool, objectPoolProvider, options, jsonOptions)
{
}
public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
{
var result = await base.ReadRequestBodyAsync(context);
if (result.Model is Movie movie && movie.IsSpecial)
{
context.HttpContext.Request.Body.Position = 0;
string request = await new StreamReader(context.HttpContext.Request.Body).ReadToEndAsync();
var tickets = JObject.Parse(request)["Tickets"].ToObject<List<TicketSpecial>>();
movie.Tickets = tickets;
}
return result;
}
}
Register MyJsonInputFormatter
services.AddMvc(o =>
{
var serviceProvider = services.BuildServiceProvider();
var customJsonInputFormatter = new MyJsonInputFormatter(
serviceProvider.GetRequiredService<ILoggerFactory>().CreateLogger<MyJsonInputFormatter>(),
serviceProvider.GetRequiredService<IOptions<MvcJsonOptions>>().Value.SerializerSettings,
serviceProvider.GetRequiredService<ArrayPool<char>>(),
serviceProvider.GetRequiredService<ObjectPoolProvider>(),
o,
serviceProvider.GetRequiredService<IOptions<MvcJsonOptions>>().Value
);
o.InputFormatters.Insert(0, customJsonInputFormatter);
})
Request
{
"IsSpecial": true,
"Tickets": [
{"SpecialProp1": "Mumbai Test"}
]
}
Change "Isspecial" to "isSpecial", same with the other property.
Another problem is that you're checking "IsSpecial" in the constructor and at this time it should be false anyway.

ASP.NET Core WebAPI : custom InputFormatter validate Model State

I have used custom InputFormatters for creating a subset of request from the generic request that request body receives in API request.
var reqModel = new XmlSerializer(CurrentType).Deserialize(xmlDoc.CreateReader());
SubSetRequest model = ConvertToSubSetRequestObject(reqModel as BigRequest);
return InputFormatterResult.Success(model);
Now in controller ModelState.IsValid is not pointing to SubSetRequest but to BigRequest, which I have received request body
public ActionResult<object> Calculate(SubSetRequest request)
{
if (!ModelState.IsValid){ }
// other codes..
}
Any idea how can we validate ModelState against SubSetRequest type.
Important Classes :
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddMvcCore(options =>
{
options.OutputFormatters.Add(new XmlSerializerOutputFormatter());
options.InputFormatters.Insert(0, new XMLDocumentInputFormatter());
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.AddXmlSerializerFormatters()
.AddXmlDataContractSerializerFormatters();
}
BigRequest.cs
[System.SerializableAttribute()]
public class BigRequest
{
public string Name { get; set; }
public string Email { get; set; }
public string Address { get; set; }
public string Company { get; set; }
public string Designation { get; set; }
public string CompanyAddress { get; set; }
}
SubSetRequest.cs
[System.SerializableAttribute()]
public class SubSetRequest
{
public string Name { get; set; }
[Required] //This should tiger **Validation** error
public string Email { get; set; }
public string Address { get; set; }
}
XMLDocumentInputFormatter.cs
internal class XMLDocumentInputFormatter : InputFormatter
{
private Type CurrentType { get; set; }
public XMLDocumentInputFormatter()
{
SupportedMediaTypes.Add("application/xml");
}
public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
{
using (var streamReader = new StreamReader(context.HttpContext.Request.Body))
{
CurrentType = typeof(BigRequest);
var xmlDoc = await XDocument.LoadAsync(streamReader, LoadOptions.None, CancellationToken.None);
var reqModel = new XmlSerializer(CurrentType).Deserialize(xmlDoc.CreateReader());
var model = ConvertToSubSetRequestObject(reqModel as BigRequest);
return InputFormatterResult.Success(model);
}
}
public SubSetRequest ConvertToSubSetRequestObject(BigRequest request)
{
var retObject = new SubSetRequest
{
Name = request.Name,
Address = request.Address
};
return retObject;
}
}
ValueController.cs
[HttpPost]
[Route("/api/Value/Calculate")]
public virtual ActionResult<object> Calculate(SubSetRequest request)
{
TryValidateModel(request);
if (ModelState.IsValid) // is giving as TRUE, even if EMAIL is NULL
{
var context = new ValidationContext(request, serviceProvider: null, items: null);
var results = new List<ValidationResult>();
// this is working properly
var isValid = Validator.TryValidateObject(request, context, results);
}
return new ActionResult<object>(request.ToString());
}

Categories

Resources