using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.AspNetCore.OData.Query;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.OData.ModelBuilder;
using Microsoft.OData.Edm;
using Microsoft.OData.UriParser;
using AutoMapper.AspNet.OData;
using AutoMapper.Extensions.ExpressionMapping;
public static class Program
public static void Main()
var model = CreateEdmModel();
var mapper = BuildMapper();
var db = new ClientContext();
var opts = BuildQueryOptions<ArchiveDto>(model, "$select=id&$filter=Name eq 'Test'");
var query = db.Archives.GetQuery(mapper, opts);
Console.WriteLine("\n----- Select + Filter -----");
Console.WriteLine(query.Expression.Print());
opts = BuildQueryOptions<ArchiveDto>(model, "$select=id&$expand=Documents");
query = db.Archives.GetQuery(mapper, opts);
Console.WriteLine("\n----- Select + Expand -----");
Console.WriteLine(query.Expression.Print());
opts = BuildQueryOptions<ArchiveDto>(model, "$select=id&$expand=Documents($select=id;$filter=Name eq 'Test')");
query = db.Archives.GetQuery(mapper, opts);
Console.WriteLine("\n----- Expand(Select + Filter) -----");
Console.WriteLine(query.Expression.Print());
private static ODataQueryOptions<T> BuildQueryOptions<T>(IEdmModel model, string? query=null)
var root = "http://localhost/odata";
var entitySets = model.EntityContainer.Elements.OfType<IEdmEntitySet>();
var entitySet = entitySets.Single(s => s.EntityType().FullTypeName() == "Default." + typeof(T).Name);
var setName = entitySet.Name;
var parser = new ODataUriParser(model, new Uri(root), new Uri($"{root}/{setName}"));
var path = parser.ParsePath();
var req = BuildRequest(setName, query);
var ctx = new ODataQueryContext(model, typeof(T), path);
return new ODataQueryOptions<T>(ctx, req);
private static HttpRequest BuildRequest(string entitySet, string? query)
var req = new DefaultHttpContext().Request;
req.Host = new HostString("localhost");
req.Path = $"/odata/{entitySet}";
req.QueryString = new QueryString("?" + query);
private static IMapper BuildMapper()
var config = new MapperConfiguration(cfg =>
cfg.AddExpressionMapping();
var archiveMap = cfg.CreateMap<Archive, ArchiveDto>();
archiveMap.ForAllMembers(o => o.ExplicitExpansion());
var archiveRevMap = archiveMap.ReverseMap();
archiveRevMap.ForAllMembers(o => o.ExplicitExpansion());
var docMap = cfg.CreateMap<Document, DocumentDto>();
docMap.ForAllMembers(o => o.ExplicitExpansion());
var docRevMap = archiveMap.ReverseMap();
docRevMap.ForAllMembers(o => o.ExplicitExpansion());
config.AssertConfigurationIsValid();
return config.CreateMapper();
private static IEdmModel CreateEdmModel()
var builder = new ODataConventionModelBuilder();
builder.EntitySet<ArchiveDto>("Archives");
builder.EntitySet<DocumentDto>("Documents");
return builder.GetEdmModel();
public required int Id { get; set; }
public required string Name { get; set; }
public List<Document>? Documents { get; set; }
public required int Id { get; set; }
public required string Name { get; set; }
public List<DocumentDto>? Documents { get; set; }
public required int Id { get; set; }
public required string Name { get; set; }
public int ArchiveId { get; set; }
[ForeignKey(nameof(ArchiveId))]
public Archive? Archive { get; set; }
public record DocumentDto
public required int Id { get; set; }
public required string Name { get; set; }
public int ArchiveId { get; set; }
public ArchiveDto? Archive { get; set; }
public class ClientContext : DbContext
public ClientContext() : base()
this.Database.EnsureDeleted();
this.Database.EnsureCreated();
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
optionsBuilder.UseSqlite("Data Source=Test.db");
public DbSet<Archive> Archives { get; set; }
public DbSet<Document> Documents { get; set; }