Unit testing memory leaks using xUnit - unit-testing

I recently read this article and it was all about finding, fixing and avoiding memory leaks. I had a feeling that my ASP.NET Core 3.1 app was leaking because it kept increasing the memory usage for the first few minutes but after I kept it opened for like 5 minutes, it sticked to 350 MB in the Diagnostic Tools in Visual Studio 2019. Everything is probably okay because I usually don't forget to dispose stuff (by using block) but I wanted to make sure because my web app is listening to web sockets event for lifetime unless a CancellationToken is requested by the user.
In that article, I saw that it's possible to test an object for memory leak.
[Test]
void MemoryLeakTest()
{
var weakRef = new WeakReference(leakyObject)
// Ryn an operation with leakyObject
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
Assert.IsFalse(weakRef.IsAlive);
}
Can the following code be tested for memory leaks in unit testing using xUnit? More specifically, timer and _subscription.
using Binance.Entity;
using Binance.Extensions;
using Binance.Helpers;
using Binance.Interfaces;
using Binance.Models.Redis;
using Binance.Net;
using Binance.Net.Interfaces;
using Binance.Net.Objects;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Sockets;
using Hangfire;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Binance.Services
{
public interface IBotService
{
void RunStart(Bot bot);
Task StopAsync();
}
public class BotService : IBotService, IDisposable
{
private readonly ILogger _logger;
private readonly IConfiguration _configuration;
private readonly IBinanceClient _client;
private readonly IBinanceSocketClient _socketClient;
private readonly INotificationService _notificationService;
public BotService(
ILogger<BotService> logger,
IConfiguration configuration,
IBinanceClient client,
IBinanceSocketClient socketClient,
INotificationService notificationService)
{
_logger = logger;
_configuration = configuration;
_client = client;
_socketClient = socketClient;
_notificationService = notificationService;
}
private void ApplyCredentialsOnClient()
{
BinanceClient.SetDefaultOptions(new BinanceClientOptions()
{
ApiCredentials = new ApiCredentials(_configuration.GetValue<string>("BinanceConfig:ApiKey"), _configuration.GetValue<string>("BinanceConfig:SecretKey")),
AutoTimestamp = true,
AutoTimestampRecalculationInterval = TimeSpan.FromMinutes(30)
});
BinanceSocketClient.SetDefaultOptions(new BinanceSocketClientOptions()
{
ApiCredentials = new ApiCredentials(_configuration.GetValue<string>("BinanceConfig:ApiKey"), _configuration.GetValue<string>("BinanceConfig:SecretKey")),
AutoReconnect = true,
ReconnectInterval = TimeSpan.FromHours(24)
});
}
private UpdateSubscription _subscription;
public void RunStart(Bot bot)
{
BackgroundJob.Enqueue(() => Start(bot));
}
public void Start(Bot bot)
{
ApplyCredentialsOnClient();
bool locked = false;
Timer timer = null;
BinanceKline lastKnownKline = null;
_subscription = _socketClient.SubscribeToKlineUpdates(bot.CryptoPair.Symbol, bot.TimeInterval.Interval, async data =>
{
if (data.Data.Final)
{
lastKnownKline = data.Data.ToKline();
// Static logic
// Clean up
if (locked)
{
locked = false;
timer?.Dispose();
}
}
// This block won't be executed before the static block is executed first
else if (lastKnownKline != null && lastKnownKline.Close != data.Data.Close)
{
lastKnownKline = data.Data.ToKline();
if (!locked)
{
locked = true;
timer = new Timer(async state =>
{
// Real time logic
bool condition = true;
if (condition)
{
timer?.Change(Timeout.Infinite, 0);
}
}, null, TimeSpan.Zero, TimeSpan.FromSeconds(scheduledTime));
}
}
}).Data;
}
public async Task StopAsync()
{
await _socketClient.Unsubscribe(_subscription);
}
private bool _disposed = false;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing)
{
_client.Dispose();
}
_disposed = true;
}
}
}

Related

How to debug a WCF call when it's a singleton and being run in a test

I am self-hosting a duplex-contract, WCF service.
In composing a test that exercises if my client is receiving messages from the service, I have found that I can't debug the service itself.
Thus, I made a simple example that seems to help me repeat the issue.
This is an example of the test I'm attempting:
[TestClass]
public class UnitTest1
{
[TestMethod]
public void TestMethod1()
{
ServiceRunner.Run(null);
var client = new ServiceReference1.Service1Client();
var result = client.GetData(11);
Assert.IsNotNull(result);
ServiceRunner.Host.Close();
}
}
ServiceRunner will host the WCF contract in a singleton. The client is from a service reference that points to the self-hosted service. When I call GetData(11) I get a response, it's just that my breakpoint in the service is never hit.
Why is that?
Here's the implementation of the service for completeness:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Description;
using System.Text;
namespace CanYouDebugThis
{
[ServiceContract]
public interface IService1
{
[OperationContract]
string GetData(int value);
}
[ServiceBehaviorAttribute(InstanceContextMode = InstanceContextMode.Single)]
public class Service1 : IService1
{
public string GetData(int value)
{
Console.WriteLine($"Get data with {value}");
return string.Format("You entered: {0}", value);
}
}
public class ServiceRunner
{
public static ServiceHost Host;
public static void Run(String[] args)
{
var serviceInstance = new Service1();
Uri baseAddress = new Uri("http://localhost:8080/hello");
Host = new ServiceHost(serviceInstance, baseAddress);
ServiceMetadataBehavior smb = new ServiceMetadataBehavior();
smb.HttpGetEnabled = true;
smb.MetadataExporter.PolicyVersion = PolicyVersion.Policy15;
Host.Description.Behaviors.Add(smb);
Host.Open();
}
}
}
There is something wrong with hosting the service. We should add service endpoint and MEX endpoint for exchanging metadata. Please refer to the below code segments.
public static ServiceHost Host;
public static void Main(String[] args)
{
var serviceInstance = new Service1();
Uri baseAddress = new Uri("http://localhost:8080/hello");
BasicHttpBinding binding = new BasicHttpBinding();
//Host = new ServiceHost(serviceInstance, baseAddress);
Host = new ServiceHost(typeof(Service1), baseAddress);
Host.AddServiceEndpoint(typeof(IService1), binding, "");
ServiceMetadataBehavior smb = new ServiceMetadataBehavior();
smb.HttpGetEnabled = true;
smb.MetadataExporter.PolicyVersion = PolicyVersion.Policy15;
Host.Description.Behaviors.Add(smb);
Binding mexbinding = MetadataExchangeBindings.CreateMexHttpBinding();
Host.AddServiceEndpoint(typeof(IMetadataExchange), mexbinding, "mex");
Host.Open();
Console.WriteLine("Service is ready...");
//pause, accepting a word would teminate the service.
Console.ReadLine();
Host.Close();
Console.WriteLine("Service is closed....");
}
Please host the service in an individual Console project first. Then on the client-side, we generate the client proxy by adding the service reference. Please pay attention to the auto-generated service endpoint, which should be corresponding to the actual server endpoint.
Result.
Feel free to let me know if there is anything I can help with.
Updated.
[TestClass]
public class UnitTest1
{
[TestMethod]
public void TestMethod1()
{
ServiceRunner.Run(null);
ServiceReference1.Service1Client client = new ServiceReference1.Service1Client();
var result = client.GetData(34);
Assert.IsNotNull(result);
}
}
[ServiceContract]
public interface IService1
{
[OperationContract]
string GetData(int value);
}
[ServiceBehaviorAttribute(InstanceContextMode = InstanceContextMode.Single)]
public class Service1 : IService1
{
public string GetData(int value)
{
Console.WriteLine($"Get data with {value}");
return string.Format("You entered: {0}", value);
}
}
public class ServiceRunner
{
public static ServiceHost Host;
public static void Run(String[] args)
{
var serviceInstance = new Service1();
Uri baseAddress = new Uri("http://localhost:8080/hello");
Host = new ServiceHost(serviceInstance, baseAddress);
ServiceMetadataBehavior smb = new ServiceMetadataBehavior();
smb.HttpGetEnabled = true;
smb.MetadataExporter.PolicyVersion = PolicyVersion.Policy15;
Host.Description.Behaviors.Add(smb);
Host.Open();
}
}
Result.
At last, please pay attention to the automatically generated client endpoint address.

Integrated Unit Testing is not running in ASP.NET core MVC/Web API

I am developing a web API using ASP.Net core. I am doing integrated testing to my project. I am following this link, https://koukia.ca/integration-testing-in-asp-net-core-2-0-51d14ede3968. This is my code.
I have the controller to be tested in the thegoodyard.api project.
namespace thegoodyard.api.Controllers
{
[Produces("application/json")]
[Route("api/category")]
public class CategoryController: Controller
{
[HttpGet("details/{id}")]
public string GetCategory(int id = 0)
{
return "This is the message: " + id.ToString();
}
}
}
I added a new unit test project called thegoodyard.tests to the solution. I added a TestServerFixture class with the following definition
namespace thegoodyard.tests
{
public class TestServerFixture : IDisposable
{
private readonly TestServer _testServer;
public HttpClient Client { get; }
public TestServerFixture()
{
var builder = new WebHostBuilder()
.UseContentRoot(GetContentRootPath())
.UseEnvironment("Development")
.UseStartup<Startup>(); // Uses Start up class from your API Host project to configure the test server
_testServer = new TestServer(builder);
Client = _testServer.CreateClient();
}
private string GetContentRootPath()
{
var testProjectPath = PlatformServices.Default.Application.ApplicationBasePath;
var relativePathToHostProject = #"..\..\..\..\..\..\thegoodyard.api";
return Path.Combine(testProjectPath, relativePathToHostProject);
}
public void Dispose()
{
Client.Dispose();
_testServer.Dispose();
}
}
}
Then again in the test project, I created a new class called, CategoryControllerTests with the following definition.
namespace thegoodyard.tests
{
public class CategoryControllerTests: IClassFixture<TestServerFixture>
{
private readonly TestServerFixture _fixture;
public CategoryControllerTests(TestServerFixture fixture)
{
_fixture = fixture;
}
[Fact]
public async Task GetCategoryDetai()
{
var response = await _fixture.Client.GetAsync("api/category/details/3");
response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync();
bool containMessage = false; //responseString.Contains("This is the message: 3"); - I commented on purpose to make the test fails.
Assert.True(containMessage);
}
}
}
Then I right on the test method and clicked run tests in the option to run the test. But none of the tests was run. This is the output.
What is missing in my code? How can I get my integrated test running?
Please check following NuGet packages in your project:
Microsoft.AspNetCore.TestHost
Microsoft.NET.Test.Sdk
xunit
xunit.runner.visualstudio
Perhaps there's a build error that's preventing the project from being compiled. There's really not enough information here to say for sure. Rebuild your solution, and ensure there's no errors.
Aside from that, you can remove some variables by reducing the test code needed. ASP.NET Core includes a WebApplicationFactory<TEntryPoint> fixture out of the box for bootstrapping a test server. You can therefore change your test code to just:
public class CategoryControllerTests: IClassFixture<WebApplicationFactory<Startup>>
{
private readonly WebApplicationFactory<Startup> _factory;
public CategoryControllerTests(WebApplicationFactory<Startup> factory)
{
_factory = factory;
}
[Fact]
public async Task GetCategoryDetail()
{
var client = _factory.CreateClient();
var response = await client.GetAsync("api/category/details/3");
...
See the documentation for additional information and more advanced scenarios.
This way works fine for xUnit based intergration tests, which use Startup configuration. the code blow also demonstrates how to override some settings in appSetting.json to specific values for testing, as well as how to access to DI services.
using System;
using System.Net.Http;
using MyNamespace.Web;
using MyNamespace.Services;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace MyNamespace.Tests
{
public class TestServerDependent : IDisposable
{
private readonly TestServerFixture _fixture;
public TestServer TestServer => _fixture.Server;
public HttpClient Client => _fixture.Client;
public TestServerDependent()
{
_fixture = new TestServerFixture();
var myService = GetService<IMyService>();
// myService.PerformAnyPreparationsForTests();
}
protected TService GetService<TService>()
where TService : class
{
return _fixture.GetService<TService>();
}
public void Dispose()
{
_fixture?.Dispose();
}
}
public class TestServerFixture : IDisposable
{
public TestServer Server { get; }
public HttpClient Client { get; }
public TestServerFixture()
{
var hostBuilder = WebHost.CreateDefaultBuilder()
.ConfigureAppConfiguration(
(builderContext, config) =>
{
var env = builderContext.HostingEnvironment;
config
.AddJsonFile("appsettings.json", optional: false)
.AddJsonFile("appsettings.Testing.json", optional: false,
reloadOnChange: true);
})
.ConfigureLogging(
(hostingContext, logging) =>
{
logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
logging.AddConsole();
logging.AddDebug();
})
.UseStartup<Startup>();
Server = new TestServer(hostBuilder);
Client = Server.CreateClient();
}
public void Dispose()
{
Server.Dispose();
Client.Dispose();
}
public TService GetService<TService>()
where TService : class
{
return Server?.Host?.Services?.GetService(typeof(TService)) as TService;
}
}
}
How the simple integration test might look like with the described above:
using System.Net;
using Xunit;
namespace MyNamespace.Tests
{
public class SimpleIntegrationTest : TestServerDependent
{
[Fact]
public void RedirectToLoginPage()
{
var httpResponseMessage = Client.GetAsync("/").Result;
// Smoke test to make sure you are redirected (to Login page for instance)
Assert.Equal(HttpStatusCode.Redirect, httpResponseMessage.StatusCode);
}
}
}

Why is my programatically added FaultContract not recognized?

I try to have my WCF services always throw detailed faults, even when not throwing them explicitly. To achieve it, I implemented:
an ErrorHandler, whose IErrorHandler.ProvideFault wraps the non-fault error as FaultException
a ServiceBehavior extension, attaching this handler AND adding to each operation a fault description of this FaultException, so the client might catch it as such.
I've decorated my service with the error handler attribute (originally I had two distinct implementations of IServiceBehavior, for the ErrorHandler and for the Operation.Faults).
I also made sure the data set into the new FaultDescription is identical to the one I inspected when defining the FaultContract on my contract.
No matter what I try, when using the FaultContract as attribute on my contract, the fault is being properly caught by the client, but when having it attached at runtime through the ApplyDispatchBehavior, only a general FaultException is being caught. Apparently, everything else (error wrapping and throwing) is working, only the addition of a FaultContract to each action at runtime fails.
Please help...
here's the code:
ErrorHandling.cs
using System;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
using System.Text;
using Shared.Contracts.Faults;
namespace Server.WcfExtensions
{
public class MyErrorHandler : IErrorHandler
{
#region IErrorHandler Members
public bool HandleError(Exception error)
{
return false;
}
public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
{
if (error is FaultException) return;
if (!error.GetType().IsSerializable) return;
FaultException<GeneralServerFault> faultExc = new FaultException<GeneralServerFault>(new GeneralServerFault(error), new FaultReason("Server Level Error"));
MessageFault messageFault = faultExc.CreateMessageFault();
fault = Message.CreateMessage(version, messageFault, faultExc.Action);
}
#endregion
}
class ErrorHandler : Attribute, IServiceBehavior
{
Type M_ErrorHandlerType;
public Type ErrorHandlerType
{
get { return M_ErrorHandlerType; }
set { M_ErrorHandlerType = value; }
}
#region IServiceBehavior Members
public void AddBindingParameters(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase, System.Collections.ObjectModel.Collection<ServiceEndpoint> endpoints, System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
{
}
public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
{
IErrorHandler errorHandler;
try
{
errorHandler = (IErrorHandler)Activator.CreateInstance(ErrorHandlerType);
}
catch (MissingMethodException e)
{
throw new ArgumentException("Must have a public empty constructor.", e);
}
catch (InvalidCastException e)
{
throw new ArgumentException("Must implement IErrorHandler.", e);
}
foreach (ChannelDispatcherBase channelDispatcherBase in serviceHostBase.ChannelDispatchers)
{
ChannelDispatcher channelDispatcher = channelDispatcherBase as ChannelDispatcher;
channelDispatcher.ErrorHandlers.Add(errorHandler);
}
foreach (ServiceEndpoint ep in serviceDescription.Endpoints)
{
foreach (OperationDescription opDesc in ep.Contract.Operations)
{
Type t = typeof(GeneralServerFault);
string name = t.Name;
FaultDescription faultDescription = new FaultDescription(ep.Contract.Namespace + "/" + ep.Contract.Name + "/" + opDesc.Name + name + "Fault");
faultDescription.Name = name + "Fault";
faultDescription.Namespace = ep.Contract.Namespace;
faultDescription.DetailType = t;
opDesc.Faults.Add(faultDescription);
}
}
}
public void Validate(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase)
{
}
#endregion
}
}
GeneralServerFault.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Threading.Tasks;
namespace Shared.Contracts.Faults
{
[DataContract] //[Serializable]
public class GeneralServerFault
{
[DataMember]
public SerializableException Wrapped
{
get;
private set;
}
public GeneralServerFault()
: base()
{
Wrapped = new SerializableException();
}
public GeneralServerFault(Exception toWrap)
: base()
{
Wrapped = new SerializableException(toWrap);
}
}
[Serializable]
public class SerializableException
{
public string Type { get; set; }
public DateTime TimeStamp { get; set; }
public string Message { get; set; }
public string StackTrace { get; set; }
public SerializableException()
{
this.TimeStamp = DateTime.Now;
}
public SerializableException(string Message)
: this()
{
this.Message = Message;
}
public SerializableException(System.Exception ex)
: this(ex.Message)
{
if (ex == null) return;
Type = ex.GetType().ToString();
this.StackTrace = ex.StackTrace;
}
public override string ToString()
{
return this.Type + " " + this.Message + this.StackTrace;
}
}
}
IContractService.cs
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.ServiceModel;
using Shared.Contracts.Faults;
namespace Shared
{
internal static class Namespaces
{
internal static class Contracts
{
public const string ServiceContracts = "http://mycompany/services";
}
}
[ServiceContract(Namespace = Namespaces.Contracts.ServiceContracts, SessionMode = SessionMode.Required)]
public interface IContactServices
{
[OperationContract]
[FaultContract(typeof(DataNotFoundFault))]
//[FaultContract(typeof(GeneralServerFault))]
void DoSomething();
}
}
ContractService.cs
using System;
using System.Collections.Generic;
using System.ServiceModel;
using Shared;
using Shared.Contracts.Faults;
using Server.WcfExtensions;
namespace Server.Services
{
[ErrorHandler(ErrorHandlerType = typeof(MyErrorHandler))]
public class ContactSevices : IContactServices
{
[OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = false)]
public void DoSomething()
{
throw new InvalidCastException("bla");
}
}
}
I omitted the code of client and host

Orchard CMS WCF Service with ServiceRoute

I have configured a Orchard module to expose a service and have enabled it. I cannot work out the URL to use based on the following.
Routes.cs
namespace OrchardRestService
{
using System.Collections.Generic;
using System.ServiceModel.Activation;
using Orchard.Mvc.Routes;
using Orchard.Wcf;
public class Routes : IRouteProvider
{
#region Implementation of IRouteProvider
public IEnumerable<RouteDescriptor> GetRoutes()
{
return new[] {
new RouteDescriptor {
Priority = 20,
Route = new ServiceRoute(
"ContentService",
new OrchardServiceHostFactory(),
typeof(IContentService))
}
};
}
public void GetRoutes(ICollection<RouteDescriptor> routes)
{
foreach (var routeDescriptor in GetRoutes())
routes.Add(routeDescriptor);
}
#endregion
}
}
IContentService.cs:
namespace OrchardRestService
{
using System.ServiceModel;
using Orchard;
[ServiceContract]
public interface IContentService : IDependency
{
[OperationContract]
ContentResult GetContent(string contentPath);
}
}
ContentService.cs:
namespace OrchardRestService
{
using System.Collections.Generic;
using System.ServiceModel.Activation;
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class ContentService : IContentService
{
public ContentResult GetContent(string contentPath)
{
var contentResult = new ContentResult
{ ContentValues = new Dictionary<string, string>(), Found = true, Path = contentPath };
return contentResult;
}
}
}
I've tried to follow what Bertrand Le Roy has written here and here but seem to be missing something.
My code is .Net 4 by the way so no need for an SVC file.
Closing as I'd be jumping through hoops to use N2 in this way rather than what it is designed for.

Unit Test a CSLA Asynchronous Validation Rule

I have a validation rule on a CSLA Business Base stereotyped class. I'm having trouble figuring out how to unit test the validation rule as it includes an asynchronous callback lambda expression. Here's some example code:
using System;
using System.Collections.Generic;
using Csla;
using Csla.Validation;
namespace UnitTestCSLAAsyncValidationRule
{
public class BusinessObject : BusinessBase<BusinessObject>
{
protected static PropertyInfo<string> CodeProperty = RegisterProperty<string>(p => p.Code);
public string Code
{
get { return GetProperty(CodeProperty); }
set { SetProperty(CodeProperty, value); }
}
protected override void AddBusinessRules()
{
ValidationRules.AddRule(CodeValidator, new AsyncRuleArgs(CodeProperty));
}
public static void CodeValidator(AsyncValidationRuleContext context)
{
var code = (string) context.PropertyValues["Code"];
CodeList codeList;
CodeList.GetCodeList((o, l) =>
{
codeList = l.Object;
if (codeList.Contains(code))
{
context.OutArgs.Result = false;
context.OutArgs.Description = "Code already in use.";
}
else
{
context.OutArgs.Result = true;
}
});
context.Complete();
}
}
public class CodeList : List<string>
{
public static void GetCodeList(EventHandler<DataPortalResult<CodeList>> handler)
{
DataPortal<CodeList> dp = new DataPortal<CodeList>();
dp.FetchCompleted += handler;
dp.BeginFetch();
}
private void DataPortal_Fetch()
{
// some existing codes..
Add("123");
Add("456");
}
}
}
I would like to test this with a test similar to the following:
using NUnit.Framework;
namespace UnitTestCSLAAsyncValidationRule.Test
{
[TestFixture]
public class BusinessObjectTest
{
[Test]
public void CodeValidationTest()
{
var bo = new BusinessObject();
bo.Code = "123";
Assert.IsNotEmpty(bo.BrokenRulesCollection);
}
}
}
However, the test Assert runs before the async callback. Is this something UnitDriven could help with? I've had a look at it but can't see how to use it in this scenario.
Thanks,
Tom
Answered by JonnyBee on http://forums.lhotka.net/forums/p/10023/47030.aspx#47030:
using NUnit.Framework;
using UnitDriven;
namespace UnitTestCSLAAsyncValidationRule.Test
{
[TestFixture]
public class BusinessObjectTest : TestBase
{
[Test]
public void CodeValidationTest()
{
UnitTestContext context = GetContext();
var bo = new BusinessObject();
bo.ValidationComplete += (o, e) =>
{
context.Assert.IsFalse(bo.IsValid);
context.Assert.Success();
//Assert.IsNotEmpty(bo.BrokenRulesCollection);
};
bo.Code = "123";
context.Complete();
}
}
}
Please not there was a small bug in my validation rule method - the call to AsyncValidationRuleContext.Complete() needs to be inside the lambda.
Thanks,
Tom