A Caché of Tips - .Net Proxy Classes

83 views
Skip to first unread message

Jason Warner

unread,
Jun 25, 2012, 10:15:44 AM6/25/12
to InterSy...@googlegroups.com
One of the great things about Cache is how easily it integrates with other technologies. Here at Brasher's, we rely heavily on .Net proxy classes for our .Net code. Proxy classes have some great benefits. They are essentially projections of Cache objects to the .Net framework so you can work with them in much the same way you write MV (or COS) code and they are very fast in our experience.

Proxy classes also have some drawbacks to consider when making a decision about whether or not to use them. First, they are generated against a version of your Cache classes. If the classes they were generated against change significantly, you will need to rebuild your proxies and recompile your application with the new proxy classes. Finally, if your underlying Cache classes are large, your proxy classes can grow quite large also.

To work with proxy classes, Cache has provided a great wizard to generate the .Net code for you. The tool is found at <CacheInstall>\dev\dotnet\bin\CacheNetWizard.exe. I would recommend adding this to your External Tools in Visual Studio.

  1. Go to the menu Tools->External Tools...
  2. In the dialog the appears select the "Add" button
  3. Fill out the information on the form. I usually name mine "Cache Proxy Tool". If I have multiple versions of Cache installed, I make sure to point to the version that we are using in production for the Wizard. Different versions of Cache will generate different .Net code.
  4. Press "OK" to add to your Tools menu

Once you have added the tool to Visual Studio, generating classes is a matter of starting the wizard by choosing the menu item.

  1. Choose the item you added from the Tools menu. You will be presented with a dialog
  2. Press the "Connect" button.
  3. Pick the server and namespace for the classes you want to generate. I recommend using the namespace that contains the same versions of your objects that you will be deploying this application against.
  4. Choose whether you want an assembly file or source files and the language you want the source to be generated in.
  5. Choose where you want this file to be generated.
  6. Choose the classes that you want to be projected to .Net.
    1. One thing to note is that any classes used by the object will also be projected to .Net. In the case of our Vehicle class, we have a property of type MVFILE.Dealer, a property of type MVFILE.Agent, and various other properties for classes. This means we get the Vehicle class in .Net along with an MVFILE.Dealer, an MVFILE.Agent and all the other classes used by our properties (or method return types). Keep this in mind because it can cause large proxy classes unexpectedly.
    2. A hint for proxy classes -- You will generate these classes many times. Because of this, we have started creating a text file that we add to our .Net projects (and source control) that is a list of all classes that we have projected to .Net. We also include the Application Namespace that our proxies were generated under. This way, when we need to generate the proxies again, we don't have to guess which classes we included and what namespace we used when generating the classes.
  7. Enter the .Net namespace where you want your proxy classes in your application in the "Application Namespace" text box.
    1. We usually put our proxy classes under a folder in our .Net project so our "Application Namespace" usually looks like MyProject.InterSystemsCache or something similar.
  8. Click "Generate" and in a short amount of time you will have your brand new proxy classes.
  9. Add your assembly to your project references or your proxy code to the appropriate spot in your project.

I skipped over some of the options that I have never personally used in the dialog. A lot of this same documentation can be found in the Cache docs under the .NET section "Using the Cache Managed Provider for .NET", chapter 5 "Using Cache Proxy Classes". This chapter has good documentation on all options for the .Net Wizard.

Now that you have generated your proxy classes, you need to add a references in your .Net project to be able to use them. The .dll is located at <CacheInstall>\dev\dotnet\bin\InterSystems.Data.CacheClient.dll. Make sure to use the .dll from the same folder that you chose for your proxy class wizard. This .dll contains everything necessary to interact with your proxy classes. In most classes you will add the following using statements to work with your proxy classes:

using InterSystems.Data.CacheClient;
using InterSystems.Data.CacheTypes;

I also recommend using a function to easily grab a connection for you. The function we use is:

		public static CacheConnection CacheConnect(string user, string password) {
			var conStr = new CacheConnectionStringBuilder {
			                                              	Server = WebConfigurationManager.AppSettings["server"],
			                                              	Port = int.Parse(WebConfigurationManager.AppSettings["port"]),
			                                              	Namespace = WebConfigurationManager.AppSettings["namespace"],
			                                              	User = user,
			                                              	Password = password,
			                                              	LogFile = null
			                                              };
 
 
			var con = new CacheConnection(conStr.ToString()) {ConnectionTimeout = 30, AppNamespace = "Atlas.InterSystemsCache"};
 
 
			return con;
		}

This is for our web application and it grabs settings from our web.config file so different locations can point to different Cache servers. One thing to point out about the code is the line where we actually connect.

var con = new CacheConnection(conStr.ToString()) {ConnectionTimeout = 30, AppNamespace = "Atlas.InterSystemsCache"};

In this line, we specify an AppNamespace. You need to provide this property with the exact namespace you chose from the .Net wizard above if you plan on calling methods from your proxy classes that will return a %ListOfObjects back to your .Net application. For example, we have a method that I can send in a sale date and it will return a %ListOfObjects back containing Brashers.Lane objects from Cache. In the .Net application, a lookup is required at runtime to determine what type of class the %ListOfObjects could contain. If you don't provide the AppNamespace pointing to the same place as your proxy classes you can get an error stating:

Cannot instantiate a proxy class for Brashers.Lane. Please check that you generated the proxy class 
and that the Application namespace is configured correctly.

Once you have this set up, you can start using your proxy classes. To do this, you need to get a CacheConnection (easy if you have a method to do it for you) and then open the object the same way you would in Cache. You have a projection of all the unique index opens that are available on the Cache side also. For example, here is some code used to do a vehicle lookup on our system. You can see that we are using the .Net managed provider to query the database and then using proxy classes to read the cars:

		public static List<VehicleFindModel> FindVehicles(string query, CacheConnection con) {
			var cars = new List<VehicleFindModel>();
			var url = new UrlHelper(HttpContext.Current.Request.RequestContext);
 
			using (CacheCommand com = con.CreateCommand()) {
				com.CommandText = "select"" id,"

					...snip...
					+ " from"" Brashers.VehicleBase"" where"" CarNumber = ?"" or INumber = ?"" or SerialNumber like ?";
 
				com.Parameters.Add(new CacheParameter("query", query.ToUpperInvariant()));
				com.Parameters.Add(new CacheParameter("query", query.ToUpperInvariant()));
				com.Parameters.Add(new CacheParameter("query""%" + query));
 
				using (CacheDataReader dr = com.ExecuteReader()) {
					while (dr.Read()) {
						string id = (dr["id"] ?? "").ToString();
 
						if (Vehicle.ExistsId(con, id) ?? false) {
							using(var veh = Vehicle.OpenId(con, id)) {
								cars.Add(CreateVehicleFindModel(url, veh, id));
							}
						}
						else if (VehicleArchive.ExistsId(con, id) ?? false) {
							using (var veh = VehicleArchive.OpenId(con, id)) {
								cars.Add(CreateVehicleFindModel(url, veh, id));
							}
						}
					}
				}
			}
 
			return cars;
		}

All proxy classes implement IDisposable so I recommend wrapping them with using() statements to clean up the resources they use to avoid memory leaks. As you can see, the code to open a proxy class is very similar to both the MV And COS methods:

							using(var veh = Vehicle.OpenId(con, id)) {
								cars.Add(CreateVehicleFindModel(url, veh, id));
							}

And the code from CreateVehicleFindModel demonstrates that it is easy to work with the classes too:

		private static VehicleFindModel CreateVehicleFindModel(UrlHelper url, VehicleBase veh, string id) {
			return new VehicleFindModel {
				Id = id,
				RunNumber = veh.CarNumber,
				RunNumberLink = RunNumberLink(veh, url),
				INumber = veh.INumber,
				Description = string.Format("{0} {1} {2} {3}", veh.Year, veh.MakeLongLC, veh.ModelLongLC, veh.TrimLC),
				BodyStyle = veh.Style,
				Color = veh.ExteriorColor == null ? null : veh.ExteriorColor.Name,
				Status = veh.VehicleStatus,
				SaleDate = ConnectionUtility.GetDateTime(veh.SaleDate),
				ArchiveDate = ConnectionUtility.GetDateTime(veh.ArchiveDate),
				Vin = veh.SerialNumber,
				ViewLink = url.Action("DispCar""Vehicle"new {id})
			};
		}

You interact with the objects in the same way that you would a Cache object or .Net object.

One thing to note is that conversion from a CacheDate or CacheTime to a .Net DateTime? can be problematic. For some reason, a null CacheDate or CacheTime does not convert to a null DateTime?. The CacheTime is particularly problematic for people using MV code. The reason is that Cache MV stores times as strings, but the .Net class expects a float. This causes conversion errors when trying to work with MV times in .Net. To get around these issues, we added a couple of methods:

		/// <summary>
		/// Gets the date from cache and puts it into a DateTime? variable.
		/// </summary>
		/// <param name="cacheDate">The cache date.</param>
		/// <returns></returns>
		public static DateTime? GetDateTime(CacheDate cacheDate) {
			if (cacheDate.IsNull)
				return null;
 
			return cacheDate;
		}
 
		/// <summary>
		/// Gets the time from cache and puts it into a DateTime? variable
		/// </summary>
		/// <param name="cacheTime">The cache time.</param>
		/// <returns></returns>
		public static DateTime? GetTimeOnly(CacheTime cacheTime)
		{
			if (cacheTime.IsNull)
				return null;
 
			return cacheTime;
		}
 
		/// <summary>
		/// Fixes the cache time.
		/// </summary>
		/// <typeparam name="T"></typeparam>
		/// <typeparam name="TU"></typeparam>
		/// <param name="cacheObject">The cache object.</param>
		/// <param name="expression">The expression.</param>
		/// <returns></returns>
		public static DateTime? FixCacheTime<T, TU>(T cacheObject, Expression<Func<TU>> expression)  where T : CacheObject {
			string propName = ReflectionHelper.GetPropertyName(expression);
 
			try {
				System.Threading.Monitor.Enter(cacheObject.Connection);
				cacheObject.AssertIsConnected();
				cacheObject.Connection.GeneratedAssembly = System.Reflection.Assembly.GetExecutingAssembly();
				CacheMethodSignature mtdSignature = cacheObject.Connection.GetMtdSignature();
				
				try {
					//We're going to assume all times are strings because of the Cache bug...
					mtdSignature.SetReturnType(cacheObject.Connection, 4);
					cacheObject.GetPropertyS(propName, mtdSignature);
 
					var timeString = ((CacheStringReturnValue)(mtdSignature.ReturnValue)).Value;
 
					return CacheToDateTime(timeString);
				}
				finally {
					mtdSignature.Clear();
					cacheObject.Connection.PostProcessing();
				}
			}
			finally {
				System.Threading.Monitor.Exit(cacheObject.Connection);
			}
		}
 
		private static DateTime? CacheToDateTime(string timeString) {
			if (!string.IsNullOrWhiteSpace(timeString)) {
				double ticks;
 
				if (double.TryParse(timeString, out ticks)) {
					var time = new CacheTime((long)(ticks * CacheTime.CacheTicksPerSecond));
 
					if (!time.IsNull)
						return time;
				}
			}
 
			return null;
		}
 
		public static List<DateTime?> FixCacheTimeArray<T, TU>(T cacheObject, Expression<Func<TU>> expression) where T : CacheObject {
			string propName = ReflectionHelper.GetPropertyName(expression);
			var toReturn = new List<DateTime?>();
 
			try {
				System.Threading.Monitor.Enter(cacheObject.Connection);
				cacheObject.AssertIsConnected();
				cacheObject.Connection.GeneratedAssembly = System.Reflection.Assembly.GetExecutingAssembly();
				CacheMethodSignature mtdSignature = cacheObject.Connection.GetMtdSignature();
 
				try {
					//We're going to assume all times are strings because of the Cache bug...
					mtdSignature.SetReturnType(cacheObject.Connection, 1, 4);
					cacheObject.GetPropertyS(propName, mtdSignature);
 
					var timeString = ((CacheListOfStrings)(((CacheObjReturnValue)(mtdSignature.ReturnValue)).Value));
 
					toReturn.AddRange(timeString.Select(CacheToDateTime));
				}
				finally {
					mtdSignature.Clear();
					cacheObject.Connection.PostProcessing();
				}
			}
			finally {
				System.Threading.Monitor.Exit(cacheObject.Connection);
			}
 
			return toReturn;
		}

The last two method rely on Cache's dynamic proxy classes. You can find this documented very well in the Cache docs mentioned above.

One last thing to mention is that some versions of Cache have a problem when using null values in collections. We have written some extension methods that allow us to get around this. They behave the same as the SetAt() and RemoveAt() available on collections in Cache. I have attached them to this email if you are interested.

I enjoyed this opportunity to share some of our experiences at Brasher's Auto Auctions with the community and look forward to sharing more in the future.

Jason Warner
Code Monkey
Brasher's Auto Auctions

CacheExtensionException.cs
CacheExtensions.cs

Lee Burstein

unread,
Jun 25, 2012, 10:29:29 AM6/25/12
to <intersystems-mv@googlegroups.com>
Thank you Jason for contributing to our Caché of Tips. It's always great to get a customer's perspective and practical experiences.

If any other customers would like to contribute please contact me so I can put you on our schedule.


On Jun 25, 2012, at 10:16 AM, Jason Warner wrote:

One of the great things about Cache is how easily it integrates with other technologies. Here at Brasher's, we rely heavily on .Net proxy classes for our .Net code. Proxy classes have some great benefits. They are essentially projections of Cache objects to the .Net framework so you can work with them in much the same way you write MV (or COS) code and they are very fast in our experience.

Proxy classes also have some drawbacks to consider when making a decision about whether or not to use them. First, they are generated against a version of your Cache classes. If the classes they were generated against change significantly, you will need to rebuild your proxies and recompile your application with the new proxy classes. Finally, if your underlying Cache classes are large, your proxy classes can grow quite large also.

To work with proxy classes, Cache has provided a great wizard to generate the .Net code for you. The tool is found at <CacheInstall>\dev\dotnet\bin\CacheNetWizard.exe. I would recommend adding this to your External Tools in Visual Studio.

  1. Go to the menu Tools->External Tools...
  1. In the dialog the appears select the "Add" button
  1. <Mail Attachment.png>
  1. Fill out the information on the form. I usually name mine "Cache Proxy Tool". If I have multiple versions of Cache installed, I make sure to point to the version that we are using in production for the Wizard. Different versions of Cache will generate different .Net code.
  1. Press "OK" to add to your Tools menu
  1. <Mail Attachment.png>

Once you have added the tool to Visual Studio, generating classes is a matter of starting the wizard by choosing the menu item.

  1. Choose the item you added from the Tools menu. You will be presented with a dialog
  1. <Mail Attachment.png>

--
You received this message because you are subscribed to the Google Groups "InterSystems: MV Community" group.
To post to this group, send email to Cac...@googlegroups.com
To unsubscribe from this group, send email to CacheMV-u...@googlegroups.com
For more options, visit this group at http://groups.google.com/group/CacheMV?hl=en<CacheExtensionException.cs><CacheExtensions.cs>


Lee H. Burstein
Technical Trainer
InterSystems
Office: 617-225-3145
Home Office: 302-477-0180
Cell: 302-345-0810

Tony Gravagno

unread,
Jun 25, 2012, 12:38:43 PM6/25/12
to InterSy...@googlegroups.com

Jason, thanks for that great posting. I spend most of my life in Visual Studio with C#, and the world between thick/thin clients and MV. So it's nice to see so much detail about that tier here. I only wish there were more sites looking to use .NET with MV on Caché. This is such a sub-sub-sub-specialty.

 

You're using a SQL-style query and data reader. Isn't there a way for MV developers to pass in a more MV-style Select? I'm guessing this would be done through a method which then executes a query in the class. In one respect that's good because it abstracts how data is selected from applications. On the other hand that would restrict .NET developers freedom in this area, and perhaps require more proxy regeneration. Have you dabbled with any of that?

 

And is ISC looking to incorporate your extensions into their own to eliminate some of the mismatch between tiers?

 

Regards,

T

 

Tony Gravagno

Nebula Research and Development

TG@ remove.pleaseNebula-RnD.com

Nebula-RnD.com/blog

Visit http://PickWiki.com! Contribute!

http://Twitter.com/TonyGravagno

http://groups.google.com/group/mvdbms/about

Jason Warner

unread,
Jun 25, 2012, 4:08:28 PM6/25/12
to intersy...@googlegroups.com
Tony,

> You're using a SQL-style query and data reader. Isn't there a way for
> MV developers to pass in a more MV-style Select? I'm guessing this
> would be done through a method which then executes a query in the
> class.

Correct. We have a class that creates information about sales for
vehicles. This data is only available through some MV queries and reads
of some MV files. To get this data into our .Net application, we had to
create custom queries in the class and then add the [SqlProc] tag to
the queries. From there, you treat them just like a stored procedure in
.Net. I actually prefer this method because it allows the .Net
developers to work with queries and result sets in a manner that they
are used to.

> In one respect that's good because it abstracts how data is
> selected from applications. On the other hand that would restrict .NET
> developers freedom in this area, and perhaps require more proxy
> regeneration. Have you dabbled with any of that?

One thing to note about proxy regeneration is that it seems to rely on
method signatures and properties. So for example, if you create a query
and generate proxies based on that query and then you modify the guts
of the query, but don't change the method signature, you can get by
without generating the proxy classes. Perhaps someone at InterSystems
can comment on what does and doesn't merit rebuilding proxy classes. We
test our code in our development environments and if a change throws an
exception about the proxies being out of date, we rebuild. If it
doesn't, then we assume that no proxy generation is needed for the
current build.

> And is ISC looking to incorporate your extensions into their own to
> eliminate some of the mismatch between tiers?

We have brought our issues to ISC and some of them have been fixed. For
example, adding null objects to a collection. I haven't approached
anyone with our fixes, so I doubt they have been incorporated.

Jason
Reply all
Reply to author
Forward
0 new messages