I'm having an issue with getting the same instance of the IIdentityService(my own class) or IServiceProvider in my consumer observer. I have an identity service where I set the credentials for the user, which is used later in the consumer pipeline.
I have tried the code below along with other code and configuration changes.
_serviceProvider.GetRequiredService<IIdentityService>()
_serviceProvider.CreateScope()
var consumerScopeProvider = _serviceProvider.GetRequiredService<IConsumerScopeProvider>();
using (var scope = consumerScopeProvider.GetScope(context))
{
// this next line of code is where we must access the payload
// using a container specific interface to get access to the
// scoped IServiceProvider
var serviceScope = scope.Context.GetPayload<IServiceScope>();
var serviceProviderScoped = serviceScope.ServiceProvider;
IIdentityService identityService = _serviceProvider.GetRequiredService<IIdentityService>();
}
// Also tried this as Scoped
services.AddTransient<CustomConsumer>();
var busControl = Bus.Factory.CreateUsingRabbitMq(cfg =>
{
var host = cfg.Host(new Uri(busConfiguration.Address), h =>
{
h.Username(busConfiguration.Username);
h.Password(busConfiguration.Password);
});
cfg.ReceiveEndpoint(host, "queue_1", endpointCfg => ConfigureConsumers(endpointCfg, provider));
cfg.UseServiceScope(provider);
});
busControl.ConnectConsumeObserver(new ConsumeObserver(provider));
public class ConsumeObserver : IConsumeObserver
{
private readonly IServiceProvider _serviceProvider;
public ConsumeObserver(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
Task IConsumeObserver.PreConsume<T>(ConsumeContext<T> context)
{
var identityMessage = (IIdentityMessage)context.Message;
if (identityMessage == null)
{
return Task.CompletedTask;
}
// Here I get a "Cannot resolve scoped service '' from root provider." error
IIdentityService identityService = _serviceProvider.GetRequiredService<IIdentityService>();
var task = identityService.SetIdentityAsync(identityMessage.Identity.TenantIdentifier, identityMessage.Identity.UserIdentifier);
// This gets a different instance of the IIdentityService.cs service.
//IIdentityService identityService = _serviceProvider.CreateScope().ServiceProvider.GetRequiredService<IIdentityService>();
// called before the consumer's Consume method is called
return task;
}
Task IConsumeObserver.PostConsume<T>(ConsumeContext<T> context)
{
// called after the consumer's Consume method is called
// if an exception was thrown, the ConsumeFault method is called instead
return TaskUtil.Completed;
}
Task IConsumeObserver.ConsumeFault<T>(ConsumeContext<T> context, Exception exception)
{
// called if the consumer's Consume method throws an exception
return TaskUtil.Completed;
}
}
// This is where I need to identity credentials
services.AddScoped<UserContext>((provider =>
{
// This gets a different instance of IIdentityService
var identityService = provider.GetRequiredService<IIdentityService>();
var contextProvider = provider.GetRequiredService<IContextProvider>();
var identity = identityService.GetIdentity();
return contextProvider.GetContext(identity.UserId);
}));
var task = identityService.SetIdentityAsync(identityMessage.Identity.TenantIdentifier, identityMessage.Identity.UserIdentifier);
The setting of the identity above should be retrievable below.
var identity = identityService.GetIdentity();
What I get is null reference as the service provider is of a different instance.
Can anyone tell me how to get the same instance of the service provider through out the consumer pipeline?
Related
I have a microservice architecture with ASP.Net Core applications and RabbitMq as the event bus between the microservices.
I also want to support multi tenancy.
So I have following dependency injection service defined in the Startup.cs to open a connection to the Database on every request based on the user's tenant id.
services.AddScoped<IDocumentSession>(ds =>
{
var store = ds.GetRequiredService<IDocumentStore>();
var httpContextAccessor = ds.GetRequiredService<IHttpContextAccessor>();
var tenant = httpContextAccessor?.HttpContext?.User?.Claims.FirstOrDefault(c => c.Type == "tid")?.Value;
return tenant != null ? store.OpenSession(tenant) : store.OpenSession();
});
The problem is when the service processes an event bus message (like UserUpdatedEvent).
In that case when it tries to open the Db connection, it obviously does not have the user information from the http context.
How do I send/access the tenant id of the respective user when injecting the scoped service and processing an event with RabbitMq?
Or rephrasing my question:
Is there any way to access the RabbitMQ message (and for example its headers) when the dependency injection code is executed?
Since there is no HttpContext, because a RabbitMq request is not a Http request, as pointed out in #istepaniuk's answer, I created my own context and called it AmqpContext:
public interface IAmqpContext
{
void ClearHeaders();
void AddHeaders(IDictionary<string, object> headers);
string GetHeaderByKey(string headerKey);
}
public class AmqpContext : IAmqpContext
{
private readonly Dictionary<string, object> _headers;
public AmqpContext()
{
_headers = new Dictionary<string, object>();
}
public void ClearHeaders()
{
_headers.Clear();
}
public void AddHeaders(IDictionary<string, object> headers)
{
foreach (var header in headers)
_headers.Add(header.Key, header.Value);
}
public string GetHeaderByKey(string headerKey)
{
if (_headers.TryGetValue(headerKey, out object headerValue))
{
return Encoding.Default.GetString((byte[])headerValue);
}
return null;
}
}
And when sending the RabbitMq message I send the tenant id via the headers like this:
var properties = channel.CreateBasicProperties();
if (tenantId != null)
{
var headers = new Dictionary<string, object>
{
{ "tid", tenantId }
};
properties.Headers = headers;
}
channel.BasicPublish(exchange: BROKER_NAME,
routingKey: eventName,
mandatory: true,
basicProperties: properties,
body: body);
Then when on the receiving service I register the AmqpContext as a scoped service in the Startup.cs:
services.AddScoped<IAmqpContext, AmqpContext>();
When receiving the RabbitMq message, within the consumer channel, a scope and the Amqp context is created:
consumer.Received += async (model, ea) =>
{
var eventName = ea.RoutingKey;
var message = Encoding.UTF8.GetString(ea.Body);
var properties = ea.BasicProperties;
using (var scope = _serviceProvider.CreateScope())
{
var amqpContext = scope.ServiceProvider.GetService<IAmqpContext>();
if (amqpContext != null)
{
amqpContext.ClearHeaders();
if (properties.Headers != null && amqpContext != null)
{
amqpContext.AddHeaders(properties.Headers);
}
}
var handler = scope.ServiceProvider.GetService(subscription.HandlerType);
if (handler == null) continue;
var eventType = _subsManager.GetEventTypeByName(eventName);
var integrationEvent = JsonConvert.DeserializeObject(message, eventType);
var concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType);
await (Task)concreteType.GetMethod("Handle").Invoke(handler, new object[] { integrationEvent });
}
channel.BasicAck(ea.DeliveryTag, multiple: false);
};
Then when the scoped Db connection service is created (see my question) I can access the tenant id from the message headers:
services.AddScoped<IDocumentSession>(ds =>
{
var store = ds.GetRequiredService<IDocumentStore>();
string tenant = null;
var httpContextAccessor = ds.GetRequiredService<IHttpContextAccessor>();
if (httpContextAccessor.HttpContext != null)
{
tenant = httpContextAccessor.HttpContext.User?.Claims.FirstOrDefault(c => c.Type == "tid")?.Value;
}
else
{
var amqpContext = ds.GetRequiredService<IAmqpContext>();
tenant = amqpContext.GetHeaderByKey("tid");
}
return tenant != null ? store.OpenSession(tenant) : store.OpenSession();
});
You can't
Or maybe, but not if your design depends on the HTTP context. As the .NET documentation on service lifetime states:
Scoped lifetime services are created once per client request
(connection).
So from the point of view of your (HTTP) service, the request is an entry point that used container magic to, by means of the global HTTP context, set up your database per request, before any of your business logic. This does not seem to be the best design choice, especially if you plan to use this same logic outside of an HTTP request.
In contrast, your message consumer service is long-running; In this lifetime cycle, if your connection setup requires information from each message (tenant id) you can't solely rely on dependency injection.
The "right" way would be not to rely on global state in the HTTP context to set up the database connection. Set up a database context that works for all your tenants instead.
I'm developing the service within ASP.NET Boilerplate engine and getting the error from the subject. The nature of the error is not clear, as I inheriting from ApplicationService, as documentation suggests. The code:
namespace MyAbilities.Api.Blob
{
public class BlobService : ApplicationService, IBlobService
{
public readonly IRepository<UserMedia, int> _blobRepository;
public BlobService(IRepository<UserMedia, int> blobRepository)
{
_blobRepository = blobRepository;
}
public async Task<List<BlobDto>> UploadBlobs(HttpContent httpContent)
{
var blobUploadProvider = new BlobStorageUploadProvider();
var list = await httpContent.ReadAsMultipartAsync(blobUploadProvider)
.ContinueWith(task =>
{
if (task.IsFaulted || task.IsCanceled)
{
if (task.Exception != null) throw task.Exception;
}
var provider = task.Result;
return provider.Uploads.ToList();
});
// store blob info in the database
foreach (var blobDto in list)
{
SaveBlobData(blobDto);
}
return list;
}
public void SaveBlobData(BlobDto blobData)
{
UserMedia um = blobData.MapTo<UserMedia>();
_blobRepository.InsertOrUpdateAndGetId(um);
CurrentUnitOfWork.SaveChanges();
}
public async Task<BlobDto> DownloadBlob(int blobId)
{
// TODO: Implement this helper method. It should retrieve blob info
// from the database, based on the blobId. The record should contain the
// blobName, which should be returned as the result of this helper method.
var blobName = GetBlobName(blobId);
if (!String.IsNullOrEmpty(blobName))
{
var container = BlobHelper.GetBlobContainer();
var blob = container.GetBlockBlobReference(blobName);
// Download the blob into a memory stream. Notice that we're not putting the memory
// stream in a using statement. This is because we need the stream to be open for the
// API controller in order for the file to actually be downloadable. The closing and
// disposing of the stream is handled by the Web API framework.
var ms = new MemoryStream();
await blob.DownloadToStreamAsync(ms);
// Strip off any folder structure so the file name is just the file name
var lastPos = blob.Name.LastIndexOf('/');
var fileName = blob.Name.Substring(lastPos + 1, blob.Name.Length - lastPos - 1);
// Build and return the download model with the blob stream and its relevant info
var download = new BlobDto
{
FileName = fileName,
FileUrl = Convert.ToString(blob.Uri),
FileSizeInBytes = blob.Properties.Length,
ContentType = blob.Properties.ContentType
};
return download;
}
// Otherwise
return null;
}
//Retrieve blob info from the database
private string GetBlobName(int blobId)
{
throw new NotImplementedException();
}
}
}
The error appears even before the app flow jumps to 'SaveBlobData' method. Am I missed something?
Hate to answer my own questions, but here it is... after a while, I found out that if UnitOfWorkManager is not available for some reason, I can instantiate it in the code, by initializing IUnitOfWorkManager in the constructor. Then, you can simply use the following construction in your Save method:
using (var unitOfWork = _unitOfWorkManager.Begin())
{
//Save logic...
unitOfWork.Complete();
}
I am trying to use multiple in-process owin listeners. Each should have a distinct set of controllers, where they may have the same route handled by a different controller. For instance
localhost:1234/api/app/test should resolve to ControllerA
localhost:5678/api/app/test should resolve to ControllerB
controller a, in owin host 1, has route attribute
[Route("api/app/test")]
controller b, in owin host 2, has route attribute
[Route("api/app/{*path}")]
and is used to forward requests to the other owin host.
We are using Autofac for dependency injection. Routes are configured through attribute routing.
autofac requires a line such as
builder.RegisterApiControllers(typeof(ControllerA).Assembly)
Our OWIN configuration contains:
var config = ConfigureWebApi();
// Configure Autofac
config.DependencyResolver = new AutofacWebApiDependencyResolver(container);
app.UseAutofacMiddleware(container);
app.UseAutofacWebApi(config);
app.UseWebApi(config);
However when starting two listeners, I need to include both assemblies for controller resolving. This leads to a 'duplicate route' exception:
Multiple controller types were found that match the URL. This can
happen if attribute routes on multiple controllers match the requested
URL.\r\n\r\nThe request has found the following matching controller
types:
\r\nLib1.Controllers.ControllerA\r\nLib2.Controllers.ControllerB"
When running the OWIN listeners in separate processes, there are no issues.
I have also tried to use multiple DI containers, one for each OWIN listener, but that conflicts with Web Api 2 as it requires GlobalConfiguration.Configuration.DependencyResolver to be set. Which conflicts with the concept of multiple DI containers.
Can someone guide me how to configure such a setup?
Use the OWIN environment and customize the HttpControllerSelector
Using the OWIN pipeline you can pass information about the request to a custom HttpControllerSelector. This allows you to be selective about which controllers are used to match which routes.
Of course this is easier said than done. The inner workings of WebAPI with respect to routing are not very transparent - source code is often the best documentation in this area.
I could not get the HttpControllerSelector to fully work, so there's an ugly workaround in CustomHttpActionSelector. It may still be sufficient if all you need to do is forward requests from one host to the other.
The end result is:
GET to http://localhost:1234/api/app/test returns "HellofromAController" (directly invokes AController)
GET to http://localhost:5678/api/app/test returns "(FromBController): \"HellofromAController\"" (invokes BController, which forwards the request to AController)
See the full source on github
I left the logging code as-is in case it's useful, but it's not relevant to the solution.
So without further ado:
CustomHttpControllerSelector.cs:
Uses the port-specific OWIN env variable ApiControllersAssembly in to filter the controllers.
public sealed class CustomHttpControllerSelector : DefaultHttpControllerSelector
{
private static readonly ILog Logger;
static CustomHttpControllerSelector()
{
Logger = LogProvider.GetCurrentClassLogger();
}
public CustomHttpControllerSelector(HttpConfiguration configuration) : base(configuration)
{
}
public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
{
var apiControllerAssembly = request.GetOwinEnvironment()["ApiControllersAssembly"].ToString();
Logger.Debug($"{nameof(CustomHttpControllerSelector)}: {{{nameof(apiControllerAssembly)}: {apiControllerAssembly}}}");
var routeData = request.GetRouteData();
var routeCollectionRoute = routeData.Route as IReadOnlyCollection<IHttpRoute>;
var newRoutes = new List<IHttpRoute>();
var newRouteCollectionRoute = new RouteCollectionRoute();
foreach (var route in routeCollectionRoute)
{
var filteredDataTokens = FilterDataTokens(route, apiControllerAssembly);
if (filteredDataTokens.Count == 2)
{
var newRoute = new HttpRoute(route.RouteTemplate, (HttpRouteValueDictionary)route.Defaults, (HttpRouteValueDictionary)route.Constraints, filteredDataTokens);
newRoutes.Add(newRoute);
}
}
var newRouteDataValues = new HttpRouteValueDictionary();
foreach (var routeDataKvp in routeData.Values)
{
var newRouteDataCollection = new List<IHttpRouteData>();
var routeDataCollection = routeDataKvp.Value as IEnumerable<IHttpRouteData>;
if (routeDataCollection != null)
{
foreach (var innerRouteData in routeDataCollection)
{
var filteredDataTokens = FilterDataTokens(innerRouteData.Route, apiControllerAssembly);
if (filteredDataTokens.Count == 2)
{
var newInnerRoute = new HttpRoute(innerRouteData.Route.RouteTemplate, (HttpRouteValueDictionary)innerRouteData.Route.Defaults, (HttpRouteValueDictionary)innerRouteData.Route.Constraints, filteredDataTokens);
var newInnerRouteData = new HttpRouteData(newInnerRoute, (HttpRouteValueDictionary)innerRouteData.Values);
newRouteDataCollection.Add(newInnerRouteData);
}
}
newRouteDataValues.Add(routeDataKvp.Key, newRouteDataCollection);
}
else
{
newRouteDataValues.Add(routeDataKvp.Key, routeDataKvp.Value);
}
HttpRouteData newRouteData;
if (newRoutes.Count > 1)
{
newRouteCollectionRoute.EnsureInitialized(() => newRoutes);
newRouteData = new HttpRouteData(newRouteCollectionRoute, newRouteDataValues);
}
else
{
newRouteData = new HttpRouteData(newRoutes[0], newRouteDataValues);
}
request.SetRouteData(newRouteData);
}
var controllerDescriptor = base.SelectController(request);
return controllerDescriptor;
}
private static HttpRouteValueDictionary FilterDataTokens(IHttpRoute route, string apiControllerAssembly)
{
var newDataTokens = new HttpRouteValueDictionary();
foreach (var dataToken in route.DataTokens)
{
var actionDescriptors = dataToken.Value as IEnumerable<HttpActionDescriptor>;
if (actionDescriptors != null)
{
var newActionDescriptors = new List<HttpActionDescriptor>();
foreach (var actionDescriptor in actionDescriptors)
{
if (actionDescriptor.ControllerDescriptor.ControllerType.Assembly.FullName == apiControllerAssembly)
{
newActionDescriptors.Add(actionDescriptor);
}
}
if (newActionDescriptors.Count > 0)
{
newDataTokens.Add(dataToken.Key, newActionDescriptors.ToArray());
}
}
else
{
newDataTokens.Add(dataToken.Key, dataToken.Value);
}
}
return newDataTokens;
}
}
CustomHttpActionSelector.cs:
You shouldn't need a CustomHttpActionSelector, this only exists to work around an issue with the ActionDescriptors for BController. It works as long as BController has only one method, otherwise you'll need to implement some route-specific logic.
public sealed class CustomHttpActionSelector : ApiControllerActionSelector
{
private static readonly ILog Logger;
static CustomHttpActionSelector()
{
Logger = LogProvider.GetCurrentClassLogger();
}
public override HttpActionDescriptor SelectAction(HttpControllerContext controllerContext)
{
try
{
var actionDescriptor = base.SelectAction(controllerContext);
return actionDescriptor;
}
catch (Exception ex)
{
Logger.WarnException(ex.Message, ex);
IDictionary<string, object> dataTokens;
var route = controllerContext.Request.GetRouteData().Route;
var routeCollectionRoute = route as IReadOnlyCollection<IHttpRoute>;
if (routeCollectionRoute != null)
{
dataTokens = routeCollectionRoute
.Select(r => r.DataTokens)
.SelectMany(dt => dt)
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
}
else
{
dataTokens = route.DataTokens;
}
var actionDescriptors = dataTokens
.Select(dt => dt.Value)
.Where(dt => dt is IEnumerable<HttpActionDescriptor>)
.Cast<IEnumerable<HttpActionDescriptor>>()
.SelectMany(r => r)
.ToList();
return actionDescriptors.FirstOrDefault();
}
}
}
Program.cs:
internal class Program
{
private static readonly ILog Logger;
static Program()
{
Log.Logger = new LoggerConfiguration()
.WriteTo
.LiterateConsole()
.MinimumLevel.Is(LogEventLevel.Verbose)
.CreateLogger();
Logger = LogProvider.GetCurrentClassLogger();
}
internal static void Main(string[] args)
{
var builder = new ContainerBuilder();
builder.RegisterModule(new LogRequestModule());
builder.RegisterApiControllers(typeof(AController).Assembly);
builder.RegisterApiControllers(typeof(BController).Assembly);
var container = builder.Build();
var config = GetHttpConfig();
config.DependencyResolver = new AutofacWebApiDependencyResolver(container);
var options = new StartOptions();
options.Urls.Add("http://localhost:1234");
options.Urls.Add("http://localhost:5678");
var listener = WebApp.Start(options, app =>
{
app.Use((ctx, next) =>
{
if (ctx.Request.LocalPort.HasValue)
{
var port = ctx.Request.LocalPort.Value;
string apiControllersAssemblyName = null;
if (port == 1234)
{
apiControllersAssemblyName = typeof(AController).Assembly.FullName;
}
else if (port == 5678)
{
apiControllersAssemblyName = typeof(BController).Assembly.FullName;
}
ctx.Set("ApiControllersAssembly", apiControllersAssemblyName);
Logger.Info($"{nameof(WebApp)}: Port = {port}, ApiControllersAssembly = {apiControllersAssemblyName}");
}
return next();
});
app.UseAutofacMiddleware(container);
app.UseAutofacWebApi(config);
app.UseWebApi(config);
});
Logger.Info(#"Press [Enter] to exit");
Console.ReadLine();
listener.Dispose(); ;
}
private static HttpConfiguration GetHttpConfig()
{
var config = new HttpConfiguration();
config.MapHttpAttributeRoutes();
config.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always;
config.Services.Add(typeof(IExceptionLogger), new LogProviderExceptionLogger());
config.Formatters.Remove(config.Formatters.XmlFormatter);
config.Services.Replace(typeof(IHttpControllerSelector), new CustomHttpControllerSelector(config));
config.Services.Replace(typeof(IHttpActionSelector), new CustomHttpActionSelector());
var traceSource = new TraceSource("LibLog") { Switch = { Level = SourceLevels.All } };
traceSource.Listeners.Add(new LibLogTraceListener());
var diag = config.EnableSystemDiagnosticsTracing();
diag.IsVerbose = false;
diag.TraceSource = traceSource;
return config;
}
}
LibA\Controllers\AController.cs:
[RoutePrefix("api/app")]
public class AController : ApiController
{
private static readonly ILog Logger;
static AController()
{
Logger = LogProvider.GetCurrentClassLogger();
Logger.Debug($"{nameof(AController)}: Static Constructor");
}
public AController()
{
Logger.Debug($"{nameof(AController)}: Constructor");
}
[HttpGet, Route("test")]
public async Task<IHttpActionResult> Get()
{
Logger.Debug($"{nameof(AController)}: Get()");
return Ok($"Hello from {nameof(AController)}");
}
}
LibB\Controllers\BController.cs:
[RoutePrefix("api/app")]
public class BController : ApiController
{
private static readonly ILog Logger;
static BController()
{
Logger = LogProvider.GetCurrentClassLogger();
Logger.Debug($"{nameof(BController)}: Static Constructor");
}
public BController()
{
Logger.Debug($"{nameof(BController)}: Constructor");
}
[HttpGet, Route("{*path}")]
public async Task<IHttpActionResult> Get([FromUri] string path)
{
if (path == null)
{
path = Request.RequestUri.PathAndQuery.Split(new[] {"api/app/"}, StringSplitOptions.RemoveEmptyEntries)[1];
}
Logger.Debug($"{nameof(BController)}: Get({path})");
using (var client = new HttpClient {BaseAddress = new Uri("http://localhost:1234/api/app/")})
{
var result = await client.GetAsync(path);
var content = await result.Content.ReadAsStringAsync();
return Ok($"(From {nameof(BController)}): {content}");
}
}
}
I might have another go at it when I have more time.
Let me know if you make any progress!
I made a repository class to access my DB and then I made a unit test using the FakeItEasy library. Using a real repository I got the expected result, while using the fake repository returns null.
[TestClass]
public class clientRepositoryTest
{
[TestMethod]
public void GetclientById()
{
var fakeRepository = A.Fake<IclientRepository>();
const int expectedclient = 1;
IclientRepository realRepository = new clientRepository();
var realResult = realRepository.GetclientById(expectedclient); // returns expected object
try
{
A.CallTo(() => fakeRepository.GetclientById(expectedclient)).MustHaveHappened();
var fakeResult = fakeRepository.GetSupplierById(expectedSupplier); // returns null
A.CallTo(() => fakeRepository.GetSupplierById(expectedSupplier).IdSupplier).Equals(expectedSupplier);
}
catch (Exception ex)
{
//The current proxy generator can not intercept the specified method for the following reason:
// - Non virtual methods can not be intercepted.
}
}
Before calling any actual function call you need to make sure to call all the internal fake call like below
A.CallTo(() => fakeRepository.GetclientById(expectedclient)).WithAnyArguments().Returns(Fakeobject/harcoded object);
Then go for the unit test call
var fakeResult = fakeRepository.GetSupplierById(expectedSupplier);
after that go for MustHaveHappened/ MustNotHaveHappened/Equals
A.CallTo(() => fakeRepository.GetclientById(expectedclient)).MustHaveHappened();
A.CallTo(() => fakeRepository.GetSupplierById(expectedSupplier).IdSupplier).Equals(expectedSupplier);
The Implementation should be like this
[TestClass]
public class clientRepositoryTest
{
[TestMethod]
public void GetclientById()
{
var fakeRepository = A.Fake<IclientRepository>();
const int expectedclient = 1;
IclientRepository realRepository = new clientRepository();
var realResult = realRepository.GetclientById(expectedclient); // returns expected object
try
{
A.CallTo(() => fakeRepository.GetclientById(expectedclient)).WithAnyArguments().Returns(Fakeobject/harcoded object);
var fakeResult = fakeRepository.GetSupplierById(expectedSupplier); // returns null
A.CallTo(() => fakeRepository.GetclientById(expectedclient)).MustHaveHappened();
A.CallTo(() => fakeRepository.GetSupplierById(expectedSupplier).IdSupplier).Equals(expectedSupplier);
}
catch (Exception ex)
{
//The current proxy generator can not intercept the specified method for the following reason:
// - Non virtual methods can not be intercepted.
}
}
I am trying to write some unit tests for my MVC3 project (the first tests for this project) and I am getting stumped. Basically, I have a MemberQueries class that is used by my MemberController to handle all the logic.
I want to start writing tests on this class and want to start with a simple example. I have a method in this class called IsEditModeAvailable which determines if the user is a member of the "Site Administrator" role or that the user is able to edit their own data, but no one elses. I determine the last requirement by comparing the passed in Id value to the HttpContext User property.
The problem that I'm running into is I don't know how to mock or inject the proper parameters into my unit tests when creating the MemberQueries object. I am using, NUnit, Moq and Ninject, but I'm just not sure how to write the code. If I'm just not structuring this properly, please let me know as I'm a complete noob to unit testing.
Here's a sample of the code from my MemberQueries class:
public class MemberQueries : IMemberQueries
{
private readonly IRepository<Member> _memberRepository;
private readonly IMemberServices _memberServices;
private readonly IPrincipal _currentUser;
public MemberQueries(IUnitOfWork unitOfWork, IMemberServices memberServices, IPrincipal currentUser)
{
_memberRepository = unitOfWork.RepositoryFor<Member>();
_memberServices = memberServices;
_currentUser = currentUser;
}
public bool IsEditModeAvailable(int memberIdToEdit)
{
if (_currentUser.IsInRole("Site Administrator")) return true;
if (MemberIsLoggedInUser(memberIdToEdit)) return true;
return false;
}
public bool MemberIsLoggedInUser(int memberIdToEdit)
{
var loggedInUser = _memberServices.FindByEmail(_currentUser.Identity.Name);
if (loggedInUser != null && loggedInUser.Id == memberIdToEdit) return true;
return false;
}
}
Here's a sample from my MemberServices class (which is in my domain project, referenced by MemberQueries):
public class MemberServices : IMemberServices
{
private readonly IRepository<Member> _memberRepository;
public MemberServices(IUnitOfWork unitOfWork)
{
_memberRepository = unitOfWork.RepositoryFor<Member>();
}
public Member Find(int id)
{
return _memberRepository.FindById(id);
}
public Member FindByEmail(string email)
{
return _memberRepository.Find(m => m.Email == email).SingleOrDefault();
}
}
Finally, here's the stub of the unit test I am trying to write:
[Test]
public void LoggedInUserCanEditTheirOwnInformation()
{
var unitOfWork = new UnitOfWork();
var currentUser = new Mock<IPrincipal>();
// I need to somehow tell Moq that the logged in user has a HttpContext.User.Name of "jdoe#acme.com"
var memberServices = new Mock<MemberServices>();
// I then need to tell Moq that it's FindByEmail("jdoe#acme.com") method should return a member with a UserId of 1
var memberQueries = new MemberQueries(unitOfWork, memberServices.Object, currentUser.Object);
// If the logged in user is "jdoe#acme.com" who has an Id of 1, then IsEditModeAvailable(1) should return true
Assert.IsTrue(memberQueries.IsEditModeAvailable(1));
}
It looks like you are trying to test the MemberQueries.IsEditModeAvailable method. You have 2 cases to cover here. The Site Administrators case and the case where there's a currently logged user whose id matches the one passed as argument. And since the MemberQueries class relies purely on interfaces you could mock everything:
[TestMethod]
public void EditMode_Must_Be_Available_For_Site_Administrators()
{
// arrange
var unitOfWork = new Mock<IUnitOfWork>();
var currentUser = new Mock<IPrincipal>();
currentUser.Setup(x => x.IsInRole("Site Administrator")).Returns(true);
var memberServices = new Mock<IMemberServices>();
var memberQueries = new MemberQueries(unitOfWork.Object, memberServices.Object, currentUser.Object);
// act
var actual = memberQueries.IsEditModeAvailable(1);
// assert
Assert.IsTrue(actual);
}
[TestMethod]
public void EditMode_Must_Be_Available_For_Logged_In_Users_If_His_Id_Matches()
{
// arrange
var unitOfWork = new Mock<IUnitOfWork>();
var currentUser = new Mock<IPrincipal>();
var identity = new Mock<IIdentity>();
identity.Setup(x => x.Name).Returns("john.doe#gmail.com");
currentUser.Setup(x => x.Identity).Returns(identity.Object);
currentUser.Setup(x => x.IsInRole("Site Administrator")).Returns(false);
var memberServices = new Mock<IMemberServices>();
var member = new Member
{
Id = 1
};
memberServices.Setup(x => x.FindByEmail("john.doe#gmail.com")).Returns(member);
var memberQueries = new MemberQueries(unitOfWork.Object, memberServices.Object, currentUser.Object);
// act
var actual = memberQueries.IsEditModeAvailable(1);
// assert
Assert.IsTrue(actual);
}
Actually there's a third case you need to cover: you have a currently logged in user, who is not a Site Administrator and whose id doesn't match the one passed as argument:
[TestMethod]
public void EditMode_Should_Not_Be_Available_For_Logged_In_Users_If_His_Id_Doesnt_Match()
{
// arrange
var unitOfWork = new Mock<IUnitOfWork>();
var currentUser = new Mock<IPrincipal>();
var identity = new Mock<IIdentity>();
identity.Setup(x => x.Name).Returns("john.doe#gmail.com");
currentUser.Setup(x => x.Identity).Returns(identity.Object);
currentUser.Setup(x => x.IsInRole("Site Administrator")).Returns(false);
var memberServices = new Mock<IMemberServices>();
var member = new Member
{
Id = 2
};
memberServices.Setup(x => x.FindByEmail("john.doe#gmail.com")).Returns(member);
var memberQueries = new MemberQueries(unitOfWork.Object, memberServices.Object, currentUser.Object);
// act
var actual = memberQueries.IsEditModeAvailable(1);
// assert
Assert.IsFalse(actual);
}
The good news is that you are passing the user as an IPrincipal into the code that needs it rather than referring to HttpContext.Current.User. All you should need to do is setup the mock IPrincipal so that it returns the vales you need for that test.
var mockIdentity = new Mock<IIdentity>();
mockIdentity.Setup(x => x.Name).Returns("joe#acme.com");
var mockPrincipal = new Mock<IPrincipal>();
mockPrincipal.Setup(x => x.Identity).Returns(mockIdentity.Object);