Integration Tests for Web API with Azure Key Vault - c#

I followed the tutorial here but it seems like the start up file does not recognize the appsetting.json file.
So when I run the actual project, the Iconfiguration have 7 properties.
But when I run the test, it only has one property.
So I was thinking maybe I missed something in the test method to configure the AppSetting.json file..
Here's my test method:
public class StudentServiceRequestsTest
{
private readonly TestServer _server;
private readonly HttpClient _client;
public IndividualServiceRequestsTest()
{
// Arrange
_server = new TestServer(new WebHostBuilder()
.UseStartup<Startup>());
_client = _server.CreateClient();
}
[Fact]
public async Task GetStudentsByDeptShould()
{
try
{
//Act
var response = await _client.GetAsync("/api/department/200/students");
response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync();
//Assert
Assert.Equal("hi", responseString);
}
catch (Exception e)
{
throw;
}
}
Here's my startup class, apprently I added the json file which includes all the keys and secrets required in the Web API.
namespace CIS.API.Student
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; set; }
services.AddSingleton(Configuration);
services.AddMvc();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseCors(x => x
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials());
app.UseMvc();
}
}
}
Anyone knows why it would happen?

After some research, including the tutorial: Integration tests in ASP.NET Core, I made it working.
Step 1: Copy the "appsetting.json" file to the integration test project.
Step 2: Modify the test class constructor to:
public class StudentServiceTest
{
private readonly TestServer _server;
private readonly HttpClient _client;
public StudentServiceTest()
{
var config = new ConfigurationBuilder().SetBasePath(Path.GetFullPath(#"..\..\..\..\Student.IntegrationTest"))
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddEnvironmentVariables();
var builtConfig = config.Build();
config.AddAzureKeyVault(
$"https://{builtConfig["Vault"]}.vault.azure.net/",
builtConfig["ClientId"],
builtConfig["ClientSecret"]);
var Configuration = config.Build();
_server = new TestServer(WebHost.CreateDefaultBuilder()
.UseConfiguration(Configuration)
.UseStartup<Startup>());
_client = _server.CreateClient();
_client.BaseAddress = new Uri("http://localhost:xxxxx");
}
[Fact]
public async Task StudentShould()
{
try
{
//Act
var response = await _client.GetAsync("/api/getStudentByID/200");
response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync();
//Assert
Assert.Equal("bla bla", responseString);
}
catch (Exception e)
{
throw;
}
}
}

Related

ASP.NET Core Web API, How can I access HttpContext in startup class

I'm trying to access HttpContext to get RemoteIpAddress and User-Agent, but within Startup.cs.
public class Startup
{
public Startup(IConfiguration configuration, IHttpContextAccessor httpContextAccessor)
{
Configuration = configuration;
_httpContextAccessor = httpContextAccessor;
}
public IConfiguration Configuration { get; }
public IHttpContextAccessor _httpContextAccessor { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
IdentityModelEventSource.ShowPII = true;
var key = Encoding.ASCII.GetBytes(Configuration.GetValue<string>("claveEncriptacion"));
var ip = _httpContextAccessor.HttpContext.Connection.RemoteIpAddress.ToString();
var userAgent = _httpContextAccessor.HttpContext.Request.Headers["User-Agent"].ToString();
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(x =>
{
//x.Audience = ip + "-" + userAgent;
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = true
};
});
With the previous code I have an error executing the project.
Unable to resolve service for type 'Microsoft.AspNetCore.Http.IHttpContextAccessor' while attempting to activate 'JobSiteMentorCore.Startup'.'
According to the ASP.NET Core documentation , only the following service types can be injected into the Startup constructor when using the Generic Host (IHostBuilder):
IWebHostEnvironment
IHostEnvironment
IConfiguration
So you cannot inject IHttpContextAccessor to Startup constructor.
However you can get DI resolved service in ConfigureServices method of the Startup class as follows:
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IYourService, YourService>();
// Build an intermediate service provider
var serviceProvider = services.BuildServiceProvider();
// Resolve the services from the service provider
var yourService = serviceProvider.GetService<IYourService>();
}
But you can not get the HttpContext using IHttpContextAccessor similarly because HttpContext is null unless the code executed during any HttpRequest. So you have to do your desired operation from any custom middleware in Configure method of the Startup class as follows:
public class YourCustomMiddleMiddleware
{
private readonly RequestDelegate _requestDelegate;
public YourCustomMiddleMiddleware(RequestDelegate requestDelegate)
{
_requestDelegate = requestDelegate;
}
public async Task Invoke(HttpContext context)
{
// Your HttpContext related task is in here.
await _requestDelegate(context);
}
}
Then in the Configure method of the Startup class as follows:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseMiddleware(typeof(YourCustomMiddleMiddleware));
}
I've finally found a possible solution, using middleware to validate the token.
I created a class ValidationHandler, and this class can use HttpContext.
public class ValidationRequirement : IAuthorizationRequirement
{
public string Issuer { get; }
public string Scope { get; }
public HasScopeRequirement(string scope, string issuer)
{
Scope = scope ?? throw new ArgumentNullException(nameof(scope));
Issuer = issuer ?? throw new ArgumentNullException(nameof(issuer));
}
}
public class ValidationHandler : AuthorizationHandler<ValidationRequirement>
{
IHttpContextAccessor _httpContextAccessor;
public HasScopeHandler(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, HasScopeRequirement requirement)
{
var ip = _httpContextAccessor.HttpContext.Connection.RemoteIpAddress.ToString();
var userAgent = _httpContextAccessor.HttpContext.Request.Headers["User-Agent"].ToString();
//context.Succeed(requirement);
return Task.CompletedTask;
}
}
Finally in the Startup.cs class it is necessary to add the following.
services.AddAuthorization(options =>
{
options.AddPolicy("read:messages", policy => policy.Requirements.Add(new HasScopeRequirement("read:messages", "david")));
});
Thank you vidgarga. I would upvote but I have a new account. Your answer helped me on my project. I'm coming from f# land so I am including my implementation for any fsharpers that need this solution.
type HasScopeHandler() =
inherit AuthorizationHandler<HasScopeRequirement>()
override __.HandleRequirementAsync(context, requirement) =
let scopeClaimFromIssuer = Predicate<Claim>(fun (c: Claim) -> c.Type = "scope" && c.Issuer = requirement.Issuer)
let userDoesNotHaveScopeClaim = not (context.User.HasClaim(scopeClaimFromIssuer))
let isRequiredScope s = (s = requirement.Scope)
let claimOrNull = context.User.FindFirst(scopeClaimFromIssuer)
if (userDoesNotHaveScopeClaim) then
Task.CompletedTask
else
match claimOrNull with
| null -> Task.CompletedTask
| claim ->
let scopes = claim.Value.Split(' ')
let hasRequiredScope = scopes.Any(fun s -> isRequiredScope s)
if (hasRequiredScope) then
context.Succeed(requirement)
Task.CompletedTask
else
Task.CompletedTask

Net Core Integration Test: Run Startup.cs and Configuration from other Program

We are creating an Integration Unit test (Xunit), which is calling the Real Application Startup.cs.
For some reason, Real project can read Configuration file/property correctly, however running it from the Integration test, it cannot read it. Its not placing anything into Configuration (conf) variable below. How would I resolve this?
The reason is its not picking up is its not reading the internal dependency injection new from Net Core 2.2 which reads Configuration file. Trying to use .CreateDefaultBuilder Now.
IntegrationTest1.cs
TestServer _server = new TestServer(new WebHostBuilder() .UseContentRoot("C:\\RealProject\\RealProject.WebAPI")
.UseEnvironment("Development")
.UseConfiguration(new ConfigurationBuilder()
.SetBasePath("C:\\RealProject\\RealProject.WebAPI")
.AddJsonFile("appsettings.json")
.Build())
.UseStartup<Startup>()
.UseStartup<Startup>());
Real Project Startup.cs
public Startup(IConfiguration configuration, IHostingEnvironment hostingEnvironment)
{
Configuration = configuration;
HostingEnvironment = hostingEnvironment;
}
public IHostingEnvironment HostingEnvironment { get; }
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public IServiceProvider ConfigureServices(IServiceCollection services)
{
var conf = Configuration;
IConfiguration appConf = conf.GetSection("ConnectionStrings");
var connstring = appConf.GetValue<string>("DatabaseConnection");
services.AddDbContext<DbContext>(a => a.UseSqlServer(connstring));
Appsettings.Json
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"ConnectionStrings": {
"DatabaseConnection": "Data Source=.;Initial Catalog=ApplicationDatabase;Integrated Security=True"
}
}
What you want to do is create a Factory for your WebApplication which takes a Startup type. You can then use the IClassFixture interface to share the context of this factory with all of your tests in your test class.
How this looks in practice is:
public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<Startup> where TStartup : class
{
public CustomWebApplicationFactory() { }
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder
.ConfigureTestServices(
services =>
{
services.Configure(AzureADDefaults.OpenIdScheme, (System.Action<OpenIdConnectOptions>)(o =>
{
// CookieContainer doesn't allow cookies from other paths
o.CorrelationCookie.Path = "/";
o.NonceCookie.Path = "/";
}));
}
)
.UseEnvironment("Production")
.UseStartup<Startup>();
}
}
public class AuthenticationTests : IClassFixture<CustomWebApplicationFactory<Startup>>
{
private HttpClient _httpClient { get; }
public AuthenticationTests(CustomWebApplicationFactory<Startup> fixture)
{
WebApplicationFactoryClientOptions webAppFactoryClientOptions = new WebApplicationFactoryClientOptions
{
// Disallow redirect so that we can check the following: Status code is redirect and redirect url is login url
// As per https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-2.2#test-a-secure-endpoint
AllowAutoRedirect = false
};
_httpClient = fixture.CreateClient(webAppFactoryClientOptions);
}
[Theory]
[InlineData("/")]
[InlineData("/Index")]
[InlineData("/Error")]
public async Task Get_PagesNotRequiringAuthenticationWithoutAuthentication_ReturnsSuccessCode(string url)
{
// Act
HttpResponseMessage response = await _httpClient.GetAsync(url);
// Assert
response.EnsureSuccessStatusCode();
}
}
I followed the guide here to get this working in my own code.

System.InvalidOperationException: Cannot consume scoped service 'MyDbContext' from singleton 'Microsoft.Extensions.Hosting.IHostedService' [duplicate]

I've build a background task in my ASP.NET Core 2.1 following this tutorial: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-2.1#consuming-a-scoped-service-in-a-background-task
Compiling gives me an error:
System.InvalidOperationException: 'Cannot consume scoped service 'MyDbContext' from singleton 'Microsoft.AspNetCore.Hosting.Internal.HostedServiceExecutor'.'
What causes that error and how to fix it?
Background task:
internal class OnlineTaggerMS : IHostedService, IDisposable
{
private readonly CoordinatesHelper _coordinatesHelper;
private Timer _timer;
public IServiceProvider Services { get; }
public OnlineTaggerMS(IServiceProvider services, CoordinatesHelper coordinatesHelper)
{
Services = services;
_coordinatesHelper = coordinatesHelper;
}
public Task StartAsync(CancellationToken cancellationToken)
{
// Run every 30 sec
_timer = new Timer(DoWork, null, TimeSpan.Zero,
TimeSpan.FromSeconds(30));
return Task.CompletedTask;
}
private async void DoWork(object state)
{
using (var scope = Services.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<MyDbContext>();
Console.WriteLine("Online tagger Service is Running");
// Run something
await ProcessCoords(dbContext);
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
_timer?.Change(Timeout.Infinite, 0);
return Task.CompletedTask;
}
private async Task ProcessCoords(MyDbContext dbContext)
{
var topCoords = await _coordinatesHelper.GetTopCoordinates();
foreach (var coord in topCoords)
{
var user = await dbContext.Users.SingleOrDefaultAsync(c => c.Id == coord.UserId);
if (user != null)
{
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
//expire time = 120 sec
var coordTimeStamp = DateTimeOffset.FromUnixTimeMilliseconds(coord.TimeStamp).AddSeconds(120).ToUnixTimeMilliseconds();
if (coordTimeStamp < now && user.IsOnline == true)
{
user.IsOnline = false;
await dbContext.SaveChangesAsync();
}
else if (coordTimeStamp > now && user.IsOnline == false)
{
user.IsOnline = true;
await dbContext.SaveChangesAsync();
}
}
}
}
public void Dispose()
{
_timer?.Dispose();
}
}
Startup.cs:
services.AddHostedService<OnlineTaggerMS>();
Program.cs:
public class Program
{
public static void Main(string[] args)
{
var host = BuildWebHost(args);
using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;
try
{
var context = services.GetRequiredService<TutorDbContext>();
DbInitializer.Initialize(context);
}
catch(Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred while seeding the database.");
}
}
host.Run();
}
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build();
}
Full startup.cs:
public class Startup
{
public Startup(IConfiguration configuration)
{
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.AddCors(options =>
{
options.AddPolicy("CorsPolicy",
builder => builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials());
});
services.AddDbContext<MyDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
// ===== Add Identity ========
services.AddIdentity<User, IdentityRole>()
.AddEntityFrameworkStores<TutorDbContext>()
.AddDefaultTokenProviders();
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); // => remove default claims
services
.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(cfg =>
{
cfg.RequireHttpsMetadata = false;
cfg.SaveToken = true;
cfg.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = Configuration["JwtIssuer"],
ValidAudience = Configuration["JwtIssuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JwtKey"])),
ClockSkew = TimeSpan.Zero // remove delay of token when expire
};
});
//return 401 instead of redirect
services.ConfigureApplicationCookie(options =>
{
options.Events.OnRedirectToLogin = context =>
{
context.Response.StatusCode = 401;
return Task.CompletedTask;
};
options.Events.OnRedirectToAccessDenied = context =>
{
context.Response.StatusCode = 401;
return Task.CompletedTask;
};
});
services.AddMvc();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Info { Version = "v1", Title = "xyz", });
// Swagger 2.+ support
var security = new Dictionary<string, IEnumerable<string>>
{
{"Bearer", new string[] { }},
};
c.AddSecurityDefinition("Bearer", new ApiKeyScheme
{
Description = "JWT Authorization header using the Bearer scheme. Example: \"Bearer {token}\"",
Name = "Authorization",
In = "header",
Type = "apiKey"
});
c.AddSecurityRequirement(security);
});
services.AddHostedService<OnlineTaggerMS>();
services.AddTransient<UsersHelper, UsersHelper>();
services.AddTransient<CoordinatesHelper, CoordinatesHelper>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IServiceProvider serviceProvider, IApplicationBuilder app, IHostingEnvironment env, TutorDbContext dbContext)
{
dbContext.Database.Migrate();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseCors("CorsPolicy");
app.UseAuthentication();
app.UseMvc();
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("v1/swagger.json", "xyz V1");
});
CreateRoles(serviceProvider).GetAwaiter().GetResult();
}
private async Task CreateRoles(IServiceProvider serviceProvider)
{
var RoleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();
var UserManager = serviceProvider.GetRequiredService<UserManager<User>>();
string[] roleNames = { "x", "y", "z", "a" };
IdentityResult roleResult;
foreach (var roleName in roleNames)
{
var roleExist = await RoleManager.RoleExistsAsync(roleName);
if (!roleExist)
{
roleResult = await RoleManager.CreateAsync(new IdentityRole(roleName));
}
}
var _user = await UserManager.FindByEmailAsync("xxx");
if (_user == null)
{
var poweruser = new User
{
UserName = "xxx",
Email = "xxx",
FirstName = "xxx",
LastName = "xxx"
};
string adminPassword = "xxx";
var createPowerUser = await UserManager.CreateAsync(poweruser, adminPassword);
if (createPowerUser.Succeeded)
{
await UserManager.AddToRoleAsync(poweruser, "xxx");
}
}
}
You need to inject IServiceScopeFactory to generate a scope. Otherwise you are not able to resolve scoped services in a singleton.
using (var scope = serviceScopeFactory.CreateScope())
{
var context = scope.ServiceProvider.GetService<MyDbContext>();
}
Edit:
It's perfectly fine to just inject IServiceProvider and do the following:
using (var scope = serviceProvider.CreateScope()) // this will use `IServiceScopeFactory` internally
{
var context = scope.ServiceProvider.GetService<MyDbContext>();
}
The second way internally just resolves IServiceProviderScopeFactory and basically does the very same thing.
For Entity Framework DbContext classes there is a better way now with:
services.AddDbContextFactory<MyDbContext>(options => options.UseSqlServer(...))
Then you can just inject the factory in your singleton class like this:
IDbContextFactory<MyDbContext> myDbContextFactory
And finally use it like this:
using var myDbContex = _myDbContextFactory.CreateDbContext();
Though #peace answer worked for him, if you do have a DBContext in your IHostedService you need to use a IServiceScopeFactory.
To see an amazing example of how to do this check out this answer How should I inject a DbContext instance into an IHostedService?.
If you would like to read more about it from a blog, check this out .
I found the reason of an error. It was the CoordinatesHelper class, which is used in the the background task OnlineTaggerMS and is a Transient - so it resulted with an error. I have no idea why compiler kept throwing errors pointing at MyDbContext, keeping me off track for few hours.
You still need to register MyDbContext with the service provider. Usually this is done like so:
services.AddDbContext<MyDbContext>(options => {
// Your options here, usually:
options.UseSqlServer("YourConnectionStringHere");
});
If you also posted your Program.cs and Startup.cs files, it may shed some more light on things, as I was able to quickly setup a test project implementing the code and was unable to reproduce your issue.

Cannot consume scoped service 'MyDbContext' from singleton 'Microsoft.AspNetCore.Hosting.Internal.HostedServiceExecutor'

I've build a background task in my ASP.NET Core 2.1 following this tutorial: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-2.1#consuming-a-scoped-service-in-a-background-task
Compiling gives me an error:
System.InvalidOperationException: 'Cannot consume scoped service 'MyDbContext' from singleton 'Microsoft.AspNetCore.Hosting.Internal.HostedServiceExecutor'.'
What causes that error and how to fix it?
Background task:
internal class OnlineTaggerMS : IHostedService, IDisposable
{
private readonly CoordinatesHelper _coordinatesHelper;
private Timer _timer;
public IServiceProvider Services { get; }
public OnlineTaggerMS(IServiceProvider services, CoordinatesHelper coordinatesHelper)
{
Services = services;
_coordinatesHelper = coordinatesHelper;
}
public Task StartAsync(CancellationToken cancellationToken)
{
// Run every 30 sec
_timer = new Timer(DoWork, null, TimeSpan.Zero,
TimeSpan.FromSeconds(30));
return Task.CompletedTask;
}
private async void DoWork(object state)
{
using (var scope = Services.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<MyDbContext>();
Console.WriteLine("Online tagger Service is Running");
// Run something
await ProcessCoords(dbContext);
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
_timer?.Change(Timeout.Infinite, 0);
return Task.CompletedTask;
}
private async Task ProcessCoords(MyDbContext dbContext)
{
var topCoords = await _coordinatesHelper.GetTopCoordinates();
foreach (var coord in topCoords)
{
var user = await dbContext.Users.SingleOrDefaultAsync(c => c.Id == coord.UserId);
if (user != null)
{
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
//expire time = 120 sec
var coordTimeStamp = DateTimeOffset.FromUnixTimeMilliseconds(coord.TimeStamp).AddSeconds(120).ToUnixTimeMilliseconds();
if (coordTimeStamp < now && user.IsOnline == true)
{
user.IsOnline = false;
await dbContext.SaveChangesAsync();
}
else if (coordTimeStamp > now && user.IsOnline == false)
{
user.IsOnline = true;
await dbContext.SaveChangesAsync();
}
}
}
}
public void Dispose()
{
_timer?.Dispose();
}
}
Startup.cs:
services.AddHostedService<OnlineTaggerMS>();
Program.cs:
public class Program
{
public static void Main(string[] args)
{
var host = BuildWebHost(args);
using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;
try
{
var context = services.GetRequiredService<TutorDbContext>();
DbInitializer.Initialize(context);
}
catch(Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred while seeding the database.");
}
}
host.Run();
}
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build();
}
Full startup.cs:
public class Startup
{
public Startup(IConfiguration configuration)
{
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.AddCors(options =>
{
options.AddPolicy("CorsPolicy",
builder => builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials());
});
services.AddDbContext<MyDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
// ===== Add Identity ========
services.AddIdentity<User, IdentityRole>()
.AddEntityFrameworkStores<TutorDbContext>()
.AddDefaultTokenProviders();
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); // => remove default claims
services
.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(cfg =>
{
cfg.RequireHttpsMetadata = false;
cfg.SaveToken = true;
cfg.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = Configuration["JwtIssuer"],
ValidAudience = Configuration["JwtIssuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JwtKey"])),
ClockSkew = TimeSpan.Zero // remove delay of token when expire
};
});
//return 401 instead of redirect
services.ConfigureApplicationCookie(options =>
{
options.Events.OnRedirectToLogin = context =>
{
context.Response.StatusCode = 401;
return Task.CompletedTask;
};
options.Events.OnRedirectToAccessDenied = context =>
{
context.Response.StatusCode = 401;
return Task.CompletedTask;
};
});
services.AddMvc();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Info { Version = "v1", Title = "xyz", });
// Swagger 2.+ support
var security = new Dictionary<string, IEnumerable<string>>
{
{"Bearer", new string[] { }},
};
c.AddSecurityDefinition("Bearer", new ApiKeyScheme
{
Description = "JWT Authorization header using the Bearer scheme. Example: \"Bearer {token}\"",
Name = "Authorization",
In = "header",
Type = "apiKey"
});
c.AddSecurityRequirement(security);
});
services.AddHostedService<OnlineTaggerMS>();
services.AddTransient<UsersHelper, UsersHelper>();
services.AddTransient<CoordinatesHelper, CoordinatesHelper>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IServiceProvider serviceProvider, IApplicationBuilder app, IHostingEnvironment env, TutorDbContext dbContext)
{
dbContext.Database.Migrate();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseCors("CorsPolicy");
app.UseAuthentication();
app.UseMvc();
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("v1/swagger.json", "xyz V1");
});
CreateRoles(serviceProvider).GetAwaiter().GetResult();
}
private async Task CreateRoles(IServiceProvider serviceProvider)
{
var RoleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();
var UserManager = serviceProvider.GetRequiredService<UserManager<User>>();
string[] roleNames = { "x", "y", "z", "a" };
IdentityResult roleResult;
foreach (var roleName in roleNames)
{
var roleExist = await RoleManager.RoleExistsAsync(roleName);
if (!roleExist)
{
roleResult = await RoleManager.CreateAsync(new IdentityRole(roleName));
}
}
var _user = await UserManager.FindByEmailAsync("xxx");
if (_user == null)
{
var poweruser = new User
{
UserName = "xxx",
Email = "xxx",
FirstName = "xxx",
LastName = "xxx"
};
string adminPassword = "xxx";
var createPowerUser = await UserManager.CreateAsync(poweruser, adminPassword);
if (createPowerUser.Succeeded)
{
await UserManager.AddToRoleAsync(poweruser, "xxx");
}
}
}
You need to inject IServiceScopeFactory to generate a scope. Otherwise you are not able to resolve scoped services in a singleton.
using (var scope = serviceScopeFactory.CreateScope())
{
var context = scope.ServiceProvider.GetService<MyDbContext>();
}
Edit:
It's perfectly fine to just inject IServiceProvider and do the following:
using (var scope = serviceProvider.CreateScope()) // this will use `IServiceScopeFactory` internally
{
var context = scope.ServiceProvider.GetService<MyDbContext>();
}
The second way internally just resolves IServiceProviderScopeFactory and basically does the very same thing.
For Entity Framework DbContext classes there is a better way now with:
services.AddDbContextFactory<MyDbContext>(options => options.UseSqlServer(...))
Then you can just inject the factory in your singleton class like this:
IDbContextFactory<MyDbContext> myDbContextFactory
And finally use it like this:
using var myDbContex = _myDbContextFactory.CreateDbContext();
Though #peace answer worked for him, if you do have a DBContext in your IHostedService you need to use a IServiceScopeFactory.
To see an amazing example of how to do this check out this answer How should I inject a DbContext instance into an IHostedService?.
If you would like to read more about it from a blog, check this out .
I found the reason of an error. It was the CoordinatesHelper class, which is used in the the background task OnlineTaggerMS and is a Transient - so it resulted with an error. I have no idea why compiler kept throwing errors pointing at MyDbContext, keeping me off track for few hours.
You still need to register MyDbContext with the service provider. Usually this is done like so:
services.AddDbContext<MyDbContext>(options => {
// Your options here, usually:
options.UseSqlServer("YourConnectionStringHere");
});
If you also posted your Program.cs and Startup.cs files, it may shed some more light on things, as I was able to quickly setup a test project implementing the code and was unable to reproduce your issue.

Integration Testing with AutoMapper fails to initialise configuration

Frameworks & Packages
.NETCoreApp 1.1
Xunit 2.2.0
AutoMapper 6.0.2
Microsoft.AspNetCore.TestHost 1.1.1
Microsoft.NET.Test.Sdk 15.0.0
Integration Test
public class ControllerRequestsShould
{
private readonly TestServer _server;
private readonly HttpClient _client;
public ControllerRequestsShould()
{
_server = new TestServer(new WebHostBuilder()
.UseContentRoot(Constants.apiProjectRoot)
.UseStartup<Startup>()
.UseEnvironment(Constants.testingEnvironment));
_client = _server.CreateClient();
_client.BaseAddress = new Uri(Constants.localHostUri);
}
[Fact]
public async Task CreateAnEntity()
{
// Arrange
var entityForCreationDto = new entityForCreationDto { Code = "00001", Name = "Entity One" };
var jsonContent = JsonConvert.SerializeObject(entityForCreationDto);
var stringContent = new StringContent(jsonContent);
stringContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
// Act
var response = await _client.PostAsync("/api/controller", stringContent);
response.EnsureSuccessStatusCode();
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
}
Startup.cs
public class Startup
{
public IConfigurationRoot Configuration { get; }
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
}
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
// Add framework services
services.AddMvc(setupAction =>
{
setupAction.ReturnHttpNotAcceptable = true;
setupAction.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter());
setupAction.InputFormatters.Add(new XmlDataContractSerializerInputFormatter());
});
// Db context configuration
var connectionString = Configuration["ConnectionStrings:DefaultConnection"];
services.AddDbContext<YourContext>(options =>
{
options.UseSqlServer(connectionString);
});
// Register services for dependency injection
services.AddScoped<IYourRepository, YourRepository>();
services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();
services.AddScoped<IUrlHelper, UrlHelper>(implementationFactory =>
{
var actionContext =
implementationFactory.GetService<IActionContextAccessor>().ActionContext;
return new UrlHelper(actionContext);
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole();
loggerFactory.AddDebug(LogLevel.Information);
loggerFactory.AddNLog();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler(appBuilder =>
{
appBuilder.Run(async context =>
{
var exceptionHandlerFeature = context.Features.Get<IExceptionHandlerFeature>();
if (exceptionHandlerFeature != null)
{
var logger = loggerFactory.CreateLogger("Global exception logger");
logger.LogError(500,
exceptionHandlerFeature.Error,
exceptionHandlerFeature.Error.Message);
}
context.Response.StatusCode = 500;
await context.Response.WriteAsync("An unexpected fault happened. Try again later");
});
});
}
Mapper.Initialize(cfg =>
{
cfg.CreateMap<DataStore.Entities.Entity, Models.EntityDto>();
cfg.CreateMap<Models.EntityDto, DataStore.Entities.Entity>();
cfg.CreateMap<Models.EntityForCreationDto, DataStore.Entities.Entity>();
cfg.CreateMap<DataStore.Entities.Entity, Models.EntityForCreationDto>();
});
app.UseMvc();
}
Problem
The integration test fails after the controller method is invoked:
var response = await _client.PostAsync("/api/controller", stringContent);
It fails because AutoMapper has not been initialised.
The way I understood this was that since the TestServer has the UseStartup method, it should use all the services configured in the api Startup.cs class (the UseContentRoot is pointing to my api project root)
This clearly isn't happening. Could someone show me how I need to configure the TestServer so that the AutoMapper configuration is picked up correctly please?
You should specify the assembly in the ConfigureServices method :
var assembly = typeof(Program).GetTypeInfo().Assembly;
services.AddAutoMapper(assembly);
I'm using Automapper Modules, so the mapping config is picked up automatically by AutoMapper, but even then, you still need the above config.
Or just use this line
services.AddAutoMapper(typeof(Startup));
instead of
var assembly = typeof(Program).GetTypeInfo().Assembly;
services.AddAutoMapper(assembly);
which is more clear and clean in my opinion
Thank you, it's work for me. Additionally you can add configuration options like this.
var assembly = typeof(Program).GetTypeInfo().Assembly;
services.AddAutoMapper(cfg =>
{
cfg.AllowNullDestinationValues = true;
cfg.CreateMap<ApplicationUser, ApplicationUserView> ().IgnoreAllPropertiesWithAnInaccessibleSetter();}, assembly);

Categories

Resources