Translator.cs source code in C# .NET

Source code for the .NET framework in C#

                        

Code:

/ 4.0 / 4.0 / untmp / DEVDIV_TFS / Dev10 / Releases / RTMRel / ndp / fx / src / DLinq / Dlinq / SqlClient / Query / Translator.cs / 1305376 / Translator.cs

                            using System; 
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
using System.Collections.ObjectModel; 
using System.Text;
using System.Data.Linq; 
using System.Data.Linq.Mapping; 
using System.Data.Linq.Provider;
using System.Linq; 
using System.Diagnostics.CodeAnalysis;

namespace System.Data.Linq.SqlClient {
 

    internal class Translator { 
        IDataServices services; 
        SqlFactory sql;
        TypeSystemProvider typeProvider; 

        internal Translator(IDataServices services, SqlFactory sqlFactory, TypeSystemProvider typeProvider) {
            this.services = services;
            this.sql = sqlFactory; 
            this.typeProvider = typeProvider;
        } 
 
        internal SqlSelect BuildDefaultQuery(MetaType rowType, bool allowDeferred, SqlLink link, Expression source) {
            System.Diagnostics.Debug.Assert(rowType != null && rowType.Table != null); 
            if (rowType.HasInheritance && rowType.InheritanceRoot != rowType) {
                // RowType is expected to be an inheritance root.
                throw Error.ArgumentWrongValue("rowType");
            } 
            SqlTable table = sql.Table(rowType.Table, rowType, source);
            SqlAlias tableAlias = new SqlAlias(table); 
            SqlAliasRef tableAliasRef = new SqlAliasRef(tableAlias); 

            SqlExpression projection = this.BuildProjection(tableAliasRef, table.RowType, allowDeferred, link, source); 
            return new SqlSelect(projection, tableAlias, source);
        }

        internal SqlExpression BuildProjection(SqlExpression item, MetaType rowType, bool allowDeferred, SqlLink link, Expression source) { 
            if (!rowType.HasInheritance) {
                return this.BuildProjectionInternal(item, rowType, (rowType.Table != null) ? rowType.PersistentDataMembers : rowType.DataMembers, allowDeferred, link, source); 
            } 
            else {
                // Build a type case that represents a switch between the various type. 
                List mappedTypes = new List(rowType.InheritanceTypes);
                List whens = new List();
                SqlTypeCaseWhen @else = null;
 
                MetaType root = rowType.InheritanceRoot;
                MetaDataMember discriminator = root.Discriminator; 
                Type dt = discriminator.Type; 
                SqlMember dm = sql.Member(item, discriminator.Member);
 
                foreach (MetaType type in mappedTypes) {
                    if (type.HasInheritanceCode) {
                        SqlNew defaultProjection = this.BuildProjectionInternal(item, type, type.PersistentDataMembers, allowDeferred, link, source);
                        if (type.IsInheritanceDefault) { 
                            @else = new SqlTypeCaseWhen(null, defaultProjection);
                        } 
                        // Add an explicit case even for the default. 
                        // Redundant results will be optimized out later.
                        object code = InheritanceRules.InheritanceCodeForClientCompare(type.InheritanceCode, dm.SqlType); 
                        SqlExpression match = sql.Value(dt, sql.Default(discriminator), code, true, source);
                        whens.Add(new SqlTypeCaseWhen(match, defaultProjection));
                    }
                } 
                if (@else == null) {
                    throw Error.EmptyCaseNotSupported(); 
                } 
                whens.Add(@else);   // Add the else at the end.
 
                return sql.TypeCase(root.Type, root, dm, whens.ToArray(), source);
            }
        }
 
        /// 
        ///  Check whether this member will be preloaded. 
        ///  
        private bool IsPreloaded(MemberInfo member) {
            if (this.services.Context.LoadOptions == null) { 
                return false;
            }
            return this.services.Context.LoadOptions.IsPreloaded(member);
        } 

        private SqlNew BuildProjectionInternal(SqlExpression item, MetaType rowType, IEnumerable members, bool allowDeferred, SqlLink link, Expression source) { 
            List bindings = new List(); 
            foreach (MetaDataMember mm in members) {
                if (allowDeferred && (mm.IsAssociation || mm.IsDeferred)) { 
                    // check if this member is the reverse association to the supplied link
                    if (link != null && mm != link.Member && mm.IsAssociation
                        && mm.MappedName == link.Member.MappedName
                        && !mm.Association.IsMany 
                        && !IsPreloaded(link.Member.Member)) {
                        // place a new link here with an expansion that is previous link's root expression. 
                        // this will allow joins caused by reverse association references to 'melt' away. :-) 
                        SqlLink mlink = this.BuildLink(item, mm, source);
                        mlink.Expansion = link.Expression; 
                        bindings.Add(new SqlMemberAssign(mm.Member, mlink));
                    }
                    else {
                        bindings.Add(new SqlMemberAssign(mm.Member, this.BuildLink(item, mm, source))); 
                    }
                } 
                else if (!mm.IsAssociation) { 
                    bindings.Add(new SqlMemberAssign(mm.Member, sql.Member(item, mm)));
                } 
            }
            ConstructorInfo cons = rowType.Type.GetConstructor(BindingFlags.Instance|BindingFlags.Public|BindingFlags.NonPublic, null, System.Type.EmptyTypes, null);
            if (cons == null) {
                throw Error.MappedTypeMustHaveDefaultConstructor(rowType.Type); 
            }
            return sql.New(rowType, cons, null, null, bindings, source); 
        } 

        private SqlLink BuildLink(SqlExpression item, MetaDataMember member, Expression source) { 
            if (member.IsAssociation) {
                SqlExpression[] exprs = new SqlExpression[member.Association.ThisKey.Count];
                for (int i = 0, n = exprs.Length; i < n; i++) {
                    MetaDataMember mm = member.Association.ThisKey[i]; 
                    exprs[i] = sql.Member(item, mm.Member);
                } 
                MetaType otherType = member.Association.OtherType; 
                return new SqlLink(new object(), otherType, member.Type, typeProvider.From(member.Type), item, member, exprs, null, source);
            } 
            else {
                // if not association link is always based on primary key
                MetaType thisType = member.DeclaringType;
                System.Diagnostics.Debug.Assert(thisType.IsEntity); 
                List exprs = new List();
                foreach (MetaDataMember mm in thisType.IdentityMembers) { 
                    exprs.Add(sql.Member(item, mm.Member)); 
                }
                SqlExpression expansion = sql.Member(item, member.Member); 
                return new SqlLink(new object(), thisType, member.Type, typeProvider.From(member.Type), item, member, exprs, expansion, source);
            }
        }
 
        internal SqlNode TranslateLink(SqlLink link, bool asExpression) {
            return this.TranslateLink(link, link.KeyExpressions, asExpression); 
        } 

        ///  
        /// Create an Expression representing the given association and key value expressions.
        /// 
        internal static Expression TranslateAssociation(DataContext context, MetaAssociation association, Expression otherSource, Expression[] keyValues, Expression thisInstance) {
            if (association == null) 
                throw Error.ArgumentNull("association");
            if (keyValues == null) 
                throw Error.ArgumentNull("keyValues"); 

            if (context.LoadOptions!=null) { 
                LambdaExpression subquery = context.LoadOptions.GetAssociationSubquery(association.ThisMember.Member);
                if (subquery!=null) {
                    RelationComposer rc = new RelationComposer(subquery.Parameters[0], association, otherSource, thisInstance);
                    return rc.Visit(subquery.Body); 
                }
            } 
            return WhereClauseFromSourceAndKeys(otherSource, association.OtherKey.ToArray(), keyValues); 
        }
 
        internal static Expression WhereClauseFromSourceAndKeys(Expression source, MetaDataMember[] keyMembers, Expression [] keyValues) {
            Type elementType = TypeSystem.GetElementType(source.Type);
            ParameterExpression p = Expression.Parameter(elementType, "p");
            Expression whereExpression=null; 
            for (int i = 0; i < keyMembers.Length; i++) {
                MetaDataMember metaMember = keyMembers[i]; 
                Expression parameterAsDeclaring = elementType == metaMember.Member.DeclaringType ? 
                    (Expression)p : (Expression)Expression.Convert(p, metaMember.Member.DeclaringType);
                Expression memberExpression = (metaMember.Member is FieldInfo) 
                    ? Expression.Field(parameterAsDeclaring, (FieldInfo)metaMember.Member)
                    : Expression.Property(parameterAsDeclaring, (PropertyInfo)metaMember.Member);
                Expression keyValue = keyValues[i];
                if (keyValue.Type != memberExpression.Type) 
                    keyValue = Expression.Convert(keyValue, memberExpression.Type);
                Expression memberEqualityExpression = Expression.Equal(memberExpression, keyValue); 
                whereExpression = (whereExpression != null) 
                    ? Expression.And(whereExpression, memberEqualityExpression)
                    : memberEqualityExpression; 
            }
            Expression sequenceExpression = Expression.Call(typeof(Enumerable), "Where", new Type[] {p.Type}, source, Expression.Lambda(whereExpression, p));
            return sequenceExpression;
        } 

        ///  
        /// Composes a subquery into a linked association. 
        /// 
        private class RelationComposer : ExpressionVisitor { 
            ParameterExpression parameter;
            MetaAssociation association;
            Expression otherSouce;
            Expression parameterReplacement; 
            internal RelationComposer(ParameterExpression parameter, MetaAssociation association, Expression otherSouce, Expression parameterReplacement) {
                if (parameter==null) 
                    throw Error.ArgumentNull("parameter"); 
                if (association == null)
                    throw Error.ArgumentNull("association"); 
                if (otherSouce == null)
                    throw Error.ArgumentNull("otherSouce");
                if (parameterReplacement==null)
                    throw Error.ArgumentNull("parameterReplacement"); 
                this.parameter = parameter;
                this.association = association; 
                this.otherSouce = otherSouce; 
                this.parameterReplacement = parameterReplacement;
            } 
            internal override Expression VisitParameter(ParameterExpression p) {
                if (p == parameter) {
                    return this.parameterReplacement;
                } 
                return base.VisitParameter(p);
            } 
 
            private static Expression[] GetKeyValues(Expression expr, ReadOnlyCollection keys) {
                List values = new List(); 
                foreach(MetaDataMember key in keys){
                    values.Add(Expression.PropertyOrField(expr, key.Name));
                }
                return values.ToArray(); 
            }
 
            internal override Expression VisitMemberAccess(MemberExpression m) { 
                if (MetaPosition.AreSameMember(m.Member, this.association.ThisMember.Member)) {
                    Expression[] keyValues = GetKeyValues(this.Visit(m.Expression), this.association.ThisKey); 
                    return WhereClauseFromSourceAndKeys(this.otherSouce, this.association.OtherKey.ToArray(), keyValues);
                }
                Expression exp = this.Visit(m.Expression);
                if (exp != m.Expression) { 
                    if (exp.Type != m.Expression.Type && m.Member.Name == "Count" && TypeSystem.IsSequenceType(exp.Type)) {
                        return Expression.Call(typeof(Enumerable), "Count", new Type[] {TypeSystem.GetElementType(exp.Type)}, exp); 
                    } 
                    return Expression.MakeMemberAccess(exp, m.Member);
                } 
                return m;
            }

        } 

        internal SqlNode TranslateLink(SqlLink link, List keyExpressions, bool asExpression) { 
            MetaDataMember mm = link.Member; 

            if (mm.IsAssociation) { 
                // Create the row source.
                MetaType otherType = mm.Association.OtherType;
                Type tableType = otherType.InheritanceRoot.Type;
                ITable table = this.services.Context.GetTable(tableType); 
                Expression source = new LinkedTableExpression(link, table, typeof(IQueryable<>).MakeGenericType(otherType.Type));
                // Build key expression nodes. 
                Expression[] keyExprs = new Expression[keyExpressions.Count]; 
                for (int i = 0; i < keyExpressions.Count; ++i) {
                    MetaDataMember metaMember = mm.Association.OtherKey[i]; 
                    Type memberType = TypeSystem.GetMemberType(metaMember.Member);
                    keyExprs[i] = InternalExpression.Known(keyExpressions[i], memberType);
                }
                Expression lex = link.Expression != null 
                    ? (Expression)InternalExpression.Known(link.Expression)
                    : (Expression)Expression.Constant(null, link.Member.Member.DeclaringType); 
                Expression expr = TranslateAssociation(this.services.Context, mm.Association, source, keyExprs, lex); 
                // Convert
                QueryConverter qc = new QueryConverter(this.services, this.typeProvider, this, this.sql); 
                SqlSelect sel = (SqlSelect)qc.ConvertInner(expr, link.SourceExpression);
                // Turn it into an expression is necessary
                SqlNode result = sel;
                if (asExpression) { 
                    if (mm.Association.IsMany) {
                        result = new SqlSubSelect(SqlNodeType.Multiset, link.ClrType, link.SqlType, sel); 
                    } 
                    else {
                        result = new SqlSubSelect(SqlNodeType.Element, link.ClrType, link.SqlType, sel); 
                    }
                }
                return result;
            } 
            else {
                System.Diagnostics.Debug.Assert(link.Expansion != null); 
                System.Diagnostics.Debug.Assert(link.KeyExpressions == keyExpressions); 
                // deferred expression already defined...
                return link.Expansion; 
            }
        }

        [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification="These issues are related to our use of if-then and case statements for node types, which adds to the complexity count however when reviewed they are easy to navigate and understand.")] 
        internal SqlExpression TranslateEquals(SqlBinary expr) {
            System.Diagnostics.Debug.Assert( 
                expr.NodeType == SqlNodeType.EQ || expr.NodeType == SqlNodeType.NE || 
                expr.NodeType == SqlNodeType.EQ2V || expr.NodeType == SqlNodeType.NE2V);
            SqlExpression eLeft = expr.Left; 
            SqlExpression eRight = expr.Right;

            if (eRight.NodeType == SqlNodeType.Element) {
                SqlSubSelect sub = (SqlSubSelect)eRight; 
                SqlAlias alias = new SqlAlias(sub.Select);
                SqlAliasRef aref = new SqlAliasRef(alias); 
                SqlSelect select = new SqlSelect(aref, alias, expr.SourceExpression); 
                select.Where = sql.Binary(expr.NodeType, sql.DoNotVisitExpression(eLeft), aref);
                return sql.SubSelect(SqlNodeType.Exists, select); 
            }
            else if (eLeft.NodeType == SqlNodeType.Element) {
                SqlSubSelect sub = (SqlSubSelect)eLeft;
                SqlAlias alias = new SqlAlias(sub.Select); 
                SqlAliasRef aref = new SqlAliasRef(alias);
                SqlSelect select = new SqlSelect(aref, alias, expr.SourceExpression); 
                select.Where = sql.Binary(expr.NodeType, sql.DoNotVisitExpression(eRight), aref); 
                return sql.SubSelect(SqlNodeType.Exists, select);
            } 

            MetaType mtLeft = TypeSource.GetSourceMetaType(eLeft, this.services.Model);
            MetaType mtRight = TypeSource.GetSourceMetaType(eRight, this.services.Model);
 
            if (eLeft.NodeType == SqlNodeType.TypeCase) {
                eLeft = BestIdentityNode((SqlTypeCase)eLeft); 
            } 
            if (eRight.NodeType == SqlNodeType.TypeCase) {
                eRight = BestIdentityNode((SqlTypeCase)eRight); 
            }

            if (mtLeft.IsEntity && mtRight.IsEntity && mtLeft.Table != mtRight.Table) {
                throw Error.CannotCompareItemsAssociatedWithDifferentTable(); 
            }
 
            // do simple or no translation for non-structural types 
            if (!mtLeft.IsEntity && !mtRight.IsEntity &&
                (eLeft.NodeType != SqlNodeType.New || eLeft.SqlType.CanBeColumn) && 
                (eRight.NodeType != SqlNodeType.New || eRight.SqlType.CanBeColumn)) {
                if (expr.NodeType == SqlNodeType.EQ2V || expr.NodeType == SqlNodeType.NE2V) {
                    return this.TranslateEqualsOp(expr.NodeType, sql.DoNotVisitExpression(expr.Left), sql.DoNotVisitExpression(expr.Right), false);
                } 
                return expr;
            } 
 
            // If the two types are not comparable, we return the predicate "1=0".
            if ((mtLeft != mtRight) && (mtLeft.InheritanceRoot != mtRight.InheritanceRoot)) { 
                return sql.Binary(SqlNodeType.EQ, sql.ValueFromObject(0,expr.SourceExpression), sql.ValueFromObject(1,expr.SourceExpression));
            }

            List exprs1; 
            List exprs2;
 
            SqlLink link1 = eLeft as SqlLink; 
            if (link1 != null && link1.Member.IsAssociation && link1.Member.Association.IsForeignKey) {
                exprs1 = link1.KeyExpressions; 
            }
            else {
                exprs1 = this.GetIdentityExpressions(mtLeft, sql.DoNotVisitExpression(eLeft));
            } 

            SqlLink link2 = eRight as SqlLink; 
            if (link2 != null && link2.Member.IsAssociation && link2.Member.Association.IsForeignKey) { 
                exprs2 = link2.KeyExpressions;
            } 
            else {
                exprs2 = this.GetIdentityExpressions(mtRight, sql.DoNotVisitExpression(eRight));
            }
 
            System.Diagnostics.Debug.Assert(exprs1.Count > 0);
            System.Diagnostics.Debug.Assert(exprs2.Count > 0); 
            System.Diagnostics.Debug.Assert(exprs1.Count == exprs2.Count); 

            SqlExpression exp = null; 
            SqlNodeType eqKind = (expr.NodeType == SqlNodeType.EQ2V || expr.NodeType == SqlNodeType.NE2V) ? SqlNodeType.EQ2V : SqlNodeType.EQ;
            for (int i = 0, n = exprs1.Count; i < n; i++) {
                SqlExpression eq = this.TranslateEqualsOp(eqKind, exprs1[i], exprs2[i], !mtLeft.IsEntity);
                if (exp == null) { 
                    exp = eq;
                } 
                else { 
                    exp = sql.Binary(SqlNodeType.And, exp, eq);
                } 
            }
            if (expr.NodeType == SqlNodeType.NE || expr.NodeType == SqlNodeType.NE2V) {
                exp = sql.Unary(SqlNodeType.Not, exp, exp.SourceExpression);
            } 
            return exp;
        } 
 
        private SqlExpression TranslateEqualsOp(SqlNodeType op, SqlExpression left, SqlExpression right, bool allowExpand) {
            switch (op) { 
                case SqlNodeType.EQ:
                case SqlNodeType.NE:
                    return sql.Binary(op, left, right);
                case SqlNodeType.EQ2V: 
                    if (SqlExpressionNullability.CanBeNull(left) != false &&
                        SqlExpressionNullability.CanBeNull(right) != false) { 
                        SqlNodeType eqOp = allowExpand ? SqlNodeType.EQ2V : SqlNodeType.EQ; 
                        return
                            sql.Binary(SqlNodeType.Or, 
                                sql.Binary(SqlNodeType.And,
                                    sql.Unary(SqlNodeType.IsNull, (SqlExpression)SqlDuplicator.Copy(left)),
                                    sql.Unary(SqlNodeType.IsNull, (SqlExpression)SqlDuplicator.Copy(right))
                                    ), 
                                sql.Binary(SqlNodeType.And,
                                    sql.Binary(SqlNodeType.And, 
                                        sql.Unary(SqlNodeType.IsNotNull, (SqlExpression)SqlDuplicator.Copy(left)), 
                                        sql.Unary(SqlNodeType.IsNotNull, (SqlExpression)SqlDuplicator.Copy(right))
                                        ), 
                                    sql.Binary(eqOp, left, right)
                                    )
                                );
                    } 
                    else {
                        SqlNodeType eqOp = allowExpand ? SqlNodeType.EQ2V : SqlNodeType.EQ; 
                        return sql.Binary(eqOp, left, right); 
                    }
                case SqlNodeType.NE2V: 
                    if (SqlExpressionNullability.CanBeNull(left) != false &&
                        SqlExpressionNullability.CanBeNull(right) != false) {
                        SqlNodeType eqOp = allowExpand ? SqlNodeType.EQ2V : SqlNodeType.EQ;
                        return 
                            sql.Unary(SqlNodeType.Not,
                                sql.Binary(SqlNodeType.Or, 
                                    sql.Binary(SqlNodeType.And, 
                                        sql.Unary(SqlNodeType.IsNull, (SqlExpression)SqlDuplicator.Copy(left)),
                                        sql.Unary(SqlNodeType.IsNull, (SqlExpression)SqlDuplicator.Copy(right)) 
                                        ),
                                    sql.Binary(SqlNodeType.And,
                                        sql.Binary(SqlNodeType.And,
                                            sql.Unary(SqlNodeType.IsNotNull, (SqlExpression)SqlDuplicator.Copy(left)), 
                                            sql.Unary(SqlNodeType.IsNotNull, (SqlExpression)SqlDuplicator.Copy(right))
                                            ), 
                                        sql.Binary(eqOp, left, right) 
                                        )
                                    ) 
                                );
                    }
                    else {
                        SqlNodeType neOp = allowExpand ? SqlNodeType.NE2V : SqlNodeType.NE; 
                        return sql.Binary(neOp, left, right);
                    } 
                default: 
                    throw Error.UnexpectedNode(op);
            } 
        }

        internal SqlExpression TranslateLinkEquals(SqlBinary bo) {
            SqlLink link1 = bo.Left as SqlLink; 
            SqlLink link2 = bo.Right as SqlLink;
            if ((link1 != null && link1.Member.IsAssociation && link1.Member.Association.IsForeignKey) || 
                (link2 != null && link2.Member.IsAssociation && link2.Member.Association.IsForeignKey)) { 
                return this.TranslateEquals(bo);
            } 
            return bo;
        }

        internal SqlExpression TranslateLinkIsNull(SqlUnary expr) { 
            System.Diagnostics.Debug.Assert(expr.NodeType == SqlNodeType.IsNull || expr.NodeType == SqlNodeType.IsNotNull);
 
            SqlLink link = expr.Operand as SqlLink; 
            if (!(link != null && link.Member.IsAssociation && link.Member.Association.IsForeignKey)) {
                return expr; 
            }

            List exprs = link.KeyExpressions;
            System.Diagnostics.Debug.Assert(exprs.Count > 0); 

            SqlExpression exp = null; 
            SqlNodeType combo = (expr.NodeType == SqlNodeType.IsNull) ? SqlNodeType.Or : SqlNodeType.And; 
            for (int i = 0, n = exprs.Count; i < n; i++) {
                SqlExpression compare = sql.Unary(expr.NodeType, sql.DoNotVisitExpression(exprs[i]), expr.SourceExpression); 
                if (exp == null) {
                    exp = compare;
                }
                else { 
                    exp = sql.Binary(combo, exp, compare);
                } 
            } 
            return exp;
        } 

        /// 
        /// Find the alternative in type case that will best identify the object.
        /// If there is a SqlNew it is expected to have all the identity fields. 
        /// If there is no SqlNew then we must be dealing with all literal NULL alternatives. In this case,
        /// just return the first one. 
        ///  
        private static SqlExpression BestIdentityNode(SqlTypeCase tc) {
            foreach (SqlTypeCaseWhen when in tc.Whens) { 
                if (when.TypeBinding.NodeType == SqlNodeType.New) {
                    return when.TypeBinding;
                }
            } 
            return tc.Whens[0].TypeBinding; // There were no SqlNews, take the first alternative
        } 
 
        private static bool IsPublic(MemberInfo mi) {
            FieldInfo fi = mi as FieldInfo; 
            if (fi != null) {
                return fi.IsPublic;
            }
            PropertyInfo pi = mi as PropertyInfo; 
            if (pi != null) {
                if (pi.CanRead) { 
                    var gm = pi.GetGetMethod(); 
                    if (gm != null) {
                        return gm.IsPublic; 
                    }
                }
            }
            return false; 
        }
 
        [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification="Unknown reason.")] 
        private IEnumerable GetIdentityMembers(MetaType type) {
            if (type.IsEntity) { 
                return type.IdentityMembers;
            }
            return type.DataMembers.Where(m => IsPublic(m.Member));
        } 

        private List GetIdentityExpressions(MetaType type, SqlExpression expr) { 
            List members = GetIdentityMembers(type).ToList(); 
            System.Diagnostics.Debug.Assert(members.Count > 0);
            List exprs = new List(members.Count); 
            foreach (MetaDataMember mm in members) {
                exprs.Add(sql.Member((SqlExpression)SqlDuplicator.Copy(expr), mm));
            }
            return exprs; 
        }
    } 
} 

// File provided for Reference Use Only by Microsoft Corporation (c) 2007.
// Copyright (c) Microsoft Corporation. All rights reserved.


                        

Link Menu

Network programming in C#, Network Programming in VB.NET, Network Programming in .NET
This book is available now!
Buy at Amazon US or
Buy at Amazon UK