Working sample at the end of this article (I kept all things I tried [reason for long article], so that others could benefit from it later)
I am trying to write integration tests for my EF Core 3.1 class library. As unit test framework, I have used XUnit and followed guide from Microsoft: https://learn.microsoft.com/en-us/ef/core/testing/sharing-databases
Here is how the setup looks like (it's a bit longer because I am actually creating a database in my SQL Server in case I need to see the real result from tests output):
public class SharedDatabaseFixture : IDisposable
{
private static readonly object _lock = new object();
private static bool _databaseInitialized;
private static string _DatabaseName = "Database.Server.Local";
private static IConfigurationRoot config;
public SharedDatabaseFixture()
{
config = new ConfigurationBuilder()
.AddJsonFile($"appsettings.Development.json", true, true)
.Build();
var test = config.GetValue<string>("DataSource");
var connectionStringBuilder = new SqlConnectionStringBuilder
{
DataSource = config.GetValue<string>("DataSource"),
InitialCatalog = _DatabaseName,
IntegratedSecurity = true,
};
var connectionString = connectionStringBuilder.ToString();
Connection = new SqlConnection(connectionString);
CreateEmptyDatabaseAndSeedData();
Connection.Open();
}
public bool ShouldSeedActualData { get; set; } = true;
public DbConnection Connection { get; set; }
public ApplicationDbContext CreateContext(DbTransaction transaction = null)
{
var identity = new GenericIdentity("admin#sample.com", "Admin");
var contextUser = new ClaimsPrincipal(identity); //add claims as needed
var httpContext = new DefaultHttpContext() { User = contextUser };
var defaultHttpContextAccessor = new HttpContextAccessor();
defaultHttpContextAccessor.HttpContext = httpContext;
var context = new ApplicationDbContext(new DbContextOptionsBuilder<ApplicationDbContext>().UseSqlServer(Connection).Options, null, defaultHttpContextAccessor);
if (transaction != null)
{
context.Database.UseTransaction(transaction);
}
return context;
}
private static void ExecuteSqlCommand(SqlConnectionStringBuilder connectionStringBuilder, string commandText)
{
using (var connection = new SqlConnection(connectionStringBuilder.ConnectionString))
{
connection.Open();
using (var command = connection.CreateCommand())
{
command.CommandText = commandText;
command.ExecuteNonQuery();
}
}
}
private static SqlConnectionStringBuilder Master => new SqlConnectionStringBuilder
{
DataSource = config.GetValue<string>("DataSource"),
InitialCatalog = "master",
IntegratedSecurity = true
};
private static string Filename => Path.Combine(Path.GetDirectoryName(typeof(SharedDatabaseFixture).GetTypeInfo().Assembly.Location), $"{_DatabaseName}.mdf");
private static string LogFilename => Path.Combine(Path.GetDirectoryName(typeof(SharedDatabaseFixture).GetTypeInfo().Assembly.Location), $"{_DatabaseName}_log.ldf");
private static void CreateDatabaseRawSQL()
{
ExecuteSqlCommand(Master, $#"IF(db_id(N'{_DatabaseName}') IS NULL) BEGIN CREATE DATABASE [{_DatabaseName}] ON (NAME = '{_DatabaseName}', FILENAME = '{Filename}') END");
}
private static List<T> ExecuteSqlQuery<T>(SqlConnectionStringBuilder connectionStringBuilder, string queryText, Func<SqlDataReader, T> read)
{
var result = new List<T>();
using (var connection = new SqlConnection(connectionStringBuilder.ConnectionString))
{
connection.Open();
using (var command = connection.CreateCommand())
{
command.CommandText = queryText;
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
result.Add(read(reader));
}
}
}
}
return result;
}
private static void DestroyDatabaseRawSQL()
{
var fileNames = ExecuteSqlQuery(Master, $#"SELECT [physical_name] FROM [sys].[master_files] WHERE [database_id] = DB_ID('{_DatabaseName}')", row => (string)row["physical_name"]);
if (fileNames.Any())
{
ExecuteSqlCommand(Master, $#"ALTER DATABASE [{_DatabaseName}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;EXEC sp_detach_db '{_DatabaseName}', 'true'");
fileNames.ForEach(File.Delete);
}
if (File.Exists(Filename))
File.Delete(Filename);
if (File.Exists(LogFilename))
File.Delete(LogFilename);
}
private void CreateEmptyDatabaseAndSeedData()
{
lock (_lock)
{
if (!_databaseInitialized)
{
using (var context = CreateContext())
{
try
{
DestroyDatabaseRawSQL();
}
catch (Exception) { }
try
{
CreateDatabaseRawSQL();
context.Database.EnsureCreated();
}
catch (Exception) { }
if (ShouldSeedActualData)
{
List<UserDB> entities = new List<UserDB>()
{
new UserDB() { Id = "Admin#sample.com", Name= "Admin" }
};
context.Users.AddRange(entities);
context.SaveChanges();
List<IdentityRole> roles = new List<IdentityRole>()
{
new IdentityRole(){Id = "ADMIN",Name = nameof(DefaultRoles.Admin), NormalizedName = nameof(DefaultRoles.Admin)},
new IdentityRole(){Id = "FINANCE",Name = nameof(DefaultRoles.Finance), NormalizedName = nameof(DefaultRoles.Finance)}
};
context.Roles.AddRange(roles);
context.SaveChanges();
}
}
_databaseInitialized = true;
}
}
}
public void Dispose()
{
Connection.Dispose();
}
}
Then, test class looks like following (for simplicity showing just 2 tests):
public class BaseRepositoryTests : IClassFixture<SharedDatabaseFixture>
{
private readonly SharedDatabaseFixture fixture;
private IMapper _mapper;
public BaseRepositoryTests(SharedDatabaseFixture fixture)
{
this.fixture = fixture;
var config = new MapperConfiguration(opts =>
{
opts.AddProfile<CountriesDBMapper>();
opts.AddProfile<EmployeeDBMapper>();
opts.AddProfile<EmployeeAccountDBMapper>();
});
_mapper = config.CreateMapper();
}
[Fact]
public async Task EntityCannotBeSavedIfDbEntityIsNotValid()
{
using (var transaction = fixture.Connection.BeginTransaction())
{
using (var context = fixture.CreateContext(transaction))
{
var baseCountryRepository = new BaseRepository<CountryDB, Country>(context, _mapper);
var invalidCountry = new Country() { };
//Act
var exception = await Assert.ThrowsAsync<DbUpdateException>(async () => await baseCountryRepository.CreateAsync(invalidCountry));
Assert.NotNull(exception.InnerException);
Assert.Contains("Cannot insert the value NULL into column", exception.InnerException.Message);
}
}
}
[Fact]
public async Task EntityCanBeSavedIfEntityIsValid()
{
using (var transaction = fixture.Connection.BeginTransaction())
{
using (var context = fixture.CreateContext(transaction))
{
var baseCountryRepository = new BaseRepository<CountryDB, Country>(context, _mapper);
var item = new Country() { Code = "SK", Name = "Slovakia" };
//Act
var result = await baseCountryRepository.CreateAsync(item);
Assert.NotNull(result);
Assert.Equal(1, result.Id);
}
}
}
}
Finally here is a sample of repository implementation (CRUD):
public async Task<TModel> CreateAsync(TModel data)
{
var newItem = mapper.Map<Tdb>(data);
var entity = await context.Set<Tdb>().AddAsync(newItem);
await context.SaveChangesAsync();
return mapper.Map<TModel>(entity.Entity);
}
public async Task<bool> DeleteAsync(long id)
{
var item = await context.Set<Tdb>().FindAsync(id).ConfigureAwait(false);
if (item == null)
throw new ArgumentNullException();
var result = context.Set<Tdb>().Remove(item);
await context.SaveChangesAsync();
return (result.State == EntityState.Deleted || result.State == EntityState.Detached);
}
If I run these tests individually, each one of them passes without a problem. However if I run all tests from BaseRepositoryTests then I am getting random problems as somehow, database transactions are not rolled back but the data is saved and shared between tests.
I have checked and truly, each transaction has it's own unique ID, thus they should not collide. Am I missing anything here? I mean according to Microsoft this is correct approach, but there is clearly something I have missed. The only thing different from other guides that I could find is, that I am using SaveChangesAsync in my repository implementation, while other's use SaveChanges...however I believe that this should not be the root cause of my problem.
Any help in respect to this matter would be highly appreciated.
Update 1:
As suggested by comments, I have tried two separate approaches. First one was to use CommitableTransaction like following:
Method update:
[Fact]
public async Task EntityCanBeSavedIfEntityIsValid()
{
using (var transaction = new CommittableTransaction(new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
{
using (var context = fixture.CreateContext(transaction))
{
var baseCountryRepository = new BaseRepository<CountryDB, Country>(context, _mapper);
var item = new Country() { Code = "SK", Name = "Slovakia" };
//Act
var result = await baseCountryRepository.CreateAsync(item);
Assert.NotNull(result);
Assert.Equal(1, result.Id);
}
}
}
Shared fixture update:
public ApplicationDbContext CreateContext(CommittableTransaction transaction = null)
{
... other code
if (transaction != null)
{
context.Database.EnlistTransaction(transaction);
}
return context;
}
This unfortunately ended with the same result when running my code tests in bulk (data that I was saving ended up being incremented and not discarded after each test)
Second thing I tried was using TransactionScope like following:
[Fact]
public async Task EntityCanBeModifiedIfEntityExistsAndIsValid()
{
using (var scope = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions { IsolationLevel = IsolationLevel.ReadUncommitted }, TransactionScopeAsyncFlowOption.Enabled))
{
using (var context = fixture.CreateContext())
{
var baseCountryRepository = new BaseRepository<CountryDB, Country>(context, _mapper);
var item = new Country() { Code = "SK", Name = "Slovakia" };
//Act
var insertResult = await baseCountryRepository.CreateAsync(item);
Assert.NotNull(insertResult);
Assert.Equal(1, insertResult.Id);
Assert.Equal("SK", insertResult.Code);
Assert.Equal("Slovakia", insertResult.Name);
//Act
insertResult.Code = "SVK";
var result = await baseCountryRepository.UpdateAsync(insertResult.Id, insertResult);
Assert.Equal(1, result.Id);
Assert.Equal("SVK", result.Code);
Assert.Equal("Slovakia", result.Name);
}
scope.Complete();
}
}
Just as before this did not yield any new results.
Last thing that I tried was to drop :IClassFixture<SharedDatabaseFixture> from the test class and instead, create a new instance of my database fixture in constructor (which is being triggered for each test run) like following:
public BaseRepositoryTests()
{
this.fixture = new SharedDatabaseFixture();
var config = new MapperConfiguration(opts =>
{
opts.AddProfile<CountriesDBMapper>();
opts.AddProfile<EmployeeDBMapper>();
opts.AddProfile<EmployeeAccountDBMapper>();
});
_mapper = config.CreateMapper();
}
Just as before, no new results came from this update.
Working setup
Shared database fixture (basically class responsible for creating database...the main difference between previous version now is, that in constructor it accepts unique guid that is used when creating database -> to create a database with unique name. Furthermore I have also added a new method ForceDestroyDatabase() which is responsible to destroy the database after test has done it's job. I did not place it in Dispose() method, as sometimes you want to check what actually happened to database, where in that case you just don't call the method...see later)
public class SharedDatabaseFixture : IDisposable
{
private static readonly object _lock = new object();
private static bool _databaseInitialized;
private string _DatabaseName = "FercamPortal.Server.Local.";
private static IConfigurationRoot config;
public SharedDatabaseFixture(string guid)
{
config = new ConfigurationBuilder()
.AddJsonFile($"appsettings.Development.json", true, true)
.Build();
var test = config.GetValue<string>("DataSource");
this._DatabaseName += guid;
var connectionStringBuilder = new SqlConnectionStringBuilder
{
DataSource = config.GetValue<string>("DataSource"),
InitialCatalog = _DatabaseName,
IntegratedSecurity = true,
};
var connectionString = connectionStringBuilder.ToString();
Connection = new SqlConnection(connectionString);
CreateEmptyDatabaseAndSeedData();
Connection.Open();
}
...other code the same as above, skipped for clarity
private void CreateEmptyDatabaseAndSeedData()
{
lock (_lock)
{
using (var context = CreateContext())
{
try
{
DestroyDatabaseRawSQL();
}
catch (Exception ex) { }
try
{
CreateDatabaseRawSQL();
context.Database.EnsureCreated();
}
catch (Exception) { }
if (ShouldSeedActualData)
{
List<UserDB> entities = new List<UserDB>()
{
new UserDB() { Id = "Robert_Jokl#swissre.com", Name= "Robert Moq" },
new UserDB() { Id = "Test_User#swissre.com", Name= "Test User" }
};
context.Users.AddRange(entities);
context.SaveChanges();
List<IdentityRole> roles = new List<IdentityRole>()
{
new IdentityRole(){Id = "ADMIN",Name = nameof(FercamDefaultRoles.Admin), NormalizedName = nameof(FercamDefaultRoles.Admin)},
new IdentityRole(){Id = "FINANCE",Name = nameof(FercamDefaultRoles.Finance), NormalizedName = nameof(FercamDefaultRoles.Finance)}
};
context.Roles.AddRange(roles);
context.SaveChanges();
}
}
}
}
public void ForceDestroyDatabase()
{
DestroyDatabaseRawSQL();
}
public void Dispose()
{
Connection.Close();
Connection.Dispose();
}
}
Sample test class:
public class DailyTransitDBRepositoryTests : IDisposable
{
private readonly SharedDatabaseFixture fixture;
private readonly ApplicationDbContext context;
private IMapper _mapper;
public DailyTransitDBRepositoryTests()
{
this.fixture = new SharedDatabaseFixture(Guid.NewGuid().ToString("N"));
this.context = this.fixture.CreateContext();
this.context.Database.OpenConnection();
var config = new MapperConfiguration(opts =>
{
opts.AddProfile<DailyTransitDBMapper>();
opts.AddProfile<EmployeeDBMapper>();
opts.AddProfile<EmployeeAccountDBMapper>();
opts.AddProfile<CountriesDBMapper>();
});
_mapper = config.CreateMapper();
}
...other code ommited for clarity
public void Dispose()
{
this.context.Database.CloseConnection();
this.context.Dispose();
this.fixture.ForceDestroyDatabase();
this.fixture.Dispose();
}
[Fact]
public async Task GetTransitsForYearAndMonthOnlyReturnsValidItems()
{
var employees = await PopulateEmployeesAndReturnThemAsList(context);
var countries = await PopulateCountriesAndReturnThemAsList(context);
var transitRepository = new DailyTransitDBRepository(context, _mapper);
var transitItems = new List<DailyTransit>() {
new DailyTransit()
{
Country = countries.First(),
Employee = employees.First(),
Date = DateTime.Now,
TransitionDurationType = DailyTransitDurationEnum.FullDay
},
new DailyTransit()
{
Country = countries.First(),
Employee = employees.Last(),
Date = DateTime.Now.AddDays(1),
TransitionDurationType = DailyTransitDurationEnum.FullDay
},
new DailyTransit()
{
Country = countries.First(),
Employee = employees.Last(),
Date = DateTime.Now.AddMonths(1),
TransitionDurationType = DailyTransitDurationEnum.FullDay
}
};
//Act
await transitRepository.CreateRangeAsync(transitItems);
//retrieve all items
using (var context2 = fixture.CreateContext())
{
var transitRepository2 = new DailyTransitDBRepository(context2, _mapper);
var items = await transitRepository2.GetEmployeeTransitsForYearAndMonth(DateTime.Now.Year, DateTime.Now.Month);
Assert.Equal(2, items.Count());
Assert.Equal("Janko", items.First().Employee.Name);
Assert.Equal("John", items.Last().Employee.Name);
}
}
}
Robert, Glad It helped! As per your request, I re-submit the answer for anyone that could find this answer helpful as you.
I learn the hard way that trying to share the entity framework database context over IClassFixture or CollectionFixtures would eventually end up in tests being polluted with another test data or deadlock/race conditions due to the parallel execution of xUnit, entity framework throwing exceptions because it already tracked that object with a given Id and more headaches like that.
Personally, I would kindly recommend that for your specific use cause, stick the database context creation/cleanup within the constructor/dispose alternative such as:
public class TestClass : IDisposable
{
DatabaseContext DatabaseContext;
public TestClass()
{
var options = new DbContextOptionsBuilder<DatabaseContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
DatabaseContext = new DatabaseContext(options);
//insert the data that you want to be seeded for each test method:
DatabaseContext.Set<Product>().Add(new Product() { Id = 1, Name = Guid.NewGuid().ToString() });
DatabaseContext.SaveChanges();
}
[Fact]
public void FirstTest()
{
var product = DatabaseContext.Set<Product>().FirstOrDefault(x => x.Id == 1).Name;
//product evaluates to => 0f25a10b-1dfd-4b4b-a69d-4ec587fb465b
}
[Fact]
public void SecondTest()
{
var product = DatabaseContext.Set<Product>().FirstOrDefault(x => x.Id == 1).Name;
//product evaluates to => eb43d382-40a5-45d2-8da9-236d49b68c7a
//It's different from firstTest because is another object
}
public void Dispose()
{
DatabaseContext.Dispose();
}
}
Of course you can always do some refinement, but the idea is there
I have these methods below that call some SOAP web services, all from the same provider, so they all have the same methods/calls/etc. I'm looking for a more OOP/abstract way to call these without writing so many methods? Ideally I'd like one method for each -> GetClaim(), AddClaim(), SearchClaim(), RemoveClaim(), etc.
Question - Should I pass in the parameters specific to the service to make the method more generic, there by eliminating 15 other methods all like this or is there a better more oop/abstract approach? Can somebody please provide me with an example?
// ex. how can I make these two methods 1?
public async void ClaimSearchForWRG(string url, string userName, string password) {
var client = new WebServiceWRGClient();
var binding = new BasicHttpBinding(BasicHttpSecurityMode.Transport);
var endpoint = new EndpointAddress(url);
var channelFactory = new ChannelFactory<WebServiceWRG>(binding, endpoint);
var webService = channelFactory.CreateChannel();
var user = new User();
user.UserName = await webService.EncryptValueAsync(userName);
user.Password = await webService.EncryptValueAsync(password);
var response = await client.ClaimSearchAsync(user, "", "", 12345, statuscode.NotSet, "");
}
// another call (same provider) with the same call -> ClaimSearchAsync()
public async void ClaimSearchForAWI(string url, string userName, string password) {
var client = new WebServiceAWIClient();
var binding = new BasicHttpBinding(BasicHttpSecurityMode.Transport);
var endpoint = new EndpointAddress(url);
var channelFactory = new ChannelFactory<WebServiceAWI>(binding, endpoint);
var webService = channelFactory.CreateChannel();
var user = new ArmUser();
user.UserName = await webService.EncryptValueAsync(userName);
user.Password = await webService.EncryptValueAsync(password);
var response = await client.ClaimSearchAsync(user, "", "", 12345, ArmStatuscode.NotSet, "");
}
// then we have 15 other web service calls from the same provider for ClaimSearchAsync()
// then we have 15 more calls for ClaimGetAsync()
// then we have 15 more calls for AddClaimAsync()
// then we have 15 more calls for RemoveClaimAsync()
// etc, etc, etc
UPDATED After trying this code below to make things a little more generic (to eliminate redundancy) I'm getting some errors in the code. Specifically related to the compiler not finding the properties associated with the generic entities I'm passing into the method. ex. user.Username is not found -> error message says "'TTwo' does not contain a definition for 'UserName'"
public class Test {
public void TestWebService() {
var ws = new WebService<WebServiceWRG>();
ws.SearchClaim(new WebServiceWRGClient(), new GraceUser(),
"https://trustonline.delawarecpf.com/tows/webservicewrg.svc", "userName", "password");
}
}
public class WebService<T> {
public void SearchClaim<TOne, TTwo>(TOne entity1, TTwo entity2, string url, string userName, string password)
where TOne : class
where TTwo : class
{
var client = entity1;
var binding = new BasicHttpBinding(BasicHttpSecurityMode.Transport);
var endpoint = new EndpointAddress(url);
var channelFactory = new ChannelFactory<T>(binding, endpoint);
var webService = channelFactory.CreateChannel();
var user = entity2;
user.UserName = webService.EncryptValue(userName);
user.Password = webService.EncryptValue(password);
var response = client.ClaimSearch(user, "", "", 12345, GraceStatuscode.NotSet, "");
}
}
UPDATED I was asked to show what "ClaimSearchAsync" does or what it is. I copied this from the web service reference file that was generated from dotnet
System.Threading.Tasks.Task<GRACE_GRACES.WebServiceResult> ClaimSearchAsync(GRACE_GRACES.User user, string ssn, string lastname, int claimnumber, GRACE_GRACES.statuscode statuscode, string assignedto);
as this is a web service, there is no method or code behind that shows what it does.
The provided example methods all violate Single Responsibility Principle (SRP) and Separation of Concerns (SoC) so that is where I started in trying to make them more generic.
The creation of the service and service client should be abstracted out into their own concerns
For example, the web services can be created via a generic factory abstraction
public interface IWebServiceFactory {
TWebService Create<TWebService>(string uri);
}
and simple implementation which encapsulates the creation of the channel factory using the provided URL.
public class ServiceFactory : IWebServiceFactory {
public TWebService Create<TWebService>(string url) {
var binding = new BasicHttpBinding(BasicHttpSecurityMode.Transport) {
MaxReceivedMessageSize = Int32.MaxValue,
MaxBufferSize = Int32.MaxValue
};
var endpoint = new EndpointAddress(url);
var channelFactory = new ChannelFactory<TWebService>(binding, endpoint);
TWebService webService = channelFactory.CreateChannel();
return webService;
}
}
The service clients' creation can also be abstracted out into it own concern.
public interface IClientFactory {
TClient Create<TClient>() where TClient : class, new();
}
to be implemented based on the common definition of your clients.
Now for the creation of a generic client you need to take the common functionality expected from the types involved with the member to be invoked.
This can allow for a convention to be used for the expected types. Dynamic expressions were used to construct the conventions applied.
Resulting in the following helpers for the SearchClaimAsync
static class ExpressionHelpers {
public static Func<string, string, TUserResult> CreateUserDelegate<TUserResult>() {
var type = typeof(TUserResult);
var username = type.GetProperty("username", BindingFlags.Instance | BindingFlags.IgnoreCase | BindingFlags.Public);
var password = type.GetProperty("password", BindingFlags.Instance | BindingFlags.IgnoreCase | BindingFlags.Public);
//string username =>
var usernameSource = Expression.Parameter(typeof(string), "username");
//string password =>
var passwordSource = Expression.Parameter(typeof(string), "password");
// new TUser();
var user = Expression.New(type);
// new TUser() { UserName = username, Password = password }
var body = Expression.MemberInit(user, bindings: new[] {
Expression.Bind(username, usernameSource),
Expression.Bind(password, passwordSource)
});
// (string username, string password) => new TUser() { UserName = username, Password = password }
var expression = Expression.Lambda<Func<string, string, TUserResult>>(body, usernameSource, passwordSource);
return expression.Compile();
}
public static Func<TService, string, Task<string>> CreateEncryptValueDelegate<TService>() {
// (TService service, string name) => service.EncryptValueAsync(name);
var type = typeof(TService);
// TService service =>
var service = Expression.Parameter(type, "service");
// string name =>
var name = Expression.Parameter(typeof(string), "name");
// service.EncryptValueAsync(name)
var body = Expression.Call(service, type.GetMethod("EncryptValueAsync"), name);
// (TService service, string name) => service.EncryptValueAsync(name);
var expression = Expression.Lambda<Func<TService, string, Task<string>>>(body, service, name);
return expression.Compile();
}
public static Func<TClient, TUser, Task<TResponse>> CreateClaimSearchDelegate<TClient, TUser, TResponse>() {
var type = typeof(TClient);
// TClient client =>
var client = Expression.Parameter(type, "client");
// TUser user =>
var user = Expression.Parameter(typeof(TUser), "user");
var method = type.GetMethod("ClaimSearchAsync");
var enumtype = method.GetParameters()[4].ParameterType; //statuscode
var enumDefault = Activator.CreateInstance(enumtype);
var arguments = new Expression[] {
user,
Expression.Constant(string.Empty), //ssn
Expression.Constant(string.Empty), //lastname
Expression.Constant(12345), //claimnumber
Expression.Constant(enumDefault), //statuscode
Expression.Constant(string.Empty)//assignto
};
// client.ClaimSearchAsync(user, ssn: "", lastname: "", claimnumber: 12345, statuscode: default(enum), assignedto: "");
var body = Expression.Call(client, method, arguments);
// (TClient client, TUser user) => client.ClaimSearchAsync(user,....);
var expression = Expression.Lambda<Func<TClient, TUser, Task<TResponse>>>(body, client, user);
return expression.Compile();
}
}
Take some time to review the comments to get a better understanding of what is being done.
The generic web service can then be defined as follows
public class WebService<TWebServiceClient, TWebService, TUser>
where TWebService : class
where TWebServiceClient : class, new()
where TUser : class, new() {
/// <summary>
/// Create user object model
/// </summary>
private static readonly Func<string, string, TUser> createUser =
ExpressionHelpers.CreateUserDelegate<TUser>();
/// <summary>
/// Encrypt provided value using <see cref="TWebService"/>
/// </summary>
private static readonly Func<TWebService, string, Task<string>> encryptValueAsync =
ExpressionHelpers.CreateEncryptValueDelegate<TWebService>();
private readonly IWebServiceFactory serviceFactory;
private readonly IClientFactory clientFactory;
Lazy<TWebServiceClient> client;
public WebService(IWebServiceFactory serviceFactory, IClientFactory clientFactory) {
this.serviceFactory = serviceFactory ?? throw new ArgumentNullException(nameof(serviceFactory));
this.clientFactory = clientFactory ?? throw new ArgumentNullException(nameof(clientFactory));
client = new Lazy<TWebServiceClient>(() => clientFactory.Create<TWebServiceClient>());
}
public async Task<TResponse> SearchClaimAsync<TResponse>(WebServiceOptions options) {
TWebService webService = serviceFactory.Create<TWebService>(options.URL);
TUser user = createUser(
await encryptValueAsync(webService, options.UserName),
await encryptValueAsync(webService, options.Password)
);
Func<TWebServiceClient, TUser, Task<TResponse>> claimSearchAsync =
ExpressionHelpers.CreateClaimSearchDelegate<TWebServiceClient, TUser, TResponse>();
TResponse response = await claimSearchAsync.Invoke(client.Value, user);
return response;
}
//...other generic members to be done
}
public class WebServiceOptions {
public string URL { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
}
The code it self is decoupled enough from implementation concerns to allow for it to be tested in isolation to ensure that it behaves as expected.
As demonstrated in the following unit tested
[TestClass]
public class GenericWebServiceTests {
[TestMethod]
public void Should_Create_New_WebService() {
//Arrange
var serviceFactory = Mock.Of<IWebServiceFactory>();
var clientFactory = Mock.Of<IClientFactory>();
//Act
var actual = new WebService<WebServiceWRGClient, IWebService, User1>(serviceFactory, clientFactory);
//Assert
actual.Should().NotBeNull();
}
[TestMethod]
public async Task Should_ClaimSearchAsync() {
//Arrange
var service = Mock.Of<IWebService>();
Mock.Get(service)
.Setup(_ => _.EncryptValueAsync(It.IsAny<string>()))
.ReturnsAsync((string s) => s);
var serviceFactory = Mock.Of<IWebServiceFactory>();
Mock.Get(serviceFactory)
.Setup(_ => _.Create<IWebService>(It.IsAny<string>()))
.Returns(service);
var clientFactory = Mock.Of<IClientFactory>();
Mock.Get(clientFactory)
.Setup(_ => _.Create<WebServiceWRGClient>())
.Returns(() => new WebServiceWRGClient());
string url = "url";
string username = "username";
string password = "password";
var options = new WebServiceOptions {
URL = url,
UserName = username,
Password = password
};
var webService = new WebService<WebServiceWRGClient, IWebService, User1>(serviceFactory, clientFactory);
//Act
var actual = await webService.SearchClaimAsync<WebServiceResult>(options);
//Assert
//Mock.Get(serviceFactory).Verify(_ => _.Create<IService1>(url));
//Mock.Get(service).Verify(_ => _.EncryptValue(username));
//Mock.Get(service).Verify(_ => _.EncryptValue(password));
//Mock.Get(clientFactory).Verify(_ => _.Create<Client1>());
actual.Should().NotBeNull();
}
#region Support
public class User1 {
public string UserName { get; set; }
public string Password { get; set; }
}
public class User2 {
public string UserName { get; set; }
public string Password { get; set; }
}
public class WebServiceWRGClient {
public Task<WebServiceResult> ClaimSearchAsync(User1 user, string ssn, string lastname, int claimnumber, statuscode statuscode, string assignedto) {
return Task.FromResult(new WebServiceResult());
}
}
public enum statuscode {
NotSet = 0,
}
public class Client2 { }
public interface IWebService {
Task<string> EncryptValueAsync(string value);
}
public interface IService2 {
Task<string> EncryptValueAsync(string value);
}
public class Service1 : IWebService {
public Task<string> EncryptValueAsync(string value) {
return Task.FromResult(value);
}
}
public class WebServiceResult {
}
#endregion
}
This should be enough to get you started in reviewing the other members to be made generic. The above provided code has been tested and works as expected based on what was provided in the original question.
Do note that this does seem like a large task depending on the amount of members to be refactored. You should take some time to make sure the effort is even worth it.
You have classical Divergent Change smell here.
Signs and Symptoms. You find yourself having to change many unrelated methods when you make changes to a class. For example, when adding a new product type you have to change the methods for finding, displaying, and ordering products.
I suggest to make refactoring to Abstract Factory pattern. You will separate web service and object creation logic.
Abstract Factory is a creational design pattern that lets you produce families of related objects without specifying their concrete classes.
So you will have something like:
And some code:
public interface IFactory
{
Client CreateClient();
User CreateUser();
Channel CreateChannel(BasicHttpBinding binding, EndpointAddress endpoint);
}
abstract public class AbstractFactory<T> : IFactory
{
public abstract Client CreateClient()
public abstract User CreateUser();
public Channel CreateChannel(BasicHttpBinding binding, EndpointAddress endpoint)
{
var channelFactory = new ChannelFactory<T>(binding, endpoint);
return channelFactory.CreateChannel();
}
}
public class AWIFactory : AbstractFactory<WebServiceAWI>
{
public override Client CreateClient()
{
return new WebServiceAWIClient();
}
public override User CreateUser()
{
return new ArmUser();
}
}
public class WRGFactory : AbstractFactory<WebServiceWRG>
{
public override Client CreateClient()
{
return new WebServiceWRGClient();
}
public override User CreateUser()
{
return new User();
}
}
public class WebService
{
private readonly IFactory _factory;
public WebService(IFactory factory)
{
_factory = factory;
}
public async void ClaimSearchAsync(string url, string userName, string password)
{
var client = _factory.CreateClient();
var binding = new BasicHttpBinding(BasicHttpSecurityMode.Transport);
var endpoint = new EndpointAddress(url);
var channel = _factory.CreateChannel(binding, endpoint);
var user = _factory.CreateUser();
user.UserName = await channel.EncryptValueAsync(userName);
user.Password = await channel.EncryptValueAsync(password);
var response = await client.ClaimSearchAsync(user, "", "", 12345, statusCode, "");
}
...
}
And here is how you create WebService:
var wrgWebService = new WebService(new WRGFactory());
I did something similar when I had several different soap endpoints, where every endpoint had some types that were completely the same, only with a different class name. Automatically generated classes contain the partial modifier, which enables you to add additional logic to the generated class.
In your case:
"'TTwo' does not contain a definition for 'UserName'"
You have to create an interface that contains a property Username and a property Password:
public interface IUser {
string UserName { get; }
string Password { get; }
}
public partial User : IUser { } //must be in the correct namespace for partial to work
public partial ArmUser : IUser { } //must be in the correct namespace for partial to work
public class Test {
public void TestWebService() {
var ws = new WebService<WebServiceWRG>();
ws.SearchClaim(new WebServiceWRGClient(), new GraceUser(),
"https://trustonline.delawarecpf.com/tows/webservicewrg.svc", "userName", "password");
}
}
public class WebService<T> {
public void SearchClaim<TOne, TTwo>(TOne entity1, TTwo entity2, string url, string userName, string password)
where TOne : class
where TTwo : IUser // limits the TTwo class to implement IUser
{
var client = entity1;
var binding = new BasicHttpBinding(BasicHttpSecurityMode.Transport);
var endpoint = new EndpointAddress(url);
var channelFactory = new ChannelFactory<T>(binding, endpoint);
var webService = channelFactory.CreateChannel();
var user = entity2;
user.UserName = webService.EncryptValue(userName);
user.Password = webService.EncryptValue(password);
var response = client.ClaimSearch(user, "", "", 12345, GraceStatuscode.NotSet, "");
}
}
Instead of passing TTwo, you can then also add the new modifier to the TTwo condition (where T : TTwo, new()), then you can generate an instance of TTwo inside the SearchClaim function, which would make it look like the following:
public interface IUser {
string UserName { get; set; }
string Password { get; set; }
}
public partial User : IUser { } //must be in the correct namespace for partial to work
public partial ArmUser : IUser { } //must be in the correct namespace for partial to work
public class Test {
public void TestWebService() {
var ws = new WebService<WebServiceWRG>();
ws.SearchClaim(new WebServiceWRGClient(), new GraceUser(),
"https://trustonline.delawarecpf.com/tows/webservicewrg.svc", "userName", "password");
}
}
public class WebService<T> {
public void SearchClaim<TOne, TTwo>(TOne entity1, string url, string userName, string password)
where TOne : class
where TTwo : IUser, new() // limits the TTwo class to implement IUser
{
var client = entity1;
var binding = new BasicHttpBinding(BasicHttpSecurityMode.Transport);
var endpoint = new EndpointAddress(url);
var channelFactory = new ChannelFactory<T>(binding, endpoint);
var webService = channelFactory.CreateChannel();
var user = new TTwo();
user.UserName = webService.EncryptValue(userName);
user.Password = webService.EncryptValue(password);
var response = client.ClaimSearch(user, "", "", 12345, GraceStatuscode.NotSet, "");
}
}
You might also have to make some interface for your TOne, but you should be able to figure that out on your own.
Not sure how is your code structure, but I'll just focus on the provided sample.
From what I've seen, if this method and the other related methods are used in different classes, I would suggest to create a class that will handle it, and then use this class instead of the methods. But if this method and other related methods are used in a specific class, I would recommend to create a generic methods that will substitute the redundant methods. You'll need to compare all related methods first, and get the common numerator between them, make this a start point for your generic approach.
Here is untested example (based on what I understood from your sample) :
public class CallWebService<T> // don't forget to inherit IDisposal.
{
private WebServiceWRGClient Client {get; set;}
private BasicHttpBinding HttpBinding {get; set;}
private EndpointAddress Endpoint {get; set;}
private ChannelFactory Channel {get; set;}
// if needed outside this class, make it public to be accessed globally.
private User UserAccount {get; set;}
public CallWebService<T>(string url)
{
Client = new WebServiceWRGClient();
//See which Binding is the default and use it in this constructor.
HttpBinding = new BasicHttpBinding(BasicHttpSecurityMode.Transport);
Endpoint = new EndpointAddress(url);
// T is generic, WebServiceWRG in this example
Channel = new ChannelFactory<T>(HttpBinding, Endpoint).CreateChannel();
UserAccount = new User();
}
// another constructor with BasicHttpBinding
public CallWebService<T>(string url, BasicHttpSecurityMode securityMode)
{
Client = new WebServiceWRGClient();
//See which Binding is the default and use it in this constructor.
HttpBinding = new BasicHttpBinding(securityMode);
Endpoint = new EndpointAddress(url);
// T is generic, WebServiceWRG in this example
Channel = new ChannelFactory<T>(HttpBinding, Endpoint).CreateChannel();
UserAccount = new User();
}
// Change this method to return the response. Task<Response> is just a placeholder for this example
public async Task<Response> Call(string userName, string password)
{
UserAccount.UserName = await Channel.EncryptValueAsync(userName);
UserAccount.Password = await Channel.EncryptValueAsync(password);
var response = await Client.ClaimSearchAsync(User, "", "", 12345, statuscode.NotSet, "");
}
/*
[To-Do] : gather all other releated methods into this class, then try to simplify them.
*/
}
You can also configure the constructors as needed, for instance, you can make constructors that takes WebServiceWRGClient and BasicHttpBinding ..etc. So, it's more open to you.
You could do similar approach if it'll be used across the project, but if it's only used in one class, then you could do something like this :
// Configure it as needed, but avoid using `void` with async, as the exceptions in sync and async methods handled differently.
// Also, try to make sense here, make the method return the results.
public async Task CallWebService<T>(WebServiceWRGClient client, string url, string userName, string password)
{
var channelFactory = new ChannelFactory<T>(new BasicHttpBinding(BasicHttpSecurityMode.Transport, new EndpointAddress(url)).CreateChannel();
var user = new User(); // coming from service reference
user.UserName = await channelFactory.EncryptValueAsync(userName);
user.Password = await channelFactory.EncryptValueAsync(password);
var response = await client.ClaimSearchAsync(user, "", "", 12345, statuscode.NotSet, "");
}