I have the following mapping profile
public class DomainProfile : Profile
{
private FootballPredictionsContext m_Context;
public DomainProfile(FootballPredictionsContext context)
{
m_Context = context;
}
public DomainProfile()
{
CreateMap<TipModel, Tip>()
.ForMember(tip => tip.BetType, m => m.MapFrom(x => m_Context.BetTypes.First(y => y.Name == x.BetType)))
.ForMember(tip => tip.BetCategory, m => m.MapFrom(x => m_Context.BetCategories.First(y => y.Name == x.BetCategory)))
.ForMember(tip => tip.Sport, m => m.MapFrom(x => m_Context.Sports.First(y => y.Name == x.Sport)))
.ForMember(tip => tip.Tipster, m => m.MapFrom(model => m_Context.Tipsters.First(y => y.Username == model.Tipster)));
}
}
As you can see, some of the mappings are using the DbContext, so I have to somehow inject it in the DomainProfile
In the Startup class I am initializing the Automapper normally
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped(typeof(IUnificator), typeof(Unificator));
services.AddDbContext<FootballPredictionsContext>(options => options.UseSqlServer(Configuration.GetConnectionString("Database")));
services.AddDbContext<UnificationContext>(options => options.UseSqlServer(Configuration.GetConnectionString("Database")));
services.AddSingleton(provider => new MapperConfiguration(cfg =>
{
cfg.AddProfile(new UserProfile(provider.GetService<IUserManager>()));
}).CreateMapper());
services.AddMvc();
}
I tried this solution, but I am receiving 'Cannot resolve scoped service 'FootballPredictions.DAL.FootballPredictionsContext' from root provider.'
I've come across a similar problem recently and it was down to the fact that I was trying to inject a service into a service with a longer lifetime (e.g. transient and scoped). What lifetime is associated with the DomainProfile class? Have you tried changing that to Scoped or Transient to see if that helps?
As implemented by #DimitarTsonev:
So, changing the mapper scope to
services.AddScoped(provider => new MapperConfiguration(cfg =>
{
cfg.AddProfile(new DomainProfile(provider.GetService<FootballPredictionsContext>()));
}).CreateMapper());
fixed the issue
Assuming you've actually registered you context (services.AddDbContext, for example), the most likely cause of that exception is that you're registering AutoMapper before you're registering the context. Make sure your context is registered first, before you do something like services.AddAutoMapper.
Related
Info
The app type is a Hosted Blazor web-assembly. And below are the versions of the nuget packages I am using. There is an error that occurs when trying to expand a navigation property that is a many-to-many relationship. The classes are mapped to DTO classes that flattens the middle relationship class.
.Net core Version="3.1"
AutoMapper Version="10.0.0"
AutoMapper.AspNetCore.OData.EFCore Version="2.0.1"
AutoMapper.Extensions.ExpressionMapping Version="4.0.1"
AutoMapper.Extensions.Microsoft.DependencyInjection Version="8.0.1"
Microsoft.AspNetCore.Components.WebAssembly.Server Version="3.2.1"
Microsoft.AspNetCore.OData Version="7.5.0"
To run this repo, you will need the free version of SQL Server or better
Set the EfCoreAutomapperOdata.Server project as the startup project and navigate to the Courses page (https://localhost:5001/courses) and click on either course. This will throw the following error:
System.InvalidOperationException: No generic method 'Include' on type 'Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions' is compatible with the supplied type arguments and arguments. No type arguments should be provided if the method is non-generic. at System.Linq.Expressions.Expression.FindMethod(Type type, String methodName, Type[] typeArgs, Expression[] args, BindingFlags flags)...
Models
See here - Entity Models and here - Dto Models for class definition
Automapper Config
public class AutomapperConfig : Profile
{
public AutomapperConfig()
{
CreateMap<Instructor, InstructorDto>();
CreateMap<InstructorDto, Instructor>();
CreateMap<Course, CourseDto>()
.ForMember(dto => dto.Students, opt => {
opt.MapFrom(_ => _.Students.Select(y => y.Student));
});
CreateMap<CourseDto, Course>()
.ForMember(ent => ent.Students, ex => ex
.MapFrom(x => x.Students.Select(y => new CourseStudent {
CourseId = x.Id,
StudentId = y.Id
})));
CreateMap<Student, StudentDto>()
.ForMember(dto => dto.Courses, opt => {
opt.MapFrom(x => x.Courses.Select(y => y.Course));
})
.ForMember(dto => dto.Friends, opt => {
opt.MapFrom(x => x.Friends.Select(y => y.Friend));
});
CreateMap<StudentDto, Student>()
.ForMember(ent => ent.Courses, ex => ex
.MapFrom(x => x.Courses.Select(y => new CourseStudent
{
StudentId = x.Id,
CourseId = y.Id
})));
}
}
Startup
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
// ------ Some code removed for brevity ------
services.AddOData();
services.AddAutoMapper(cfg => { cfg.AddExpressionMapping(); },typeof(AutomapperConfig));
// ------ Some code removed for brevity ------
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// ------ Some code removed for brevity ------
app.UseHttpsRedirection();
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapControllers();
endpoints.EnableDependencyInjection();
endpoints.Select().Filter().OrderBy().Count().Expand().MaxTop(1000);
endpoints.MapODataRoute("odata", "odata", GetEdmModel());
endpoints.MapFallbackToFile("index.html");
});
}
private IEdmModel GetEdmModel()
{
var builder = new ODataConventionModelBuilder();
builder.EntitySet<CourseDto>("Courses");
builder.EntitySet<InstructorDto>("Instructors");
builder.EntitySet<StudentDto>("Students");
return builder.GetEdmModel();
}
}
Course Controller
public class CourseController : ODataController
{
protected readonly BlazorContext _context;
protected readonly IMapper _mapper;
public CourseController(BlazorContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
[HttpGet]
[ODataRoute("Courses")]
public async Task<IActionResult> Get(ODataQueryOptions<CourseDto> options)
{
return Ok(await _context.Course.GetAsync(_mapper, options));
}
[HttpGet]
[ODataRoute("Courses({id})")]
public async Task<IActionResult> Get([FromODataUri] int id, ODataQueryOptions<CourseDto> options)
{
return Ok((await _context.Course.GetAsync(_mapper, options)).Where(s => s.Id == id).ToList());
}
}
Sample odata api query that fails
/odata/Courses?$expand=Students
Repro
I have built demo Blazor WASM app for this issue to reproduce
Repository
General Setup
To make expansions work, you need to allow for the $expand query option to be used. Configure it explicitly like so:
private IEdmModel GetEdmModel()
{
var builder = new ODataConventionModelBuilder();
builder.EntitySet<EstimateDto>(nameof(MyContext.Estimates))
.EntityType
.Expand(); // <-- allow expansion
builder.EntitySet<TypeDto>(nameof(MyContext.Types))
.EntityType
.Expand(); // <-- allow expansion
builder.EntitySet<SubTypeDto>(nameof(MyContext.SubTypes));
return builder.GetEdmModel();
}
You will also need to update your AutoMapper map, to allow the queries to be successfully mapped to a DTOs:
public class AutoMapperConfig : Profile
{
public AutoMapperConfig()
{
CreateMap<Estimate, EstimateDto>()
.ForMember(
dto => dto.Types,
opt => opt.MapFrom(x => x.EstimateTypes.Select(y => y.Type)));
// The following mapping is needed for expansion to work:
CreateMap<EstimateTypeRel, TypeDto>()
.ForMember(
dto => dto.SubTypes,
opt => opt.MapFrom(x => x.Type));
CreateMap<Type, TypeDto>()
.ForMember(
dto => dto.SubTypes,
opt => opt.MapFrom(x => x.SubTypes.Select(y => y.SubType)));
CreateMap<SubTypeRel, SubTypeDto>();
}
}
With that configuration being setup, there are at least two possible solutions to this issue, depending on your requirements:
A) Only expand Types
If you only want to expand Types, you will need to change your AutoMapper mappings by adding a .Where(z => z != null) clause, because as the exception tells you, null values are not allowed in collections, but OData includes them for the non-expanded SubType entities:
public class AutoMapperConfig : Profile
{
public AutoMapperConfig()
{
CreateMap<Estimate, EstimateDto>()
.ForMember(
dto => dto.Types,
opt => opt.MapFrom(
x => x.EstimateTypes.Select(y => y.Type)
.Where(z => z != null))); // <-- filter out null values
CreateMap<EstimateTypeRel, TypeDto>()
.ForMember(
dto => dto.SubTypes,
opt => opt.MapFrom(x => x.Type));
CreateMap<Type, TypeDto>()
.ForMember(
dto => dto.SubTypes,
opt => opt.MapFrom(
x => x.SubTypes.Select(y => y.SubType)
.Where(z => z != null))); // <-- filter out null values
CreateMap<SubTypeRel, SubTypeDto>();
}
}
Then you can use the following query:
https://localhost:5001/odata/Estimates(1)?$expand=Types
B) Also expand SubTypes
The alternative is to expand the SubTypes property as well, so the collection can be properly filled. To expand DTO mapped properties over multiple levels, use the $expand query option in your query string like so:
https://localhost:5001/odata/Estimates(1)?$expand=Types($expand=SubTypes)
Consider I have the following generic class:
public class Repository<T> : IRepository<T> where T : class
{
private DbContext Context { get; set; }
public Repository(DbContext context)
{
Context = context;
}
}
And I register two different dbcontext as below, using SimpleInjector :
container.Register<ContextA>(
()=> new ContextA(SqlServerDbContextOptionsExtensions.UseSqlServer(
new DbContextOptionsBuilder(), sqlConnection).Options));
container.Register<ContextB>(
() => new ContextB(SqlServerDbContextOptionsExtensions.UseSqlServer(
new DbContextOptionsBuilder(), sqlConnection).Options));
And then I have the following registration for dbContext :
container.RegisterConditional(
typeof(DbContext),
typeof(ContextA),
c=> c.Consumer.ImplementationType.GenericTypeArguments
.Any(r => r.Namespace == "NameSpace.ContextA"));
container.RegisterConditional(
typeof(DbContext),
typeof(ContextB),
c => c.Consumer.ImplementationType.GenericTypeArguments
.Any(r => r.Namespace == "NameSpace.ContextB"));
When the codes reaches container.RegisterConditional it throws error and complains about a ContextA having two constructor.
Considering that I have already injected ContextA and ContextB, what is the best way to inject appropriate DbContext for Repository based on its generic argument value?
Update
I want to inject DbContext to Repository<T>, based on the type passed to initialize the Repository<T>.
So I might have:
IRepository<Entity1> ...
Which Entity1 could be in NameSpace.ContextA.
You need to use the RegisterConditional overload that accepts a Registration. This way you can wrap the lambda expression inside the Registration as follows:
var contextARegistration =
Lifestyle.Scoped.CreateRegistration(
() => new ContextA(
SqlServerDbContextOptionsExtensions.UseSqlServer(
new DbContextOptionsBuilder(), sqlConnection).Options),
container);
container.RegisterConditional(
typeof(DbContext),
contextARegistration,
c => c.Consumer.ImplementationType.GenericTypeArguments
.Any(r => r.Namespace == "NameSpace.ContextA"));
var contextBRegistration =
Lifestyle.Scoped.CreateRegistration(
() => new ContextB(
SqlServerDbContextOptionsExtensions.UseSqlServer(
new DbContextOptionsBuilder(), sqlConnection).Options),
container);
container.RegisterConditional(
typeof(DbContext),
contextBRegistration,
c => c.Consumer.ImplementationType.GenericTypeArguments
.Any(r => r.Namespace == "NameSpace.ContextB"));
I'm using AutoMapper in my .NET Core project. The default mapper function is working well, but when I use .ForMember() in myProfile.cs class, it doesn't work.
myProfile.cs just like :
public class ServiceProfile : Profile
{
public ServiceProfile()
{
CreateMap<Organization, OrganizationDto>()
.ForMember(x => x.Active, opt => opt.MapFrom(src => src.Disabled));
}
}
The configuration in startup.cs like this:
public void ConfigureServices(IServiceCollection services)
{
var mappingConfig = new MapperConfiguration(mc =>
{
mc.AddProfile(new ServiceProfile());
});
IMapper mapper = mappingConfig.CreateMapper();
services.AddSingleton(mapper);
services.AddMvc();
}
Then
input.Disabled=0;
var output = _mapper.Map<Organization>(input);
I expect the output.Active to be 0, but the actual output is null.
UPDATE:
I'm sorry, the method has no problem, it's because I'm using dapper and skip the map step.
It could be because the mapping method is CreateMap<TSource, TDestination>, source is the first generics parameter, but in the code below your mapping is from the DTO class to organization class (in the other direction) for which the mapping is probably not specified.
You may need to create mapping also for the other direction from OrganizationDto to Organization.
Update: there now is an easy way to add mapping for the other direction by adding .ReverseMap() to the end of CreateMap call chain.
I'm trying to map one complex object to another using instance API:
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<Student, PersonType>();
cfg.CreateMap<Professor, PersonType>();
cfg.CreateMap<Branch, BranchType>()
.ForMember(x => x.Departments, opt => opt.MapFrom(src =>
new DepartmentType[] {
new DepartmentType
{
Students = Mapper.Map<Student[], PersonType[]> (src.Students),
Professors = Mapper.Map<Professor[], PersonType[]> (src.Professors),
Name = src.DepartmentName
}
}))
.ForMember(x => x.Name, opt => opt.MapFrom(src => src.Name))
.ForAllMembers(opts => opts.Condition((src, dest, srcMember) => srcMember != null));
});
var mapper = config.CreateMapper();
var test = mapper.Map<BranchType>(source);
The problem is I don't know how to achieve this without mixing instance and static API which is not working. Here is the error:
InvalidOperationException: Mapper not initialized. Call Initialize with appropriate configuration. If you are trying to use mapper instances through a container or otherwise, make sure you do not have any calls to the static Mapper.Map methods, and if you're using ProjectTo or UseAsDataSource extension methods, make sure you pass in the appropriate IConfigurationProvider instance.
Apparently mixing of the static and instance based approaches is not allowed:
Students = Mapper.Map<Student[], PersonType[]> (src.Students)
How to use existing map to apply it to a property of the complex object with instance API?
I have a problem with Autofac and Automapper.
In my WebApi I have this code to register my classes:
public static void Initialize(HttpConfiguration config, IContainer container)
{
config.DependencyResolver = new AutofacWebApiDependencyResolver(container);
_container = container;
}
private static IContainer RegisterServices(ContainerBuilder builder)
{
//Register your Web API controllers.
builder.RegisterApiControllers(Assembly.GetExecutingAssembly());
builder.RegisterModule<MapperInstaller>();
builder.RegisterType<VurpEntities>();
builder.RegisterType<EntityFrameworkUnitOfWork>().As<IUnitOfWork>();
builder.RegisterType<AppService>().As<IAppService>();
builder.RegisterType<AppRepository>().As<IAppRepository>();
builder.RegisterType<BusinessService>().As<IBusinessService>();
builder.RegisterType<BusinessRepository>().As<IBusinessRepository>();
//...
//all my types are registered here
//...
//Set the dependency resolver to be Autofac.
_container = builder.Build();
return _container;
}
I created a Resolver to map a special field of my object:
public class ProductTypeResolver : IMemberValueResolver<ProductDto, Product, string, Enumeration.ProductType>
{
public ProductType Resolve(ProductDto source, Product destination, string sourceMember, ProductType destMember, ResolutionContext context)
{
Enum.TryParse(sourceMember, out destMember);
return destMember;
}
}
I have a Profile that map DTO object in Entity object:
CreateMap<ProductDto, Product>()
.ForMember(dest => dest.Category, opt => opt.Ignore())
.ForMember(dest => dest.Feature, opt => opt.Ignore())
opts.ResolveUsing<ProductTypeResolver, string>(src => src.type));
When I try to map the object Product p = _mapper.Map<Product>(productDto); I receive the error:
An exception of type 'System.ObjectDisposedException' occurred in
Autofac.dll but was not handled in user code
Additional information: This resolve operation has already ended. When
registering components using lambdas, the IComponentContext 'c'
parameter to the lambda cannot be stored. Instead, either resolve
IComponentContext again from 'c', or resolve a Func<> based factory to
create subsequent components from.
Anyone can help me?
I solved with this:
builder.Register(c =>
{
//This resolves a new context that can be used later.
var context = c.Resolve<IComponentContext>();
var config = context.Resolve<MapperConfiguration>();
return config.CreateMapper(context.Resolve);
})
.As<IMapper>()
.InstancePerLifetimeScope();